>
section 8 of 123 min read

8. Discrete-Time Control: When the Controller Lives in a Microcontroller

Most modern controllers live inside microcontrollers, sampling the plant at a fixed rate. The math becomes discrete: difference equations, Z-transforms, the unit circle as the stability boundary.

8.1 Why discrete

A microcontroller takes ADC samples at a fixed rate (commonly 1 kHz to 100 kHz for industrial controllers, up to MHz for motor and power electronics), runs the controller code, then writes to a DAC or PWM. The plant, of course, lives in continuous time. So we have a hybrid system: continuous plant, discrete controller, periodic sampling.

To analyze it, discretize the plant. Common methods:

  • Forward Euler: x˙(x[k+1]x[k])/T\dot x \approx (x[k+1] - x[k]) / T. Simple but unstable for fast plants if TT is too large.
  • Backward Euler: x˙(x[k]x[k1])/T\dot x \approx (x[k] - x[k-1]) / T. Always stable.
  • Tustin (bilinear): s2T1z11+z1s \mapsto \frac{2}{T}\frac{1-z^{-1}}{1+z^{-1}}. Maps the LHP to the inside of the unit circle. Best for control design.
  • Zero-order hold: exact discretization assuming the input is held constant between samples. The most accurate.

After discretization, your continuous PID becomes a difference equation:

u[k]=Kpe[k]+KiTj=0ke[j]+KdT(e[k]e[k1])u[k] = K_p e[k] + K_i T \sum_{j=0}^{k} e[j] + \frac{K_d}{T} (e[k] - e[k-1])

This is the form you implement in C or Python on a microcontroller.

8.2 Sampling rate and stability

Sampling fast enough is crucial. A rule of thumb: sample at least 10x to 30x the closed-loop bandwidth. Sample too slowly and:

  • Phase margin erodes (each sample introduces approximately ωT/2\omega T / 2 of phase lag at frequency ω\omega).
  • Aliasing folds high-frequency noise into the control band.
  • The discrete approximation deviates from the continuous design.

Sample too fast and you waste CPU cycles, but otherwise it is harmless.

8.3 Z-domain stability

Discrete LTI systems are stable iff all poles of the discrete transfer function H(z)H(z) are inside the unit circle. The mapping z=esTz = e^{sT} takes the LHP to the inside of the unit circle, the imaginary axis to the unit circle, and the RHP to outside. A continuous design that you Tustin-discretize automatically preserves stability if done carefully.

8.4 Practical implementation

A discrete PID inner loop that lives inside a 3D printer firmware:

c
// Run every 100 ms (10 Hz) for the hotend
float pid_compute(float setpoint, float measured) {
    float error = setpoint - measured;
    integral += error * dt;
    // Anti-windup
    if (integral > integral_max) integral = integral_max;
    if (integral < integral_min) integral = integral_min;
    float derivative = (measured - prev_measured) / dt;
    prev_measured = measured;
    float u = Kp * error + Ki * integral - Kd * derivative;
    if (u < 0) u = 0;
    if (u > 255) u = 255;
    return u;
}

This PID-controls the heater PWM duty (0-255) based on the temperature error. Inside Marlin, Klipper, and every other printer firmware, code that looks essentially like this controls every hotend, every heated bed, every chamber heater on every 3D printer in the world.