Series / Porting FreeRTOS to STM32 From Scratch / Setting Up the SysTick Timer for the RTOS Tick

Setting Up the SysTick Timer for the RTOS Tick

Connect the FreeRTOS scheduler tick to the Cortex-M SysTick timer on STM32 and understand how tick rate, CPU clock, interrupt priority, and delay timing fit together.

On this page

The previous article configured FreeRTOSConfig.h for first STM32 bring-up. That configuration included two timing values that now need to become real hardware behavior:

#define configCPU_CLOCK_HZ    (SystemCoreClock)
#define configTICK_RATE_HZ    ((TickType_t)1000)

Those settings tell FreeRTOS what clock rate and tick frequency the project expects. They do not, by themselves, make an interrupt happen.

This article connects the RTOS tick to the Cortex-M SysTick timer. The goal is to understand what the tick does, how the tick rate maps to a timer reload value, how the handler reaches FreeRTOS, and what can go wrong when STM32 HAL and FreeRTOS both care about time.

This article does not wire PendSV or SVC yet. It also does not create the first task yet. The tick is one piece of the scheduler plumbing, and it is easier to debug when it is treated as its own boundary.

What the RTOS Tick Does

The RTOS tick is a periodic interrupt used by the kernel to track time.

FreeRTOS uses the tick to:

  • Update the kernel tick count.
  • Move delayed tasks back to the ready state when their delay expires.
  • Support APIs such as vTaskDelay() and vTaskDelayUntil().
  • Decide whether a context switch should be requested after time moves forward.

The tick is not the only reason a context switch can happen. A task can block on a queue, an interrupt can unblock a task, and a task can yield explicitly. But the tick is the regular heartbeat that makes time-based scheduling work.

If the tick never fires, a task delayed with vTaskDelay() will not become ready again. If the tick rate is wrong, delays will be too fast or too slow. If the tick interrupt is connected to the wrong handler, the scheduler may start but time-based behavior will fail immediately.

Why Use Cortex-M SysTick

SysTick is a system timer built into the Cortex-M core.

That makes it different from STM32 peripheral timers such as TIM2, TIM3, or TIM6. Those timers belong to the STM32 peripheral set. SysTick belongs to the CPU core architecture.

FreeRTOS Cortex-M ports commonly use SysTick as the scheduler tick source because it is available across Cortex-M microcontrollers and fits the kernel's need for a simple periodic interrupt.

Using SysTick for first bring-up has a practical benefit: it keeps the first RTOS tick independent of STM32 peripheral timer setup. The series can focus on the Cortex-M port path before introducing other timer choices.

That does not mean SysTick is always the best production tick source. Some low-power designs use a different timer. Some STM32 HAL configurations move the HAL time base away from SysTick when an RTOS is used. For this stage, SysTick is the clearest first path.

Tick Rate and Reload Value

SysTick counts down from a reload value and raises an interrupt when it reaches zero.

Conceptually, the reload value for a periodic RTOS tick is:

reload = configCPU_CLOCK_HZ / configTICK_RATE_HZ - 1

If the CPU clock is 80 MHz and the tick rate is 1000 Hz, the timer needs one interrupt every 1 ms:

reload = 80000000 / 1000 - 1
reload = 79999

That calculation depends on the clock source used by SysTick. If SysTick is driven from the core clock, then configCPU_CLOCK_HZ must match the core clock. If the clock tree changes and SystemCoreClock is not updated, the RTOS tick will be wrong.

This is why the configuration article used SystemCoreClock for configCPU_CLOCK_HZ. It keeps the FreeRTOS configuration tied to the same runtime clock value the STM32 system code maintains.

What FreeRTOS Expects From SysTick_Handler

The vector table contains an entry for SysTick_Handler.

When the SysTick interrupt fires, the CPU branches to that handler. In a FreeRTOS Cortex-M port, the handler must eventually call the FreeRTOS tick handler for the selected port.

Many FreeRTOS Cortex-M ports expose a handler named:

xPortSysTickHandler

A simple project-level handler may look like:

void SysTick_Handler(void)
{
    xPortSysTickHandler();
}

Some projects instead map the vector name directly to the FreeRTOS handler with a preprocessor definition or startup-file alias. The important point is not the exact style. The important point is that the SysTick vector reaches the FreeRTOS port's tick path.

If the vector table still points to a weak default handler, the first tick may send the firmware into an infinite default-handler loop. If the handler only updates the STM32 HAL tick and never calls FreeRTOS, scheduler time will not advance.

SysTick Priority

The tick interrupt has a priority like other Cortex-M exceptions.

For FreeRTOS, that priority must be compatible with the interrupt priority rules configured in FreeRTOSConfig.h.

The kernel tick should normally run at the lowest interrupt priority used by the kernel:

NVIC_SetPriority(SysTick_IRQn, configLIBRARY_LOWEST_INTERRUPT_PRIORITY);

The exact API and value depend on the CMSIS and configuration style used by the project. The concept is what matters: the RTOS tick must not be configured as a high-urgency interrupt that violates the kernel's masking and syscall priority assumptions.

Cortex-M priority numbering is inverted. Priority number 0 is the highest urgency. A larger number such as 15 is lower urgency on a device with 4 implemented priority bits.

This is a common source of mistakes. "Lowest priority" often means the largest numeric priority value, not zero.

HAL Tick vs FreeRTOS Tick

STM32 HAL often uses SysTick for its own millisecond time base.

That creates an important integration question: if FreeRTOS also uses SysTick, who owns the handler?

There are several possible strategies:

  • Let FreeRTOS own SysTick and avoid HAL delay behavior after the scheduler starts.
  • Call both the HAL tick increment and the FreeRTOS tick handler from SysTick_Handler during a controlled transition.
  • Move the HAL time base to a different STM32 timer.
  • Avoid HAL time-dependent APIs in RTOS task code.

There is no universal answer for every STM32 project. The right choice depends on how much HAL code the firmware uses and whether HAL timeouts are needed after the scheduler starts.

For this learning path, keep the rule simple: do not hide the ownership. If SysTick is the FreeRTOS tick, the handler path must make that obvious. If HAL still needs a time base, make that decision explicit instead of letting generated code and RTOS code both assume they own the same interrupt.

A Minimal SysTick Setup Shape

The FreeRTOS Cortex-M port often provides a function that configures the tick as part of scheduler startup. Depending on the port and configuration, the project may not need to call SysTick_Config() manually before starting the scheduler.

Still, the conceptual setup looks like this:

uint32_t reload = (configCPU_CLOCK_HZ / configTICK_RATE_HZ) - 1U;

SysTick->LOAD = reload;
SysTick->VAL = 0U;
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
                SysTick_CTRL_TICKINT_Msk   |
                SysTick_CTRL_ENABLE_Msk;

This shows the moving parts:

  • LOAD holds the reload value.
  • VAL clears the current count.
  • CTRL selects the clock source, enables interrupts, and starts the timer.

In a real FreeRTOS project, prefer the setup path expected by the selected port. The goal of showing the register shape is to make the mechanism understandable, not to replace the port's normal scheduler startup path.

What Should Work After This Step

After the tick path is set up correctly, the firmware has a reliable periodic interrupt path for kernel time.

That still does not mean the scheduler can run tasks yet.

The project still needs to wire the context-switching exceptions used by the port, especially PendSV and sometimes SVC. It also needs to create at least one task and call vTaskStartScheduler().

The checkpoint for this article is narrower:

  • The project knows what the RTOS tick is for.
  • configCPU_CLOCK_HZ and configTICK_RATE_HZ have a concrete meaning.
  • SysTick_Handler reaches the FreeRTOS tick path.
  • The tick priority is compatible with FreeRTOS rules.
  • HAL tick ownership is an explicit decision, not an accident.

Common Tick Setup Mistakes

One common mistake is assuming configTICK_RATE_HZ configures hardware by itself. It does not. It is a configuration value the port uses when setting up the tick source.

Another mistake is using a stale SystemCoreClock value. If the STM32 clock tree changes but SystemCoreClock is not updated, tick timing will be wrong.

A third mistake is leaving SysTick_Handler pointed at a default weak handler. The code may build and link correctly, then fault or hang as soon as the first tick arrives.

A fourth mistake is letting HAL and FreeRTOS both assume they own SysTick. That can create missing HAL delays, missing RTOS ticks, or duplicate time-base updates.

A fifth mistake is assigning the tick an interrupt priority that conflicts with the configured FreeRTOS priority rules. Keep assertions enabled so priority mistakes are caught early.

Checklist Before Wiring PendSV and SVC

Before moving on, confirm:

  • configCPU_CLOCK_HZ matches the clock used by the tick source.
  • configTICK_RATE_HZ is chosen deliberately.
  • The SysTick vector reaches the FreeRTOS tick handler path.
  • The SysTick priority is a valid kernel interrupt priority.
  • HAL time-base ownership is understood.
  • The project is not relying on HAL_Delay() inside future RTOS tasks.
  • configASSERT remains enabled.

With those decisions clear, the next step is to wire the exceptions used for context switching.

Next Steps

The next article wires PendSV and SVC for context switching.

SysTick gives FreeRTOS a periodic time base, but it is not the whole scheduler. The Cortex-M port also needs exception handlers that can start the first task and switch between task contexts. That is where the vector table, startup code, and portable layer meet directly.