← Back to Embedded C series
🔌
STM32
STM32 · Prereq: lesson 15 · STM32 · Prereq: lesson 16

16–17. SysTick Timer and Precise Delays · General-Purpose Timer · PWM Output

An empty-loop delay() is imprecise. The Cortex-M core's built-in SysTick timer solves it — a 24-bit counter present in every Cortex-M, so the same code runs across chips. The core pattern: "interrupt every 1ms → bump a global millisecond counter → measure time from it". This one pattern covers precise delays, periodic execution, and timeouts. We also build a delay_ms safe against wrap using unsigned subtraction. General-purpose timers (TIM) add channels·PWM·input capture. PWM is the most used — rapidly adjusting the on-time ratio (duty) controls LED brightness or motor speed "like analog". We make 1kHz PWM on TIM2 channel1 (PA0) and adjust the duty. Understand the three core registers PSC(prescale)·ARR(period)·CCR(duty), and the trap that an APB1 timer clock is 2× PCLK1.

SysTickdelay_msinterruptvolatilemillisecondwraptimerPWMPSCARRCCRduty
Duration
⏱ ~1.5 hours + ~1.5–2 hours
Level
📊 Intermediate
Prerequisite
🎯 Lesson 15
OUTCOME
Configure a 1ms interrupt with SysTick_Config and build a wrap-safe delay_ms from a volatile millisecond counter. / Compute and set PWM frequency and duty from PSC/ARR/CCR, and output TIM2 channel1 PWM via an AF push-pull pin.

What you'll learn

  • 1Understand SysTick's structure (24-bit down counter) and operation
  • 2Set up a 1ms periodic interrupt with SysTick_Config()
  • 3Increment the millisecond counter in SysTick_Handler
  • 4Build a wrap-safe delay_ms() with unsigned subtraction
  • 5Know SysTick's 24-bit limit and reload calculation
  • 6Compute the timer time base (PSC·ARR) and counter clock
  • 7Understand PWM mode1 (CNT vs CCR compare)
  • 8Configure TIM2 channel1 to output PWM
  • 9Adjust duty via CCR and frequency via ARR
  • 10Know the rule that APB1 timer clock is 2× PCLK1

16. SysTick Timer and Precise Delays

An empty-loop delay() is imprecise. The Cortex-M core's built-in SysTick timer solves it — a 24-bit counter present in every Cortex-M, so the same code runs across chips. The core pattern: "interrupt every 1ms → bump a global millisecond counter → measure time from it". This one pattern covers precise delays, periodic execution, and timeouts. We also build a delay_ms safe against wrap using unsigned subtraction.

Introduction

SysTick counts from a reload value down to 0, reloads, and can raise an interrupt (SysTick exception). Built into the core, it works the same on any Cortex-M.

Key concepts

1) SysTick_Config and the handler

c
SysTick_Config(SystemCoreClock / 1000u);   /* interrupt every 1ms (72MHz→72000) */

static volatile uint32_t g_ms = 0;
void SysTick_Handler(void) { g_ms++; }   /* handler name is a CMSIS convention */
⚠️

g_ms must be volatile — shared by the interrupt and main, so prevent caching. The handler name must match the startup vector table exactly (SysTick_Handler).

2) Wrap-safe delay_ms

c
static void delay_ms(uint32_t ms) {
    uint32_t start = g_ms;
    while ((g_ms - start) < ms) { }
}
/* unsigned subtraction → correct elapsed even after g_ms wraps past 32-bit */

3) 24-bit limit

SysTick is 24-bit, max reload 0xFFFFFF. At 72MHz one period is at most ~233ms. Longer periods are made by counting 1ms ticks.

Core example

c
#include "stm32f10x.h"
#define LED 13u
static volatile uint32_t g_ms = 0;
void SysTick_Handler(void){ g_ms++; }
static void delay_ms(uint32_t ms){ uint32_t s=g_ms; while((g_ms-s)<ms){} }
int main(void){
    RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;
    GPIOC->CRH &= ~(0xFu << ((LED-8u)*4u));
    GPIOC->CRH |=  (0x2u << ((LED-8u)*4u));
    SysTick_Config(SystemCoreClock / 1000u);   /* 1ms tick */
    while (1) { GPIOC->ODR ^= (1u << LED); delay_ms(500u); }  /* exactly 500ms */
}

Wrap check: start=0xFFFFFFFF, now=4 → elapsed=5. start=0xFFFFFF00, now=0x100 → elapsed=512. Even after the counter wraps, unsigned subtraction gives the correct elapsed time.

Common mistakes

Q. g_ms doesn't change or vanishes from optimization.

A. You dropped volatile. A variable shared between an interrupt handler and main must be volatile.

Q. I renamed SysTick_Handler and the interrupt stopped.

A. The handler name must match the startup vector table exactly. The CMSIS standard name is SysTick_Handler.

Q. I can't make a 1-second period from SysTick alone.

A. The 24-bit limit caps a single period at ~233ms at 72MHz. Make long times by counting 1ms ticks (g_ms).

Summary

  • SysTick is a 24-bit down counter in every Cortex-M — great portability
  • SysTick_Config(SystemCoreClock/1000) makes a 1ms periodic interrupt
  • Increment a volatile millisecond counter in the handler
  • delay_ms is wrap-safe via unsigned subtraction
  • Exceed the 24-bit limit (~233ms at 72MHz) by counting ticks

Exercises

  1. Write a non-blocking blink that toggles when 200ms has elapsed using only g_ms
  2. Use g_ms for periodic execution that increments a count every second
  3. Verify elapsed(now, start) is correct in wrap cases via pc_test

17. General-Purpose Timer · PWM Output

General-purpose timers (TIM) add channels·PWM·input capture. PWM is the most used — rapidly adjusting the on-time ratio (duty) controls LED brightness or motor speed "like analog". We make 1kHz PWM on TIM2 channel1 (PA0) and adjust the duty. Understand the three core registers PSC(prescale)·ARR(period)·CCR(duty), and the trap that an APB1 timer clock is 2× PCLK1.

Introduction

A digital pin outputs only 0/1, but rapidly changing the on-ratio changes the average voltage, acting analog. Timer output is a GPIO alternate function (AF), so PA0 must be AF push-pull (0xB).

Key concepts

1) Time base — PSC·ARR·CCR

c
counter_clk = TIMxCLK / (PSC + 1)
pwm_freq    = counter_clk / (ARR + 1)
duty(%)     = CCR * 100 / (ARR + 1)
/* PSC=71 → 72MHz/72 = 1MHz, ARR=999 → 1kHz, CCR=250 → 25% */
⚠️

Timer-clock trap: TIM2~4 are on APB1, but if the APB1 prescaler isn't 1, the timer clock is 2× PCLK1. In the standard 72MHz config PCLK1=36MHz but TIM2 clock is 72MHz.

2) PWM mode1 and setup order

PWM mode1: output High while CNT < CCR. Order: clock → pin(AF) → time base(PSC/ARR) → duty(CCR) → mode(CCMR1 OC1M=110+OC1PE) → output(CCER CC1E) → UG(force load) → CEN(start).

c
TIM2->CCMR1 |= TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_1;  /* PWM1 (110) */
TIM2->CCMR1 |= TIM_CCMR1_OC1PE;  TIM2->CCER |= TIM_CCER_CC1E;
TIM2->CR1 |= TIM_CR1_ARPE;  TIM2->EGR |= TIM_EGR_UG;  TIM2->CR1 |= TIM_CR1_CEN;

Core example

c
#include "stm32f10x.h"
int main(void){
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
    RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
    GPIOA->CRL &= ~0xFu; GPIOA->CRL |= 0xBu;   /* PA0 = AF push-pull */
    TIM2->PSC = 71u; TIM2->ARR = 999u; TIM2->CCR1 = 250u;  /* 1kHz, 25% */
    TIM2->CCMR1 |= TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1PE;
    TIM2->CCER |= TIM_CCER_CC1E;
    TIM2->CR1 |= TIM_CR1_ARPE; TIM2->EGR |= TIM_EGR_UG; TIM2->CR1 |= TIM_CR1_CEN;
    while (1) { /* changing CCR1 changes duty instantly */ }
}

Calc check: PSC=71,ARR=999,CCR=250 → 1000Hz·25%. CCR=500 → 50%. ARR=499 → 2000Hz. Add PORTA.0 to the Logic Analyzer to check the duty ratio.

Common mistakes

Q. No PWM comes out of the pin.

A. Check ① PA0 set to AF push-pull (0xB), ② output enabled via CCER's CC1E, ③ counter started via CR1's CEN.

Q. The frequency is half/double my calculation.

A. Wrong timer clock. If the APB1 prescaler isn't 1, TIM2 clock is 2× PCLK1 (72MHz). Calculating with 36MHz is off by 2×.

Q. A config change isn't reflected in the first period.

A. Forcing an update event via EGR's UG loads PSC/ARR/CCR immediately. Skip it and the old values run until the next update.

Summary

  • Timer: PSC sets counter clock, ARR the period, CCR the duty
  • PWM mode1 is High while CNT < CCR → duty = CCR/(ARR+1)
  • Timer output goes out an AF pin (PA0=0xB)
  • Order: clock → pin → time base → mode/output → UG → CEN
  • An APB1 timer clock is 2× PCLK1 (72MHz) — beware

Exercises

  1. Make a breathing LED by sweeping duty 0→100%→0
  2. Compute PSC/ARR to change the PWM frequency to 500Hz
  3. Show that CCR ≥ ARR+1 makes output always High (100%)
Example code / lecture materials

All lecture materials and example code (with homework and answers) are openly available on GitHub.

View on GitHub ↗

Comments 0

No comments yet. Be the first.

Only members can comment.

Comments with ads, abuse, personal information or illegal content are hidden or removed. Comments are shown with your nickname. Privacy Policy