So far, the delay() we have used loops in an empty loop way, so the timing is inaccurate. The actual delay varies depending on compiler optimization, clock speed, and interrupts. This time, we solve this with the SysTick timer built into the Cortex-M core. Since SysTick is a 24-bit counter present in all Cortex-Ms, we also get portability, allowing the same code to work even if the chip changes.
The core pattern is 'interrupt every 1ms → increase global millisecond counter → measure time with that counter'. With this single pattern, you can implement accurate delays, periodic execution, and timeouts.
Learning Objectives
- Understand the structure (24-bit down counter) and operation of SysTick.
- Set up a 1ms period interrupt using
SysTick_Config(). - Increase the millisecond counter with
SysTick_Handler. - Create
delay_ms()safe for wrapping with unsigned subtraction. - Know the 24-bit limit of SysTick and reload calculation.
Target Environment
- Compiler: Keil MDK-ARM (Arm Compiler 6).
- Device: STM32F103C8 (μVision simulator).
- Verification: μVision simulator (Watch
g_ms, Logic Analyzer PC13). Time calculation is PCgcc(pc_test.c).
Key Concepts
1) What is SysTick
SysTick is a 24-bit down counter inside the Cortex-M core. It counts down from the reload value to 0, and when it reaches 0 it is refilled with the reload value and can trigger an interrupt (SysTick exception). Since it is built into the core, it is used in the same way on any STM32 (or other Cortex-M).
2) SysTick_Config()
Configuration is completed with a single helper line provided by CMSIS.
SysTick_Config(SystemCoreClock / 1000u); /*Interrupt every 1 ms*/
This function internally ① writes ticks - 1 to the reload register, ② enables the SysTick interrupt, and ③ starts the counter with the core clock as the source. The parameter indicates "after how many clocks to trigger the interrupt," so at 72MHz, 72000000/1000 = 72000 corresponds to 1 ms.
3) SysTick_Handler and the Millisecond Counter
The handler name for the SysTick interrupt is designated as SysTick_Handler in the CMSIS startup. Here, the global counter is incremented by 1.
static volatile uint32_t g_ms = 0;
void SysTick_Handler(void) { g_ms++; }
volatile is required. Since the handler (interrupt) and the main loop share the same variable, the compiler must be prevented from caching it.
4) Wrap-Safe delay_ms()
static void delay_ms(uint32_t ms)
{
uint32_t start = g_ms;
while ((g_ms - start) < ms) { }
}
Even if g_ms exceeds the 32-bit limit and rolls back to 0 after about 49.7 days, unsigned subtraction g_ms - start still gives the correct elapsed time (as long as the actual elapsed time is within 2³²). This trick is directly verified in pc_test.c.
5) 24-bit Limitation
The SysTick counter is 24-bit, so the maximum reload value is 0xFFFFFF = 16,777,215. At 72MHz, one cycle is at most about 233ms (16,777,216/72MHz). A longer period cannot be made with a single SysTick, so it is solved by counting 1ms ticks (this pattern).
Creating a μVision Project
Project → New µVision Project…→ name itstm32_systick.- Device: STM32F103C8.
Manage Run-Time Environment→ CMSIS:CORE, Device:Startup.- After adding
main.c, copy and paste. - Apply
STM32F10X_MDtoC/C++ → Define. Debug → Use Simulator,-pSTM32F103C8.- Rebuild(F7) → Debug(Ctrl+F5).
Examples
Example 1 — main.c: Accurate 500ms Blink with SysTick
#include "stm32f10x.h"
/*
*Lesson 16 Example — Precise 500ms Blinking with SysTick.
*Target: STM32F103C8, Compiler: Keil MDK-ARM.
*
*SysTick is a 24-bit down counter built into the Cortex-M core.
*SysTick_Config(N) sets reload=N-1, enables the interrupt, and starts it.
*If you put SystemCoreClock/1000, SysTick_Handler is called every 1ms.
*/
#define LED_PIN 13u /* PC13 */
static volatile uint32_t g_ms = 0; /*System tick that increments every 1ms*/
void SysTick_Handler(void)
{
g_ms++;
}
/*Accurate millisecond delay based on g_ms. Safe wrapping due to unsigned subtraction.*/
static void delay_ms(uint32_t ms)
{
uint32_t start = g_ms;
while ((g_ms - start) < ms) {
/*Wait*/
}
}
int main(void)
{
RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;
GPIOC->CRH &= ~(0xFu << ((LED_PIN - 8u) * 4u));
GPIOC->CRH |= (0x2u << ((LED_PIN - 8u) * 4u)); /*Push-pull output 2MHz*/
/*1ms tick: SystemCoreClock/1000 (e.g., 72MHz → 72000)*/
SysTick_Config(SystemCoreClock / 1000u);
while (1) {
GPIOC->ODR ^= (1u << LED_PIN); /*Toggle PC13*/
delay_ms(500u); /*Exactly 500ms*/
}
}
Example 2 — pc_test.c: Wrapping-Safe Elapsed Time Check
#include <stdio.h>
#include <stdint.h>
/*
*Lecture 16 PC Check — Calculating elapsed time with an unsigned tick counter.
*elapsed = now - start (uint32 wrapping arithmetic). Even if the counter wraps around once,
*the subtraction result gives the correct elapsed time (as long as the actual elapsed time is within 2^32).
*Therefore, (g_ms - start) < ms in delay_ms is safe even with wrapping.
*/
static uint32_t elapsed(uint32_t now, uint32_t start)
{
return now - start; /*Unsigned subtraction → automatic handling of wrapping*/
}
int main(void)
{
/*{start, now} pair: The last two are the case where the counter has wrapped around once*/
uint32_t s[4] = { 1000u, 0u, 0xFFFFFFFFu, 0xFFFFFF00u };
uint32_t n[4] = { 1500u, 10u, 0x00000004u, 0x00000100u };
int i;
for (i = 0; i < 4; i++) {
printf("start=%lu now=%lu -> elapsed=%lu\n",
(unsigned long)s[i], (unsigned long)n[i],
(unsigned long)elapsed(n[i], s[i]));
}
return 0;
}
Checking with Simulator
- If you add
g_msto the Watch and run it, the value increases every 1ms (simulation time basis). - Adding
PORTC.13to the Logic Analyzer toggles exactly at 500ms intervals (1-second period). - Looking at
SysTick->LOADin the register window shows71999(72000-1, assuming 72MHz). - Expected behavior: PC13 toggles with a 1-second period (500ms ON/500ms OFF),
g_mssteadily increases.
Verification on a PC
gcc pc_test.c -o pc_test && ./pc_test
Expected output (stdout):
start=1000 now=1500 -> elapsed=500
start=0 now=10 -> elapsed=10
start=4294967295 now=4 -> elapsed=5
start=4294967040 now=256 -> elapsed=512
The last two lines are key. Even if the counter wraps from near the maximum to 0, unsigned subtraction provides the correct elapsed time (5, 512).
Common Mistakes
Q. g_ms does not change or disappears due to optimization.
A. This is because volatile was omitted. Variables shared by the interrupt handler and main must be declared with volatile.
Q. Changing the name of SysTick_Handler causes the interrupt not to trigger.
A. The handler name must exactly match the startup vector table. The CMSIS standard name is SysTick_Handler. If the spelling is different, the default (empty) handler will be called.
Q. I want to make a 1-second (1000ms) period with just one SysTick, but it doesn't work.
A. Due to the 24-bit limit, at 72MHz, one period is at most ~233ms. For longer times, use the method of counting 1ms ticks (like in this chapter, g_ms).
Q. The value of SystemCoreClock is different from what I expected.
A. SystemCoreClock is the value set by SystemInit() of system_stm32f10x.c. If you change the clock settings, you need to call SystemCoreClockUpdate() or update the value to be accurate.
Summary
- SysTick is a 24-bit down-counter built into all Cortex-M, making it highly portable.
- Create a 1ms period interrupt using
SysTick_Config(SystemCoreClock/1000). - Increment the
volatilemillisecond counter in the handler. delay_msis safe against wrapping using unsigned subtraction.- Overcome the 24-bit limit (~233ms at 72MHz) by counting ticks.
Try it yourself
Solve the assignment below. The reference solution is shown below.