// 7.1 Open loop vs closed loop on the same heater.
// Flip USE_FEEDBACK and watch what a disturbance does to each one.
// ESP32 core 3.x, no external libraries.
#define USE_FEEDBACK 1
const int PIN_SENSOR = 34; // ADC1_CH6, input only
const int PIN_HEATER = 25; // MOSFET gate through a series resistor
const int PWM_FREQ = 500; // Hz
const int PWM_BITS = 10; // 0..1023
const float T_AMB = 20.0; // ambient, degC
const float K_TH = 80.0; // full drive lifts it by this much
const float T_SET = 70.0; // wanted, degC
const float KP = 0.25; // only used with feedback
// LM35-style sensor: 10 mV per degC, 0 mV at 0 degC.
float readCelsius() {
return analogReadMilliVolts(PIN_SENSOR) / 10.0;
}
void drive(float u) {
if (u < 0) u = 0;
if (u > 1) u = 1;
ledcWrite(PIN_HEATER, (int)(u * ((1 << PWM_BITS) - 1)));
}
void setup() {
Serial.begin(115200);
analogReadResolution(12);
ledcAttach(PIN_HEATER, PWM_FREQ, PWM_BITS);
}
void loop() {
float y = readCelsius();
float u;
#if USE_FEEDBACK
float e = T_SET - y; // the error only exists when we measure
u = KP * e;
#else
u = (T_SET - T_AMB) / K_TH; // calibrated once, never corrected
#endif
drive(u);
Serial.printf("y=%.2f u=%.3f\n", y, u);
delay(200);
}
// 7.2 The six signals of the block diagram, one variable each.
// SENSOR_GAIN != 1.0 makes the measured signal drift away from the real one,
// and the controller keeps trusting the measured one.
const int PIN_SENSOR = 34;
const int PIN_ACT = 25;
const int PWM_FREQ = 500;
const int PWM_BITS = 10;
const float R_SET = 70.0; // r - wanted, never measured
const float KP = 0.25;
const float SENSOR_GAIN = 1.10; // a 10% sensor error, on purpose
void setup() {
Serial.begin(115200);
analogReadResolution(12);
ledcAttach(PIN_ACT, PWM_FREQ, PWM_BITS);
Serial.println("r\tm\te\tu\ty");
}
void loop() {
float y = analogReadMilliVolts(PIN_SENSOR) / 10.0; // y - what really happens
float m = y * SENSOR_GAIN; // m - what we measured
float e = R_SET - m; // e - error, from m not y
float u = KP * e; // u - controller output
if (u < 0) u = 0;
if (u > 1) u = 1;
ledcWrite(PIN_ACT, (int)(u * ((1 << PWM_BITS) - 1)));
// The controller can only ever be as right as its sensor.
float eTrue = R_SET - y;
Serial.printf("%.1f\t%.1f\t%.1f\t%.3f\t%.1f\t(true error %.1f)\n",
R_SET, m, e, u, y, eTrue);
delay(500);
}
// 7.3 Sensor -> ADC -> processor -> driver, with the resolution written out.
// The ESP32 ADC is not linear, so analogReadMilliVolts() is used: it applies
// the calibration burned into the chip at the factory. analogRead() alone
// would give raw counts that do not map to volts by a clean ratio.
const int PIN_SENSOR = 34;
const int PIN_RELAY = 26; // opto-isolated relay module, active HIGH
const int ADC_BITS = 12; // the ESP32 SAR ADC
// Nominal full scale, used below only to show how LSB is worked out. The real
// volts-per-count of a given chip comes from its own calibration, which is
// what analogReadMilliVolts() reads out - do not treat this as a spec.
const float V_REF_MV = 3100.0;
const float S_MV_C = 10.0; // sensor sensitivity, mV per degC
const float T_TRIP = 60.0; // switch the load above this
void setup() {
Serial.begin(115200);
analogReadResolution(ADC_BITS);
analogSetPinAttenuation(PIN_SENSOR, ADC_11db);
pinMode(PIN_RELAY, OUTPUT);
long levels = 1L << ADC_BITS;
float lsb_mv = V_REF_MV / levels;
Serial.printf("levels=%ld LSB=%.4f mV one count = %.4f degC\n",
levels, lsb_mv, lsb_mv / S_MV_C);
}
void loop() {
int raw = analogRead(PIN_SENSOR); // what the register holds
float mv = analogReadMilliVolts(PIN_SENSOR); // the same reading, calibrated
float t = mv / S_MV_C;
digitalWrite(PIN_RELAY, t > T_TRIP ? HIGH : LOW);
Serial.printf("N=%4d V=%.0f mV t=%.2f degC relay=%d\n",
raw, mv, t, t > T_TRIP ? 1 : 0);
delay(300);
}
// 7.4 Two-state control with a hysteresis band.
// Set GAP to 0 and the relay chatters at the sampling rate - which is exactly
// the failure the band exists to prevent. The sketch measures its own period
// so the number on the screen can be compared with the formula.
const int PIN_SENSOR = 34;
const int PIN_HEATER = 26;
const float T_SET = 70.0;
const float GAP = 2.0; // the whole band, degC
const float T_LO = T_SET - GAP / 2.0; // turn on below this
const float T_HI = T_SET + GAP / 2.0; // turn off above this
bool heaterOn = false;
unsigned long lastEdge = 0;
unsigned long tOn = 0, tOff = 0;
void setup() {
Serial.begin(115200);
analogReadResolution(12);
pinMode(PIN_HEATER, OUTPUT);
lastEdge = millis();
}
void loop() {
float t = analogReadMilliVolts(PIN_SENSOR) / 10.0;
bool wanted = heaterOn;
if (heaterOn && t >= T_HI) wanted = false;
else if (!heaterOn && t <= T_LO) wanted = true;
if (wanted != heaterOn) {
unsigned long now = millis();
if (heaterOn) tOn = now - lastEdge; // the on stretch just ended
else tOff = now - lastEdge;
lastEdge = now;
heaterOn = wanted;
digitalWrite(PIN_HEATER, heaterOn ? HIGH : LOW);
if (tOn > 0 && tOff > 0) {
unsigned long period = tOn + tOff;
// Duty settles on (T_SET - ambient) / K - the same value a continuous
// controller would have asked for. The band only sets the frequency.
Serial.printf("t_on=%lu ms t_off=%lu ms T=%lu ms D=%.3f\n",
tOn, tOff, period, (float)tOn / period);
}
}
delay(20); // this is the sampling rate, and with GAP=0 it sets the chatter
}
// 7.5 PWM as a continuous actuator, driven by a proportional controller.
// ledcAttach() takes the frequency and the resolution together; the duty
// written later is a plain integer in 0..(2^bits - 1).
const int PIN_SENSOR = 34;
const int PIN_HEATER = 25;
const int PWM_FREQ = 500; // Hz -> 2.00 ms period
const int PWM_BITS = 10; // 1024 steps
const int PWM_MAX = (1 << PWM_BITS) - 1;
const float VCC = 12.0; // supply the load actually sees
const float T_SET = 70.0;
const float KP = 0.10;
void setup() {
Serial.begin(115200);
analogReadResolution(12);
if (!ledcAttach(PIN_HEATER, PWM_FREQ, PWM_BITS)) {
Serial.println("ledcAttach failed - no free timer");
}
}
void loop() {
float t = analogReadMilliVolts(PIN_SENSOR) / 10.0;
float e = T_SET - t;
float u = KP * e;
if (u < 0) u = 0;
if (u > 1) u = 1;
ledcWrite(PIN_HEATER, (int)(u * PWM_MAX));
float period_ms = 1000.0 / PWM_FREQ;
// Pure proportional control never closes the gap: if the error reached zero
// the output would too, and the heating would stop.
Serial.printf("t=%.2f e=%.2f D=%.3f t_on=%.3f ms Vavg=%.2f V\n",
t, e, u, period_ms * u, u * VCC);
delay(200);
}
// 7.6 Motor speed from an encoder, counted in a fixed window.
// The ISR lives in IRAM and touches one volatile counter. The counter is read
// and cleared inside a critical section so a pulse arriving mid-read cannot be
// counted twice or lost.
const int PIN_ENC = 27;
const int PIN_PWM = 25;
const int PWM_FREQ = 20000; // above hearing, so the motor stays quiet
const int PWM_BITS = 10;
const int PWM_MAX = (1 << PWM_BITS) - 1;
const int SLOTS = 20; // pulses per revolution
const unsigned long WINDOW_MS = 100;
const float RPM_SET = 1725.0;
const float KP = 0.0004; // output per RPM of error
volatile unsigned long pulses = 0;
portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED;
void IRAM_ATTR onPulse() {
portENTER_CRITICAL_ISR(&mux);
pulses++;
portEXIT_CRITICAL_ISR(&mux);
}
unsigned long windowStart = 0;
float duty = 0.5;
void setup() {
Serial.begin(115200);
pinMode(PIN_ENC, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(PIN_ENC), onPulse, RISING);
ledcAttach(PIN_PWM, PWM_FREQ, PWM_BITS);
ledcWrite(PIN_PWM, (int)(duty * PWM_MAX));
windowStart = millis();
}
void loop() {
if (millis() - windowStart < WINDOW_MS) return;
windowStart += WINDOW_MS;
portENTER_CRITICAL(&mux);
unsigned long n = pulses;
pulses = 0;
portEXIT_CRITICAL(&mux);
// One count is worth this many RPM, and nothing finer can be seen.
float step = 60000.0 / (SLOTS * (float)WINDOW_MS);
float rpm = n * step;
float e = RPM_SET - rpm;
duty += KP * e;
if (duty < 0) duty = 0;
if (duty > 1) duty = 1;
ledcWrite(PIN_PWM, (int)(duty * PWM_MAX));
Serial.printf("N=%lu rpm=%.0f step=%.1f e=%.0f D=%.3f\n",
n, rpm, step, e, duty);
}
// 7.7 Servo and stepper, side by side.
// The servo is driven straight from LEDC at 50 Hz - no library needed, and the
// arithmetic on screen is the arithmetic in the sketch. The stepper is open
// loop: the sketch counts what it sent, not what the shaft did.
const int PIN_SERVO = 18;
const int PIN_STEP = 19;
const int PIN_DIR = 21;
const int SERVO_FREQ = 50; // 20 ms period
const int SERVO_BITS = 16; // 65536 steps over 20 ms -> 0.305 us
const float PULSE_MIN = 1.0; // ms, one end of the travel
const float PULSE_MAX = 2.0; // ms, the other end
const float SPAN_DEG = 180.0;
const float STEP_DEG = 1.8; // motor plate value
const int STEPS_REV = (int)(360.0 / STEP_DEG); // 200
long stepsSent = 0;
void servoDegrees(float deg) {
if (deg < 0) deg = 0;
if (deg > SPAN_DEG) deg = SPAN_DEG;
float ms = PULSE_MIN + (PULSE_MAX - PULSE_MIN) * deg / SPAN_DEG;
float periodMs = 1000.0 / SERVO_FREQ;
int duty = (int)(ms / periodMs * ((1L << SERVO_BITS) - 1));
ledcWrite(PIN_SERVO, duty);
}
void stepperMove(long steps, bool forward) {
digitalWrite(PIN_DIR, forward ? HIGH : LOW);
for (long i = 0; i < steps; i++) {
digitalWrite(PIN_STEP, HIGH);
delayMicroseconds(800);
digitalWrite(PIN_STEP, LOW);
delayMicroseconds(800);
}
// Counted, not measured. A skipped step is invisible here forever.
stepsSent += forward ? steps : -steps;
}
void setup() {
Serial.begin(115200);
ledcAttach(PIN_SERVO, SERVO_FREQ, SERVO_BITS);
pinMode(PIN_STEP, OUTPUT);
pinMode(PIN_DIR, OUTPUT);
Serial.printf("%d steps/rev, %.1f deg per step\n", STEPS_REV, STEP_DEG);
}
void loop() {
for (float deg = 0; deg <= SPAN_DEG; deg += 45) {
servoDegrees(deg);
float ms = PULSE_MIN + (PULSE_MAX - PULSE_MIN) * deg / SPAN_DEG;
Serial.printf("servo %.0f deg -> %.3f ms\n", deg, ms);
delay(500);
}
stepperMove(50, true); // 50 * 1.8 = 90 degrees, if nothing was skipped
Serial.printf("stepper believes it is at %.1f deg\n", stepsSent * STEP_DEG);
delay(1000);
}