Project 12: Timer-Based PWM Motor Controller
Build a PWM generator using ARM timer peripherals to control motor speed or servo position, with smooth acceleration and position feedback via encoder.
Quick Reference
| Attribute | Value |
|---|---|
| Difficulty | Level 2 - Intermediate |
| Time Estimate | 1 week |
| Language | C (primary), Rust, MicroPython (for comparison) |
| Prerequisites | Project 3 (bare-metal), basic understanding of DC motors |
| Key Topics | Timers, PWM Generation, Output Compare, Encoder Mode, H-Bridge Control |
1. Learning Objectives
After completing this project, you will:
- Understand ARM timer architecture and counter modes
- Master PWM generation using output compare units
- Calculate prescaler and period values for any frequency
- Implement smooth acceleration/deceleration profiles
- Use timer encoder mode for position feedback
- Control motor direction with H-bridge drivers
- Understand complementary outputs and dead-time insertion
- Implement basic PID control concepts
2. Theoretical Foundation
2.1 Core Concepts
ARM Timer Architecture
ARM Cortex-M microcontrollers have multiple timer types with different capabilities:
Timer Hierarchy (STM32F4 Example):
┌─────────────────────────────────────────────────────────────────┐
│ Advanced Timers (TIM1, TIM8) │
│ • 16-bit counter with auto-reload │
│ • 4 capture/compare channels │
│ • Complementary outputs with dead-time │
│ • Break input for motor control │
│ • Repetition counter │
├─────────────────────────────────────────────────────────────────┤
│ General Purpose Timers (TIM2-TIM5) │
│ • 16 or 32-bit counter │
│ • 4 capture/compare channels │
│ • PWM, input capture, output compare │
│ • Encoder interface mode │
├─────────────────────────────────────────────────────────────────┤
│ Basic Timers (TIM6, TIM7) │
│ • 16-bit counter only │
│ • No capture/compare │
│ • DAC trigger, time base only │
└─────────────────────────────────────────────────────────────────┘
Timer Counter Operation
Timer Counter Flow:
┌──────────────────┐
Timer Clock ────────────────────►│ Prescaler │
(e.g., 72MHz) │ (PSC + 1) │
└────────┬─────────┘
│
Counter Clock
(72MHz / (PSC+1))
│
┌────────▼─────────┐
│ Counter │
│ (CNT) │
│ │
│ 0 → 1 → 2 → ... │
│ → ARR → 0 │
└────────┬─────────┘
│
┌──────────────┼──────────────┐
│ │ │
┌─────────▼────┐ ┌──────▼──────┐ ┌───▼───┐
│ CCR1 Match │ │ CCR2 Match │ │ etc. │
│ (PWM Ch1) │ │ (PWM Ch2) │ │ │
└──────────────┘ └─────────────┘ └───────┘
Counter value goes 0 → ARR (Auto-Reload Register)
When CNT = CCRx, output changes state
When CNT = ARR, counter resets to 0 (update event)
PWM Generation Modes
PWM Mode 1 (Most Common):
- Output HIGH when CNT < CCR
- Output LOW when CNT >= CCR
ARR
┌──────────┼──────────┐
│ │ │
CCR ───┼───────┬──┼──────────┼─────
│ │ │ │
│ │ │ │
CNT ──┼───────┘ └──────────┼───
│ │
▼ ▼
Output ─┐ ┌───────────────┐ ┌───
│ │ │ │
└─────┘ └─────┘
PWM Mode 2 (Inverted):
- Output LOW when CNT < CCR
- Output HIGH when CNT >= CCR
Duty Cycle = CCR / (ARR + 1) × 100%
Example: ARR=999, CCR=500 → 50% duty cycle
ARR=999, CCR=250 → 25% duty cycle
PWM Frequency Calculation
PWM Frequency Formula:
Timer Clock
f_PWM = ─────────────────────────────
(PSC + 1) × (ARR + 1)
Example: 72MHz clock, 20kHz PWM
20,000 = 72,000,000 / ((PSC+1) × (ARR+1))
(PSC+1) × (ARR+1) = 3600
Options:
PSC=0, ARR=3599 → 10-bit resolution
PSC=1, ARR=1799 → ~11-bit resolution
PSC=3, ARR=899 → ~10-bit resolution
For 50% duty: CCR = ARR/2 = 1799 (if PSC=1)
Encoder Interface Mode
Timers can count pulses from rotary encoders:
Quadrature Encoder Signals:
┌───┐ ┌───┐ ┌───┐ ┌───┐
Ch A ─┘ └───┘ └───┘ └───┘ └───
┌───┐ ┌───┐ ┌───┐ ┌───
Ch B ─────┘ └───┘ └───┘ └───┘
Forward rotation: A leads B (phase difference +90°)
Reverse rotation: B leads A (phase difference -90°)
Encoder Mode Options:
Mode 1: Count on TI1 edges only
Mode 2: Count on TI2 edges only
Mode 3: Count on both edges (4× resolution)
Standard encoder: 100 PPR (pulses per revolution)
Mode 3: 400 counts per revolution
H-Bridge Motor Control
H-Bridge Circuit:
VCC VCC
│ │
┌─┴─┐ ┌─┴─┐
│Q1 │ │Q3 │
│ │ │ │
└─┬─┘ └─┬─┘
├────────────┬───────────────┤
│ │ │
│ ┌─┴─┐ │
│ │ M │ Motor │
│ └─┬─┘ │
│ │ │
├────────────┴───────────────┤
┌─┴─┐ ┌─┴─┐
│Q2 │ │Q4 │
│ │ │ │
└─┬─┘ └─┬─┘
│ │
GND GND
Control Logic:
Forward: Q1=ON, Q4=ON, Q2=OFF, Q3=OFF
Reverse: Q2=ON, Q3=ON, Q1=OFF, Q4=OFF
Brake: Q1=OFF, Q2=ON, Q3=OFF, Q4=ON
Coast: All OFF
PWM Control:
Apply PWM to Q1/Q3 (high-side) or Q2/Q4 (low-side)
L298N, L293D, TB6612 are common H-bridge ICs
2.2 Why This Matters
Timers are the most complex ARM peripherals. They’re used for:
- Motor control (PWM for DC motors, servo signals)
- Audio generation (DAC triggering)
- Timing measurements (input capture)
- Event counting (encoder interfaces)
- Communication timing (UART, I2C bit-banging)
Mastering timers unlocks robotics, industrial control, and many IoT applications.
Industry usage:
- CNC machines use PWM for spindle speed control
- Electric vehicles use advanced PWM for motor control
- Drones use PWM for ESC (Electronic Speed Controller) signals
- Industrial robots use encoder feedback for precise positioning
2.3 Historical Context
PWM motor control has evolved significantly:
- Early days: Resistive speed control (wasteful, hot)
- 1960s: Thyristor/SCR control (industrial)
- 1980s: MOSFET H-bridges (efficient, affordable)
- 1990s: Microcontroller PWM (precise, programmable)
- Today: Field-Oriented Control (FOC) with advanced MCUs
Modern ARM MCUs include dedicated motor control features:
- Dead-time insertion (prevents shoot-through)
- Break inputs (emergency stop)
- Complementary outputs (H-bridge driving)
- Center-aligned PWM (reduced harmonics)
2.4 Common Misconceptions
Misconception 1: “Higher PWM frequency is always better”
- Reality: Higher frequencies increase switching losses. 20kHz is typically optimal for DC motors (above hearing, low losses).
Misconception 2: “100% duty cycle means full speed”
- Reality: Motor speed depends on voltage and load. PWM controls average voltage, but motor physics limits actual speed.
Misconception 3: “Encoder counts equal position”
- Reality: Encoder counts accumulate. You need to track direction and handle overflow for true position.
Misconception 4: “Any GPIO can do PWM”
- Reality: Hardware PWM requires timer output compare channels on specific pins. Software PWM is possible but CPU-intensive.
Misconception 5: “H-bridge only needs 2 control signals”
- Reality: Full control needs at least 2 signals (direction + PWM), but proper H-bridges have 4 inputs for maximum flexibility.
3. Project Specification
3.1 What You Will Build
A complete motor control system that:
- Generates PWM at configurable frequency and duty cycle
- Controls motor direction via H-bridge
- Implements smooth acceleration/deceleration
- Reads position/speed from rotary encoder
- Provides a serial command interface
- Works on STM32 or similar Cortex-M boards
3.2 Functional Requirements
- PWM generation: Configurable frequency (1kHz-100kHz)
- Duty cycle control: 0-100% with 0.1% resolution
- Direction control: Forward, reverse, brake, coast
- Speed ramping: Smooth acceleration/deceleration
- Encoder reading: Position and speed feedback
- Serial interface: Commands for speed, direction, position
- LED indicator: PWM-controlled brightness for testing
3.3 Non-Functional Requirements
- Frequency accuracy: Within 1% of target
- Response time: Speed changes within 10ms
- Smooth operation: No audible motor whine (>18kHz PWM)
- Safety: Emergency stop capability
- Efficiency: Minimal CPU usage (hardware PWM)
3.4 Example Usage / Output
=== PWM Motor Controller ===
Timer: TIM1, 72MHz base, PWM @ 20kHz
Motor: DC brushed, H-bridge driver (L298N)
Encoder: 100 PPR quadrature
Commands:
S<n> - Set speed (-100 to 100, negative=reverse)
A<n> - Set acceleration rate (1-100)
P - Print position
R - Reset encoder position
? - Show status
> S50
Ramping to 50%...
10% -> 20% -> 30% -> 40% -> 50%
Current: 50%, Duty: 500/1000, Direction: FWD
> S-30
Reversing direction...
50% -> 40% -> 30% -> 20% -> 10% -> 0%
Changing direction...
0% -> -10% -> -20% -> -30%
Current: -30%, Duty: 300/1000, Direction: REV
> P
Encoder count: 4523
Estimated RPM: 1200
Position: 11.3 revolutions (since reset)
> A10
Acceleration rate set to 10
> ?
PWM Frequency: 20000 Hz
Current Speed: -30%
Duty Cycle: 300/1000 (30.0%)
Direction: REVERSE
Encoder Position: 4523
Encoder Speed: 1200 RPM
Temperature: OK
Physical result: Motor smoothly accelerates, holds speed, reverses
3.5 Real World Outcome
What success looks like:
- LED dims smoothly: PWM varies LED brightness
- Motor speed controllable: Responds to duty cycle changes
- Smooth ramping: No jerky starts or stops
- Direction changes: Clean forward/reverse transitions
- Encoder feedback: Accurate position/speed reading
4. Solution Architecture
4.1 High-Level Design
┌─────────────────────────────────────────────────────────────────┐
│ Motor Control System │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Application Layer │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ Serial │ │ Control │ │ Feedback │ │ │
│ │ │ Commands │ │ Logic │ │ Display │ │ │
│ │ │ "S50", "P" │ │ Ramping │ │ Position │ │ │
│ │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ │
│ └─────────│────────────────│────────────────│─────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Motor Control Layer │ │
│ │ ┌───────────────────────────────────────────────────┐ │ │
│ │ │ set_speed() set_direction() get_position() │ │ │
│ │ │ set_acceleration() emergency_stop() │ │ │
│ │ └───────────────────────────────────────────────────┘ │ │
│ └────────────────────────────┬────────────────────────────┘ │
│ │ │
│ ┌────────────────┼────────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
│ │ PWM Driver │ │ Direction │ │ Encoder │ │
│ │ │ │ Driver │ │ Driver │ │
│ │ TIM1 Ch1 │ │ GPIO x2 │ │ TIM2 Enc │ │
│ │ 20kHz PWM │ │ IN1, IN2 │ │ Mode 3 │ │
│ └───────┬───────┘ └───────┬───────┘ └───────┬───────┘ │
│ │ │ │ │
│ ┌───────┴──────────────────┴──────────────────┴───────┐ │
│ │ Hardware Layer │ │
│ │ │ │
│ │ PA8 (TIM1_CH1) ──────────► L298N ENA ──┐ │ │
│ │ PB4 (GPIO) ──────────────► L298N IN1 │ │ │
│ │ PB5 (GPIO) ──────────────► L298N IN2 ├──► MOTOR │ │
│ │ │ │ │
│ │ PA0 (TIM2_CH1) ◄───────── Encoder A │ │ │
│ │ PA1 (TIM2_CH2) ◄───────── Encoder B ◄──┘ │ │
│ │ │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
4.2 Key Components
| Component | Purpose | Key Functions |
|---|---|---|
| PWM Driver | Timer-based PWM generation | pwm_init(), pwm_set_duty() |
| Direction Driver | H-bridge control | motor_forward(), motor_reverse() |
| Encoder Driver | Position/speed feedback | encoder_init(), encoder_read() |
| Motor Control | High-level motor API | set_speed(), set_direction() |
| Ramping Logic | Smooth acceleration | ramp_update() |
| Serial Interface | Command parsing | cmd_parse(), cmd_execute() |
4.3 Data Structures
// PWM configuration
typedef struct {
TIM_TypeDef *timer; // Timer peripheral (TIM1, TIM2, etc.)
uint8_t channel; // PWM channel (1-4)
uint32_t frequency; // PWM frequency in Hz
uint16_t resolution; // ARR+1 (duty cycle steps)
} pwm_config_t;
// Motor state
typedef struct {
int16_t target_speed; // Target speed (-100 to 100)
int16_t current_speed; // Current speed
uint8_t acceleration; // Ramp rate (1-100)
motor_dir_t direction; // FORWARD, REVERSE, BRAKE, COAST
bool enabled; // Motor enabled flag
} motor_state_t;
// Encoder state
typedef struct {
TIM_TypeDef *timer; // Timer in encoder mode
int32_t position; // Accumulated position
int32_t last_count; // Last CNT value (for overflow handling)
uint32_t last_time; // Last read time (for RPM)
int16_t rpm; // Calculated RPM
} encoder_state_t;
// Direction enumeration
typedef enum {
MOTOR_FORWARD,
MOTOR_REVERSE,
MOTOR_BRAKE,
MOTOR_COAST
} motor_dir_t;
4.4 Algorithm Overview
FUNCTION pwm_init(frequency):
// Calculate prescaler and ARR for desired frequency
// Target: (clock / (PSC+1) / (ARR+1)) = frequency
timer_clock = get_timer_clock() // e.g., 72MHz
divisor = timer_clock / frequency // e.g., 3600 for 20kHz
// Choose PSC and ARR for good resolution
IF divisor <= 65536:
PSC = 0
ARR = divisor - 1
ELSE:
PSC = (divisor / 65536)
ARR = (divisor / (PSC + 1)) - 1
// Configure timer
TIMx->PSC = PSC
TIMx->ARR = ARR
TIMx->CCR1 = 0 // Start with 0% duty
TIMx->CCMR1 = PWM_MODE_1 // PWM mode 1
TIMx->CCER = ENABLE_CH1 // Enable output
TIMx->CR1 = ENABLE_TIMER
FUNCTION set_speed_smooth(target):
motor.target_speed = target
// Called periodically (e.g., every 10ms)
FUNCTION ramp_update():
IF motor.current_speed < motor.target_speed:
motor.current_speed += motor.acceleration
IF motor.current_speed > motor.target_speed:
motor.current_speed = motor.target_speed
ELSE IF motor.current_speed > motor.target_speed:
motor.current_speed -= motor.acceleration
IF motor.current_speed < motor.target_speed:
motor.current_speed = motor.target_speed
// Handle direction change at zero crossing
IF sign(motor.current_speed) != sign(motor.target_speed):
IF motor.current_speed == 0:
update_direction(motor.target_speed)
apply_pwm(abs(motor.current_speed))
FUNCTION encoder_read():
current_count = TIM_ENC->CNT
// Handle 16-bit overflow
delta = current_count - encoder.last_count
IF delta > 32767:
delta -= 65536
ELSE IF delta < -32768:
delta += 65536
encoder.position += delta
encoder.last_count = current_count
// Calculate RPM
current_time = get_ms()
time_delta = current_time - encoder.last_time
IF time_delta >= 100: // Update every 100ms
// RPM = (delta_counts / counts_per_rev) * (60000 / time_ms)
encoder.rpm = (delta * 60000) / (COUNTS_PER_REV * time_delta)
encoder.last_time = current_time
RETURN encoder.position
5. Implementation Guide
5.1 Development Environment Setup
# Verify toolchain
$ arm-none-eabi-gcc --version
arm-none-eabi-gcc (GNU Arm Embedded Toolchain) 12.2.1
# Create project directory
$ mkdir -p ~/projects/pwm-motor
$ cd ~/projects/pwm-motor
# Create initial file structure
$ touch main.c pwm.c pwm.h motor.c motor.h encoder.c encoder.h
$ touch startup.s linker.ld Makefile
# Hardware connections (STM32F4):
# PA8 (TIM1_CH1) ──── L298N ENA (PWM input)
# PB4 (GPIO) ──── L298N IN1
# PB5 (GPIO) ──── L298N IN2
# PA0 (TIM2_CH1) ──── Encoder Channel A
# PA1 (TIM2_CH2) ──── Encoder Channel B
# 5V/12V ──── L298N VS (motor voltage)
# 3.3V ──── L298N VCC (logic)
# GND ──── Common ground
5.2 Project Structure
pwm-motor/
├── main.c # Demo application with serial interface
├── pwm.c # PWM driver implementation
├── pwm.h # PWM driver interface
├── motor.c # Motor control logic
├── motor.h # Motor control interface
├── encoder.c # Encoder driver implementation
├── encoder.h # Encoder interface
├── uart.c # Serial interface (from Project 4)
├── uart.h
├── stm32f4xx.h # Register definitions
├── startup.s # Vector table and reset handler
├── linker.ld # Linker script
├── Makefile
└── README.md
5.3 The Core Question You’re Answering
“How do digital timers generate analog-like control signals, and how does feedback enable precise motion control?”
Before coding, understand: A motor doesn’t know it’s receiving PWM. It sees the average voltage. At 20kHz, the motor’s inductance smooths the pulses into a nearly constant current. The duty cycle determines the average voltage, thus the speed.
5.4 Concepts You Must Understand First
Stop and research these before coding:
- Timer Clock Sources
- Where does the timer clock come from?
- How do APB1/APB2 prescalers affect timer clocks?
- Book Reference: STM32 Reference Manual, RCC chapter
- PWM Resolution vs Frequency Trade-off
- Why can’t you have both high frequency and high resolution?
- How do you choose the right balance?
- Book Reference: “Making Embedded Systems” Chapter 6 - White
- Quadrature Encoding
- How do two signals give direction information?
- What is 4x decoding mode?
- Book Reference: Encoder datasheets, application notes
- H-Bridge Operation
- What is shoot-through and why is it dangerous?
- How does PWM apply to H-bridge control?
- Book Reference: L298N datasheet
5.5 Questions to Guide Your Design
Before implementing, think through these:
- PWM Frequency Selection
- Why is 20kHz a good choice for DC motors?
- What frequency would you use for servos?
- Motor Safety
- What happens if both IN1 and IN2 are high?
- How do you ensure safe direction changes?
- Encoder Overflow
- Timer counter is 16-bit. What happens at 65535?
- How do you track position beyond 65535 counts?
- Acceleration Profiles
- Linear ramp vs S-curve vs exponential?
- How fast is too fast for your motor?
5.6 Thinking Exercise
Calculate PWM Parameters
Before coding, work through this example:
Goal: 20kHz PWM with 10-bit resolution (1024 steps)
Timer clock: 72MHz (APB2 timers on STM32F4)
Step 1: What is ARR for 1024 steps?
ARR = 1023 (counter goes 0 to 1023, 1024 values)
Step 2: What is the counter frequency?
f_counter = 20kHz × 1024 = 20,480,000 Hz
Step 3: What is PSC?
72,000,000 / 20,480,000 = 3.515...
PSC = 3 (prescaler divides by PSC+1 = 4)
Step 4: Verify
f_PWM = 72,000,000 / ((3+1) × (1023+1))
= 72,000,000 / 4096
= 17,578 Hz (close enough!)
Step 5: Adjust for exactly 20kHz
(PSC+1) × (ARR+1) = 72,000,000 / 20,000 = 3600
Options: PSC=0, ARR=3599 (3600 steps - good resolution!)
PSC=3, ARR=899 (900 steps)
Questions:
- What is the maximum duty cycle resolution with PSC=0, ARR=3599?
- How does changing PSC affect resolution?
- What would you choose for a servo signal (50Hz)?
5.7 Hints in Layers
Hint 1: Timer Clock Enable
// Enable timer and GPIO clocks
RCC->APB2ENR |= RCC_APB2ENR_TIM1EN; // TIM1 for PWM
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN; // TIM2 for encoder
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN; // GPIOA
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOBEN; // GPIOB for direction
Hint 2: PWM Configuration (TIM1)
void pwm_init(uint32_t frequency) {
uint32_t timer_clock = 72000000; // 72MHz
uint32_t period = timer_clock / frequency;
// Configure GPIO PA8 as TIM1_CH1 (AF1)
GPIOA->MODER &= ~(3 << 16);
GPIOA->MODER |= (2 << 16); // Alternate function
GPIOA->AFR[1] |= (1 << 0); // AF1 = TIM1
// Configure timer
TIM1->PSC = 0; // No prescaler
TIM1->ARR = period - 1; // Auto-reload value
TIM1->CCR1 = 0; // Start at 0% duty
// PWM mode 1, preload enable
TIM1->CCMR1 = (6 << 4) | TIM_CCMR1_OC1PE;
// Enable output
TIM1->CCER = TIM_CCER_CC1E;
// Main output enable (required for TIM1)
TIM1->BDTR |= TIM_BDTR_MOE;
// Enable timer
TIM1->CR1 |= TIM_CR1_CEN;
}
void pwm_set_duty(uint16_t duty) {
// duty: 0-1000 for 0-100%
TIM1->CCR1 = (TIM1->ARR + 1) * duty / 1000;
}
Hint 3: Encoder Mode (TIM2)
void encoder_init(void) {
// Configure GPIO PA0, PA1 as TIM2_CH1, CH2 (AF1)
GPIOA->MODER &= ~((3 << 0) | (3 << 2));
GPIOA->MODER |= (2 << 0) | (2 << 2); // AF mode
GPIOA->AFR[0] |= (1 << 0) | (1 << 4); // AF1 = TIM2
// Encoder mode 3: count on both edges of both channels
TIM2->SMCR = (3 << 0); // Encoder mode 3
// Configure input capture (TI1, TI2)
TIM2->CCMR1 = (1 << 0) | (1 << 8); // CC1S=01, CC2S=01
// No filter, no inversion
TIM2->CCER = 0;
// Set auto-reload to max for 32-bit counting
TIM2->ARR = 0xFFFFFFFF;
// Reset counter
TIM2->CNT = 0;
// Enable timer
TIM2->CR1 |= TIM_CR1_CEN;
}
int32_t encoder_get_position(void) {
return (int32_t)TIM2->CNT;
}
void encoder_reset(void) {
TIM2->CNT = 0;
}
Hint 4: Direction Control
void motor_set_direction(motor_dir_t dir) {
switch (dir) {
case MOTOR_FORWARD:
GPIOB->BSRR = (1 << 4); // IN1 = HIGH
GPIOB->BSRR = (1 << (5 + 16)); // IN2 = LOW
break;
case MOTOR_REVERSE:
GPIOB->BSRR = (1 << (4 + 16)); // IN1 = LOW
GPIOB->BSRR = (1 << 5); // IN2 = HIGH
break;
case MOTOR_BRAKE:
GPIOB->BSRR = (1 << 4); // IN1 = HIGH
GPIOB->BSRR = (1 << 5); // IN2 = HIGH
break;
case MOTOR_COAST:
GPIOB->BSRR = (1 << (4 + 16)); // IN1 = LOW
GPIOB->BSRR = (1 << (5 + 16)); // IN2 = LOW
break;
}
}
5.8 The Interview Questions They’ll Ask
Prepare to answer these:
- “What is PWM and why is it used for motor control?”
- Pulse Width Modulation: fixed frequency, variable duty cycle
- Controls average voltage to motor
- Efficient (no power wasted in resistors)
- Motor inductance smooths pulses into DC
- “How do you choose PWM frequency?”
- Above audible range (>18kHz) to avoid whine
- Below where switching losses become significant (<100kHz)
- Typical: 20kHz for DC motors, 50Hz for servos
- Trade-off with resolution
- “What is dead-time and why does it matter?”
- Brief period when both high and low side switches are off
- Prevents shoot-through (both switches on = short circuit)
- Advanced timers have hardware dead-time insertion
- “How does a quadrature encoder work?”
- Two channels, 90 degrees out of phase
- Direction determined by which leads
- 4x mode: count all edges = 4x resolution
- Common: 100 PPR = 400 counts/rev in 4x mode
- “How would you implement PID control?”
- Error = target - actual (from encoder)
- P: proportional to error
- I: integral of error over time
- D: derivative (rate of change of error)
- Output adjusts PWM duty cycle
5.9 Books That Will Help
| Topic | Book | Chapter |
|---|---|---|
| Timer architecture | STM32 Reference Manual | Timer chapters |
| PWM fundamentals | “Making Embedded Systems” by White | Ch. 6 |
| Motor control | “Making Embedded Systems” by White | Ch. 11 |
| Control theory | “Control Systems Engineering” by Nise | Ch. 1-5 |
| Encoder interfaces | STM32 Timer Application Notes | Encoder mode |
5.10 Implementation Phases
Phase 1: Basic PWM Output (2-3 hours)
- Configure timer for PWM
- Connect to LED for visual feedback
- Verify frequency with oscilloscope or multimeter
- Implement duty cycle control
Phase 2: Motor Connection (2-3 hours)
- Wire up H-bridge (L298N or similar)
- Implement direction control GPIOs
- Test motor spins in both directions
- Verify PWM controls speed
Phase 3: Smooth Ramping (2-3 hours)
- Implement acceleration/deceleration
- Use SysTick for periodic updates
- Handle direction change at zero crossing
- Test smooth start/stop
Phase 4: Encoder Feedback (3-4 hours)
- Configure timer in encoder mode
- Verify count changes with rotation
- Implement position tracking (handle overflow)
- Calculate RPM from count changes
Phase 5: Serial Interface (2-3 hours)
- Implement command parser
- Add speed, direction, position commands
- Display status information
- Add emergency stop command
Phase 6: Demo and Polish (2-3 hours)
- Create impressive demo sequence
- Add safety features (overcurrent detection if available)
- Document API
- Test edge cases
5.11 Key Implementation Decisions
| Decision | Trade-offs |
|---|---|
| Center vs edge-aligned PWM | Center: better EMI, symmetric. Edge: simpler |
| Encoder polling vs interrupt | Polling: simpler. Interrupt: never miss counts |
| Linear vs S-curve ramp | Linear: simpler. S-curve: smoother motion |
| Software vs hardware dead-time | HW: safer, guaranteed. SW: more flexible |
| 16-bit vs 32-bit timer for encoder | 32-bit: larger range without overflow handling |
6. Testing Strategy
6.1 Unit Tests
| Test | Description | Expected Result |
|---|---|---|
| PWM frequency | Measure SCK with scope | Within 1% of target |
| PWM duty cycle | Measure pulse width | Correct percentage |
| Direction GPIOs | Check voltage levels | Correct for each state |
| Encoder count | Rotate manually | Count increases/decreases |
6.2 Integration Tests
| Test | Command/Action | Expected Result |
|---|---|---|
| LED brightness | Vary duty 0-100% | Smooth dimming |
| Motor speed | S50 command | Motor runs at ~50% speed |
| Direction | S50, S-50 | Motor reverses smoothly |
| Ramping | A10, S100 | Gradual acceleration |
| Encoder | Rotate motor | Position updates |
| RPM | Steady speed | RPM reading stable |
6.3 Verification Commands
# Measure PWM frequency with multimeter (frequency mode)
# Expected: 20.0 kHz +/- 0.5 kHz
# Measure duty cycle with scope
# Set 50%, measure: should be 50% +/- 1%
# Verify encoder
# Rotate one full revolution
# Expected count: 400 (100 PPR encoder, 4x mode)
# Test ramping
# Command: A5, S100, S-100
# Motor should take ~2 seconds to ramp up, stop, reverse
7. Common Pitfalls & Debugging
Problem 1: “PWM frequency is wrong”
- Why: Incorrect timer clock assumption, wrong prescaler
- Fix: Check RCC configuration, APB prescalers affect timer clocks
- Debug: Print calculated PSC and ARR, measure with scope
Problem 2: “Motor doesn’t spin”
- Why: H-bridge not powered, wrong GPIO configuration, no PWM enable
- Fix: Verify motor voltage, check IN1/IN2 logic, verify PWM on ENA
- Debug: Disconnect motor, check H-bridge outputs with multimeter
Problem 3: “Motor spins but won’t reverse”
- Why: Direction GPIOs not toggling, H-bridge damage
- Fix: Verify GPIO states change, try new H-bridge if needed
- Debug: Measure IN1/IN2 with multimeter during direction change
Problem 4: “Encoder count jumps randomly”
- Why: Electrical noise, wrong edge polarity, loose connections
- Fix: Add decoupling capacitors, check pull-up resistors
- Debug: View encoder signals on scope, check for bounce
Problem 5: “RPM reading is unstable”
- Why: Sampling too frequently, integer division errors
- Fix: Average over longer period, use floating-point for calculation
- Debug: Print raw count deltas, check timing
Problem 6: “Motor jerks during ramping”
- Why: Ramp too aggressive, direction change not handled
- Fix: Slow down acceleration, ensure speed crosses zero before direction change
- Debug: Print speed during ramp, verify smooth progression
8. Extensions & Challenges
8.1 Easy Extensions
| Extension | Description | Learning |
|---|---|---|
| Speed display | Show RPM on OLED (Project 10) | System integration |
| Servo control | 50Hz PWM for RC servos | Different PWM parameters |
| Soft limits | Limit position range | Safety features |
| Multiple motors | Control 2+ motors | Timer channel usage |
8.2 Advanced Challenges
| Challenge | Description | Learning |
|---|---|---|
| PID control | Closed-loop speed/position control | Control theory |
| Brushless DC | 3-phase motor control | Advanced PWM |
| Field-Oriented Control | Sinusoidal commutation | Motor theory |
| CAN bus control | Remote motor control | Industrial protocols |
| Current sensing | Motor current feedback | Analog sensing |
| Regenerative braking | Energy recovery | Power electronics |
8.3 Research Topics
- How do industrial servo drives achieve sub-millimeter positioning?
- What is space-vector modulation for 3-phase motors?
- How do stepper motor drivers achieve microstepping?
- What is sensorless motor control and when is it used?
9. Real-World Connections
9.1 Production Systems Using This
| System | How It Uses PWM Motor Control | Notable Feature |
|---|---|---|
| CNC machines | Spindle speed, axis motors | Precise positioning |
| 3D printers | Stepper drivers, fans | Coordinated motion |
| Drones | ESC control for motors | High-speed response |
| Electric vehicles | Motor controllers | High power, efficiency |
| Robotics | Joint motors, grippers | Force/position control |
9.2 How the Pros Do It
SimpleFOC:
- Open-source brushless motor control
- Field-Oriented Control (FOC) algorithms
- Arduino/STM32 compatible
ODrive:
- High-performance brushless motor driver
- Dual motor control
- Position, velocity, current loops
Industrial Servo Drives:
- Microsecond update rates
- Sub-arc-minute positioning
- Integrated safety features
10. Self-Assessment Checklist
Before considering this project complete, verify:
- I can calculate PWM frequency from timer clock, PSC, and ARR
- I understand the relationship between duty cycle and motor speed
- My PWM generates the correct frequency (measure with scope)
- Duty cycle accurately reflects commanded value
- Direction control works (forward, reverse, brake, coast)
- Motor acceleration is smooth with no jerky motion
- Encoder mode correctly tracks position
- RPM calculation gives reasonable values
- Serial interface accepts and executes commands
- I can explain H-bridge operation and dead-time
- I understand quadrature encoding and 4x decoding
- I can answer all the interview questions listed above
Next Steps
After completing this project, you’ll be well-prepared for:
- Project 13: DMA Audio Player - Timer-triggered DAC updates
- Game Console Capstone - Game controller input and actuator output
- Advanced motor control - PID loops, brushless motors, FOC
The timer skills you’ve developed are fundamental to almost every real-time embedded system. You now understand the most complex peripheral on the MCU!