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()insystem_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:
- It begins counting down from 15999
 - Every clock cycle (every 1/16,000,000th of a second), it decrements by 1
 - After exactly 16000 clock cycles (1 millisecond), it reaches 0
 - It immediately triggers an interrupt, calling 
SysTick_Handler() - 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:
- Counts down from 15999 to 0 (taking exactly 1ms)
 - Triggers the 
SysTick_Handler()interrupt - Increments the global 
systick_countervariable - 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:
- You wind it up (configure the registers)
 - You set the pendulum swinging (enable the timer)
 - 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:
- Looks up entry number 15 in the interrupt vector table (SysTick’s position)
 - Finds the address of your 
SysTick_Handler()function - Jumps to that address and executes your function
 - 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):
- Hardware saves the current state: The processor automatically saves where it was in 
main()and what it was doing - Hardware looks up the handler: It checks position 15 in the interrupt vector table and finds the address of 
SysTick_Handler() - Automatic jump to your code: Execution jumps to your 
SysTick_Handler()function insystick.c - Your code executes: 
void SysTick_Handler(void) { systick_counter++; } - Automatic return: When your handler function ends, the hardware automatically restores the previous state and returns to exactly where it was in 
main() - 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:
- 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.
 - 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.
 - 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:
- Main Program Flow: Continues with the next line (
UART_Init(115200)) and eventually enters the main loop where it runs the task scheduler - 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
- SysTick Configuration is Simple: Just three registers to set up
 - Hardware Does the Hard Work: Automatic counting and interrupting
 - Special Names Matter: 
SysTick_Handleris found via vector table - Two Parallel Worlds: Main code and interrupt code run independently
 - 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_Handleris 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!
Pingback: STM32 Boot Secrets: Your Code’s Journey to main() - Learn By Building
Pingback: Part 4: Breakthrough UART Config – Debug Like Never Before - Learn By Building