Digital Control

Digital control implements continuous control theory on a discrete-time system — a microcontroller that samples sensors and updates actuators at a fixed rate. The continuous equations must be discretized, and sampling rate must be chosen carefully to preserve stability.

Why It Matters

Every real controller runs on a microcontroller with a fixed sample rate. The continuous PID equation becomes a difference equation. The Laplace domain becomes the z-domain. Understanding discretization, aliasing, and the z-transform is the bridge between control theory and embedded firmware.

Continuous to Discrete

A continuous-time controller G(s) must be converted to a discrete-time equivalent G(z) that runs at sample period T.

Zero-Order Hold (ZOH) Method

The most common method. Assumes the control output is held constant between samples (which is what a microcontroller actually does):

              sample          compute         hold
Sensor ──→ ADC ──→ Controller ──→ DAC ──→ Actuator
         (every T seconds)
from scipy.signal import cont2discrete
 
# Continuous: G(s) = 1/(s+2)
A_c = [[-2]]
B_c = [[1]]
C_c = [[1]]
D_c = [[0]]
T = 0.01  # 100 Hz sample rate
 
A_d, B_d, C_d, D_d, _ = cont2discrete((A_c, B_c, C_c, D_c), T, method='zoh')

Z-Transform Basics

The z-transform is to discrete-time what Laplace is to continuous-time. z represents a one-sample delay: z⁻¹ · X(z) = x[k-1].

Continuous (s)Discrete (z)Relation
s (derivative)(z-1)/(T·z) (forward difference)Approximate
1/s (integral)T·z/(z-1) (forward Euler)Approximate
e^(-aT)z⁻¹ when a = -ln(z)/TExact
Stability: Re(s) < 0Stability: |z| < 1Unit circle

Tustin (Bilinear) Transform

Better than forward/backward Euler — preserves frequency response:

s = (2/T) · (z - 1) / (z + 1)

Maps the left-half s-plane exactly onto the unit circle interior. No aliasing of stable poles to unstable ones.

Discrete PID (What Runs on MCUs)

typedef struct {
    float kp, ki, kd;
    float integral;
    float prev_error;
    float prev_measurement;  // for derivative-on-measurement
    float dt;
    float out_min, out_max;
} PID;
 
float pid_update(PID *pid, float setpoint, float measurement) {
    float error = setpoint - measurement;
 
    // Proportional
    float P = pid->kp * error;
 
    // Integral with anti-windup
    pid->integral += pid->ki * error * pid->dt;
 
    // Derivative on measurement (avoids derivative kick on setpoint change)
    float D = -pid->kd * (measurement - pid->prev_measurement) / pid->dt;
 
    float output = P + pid->integral + D;
 
    // Clamp output and back-calculate integral (anti-windup)
    if (output > pid->out_max) {
        pid->integral -= (output - pid->out_max);
        output = pid->out_max;
    } else if (output < pid->out_min) {
        pid->integral -= (output - pid->out_min);
        output = pid->out_min;
    }
 
    pid->prev_error = error;
    pid->prev_measurement = measurement;
    return output;
}

Sampling Rate Selection

The sample rate must be fast enough to capture system dynamics and leave time for computation.

GuidelineRule
Minimum (Shannon)Fs > 2 × system bandwidth
PracticalFs = 10-20 × closed-loop bandwidth
Too fastDerivative noise amplified, integer overflow in integral, wasted CPU
Too slowPhase lag, reduced stability margins, missed dynamics
ApplicationTypical Rate
Temperature control1-10 Hz
Motor position1-10 kHz
Drone attitude250-1000 Hz
Power electronics10-100 kHz

Aliasing in Control

Sampling at rate Fs cannot distinguish signals above Fs/2. A 100 Hz disturbance sampled at 80 Hz appears as a 20 Hz signal — the controller may amplify it. Anti-aliasing filters before the ADC are essential.

Discrete State Space

The continuous state equations discretize to:

x[k+1] = Ad · x[k] + Bd · u[k]
y[k]   = Cd · x[k] + Dd · u[k]

Where Ad = e^(A·T) and Bd = A⁻¹(Ad - I)B (ZOH discretization).

import numpy as np
from scipy.linalg import expm
 
T = 0.01
Ad = expm(A_c * T)                              # matrix exponential
Bd = np.linalg.solve(A_c, (Ad - np.eye(2))) @ B_c  # exact ZOH

Stability in discrete state space: all eigenvalues of Ad must be inside the unit circle (|λ| < 1).