PWM Generation
PWM Fundamentals
Pulse Width Modulation (PWM) is the fundamental technique for controlling actuator power. By rapidly switching a signal on and off at a fixed frequency, the duty cycle (ratio of on-time to total period) controls the average voltage delivered to a load.
- DC Motors: 20–25 kHz (above audible range to eliminate whine)
- Servo Motors: 50 Hz (20 ms period, standard for hobby servos)
- LED Dimming: 1–10 kHz (above flicker perception)
- Stepper Drivers: 20–50 kHz (microstepping current control)
- Power Supplies: 100 kHz–2 MHz (switching regulators)
STM32 Timer PWM
// STM32F4 Timer PWM — dual-channel motor control
#include "stm32f4xx.h"
// TIM1 CH1 (PA8) and CH2 (PA9) — 20kHz PWM for dual motor
void pwm_dual_init(void) {
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
RCC->APB2ENR |= RCC_APB2ENR_TIM1EN;
// PA8, PA9 → AF1 (TIM1_CH1, TIM1_CH2)
GPIOA->MODER |= (2U << 16) | (2U << 18);
GPIOA->AFR[1] |= (1U << 0) | (1U << 4);
// 168 MHz / (168 * 500) = 2 kHz? No — for 20 kHz:
// 168 MHz / (8 * 1050) = 20 kHz
TIM1->PSC = 8 - 1; // 21 MHz timer clock
TIM1->ARR = 1050 - 1; // 20 kHz period
TIM1->CCR1 = 0; // CH1 duty = 0%
TIM1->CCR2 = 0; // CH2 duty = 0%
// PWM Mode 1 on both channels
TIM1->CCMR1 = (6U << 4) | TIM_CCMR1_OC1PE |
(6U << 12) | TIM_CCMR1_OC2PE;
TIM1->CCER = TIM_CCER_CC1E | TIM_CCER_CC2E;
TIM1->BDTR = TIM_BDTR_MOE; // Main output enable (advanced timer)
TIM1->CR1 |= TIM_CR1_CEN;
}
void pwm_set_duty(uint8_t channel, float duty) {
if (duty < 0.0f) duty = 0.0f;
if (duty > 100.0f) duty = 100.0f;
uint32_t ccr = (uint32_t)(duty * (TIM1->ARR + 1) / 100.0f);
if (channel == 1) TIM1->CCR1 = ccr;
else if (channel == 2) TIM1->CCR2 = ccr;
}
Advanced PWM Modes
Advanced Timer PWM Modes
| Mode | Description | Application |
|---|---|---|
| Center-Aligned | Counter counts up then down; symmetric pulses reduce EMI | Motor drives, inverters |
| Complementary | CH1 and CH1N outputs, with dead-time insertion | Half/Full bridge, BLDC |
| One-Pulse | Counter stops after one period | Stepper single-step |
| Encoder Mode | Timer counts encoder pulses (CH1/CH2 as inputs) | Position/speed feedback |
H-Bridge Circuits
How H-Bridges Work
An H-Bridge is a circuit with four switches (transistors) arranged in an H pattern around a motor. By activating diagonal pairs, current flows through the motor in either direction, enabling bidirectional speed control.
- Forward: Q1 + Q4 ON (high-side left + low-side right) — current flows left to right
- Reverse: Q2 + Q3 ON (high-side right + low-side left) — current flows right to left
- Brake (slow decay): Q3 + Q4 ON (both low-side) — motor terminals shorted, electromagnetic braking
- Coast (fast decay): All OFF — motor freewheels to a stop
- FORBIDDEN: Q1 + Q3 or Q2 + Q4 simultaneously — shoot-through short circuit!
flowchart LR
subgraph Forward
F1["Q1 ON"] --- F2["Q4 ON"]
end
subgraph Reverse
R1["Q2 ON"] --- R2["Q3 ON"]
end
subgraph Brake
B1["Q3 ON"] --- B2["Q4 ON"]
end
subgraph Coast
C1["All OFF"]
end
HB["H-Bridge
Controller"] --> Forward
HB --> Reverse
HB --> Brake
HB --> Coast
Forward -->|"Current →"| MF["Motor
Forward"]
Reverse -->|"Current ←"| MR["Motor
Reverse"]
Brake -->|"Shorted"| MS["Motor
Stops Quick"]
Coast -->|"Freewheel"| MC["Motor
Coasts"]
style HB fill:#3B9797,stroke:#3B9797,color:#fff
style MF fill:#e8f4f4,stroke:#3B9797,color:#132440
style MR fill:#f0f4f8,stroke:#16476A,color:#132440
style MS fill:#fff5f5,stroke:#BF092F,color:#132440
style MC fill:#e8f4f4,stroke:#3B9797,color:#132440
Popular Driver ICs
Motor Driver IC Comparison
| Driver | Channels | Current | Voltage | Features |
|---|---|---|---|---|
| L298N | Dual H-Bridge | 2A / 3A peak | 5–46V | Classic, high dropout (2V), needs heatsink |
| TB6612FNG | Dual H-Bridge | 1.2A / 3.2A peak | 2.5–13.5V | MOSFET, low Rds(on), efficient |
| DRV8833 | Dual H-Bridge | 1.5A / 2A peak | 2.7–10.8V | Low voltage, built-in current limit |
| DRV8871 | Single H-Bridge | 3.6A | 6.5–45V | Simple 2-pin interface, current sense |
| BTS7960 | Single H-Bridge | 43A continuous | 5.5–27V | High current, for large motors |
// TB6612FNG dual motor driver control
#include "stm32f4xx.h"
typedef struct {
GPIO_TypeDef *in1_port;
uint16_t in1_pin;
GPIO_TypeDef *in2_port;
uint16_t in2_pin;
volatile uint32_t *pwm_ccr;
} Motor_t;
Motor_t motorA = { GPIOB, 0, GPIOB, 1, &TIM3->CCR1 };
Motor_t motorB = { GPIOB, 2, GPIOB, 3, &TIM3->CCR2 };
void motor_drive(Motor_t *m, int16_t speed) {
// speed: -100 to +100
if (speed > 0) {
m->in1_port->BSRR = (1U << m->in1_pin);
m->in2_port->BSRR = (1U << (m->in2_pin + 16));
} else if (speed < 0) {
m->in1_port->BSRR = (1U << (m->in1_pin + 16));
m->in2_port->BSRR = (1U << m->in2_pin);
speed = -speed;
} else {
// Brake: both LOW
m->in1_port->BSRR = (1U << (m->in1_pin + 16));
m->in2_port->BSRR = (1U << (m->in2_pin + 16));
}
// Set PWM duty (0-100 → 0-ARR)
*m->pwm_ccr = (uint32_t)speed * (TIM3->ARR + 1) / 100;
}
PID Control Theory
The PID Equation
The PID (Proportional-Integral-Derivative) controller is the most widely used feedback control algorithm. It continuously calculates the error between a desired setpoint and the measured process variable, then computes a corrective output:
$$u(t) = K_p \cdot e(t) + K_i \int_0^t e(\tau) \, d\tau + K_d \frac{de(t)}{dt}$$
- Proportional (Kp): Output proportional to current error. Large Kp = fast response but may oscillate. Too small = sluggish response
- Integral (Ki): Accumulates past errors to eliminate steady-state offset. Too large = overshoot and oscillation (integral windup)
- Derivative (Kd): Predicts future error by measuring rate of change. Damps oscillation but amplifies noise. Often filtered or disabled (PI control)
flowchart LR
SP["Setpoint
r(t)"] --> SUM(("+
−"))
SUM --> PID["PID
Controller"]
PID --> PLANT["Plant
(Motor/Heater)"]
PLANT --> OUT["Output
y(t)"]
OUT --> SENSOR["Sensor
Feedback"]
SENSOR -->|"Measured value"| SUM
PID -.->|"Kp · e(t)"| PID
PID -.->|"Ki · ∫e(t)"| PID
PID -.->|"Kd · de/dt"| PID
style SP fill:#3B9797,stroke:#3B9797,color:#fff
style OUT fill:#132440,stroke:#132440,color:#fff
style PID fill:#e8f4f4,stroke:#3B9797,color:#132440
style SENSOR fill:#f0f4f8,stroke:#16476A,color:#132440
Tuning Methods
PID Tuning Approaches
| Method | Approach | Pros / Cons |
|---|---|---|
| Manual Tuning | Set Ki, Kd = 0. Increase Kp until oscillation. Add Ki. Add Kd to damp | Simple but time-consuming |
| Ziegler-Nichols | Find ultimate gain (Ku) and oscillation period (Tu). Calculate gains from table | Classic, aggressive response |
| Cohen-Coon | Step response: measure delay (L) and time constant (T) | Better for processes with dead time |
| Software Auto-Tune | Relay feedback, frequency analysis | Automated, good starting point |
flowchart TD
START["Observe System
Response"] --> Q1{"Oscillating?"}
Q1 -->|"Yes, large"| A1["Reduce Kp"]
Q1 -->|"No"| Q2{"Steady-state
error?"}
A1 --> START
Q2 -->|"Yes"| A2["Increase Ki
(slowly)"]
Q2 -->|"No"| Q3{"Overshoot?"}
A2 --> START
Q3 -->|"Yes"| A3["Increase Kd
or reduce Kp"]
Q3 -->|"No"| Q4{"Response
too slow?"}
A3 --> START
Q4 -->|"Yes"| A4["Increase Kp"]
Q4 -->|"No"| DONE["✅ Tuned!"]
A4 --> START
style START fill:#3B9797,stroke:#3B9797,color:#fff
style DONE fill:#132440,stroke:#132440,color:#fff
style Q1 fill:#fff5f5,stroke:#BF092F,color:#132440
style Q2 fill:#fff5f5,stroke:#BF092F,color:#132440
style Q3 fill:#fff5f5,stroke:#BF092F,color:#132440
style Q4 fill:#fff5f5,stroke:#BF092F,color:#132440
Code Implementation
// PID controller implementation for embedded systems
#include <stdint.h>
typedef struct {
float kp, ki, kd;
float integral;
float prev_error;
float output_min, output_max;
float integral_max; // Anti-windup limit
float dt; // Sample time in seconds
} PID_Controller;
void pid_init(PID_Controller *pid, float kp, float ki, float kd,
float dt, float out_min, float out_max) {
pid->kp = kp;
pid->ki = ki;
pid->kd = kd;
pid->dt = dt;
pid->output_min = out_min;
pid->output_max = out_max;
pid->integral_max = out_max * 0.5f; // Limit integral to 50% of output
pid->integral = 0.0f;
pid->prev_error = 0.0f;
}
float pid_compute(PID_Controller *pid, float setpoint, float measurement) {
float error = setpoint - measurement;
// Proportional term
float p_term = pid->kp * error;
// Integral term with anti-windup
pid->integral += error * pid->dt;
if (pid->integral > pid->integral_max)
pid->integral = pid->integral_max;
else if (pid->integral < -pid->integral_max)
pid->integral = -pid->integral_max;
float i_term = pid->ki * pid->integral;
// Derivative term (on measurement to avoid setpoint kick)
float derivative = (error - pid->prev_error) / pid->dt;
float d_term = pid->kd * derivative;
pid->prev_error = error;
// Compute and clamp output
float output = p_term + i_term + d_term;
if (output > pid->output_max) output = pid->output_max;
if (output < pid->output_min) output = pid->output_min;
return output;
}
void pid_reset(PID_Controller *pid) {
pid->integral = 0.0f;
pid->prev_error = 0.0f;
}
#!/usr/bin/env python3
"""PID controller simulation with matplotlib"""
import numpy as np
import matplotlib.pyplot as plt
class PIDController:
def __init__(self, kp, ki, kd, dt, output_limits=(-100, 100)):
self.kp, self.ki, self.kd = kp, ki, kd
self.dt = dt
self.integral = 0.0
self.prev_error = 0.0
self.out_min, self.out_max = output_limits
def compute(self, setpoint, measurement):
error = setpoint - measurement
self.integral += error * self.dt
self.integral = np.clip(self.integral, self.out_min / self.ki if self.ki else 0,
self.out_max / self.ki if self.ki else 0)
derivative = (error - self.prev_error) / self.dt
self.prev_error = error
output = self.kp * error + self.ki * self.integral + self.kd * derivative
return np.clip(output, self.out_min, self.out_max)
# Simulate DC motor speed control
dt = 0.01 # 100 Hz control loop
time = np.arange(0, 5, dt)
setpoint = np.where(time < 1.0, 0, 1000) # Step to 1000 RPM at t=1
pid = PIDController(kp=0.5, ki=2.0, kd=0.01, dt=dt)
speed = 0.0
speeds = []
for sp in setpoint:
output = pid.compute(sp, speed)
# Simple motor model: speed += (output - friction * speed) * dt
speed += (output * 10 - 0.1 * speed) * dt
speeds.append(speed)
plt.figure(figsize=(10, 5))
plt.plot(time, setpoint, 'r--', label='Setpoint')
plt.plot(time, speeds, 'b-', label='Actual Speed')
plt.xlabel('Time (s)')
plt.ylabel('Speed (RPM)')
plt.title('PID Motor Speed Control Simulation')
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()
Closed-Loop Feedback
Encoder Feedback
Quadrature encoders provide two square wave signals (A and B) offset by 90°, enabling both position and direction detection. STM32 timers have a dedicated encoder mode that counts pulses automatically in hardware.
// STM32 Timer Encoder Mode for position feedback
#include "stm32f4xx.h"
// TIM2 encoder on PA0 (CH1) and PA1 (CH2)
void encoder_init(void) {
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
// PA0, PA1 → AF1 (TIM2)
GPIOA->MODER |= (2U << 0) | (2U << 2);
GPIOA->AFR[0] |= (1U << 0) | (1U << 4);
GPIOA->PUPDR |= (1U << 0) | (1U << 2); // Pull-up
// Encoder mode 3: count on both TI1 and TI2 edges
TIM2->SMCR = 3; // SMS = 011
TIM2->CCMR1 = (1U << 0) | (1U << 8); // CC1S = TI1, CC2S = TI2
TIM2->CCER = 0; // Non-inverted
TIM2->ARR = 0xFFFFFFFF; // 32-bit counter
TIM2->CNT = 0;
TIM2->CR1 |= TIM_CR1_CEN;
}
int32_t encoder_get_position(void) {
return (int32_t)TIM2->CNT;
}
// Calculate speed (call at fixed interval, e.g., 10ms)
static int32_t last_count = 0;
float encoder_get_speed_rpm(uint16_t ppr, float dt_sec) {
int32_t current = (int32_t)TIM2->CNT;
int32_t delta = current - last_count;
last_count = current;
// Quadrature encoder: 4 counts per pulse
return (float)delta / (4.0f * ppr) / dt_sec * 60.0f;
}
Current Sensing
Current sensing is essential for torque control, overcurrent protection, and sensorless motor control. Common approaches:
- Shunt Resistor: Low-value resistor (10–100 mΩ) in series. Voltage drop = I × R. Requires amplification (INA180, INA219)
- Hall-Effect: Non-contact, ACS712/ACS758. Electrical isolation. ±5A to ±200A
- Current Transformer: AC only, high-frequency. Toroidal core around wire
- Driver IC Built-in: DRV8871, DRV8301 have integrated current sense outputs
Conclusion & Next Steps
Actuator control combines PWM power delivery with feedback-driven algorithms to achieve precise motion. The PID controller remains the workhorse of embedded control, and proper H-Bridge selection ensures efficient power delivery to motors.
- Choose PWM frequency based on actuator type (20+ kHz for DC motors, 50 Hz for servos)
- Select H-Bridge driver ICs based on voltage, current, and efficiency requirements
- PID anti-windup clamping prevents integral accumulation during saturation
- Hardware encoder mode on STM32 timers provides zero-CPU-cost position counting
- Current sensing enables torque control and overcurrent protection
In Part 7, we cover System Integration — combining sensors and actuators into complete systems with RTOS, state machines, and communication protocols.