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

  1. PWM generation: Configurable frequency (1kHz-100kHz)
  2. Duty cycle control: 0-100% with 0.1% resolution
  3. Direction control: Forward, reverse, brake, coast
  4. Speed ramping: Smooth acceleration/deceleration
  5. Encoder reading: Position and speed feedback
  6. Serial interface: Commands for speed, direction, position
  7. LED indicator: PWM-controlled brightness for testing

3.3 Non-Functional Requirements

  1. Frequency accuracy: Within 1% of target
  2. Response time: Speed changes within 10ms
  3. Smooth operation: No audible motor whine (>18kHz PWM)
  4. Safety: Emergency stop capability
  5. 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:

  1. LED dims smoothly: PWM varies LED brightness
  2. Motor speed controllable: Responds to duty cycle changes
  3. Smooth ramping: No jerky starts or stops
  4. Direction changes: Clean forward/reverse transitions
  5. 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:

  1. 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
  2. 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
  3. Quadrature Encoding
    • How do two signals give direction information?
    • What is 4x decoding mode?
    • Book Reference: Encoder datasheets, application notes
  4. 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:

  1. PWM Frequency Selection
    • Why is 20kHz a good choice for DC motors?
    • What frequency would you use for servos?
  2. Motor Safety
    • What happens if both IN1 and IN2 are high?
    • How do you ensure safe direction changes?
  3. Encoder Overflow
    • Timer counter is 16-bit. What happens at 65535?
    • How do you track position beyond 65535 counts?
  4. 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:

  1. “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
  2. “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
  3. “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
  4. “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
  5. “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!