
The Cortex-M SysTick timer is the backbone of timekeeping in almost every RTOS and bare-metal scheduler targeting ARM Cortex-M cores. It’s a 24-bit down-counter integrated directly into the NVIC, clocked from the core clock (optionally divided by 8), and tied to a dedicated exception vector (Exception #15). Despite its simplicity, misconfiguration of SysTick is a common source of subtle timing bugs — missed ticks, jitter, or complete scheduler failure.
This article walks through the SysTick register map, clock source selection, reload value calculation, interrupt setup, and common pitfalls when using SysTick for RTOS ticks or bare-metal periodic timing.
The SysTick timer exposes four 32-bit registers in the System Control Space (SCS) at base address 0xE000E010:
+--------+---------------+-------------------+-------------------+| Offset | Register | Description | Key Bits |+========+===============+===================+===================+| 0x00 | SYST_CSR | Control & Status | ENABLE (0) || | | | TICKINT (1) || | | | CLKSOURCE (2) || | | | COUNTFLAG (16) |+--------+---------------+-------------------+-------------------+| 0x04 | SYST_RVR | Reload Value | RELOAD[23:0] |+--------+---------------+-------------------+-------------------+| 0x08 | SYST_CVR | Current Value | CURRENT[23:0] |+--------+---------------+-------------------+-------------------+| 0x0C | SYST_CALIB | Calibration | TENMS[23:0] || | | | SKEW (30) || | | | NOREF (31) |+--------+---------------+-------------------+-------------------+
Key register behaviors:
The CLKSOURCE bit in SYST_CSR selects the clock input:
| CLKSOURCE | Source | Frequency |
|---|---|---|
| 0 | External reference clock (typically core_clk / 8) | Core clock ÷ 8 |
| 1 | Processor clock (core clock) | Core clock |
Always use CLKSOURCE = 1 (processor clock) unless you have a specific reason to use the divided reference. The reference clock frequency is implementation-defined and may not be exactly core_clk/8. Using the processor clock gives you deterministic, calculable timing.
// Use processor clock (core clock)#define SYST_CSR_CLKSOURCE_Msk (1UL << 2)SYST_CSR |= SYST_CSR_CLKSOURCE_Msk;
The reload value determines the period. SysTick counts down from RELOAD to 0, then reloads. The number of clock cycles per period is RELOAD + 1.
period_cycles = RELOAD + 1RELOAD = (core_clock_hz / desired_freq_hz) - 1
Example: 1 kHz tick at 72 MHz
RELOAD = (72,000,000 / 1,000) - 1 = 71,999 = 0x0001193F
Example: 1 kHz tick at 168 MHz (STM32F4) with /8 prescaler
RELOAD = (168,000,000 / 8 / 1,000) - 1 = 20,999 = 0x0000520FSYST_CSR |= SYST_CSR_CLKSOURCE_Msk; // Use core clock (don't divide)
Maximum reload value: 0x00FFFFFF (24-bit). At 72 MHz with /8 prescaler, max period ≈ 2.3 seconds.
Enable the SysTick exception by setting TICKINT (bit 1) in SYST_CSR:
#define SYST_CSR_ENABLE_Msk (1UL << 0)#define SYST_CSR_TICKINT_Msk (1UL << 1)#define SYST_CSR_CLKSOURCE_Msk (1UL << 2)void systick_init(uint32_t reload) {SYST_RVR = reload; // Set reload valueSYST_CVR = 0; // Clear current valueSYST_CSR = SYST_CSR_CLKSOURCE_Msk | // Processor clockSYST_CSR_TICKINT_Msk | // Enable interruptSYST_CSR_ENABLE_Msk; // Enable counter}
The SysTick exception handler is SysTick_Handler (Exception #15). In an RTOS context, this calls the tick increment function:
void SysTick_Handler(void) {// RTOS tick hookxTaskIncrementTick(); // FreeRTOS// osSystickHandler(); // CMSIS-RTOS2}
Priority: SysTick should typically run at the lowest priority (255 on Cortex-M3/M4/M7) to avoid preempting higher-priority interrupts. Configure via NVIC:
NVIC_SetPriority(SysTick_IRQn, 0xFF); // Lowest priority
Wrong: SYST_RVR = core_hz / tick_hz;
Right: SYST_RVR = (core_hz / tick_hz) - 1;
The counter counts from RELOAD down to 0 inclusive — that’s RELOAD + 1 cycles.
If you don’t write to SYST_CVR before enabling, the counter starts from a random power-on value, causing a wildly incorrect first period.
SYST_CVR = 0; // Must clear before enabling
The CLKSOURCE = 0 reference clock is not guaranteed to be core_clk/8. Some vendors implement it differently. Always verify in the device datasheet or use CLKSOURCE = 1.
If you poll COUNTFLAG in a loop, reading SYST_CVR clears it. This creates a race where you might miss a tick if an interrupt fires between the read and the check.
// Unreliable: reading CVR clears COUNTFLAGwhile (!(SYST_CSR & (1 << 16))) { /* wait */ }uint32_t val = SYST_CVR; // Clears COUNTFLAG!
Better: use the interrupt, or if polling, accept that reading CVR consumes the flag.
Setting SysTick priority higher than application interrupts causes the RTOS tick to preempt critical sections, breaking atomic operations. Always use the lowest priority.
For timestamping without periodic interrupts, configure SysTick with maximum reload:
void systick_freerun_init(void) {SYST_RVR = 0x00FFFFFF; // Max 24-bit valueSYST_CVR = 0;SYST_CSR = SYST_CSR_CLKSOURCE_Msk | SYST_CSR_ENABLE_Msk; // No TICKINT}// Read timestamp (down-counter)uint32_t timestamp_us(void) {return (0x00FFFFFF - SYST_CVR) * (1_000_000 / (SystemCoreClock / 1_000_000));}// Or as up-counteruint32_t timestamp_ticks(void) {return 0x00FFFFFF - SYST_CVR;}
This gives a ~2.3 second wraparound at 72 MHz (core clock). For longer periods, chain with a software counter in a lower-frequency timer.
On dual-core Cortex-M devices (e.g., STM32H7), each core has its own SysTick timer. They are not synchronized by hardware. For cross-core time sync, use a shared hardware timer (TIM, LPTIM) or a hardware semaphore with a common timebase.
0xE000E010 with four registersCLKSOURCE = 1 (processor clock) for deterministic timingRELOAD = (f_clk / f_tick) - 1 — don’t forget the -1SYST_CVR before enablingTICKINT bit, set priority to lowest (0xFF)xTaskIncrementTick for FreeRTOS)xTaskIncrementTick implementationQuick Links
Legal Stuff





