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:
- Microcontrollers (Cortex-M, C2000, AVR) — low cost, real-time peripherals
- DSPs and MCUs with FPUs/math accelerators — fast floating-point control
- FPGAs — nanosecond-deterministic loops, custom interfaces (high-end drives)
- PLCs / motion controllers — industrial I/O, IEC 61131-3, safety certification
- Industrial PCs / embedded Linux with PREEMPT_RT — complex robots, vision, ROS 2
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
- Cycle budget:
cycles = f_CPU / f_loop - Q-format:
value = integer / 2ⁿ; product (Qm × Qn) = Q(m+n) → shift right - Discrete PI (backward Euler):
I_k = I_{k−1} + K_i·T_s·e_k; u_k = K_p·e_k + I_k - Speed from encoder (M-method):
ω = 2π·Δcounts / (CPR·T_s) - Latency-induced phase lag:
φ = 360°·f·(T_s/2 + t_compute)
Worked example: Cycle budget
Given: 170 MHz MCU, 20 kHz loop
Find: Cycles per period.
- 170e6/20e3
Answer: 8500 cycles (≈ 5100 at 60% load)
Worked example: Q15 conversion
Given: Gain 0.5
Find: Q15 integer.
- 0.5 × 32768
Answer: 16384
Worked example: Low-speed encoder resolution
Given: 4096 CPR, 1 kHz sampling
Find: Speed resolution (M-method).
- 1 count per 1 ms → 2π/4096/0.001
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 cyclesThe 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
- Wrong: Running control in the main loop with delays. — Right: Timer-triggered ISRs or RTOS periodic tasks.
- Wrong: Fixed-point without saturation. — Right: Saturate after every multiply-accumulate; check overflow.
- Wrong: No fault state or watchdog. — Right: Design fault reactions and test them.
Applications
- Servo drives: Current/velocity/position loops on MCUs and FPGAs.
- Drones: 400 Hz+ attitude loops on flight controllers.
- E-mobility: Inverter firmware with FOC and safety.
- Robots: EtherCAT joint controllers and ROS 2 high-level control.
Key takeaways
- Hardware-timed, deterministic loops.
- Budget cycles; keep ISR load < ~70%.
- Fixed-point needs scaling and saturation.
- State machines, watchdogs and HIL for safety.
Going further
- Rate-monotonic analysis: Schedulability tests for periodic tasks.
- Auto-code generation: Simulink Embedded Coder, certified generators.
- FPGA control: Sub-microsecond current loops and custom PWM.