Timers and PWM

Timers are the workhorses of embedded systems. At their core, they're just hardware counters that tick at a known rate. But from that simple mechanism comes an astonishing range of capabilities: generating precise waveforms, measuring pulse widths, driving motors, controlling servos, and running real-time control loops.

What Is a Hardware Timer?

A timer is a counter register inside the MCU that increments (or decrements) on every tick of its input clock. When it reaches a target value, it resets to zero and can trigger an action — fire an interrupt, toggle a pin, start a DMA transfer, or simply keep counting.

Clock ticks:   ↑  ↑  ↑  ↑  ↑  ↑  ↑  ↑  ↑  ↑  ↑  ↑  ↑
Counter:       0  1  2  3  4  5  0  1  2  3  4  5  0  1 ...
                              ↑                 ↑
                           Reset!            Reset!
                     (ARR = 5, resets at 5)

Two key registers control a timer's behavior:

  • PSC (Prescaler): Divides the input clock. A prescaler of 9 means the counter ticks once every 10 clock cycles.
  • ARR (Auto-Reload Register): The target value. When the counter reaches ARR, it resets and the cycle repeats.

Timer Math

The output frequency of a timer is determined by this formula:

Frequency = Timer_Clock / ((PSC + 1) × (ARR + 1))

The "+1" on both terms exists because the registers are zero-indexed — a PSC value of 0 means divide-by-1, not divide-by-0.

Example: Getting 1 kHz from a 240 MHz Clock

Let's say your timer's input clock is 240 MHz (typical for TIM1 on an H743 running at full speed) and you want a 1 kHz interrupt:

1,000 = 240,000,000 / ((PSC + 1) × (ARR + 1))
(PSC + 1) × (ARR + 1) = 240,000

One solution: PSC = 239, ARR = 999
Check: 240,000,000 / (240 × 1000) = 1,000 Hz ✓

There are many valid combinations. A good rule of thumb: use the prescaler to bring the frequency down to a manageable range, then use ARR for fine-tuning.

💡 Fun Fact: STM32 advanced timers (TIM1, TIM8) have 16-bit prescalers and 16-bit counters, giving you a maximum division factor of 65536 x 65536 = over 4 billion. That means you can generate frequencies from hundreds of MHz all the way down to fractions of a Hertz from a single timer.

PWM — Pulse Width Modulation

PWM is one of the most useful things you can do with a timer. Instead of just resetting at the target count, the timer also toggles an output pin at a specific count within each cycle. The result is a square wave where you control the duty cycle — the fraction of time the signal is HIGH.

100% duty:  ████████████████████████████████

 75% duty:  ██████████████████████________

 50% duty:  ████████████████________________

 25% duty:  ████████________________________

  0% duty:  ________________________________

Why is this useful? Because many devices respond to average power. An LED at 50% duty cycle appears half as bright. A motor at 75% duty runs at roughly 75% speed. The switching happens so fast (typically thousands of times per second) that the physical device can't follow individual pulses — it just sees the average.

🧠 Think About It: Your laptop screen probably uses PWM for brightness control. At low brightness settings, some screens flicker at frequencies that sensitive people can perceive. Higher-quality displays use higher PWM frequencies or DC dimming to avoid this.

PWM with Embassy

Embassy makes PWM straightforward through its SimplePwm driver. Here's how to generate a PWM signal on a TIM1 channel:

#![no_std]
#![no_main]

use embassy_executor::Spawner;
use embassy_stm32::gpio::OutputType;
use embassy_stm32::time::khz;
use embassy_stm32::timer::simple_pwm::{PwmPin, SimplePwm};
use embassy_time::Timer;
use {defmt_rtt as _, panic_probe as _};

#[embassy_executor::main]
async fn main(_spawner: Spawner) {
    let p = embassy_stm32::init(Default::default());

    let pwm_pin = PwmPin::new_ch1(p.PE9, OutputType::PushPull);
    let mut pwm = SimplePwm::new(
        p.TIM1, Some(pwm_pin), None, None, None,
        khz(1), Default::default(),
    );

    let max_duty = pwm.get_max_duty();
    pwm.enable(embassy_stm32::timer::Channel::Ch1);

    // Breathe effect: ramp brightness up and down
    loop {
        for i in 0..=100 {
            pwm.set_duty(embassy_stm32::timer::Channel::Ch1, max_duty * i / 100);
            Timer::after_millis(10).await;
        }
        for i in (0..=100).rev() {
            pwm.set_duty(embassy_stm32::timer::Channel::Ch1, max_duty * i / 100);
            Timer::after_millis(10).await;
        }
    }
}

The get_max_duty() returns the ARR value. Setting duty to max_duty / 2 gives 50%. Setting it to max_duty * 75 / 100 gives 75%.

Controlling a Servo Motor

Hobby servos expect a very specific PWM signal:

  • Frequency: 50 Hz (20 ms period)
  • Pulse width: 1 ms (0 degrees) to 2 ms (180 degrees)
  • Center position: 1.5 ms (90 degrees)
 0°:    ██__________________    (1ms HIGH, 19ms LOW)
90°:    ███_________________    (1.5ms HIGH, 18.5ms LOW)
180°:   ████________________    (2ms HIGH, 18ms LOW)
        ←────── 20ms ──────→

Here's how to control a servo with Embassy:

#![allow(unused)]
fn main() {
let pwm_pin = PwmPin::new_ch1(p.PE9, OutputType::PushPull);
let mut pwm = SimplePwm::new(
    p.TIM1, Some(pwm_pin), None, None, None,
    hz(50), Default::default(),  // 50 Hz for servo
);
pwm.enable(embassy_stm32::timer::Channel::Ch1);
let max_duty = pwm.get_max_duty();

// Convert angle (0-180) to duty cycle
// 1ms = 5% of 20ms period, 2ms = 10% of 20ms period
let angle_to_duty = |angle: u32| -> u32 {
    let min_duty = max_duty * 5 / 100;   // 1ms pulse (0 degrees)
    let max_pulse = max_duty * 10 / 100;  // 2ms pulse (180 degrees)
    min_duty + (max_pulse - min_duty) * angle / 180
};

// Sweep from 0 to 180 degrees
for angle in (0..=180).step_by(1) {
    pwm.set_duty(embassy_stm32::timer::Channel::Ch1, angle_to_duty(angle));
    Timer::after_millis(15).await;
}
}

💡 Fun Fact: Servo control signals were standardized in the 1960s for radio-controlled aircraft. The 1-2ms pulse width range was chosen because the analog circuits of the era could reliably discriminate pulse widths in that range. We still use the same protocol today, over 60 years later.

Precise Periodic Loops with Ticker

Many embedded applications need to run code at an exact, consistent rate — PID control loops, sensor sampling, communication protocols. Embassy's Ticker provides precisely timed periodic execution:

#![allow(unused)]
fn main() {
use embassy_time::Ticker;
use core::time::Duration;

#[embassy_executor::task]
async fn control_loop_task() {
    // Tick every 1ms = 1kHz control loop
    let mut ticker = Ticker::every(Duration::from_millis(1));

    loop {
        // Read sensor
        let position = read_encoder();

        // Compute PID output
        let output = compute_pid(position, target);

        // Apply to motor
        set_motor_pwm(output);

        // Wait for next tick — compensates for execution time
        ticker.next().await;
    }
}
}

The critical difference between Ticker and Timer::after_millis is drift compensation. Timer::after_millis(1) waits 1ms from the point you call it — so your loop period is 1ms plus whatever time your code took to execute. Ticker::every maintains a fixed period regardless of execution time. If your code takes 0.3ms, the ticker waits 0.7ms. If it occasionally takes 0.8ms, the ticker waits 0.2ms. The frequency stays locked.

🧠 Think About It: In a PID control loop running at 1 kHz, consistent timing directly affects the derivative and integral calculations. If your loop period varies randomly between 0.8ms and 1.5ms, your derivative term will produce noisy, unreliable values. Ticker solves this.

Timer Channels and Applications

Most STM32 timers have multiple output channels, each capable of independent PWM with different duty cycles but sharing the same frequency.

TimerChannelsResolutionTypical Applications
TIM1, TIM84 + complementary16-bitMotor control, complementary PWM, 3-phase inverters
TIM2, TIM5432-bitLong-duration timing, input capture, encoder interface
TIM3, TIM4416-bitGeneral purpose PWM, LED control, buzzer
TIM6, TIM70 (basic)16-bitDAC triggering, periodic interrupts
TIM12-TIM141-216-bitSimple PWM, auxiliary timing

TIM1/TIM8 are "advanced" timers with complementary outputs and dead-time insertion -- essential for H-bridge motor drivers. TIM2/TIM5 have 32-bit counters (up to ~17.9 seconds at 240 MHz). TIM6/TIM7 have no output pins -- they're purely internal, often used to trigger DAC conversions.

Putting It All Together

Here's a practical example combining PWM and async tasks -- an LED that breathes at a speed controlled by a button:

#![allow(unused)]
fn main() {
use embassy_sync::signal::Signal;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;

static SPEED: Signal<CriticalSectionRawMutex, u64> = Signal::new();

#[embassy_executor::task]
async fn button_task(mut button: ExtiInput<'static>) {
    let speeds = [5, 10, 20, 50]; // ms per step
    let mut index = 0;
    loop {
        button.wait_for_falling_edge().await;
        index = (index + 1) % speeds.len();
        SPEED.signal(speeds[index]);
        Timer::after_millis(50).await;
    }
}

#[embassy_executor::task]
async fn breathe_task(mut pwm: SimplePwm<'static, embassy_stm32::peripherals::TIM1>) {
    let max_duty = pwm.get_max_duty();
    let ch = embassy_stm32::timer::Channel::Ch1;
    pwm.enable(ch);
    let mut step_ms: u64 = 10;

    loop {
        for i in 0..=100 {
            if let Some(s) = SPEED.try_take() { step_ms = s; }
            pwm.set_duty(ch, max_duty * i / 100);
            Timer::after_millis(step_ms).await;
        }
        for i in (0..=100).rev() {
            if let Some(s) = SPEED.try_take() { step_ms = s; }
            pwm.set_duty(ch, max_duty * i / 100);
            Timer::after_millis(step_ms).await;
        }
    }
}
}

This combines PWM, async tasks, and inter-task communication -- all running cooperatively on a single-core MCU with no OS.

What's Next?

Timers and PWM give you precise control over time and waveforms. Next, we'll explore serial communication — UART, SPI, and I2C — the protocols that let your microcontroller talk to sensors, displays, and other devices.