Embedded Real-Time Control Implementation

Turning control algorithms into reliable firmware. Learn controller hardware (MCU, DSP, FPGA, PLC, industrial PC), timer-driven control loops and ISRs, timing budgets and jitter, quadrature encoder interfaces, ADC sampling synchronised with PWM, fixed-point vs floating-point arithmetic (Q-format, saturation), discretisation, anti-windup in code, RTOS task design, safety states and watchdogs, and testing (MIL/SIL/HIL) — with Python for encoder decoding and a Q15 PI step.

A controller that works in simulation can fail on hardware because of timing jitter, quantisation, overflow, sensor noise or a missed interrupt. Embedded control engineering is about implementing algorithms that run deterministically, fast enough, with correct numerics and safe behaviour on faults.

Modern motor-control MCUs (STM32G4, TI C2000, NXP i.MX RT) include PWM timers, synchronised ADCs, encoder interfaces and math accelerators precisely for this purpose.

An analogy

An embedded control loop is like a drummer keeping time for a band: it must hit every beat exactly on time (deterministic sampling); an early or late beat (jitter) throws everyone off, however good the notes are.

Controller hardware

Choose by speed, I/O and ecosystem:

Timing and determinism

Control loops must run at a fixed rate triggered by hardware timers, not busy-wait loops. Measure WCET, keep CPU load below ~60–70% in the fastest ISR, prioritise interrupts (current > velocity > communication), and avoid blocking calls, dynamic memory and unbounded loops in real-time code. Synchronise ADC sampling to the PWM centre to avoid switching noise.

Numerics

Fixed-point (Q15/Q31) is fast on small MCUs but needs careful scaling, saturation and overflow handling; floating-point simplifies code on MCUs with FPUs. Discretise with backward Euler or Tustin; pre-warp for filters near Nyquist; implement anti-windup and bumpless transfer; watch quantisation of small integrator increments and encoder-derived velocity noise at low speed (use M/T methods or observers).

Architecture, safety and testing

Layered firmware: HAL/drivers → control tasks → state machine (init, ready, running, fault) → communication. RTOS task design with rate-monotonic priorities. Safety: watchdogs, plausibility checks, safe torque off (STO), fault reactions, and standards (IEC 61508, ISO 13849, ISO 26262). Verification: model-in-the-loop, auto-generated code (Embedded Coder), software-in-the-loop, processor-in-the-loop and hardware-in-the-loop test benches, plus unit tests and static analysis (MISRA C).

Formulas

Worked example: Cycle budget

Given: 170 MHz MCU, 20 kHz loop

Find: Cycles per period.

Answer: 8500 cycles (≈ 5100 at 60% load)

Worked example: Q15 conversion

Given: Gain 0.5

Find: Q15 integer.

Answer: 16384

Worked example: Low-speed encoder resolution

Given: 4096 CPR, 1 kHz sampling

Find: Speed resolution (M-method).

Answer: 1.53 rad/s (≈ 14.6 rpm) — too coarse at low speed; use T-method or observer

Quadrature decoding and a Q15 PI controller

# Quadrature decoding state machine and a fixed-point (Q15) PI controller step
table = {(0,1):+1,(1,3):+1,(3,2):+1,(2,0):+1,(1,0):-1,(3,1):-1,(2,3):-1,(0,2):-1}
seq = [0,1,3,2,0,1,3,2,0,2,3,1,0]      # A/B states: 8 steps forward, then 4 back
count, prev = 0, seq[0]
for s in seq[1:]:
    count += table.get((prev, s), 0); prev = s
print(f"encoder count = {count} (8 forward - 4 back)")
# Q15 PI: u = Kp*e + Ki*sum(e)
Q = 1 << 15
Kp, Ki = int(0.5 * Q), int(0.01 * Q)
e_list = [8000, 6000, 3000, 1000]
acc = 0
for e in e_list:
    acc += e
    u = (Kp * e + Ki * acc) >> 15
    u = max(-32768, min(32767, u))
    print(f"e={e:5d}  integral={acc:6d}  u={u}")
# ISR timing budget: 20 kHz current loop on a 170 MHz MCU
cycles = 170e6 / 20e3
print(f"cycles available per 20 kHz ISR: {cycles:.0f}; at 60% budget -> {int(cycles*0.6)} cycles")
# encoder count = 4 (8 forward - 4 back)
# e= 8000  integral=  8000  u=4079
# e= 6000  integral= 14000  u=3139
# e= 3000  integral= 17000  u=1669
# e= 1000  integral= 18000  u=679
# cycles available per 20 kHz ISR: 8500; at 60% budget -> 5100 cycles

The state table counts ±1 for each valid transition of the A/B Gray code (hardware encoder peripherals do this for you). The Q15 PI multiplies gains (scaled by 2¹⁵) by integer errors, shifts back, and saturates — exactly what runs on MCUs without an FPU. The integrator should also be clamped (anti-windup) in real firmware.

Timer-triggered control ISR in C (illustrative)

// 20 kHz current loop triggered by the PWM timer; ADC sampled mid-PWM (low ripple)
volatile float i_ref;                 // set by the 2 kHz velocity loop
static float integ;

void ADC_IRQHandler(void) {           // fires when the synchronised ADC conversion completes
    float i_meas = adc_to_amps(ADC1->DR);
    float e = i_ref - i_meas;
    float u = KP * e + integ;
    if (u > V_MAX)       u = V_MAX;   // saturate output...
    else if (u < -V_MAX) u = -V_MAX;
    else                 integ += KI * TS * e;   // ...and integrate only when not saturated
    pwm_set_duty(u / V_BUS);          // update compare register for next PWM period
    if (i_meas > I_TRIP) pwm_disable_all();       // fast overcurrent protection
}

Illustrative C in the style of a Cortex-M motor controller (register and helper names are placeholders). The loop is triggered by hardware so sampling is jitter-free; slower loops run in lower-priority interrupts or RTOS tasks.

Common mistakes

Applications

Key takeaways

Going further