Embedded C track

Lesson 16 · Part 3: STM32 (Keil MDK-ARM / STM32F103C8, direct register control)

SysTick Timer & Precise Delays

`SysTick_Config` and millisecond ticks

Part 3: STM32 (Keil MDK-ARM / STM32F103C8, direct register control)6 min readFree lesson

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 PC gcc(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

  1. Project → New µVision Project… → name it stm32_systick.
  2. Device: STM32F103C8.
  3. Manage Run-Time Environment → CMSIS:CORE, Device:Startup.
  4. After adding main.c, copy and paste.
  5. Apply STM32F10X_MD to C/C++ → Define.
  6. Debug → Use Simulator, -pSTM32F103C8.
  7. 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_ms to the Watch and run it, the value increases every 1ms (simulation time basis).
  • Adding PORTC.13 to the Logic Analyzer toggles exactly at 500ms intervals (1-second period).
  • Looking at SysTick->LOAD in the register window shows 71999 (72000-1, assuming 72MHz).
  • Expected behavior: PC13 toggles with a 1-second period (500ms ON/500ms OFF), g_ms steadily 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 volatile millisecond counter in the handler.
  • delay_ms is 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.

Example files

The runnable files used in this lesson. Copy them into your own project and experiment.

src/stm32_systick/main.cC
#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*/
    }
}
src/stm32_systick/pc_test.cC
#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;
}

Assignment

Problem 1 is pure logic (PC gcc), Problem 2 is MDK (STM32F103C8, μVision simulator).


Problem 1 — SysTick reload calculation and 24-bit limit

  • File name: homework_01/main.c
  • Key concept: reload = ticks − 1, 24-bit limit (0xFFFFFF)
  • Platform: Common (PC gcc)

Requirements

  • Assuming a core clock of 72MHz, calculate SysTick reload values for periods of 1, 10, 100, and 1000ms.
  • ticks = 72000 × period_ms, reload = ticks − 1.
  • If reload is within the 24-bit limit 0xFFFFFF (16777215), append (OK); if it exceeds, append (OVERFLOW).
  • The format period N ms -> reload R (...) must match the expected output exactly, character by character.

Expected output

period 1 ms -> reload 71999 (OK)
period 10 ms -> reload 719999 (OK)
period 100 ms -> reload 7199999 (OK)
period 1000 ms -> reload 71999999 (OVERFLOW)

Hint

  • uint32_t ticks = 72000u * period_ms; uint32_t reload = ticks - 1u;
  • Limit comparison: reload <= 0xFFFFFFu ? "OK" : "OVERFLOW".
  • 1000ms has a reload of about 72 million, which far exceeds the 24-bit limit (about 16.77 million).

Problem 2 — Toggle every second + second counter

  • File name: homework_02/main.c
  • Key concept: SysTick 1ms tick, long period with delay_ms
  • Platform: MDK (STM32F103C8)

Requirements

  • Create a 1ms tick (g_ms) with SysTick.
  • Use delay_ms(1000) to toggle PC13 every second and increase variable seconds by 1.

Expected Operation (Simulator)

  • g_ms increments every 1ms, seconds increments by 1 every second (check with Watch).
  • PC13 (PORTC.13) toggles every second (2-second period).

Hint

  • Use the SysTick_Handler / delay_ms pattern from the example as is.
  • Using seconds as volatile also allows stable observation in the debugger.

Check your answers

After solving it yourself, compare it with the reference solution below. The solution files include comments on key points and common mistakes.

Show the reference solution

Try the assignment yourself first — comparing your attempt with a working solution is where most of the learning happens.

homework_01/main.cC
#include <stdio.h>
#include <stdint.h>

/*
 *Answer 1 — SysTick reload calculation and 24-bit limit
 *
 *Key Points:
 *- SysTick_Config(N) writes N-1 to the reload register.
 *- At 72MHz, ticks = 72000 × period_ms, reload = ticks - 1.
 *- Since the counter is 24-bit, the maximum reload value is 0xFFFFFF (16,777,215). Exceeding this is not possible.
 *
 *Common mistake:
 *- Confusing reload and ticks (reload is ticks-1).
 *- Mistaking the 24-bit limit for 32-bit and thinking it results in a 1-second period.
 */

int main(void)
{
    unsigned int periods[4] = { 1u, 10u, 100u, 1000u };
    int i;

    for (i = 0; i < 4; i++) {
        uint32_t ticks  = 72000u * periods[i];   /*Based on 72MHz*/
        uint32_t reload = ticks - 1u;
        const char *tag = (reload <= 0xFFFFFFu) ? "OK" : "OVERFLOW";
        printf("period %u ms -> reload %lu (%s)\n",
               periods[i], (unsigned long)reload, tag);
    }
    return 0;
}
homework_02/main.cC
#include "stm32f10x.h"

/*
 *Answer 2 (MDK, STM32F103C8) — Toggle every 1 second + second counter
 *
 *Key Points:
 *- Count 1ms SysTick ticks to create a long period (1000ms). With a 24-bit register alone,
 *1 second cannot be created, but counting 1ms 1000 times works.
 *- delay_ms is safe for wrapping using unsigned subtraction.
 *
 *Common mistake:
 *- Missing volatile for g_ms/seconds.
 *- Interrupt will not occur if SysTick_Handler name is misspelled.
 *
 *Expected behavior (simulator): g_ms increases every 1ms, seconds +1 every second, PC13 toggles every second.
 */

#define LED_PIN   13u

static volatile uint32_t g_ms = 0;

void SysTick_Handler(void)
{
    g_ms++;
}

static void delay_ms(uint32_t ms)
{
    uint32_t start = g_ms;
    while ((g_ms - start) < ms) {
    }
}

int main(void)
{
    volatile uint32_t seconds = 0;

    RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;
    GPIOC->CRH &= ~(0xFu << ((LED_PIN - 8u) * 4u));
    GPIOC->CRH |=  (0x2u << ((LED_PIN - 8u) * 4u));

    SysTick_Config(SystemCoreClock / 1000u);   /*1ms tick*/

    while (1) {
        delay_ms(1000u);
        GPIOC->ODR ^= (1u << LED_PIN);   /*Toggle every second*/
        seconds++;
    }
}

Learn this lesson with a mentor

Work through the Embedded C track on your own, or join a guided cohort at our Pitampura campus and online.

Call NowEnroll Now