SysTick Timer Configuration
Configure the Cortex-M SysTick timer correctly: compute SYST_RVR reload values, write the interrupt handler, and avoid clock-source pitfalls.

Contents & prerequisites
What is the SysTick timer and why does every Cortex-M need it?
SysTick is a 24-bit down-counting timer built into the Cortex-M core itself, present on every M0, M0+, M3, M4, M7, M23, M33, and M55 part regardless of which vendor silicon it sits in. Because it's part of the ARM core architecture rather than a vendor peripheral, code written against SysTick on an STM32 behaves identically on an NXP, Nordic, or Microchip part — the register layout doesn't change when you change silicon vendor.
Core role in real-time kernels and timing abstractions
Every RTOS that targets Cortex-M — FreeRTOS, Zephyr, ThreadX, RTX — uses SysTick as its default tick source for time slicing and timeout tracking. The timer counts down from a programmed reload value, hits zero, raises an exception, and reloads automatically. That periodic exception is what an RTOS scheduler uses to decide "has this task's time slice expired" or "has this delay elapsed." Bare-metal firmware with no RTOS at all still typically uses SysTick for HAL_Delay()-style millisecond counters, debounce timers, and watchdog-feed scheduling.
Built-in peripheral vs. general-purpose timers
SysTick trades flexibility for portability. A general-purpose timer (TIMx on STM32, CTIMER on LPC parts) gives you multiple channels, PWM output, input capture, and often 32-bit counters, but the register map is vendor-specific and sometimes changes between families from the same vendor. SysTick gives you exactly one 24-bit countdown counter with one interrupt, the same four registers everywhere. For a system tick that drives scheduling, that lack of features is the point — you don't want your RTOS porting layer to depend on which timer peripheral variant shipped on a given die.
Relationship to system clock and RTOS tick rates
SysTick can be clocked from the processor clock or from an implementation-defined reference clock (more on that distinction below), and its reload value is computed directly from whichever clock drives it. A 1 kHz RTOS tick (1 ms period) is the de facto default in FreeRTOS configs, but 100 Hz and 10 kHz tick rates both show up in production firmware depending on how fine-grained the scheduler's timeout resolution needs to be.
How do you read and configure the SysTick control and value registers?
SysTick exposes four memory-mapped registers: SYST_CSR, SYST_RVR, SYST_CVR, and SYST_CALIB. All four are documented in the ARMv7-M or ARMv8-M Architecture Reference Manual, not in your MCU vendor's datasheet — the addresses are fixed in the System Control Space and identical across Cortex-M implementations. If you're accessing these through CMSIS rather than raw addresses, see Register-Level Peripheral Access via CMSIS for how the SysTick_Type struct maps onto them.
SYST_CSR: enable, interrupt, clock source, and COUNTFLAG bits
SYST_CSR (Control and Status Register) holds four bits that matter: ENABLE (bit 0) starts the counter, TICKINT (bit 1) enables the exception on count-to-zero, CLKSOURCE (bit 2) selects processor clock (1) or reference clock (0), and COUNTFLAG (bit 16) is a read-clear flag set when the counter reached zero since the last read. Reading SYST_CSR clears COUNTFLAG — if you read it twice in a row expecting to poll it, the second read always returns 0 for that bit.
SYST_RVR: reload value and maximum count limits
SYST_RVR (Reload Value Register) holds the value the counter reloads to after reaching zero, and it's only 24 bits wide — the maximum reload value is 0x00FFFFFF (16,777,215). Writing a value that doesn't fit in 24 bits silently truncates on most implementations, so always check your computed reload against that ceiling before writing it.
SYST_CVR: current timer value and when to clear it
SYST_CVR (Current Value Register) holds the live countdown value. Writing any value to SYST_CVR clears it to zero and also clears COUNTFLAG — it does not write the value you supplied. This is a common source of confusion: engineers expect to preset SYST_CVR like a normal counter register, but the architecture defines the write as clear-only.
SYST_CALIB: calibration value for 1ms reference tick
SYST_CALIB (Calibration Value Register) provides a vendor-supplied TENMS field intended to give a 10 ms reload value for the reference clock, plus a SKEW bit indicating whether that value is exact. Not every vendor populates this field correctly — check your MCU's errata before relying on SYST_CALIB instead of computing your own reload value from the known system clock frequency.
How do you calculate reload values for arbitrary tick frequencies?
The reload value is (clock_frequency_Hz / tick_frequency_Hz) - 1, written into SYST_RVR, with the result checked against the 24-bit maximum. The minus one accounts for the counter including the zero count in its period — a reload of N produces N+1 clock cycles per period.
Deriving SYST_RVR from system clock and desired period
For a processor clock of 64 MHz and a desired 1 ms tick: reload = (64,000,000 / 1,000) - 1 = 63,999. For a 10 ms tick on the same clock: reload = (64,000,000 / 100) - 1 = 639,999. Both fit comfortably under 0x00FFFFFF.
#define SYSCLK_HZ 64000000UL
#define TICK_HZ 1000UL /* 1 ms tick */
uint32_t reload = (SYSCLK_HZ / TICK_HZ) - 1UL;
SysTick->LOAD = reload & 0x00FFFFFFUL;
SysTick->VAL = 0UL; /* clears CVR and COUNTFLAG */
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
SysTick_CTRL_TICKINT_Msk |
SysTick_CTRL_ENABLE_Msk;
Trade-offs between tick resolution and CPU load
A higher tick frequency gives finer timeout resolution but costs more CPU time in the handler — at 10 kHz you're entering and exiting the exception 10,000 times a second, and even a handler that does almost nothing (increment a counter, return) consumes cycles proportional to Cortex-M's exception entry/exit latency, typically 12–16 cycles on M3/M4 class cores per the architecture manual. For details on how pipeline depth affects that entry cost across core variants, see ARM Cortex-M Pipeline Stages (M0 to M55).
Handling clock source selection: processor clock vs. reference clock
| Aspect | Processor clock (CLKSOURCE=1) | Reference clock (CLKSOURCE=0) |
|---|---|---|
| Frequency | Same as CPU core clock | Implementation-defined, often CPU/8 |
| Typical use | Precise RTOS tick tied to known SYSCLK | Legacy compatibility, calibration |
| Availability | Always present | Not guaranteed on all cores |
| Reload math | reload = f_cpu / f_tick - 1 | reload = f_ref / f_tick - 1 |
Most production firmware uses the processor clock because its frequency is known exactly from clock-tree configuration, whereas the reference clock's frequency is implementation-defined and must be looked up per part.
Practical examples: 1ms, 10ms, and custom tick rates
For a 48 MHz clock targeting a 5 ms tick: reload = (48,000,000/200) - 1 = 239,999. Always recompute reload whenever you change the system clock configuration (PLL settings, prescalers) — a reload value computed for 64 MHz silently produces the wrong tick period if the clock tree later gets reconfigured to 48 MHz and nobody updates SysTick.
How do you enable the SysTick interrupt and write the exception handler?
Setting TICKINT in SYST_CSR enables the interrupt; the handler function must be named SysTick_Handler and placed in the vector table at its fixed exception slot (exception number 15) for the linker and startup code to find it.
SysTick_Handler naming and placement in vector table
Most CMSIS-based startup files already declare a weak SysTick_Handler stub; you override it by defining a strong symbol with the exact name in your application code. If you're unclear on how weak/strong symbol resolution and linkage work for this kind of override, see Static, Inline, and Extern Linkage.
Setting TICKINT bit and managing NVIC priority (fixed at lowest)
SysTick's exception priority is configurable via SCB->SHPR3 like other system exceptions, but convention sets it to the lowest priority (numerically highest value) so it doesn't preempt time-critical peripheral interrupts. An RTOS scheduler tick firing in the middle of a higher-priority ISR would introduce jitter into exactly the interrupts that can least tolerate it.
void SysTick_Handler(void)
{
tick_count++; /* keep this short */
}
Reading COUNTFLAG to detect missed ticks
COUNTFLAG tells you whether the counter reached zero since the last read of SYST_CSR, which is useful for detecting that your handler is being starved by a higher-priority interrupt — if COUNTFLAG is set again before your handler finishes, you've missed a tick.
Common pitfalls: handler latency and re-entrancy
If SysTick_Handler is not firing, check three things in order: TICKINT is actually set in SYST_CSR, ENABLE is set, and the function name matches exactly (case-sensitive, no name-mangling issues if mixing C and C++). A fourth, less obvious cause is a higher-priority interrupt handler that never returns — SysTick's exception is pending but never services if the core is stuck in a tighter interrupt loop.
What is the difference between polling and interrupt-driven SysTick usage?
Polling reads COUNTFLAG in a loop with no interrupt enabled; interrupt-driven usage sets TICKINT and lets the core dispatch SysTick_Handler automatically. The two models suit different workloads and aren't interchangeable without changing your timing architecture.
Polling COUNTFLAG for timing checks without interrupts
SysTick->CTRL &= ~SysTick_CTRL_TICKINT_Msk; /* no interrupt */
while (!(SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk)) {
/* busy wait */
}
This avoids exception overhead entirely but blocks the CPU for the full tick period, making it unsuitable for anything beyond simple blocking delays in single-threaded bare-metal code.
Interrupt-driven approach for kernel tick and preemption
An RTOS needs the interrupt because scheduling decisions (which task runs next) must happen asynchronously to whatever code is currently executing — polling would require the application to voluntarily check the timer, defeating preemption.
CPU efficiency and context-switching overhead
Polling costs zero exception overhead but 100% CPU occupancy during the wait; interrupt-driven costs a fixed entry/exit latency per tick but frees the CPU for other work between ticks. For a kernel tick at 1 kHz, the interrupt overhead is a rounding error; for a tight microsecond-level polling loop, the interrupt approach would cost more than it saves.
Choosing the right model for baremetal vs. RTOS scenarios
Use polling for simple delay loops in bare-metal firmware with no scheduler; use the interrupt-driven model for anything running an RTOS or needing background timeout tracking.
How do you implement microsecond and nanosecond delays using SysTick?
Microsecond delays are achievable by reading SYST_CVR directly and computing elapsed cycles, but true nanosecond-level resolution is limited by the processor clock period itself — at 64 MHz, one cycle is already 15.6 ns, so "nanosecond delay" really means "small integer number of cycles," not an independently tunable nanosecond timer.
Reading SYST_CVR for sub-tick timing
uint32_t start = SysTick->VAL;
/* ... code to time ... */
uint32_t elapsed = (start - SysTick->VAL) & 0x00FFFFFF;
uint32_t elapsed_ns = (elapsed * 1000UL) / (SYSCLK_HZ / 1000000UL);
Because SYST_CVR counts down, elapsed cycles is start - current, masked to 24 bits to handle wraparound if the measurement spans a reload.
Busy-wait loops and their CPU cost
A microsecond delay function built on SYST_CVR polling consumes the CPU entirely for the duration — fine for short, deterministic delays (sensor setup times, bit-bang protocol timing) but wrong for anything that should yield to other work.
Compensating for instruction pipeline delays
The overhead of reading SYST_CVR, subtracting, and branching costs a handful of cycles that matter when you're timing single-digit microsecond intervals. On deeper-pipeline cores this overhead is less predictable cycle-for-cycle than on M0 class cores; consult ARM Cortex-M Pipeline Stages (M0 to M55) for how pipeline depth affects instruction timing on your specific core.
When to use SysTick vs. a dedicated general-purpose timer
SysTick works for coarse microsecond delays and system-tick duty, but a dedicated hardware timer with capture/compare gives you interrupt-free, CPU-free timing for anything precise or long-running — use a general-purpose timer when you need to measure or generate signals without burning CPU cycles in a busy-wait.
How do you debug SysTick configuration errors and verify correct operation?
Most SysTick failures trace to one of three causes: wrong clock source selected, reload value computed from the wrong clock frequency, or TICKINT left clear. Checking SYST_CSR, SYST_RVR, and the actual configured system clock frequency side by side catches the majority of these in under a minute.
Common mistakes: clock source selection and reload overflow
A reload value computed against the wrong clock frequency (common after a clock-tree change) produces a tick period that's wrong by exactly the ratio of actual-to-assumed frequency — if your tick is running 2x too slow, check whether the system clock doubled without updating SYST_RVR. A reload value that silently exceeds the 24-bit limit truncates and produces a much shorter period than intended.
Using debugger breakpoints in SysTick_Handler
Setting a breakpoint inside SysTick_Handler and checking its hit rate against expected frequency is the fastest sanity check — if it's not hitting at all, TICKINT or ENABLE is probably clear; if it's hitting at the wrong rate, the reload value or clock source is wrong.
Observing tick jitter and handler execution time
A logic analyzer or oscilloscope probe toggling a GPIO at the top of SysTick_Handler shows both the actual tick period and any jitter introduced by higher-priority interrupts delaying SysTick's dispatch. If you're accessing GPIO registers directly inside the handler for this kind of instrumentation, the bit-field patterns used for register access are covered in Bit-Field Struct for Register Mapping, and the underlying pointer and struct mechanics in C Language Basics: Pointers, Structs, Unions.
Validation: scope measurements and timing assertions
Confirm the measured period against the computed one within the tolerance of your clock source — a crystal-derived SYSCLK should match within parts-per-million, while an internal RC oscillator may drift several percent across temperature, so check your MCU's datasheet for the oscillator's tolerance before treating a few percent of tick-period error as a configuration bug rather than expected clock drift.
Learning
Sign in to track your progress.
Evidence
Public projects engineers linked to SysTick Timer Configuration.
No engineer has linked a project to this topic yet. Built something that proves it? Add the project and tag it with embedded-firmware-systick-timer-configuration — it then shows here and on your public profile.
