עקרונות במערכות בקרה ממוחשבת

✦ בנוי על ידי פהד גאנם ✦

dTdt = K·u − (T − Ta)·(1+d)τ   |   e = Tset − T   |   uopen = Tset − TaK   |   uclosed = Kp·e
70 °C
0.0
0.25

u = (70 − 20) / 80 = 0.625
T = 20 + 80 · 0.625 / (1 + 1) = 20 + 25 = 45.00 °C  →  e = 70 − 45 = 25.00 °C
T = (Ta·(1+d) + K·Kp·Tset) / (1 + d + K·Kp) = (20·2 + 80·0.25·70) / (2 + 20)
T = 1240 / 22 = 65.45 °C  →  e = 70 − 65.45 = 4.55 °C
ESP32 · Arduino C++
// 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);
}

r  →  e = r − m  →  u = f(e)  →  y  →  m = ks·y   |   d  ⇢  y
70
52
0 %

m = 120 · 1.10 = 132 cm
ectrl = 150 − 132 = 18 cm   |   etrue = 150 − 120 = 30 cm
m = 150 ⟹ y = 150 / 1.10 = 136.36 cm
150 − 136.36 = 13.64 cm
ESP32 · Arduino C++
// 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);
}

Vs = S · t   |   LSB = Vref2n   |   N = floor( VsVref · 2n )   |   Δt = LSBS
40 °C
10
5.00 V
10 mV/°C

LSB = 5 / 210 = 5 / 1024 = 4.8828 mV
Δt = 4.8828 mV / 10 mV/°C = 0.488 °C
Vs = 0.600 V  →  0.600 / 5 · 1024 = 122.88  →  N = 122
122 · 0.48828 = 59.57 °C  →  60 − 59.57 = 0.43 °C
ESP32 · Arduino C++
// 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);
}

ton = τ·ln T − TloT − Thi   |   toff = τ·ln Thi − TaTlo − Ta   |   T = Ta + K   |   D = tonton + toff
70 °C
2.0 °C
60 s

T = 20 + 80 = 100 °C   |   Ta = 20 °C
ton = 60 · ln(31 / 29) = 60 · 0.06669 = 4.00 s
toff = 60 · ln(51 / 49) = 60 · 0.04001 = 2.40 s
T = 4.00 + 2.40 = 6.40 s  →  f = 0.156 Hz  →  D = 4.00 / 6.40 = 0.625
D · K = Tset − Ta  →  D = (70 − 20) / 80 = 0.625 ✓
ESP32 · Arduino C++
// 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
}

D = tonT   |   Vavg = D · Vcc   |   T = 1f   |   ess = Tset − Ta1 + K·Kp
0.625
500 Hz
12.0 V
0.10

T = 1 / 500 = 0.002 s = 2.00 ms
ton = 2.00 · 0.625 = 1.25 ms   |   toff = 2.00 − 1.25 = 0.75 ms
Vavg = 0.625 · 12 = 7.50 V
e = (70 − 20) / (1 + 80 · 0.10) = 50 / 9 = 5.556 °C  →  T = 64.44 °C
ESP32 · Arduino C++
// 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);
}

N = floor( nrpm60 · P · tw )   |   nread = N · 60P · tw   |   Δn = 60P · tw
1725 RPM
20
100 ms

1725 / 60 · 20 · 0.100 = 28.75 · 20 · 0.1 = 57.50
N = floor(57.50) = 57
Δn = 60 / (20 · 0.1) = 30 RPM  →  nread = 57 · 30 = 1710 RPM
143.75 → N = 143, Δn = 12 → 1716 RPM  →  שגיאה 9 RPM
ESP32 · Arduino C++
// 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);
}

θservo = (tp − 1.0) · 180   |   T = 20 ms   |   θstep = 360S   |   θ = k · θstep   |   nrpm = 60 · fpS
1.50 ms
1.8°
50
0

tp = 1.0 + 90 / 180 = 1.50 ms
D = 1.50 / 20 = 0.075 = 7.5 %
180 / 1000 = 0.180 °/µs
360 / 1.8 = 200 steps/rev  |  90 / 1.8 = 50 steps
47 · 1.8 = 84.6°  →  שגיאה = 90 − 84.6 = 5.4°
ESP32 · Arduino C++
// 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);
}
🏠