Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 27

SysTick Driver for Periodic Interrupts

Recommended resources

Hello! Welcome back to our series on bare-metal driver development.

In our last lesson, we explored the architecture of general-purpose timers, understanding their core components like the prescaler (PSC), counter (CNT), and auto-reload register (ARR). We saw how these timers can be used in various modes, including the periodic mode for generating recurring interrupts.

Today, we'll put that theory into practice. Our goal is to implement a driver for the SysTick timer to generate periodic interrupts for time-basing. The SysTick is a simple yet powerful timer built directly into the ARM Cortex-M core itself. While you've used higher-level timer services in AUTOSAR (like the GPT module), this lesson will give you the hands-on, register-level experience of creating a system's fundamental "heartbeat," a critical skill for low-level development and a common topic in embedded interviews.

1. The SysTick Timer: A Core Peripheral

Unlike the general-purpose timers we discussed previously (which are vendor-specific peripherals like those from ST or NXP), the SysTick timer is a standard feature of the ARM Cortex-M processor core. Its primary purpose is to provide a simple, reliable timer to generate periodic interrupts, making it the ideal candidate for an operating system's system tick.

Let's begin with a short video that introduces the SysTick timer and its main applications.

Lecture 12: System Timer (SysTick)

This video from Embedded Systems and Deep Learning provides a concise overview of the SysTick timer's purpose and architecture.

Please watch from the beginning until 01:21. Focus on the typical use cases mentioned, such as time measurement and periodic task execution, which are fundamental to RTOS scheduling.

The key architectural features of the SysTick timer are:

  • A simple 24-bit down-counter.
  • It counts down from a value you specify in a reload register.
  • When the counter reaches 0, it automatically reloads the value and triggers an interrupt.

This is simpler than the general-purpose timers we saw before, as it lacks input capture or dedicated PWM generation channels. Its design is streamlined for one main job: creating a periodic time base.

Let's look at its internal structure.

Diagram of System Timer (SysTick)
This diagram illustrates the architecture of the SysTick timer. Note the main components: the clock source selection, the 24-bit down-counter (`SysTick Current Value`), the `SysTick Reload Value` register, and the control logic that generates the interrupt.

2. Configuring the SysTick Timer

To control the SysTick timer, we interact with four core registers. For easier and more readable access in C, we can group these registers into a struct, mapping it to the timer's base address in memory.

// Base address for SysTick registers
#define SYSTICK_BASE 0xE000E010

// SysTick register map struct
typedef struct {
    volatile uint32_t CTRL;  // Control and Status Register
    volatile uint32_t LOAD;  // Reload Value Register
    volatile uint32_t VAL;   // Current Value Register
    volatile uint32_t CALIB; // Calibration Value Register
} SysTick_Type;

#define SysTick ((SysTick_Type *)SYSTICK_BASE)

Your experience with memory-mapped peripherals will make this pattern familiar. The volatile keyword is crucial here, as these register values can be changed by hardware unexpectedly.

Now, let's dive into what these registers do.

Lecture 12: System Timer (SysTick)

The same video provides an excellent and detailed walkthrough of the SysTick registers and their functions.

Please watch from 01:21 to 07:54. This segment covers: How the down-counter and reload mechanism work. The bits in the Control and Status Register (CTRL): ENABLE, TICKINT, CLKSOURCE. The function of the Reload (LOAD) and Current Value (VAL) registers.

Here's a summary of the key register operations for our goal:

  • SysTick->LOAD (Reload Value Register): A 24-bit value that is loaded into the counter when it reaches zero. The interrupt period is (LOAD + 1) clock cycles.
  • SysTick->VAL (Current Value Register): The current value of the down-counter. Writing any value to this register clears it to 0 and also clears the count flag. It's good practice to write to it during initialization to ensure a known state.
  • SysTick->CTRL (Control and Status Register): This is the main control register. We are interested in three bits:
    • Bit 0 (ENABLE): Set to 1 to enable the timer.
    • Bit 1 (TICKINT): Set to 1 to enable the interrupt when the counter reaches 0.
    • Bit 2 (CLKSOURCE): Set to 1 to use the processor clock (e.g., AHB clock). Set to 0 to use an external reference clock (often AHB/8). We will typically use the processor clock for precision.

3. Implementing the SysTick Driver

Let's combine this knowledge to build a driver. The process involves three main steps: calculating the reload value, initializing the timer, and writing the interrupt service routine (ISR).

Step 1: Calculate the Reload Value

The goal is to generate an interrupt at a specific frequency (e.g., 1000 Hz for a 1ms tick). The formula is:

For example, on a system running at 180 MHz, to get a 1ms interrupt (1000 Hz):

This value is what you'll write to the SysTick->LOAD register.

Step 2: Initialize the Timer

Here is a C function that initializes the SysTick timer to generate interrupts at a specified frequency.

#include <stdint.h>

// Assume SystemCoreClock is a global variable holding the core clock frequency
extern uint32_t SystemCoreClock; 

void SysTick_Init(uint32_t frequency_hz) {
    // 1. Calculate Reload Value
    // The SysTick is a 24-bit timer, max reload value is 0xFFFFFF
    uint32_t reload_value = (SystemCoreClock / frequency_hz) - 1;
    if (reload_value > 0xFFFFFF) {
        // Handle error: desired frequency is too low for the system clock
        return;
    }
    SysTick->LOAD = reload_value;

    // 2. Set Interrupt Priority
    // This requires accessing the NVIC registers, we will set it to a low priority for now.
    // For a Cortex-M4, we can use NVIC_SetPriority(). A lower-level way is shown in interrupts lesson.
    // NVIC_SetPriority(SysTick_IRQn, (1UL << __NVIC_PRIO_BITS) - 1UL); // Lowest priority

    // 3. Reset the SysTick Current Value
    SysTick->VAL = 0;

    // 4. Configure and Enable SysTick
    // Enable SysTick interrupt, use processor clock, and enable the timer
    SysTick->CTRL = (1 << 1) | (1 << 2) | (1 << 0);
    // SysTick_CTRL_TICKINT_Msk | SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_ENABLE_Msk;
}

Note on CMSIS: ARM's Cortex Microcontroller Software Interface Standard (CMSIS) provides a standardized helper function, SysTick_Config(ticks), which performs these steps for you. You'll often see SysTick_Config(SystemCoreClock / 1000); used to quickly set up a 1ms tick. Understanding the underlying register manipulation, however, is crucial for driver development and debugging.

Step 3: The Interrupt Service Routine (ISR)

When the SysTick counter hits zero, it triggers an exception. The processor automatically calls the SysTick_Handler() function. Our job is to implement this function to perform a periodic action. A common use is to increment a global variable that acts as a system-wide time counter.

This is where the volatile keyword becomes critically important.

Blinking with SysTick interrupt

This bare-metal programming guide provides a perfect, practical example of implementing the SysTick handler and demonstrates why the volatile keyword is non-negotiable.

Please read from the section 'Blinky with SysTick interrupt' down to the assembly code comparison. Pay close attention to the implementation of s_ticks and SysTick_Handler(), and especially the explanation of how the compiler optimizes code with and without volatile.

Here's the essential code structure from that guide:

// A global variable to count the number of SysTick interrupts (ticks).
// 'volatile' is essential! It tells the compiler that this variable can
// change at any time, preventing it from making unsafe optimizations.
static volatile uint32_t s_ticks;

// The SysTick Interrupt Service Routine.
// The function name 'SysTick_Handler' is fixed by the ARM architecture.
void SysTick_Handler(void) {
    s_ticks++;
}

// A function to get the current tick count.
uint32_t Get_Ticks(void) {
    return s_ticks;
}

// A simple blocking delay function using the ticks.
void Delay_ms(uint32_t ms) {
    uint32_t start_ticks = Get_Ticks();
    while ((Get_Ticks() - start_ticks) < ms) {
        // Wait here
    }
}

With SysTick_Init(1000) and this handler, you have a millisecond-precision time base!

Test your understanding!

An interviewer asks: "I have a global variable that I increment inside my SysTick_Handler and read in my main loop. The program isn't working as expected. What is the most likely C language keyword I forgot, and why does it cause this problem?"

Show answer

The missing keyword is volatile.

Without volatile, the compiler assumes that the variable's value doesn't change unexpectedly. In the main loop, it might read the variable into a CPU register once and then keep using that cached register value in a loop, never re-reading the actual value from memory. Since the SysTick_Handler is updating the variable in memory, the main loop never sees the change. The volatile keyword forces the compiler to generate code that always reads the variable from memory, ensuring it gets the most up-to-date value.


Interview Preparation

  • Question: "Describe the steps to configure the SysTick timer to generate a 1ms interrupt on an ARM Cortex-M4 running at 168 MHz."

    • Your Answer: "First, I'd calculate the reload value: (168,000,000 / 1000) - 1, which is 167,999. I'd write this value to the SysTick->LOAD register. Next, I'd clear the SysTick->VAL register by writing 0 to it. Finally, I would write to the SysTick->CTRL register to select the processor clock, enable the SysTick interrupt, and enable the timer itself. I would also need to implement the SysTick_Handler function, which is the ISR that will be called every 1ms."
  • Question: "How would you implement a non-blocking delay in a bare-metal system? Why is this approach preferable to a simple for loop delay?"

    • Your Answer: "I would use a periodic timer like SysTick to maintain a global tick counter. A non-blocking delay function would record the start tick count. In the main loop, I would periodically check if the difference between the current tick count and the start tick count has exceeded the desired delay period. This is preferable to a for loop (busy-wait) because the CPU is not stuck in a tight loop doing nothing. The main loop can continue to perform other tasks, like checking other sensors or communication interfaces, while it 'waits' for the timer to expire. This leads to a much more responsive and efficient system." The timer_expired() function from the bare-metal-programming-guide is a great example of this pattern.
  • Question: "What are the potential dangers of setting the SysTick interrupt frequency too high?"

    • Your Answer: "If the interrupt frequency is too high, the time between interrupts might be shorter than the time it takes to execute the ISR itself, including the processor's context switch overhead. This can lead to the system spending all its time in the interrupt handler, starving the main application code of any CPU time. In the worst case, the system could get stuck continuously re-entering the ISR, a condition known as livelock."

Conclusion

Today, we successfully implemented a driver for the SysTick timer, creating a fundamental time-basing service for a bare-metal system. This is a cornerstone of embedded programming, forming the basis for delays, timeouts, and RTOS schedulers.

Key Takeaways:

  • The SysTick is a standard 24-bit down-counter in the ARM Cortex-M core, ideal for creating a system tick.
  • Configuration is done via the CTRL, LOAD, and VAL registers.
  • The interrupt period is determined by (LOAD + 1) * clock_period.
  • The ISR, SysTick_Handler, is where the periodic action (like incrementing a counter) is performed.
  • The volatile keyword is absolutely essential for variables shared between an ISR and the main application code to prevent unsafe compiler optimizations.

Next Lesson Preview:
Now that we've mastered the simple, periodic interrupt capability of the SysTick timer, we'll return to the more complex general-purpose timers. In the next lesson, you will develop a timer driver to generate a PWM signal with a configurable frequency and duty cycle. This will require you to use not just the time-base unit but also the output compare features and Capture/Compare Registers (CCRs) we discussed previously.