HomeUncategorizedSoftware Timers: Periodic & One-Shot

STM32 FreeRTOS Software Timers: How to Use Periodic and One-Shot Timers

In the previous part of this series, we covered event flags and how to use them to synchronize tasks and handle hardware interrupts cleanly. In this part, we will look at software timers.

A software timer lets you run a function after a set period of time, without blocking a task or burning CPU cycles in a delay loop. You can use it to blink an LED, poll a sensor at a fixed interval, trigger a timeout, or debounce a button.

In this tutorial, we will cover how software timers work in FreeRTOS, what the CMSIS-RTOS V2 API looks like, and we will build examples using both periodic and one-shot timers on an STM32 board.

If you have not gone through the previous parts of this series, here are the links:

STM32 FreeRTOS Software Timers: How to Use Periodic and One-Shot Timers

What Is a Software Timer in FreeRTOS?

Let’s first understand what a software timer is and how it works inside FreeRTOS. This section covers the basics — why we use software timers, how FreeRTOS runs them internally, and the difference between the two timer types.

Why Use Software Timers Instead of Hardware Timers

In embedded systems, hardware timers are a limited resource. A microcontroller typically has only a handful of them, and they are often already occupied by peripherals like PWM, input capture, or ADC triggering.

A software timer solves this by sitting on top of a single hardware timer managed by FreeRTOS. You can create as many software timers as the heap allows, and each one calls a callback function when its period expires.

The image below shows how multiple software timers share a single hardware timer resource.

Diagram comparing hardware timer usage without FreeRTOS (two hardware timers consumed by PWM and input capture, none left for tasks) vs with FreeRTOS software timers (one hardware timer driving three virtual software timers, each with its own callback).

How the Timer Daemon Task Works

FreeRTOS does not run timer callbacks inside the task or inside an interrupt. Instead, it has a dedicated background task called the timer daemon task (also known as the timer service task).

When a timer expires, FreeRTOS sends a message to the daemon task through an internal queue. The daemon task picks up that message and calls the callback function.

The image below shows how the timer daemon task fits into the FreeRTOS system.

Flowchart showing that when a FreeRTOS timer expires, it sends a message to the timer queue, which wakes the timer daemon task, which then calls the callback function. A note below warns to keep callbacks short and set daemon priority correctly.

Two things to always keep in mind:

  • Keep callbacks short and non-blocking. If you put a long delay inside a callback, the daemon task is blocked and all other waiting callbacks are delayed.
  • Set the daemon task priority correctly. If the daemon task has a lower priority than your other tasks, callbacks will not fire on time. You can configure this in CubeMX under FreeRTOS advanced settings.

Periodic Timer vs One-Shot Timer

There are two types of software timers in FreeRTOS.

  • A periodic timer keeps repeating. When the period expires, the callback runs, the timer resets, and it starts counting again. This continues until you explicitly stop it.
  • A one-shot timer fires only once. After the period expires and the callback runs, the timer stops on its own. To run it again, you must restart it manually.

The image below shows the difference in behavior between the two timer types.

Timeline diagram comparing periodic and one-shot timers. The periodic timer shows three consecutive equal periods each ending with a callback, then continuing. The one-shot timer shows a single period, one callback, then a dashed line indicating the timer has stopped, with a note to call osTimerStart() to restart it.

FreeRTOS Software Timer APIs

FreeRTOS provides a set of functions to create and manage software timers. In STM32CubeIDE with CMSIS-RTOS V2, these functions use the osTimer prefix. We will use four of them in this tutorial. Let us go through each one.

osTimerNew: Creating a Software Timer

This function creates a new software timer and returns a handle that we use in every subsequent call.

osTimerId_t osTimerNew(osTimerFunc_t func, osTimerType_t type, void *argument, const osTimerAttr_t *attr);

The parameters are as follows:

  • func — the callback function to call when the timer expires.
  • type — either osTimerPeriodic for a repeating timer, or osTimerOnce for a one-shot timer.
  • argument — a pointer passed to the callback. Set to NULL if we do not need it.
  • attr — timer attributes such as name and memory allocation settings. CubeMX generates this for us.

When we create a timer through the CubeMX GUI, this function is called automatically inside main() function. You will see something like this in the generated code:

PeriodicTimerHandle = osTimerNew(PTCallback, osTimerPeriodic, NULL, &PeriodicTimer_attributes);
OneShotTimerHandle = osTimerNew(OSTCallback, osTimerOnce, NULL, &OneShotTimer_attributes);

We do not need to call this manually. CubeMX handles it.


osTimerStart: Starting a Timer

Creating a timer does not start it automatically. We need to start it explicitly using:

osStatus_t osTimerStart(osTimerId_t timer_id, uint32_t ticks);
  • timer_id — the handle returned by osTimerNew.
  • ticks — the time period in milliseconds.

For example, to start the periodic timer with a one-second interval:

osTimerStart(PeriodicTimerHandle, 1000);

And to start the one-shot timer with a five-second delay:

osTimerStart(OneShotTimerHandle, 5000);

One important thing to note — if you call osTimerStart on a timer that is already running, it does not create a second timer. It simply resets the existing one and restarts the countdown from the new value. We will use this behavior intentionally when we demonstrate the one-shot timer reset later.

The image below shows what happens when osTimerStart is called on an already-running timer.

Timeline diagram showing a one-shot timer with a 5-second period. When osTimerStart is called again midway, the original expiry is cancelled and a full new 5-second period starts from that point. The callback only fires at the end of the new period.

osTimerStop: Stopping a Timer

To stop a running timer before it expires, we use:

osStatus_t osTimerStop(osTimerId_t timer_id);

Calling this on a periodic timer stops it from repeating. Calling it on a one-shot timer cancels it before it fires. In both cases, the callback is not called.

For example, to stop the periodic timer:

osTimerStop(PeriodicTimerHandle);

The function returns an osStatus_t value. It returns osOK if the timer was stopped successfully. If you call it on a timer that is not running, it returns osErrorResource. You can check this return value if your application needs error handling, but in most cases we can ignore it.


osTimerIsRunning: Checking Timer State

Before starting or stopping a timer, it is useful to check whether it is currently active. We do this with:

uint32_t osTimerIsRunning(osTimerId_t timer_id);

This function returns 1 if the timer is running, and 0 if it is stopped.

We will use this to conditionally start or stop timers. For example:

if (osTimerIsRunning(PeriodicTimerHandle))
{
    osTimerStop(PeriodicTimerHandle);
}

And to restart it only if it is currently stopped:

if (!osTimerIsRunning(periodic_timerHandle))
{
    osTimerStart(PeriodicTimerHandle, 1000);
}

CubeMX Setup for FreeRTOS Software Timers

Let us set up the project in CubeMX. We will configure the timers, UART, and GPIO for this project.

Creating the Timers in CubeMX

Go to the Tasks and Queues tab. A default task is already created here with normal priority and a stack size of 128 words. We will use this task in our project, so leave it as it is.

The image below shows the default task configuration.

Tasks and Queues tab showing the default task

Now go to the Timers and Semaphores tab. You will see a dedicated section for software timers. Click Add to create the first timer.

Set the following for the first timer:

  • Type — Periodic
  • Timer NamePeriodicTimer
  • CallbackPTCallback
  • Allocation — Dynamic

The image below shows the first timer configured as a periodic timer.

STM32 Software timer periodic timer configuration.

Click Add again to create the second timer. Set the following:

  • Type — Timer Once
  • Timer NameOneShotTimer
  • CallbackOSTCallback
  • Allocation — Dynamic

The image below shows the second timer configured as a one-shot timer.

STM32 Software timer one shot time rconfiguration.

The table below summarizes the timer configuration:

ParameterPeriodic TimerOne-Shot Timer
TypePeriodicOne Shot
NamePeriodicTimerOneShotTimer
CallbackPTCallbackOSTCallback
AllocationDynamicDynamic

Configuring LPUART1 for printf Output

We will use printf to print the received data to a serial console. On the STM32L496 Nucleo board, the Virtual COM port routes UART data through the ST-LINK USB connection. Looking at the board schematic, ST-LINK RX is connected to LPUART1 TX and ST-LINK TX is connected to LPUART1 RX. These map to pins PG7 and PG8.

Image shows the virtual com port connection in STM32L496 Nucleo.

Go to Connectivity and enable LPUART1 in Asynchronous mode. By default, CubeMX assigns pins PC0 and PC1, so change these to PG7 (TX) and PG8 (RX).

Image shows the LPUART configuration in Nucleo L496 to print the data on the serial console.

Use the following settings:

ParameterValue
Baud Rate115200
Word Length8 bits
ParityNone
Stop Bits1

This is the standard UART configuration and matches what we will set in the serial console later.


Configuring GPIO for LED and button

Now we will configure the onboard LED. According to the board schematic, the LEDs on Nucleo-L496 are connected as shown below:

Image shows the LED connections on Nucleo-L496 board.

I am just going to use the Blue LED for this tutorial, which is connected to pin PB7. Therefore, configure the pins PB7 in the output mode. This is shown in the image below.

Pin PB7 is configured as output in the STM32CubeMX.

We also need a button. I have connected the button between pin PA3 and Ground. The pin will be high by default, and when the button is pressed, it will be pulled low to the ground.

Image shows how the button is connected between the nucleo pin and ground.

Configure the pin PA3 in the GPIO Input Mode. Set the pull to Pull-up so the pin stays high by default and goes low when the button is pressed.

GPIO Input configuration for pin PA3 in STM32CubeMX.

Changing HAL Time Base from SysTick to TIM6

This step is very important in STM32 FreeRTOS projects. By default, STM32 uses SysTick as the HAL time base. But FreeRTOS also uses SysTick for its scheduler.
If both HAL and FreeRTOS share SysTick, it may create timing conflicts.

So we should dedicate:

  • SysTick → FreeRTOS Scheduler
  • TIM6 (or TIM7) → HAL Time Base
Image shows how to change the timebase when using FreeRTOS in STM32.
  • Go to System Core → SYS
  • Change Timebase Source from SysTick to TIM6 (or TIM7)

TIM6 and TIM7 are basic timers. They do not support PWM or advanced features. That is why they are ideal for this purpose.


Enabling Newlib Reentrant Support

Go to Middleware → FreeRTOS → Advanced Settings and Enable Use newlib reentrant.

Image shows how to enable NewLib Reentrant for FreeRTOS in STM32.

In an RTOS environment, multiple tasks may call functions like printf(); sprintf(); malloc(); etc. These standard C library functions are not thread-safe by default.

If two tasks call printf() at the same time, the output may get corrupted. Enabling newlib reentrant makes these functions safe in multitasking systems. It uses slightly more memory, but it prevents difficult debugging issues later.

Writing the Code for Software Timers

CubeMX has already generated the initialization code for both timers inside main()function:

PeriodicTimerHandle = osTimerNew(PTCallback, osTimerPeriodic, NULL, &PeriodicTimer_attributes);
OneShotTimerHandle  = osTimerNew(OSTCallback, osTimerOnce,    NULL, &OneShotTimer_attributes);

Both timers are created but not started. We need to start them manually from within our code. Let us go through each part.

Starting the Periodic Timer and Writing Its Callback

We start the periodic timer at the beginning of StartDefaultTask, before the infinite loop. Since this line runs only once, it is the right place to start a timer that is meant to run continuously.

void StartDefaultTask(void *argument)
{
    osTimerStart(PeriodicTimerHandle, 1000);

    for(;;)
    {
        // ...
        osDelay(1);
    }
}

We pass 1000 as the period, so the callback fires every one second.

Now let us write the callback. The periodic timer callback PTCallback simply prints a log message to the serial console to confirm it is running.

void PTCallback(void *argument)
{
    printf("Data Sent from Periodic Timer's Callback\n");
}

Keep in mind that this callback runs inside the timer daemon task. Keep it short. Do not add any blocking calls or long delays here.


Starting the One-Shot Timer on Button Press

Inside the infinite loop, we poll pin PA3. If it reads GPIO_PIN_RESET, the button has been pressed and the pin has been pulled LOW. At that point, we turn the LED on and start the one-shot timer with a 5000ms delay.

for(;;)
{
    if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_3) == GPIO_PIN_RESET)
    {
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_SET);
        osTimerStart(OneShotTimerHandle, 5000);
    }
    osDelay(1);
}

Five seconds after the button is pressed, the one-shot timer expires and OSTCallback is called. Inside this callback, we turn the LED off and print a log message to confirm the timer has fired.

void OSTCallback(void *argument)
{
    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_RESET);
    printf("##### TIMER EXPIRED #####\n\n");
}

Notice the behavior when the button is pressed multiple times before the five seconds are up. Every call to osTimerStart on an already-running one-shot timer resets it and pushes the expiry forward by another 5000ms. The timer never fires as long as you keep pressing within the window. This is useful for implementing inactivity detection or debounce logic.


We also handle the periodic timer inside the default task and the one-shot callback. When the button is pressed, we check if the periodic timer is running and stop it. When the one-shot timer expires, we check if the periodic timer is stopped and restart it.

/* In the default task — stop periodic timer on button press */
if (osTimerIsRunning(PeriodicTimerHandle) == 1)
{
    osTimerStop(PeriodicTimerHandle);
}

/* In OSTCallback — restart periodic timer after one-shot expires */
if (osTimerIsRunning(PeriodicTimerHandle) == 0)
{
    osTimerStart(PeriodicTimerHandle, 1000);
}

We will cover this behavior in detail in the next section. For now, let us look at the complete code.


Complete Code

Here is the full code combining everything we have written so far.

/* USER CODE BEGIN Includes */
#include "stdio.h"
/* USER CODE END Includes */

/* USER CODE BEGIN 0 */
int __io_putchar(int ch)
{
    uint8_t c = ch;
    HAL_UART_Transmit(&hlpuart1, &c, 1, HAL_MAX_DELAY);
    return ch;
}
/* USER CODE END 0 */

void StartDefaultTask(void *argument)
{
    /* Start the periodic timer — fires every 1 second */
    osTimerStart(PeriodicTimerHandle, 1000);

    for(;;)
    {
        /* Check for button press — PA3 reads LOW when pressed */
        if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_3) == GPIO_PIN_RESET)
        {
            /* Turn LED on */
            HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_SET);

            /* Start one-shot timer — fires once after 5 seconds */
            osTimerStart(OneShotTimerHandle, 5000);

            /* Stop the periodic timer while one-shot is running */
            if (osTimerIsRunning(PeriodicTimerHandle) == 1)
            {
                osTimerStop(PeriodicTimerHandle);
            }
        }
        osDelay(1);
    }
}

/* Periodic timer callback — called every 1 second */
void PTCallback(void *argument)
{
    printf("Data Sent from Periodic Timer's Callback\n");
}

/* One-shot timer callback — called once after 5 seconds */
void OSTCallback(void *argument)
{
    /* Turn LED off */
    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_RESET);

    printf("##### TIMER EXPIRED #####\n\n");

    /* Restart the periodic timer if it was stopped */
    if (osTimerIsRunning(PeriodicTimerHandle) == 0)
    {
        osTimerStart(PeriodicTimerHandle, 1000);
    }
}

A few things to note about this code:

  • The __io_putchar function redirects printf output through LPUART1 to the serial console. Make sure stdio.h is included at the top.
  • osDelay(1) inside the task loop gives the scheduler a chance to run other tasks and also acts as a basic debounce for the button.
  • Both osTimerIsRunning checks use the return value to avoid redundant start or stop calls on a timer that is already in the desired state.

Output

The GIF below shows the complete behavior of both timers running on the STM32 Nucleo board.

Screen recording showing the STM32 Nucleo board and a serial console side by side. The console prints "Data Sent from Periodic Timer's Callback" every second. When the button is pressed, the blue LED on the board turns on and the periodic timer logs stop. After five seconds, the LED turns off, the message "##### TIMER EXPIRED #####" appears on the console, and the periodic timer logs resume. In a second demonstration, the button is pressed multiple times within the five-second window, causing the LED to stay on and the expiry message to be delayed each time.

The periodic timer prints a log message every second. Pressing the button turns the LED on and starts the one-shot timer. After five seconds, the LED turns off and the TIMER EXPIRED message appears on the console. Pressing the button again before the five seconds are up resets the one-shot timer, keeping the LED on and pushing the expiry forward.

Stopping and Restarting a Timer Dynamically

So far we have seen how to start both timers. But FreeRTOS also lets us stop a running timer and restart it later from a different point in the code. In this section, we will use osTimerStop and osTimerIsRunning to stop the periodic timer when the button is pressed, and restart it automatically once the one-shot timer expires.

Stopping the Periodic Timer on Button Press

Inside the default task, we already start the one-shot timer and turn the LED on when the button is pressed. We now add one more step — we check if the periodic timer is currently running and stop it.

if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_3) == GPIO_PIN_RESET)
{
    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_SET);
    osTimerStart(OneShotTimerHandle, 5000);

    if (osTimerIsRunning(PeriodicTimerHandle) == 1)
    {
        osTimerStop(PeriodicTimerHandle);
    }
}

We use osTimerIsRunning before calling osTimerStop. This avoids calling stop on a timer that is already stopped — for example, if the button is pressed a second time while the one-shot timer is still counting. In that case, the periodic timer is already stopped, so we skip the stop call entirely.


Restarting the Periodic Timer from the One-Shot Callback

Once the one-shot timer expires, OSTCallback is called. Inside this callback, we turn the LED off and then check if the periodic timer is stopped. If it is, we restart it.

void OSTCallback(void *argument)
{
    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_RESET);
    printf("##### TIMER EXPIRED #####\n\n");

    if (osTimerIsRunning(PeriodicTimerHandle) == 0)
    {
        osTimerStart(PeriodicTimerHandle, 1000);
    }
}

Again, we use osTimerIsRunning before restarting. This ensures we do not accidentally restart a timer that is already running — for example, if somehow the periodic timer was never stopped before this callback fired.


Complete Code

Here is the full code for this section with both the stop and restart logic included.

void StartDefaultTask(void *argument)
{
    /* Start the periodic timer — fires every 1 second */
    osTimerStart(PeriodicTimerHandle, 1000);

    for(;;)
    {
        if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_3) == GPIO_PIN_RESET)
        {
            /* Turn LED on */
            HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_SET);

            /* Start one-shot timer — fires once after 5 seconds */
            osTimerStart(OneShotTimerHandle, 5000);

            /* Stop the periodic timer if it is currently running */
            if (osTimerIsRunning(PeriodicTimerHandle) == 1)
            {
                osTimerStop(PeriodicTimerHandle);
            }
        }
        osDelay(1);
    }
}

/* Periodic timer callback — called every 1 second */
void PTCallback(void *argument)
{
    printf("Data Sent from Periodic Timer's Callback\n");
}

/* One-shot timer callback — called once after 5 seconds */
void OSTCallback(void *argument)
{
    /* Turn LED off */
    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_RESET);

    printf("##### TIMER EXPIRED #####\n\n");

    /* Restart the periodic timer if it is currently stopped */
    if (osTimerIsRunning(PeriodicTimerHandle) == 0)
    {
        osTimerStart(PeriodicTimerHandle, 1000);
    }
}

Output

The GIF below shows the full dynamic behavior on the STM32 Nucleo board. The periodic timer runs and prints a log every second. When the button is pressed, the periodic timer stops and the LED turns on. After five seconds, the one-shot timer expires, the LED turns off, and the periodic timer restarts automatically.

Screen recording showing the STM32 Nucleo board and serial console side by side. The console prints "Data Sent from Periodic Timer's Callback" every second. When the button is pressed, the logs stop and the blue LED turns on. After five seconds, the LED turns off, the message "##### TIMER EXPIRED #####" appears on the console, and the periodic timer logs resume automatically.

Video Tutorial

STM32 FreeRTOS Software Timers — Periodic and One-Shot Timer Tutorial Video

Watch me configure both timers in CubeMX, write the task function using osTimerStart() and osTimerStop(), implement the periodic and one-shot callbacks, demonstrate the reset behavior on button press, and verify the output on the serial console.

Watch the FreeRTOS Software Timers Tutorial

Conclusion

We saw how to create both a periodic timer and a one-shot timer in CubeMX, how the timer daemon task handles callback execution internally, and why it is important to keep callbacks short and non-blocking. We then wrote the code to start, stop, and restart timers dynamically using osTimerStart, osTimerStop, and osTimerIsRunning, and verified the behavior on the STM32 Nucleo board.

Software timers are extremely useful in embedded applications because they let you execute time-based logic without blocking a task or wasting CPU cycles in a delay loop. The periodic timer is a clean replacement for any repeated polling or toggling you would normally do inside a task with a fixed delay. The one-shot timer is particularly powerful for timeout detection and inactivity monitoring — and as we saw, calling osTimerStart on an already-running one-shot timer resets it, which gives you a simple and reliable way to implement watchdog-style logic within FreeRTOS.

Browse More CMSIS RTOS Tutorials

STM32 RTOS Project Download

Info

You can help with the development by DONATING Below.
To download the project, click the DOWNLOAD button.

STM32 FreeRTOS Timers FAQs

Subscribe
Notify of

0 Comments
Newest
Oldest Most Voted
Inline Feedbacks
View all comments