Analyzing the Core Mechanism Behind SysTick Based Delays
Developers frequently encounter pre-written delay functions such as Delay() in reference projects, yet the mathematical foundation required to generate these routines oftan remains obscure. While widely utilized, understanding the relationship between the system clock frequency and the timer's count values is crucial for implementing accurate timings with out an operating system.
This guide explores the creation of a microsecond-level delay function using the SysTick timer on an ARM Cortex-M3 or M4 architecture, utilizing the STM32F103 microcontroller as a case study. Unlike generic software loops that consume processing cycles unpredictably, the SysTick offers a hardware-backed solution independent of CPU load.
SysTick Architecture Overview
The SysTick timer is a 24-bit downward counter integrated directly into the Nested Vectored Interrupt Controller (NVIC). Its primary role is to act as a system heartbeat or provide precise interval measurements. Key characteristics include:
- Operation Mode: It decrements continuously from a loaded value to zero, then automatically reloads.
- Wake-up Support: It continues to operate even when the processor enters sleep modes.
- Resource Efficiency: It reserves no General Purpose Timer (GPTimer) resources, allowing them for other peripheral tasks.
- Interrupt Generation: It triggers ecxeption vector number 15 upon reaching zero.
Essential Control Registers
Direct manipulation of the SysTick involves four primary registers located in the memory map:
// Header file inclusion (typically arm_cm3.h or mpu_stm32f1xx.h)
volatile uint32_t *SysTick_CTRL = (uint32_t *)0xE000E010;
volatile uint32_t *SysTick_LOAD = (uint32_t *)0xE000E014;
volatile uint32_t *SysTick_VAL = (uint32_t *)0xE000E018;
volatile uint32_t *SysTick_CALIB= (uint32_t *)0xE000E01C;
Register Breakdown:
- CTRL (Control and Status): Bit 0 enables the counter. Bit 2 selects the clock source (typically AHB clock). Bit 1 enables the interrupt request.
- VAL (Current Value): Holds the current countdown value. Reading this returns the remaining counts.
- LOAD (Reload): Determines the starting value for the next countdown cycle. Only the lower 24 bits are effective.
- CALIB (Calibration): Stores calibration data regarding the system clock drift; less critical for simple timing calculations.
Configuration and Clock Source Setup
To achieve high-precision delays, the clock driving the SysTick must be known accurately. On the STM32 platform, the source is typically derived from the High-Speed Internal (HSI) or HCLK bus.
In many configurations, the HCLK is divided by 8 before reaching the SysTick. If we assume a system configuration where the external oscillator is 8MHz and the Phase Locked Loop (PLL) sets the SystemCoreClock to 72MHz:
- Source Frequency =
SystemCoreClock/ 8 - HCLK (AHB Bus) = 72 MHz
- SysTick Input = 9 MHz
Mathematical Derivation for Time Conversion
The core challenge lies in converting human-readable time units (microseconds) into integer timer counts. The relationship follows basic frequency physics:
Time (Seconds) = Counts / Frequency
Rearranged for Counts: Counts = Time × Frequency
If we require a 1-microsecond (1µs) delay:
- SysTick Clock Frequency ($F_{sys}$) = $SystemCoreClock / 8$
- Target Time ($T$) = 1 µs = $1 \times 10^{-6}$ seconds
The required reload value ($N$) for a single microsecond is calculated as:
N = (SystemCoreClock / 8) * 0.000001
// Example with 72MHz SystemCoreClock
N = (72,000,000 / 8) * 0.000001
N = 9 * 1,000,000 * 0.000001
N = 9
Thus, for every 1µs of delay, the SysTick must decrement exactly 9 times. To support millisecond calculations, we simply multiply this factor by 1000.
Implementation Logic
Below is a refactored implementation demonstrating the initialization of the scaling factors based on the active clock speed. Note the variable naming conventions have been updated for clarity and maintainability.
System Initialization
We establish constants representing the ticks-per-time-unit ratios. This avoids redundant division operations during runtime.
#include <stdint.h>
// Global multipliers for timing calculations
static uint32_t ticks_per_microsecond;
static uint32_t ticks_per_millisecond;
/**
* @brief Initialize SysTick timing parameters
*
* @param sys_clock_freq Frequency of the system core in Hz
*/
void Init_Timing_Scaling(uint32_t sys_clock_freq)
{
// Select HCLK as source, divided by 8
// In HAL context: HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK_DIV8);
SysTick->CTRL |= (1 << 2);
// Disable interrupts temporarily to set up config safely
SysTick->CTRL &= ~(1 << 1);
// Reset the counter to avoid startup jitter
SysTick->VAL = 0;
// Pre-calculate conversion factors
// Formula: Factor = (SystemClockHz / 8) / 1,000,000
ticks_per_microsecond = (sys_clock_freq / 8U) / 1000000U;
// Convert to milliseconds
ticks_per_millisecond = ticks_per_microsecond * 1000U;
// Re-enable SysTick control
SysTick->CTRL |= (1 << 0); // Enable Counter
}
/**
* @brief Busy wait delay in microseconds
*/
void Delay_Microseconds(uint32_t us)
{
volatile uint32_t start = SysTick->VAL;
while(SysTick->VAL > start - ticks_per_microsecond * us);
}
Summary
By mapping the abstract concept of time to concrete hardware counters, we bridge the gap between logical requirements and physical execution. The calculation (SystemCoreClock / 8) / 1,000,000 serves as the critical bridge between the CPU frequency and the desired microsecond resolution. This method ensures consistent timing regardless of interrupt activity, providing a reliable timing backbone for bare-metal applications.