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

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.

SysTickdelay_msinterruptvolatilemillisecondwrap
Duration
~1.5 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.

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

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
Example code / lecture materials

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

View on GitHub ↗