Skip to content

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.

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 by PSC + 1, so PSC = 0 means 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 to ARR and wraps to 0.
  • Auto-reload register (ARR) sets the top of the count. The counter period is ARR + 1 ticks.
  • Update event happens when the counter wraps. It sets the UIF flag, 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 CNT with CCRx every tick and can change an output pin on a match. In capture mode, an edge on the pin copies the current CNT into CCRx.

The fundamental formula:

f_update = f_timer / ((PSC + 1) * (ARR + 1))

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.

TimersTypeCounterChannelsNotes
TIM2, TIM5general purpose32-bit4best for long intervals and input capture
TIM3, TIM4general purpose16-bit4TIM4 drives the Discovery LEDs
TIM9 to TIM14general purpose16-bit1 or 2simpler feature set
TIM6, TIM7basic16-bit0time base and DAC trigger only
TIM1, TIM8advanced16-bit4 + complementarydead time, break input, motor control; outputs need BDTR.MOE set

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:

tim.h
#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
#endif

The _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:

RegisterBit(s)NameMeaning
CR10CENcounter runs
CR13OPMstop at the next update (one-pulse)
CR17ARPEARR writes go to a shadow, applied at update
DIER0 / 1..4UIE / CCxIEupdate / channel interrupt enable
SR0 / 1..4UIF / CCxIFupdate / channel flag
EGR0UGforce an update event now
CCMR11:0, 9:8CC1S, CC2S00 output, 01/10 input from TI1/TI2
CCMR16:4, 14:12OC1M, OC2Moutput mode: 011 toggle, 110 PWM1, 111 PWM2
CCMR13, 11OC1PE, OC2PECCRx preload enable
CCER0, 4, 8, 12CCxEchannel output or capture enabled
CCER1, 5, 9, 13CCxPpolarity (active low output / falling edge capture)

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.

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.

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:

PSCTickARRPeriodComment
831 µs (1 MHz)9991 msround numbers, CCR values in microseconds
011.9 ns83 9991 msfinest resolution; needs 32-bit TIM2/TIM5, ARR > 65535
8399100 µs91 mscoarse, 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.

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.

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:

  • CCMR2 has the same layout as CCMR1, with channel 3 in the low half and channel 4 in the high half, which is why the same macros serve both.
  • Preload on (OCxPE and ARPE). Without preload, writing a smaller CCR value just after CNT has 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 MODER switches 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.

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.

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.

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.

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.

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.

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.

NeedUseWhy
OS tick, millis()SysTickin every Cortex-M core, portable, exactly what RTOSes expect
Precise periodic interruptTIM6/TIM7 or any general timerdedicated, configurable priority, does not disturb SysTick
PWM on pinsTIM2 to TIM5, TIM9 to TIM14hardware output, zero jitter
Motor control with complementary outputsTIM1, TIM8dead-time insertion, emergency break input
Measuring pulses or frequencyTIM2/TIM5 input capture32-bit range avoids overflow handling
Wall-clock time, alarms, wake from StandbyRTCruns from the 32.768 kHz LSE, survives sleep with a coin cell on VBAT
Timed wake from deep sleep at low powerlow-power timer (LPTIM)not on the F407; found on STM32L4, G4, F7 and others

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.

swtimer.h
#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
swtimer.c
#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_process runs 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 += period keeps 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.

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 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.

main.c
#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 * lin is 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 + 1u is the 100% duty case: CNT is always less than CCR4, so the output never goes inactive.
  • NULL contexts. Each callback here is specific to one timer, so ctx is 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.

  • Assuming the APB1 timer clock is 42 MHz. Fix: with APB1 /4, APB1 timers run at 84 MHz.
  • Forgetting that PSC only loads at an update event, so the first period is wrong. Fix: write EGR = UG after configuring, then clear SR.
  • Clearing a flag with TIMx->SR &= ~FLAG, losing other flags. Fix: TIMx->SR = ~FLAG.
  • Enabling UIE before UG, so the forced update fires an interrupt immediately. Fix: UG, clear SR, then enable UIE.
  • Updating CCR without preload and seeing glitches. Fix: set OCxPE and ARPE.
  • 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 MOE in BDTR.
  1. Compute PSC and ARR for 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.)
  2. 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.
  3. Jumper PD12 to PA15, run the TIM4 PWM and the PWM input capture together, and print or inspect TIM2->CCR1 and TIM2->CCR2 for several duty settings. Explain the reading when TIM4->CCR1 = 0.
  4. Compile swtimer.c with -O2 -fstack-usage and inspect the .su file and the disassembly of swtimer_process. How many bytes of stack does it use, and is the callback called with blx? Then make the list sorted by deadline so swtimer_process can stop at the first timer not yet due.
  5. Replace the servo’s 1 µs resolution with the finest possible: choose the smallest PSC for which an ARR that fits in 16 bits gives exactly 50 Hz from the 84 MHz timer clock. What are PSC and ARR, and what is the pulse width resolution? (Answer: PSC = 27, ARR = 59999, about 333 ns.)

Question 1:TIM2 runs from an 84 MHz clock with PSC = 41 and ARR = 1999. What is the update frequency?

Question 2:An ISR clears the update flag with TIM2->SR &= ~TIM_SR_UIF;. Input capture on channel 1 occasionally misses events. Why?

Question 3:You configure PSC = 83 on a freshly reset timer and start it without writing EGR. What is the first period?

Question 4:In PWM mode 1 with ARR = 999, which CCR1 value gives a constant high output?

Question 5:Which approach gives the lowest jitter for a 1 kHz output square wave?

  • f_update = f_timer / ((PSC + 1) × (ARR + 1)), and the APB1 timers run at 84 MHz in the standard 168 MHz setup.
  • PSC (and ARR/CCR with preload) only take effect at an update event; write UG after configuring, then clear SR.
  • Timer status flags are rc_w0: clear with SR = ~FLAG, never &=.
  • PWM mode 1 holds the output active while CNT < CCR; duty is CCR / (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.