Chapter 15: Timer Services in FreeRTOS
Chapter Objectives
- Understand the concept and purpose of software timers in an RTOS context.
- Learn about the FreeRTOS Timer Service (Daemon) Task.
- Differentiate between one-shot and auto-reload timers.
- Master the FreeRTOS Timer API functions for creating, starting, stopping, resetting, and deleting timers.
- Understand the execution context and restrictions of timer callback functions.
- Implement practical examples using software timers on the ESP32.
- Identify and troubleshoot common issues related to timer usage.
Introduction
In previous chapters, we explored task management, communication, and synchronization primitives in FreeRTOS. While task delays (vTaskDelay
) are useful for simple periodic actions or yielding, they are often inefficient or unsuitable for managing multiple, potentially short-duration, timed events, especially when those events don’t require the overhead of a dedicated task.
Consider scenarios like timing out a network request, blinking an LED at a specific rate without blocking a main task, or triggering a sensor reading periodically. Implementing these with dedicated tasks and delays can lead to unnecessary resource consumption (stack memory, context switching overhead).
This is where FreeRTOS software timers provide an elegant and efficient solution. They allow you to schedule the execution of a function (a “callback”) at a specific time in the future or periodically, without needing a dedicated task for each timed event. These timers are managed centrally by FreeRTOS, offering a lightweight mechanism for time-based event handling. This chapter explores the theory and practical application of FreeRTOS software timers within the ESP-IDF environment.
Theory
What are Software Timers?
Software timers are timers implemented purely in software, managed by the RTOS kernel, as opposed to hardware timers which are dedicated peripheral units on the microcontroller. FreeRTOS software timers allow application developers to define callback functions that the kernel will execute at a specified time or interval.
Key characteristics include:
Characteristic | Description |
---|---|
Callback-based | Timers execute a user-defined function (callback) when they expire. |
Managed by RTOS | The FreeRTOS kernel handles the scheduling and execution of timer callbacks, abstracting low-level hardware details. |
Efficiency | Generally more resource-efficient (CPU time, memory) than creating a separate task for simple or numerous timed events. |
Resolution | Limited by the FreeRTOS tick rate (configTICK_RATE_HZ ). The minimum effective timer period is one tick. |
Context | All timer callbacks execute in the context of the Timer Service Task, not directly from an interrupt (unless configTIMER_TASK_DEFERRED_PROCESSING is 0, which is less common). |
The Timer Service (Daemon) Task
FreeRTOS does not execute timer callback functions directly from an interrupt context. Instead, it uses a dedicated, standard FreeRTOS task called the Timer Service Task (often referred to as the Timer Daemon Task).
When you interact with the timer API (e.g., starting, stopping, or resetting a timer), you are not directly manipulating the timer’s state but rather sending a command to this Timer Service Task via a dedicated Timer Command Queue.
graph LR subgraph "Application Domain" TaskA["Application Task A"] TaskB["Application Task B"] TaskC["..."] end subgraph "FreeRTOS Kernel Space" CmdQueue["Timer Command Queue<br>(FIFO)"] TimerServiceTask["Timer Service Task<br>(Daemon)"] subgraph "Timer Callback Execution" Callback1["TimerCallback1()"] Callback2["TimerCallback2()"] CallbackN["..."] end end TaskA -- "xTimerStart(Timer1)" --> CmdQueue TaskB -- "xTimerStop(Timer2)" --> CmdQueue TaskC -- "xTimerReset(Timer3)" --> CmdQueue CmdQueue -- "Processes Commands" --> TimerServiceTask TimerServiceTask -- "Timer1 Expires" --> Callback1 TimerServiceTask -- "TimerX Expires" --> Callback2 TimerServiceTask -- "TimerN Expires" --> CallbackN %% Styling classDef primary fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6; classDef process fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF; classDef queue fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E; %% Using decision node style for queue classDef callback fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46; %% Using success node style for callbacks class TaskA,TaskB,TaskC primary; class TimerServiceTask process; class CmdQueue queue; class Callback1,Callback2,CallbackN callback;
The Timer Service Task is a standard FreeRTOS task that runs at a specific priority and has its own stack. Its responsibilities include:
- Receiving timer commands from the Timer Command Queue.
- Processing these commands (e.g., adding a timer to the active list, removing it).
- Keeping track of active timers and their expiry times.
- When a timer expires, executing its associated callback function.
The configuration of this task is crucial for the correct operation of software timers:
Configuration Parameter (menuconfig) | Description | Common/Default Value |
---|---|---|
CONFIG_FREERTOS_TIMER_TASK_PRIORITY |
Sets the priority of the Timer Service Task. It should be high enough for timely processing but typically not the absolute highest. | configMAX_PRIORITIES - 1 (often, but check project defaults) |
CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH |
Defines the stack size (in words or bytes, depending on FreeRTOS port) allocated to the Timer Service Task. Must be sufficient for command processing and the most complex timer callback. | Typically 2048 bytes or higher (e.g., 2048 , 4096 ). Check ESP-IDF defaults. |
CONFIG_FREERTOS_TIMER_QUEUE_LENGTH |
Sets the capacity of the Timer Command Queue. This determines how many timer API calls (start, stop, etc.) can be buffered. | Typically 10 or 20 . |
These settings can be adjusted via menuconfig
under Component config
-> FreeRTOS
-> Timers
.
Warning: Since all timer callbacks execute in the context of the single Timer Service Task, it is critical that callback functions execute quickly and never block (e.g., by calling
vTaskDelay
, waiting indefinitely for a semaphore, or performing lengthy I/O operations). Blocking the Timer Service Task will prevent all other software timers from being serviced.
Timer Types
FreeRTOS provides two types of software timers:
- One-Shot Timers: Execute their callback function once after the defined period has elapsed since the timer was started. After execution, the timer transitions back to a dormant (inactive) state. It must be explicitly restarted to run again. Useful for implementing timeouts or delayed actions.
- Auto-Reload Timers: Execute their callback function periodically. After the callback executes, the timer automatically restarts itself for the same initial period. Useful for recurring events like polling sensors, blinking LEDs, or sending periodic keep-alive messages.
Feature | One-Shot Timer | Auto-Reload Timer |
---|---|---|
Execution | Executes its callback function once after the defined period elapses. | Executes its callback function periodically. Automatically restarts after each execution. |
State After Expiry | Transitions to the Dormant (inactive) state after the callback executes. | Remains in the Running state and reloads its period to expire again. |
Restart | Must be explicitly restarted (e.g., using xTimerStart() or xTimerReset() ) to run again. |
Automatically restarts its period unless explicitly stopped. |
uxAutoReload Parameter in xTimerCreate() |
Set to pdFALSE . |
Set to pdTRUE . |
Common Use Cases | Implementing timeouts, delaying a single action, scheduling a one-time event. | Recurring events like polling sensors, blinking LEDs, sending periodic keep-alive messages, updating displays. |
One-Shot vs. Auto-Reload Timer Simulation
One-Shot Timer (Period: 3s, Fires Once)
Auto-Reload Timer (Period: 2s, Fires Repeatedly)
Timer States
A FreeRTOS timer can be in one of two primary states:
- Dormant: The timer exists but is not running. It will not expire, and its callback function will not be executed. Timers are in this state after being created or after being explicitly stopped. One-shot timers also return to this state after their callback has executed.
- Running: The timer has been started and is actively counting down towards its expiry time.
Timer API Functions (ESP-IDF v5.x)
The core API for managing FreeRTOS timers is provided by freertos/timers.h
.
1. Creating a Timer: xTimerCreate()
TimerHandle_t xTimerCreate( const char * const pcTimerName,
const TickType_t xTimerPeriodInTicks,
const UBaseType_t uxAutoReload,
void * const pvTimerID,
TimerCallbackFunction_t pxCallbackFunction );
pcTimerName
: A descriptive name for the timer (used mainly for debugging).xTimerPeriodInTicks
: The timer’s period specified in RTOS ticks. Use thepdMS_TO_TICKS()
macro to convert milliseconds to ticks reliably (e.g.,pdMS_TO_TICKS(1000)
for 1 second, assumingconfigTICK_RATE_HZ
is 1000).uxAutoReload
: Set topdTRUE
for an auto-reload timer orpdFALSE
for a one-shot timer.pvTimerID
: A void pointer that can be used to store application-specific data or an identifier associated with the timer. This ID can be retrieved within the callback function. It’s often useful when multiple timers share the same callback.pxCallbackFunction
: A pointer to the function that should be executed when the timer expires. The function must have the signature:void CallbackFunctionName( TimerHandle_t xTimer );
- Return Value: Returns a
TimerHandle_t
(a handle to the created timer) if successful, orNULL
if the timer could not be created (likely due to insufficient FreeRTOS heap memory).
2. Starting a Timer: xTimerStart()
BaseType_t xTimerStart( TimerHandle_t xTimer, TickType_t xTicksToWait );
xTimer
: The handle of the timer to start (obtained fromxTimerCreate
).xTicksToWait
: The maximum amount of time (in ticks) the calling task should block if the Timer Command Queue is full. Setting this to0
makes the call non-blocking. Setting it toportMAX_DELAY
blocks indefinitely (use with caution).- Return Value:
pdPASS
if the start command was successfully sent to the Timer Command Queue,pdFAIL
otherwise (e.g., queue was full andxTicksToWait
was 0). Note:pdPASS
does not mean the timer has expired, only that the start command is queued.
Starting a timer transitions it from the Dormant state to the Running state.
3. Stopping a Timer: xTimerStop()
BaseType_t xTimerStop( TimerHandle_t xTimer, TickType_t xTicksToWait );
xTimer
: The handle of the timer to stop.xTicksToWait
: Maximum time (ticks) to block if the Timer Command Queue is full.- Return Value:
pdPASS
if the stop command was successfully sent,pdFAIL
otherwise.
Stopping a timer transitions it from the Running state back to the Dormant state. Its remaining time is discarded.
4. Resetting a Timer: xTimerReset()
BaseType_t xTimerReset( TimerHandle_t xTimer, TickType_t xTicksToWait );
xTimer
: The handle of the timer to reset.xTicksToWait
: Maximum time (ticks) to block if the Timer Command Queue is full.- Return Value:
pdPASS
if the reset command was successfully sent,pdFAIL
otherwise.
Resetting a timer restarts its period. If the timer was Running, it effectively restarts the countdown from its full period. If the timer was Dormant, it transitions it to the Running state, starting the countdown. This is equivalent to calling xTimerStart()
on a Dormant timer.
5. Changing a Timer’s Period: xTimerChangePeriod()
BaseType_t xTimerChangePeriod( TimerHandle_t xTimer,
TickType_t xNewPeriod,
TickType_t xTicksToWait );
xTimer
: The handle of the timer whose period is to be changed.xNewPeriod
: The new period for the timer, in ticks. UsepdMS_TO_TICKS()
.xTicksToWait
: Maximum time (ticks) to block if the Timer Command Queue is full.- Return Value:
pdPASS
if the change period command was successfully sent,pdFAIL
otherwise.
This function changes the period of an existing timer. The new period takes effect after the command is processed by the Timer Service Task. If the timer was Running, it will use the new period the next time it expires (for auto-reload) or when it’s next started/reset.
6. Deleting a Timer: xTimerDelete()
BaseType_t xTimerDelete( TimerHandle_t xTimer, TickType_t xTicksToWait );
xTimer
: The handle of the timer to delete.xTicksToWait
: Maximum time (ticks) to block if the Timer Command Queue is full.- Return Value:
pdPASS
if the delete command was successfully sent,pdFAIL
otherwise.
This function frees the resources associated with a timer. The timer must not be referenced again after this call returns pdPASS
. The actual deletion happens when the Timer Service Task processes the command. Ensure the timer is stopped before deleting if necessary, although xTimerDelete
can delete active timers.
7. Getting/Setting the Timer ID: pvTimerGetTimerID()
and vTimerSetTimerID()
void *pvTimerGetTimerID( TimerHandle_t xTimer );
void vTimerSetTimerID( TimerHandle_t xTimer, void *pvNewID );
pvTimerGetTimerID()
: Retrieves the ID (thevoid *pvTimerID
value set during creation or viavTimerSetTimerID
) associated with the timer handlexTimer
. This is commonly used inside the timer callback function to identify which timer triggered it if multiple timers share the same callback.vTimerSetTimerID()
: Updates the ID associated with the timer handlexTimer
topvNewID
.
Function Name | Purpose | Key Parameters | Returns |
---|---|---|---|
xTimerCreate() |
Creates a new software timer. | Name, Period (ticks), Auto-reload (pdTRUE /pdFALSE ), Timer ID, Callback function |
TimerHandle_t (handle) or NULL on failure |
xTimerStart() |
Starts a timer that was previously created. Transitions timer to Running state. | Timer handle, Ticks to wait (for command queue) | pdPASS on success, pdFAIL on failure |
xTimerStop() |
Stops a running timer. Transitions timer to Dormant state. | Timer handle, Ticks to wait | pdPASS on success, pdFAIL on failure |
xTimerReset() |
Restarts a timer’s period. If Dormant, starts it. If Running, restarts its countdown. | Timer handle, Ticks to wait | pdPASS on success, pdFAIL on failure |
xTimerChangePeriod() |
Changes the period of an existing timer. Takes effect when command is processed. | Timer handle, New period (ticks), Ticks to wait | pdPASS on success, pdFAIL on failure |
xTimerDelete() |
Deletes a timer, freeing its resources. | Timer handle, Ticks to wait | pdPASS on success, pdFAIL on failure |
pvTimerGetTimerID() |
Retrieves the ID (void* ) associated with a timer. |
Timer handle | void* (the timer ID) |
vTimerSetTimerID() |
Updates the ID associated with a timer. | Timer handle, New void* ID |
void |
Timer Callback Function Context
As mentioned, timer callbacks execute within the context of the Timer Service Task. This has important implications:
- Non-Blocking: Callbacks must not call any FreeRTOS API function that could cause the calling task (the Timer Service Task) to block. This includes
vTaskDelay
,vTaskDelayUntil
,ulTaskNotifyTake
,xQueueReceive
with a non-zero block time, etc. - Execution Time: Callbacks should execute as quickly as possible. Long-running callbacks will delay the processing of other timer events and potentially impact the real-time performance of the system.
- Shared Context: All timer callbacks share the same task context. Be mindful of shared resources or potential race conditions if callbacks access global data, although this is less common than with separate tasks. If significant processing is needed, the callback should offload the work to a regular task, perhaps by sending a message to a queue or setting an event group bit.
%%{ init: { 'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans', 'primaryColor': '#FB96C1', 'primaryTextColor': '#000', 'primaryBorderColor': '#4C1D95', 'lineColor': '#6B46C1', 'secondaryColor': '#F0F4F8', 'tertiaryColor': '#EEEEF3' } } }%% flowchart TB %% Timer Creation process Start([Start]) --> Create["xTimerCreate()"] Create --> TimerHandle["TimerHandle_t obtained<br>(Timer is Dormant)"] TimerHandle --> |"Optional"| SetID["vTimerSetTimerID()"] SetID --> ReadyToStart["Timer Configured<br>(Dormant)"] TimerHandle --> ReadyToStart %% Timer operations ReadyToStart --> |"xTimerStart() or<br>xTimerReset()"| Running["Timer Running<br>(Counting Down)"] %% Active timer operations Running --> |"xTimerChangePeriod()"| Running Running --> |"xTimerReset()"| Running %% Timer expiry Running --> |"Period<br>Expires"| CallbackExec["Timer Callback<br>Executes"] CallbackExec --> |"Inside Callback:<br>pvTimerGetTimerID()"| CallbackExec %% Timer fate after callback CallbackExec --> |"One-Shot<br>Timer"| DormantAfterFire["Dormant State"] CallbackExec --> |"Auto-Reload<br>Timer"| Running %% Stop/dormant operations Running --> |"xTimerStop()"| Dormant["Dormant State<br>(Stopped)"] Dormant --> |"xTimerStart() or<br>xTimerReset()"| Running %% Delete operations ReadyToStart --> |"xTimerDelete()"| Deleted([Timer Deleted]) Running --> |"xTimerDelete()"| Deleted DormantAfterFire --> |"xTimerDelete()"| Deleted Dormant --> |"xTimerDelete()"| Deleted %% Styling classDef startEndClass fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6,rounded:true classDef apiClass fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF classDef stateClass fill:#F0F9FF,stroke:#0284C7,stroke-width:2px,color:#0369A1,rounded:true classDef callbackClass fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46,rounded:true classDef dormantClass fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E,rounded:true classDef deletedClass fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B,rounded:true class Start,Deleted startEndClass class Create,SetID apiClass class TimerHandle,ReadyToStart,Running stateClass class CallbackExec callbackClass class Dormant,DormantAfterFire dormantClass %% Subgraphs subgraph Creation["Timer Creation & Setup"] Start Create TimerHandle SetID ReadyToStart end subgraph ActiveOperations["Timer Active Operations"] Running CallbackExec DormantAfterFire Dormant end
Practical Examples
Let’s illustrate the use of FreeRTOS timers with some practical examples for the ESP32 using ESP-IDF v5.x and VS Code.
Project Setup:
- Launch VS Code.
- Open the Command Palette (Ctrl+Shift+P or Cmd+Shift+P).
- Type
ESP-IDF: Show Examples Projects
and select it. - Choose your ESP-IDF version (v5.x).
- Select the
hello_world
example. - Enter a path for the new project copy (e.g.,
esp32_timer_examples
). - Open the created project folder in VS Code.
- Replace the contents of
main/hello_world_main.c
with the code from the examples below.
Common Includes and Setup:
Add these includes at the top of your main.c
file for the following examples:
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/timers.h"
#include "esp_log.h"
static const char *TAG = "TIMER_EXAMPLE";
Example 1: One-Shot Timer for a Delayed Action
This example creates a one-shot timer that prints a message 3 seconds after the application starts.
// Timer callback function for the one-shot timer
void oneShotTimerCallback(TimerHandle_t xTimer) {
ESP_LOGI(TAG, "One-shot timer expired! Performing delayed action.");
// Note: Do not block here! If complex action is needed,
// signal another task (e.g., using a queue or event group).
}
void app_main(void) {
ESP_LOGI(TAG, "Starting One-Shot Timer Example");
// Create a one-shot timer
// Timer period: 3000ms = 3 seconds
// Auto-reload: pdFALSE (one-shot)
// Timer ID: NULL (not used in this simple example)
// Callback function: oneShotTimerCallback
TimerHandle_t xOneShotTimer = xTimerCreate(
"OneShotTimer", // Timer name for debugging
pdMS_TO_TICKS(3000), // Timer period in ticks
pdFALSE, // One-shot timer
NULL, // No Timer ID needed
oneShotTimerCallback // Callback function
);
if (xOneShotTimer == NULL) {
ESP_LOGE(TAG, "Failed to create one-shot timer");
} else {
ESP_LOGI(TAG, "One-shot timer created. Starting timer...");
// Start the timer. Wait max 100ms for the command queue if full.
if (xTimerStart(xOneShotTimer, pdMS_TO_TICKS(100)) != pdPASS) {
ESP_LOGE(TAG, "Failed to start one-shot timer");
} else {
ESP_LOGI(TAG, "One-shot timer started. Will fire in 3 seconds.");
}
}
// Keep the main task running (or do other things)
// In a real application, app_main might return or create other tasks.
while(1) {
vTaskDelay(pdMS_TO_TICKS(10000)); // Idle delay
}
}
Build, Flash, and Monitor:
- Connect your ESP32 board.
- Select the correct serial port in the VS Code status bar (click
</>
). - Click the “Build, Flash, Monitor” icon (looks like a flame 🔥) in the status bar.
- Alternatively: Build (Ctrl+E B), Flash (Ctrl+E F), Monitor (Ctrl+E M).
Expected Output:
You should see logs similar to this in the ESP-IDF terminal:
I (XXX) TIMER_EXAMPLE: Starting One-Shot Timer Example
I (XXX) TIMER_EXAMPLE: One-shot timer created. Starting timer...
I (XXX) TIMER_EXAMPLE: One-shot timer started. Will fire in 3 seconds.
... (other system logs) ...
I (XXX+3000) TIMER_EXAMPLE: One-shot timer expired! Performing delayed action.
The final message appears approximately 3 seconds after the “timer started” message.
Example 2: Auto-Reload Timer for Periodic Action
This example creates an auto-reload timer that prints a message every 2 seconds.
// Shared counter variable (use atomic operations or mutex in complex scenarios)
volatile uint32_t periodic_counter = 0;
// Timer callback function for the auto-reload timer
void autoReloadTimerCallback(TimerHandle_t xTimer) {
periodic_counter++;
ESP_LOGI(TAG, "Auto-reload timer expired! Count: %lu", periodic_counter);
// Example: Stop the timer after 5 executions
if (periodic_counter >= 5) {
ESP_LOGW(TAG, "Stopping the auto-reload timer now.");
// Attempt to stop the timer from its own callback.
// Use 0 ticks wait time as we are in the timer task context.
if (xTimerStop(xTimer, 0) != pdPASS) {
ESP_LOGE(TAG, "Failed to send stop command from callback.");
// Handle error - maybe try again later or log persistently
}
// Note: The timer handle 'xTimer' is passed to the callback.
}
}
void app_main(void) {
ESP_LOGI(TAG, "Starting Auto-Reload Timer Example");
// Create an auto-reload timer
// Timer period: 2000ms = 2 seconds
// Auto-reload: pdTRUE
// Timer ID: (void *) 123 (example ID, retrieve with pvTimerGetTimerID)
// Callback function: autoReloadTimerCallback
TimerHandle_t xAutoReloadTimer = xTimerCreate(
"AutoReloadTimer", // Timer name
pdMS_TO_TICKS(2000), // Period in ticks
pdTRUE, // Auto-reload enabled
(void *) 123, // Example Timer ID
autoReloadTimerCallback // Callback function
);
if (xAutoReloadTimer == NULL) {
ESP_LOGE(TAG, "Failed to create auto-reload timer");
} else {
// Retrieve the Timer ID we set during creation (just as an example)
void* timerID = pvTimerGetTimerID(xAutoReloadTimer);
ESP_LOGI(TAG, "Auto-reload timer created with ID: %p. Starting timer...", timerID);
// Start the timer. Wait max 100ms if command queue is full.
if (xTimerStart(xAutoReloadTimer, pdMS_TO_TICKS(100)) != pdPASS) {
ESP_LOGE(TAG, "Failed to start auto-reload timer");
} else {
ESP_LOGI(TAG, "Auto-reload timer started. Will fire every 2 seconds.");
}
}
// Keep the main task running
while(1) {
vTaskDelay(pdMS_TO_TICKS(10000)); // Idle delay
}
}
Build, Flash, and Monitor:
Follow the same steps as in Example 1.
Expected Output:
I (XXX) TIMER_EXAMPLE: Starting Auto-Reload Timer Example
I (XXX) TIMER_EXAMPLE: Auto-reload timer created with ID: 0x<some_address_for_123>. Starting timer...
I (XXX) TIMER_EXAMPLE: Auto-reload timer started. Will fire every 2 seconds.
...
I (XXX+2000) TIMER_EXAMPLE: Auto-reload timer expired! Count: 1
I (XXX+4000) TIMER_EXAMPLE: Auto-reload timer expired! Count: 2
I (XXX+6000) TIMER_EXAMPLE: Auto-reload timer expired! Count: 3
I (XXX+8000) TIMER_EXAMPLE: Auto-reload timer expired! Count: 4
I (XXX+10000) TIMER_EXAMPLE: Auto-reload timer expired! Count: 5
W (XXX+10000) TIMER_EXAMPLE: Stopping the auto-reload timer now.
The “Auto-reload timer expired!” message appears every 2 seconds, and the timer stops itself after the 5th execution.
Variant Notes
The FreeRTOS software timer API and its behavior described in this chapter are consistent across the common ESP32 variants supported by ESP-IDF v5.x, including:
- ESP32
- ESP32-S2
- ESP32-S3
- ESP32-C3
- ESP32-C6
- ESP32-H2
The underlying implementation relies on the FreeRTOS kernel and the system tick interrupt, which are configured consistently by ESP-IDF for these targets. Therefore, the code examples and concepts presented here are directly applicable without modification based solely on the choice of these specific ESP32 variants.
Common Mistakes & Troubleshooting Tips
Common Mistake | Potential Symptom(s) | Fix / Best Practice |
---|---|---|
Blocking Calls in Timer Callbacks (e.g., vTaskDelay() , indefinite xQueueReceive() ) |
All other software timers stop firing or are delayed; system appears unresponsive or sluggish; watchdog timer may trigger resets. | Ensure callbacks are short and strictly non-blocking. Offload lengthy processing or blocking operations to a separate task, signaling it from the callback (e.g., using xQueueSend with 0 block time, xEventGroupSetBits , xTaskNotify ). |
Incorrect Timer Period Unit (Passing milliseconds directly instead of ticks) |
Timers expire much faster or slower than expected, especially if configTICK_RATE_HZ is not 1000. |
Always use the pdMS_TO_TICKS(milliseconds) macro to convert time durations from milliseconds to RTOS ticks for timer period parameters. |
Forgetting to Start the Timer (Calling xTimerCreate() but not xTimerStart() or xTimerReset() ) |
The timer callback function never executes; timer remains in Dormant state. | After creating a timer with xTimerCreate() , explicitly start it using xTimerStart() or xTimerReset() when it should begin counting down. |
Long Execution Time in Callback (Complex calculations, slow I/O, extensive logging) |
Jitter in timer periodicity; delayed execution of other timer callbacks; potential impact on other system timing critical tasks. | Keep callbacks minimal and efficient. If significant work is needed, set a flag, send data to a queue, or notify another task to perform the work outside the Timer Service Task context. |
Timer Command Queue Full (Rapid API calls, busy/low-priority Timer Service Task) |
Timer API functions (xTimerStart , xTimerStop , etc.) return pdFAIL . Timer actions might be missed, delayed, or not processed. |
Ensure CONFIG_FREERTOS_TIMER_TASK_PRIORITY is appropriate. Consider increasing CONFIG_FREERTOS_TIMER_QUEUE_LENGTH if bursts of commands are expected. Check return values of API calls and use a small, non-zero xTicksToWait if occasional blocking is acceptable, or implement retry logic. |
Insufficient Timer Service Task Stack ( CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH too small) |
Stack overflow in the Timer Service Task, leading to crashes, undefined behavior, or corruption, especially when complex callbacks are executed. | Ensure CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH is large enough to accommodate the Timer Service Task’s internal needs plus the stack requirements of the deepest/most complex timer callback function. Profile or estimate generously. |
Accessing Timer Handle After Deletion | Undefined behavior, crashes, or memory corruption if xTimerDelete() has been called and the handle is used again. |
Once xTimerDelete() returns pdPASS (or the command is queued), do not use the timer handle again. Set the handle variable to NULL to prevent accidental reuse. |
Exercises
- LED Blinker Timer: Modify Example 2 to toggle an onboard LED (or a GPIO connected to an LED) every 500 milliseconds using an auto-reload timer. You will need to include GPIO driver headers (
driver/gpio.h
) and configure a GPIO pin as output. - Timeout Implementation: Create a scenario where Task A waits for an event (e.g., a button press simulated by a delay). Start a one-shot timer for 5 seconds when Task A begins waiting. If the event occurs before the timer expires, stop and delete the timer. If the timer expires first, log a “Timeout!” message. (Hint: Use a semaphore or event group for the event, and check the timer callback).
- Dynamic Period Change: Create an auto-reload timer that initially fires every 1 second. After it has fired 3 times, use
xTimerChangePeriod()
within the callback function to change its period to 3 seconds. Log messages indicating the period change. - Multiple Timers, Single Callback: Create two timers (one one-shot, one auto-reload) that use the same callback function. Assign unique IDs to each timer using the
pvTimerID
parameter inxTimerCreate()
. Inside the callback, usepvTimerGetTimerID()
to determine which timer expired and print a specific message for each.
Summary
- FreeRTOS software timers provide an efficient mechanism for scheduling function execution without dedicating a task per timed event.
- Timers are managed by a central Timer Service Task which executes all timer callback functions.
- Timer commands (start, stop, etc.) are sent to the Timer Service Task via a Timer Command Queue.
- Callbacks must execute quickly and must never block.
- There are two types: One-shot (fires once) and Auto-reload (fires periodically).
- The timer API (
xTimerCreate
,xTimerStart
,xTimerStop
,xTimerReset
,xTimerChangePeriod
,xTimerDelete
) is used to manage timer lifecycles. - Timer periods are specified in ticks; use
pdMS_TO_TICKS()
for conversion from milliseconds. - Timer IDs (
pvTimerID
) allow associating data or identifiers with timers, useful when sharing callbacks. - Timer behavior is consistent across common ESP32 variants using ESP-IDF v5.x.
Further Reading
- FreeRTOS Software Timer API Reference: https://www.freertos.org/Software-Timer-API-Functions.html
- ESP-IDF FreeRTOS Timer Documentation: Check the ESP-IDF Programming Guide for your specific version, usually under API Reference -> RTOS -> FreeRTOS Support. (Search for “FreeRTOS Timers” within the ESP-IDF documentation).
- ESP-IDF
menuconfig
Options for Timers: ExploreComponent config
->FreeRTOS
->Timers
inidf.py menuconfig
to see configurable parameters like daemon task priority, stack size, and queue length.
