Part 2 · C Meets the Metal33 min read
Timers and PWM
Hardware timers are the most versatile peripherals on a microcontroller. The same block that generates a 1 kHz interrupt can dim an LED, drive a servo, measure the frequency of an incoming signal, count encoder pulses from a motor shaft, or trigger the ADC at an exact sample rate, all without the CPU touching a pin. This lesson takes the STM32F407 general-purpose timers apart: how the counter, prescaler and compare channels work, how to compute their settings, and complete working configurations for periodic interrupts, PWM, output compare and input capture. It finishes with software timers, which let one hardware tick drive any number of timeouts, and with what limits timing accuracy.
Timer anatomy
Section titled “Timer anatomy”Every STM32 timer is built from the same pieces:
timer clock +-----------+ CK_CNT +---------------+ (84 MHz on APB1) -->| PSC |----------->| CNT (counter) |---- counts 0,1,2...ARR,0,1... | divide by | +---------------+ | PSC + 1 | | | +-----------+ | | CNT == ARR ? | v | update event (UEV) | - sets UIF in SR | - reloads PSC/ARR shadows | - interrupt if UIE, TRGO, DMA | +------------+-------------+ | capture/compare channels | | CCR1 CCR2 CCR3 CCR4 | | compare: CNT vs CCRx |--> pin (PWM, toggle) | capture: CNT -> CCRx |<-- pin edge (measure) +--------------------------+- Prescaler (
PSC) divides the timer clock byPSC + 1, soPSC = 0means no division. It is 16 bits on every STM32F4 timer. - Counter (
CNT) increments once per prescaled tick. In the default upcounting mode it counts from 0 toARRand wraps to 0. - Auto-reload register (
ARR) sets the top of the count. The counter period isARR + 1ticks. - Update event happens when the counter wraps. It sets the
UIFflag, can raise an interrupt or DMA request, and copies preloaded values into the active registers. - Capture/compare channels (up to four). In compare mode, the hardware compares
CNTwithCCRxevery tick and can change an output pin on a match. In capture mode, an edge on the pin copies the currentCNTintoCCRx.
The fundamental formula:
f_update = f_timer / ((PSC + 1) * (ARR + 1))Which timer clock?
Section titled “Which timer clock?”From Clocks, reset, and timing: with SYSCLK at 168 MHz and APB1 divided by 4, PCLK1 is 42 MHz, and because the APB1 prescaler is not 1, the APB1 timers (TIM2 to TIM7, TIM12 to TIM14) get twice that: 84 MHz. The APB2 timers (TIM1, TIM8 to TIM11) get 168 MHz. Every number in this lesson assumes that configuration. If you skip the clock setup, the timers run at 16 MHz from HSI and everything is 5.25 times slower.
Timer families on the STM32F407
Section titled “Timer families on the STM32F407”| Timers | Type | Counter | Channels | Notes |
|---|---|---|---|---|
| TIM2, TIM5 | general purpose | 32-bit | 4 | best for long intervals and input capture |
| TIM3, TIM4 | general purpose | 16-bit | 4 | TIM4 drives the Discovery LEDs |
| TIM9 to TIM14 | general purpose | 16-bit | 1 or 2 | simpler feature set |
| TIM6, TIM7 | basic | 16-bit | 0 | time base and DAC trigger only |
| TIM1, TIM8 | advanced | 16-bit | 4 + complementary | dead time, break input, motor control; outputs need BDTR.MOE set |
The registers
Section titled “The registers”TIM2 is at 0x40000000 (clock enable RCC_APB1ENR bit 0), TIM3 at 0x40000400 (bit 1), TIM4 at 0x40000800 (bit 2). All general-purpose timers share one register layout, so one struct overlay covers them. It is not in stm32f407_regs.h, so this lesson defines it:
#ifndef TIM_H#define TIM_H
#include <stddef.h>#include <stdint.h>
typedef struct { volatile uint32_t CR1; /* 0x00 control 1 */ volatile uint32_t CR2; /* 0x04 control 2 (master mode / TRGO) */ volatile uint32_t SMCR; /* 0x08 slave mode control */ volatile uint32_t DIER; /* 0x0C DMA/interrupt enable */ volatile uint32_t SR; /* 0x10 status flags, rc_w0 */ volatile uint32_t EGR; /* 0x14 event generation */ volatile uint32_t CCMR1; /* 0x18 channel 1/2 mode */ volatile uint32_t CCMR2; /* 0x1C channel 3/4 mode */ volatile uint32_t CCER; /* 0x20 channel enable/polarity */ volatile uint32_t CNT; /* 0x24 counter */ volatile uint32_t PSC; /* 0x28 prescaler */ volatile uint32_t ARR; /* 0x2C auto-reload */ volatile uint32_t RCR; /* 0x30 repetition (TIM1/8 only) */ volatile uint32_t CCR1; /* 0x34 */ volatile uint32_t CCR2; /* 0x38 */ volatile uint32_t CCR3; /* 0x3C */ volatile uint32_t CCR4; /* 0x40 */ volatile uint32_t BDTR; /* 0x44 break/dead time (TIM1/8 only) */ volatile uint32_t DCR; /* 0x48 */ volatile uint32_t DMAR; /* 0x4C */ volatile uint32_t OR; /* 0x50 option (TIM2, TIM5) */} TIM_TypeDef;
_Static_assert(offsetof(TIM_TypeDef, SR) == 0x10u, "SR offset");_Static_assert(offsetof(TIM_TypeDef, CCER) == 0x20u, "CCER offset");_Static_assert(offsetof(TIM_TypeDef, CCR1) == 0x34u, "CCR1 offset");
#define TIM2 ((TIM_TypeDef *)0x40000000u)#define TIM3 ((TIM_TypeDef *)0x40000400u)#define TIM4 ((TIM_TypeDef *)0x40000800u)
#define RCC_APB1ENR_TIM2EN (1u << 0)#define RCC_APB1ENR_TIM3EN (1u << 1)#define RCC_APB1ENR_TIM4EN (1u << 2)
#define TIM_CR1_CEN (1u << 0) /* counter enable */#define TIM_CR1_URS (1u << 2) /* only overflow generates update IRQ */#define TIM_CR1_OPM (1u << 3) /* one-pulse mode */#define TIM_CR1_ARPE (1u << 7) /* ARR preload enable */#define TIM_DIER_UIE (1u << 0)#define TIM_DIER_CC1IE (1u << 1)#define TIM_SR_UIF (1u << 0)#define TIM_SR_CC1IF (1u << 1)#define TIM_SR_CC1OF (1u << 9) /* capture overcapture */#define TIM_EGR_UG (1u << 0)
/* CCMR1 output compare fields (channel 1 at bit 0, channel 2 at bit 8) */#define TIM_CCMR_OC1PE (1u << 3)#define TIM_CCMR_OC1M_Pos 4u#define TIM_CCMR_OC2PE (1u << 11)#define TIM_CCMR_OC2M_Pos 12u#define TIM_OCM_TOGGLE 3u#define TIM_OCM_PWM1 6u#define TIM_OCM_PWM2 7u
/* CCER: channel n enable at bit 4(n-1), polarity at bit 4(n-1)+1 */#define TIM_CCER_CCxE(n) (1u << (4u * ((n) - 1u)))#define TIM_CCER_CCxP(n) (1u << (4u * ((n) - 1u) + 1u))
#define TIM2_IRQn 28u#define TIM3_IRQn 29u#define TIM4_IRQn 30u
#endifThe _Static_assert lines check the struct against the reference manual at compile time: if a field is missing or duplicated, the build fails instead of the timer misbehaving. The rest of the bit positions above are from RM0090; the important ones:
| Register | Bit(s) | Name | Meaning |
|---|---|---|---|
CR1 | 0 | CEN | counter runs |
CR1 | 3 | OPM | stop at the next update (one-pulse) |
CR1 | 7 | ARPE | ARR writes go to a shadow, applied at update |
DIER | 0 / 1..4 | UIE / CCxIE | update / channel interrupt enable |
SR | 0 / 1..4 | UIF / CCxIF | update / channel flag |
EGR | 0 | UG | force an update event now |
CCMR1 | 1:0, 9:8 | CC1S, CC2S | 00 output, 01/10 input from TI1/TI2 |
CCMR1 | 6:4, 14:12 | OC1M, OC2M | output mode: 011 toggle, 110 PWM1, 111 PWM2 |
CCMR1 | 3, 11 | OC1PE, OC2PE | CCRx preload enable |
CCER | 0, 4, 8, 12 | CCxE | channel output or capture enabled |
CCER | 1, 5, 9, 13 | CCxP | polarity (active low output / falling edge capture) |
Shadow registers and the UG trick
Section titled “Shadow registers and the UG trick”PSC is always buffered: a new value is written into a preload register and only takes effect at the next update event. ARR is buffered when ARPE is set, and each CCRx when its OCxPE bit is set. Buffering exists so that changing the period or duty cycle mid-period never produces a glitch: the new values are applied cleanly at the wrap.
The consequence surprises everyone once: write PSC = 83 to a freshly reset timer, start it, and the first period runs with a prescaler of 0, because no update event has happened yet. The fix is to force one by writing UG in EGR after configuration. UG also sets UIF, so clear SR afterwards, or that stale flag fires your interrupt immediately.
Clearing status flags
Section titled “Clearing status flags”SR flags are rc_w0: read, clear by writing 0; writing 1 has no effect. That makes the obvious code subtly wrong:
TIM2->SR &= ~TIM_SR_UIF; /* WRONG: read-modify-write */TIM2->SR = ~TIM_SR_UIF; /* right: 0 in UIF, 1 (no effect) everywhere else */The &= version reads SR, clears UIF in the copy and writes it back. If CC1IF became set between the read and the write, the copy has a 0 in that position, and writing 0 clears it: the capture event is silently lost. The plain assignment writes 1 to every other bit, which does nothing, so only UIF is cleared.
Computing PSC and ARR
Section titled “Computing PSC and ARR”Target: a 1 kHz update rate from the 84 MHz timer clock. We need (PSC + 1) × (ARR + 1) = 84 000 000 / 1000 = 84 000.
Many factorizations work. The usual approach is to pick a convenient tick first:
| PSC | Tick | ARR | Period | Comment |
|---|---|---|---|---|
| 83 | 1 µs (1 MHz) | 999 | 1 ms | round numbers, CCR values in microseconds |
| 0 | 11.9 ns | 83 999 | 1 ms | finest resolution; needs 32-bit TIM2/TIM5, ARR > 65535 |
| 8399 | 100 µs | 9 | 1 ms | coarse, only 10 steps of duty cycle |
For PWM, resolution is ARR + 1 steps: ARR = 999 gives 1000 duty levels, ARR = 83 999 gives 84 000. For a periodic interrupt, resolution does not matter, so pick whatever makes the code readable. Not every frequency divides evenly: 84 MHz / 44.1 kHz = 1904.76, so the closest integer period (PSC = 0, ARR = 1904) gives 44.094 kHz, about a 0.01% error. When the error matters, compute it explicitly.
A periodic interrupt with TIM2
Section titled “A periodic interrupt with TIM2”static void tim2_tick_init(void){ RCC->APB1ENR |= RCC_APB1ENR_TIM2EN; (void)RCC->APB1ENR;
TIM2->PSC = 83u; /* 84 MHz / 84 = 1 MHz */ TIM2->ARR = 999u; /* 1 MHz / 1000 = 1 kHz */ TIM2->EGR = TIM_EGR_UG; /* load PSC/ARR now */ TIM2->SR = 0u; /* drop the UIF that UG just set */ TIM2->DIER = TIM_DIER_UIE;
nvic_set_priority(TIM2_IRQn, 6u); nvic_enable(TIM2_IRQn);
TIM2->CR1 = TIM_CR1_CEN;}
void TIM2_IRQHandler(void){ if (TIM2->SR & TIM_SR_UIF) { TIM2->SR = ~TIM_SR_UIF; /* rc_w0: clear only UIF */ (void)TIM2->SR; /* make sure it landed before return */ /* ... 1 ms work ... */ }}The order follows Interrupts done right: clock, configure, force the update, clear the stale flag, enable the peripheral interrupt, then the NVIC. nvic_set_priority and nvic_enable are the helpers from that lesson’s nvic.h. The handler checks UIF because a timer handler is shared by all of that timer’s interrupt sources; once you also enable a capture interrupt, the same handler runs for both.
Pulse-width modulation produces a square wave with a fixed period and a variable high time. The duty cycle is the fraction of the period the output is high. Feed PWM to an LED and the eye averages it into brightness; feed it to a motor driver and the motor’s inductance averages it into current.
In PWM mode 1 (OCxM = 110) while upcounting, the output is active while CNT < CCRx and inactive otherwise:
ARR = 9, CCR1 = 3:
CNT: 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 _______ _______ __ OC1: | |___________| |___________| |<-CCR->| |<------ ARR+1 ---->| duty = CCR1 / (ARR + 1) = 30%CCR = 0 gives a constant low output, CCR = ARR + 1 (or larger) gives constant high. PWM mode 2 (111) is the inverse. The CCxP polarity bit inverts the pin as well; use whichever reads more naturally.
Dimming the Discovery LEDs with TIM4
Section titled “Dimming the Discovery LEDs with TIM4”The four LEDs on PD12 to PD15 are connected to TIM4 channels 1 to 4 when those pins are set to alternate function 2. So the timer can drive them directly, with zero CPU involvement once configured.
static void tim4_pwm_init(void){ RCC->AHB1ENR |= (1u << 3); /* GPIOD */ RCC->APB1ENR |= RCC_APB1ENR_TIM4EN; (void)RCC->APB1ENR;
/* PD12..PD15: AF2 (TIM4_CH1..CH4). AFR[1] covers pins 8..15. */ GPIOD->AFR[1] = (GPIOD->AFR[1] & 0x0000FFFFu) | 0x22220000u; GPIOD->MODER = (GPIOD->MODER & 0x00FFFFFFu) | 0xAA000000u; /* 10 = AF */
TIM4->PSC = 83u; /* 1 MHz tick */ TIM4->ARR = 999u; /* 1 kHz PWM, 1000 steps */
TIM4->CCMR1 = (TIM_OCM_PWM1 << TIM_CCMR_OC1M_Pos) | TIM_CCMR_OC1PE | (TIM_OCM_PWM1 << TIM_CCMR_OC2M_Pos) | TIM_CCMR_OC2PE; TIM4->CCMR2 = (TIM_OCM_PWM1 << TIM_CCMR_OC1M_Pos) | TIM_CCMR_OC1PE /* CH3 */ | (TIM_OCM_PWM1 << TIM_CCMR_OC2M_Pos) | TIM_CCMR_OC2PE; /* CH4 */ TIM4->CCR1 = TIM4->CCR2 = TIM4->CCR3 = TIM4->CCR4 = 0u;
TIM4->CCER = TIM_CCER_CCxE(1) | TIM_CCER_CCxE(2) | TIM_CCER_CCxE(3) | TIM_CCER_CCxE(4); TIM4->CR1 = TIM_CR1_ARPE; TIM4->EGR = TIM_EGR_UG; /* load PSC, ARR, CCRs */ TIM4->CR1 |= TIM_CR1_CEN;}The details that matter:
CCMR2has the same layout asCCMR1, with channel 3 in the low half and channel 4 in the high half, which is why the same macros serve both.- Preload on (
OCxPEandARPE). Without preload, writing a smallerCCRvalue just afterCNThas passed it means the output stays high for the rest of the period and the next one too, a visible flicker. With preload, the new value waits for the wrap. - The pin’s AF number goes in before
MODERswitches to AF, as in GPIO in depth. - A 1 kHz PWM is far above the roughly 100 Hz where the eye notices flicker.
Set brightness with TIM4->CCR1 = 250; for 25% on the green LED. Perceived brightness is roughly logarithmic, so a linear ramp of CCR looks like it brightens quickly and then saturates; squaring the level (a cheap gamma correction) looks much smoother, and the complete example below does exactly that.
Driving a hobby servo
Section titled “Driving a hobby servo”A hobby servo expects a 50 Hz pulse train where the pulse width, 1.0 to 2.0 ms, sets the angle (1.5 ms is centre). With a 1 µs tick that is trivial: PSC = 83, ARR = 19999 (20 ms period), and CCR is the pulse width in microseconds.
/* Servo on TIM3_CH1. PC6 is one option (AF2); check that your board leaves it free. */static void servo_init(void){ RCC->AHB1ENR |= (1u << 2); /* GPIOC */ RCC->APB1ENR |= RCC_APB1ENR_TIM3EN; (void)RCC->APB1ENR; GPIOC->AFR[0] = (GPIOC->AFR[0] & ~(0xFu << 24)) | (2u << 24); /* PC6 AF2 */ GPIOC->MODER = (GPIOC->MODER & ~(3u << 12)) | (2u << 12);
TIM3->PSC = 83u; /* 1 us */ TIM3->ARR = 19999u; /* 20 ms = 50 Hz */ TIM3->CCR1 = 1500u; /* centre */ TIM3->CCMR1 = (TIM_OCM_PWM1 << TIM_CCMR_OC1M_Pos) | TIM_CCMR_OC1PE; TIM3->CCER = TIM_CCER_CCxE(1); TIM3->CR1 = TIM_CR1_ARPE; TIM3->EGR = TIM_EGR_UG; TIM3->CR1 |= TIM_CR1_CEN;}
static void servo_set_us(uint32_t us){ if (us < 1000u) { us = 1000u; } /* never command beyond the */ if (us > 2000u) { us = 2000u; } /* mechanical end stops */ TIM3->CCR1 = us;}The clamp is not decoration. Many servos accept 0.5 to 2.5 ms and will stall against their end stops, drawing high current, if you send a pulse outside their real range. TIM3 is 16-bit, and 19999 fits comfortably.
Output compare: toggle mode
Section titled “Output compare: toggle mode”In toggle mode (OCxM = 011), the output flips every time CNT equals CCRx. One full cycle needs two matches, so the output frequency is half the update rate: f_out = f_timer / (2 × (PSC + 1) × (ARR + 1)). With PSC = 83 and ARR = 499, TIM4 channel 1 toggles PD12 every 500 µs, producing a 1 kHz square wave at exactly 50% duty.
Output compare is also the tool for precisely timed single events: set CCRx = CNT + delay from software and enable CCxIE; the hardware changes the pin and raises the interrupt at exactly that count, with no software latency on the pin edge itself. With several channels of a free-running 32-bit counter, you get several independent precise timeouts from one timer.
Input capture: measuring signals
Section titled “Input capture: measuring signals”In capture mode, an edge on the channel’s input pin copies CNT into CCRx and sets CCxIF. Two captures give you an interval measured by hardware, with a resolution of one timer tick and no dependence on interrupt latency. If a second capture arrives before software has read the first, the overcapture flag CCxOF is set: you lost a measurement.
Period and duty with PWM input mode
Section titled “Period and duty with PWM input mode”STM32 timers can wire one input pin to two channels, capturing the rising edge in one and the falling edge in the other, and reset the counter on every rising edge. Then CCR1 holds the period and CCR2 the high time, continuously, with no interrupts at all:
/* Measure a signal on PA15 (TIM2_CH1, AF1). PA15 is a JTAG pin, free when debugging over SWD. 1 us resolution, periods up to ~71 minutes. */static void tim2_pwm_input_init(void){ RCC->AHB1ENR |= (1u << 0); /* GPIOA */ RCC->APB1ENR |= RCC_APB1ENR_TIM2EN; (void)RCC->APB1ENR; GPIOA->AFR[1] = (GPIOA->AFR[1] & ~(0xFu << 28)) | (1u << 28); /* PA15 AF1 */ GPIOA->MODER = (GPIOA->MODER & ~(3u << 30)) | (2u << 30);
TIM2->PSC = 83u; TIM2->ARR = 0xFFFFFFFFu; /* full 32-bit range */ TIM2->CCMR1 = (1u << 0) /* CC1S = 01: IC1 <- TI1 */ | (2u << 8); /* CC2S = 10: IC2 <- TI1 */ TIM2->CCER = TIM_CCER_CCxE(1) /* CH1: rising edge */ | TIM_CCER_CCxE(2) | TIM_CCER_CCxP(2); /* CH2: falling */ TIM2->SMCR = (5u << 4) /* TS = 101: trigger = TI1FP1 */ | (4u << 0); /* SMS = 100: reset mode */ TIM2->EGR = TIM_EGR_UG; TIM2->CR1 = TIM_CR1_CEN;}
/* period_us = TIM2->CCR1; high_us = TIM2->CCR2; */Every rising edge captures the count into CCR1 (the time since the previous rising edge) and then resets the counter; the falling edge captures the high time into CCR2. Jumper PD12 (running the TIM4 PWM) to PA15 and you can read back exactly the period and duty you configured: 1000 µs and the current CCR1 of TIM4. The two registers are read separately, so they can come from adjacent periods; for a display that does not matter.
Overflow handling on a 16-bit timer
Section titled “Overflow handling on a 16-bit timer”TIM2’s 32-bit counter at 1 MHz wraps every 71 minutes, so subtracting two captures as uint32_t is wrap-safe for any interval shorter than that, the same modular trick as millis(). On a 16-bit timer at 1 MHz, the counter wraps every 65.5 ms, and intervals longer than that need the overflows counted. The subtle part is when the capture and the overflow happen close together and both flags are pending in the same interrupt: did the capture happen just before the wrap or just after?
static volatile uint32_t s_tim3_overflows;static volatile uint32_t s_period_ticks;
void TIM3_IRQHandler(void){ uint32_t sr = TIM3->SR; uint32_t ovf = s_tim3_overflows; static uint32_t last;
if (sr & TIM_SR_CC1IF) { uint32_t cap = TIM3->CCR1; /* reading CCR1 clears CC1IF */ /* Both pending: a small capture value means it happened after the wrap. */ if ((sr & TIM_SR_UIF) && cap < 0x8000u) { ovf++; } uint32_t stamp = (ovf << 16) | cap; /* extended 32-bit timestamp */ s_period_ticks = stamp - last; /* wrap-safe */ last = stamp; } if (sr & TIM_SR_UIF) { TIM3->SR = ~TIM_SR_UIF; s_tim3_overflows++; }}SR is read once, so both decisions are based on the same snapshot. If UIF is pending alongside the capture, the capture value tells us the order: a value near zero was captured after the counter wrapped (the overflow count must include that wrap), and a value near the top was captured before it. If UIF becomes set after the SR read, the wrap happened after the capture and is correctly counted on the next interrupt. This assumes ARR = 0xFFFF and that the interrupt is serviced within half a timer period.
More timer modes in brief
Section titled “More timer modes in brief”Encoder interface. Set SMS = 011 in SMCR and map CH1 and CH2 to TI1 and TI2 (CC1S = 01, CC2S = 01). The counter then counts up or down by itself on every edge of a quadrature encoder’s A and B signals: four counts per encoder cycle, direction decoded in hardware. Read the position as int16_t pos = (int16_t)TIM3->CNT; and compute motion as the difference of two int16_t readings, which is wrap-safe as long as you sample faster than 32768 counts. The input filter bits (ICxF in CCMR1) reject contact bounce on mechanical encoders.
One-pulse mode. With OPM set in CR1, the counter stops (clears CEN) at the next update event. Combine it with PWM mode 2, CCR1 = delay and ARR = delay + width - 1, and starting the counter (by software or by a trigger input in slave trigger mode) produces exactly one pulse after a precise delay. Used for triggering cameras, ultrasonic sensors and ignition.
Chaining and triggering. A timer can output a trigger signal, TRGO, selected by MMS in CR2 (for example MMS = 010: TRGO on every update). Other timers can use it as a clock or start trigger through their SMCR (the internal trigger inputs ITRx, whose mapping is a table in the reference manual), turning two 16-bit timers into a 32-bit one or starting several timers in sync. More importantly, the ADC can start conversions on a timer’s TRGO, and DMA requests can be generated on update or compare events. That gives perfectly regular sampling with no CPU involvement; ADC, DAC, and analog basics and DMA build on it.
Choosing a timer
Section titled “Choosing a timer”| Need | Use | Why |
|---|---|---|
OS tick, millis() | SysTick | in every Cortex-M core, portable, exactly what RTOSes expect |
| Precise periodic interrupt | TIM6/TIM7 or any general timer | dedicated, configurable priority, does not disturb SysTick |
| PWM on pins | TIM2 to TIM5, TIM9 to TIM14 | hardware output, zero jitter |
| Motor control with complementary outputs | TIM1, TIM8 | dead-time insertion, emergency break input |
| Measuring pulses or frequency | TIM2/TIM5 input capture | 32-bit range avoids overflow handling |
| Wall-clock time, alarms, wake from Standby | RTC | runs from the 32.768 kHz LSE, survives sleep with a coin cell on VBAT |
| Timed wake from deep sleep at low power | low-power timer (LPTIM) | not on the F407; found on STM32L4, G4, F7 and others |
Software timers
Section titled “Software timers”A product needs many timeouts: LED blink period, debounce interval, sensor poll, UART receive timeout, watchdog-ish supervision. One hardware timer per timeout does not scale. The standard pattern is one hardware tick and a list of software timers, each with a deadline and a callback.
#ifndef SWTIMER_H#define SWTIMER_H
#include <stdbool.h>#include <stdint.h>
typedef void (*swtimer_cb_t)(void *ctx);
typedef struct swtimer { uint32_t deadline; /* absolute tick at which to fire */ uint32_t period; /* 0 = one-shot */ swtimer_cb_t cb; void *ctx; bool active; bool linked; /* already in the list */ struct swtimer *next;} swtimer_t;
void swtimer_start(swtimer_t *t, uint32_t delay, uint32_t period, swtimer_cb_t cb, void *ctx);void swtimer_stop(swtimer_t *t);void swtimer_process(uint32_t now); /* call from the main loop */
#endif#include <stddef.h>#include "swtimer.h"
extern uint32_t tick_now(void); /* provided by the application */
static swtimer_t *s_head;
void swtimer_start(swtimer_t *t, uint32_t delay, uint32_t period, swtimer_cb_t cb, void *ctx){ t->deadline = tick_now() + delay; t->period = period; t->cb = cb; t->ctx = ctx; t->active = true; if (!t->linked) { t->next = s_head; s_head = t; t->linked = true; }}
void swtimer_stop(swtimer_t *t){ t->active = false; /* stays linked; cheap to restart */}
void swtimer_process(uint32_t now){ for (swtimer_t *t = s_head; t != NULL; t = t->next) { if (!t->active || (int32_t)(now - t->deadline) < 0) { continue; } if (t->period != 0u) { t->deadline += t->period; /* drift-free: next slot on the grid */ } else { t->active = false; } t->cb(t->ctx); /* may restart or stop timers */ }}Design notes:
- Timers are caller-allocated (
static swtimer_t blink;in the module that owns it). No heap, no fixed pool size to tune, and each module owns its timers. swtimer_processruns in the main loop, not in the tick ISR. Callbacks therefore run in normal context: they can take as long as they need, call non-reentrant code, and the list needs no locking because only main touches it. The ISR only increments a counter. Running callbacks inside the ISR is a design some systems use, but then every callback inherits all the restrictions of Interrupts done right.- Wraparound-safe comparison
(int32_t)(now - deadline) < 0, from Clocks, reset, and timing. deadline += periodkeeps periodic timers on their original grid even if the main loop is late; a late callback is followed by an on-time one, not by a permanently shifted one.- The list is unsorted, so processing is O(n) per tick. For a few dozen timers that costs microseconds. Systems with hundreds of timers keep the list sorted by deadline or use a hierarchical timer wheel, which is what the Linux kernel and many RTOSes do.
Accuracy, jitter, and drift
Section titled “Accuracy, jitter, and drift”Three different things limit timing quality.
Clock accuracy sets long-term drift. A timer is exactly as accurate as its clock source. The 8 MHz crystal on the Discovery is specified to a few tens of ppm; 20 ppm is 1.7 seconds per day. HSI at 1% is 14 minutes per day. Crystals also drift with temperature and age. If you need a clock that keeps time for months, use the RTC with a 32.768 kHz watch crystal and calibrate it, or synchronise to an external reference (GPS, network time).
Quantization is the tick size. With a 1 µs tick, every interval is rounded to a whole microsecond. With a 1 ms software tick, a timeout of “10 ms” fires between 9 and 10 ms after it was started, depending on where in the current tick it began.
Jitter is the variation from one event to the next. Hardware outputs (PWM, output compare) have essentially no jitter: the pin changes on an exact counter value. Anything done by software in response to a timer interrupt has the interrupt latency variation added: other interrupts, critical sections, flash wait states. Software timers processed from the main loop add the main loop’s own variation on top, which can be milliseconds. The rule: if the edge timing matters, let the hardware produce the edge; if only the average rate matters, software is fine.
The complete example
Section titled “The complete example”The program below runs three things at once with the CPU almost idle. TIM4 PWM “breathes” the four Discovery LEDs, each a quarter cycle out of phase. TIM2 provides a 1 kHz tick that drives the software timers. One software timer updates the brightness every 10 ms; another is a one-shot that turns off the breathing after 30 seconds and leaves the blue LED on. It uses clock.c from Clocks, reset, and timing, nvic.h from Interrupts done right, and tim.h and swtimer.c from this lesson.
#include <stdbool.h>#include <stdint.h>#include "stm32f407_regs.h"#include "clock.h"#include "nvic.h"#include "tim.h"#include "swtimer.h"
static volatile uint32_t s_ticks; /* 1 kHz, incremented by TIM2 */
uint32_t tick_now(void){ return s_ticks;}
void TIM2_IRQHandler(void){ if (TIM2->SR & TIM_SR_UIF) { TIM2->SR = ~TIM_SR_UIF; (void)TIM2->SR; s_ticks++; }}
static void tim2_tick_init(void){ RCC->APB1ENR |= RCC_APB1ENR_TIM2EN; (void)RCC->APB1ENR; TIM2->PSC = 83u; TIM2->ARR = 999u; TIM2->EGR = TIM_EGR_UG; TIM2->SR = 0u; TIM2->DIER = TIM_DIER_UIE; nvic_set_priority(TIM2_IRQn, 6u); nvic_enable(TIM2_IRQn); TIM2->CR1 = TIM_CR1_CEN;}
static void tim4_pwm_init(void){ RCC->AHB1ENR |= (1u << 3); RCC->APB1ENR |= RCC_APB1ENR_TIM4EN; (void)RCC->APB1ENR; GPIOD->AFR[1] = (GPIOD->AFR[1] & 0x0000FFFFu) | 0x22220000u; GPIOD->MODER = (GPIOD->MODER & 0x00FFFFFFu) | 0xAA000000u;
TIM4->PSC = 83u; TIM4->ARR = 999u; TIM4->CCMR1 = (TIM_OCM_PWM1 << TIM_CCMR_OC1M_Pos) | TIM_CCMR_OC1PE | (TIM_OCM_PWM1 << TIM_CCMR_OC2M_Pos) | TIM_CCMR_OC2PE; TIM4->CCMR2 = (TIM_OCM_PWM1 << TIM_CCMR_OC1M_Pos) | TIM_CCMR_OC1PE | (TIM_OCM_PWM1 << TIM_CCMR_OC2M_Pos) | TIM_CCMR_OC2PE; TIM4->CCER = TIM_CCER_CCxE(1) | TIM_CCER_CCxE(2) | TIM_CCER_CCxE(3) | TIM_CCER_CCxE(4); TIM4->CR1 = TIM_CR1_ARPE; TIM4->EGR = TIM_EGR_UG; TIM4->CR1 |= TIM_CR1_CEN;}
/* Triangle wave 0..1000..0 over 2000 steps, squared for perceived linearity. */static uint32_t breathe(uint32_t phase){ uint32_t p = phase % 2000u; uint32_t lin = (p < 1000u) ? p : 2000u - p; /* 0..1000 */ return (lin * lin) / 1000u; /* 0..1000 */}
static uint32_t s_phase;
static void on_breathe(void *ctx){ (void)ctx; s_phase = (s_phase + 10u) % 2000u; /* full cycle every 2 s */ TIM4->CCR1 = breathe(s_phase); TIM4->CCR2 = breathe(s_phase + 500u); TIM4->CCR3 = breathe(s_phase + 1000u); TIM4->CCR4 = breathe(s_phase + 1500u);}
static swtimer_t s_breathe_timer;static swtimer_t s_stop_timer;
static void on_stop(void *ctx){ (void)ctx; swtimer_stop(&s_breathe_timer); TIM4->CCR1 = TIM4->CCR2 = TIM4->CCR3 = 0u; TIM4->CCR4 = TIM4->ARR + 1u; /* blue fully on */}
int main(void){ (void)clock_init_168mhz(); /* timers now at 84 MHz */ tim4_pwm_init(); tim2_tick_init();
swtimer_start(&s_breathe_timer, 10u, 10u, on_breathe, NULL); swtimer_start(&s_stop_timer, 30000u, 0u, on_stop, NULL);
for (;;) { swtimer_process(tick_now()); __asm volatile ("wfi"); /* TIM2 wakes us every 1 ms */ }}What to observe:
- The CPU only works once per millisecond, for a few hundred cycles. Between ticks it sleeps in
WFI. The LED waveforms are produced entirely by TIM4. breathe()uses integer arithmetic only.lin * linis at most 1 000 000, well inside 32 bits. Squaring makes the fade look even to the eye.- Phase offsets of 500 steps put each LED a quarter cycle apart, so the light appears to rotate around the board.
TIM4->CCR4 = TIM4->ARR + 1uis the 100% duty case:CNTis always less thanCCR4, so the output never goes inactive.NULLcontexts. Each callback here is specific to one timer, soctxis unused. A generic “blink this LED” callback would receive a pointer to an LED descriptor instead.
If you have a scope, probe PD12: a 1 kHz square wave whose duty cycle sweeps smoothly. Under Renode, the LED state reflects the timer output only as far as the timer model implements PWM, so verify duty-cycle behaviour on hardware.
Common mistakes
Section titled “Common mistakes”- Assuming the APB1 timer clock is 42 MHz. Fix: with APB1 /4, APB1 timers run at 84 MHz.
- Forgetting that
PSConly loads at an update event, so the first period is wrong. Fix: writeEGR = UGafter configuring, then clearSR. - Clearing a flag with
TIMx->SR &= ~FLAG, losing other flags. Fix:TIMx->SR = ~FLAG. - Enabling
UIEbeforeUG, so the forced update fires an interrupt immediately. Fix:UG, clearSR, then enableUIE. - Updating
CCRwithout preload and seeing glitches. Fix: setOCxPEandARPE. - Forgetting the GPIO alternate function or using the wrong AF number. Fix: check the datasheet AF table; TIM4 on PD12 to PD15 is AF2, TIM2 on PA15 is AF1.
- Measuring long intervals with a 16-bit timer and no overflow handling. Fix: use TIM2/TIM5, or extend with an overflow counter as shown.
- On TIM1/TIM8, configuring everything and seeing no output. Fix: set
MOEinBDTR.
Exercises
Section titled “Exercises”- Compute
PSCandARRfor a 20 kHz PWM on TIM4 (inaudible for motors) with the maximum possible duty resolution. How many duty steps do you get? (Answer:PSC = 0,ARR = 4199, 4200 steps.) - Configure TIM4 channel 1 in toggle mode to produce a 440 Hz square wave on PD12 (the green LED will look dimly lit; listen with a small piezo buzzer if you have one). Show your arithmetic and the frequency error.
- Jumper PD12 to PA15, run the TIM4 PWM and the PWM input capture together, and print or inspect
TIM2->CCR1andTIM2->CCR2for several duty settings. Explain the reading whenTIM4->CCR1 = 0. - Compile
swtimer.cwith-O2 -fstack-usageand inspect the.sufile and the disassembly ofswtimer_process. How many bytes of stack does it use, and is the callback called withblx? Then make the list sorted by deadline soswtimer_processcan stop at the first timer not yet due. - Replace the servo’s 1 µs resolution with the finest possible: choose the smallest
PSCfor which anARRthat fits in 16 bits gives exactly 50 Hz from the 84 MHz timer clock. What arePSCandARR, and what is the pulse width resolution? (Answer:PSC = 27,ARR = 59999, about 333 ns.)
Check yourself
Section titled “Check yourself”Question 1:TIM2 runs from an 84 MHz clock with PSC = 41 and ARR = 1999. What is the update frequency?
84 MHz / (41 + 1) = 2 MHz tick, and 2 MHz / (1999 + 1) = 1 kHz.
Question 2:An ISR clears the update flag with TIM2->SR &= ~TIM_SR_UIF;. Input capture on channel 1 occasionally misses events. Why?
Status bits are rc_w0. Writing back a stale 0 clears a flag that became set in between. TIM2->SR = ~TIM_SR_UIF; only clears UIF.
Question 3:You configure PSC = 83 on a freshly reset timer and start it without writing EGR. What is the first period?
PSC is always buffered. Writing UG forces the update that transfers it, which is why initialization code writes UG.
Question 4:In PWM mode 1 with ARR = 999, which CCR1 value gives a constant high output?
The output is active while CNT < CCR1. CNT never exceeds 999, so CCR1 = 1000 keeps it active for the whole period; 999 leaves one tick low.
Question 5:Which approach gives the lowest jitter for a 1 kHz output square wave?
Hardware output compare changes the pin on an exact counter value, unaffected by interrupt latency or main loop timing.
Key takeaways
Section titled “Key takeaways”f_update = f_timer / ((PSC + 1) × (ARR + 1)), and the APB1 timers run at 84 MHz in the standard 168 MHz setup.PSC(andARR/CCRwith preload) only take effect at an update event; writeUGafter configuring, then clearSR.- Timer status flags are rc_w0: clear with
SR = ~FLAG, never&=. - PWM mode 1 holds the output active while
CNT < CCR; duty isCCR / (ARR + 1). Enable preload to avoid glitches. - Input capture timestamps edges in hardware. Use 32-bit TIM2/TIM5 to avoid overflow handling, or extend 16-bit timers carefully.
- One hardware tick plus a list of software timers serves any number of timeouts; run the callbacks in the main loop.
- Let hardware produce edges whose timing matters; software adds interrupt latency and main loop jitter.
Next: UART: serial communication puts these timing ideas to work on the most common link between a microcontroller and the outside world.