STM32 SysTick: Build a Reliable 1ms Heartbeat (Code Walkthrough)

Previously in Part 2, we traced the boot sequence and verified our interrupt vector table. Now, let’s dive deep into the first critical function call in our main() – SysTick_Init().

Furthermore, after our interrupt vector table verification block completes successfully, execution immediately moves to the next line:

SysTick_Init();

Think of this moment as pressing the “setup” button on a digital stopwatch. However, the stopwatch isn’t running yet – we’re just about to configure it. Here’s what happens next.

Step 1: Jumping to the SysTick Configuration Code

Additionally, the processor immediately jumps to the SysTick_Init() function in systick.c. At this point, no timing is happening yet – we’re just starting the configuration process:

void SysTick_Init(void) {
    // We're now inside the configuration function
    // The SysTick timer is still OFF at this point

Step 2: Getting the Clock Speed Information

Moreover, the first line of actual work is:

SysTick->LOAD = (SystemCoreClock_Get() / 1000) - 1;

Let’s break this down into two parts:

Part A: The Function Call to Get Clock Speed

First, here’s the execution flow:

  • Execution pauses in SysTick_Init()
  • Jumps to SystemCoreClock_Get() in system_config.c
  • Returns with the value 16000000 (16 MHz)
  • Execution returns to SysTick_Init()

Part B: The Mathematical Calculation

Next, the processor calculates:

(16000000 / 1000) - 1 = 15999

Furthermore, what does this number mean? Think of SysTick as a countdown timer. We’re telling it: “Count down from 15999 to 0, and when you reach 0, let me know!” At 16 MHz, counting down 16000 steps takes exactly 1 millisecond.

[IMAGE PLACEHOLDER: Visual showing 16MHz clock cycles = 1ms]

Step 3: Loading the Countdown Value

SysTick->LOAD = 15999;  // This is what actually gets stored

Additionally, at this moment, we’ve told the SysTick timer how far to count, but it’s still not running. It’s like setting a kitchen timer to 1 minute but not pressing the start button yet.

Step 4: Resetting the Current Counter

SysTick->VAL = 0;

Moreover, this line clears any previous value in the timer’s current count register. We’re essentially saying “start from a clean slate.” The timer is still not running – we’re just making sure it starts from 0 when we do turn it on.

Step 5: The Magic Moment – Starting the Timer

Subsequently, this is the exact moment when the 1-millisecond timing begins!

SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
                SysTick_CTRL_TICKINT_Msk |
                SysTick_CTRL_ENABLE_Msk;

Let’s understand what each part does:

  • SysTick_CTRL_CLKSOURCE_Msk: “Use the main processor clock (16 MHz) for counting”
  • SysTick_CTRL_TICKINT_Msk: “When you finish counting down to 0, please interrupt the processor and call the SysTick_Handler() function”
  • SysTick_CTRL_ENABLE_Msk: “START COUNTING NOW!”

Furthermore, the moment this line executes, the SysTick timer springs to life:

  1. It begins counting down from 15999
  2. Every clock cycle (every 1/16,000,000th of a second), it decrements by 1
  3. After exactly 16000 clock cycles (1 millisecond), it reaches 0
  4. It immediately triggers an interrupt, calling SysTick_Handler()
  5. It automatically reloads with 15999 and starts counting again

Step 6: Return to main()

}  // End of SysTick_Init() function

Additionally, the SysTick_Init() function completes and execution returns to main(). But here’s the crucial point: the SysTick timer is now running independently in the background!

What Happens After SysTick_Init() Completes?

Moreover, this is the key insight: once SysTick_Init() finishes, the timer keeps running on its own. Every millisecond, regardless of what other code is executing, the SysTick timer:

  1. Counts down from 15999 to 0 (taking exactly 1ms)
  2. Triggers the SysTick_Handler() interrupt
  3. Increments the global systick_counter variable
  4. Automatically reloads and starts counting again

Let’s look at what the SysTick_Handler() does:

void SysTick_Handler(void) {
    /* Increment counter every 1ms */
    systick_counter++;
}

Furthermore, this handler runs automatically every millisecond from the moment we enable SysTick until the microcontroller is powered off or reset.

Timeline of SysTick Execution

Here’s the precise timeline of when things happen:

Time 0ms: SysTick_Init() is called
Time 0ms: SystemCoreClock_Get() called and returns
Time 0ms: LOAD register set to 15999
Time 0ms: VAL register cleared to 0
Time 0ms: CTRL register configured - TIMER STARTS HERE!
Time 0ms: SysTick_Init() returns to main()
Time 1ms: First SysTick_Handler() interrupt occurs, systick_counter = 1
Time 2ms: Second SysTick_Handler() interrupt occurs, systick_counter = 2
Time 3ms: Third SysTick_Handler() interrupt occurs, systick_counter = 3
… and so on, forever …

The Critical Understanding

Additionally, the most important concept is this: SysTick_Init() doesn’t “do” 1-millisecond timing – it sets up the hardware to automatically provide 1-millisecond timing forever after.

Think of it like setting up a grandfather clock:

  1. You wind it up (configure the registers)
  2. You set the pendulum swinging (enable the timer)
  3. From that moment on, it ticks every second without you doing anything else

Similarly, after SysTick_Init() completes, your microcontroller has a precise 1-millisecond heartbeat that will continue for as long as the system runs. Moreover, this heartbeat is what allows your task scheduler to know when 500ms has passed for the LED task, or when 1000ms has passed for another task.

Why This Timing is So Important

Furthermore, every embedded system needs a reliable timebase. Without SysTick providing this 1ms rhythm, you couldn’t:

  • Schedule tasks to run at specific intervals
  • Implement delays with SysTick_Delay()
  • Measure how long functions take to execute
  • Coordinate timing between different parts of your system

The beauty of SysTick is that once initialized, it becomes an invisible but crucial background service that keeps perfect time for your entire application.

The Mystery of SysTick_Handler(): Where is it Defined and How Does Hardware Find It?

Additionally, when I mentioned that the SysTick timer “triggers the SysTick_Handler() interrupt,” you might naturally wonder: Where is this function defined, and how does the hardware magically know to call a function with this exact name? This is one of the most fascinating aspects of embedded systems programming.

Where SysTick_Handler() is Defined

If you look in your systick.c file, you’ll find this simple but crucial function:

void SysTick_Handler(void) {
    /* Increment counter every 1ms */
    systick_counter++;
}

But here’s the interesting question: How does the SysTick timer hardware know to call a function called “SysTick_Handler”? After all, you could have named it my_timer_function() or bob() or anything else. Moreover, the answer reveals one of the most elegant aspects of embedded systems design.

The Magic of Special Function Names

Furthermore, in embedded systems, certain function names are special and predefined. When you create a function called SysTick_Handler(), you’re not just picking a random name – you’re implementing a contract with the hardware and startup system.

Think of it like this: imagine you’re running a hotel, and you tell your staff, “When the fire alarm goes off, call the person whose job title is ‘Fire Safety Manager.'” You don’t need to know who that person is or where they are – you just need to know that someone with that exact title will handle the emergency. Similarly, when SysTick timer reaches zero, the hardware says, “Call the function named SysTick_Handler.”

The Interrupt Vector Table Connection

Additionally, remember that interrupt vector table verification we did at the very beginning of main()? This is where it all connects! The interrupt vector table is like a phone directory that tells the hardware exactly which function to call for each type of interrupt.

Here’s how it works:

Step 1: Startup Code Sets Up the Vector Table

When your STM32F429ZI boots up, the startup assembly code (in startup_stm32f429zitx.s) creates an interrupt vector table that looks something like this:

/* Interrupt Vector Table */
.word  _estack                    /* Stack pointer */
.word  Reset_Handler              /* Reset handler */
.word  NMI_Handler                /* NMI handler */
.word  HardFault_Handler          /* Hard fault handler */
/* ... more system exceptions ... */
.word  SysTick_Handler            /* SysTick handler - HERE IT IS! */
.word  WWDG_IRQHandler            /* Window watchdog */
.word  PVD_IRQHandler             /* PVD through EXTI line */
/* ... more interrupt handlers ... */
.word  RTC_WKUP_IRQHandler        /* RTC wake-up - this is what we verified! */

Step 2: The Linker Connects Function Names to Addresses

Moreover, when you compile your code, the linker finds your SysTick_Handler() function in systick.c and notes its memory address. It then puts this address in the correct position in the interrupt vector table.

Step 3: Hardware Uses the Vector Table

When the SysTick timer counts down to zero, the hardware automatically:

  1. Looks up entry number 15 in the interrupt vector table (SysTick’s position)
  2. Finds the address of your SysTick_Handler() function
  3. Jumps to that address and executes your function
  4. Returns to whatever was running before

The Automatic Calling Mechanism

Furthermore, this is where the magic happens. Let’s trace through exactly what occurs during that first millisecond after SysTick_Init() completes:

Time 0.000ms: SysTick_Init() finishes, returns to main(), SysTick timer starts counting down from 15999

Time 1.000ms: SysTick timer reaches zero and automatically triggers an interrupt

What happens during the interrupt (this all occurs in microseconds):

  1. Hardware saves the current state: The processor automatically saves where it was in main() and what it was doing
  2. Hardware looks up the handler: It checks position 15 in the interrupt vector table and finds the address of SysTick_Handler()
  3. Automatic jump to your code: Execution jumps to your SysTick_Handler() function in systick.c
  4. Your code executes: void SysTick_Handler(void) { systick_counter++; }
  5. Automatic return: When your handler function ends, the hardware automatically restores the previous state and returns to exactly where it was in main()
  6. Timer reloads automatically: SysTick hardware reloads with 15999 and starts counting down again

Time 2.000ms: The process repeats, systick_counter becomes 2

Time 3.000ms: The process repeats, systick_counter becomes 3

The Connection to Your Vector Table Verification

Additionally, now you can see why that vector table verification at the beginning of main() was so important! When you checked:

uint32_t rtc_wkup_handler_addr = vector_table[RTC_WKUP_IRQn + 16];

You were verifying that the interrupt vector table correctly points to your RTC_WKUP_IRQHandler() function. Moreover, the same mechanism that makes SysTick_Handler() work automatically also makes your RTC wake-up handler work when the system wakes from sleep mode.

Why This Design is Brilliant

Furthermore, this interrupt mechanism is what makes embedded systems so powerful:

  1. Automatic and Precise: Once set up, the timing happens automatically without any intervention from your main program. Your main code can be doing anything – blinking LEDs, processing data, even sleeping – and the SysTick interrupt will still fire exactly every millisecond.
  2. Non-blocking: The main program doesn’t have to sit and wait for timing events. It can do useful work while the timer runs in the background.
  3. Deterministic: The interrupt occurs at precisely the right time, regardless of what other code is running.

What This Means for Your Program Flow

Additionally, after SysTick_Init() completes and returns to main(), your system now has two parallel flows of execution:

  1. Main Program Flow: Continues with the next line (UART_Init(115200)) and eventually enters the main loop where it runs the task scheduler
  2. Interrupt Flow: Every millisecond, execution briefly jumps to SysTick_Handler(), increments the counter, and returns

These two flows run independently and automatically. Moreover, the interrupt flow provides the timebase that your main program flow uses for task scheduling, delays, and timing measurements.

A Practical Example

Furthermore, let’s say your main program is in the middle of configuring UART when the first SysTick interrupt occurs:

Time 0.5ms:  main() is executing UART_Init(), configuring GPIO pins
Time 1.0ms:  SysTick interrupt fires!
             - Hardware saves UART_Init() state
             - Jumps to SysTick_Handler()
             - Executes: systick_counter++
             - Returns to exactly where it left off in UART_Init()
Time 1.001ms: main() continues UART_Init() as if nothing happened

Additionally, the beauty is that UART_Init() has no idea this interrupt occurred. The timing system runs completely transparently in the background.

The Foundation for Everything

Moreover, this interrupt-driven timing system is the foundation that makes your entire task scheduler possible. When your task scheduler later checks:

if ((currentTime - tasks[i].lastExecuted) >= tasks[i].period_ms)

It’s comparing against that systick_counter value that’s been steadily incrementing every millisecond since the moment SysTick_Init() completed.

Key Takeaways

  1. SysTick Configuration is Simple: Just three registers to set up
  2. Hardware Does the Hard Work: Automatic counting and interrupting
  3. Special Names Matter: SysTick_Handler is found via vector table
  4. Two Parallel Worlds: Main code and interrupt code run independently
  5. Foundation for Timing: Everything time-based depends on this

Common SysTick Issues and Solutions

Counter Not Incrementing:

  • Check if interrupts are globally enabled
  • Verify SysTick_Handler is properly defined
  • Ensure vector table is correctly located

Timing Inaccurate:

  • Verify SystemCoreClock_Get() returns correct value
  • Check clock configuration in system initialization
  • Ensure no other code modifies SysTick registers

System Hangs:

  • Check for interrupt priority conflicts
  • Verify handler executes quickly (just increment)
  • Ensure no blocking code in handler

What’s Next?

Now that we have a rock-solid 1ms timing foundation with systick_counter incrementing reliably, we can build powerful features on top. In Part 4, we’ll explore how the task scheduler uses this timing to manage multiple tasks with different priorities and periods.

Next: Part 4 – UART Initialization→


This is Part 3 of the STM32 IoT Framework series. Find the complete code on GitHub.

💡 Pro Tip: You can change the SysTick period by modifying the division factor. For a 10ms tick, use (SystemCoreClock_Get() / 100) - 1!

Leave a Comment

Your email address will not be published. Required fields are marked *

2 thoughts on “STM32 SysTick: Build a Reliable 1ms Heartbeat (Code Walkthrough)”

  1. Pingback: STM32 Boot Secrets: Your Code’s Journey to main() - Learn By Building

  2. Pingback: Part 4: Breakthrough UART Config – Debug Like Never Before - Learn By Building