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.
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
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
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
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
#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
- Write a non-blocking blink that toggles when 200ms has elapsed using only g_ms
- Use g_ms for periodic execution that increments a count every second
- Verify elapsed(now, start) is correct in wrap cases via pc_test
All lecture materials and example code (with homework and answers) are openly available on GitHub.
View on GitHub ↗