7. Timer
7.1 Introduction to Timers
A timer is integrated inside the microcontroller and can be controlled through programming. The timing function of the microcontroller is implemented through counting. When the microcontroller generates a pulse in each machine cycle, the counter increments by one. The main function of a timer is to keep time. When the time is up, it can generate an interrupt to remind that the timing has expired, and then the corresponding function can be executed in the interrupt function. For example, if we want to toggle an LED every 1 second, we can use a timer configured to trigger an interrupt every 1 second, and then execute the LED toggle routine in the interrupt function. The main purposes include:
- Executing timed tasks: The most common usage scenario of a timer is to execute timed tasks. For example, if you want to execute a specific task every certain amount of time, such as 500 milliseconds, you can use a timer to achieve this requirement.
- Time measurement: Timers can also be used to measure time. For example, you can use a timer to measure the execution time of a certain code segment or to measure the interval between events.
- Precise delay: Timers can also be used to generate precise delays. For example, if you need a delay with microsecond precision, you can use a timer to achieve this delay.
- PWM signal generation: Through a timer, you can also generate PWM (Pulse Width Modulation) signals, which can be used to drive motors or adjust the brightness of LEDs, etc.
- Event triggering: In many cases, we need to trigger some events through the timer, such as interrupts. In addition, timers are also used in the implementation of a watchdog, used to monitor or reset the system.
7.2 Hardware Timers and Software Timers
Timers can be implemented based on either hardware or software, and each has its own characteristics and applicable scenarios:
- A hardware timer is a timing function provided by the microcontroller hardware, implemented by dedicated timer/counter circuits. The biggest advantage of hardware timers is their high precision and reliability, because they are not affected by software tasks and operating system scheduling. When very precise timing functions are required, such as generating PWM signals or obtaining precise time measurements, hardware timers are the first choice. Since the timing operation is directly performed by the hardware, even if the main CPU is busy with other tasks, the timer can still accurately execute the callback operation when the scheduled time arrives.
- A software timer is a timer implemented by the operating system or software library. They use the mechanisms provided by the operating system to simulate timer functions. The implementation of software timers is affected by the current system load and task scheduling policies, so they are relatively less precise than hardware timers. However, software timers are usually more flexible and can create a large number of timers, suitable for scenarios that do not require precise time control. In some cases, software timers may cause timing precision issues, for example under high-load conditions or when there are many other high-priority tasks in the system. For simple delays that do not require high precision, software timers are usually sufficient.
7.3 Basic Timer Parameters
The ESP32-S3 chip has two general-purpose timer groups, each containing two general-purpose timers, such as Timer0, Timer1, etc., and each timer contains multiple channels. You can select the specific timer and channel by specifying the timer number and channel number. Each timer can be programmed individually, and each timer can generate time-based interrupts with microsecond precision. Basic timer parameters include: timer number, channel number, prescaler, auto-reload value, timer interrupt enable, etc.
The following are some basic concepts and common properties of timers:
- Counter: The core component of a timer, responsible for continuously counting.
- Overflow: Occurs when the counter reaches its maximum value and then returns to zero.
- Preset Value: When the counter reaches this value, an interrupt or other event is generated.
- Prescaler: Used to reduce the frequency of the clock signal received by the counter, in order to extend the maximum timing range of the timer.
- Interrupt: When the timer reaches the preset value, it can be configured to generate an interrupt, and the interrupt handler will perform some tasks.
7.4 Timer Operation Flow
7.4.1 Import the Header File
ESP-IDF (the official development framework for ESP32) provides a set of APIs to configure and control timers. To use the ESP32 timer function, you first need to include the necessary header files in the code, for example:
#include "driver/gptimer.h"7.4.2 Initialize the Timer
A general-purpose timer instance is represented by gptimer_handle_t. The underlying driver manages all available hardware resources in a resource pool, so we don't need to worry about which timer group and timer the hardware belongs to — the underlying driver will allocate them automatically for us. Example:
// Define a general-purpose timer
gptimer_handle_t gptimer = NULL;2
To initialize the timer, you need to create a timer parameter structure (gptimer_config_t), which contains the configuration parameters of the timer. The parameters are described as follows:
- clk_src: Select the clock source of the timer. Multiple available clocks are listed in gptimer_clock_source_t, and only one clock can be selected. The following parameters are available:
GPTIMER_CLK_SRC_APB: Select APB as the source clock (APB=80 MHz)GPTIMER_CLK_SRC_XTAL: Select XTAL as the source clock (XTAL=40 MHz)GPTIMER_CLK_SRC_DEFAULT: Select APB as the default option - direction sets the counting direction of the timer. Multiple supported directions are listed in gptimer_count_direction_t, and only one direction can be selected. The following parameters are available:
GPTIMER_COUNT_DOWN: Count down, i.e., from 65535 to 0;GPTIMER_COUNT_UP: Count up, i.e., from 0 to 65535; - resolution_hz: Sets the resolution of the internal counter. Each tick of the counter corresponds to 1 / resolution_hz seconds.
intr_priority: Sets the interrupt priority. If set to 0, an interrupt with a default priority is assigned; otherwise, the specified priority is used.- intr_shared: Sets whether to mark the timer interrupt source as a shared source. The default is 1. If this parameter is set to true (1), multiple timers will share the same interrupt source. This means that when one of the timers triggers an interrupt, the corresponding interrupt handler will be called, and different timer interrupts can be handled in the same interrupt service function. If the intr_shared parameter is set to false (0), each timer will have an independent interrupt source, and there will be a corresponding interrupt service function for handling each timer's interrupt. After completing the above structure parameter configuration, you can pass the structure to the gptimer_new_timer() function to instantiate the timer instance and return the timer handle. Function prototype:
esp_err_t gptimer_new_timer(const gptimer_config_t *config, gptimer_handle_t *ret_timer)Description: Creates a new general-purpose timer and returns a handle. Parameters:
- config – [in] Address of the timer configuration structure parameter
- ret_timer – [out] Timer instance to configure Example:
// Define a general-purpose timer instance
gptimer_handle_t gptimer = NULL;
// Configure the timer
gptimer_config_t timer_config = {
.clk_src = GPTIMER_CLK_SRC_DEFAULT, // Timer clock source: select APB as the default option
.direction = GPTIMER_COUNT_UP, // Count up
.resolution_hz = 1000000, // 1MHz, 1 tick=1us
};
// Apply the configuration to the timer instance
gptimer_new_timer(&timer_config, &gptimer);2
3
4
5
6
7
8
9
10
7.4.3 Set the Timer Alarm
A timer alarm means that when the timer reaches the target we set, it will notify us. For most general-purpose timer usage scenarios, you should set the alarm action before starting the timer (equivalent to enabling the interrupt trigger condition). To set the alarm action, you need to configure different parameters of gptimer_alarm_config_t according to how the alarm event is used:
- alarm_count sets the target count value that triggers the alarm event. When setting the alarm value, the counting direction should also be considered. In particular, when auto_reload_on_alarm is true, alarm_count and reload_count cannot be set to the same value, because the same alarm value and reload value are meaningless.
- reload_count represents the count value to be reloaded when the alarm event occurs. This configuration only takes effect when auto_reload_on_alarm is set to true.
- auto_reload_on_alarm flag sets whether to enable the auto-reload function. If enabled, the hardware timer will immediately reload the value of reload_count into the counter when an alarm event occurs. For example, set to count up, the current value is 0, the target value is 10, and the auto-reload value is 5. When the count value increases from 0 up to 10, an alarm event will be triggered. After the alarm event, the count value will not become 0, but 5.
Note that to make the alarm configuration take effect, you need to call gptimer_set_alarm_action(). In particular, when gptimer_alarm_config_t is set to NULL, the alarm function will be disabled. Usage example:
// Alarm value setting for the general-purpose timer
gptimer_alarm_config_t alarm_config = {
.reload_count = 0, // Reload count value is 0
.alarm_count = 1000000, // Alarm target count value, period = 1s
.flags.auto_reload_on_alarm = true, // Enable reload
};
// Set the trigger alarm action
gptimer_set_alarm_action(gptimer, &alarm_config);2
3
4
5
6
7
8
The prototype of the gptimer_set_alarm_action() function is as follows:
esp_err_t gptimer_set_alarm_action(gptimer_handle_t timer, const gptimer_alarm_config_t *config)Description: Sets the alarm event action of the GPTimer. Includes the trigger condition value and whether to reload. Parameters
- timer – The timer to set;
- config – The configuration of the gptimer_alarm_config_t parameter; Return
- Please refer to the parameter definition of the esp_err_t structure;
7.4.4 Register the Callback Function
After the timer is started, specific events (such as "alarm events") can be dynamically generated. If you need to call certain functions when an event occurs, you can mount the function to the interrupt service routine (ISR) through gptimer_register_event_callbacks().
esp_err_t gptimer_register_event_callbacks(gptimer_handle_t timer, const gptimer_event_callbacks_t *cbs, void *user_data)Description: Sets the timer callback function. Parameters
- timer – The timer to set;
- cbs – The callback function to bind;
- user_data – User data, which will be passed directly to the callback function;
Return
- Please refer to the parameter definition of the esp_err_t structure;
Note that the first call to this function needs to be made before calling the
gptimer_enable()function.
7.4.5 Enable and Disable the Timer
Enable the timer Before controlling the timer, you need to call gptimer_enable() to enable the timer. This function does the following:
esp_err_t gptimer_enable(gptimer_handle_t timer)- This function switches the state of the timer driver from init to enable.
- If gptimer_register_event_callbacks() has deferred the installation of the callback service function, this function will enable the callback service function.
Disable the timer
Calling gptimer_disable() does the opposite operation, restoring the timer driver to the init state, disabling the callback service, and releasing the timer.
7.4.6 Start and Stop the Timer
After we enable the timer, it does not mean the timer has started running. We also need to call the gptimer_start() function to make the internal counter start working. And gptimer_stop() can stop the counter.
7.5 Timer Verification
In the main/hardware/timer directory (if it does not exist, create it), create two new files: bsp_timer.c and bsp_timer.h.
Remember to configure the header file path
Write the following code in bsp_timer.h.
#ifndef _BSP_TIMER_H_
#define _BSP_TIEMR_H_
#include "driver/gptimer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "freertos/queue.h"
/**
* @brief Timer initialization configuration
* @param resolution_hz=resolution of the timer alarm_count=target count value that triggers the alarm event
* @retval the created timer callback queue
*/
QueueHandle_t timerInitConfig(uint32_t resolution_hz, uint64_t alarm_count);
#endif2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Write the following code in bsp_timer.c.
#include "bsp_timer.h"
/**
* @brief Timer callback function
* @param None
* @retval None
*/
static bool IRAM_ATTR TimerCallback(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *user_data)
{
BaseType_t high_task_awoken = pdFALSE;
// Save the queue passed in
QueueHandle_t queue = (QueueHandle_t)user_data;
static int time = 0;
time++;
// Send data to the queue from the interrupt service routine (ISR)
xQueueSendFromISR(queue, &time, &high_task_awoken);
return (high_task_awoken == pdTRUE);
}
/**
* @brief Timer initialization configuration
* @param resolution_hz=resolution of the timer alarm_count=target count value that triggers the alarm event
* @retval the created timer callback queue
*/
QueueHandle_t timerInitConfig(uint32_t resolution_hz, uint64_t alarm_count)
{
// Define a general-purpose timer
gptimer_handle_t gptimer = NULL;
// Create a queue
QueueHandle_t queue = xQueueCreate(10, sizeof(10));
// If creation fails
if (!queue) {
ESP_LOGE("queue", "Creating queue failed");
return NULL;
}
// Configure timer parameters
gptimer_config_t timer_config = {
.clk_src = GPTIMER_CLK_SRC_DEFAULT, // Timer clock source: select APB as the default option
.direction = GPTIMER_COUNT_UP, // Count up
// Counter resolution (operating frequency) in Hz, therefore, the step of each count tick equals (1 / resolution_hz) seconds
// Assuming resolution_hz = 1000 000
// 1 / resolution_hz = 1 / 1000000 = 0.000001 (seconds) = 1 (microsecond) ( 1 tick= 1us )
.resolution_hz = resolution_hz,
};
// Apply the configuration to the timer
gptimer_new_timer(&timer_config, &gptimer);
// Bind a callback function
gptimer_event_callbacks_t cbs = {
.on_alarm = TimerCallback,
};
// Set the callback function of timer gptimer to cbs; the parameter passed in is NULL
gptimer_register_event_callbacks(gptimer, &cbs, queue);
// Enable the timer
gptimer_enable(gptimer);
// Alarm value setting for the general-purpose timer
gptimer_alarm_config_t alarm_config = {
.reload_count = 0, // Reload count value is 0
.alarm_count = alarm_count, // Alarm target count value 1000000 = 1s
.flags.auto_reload_on_alarm = true, // Enable reload
};
// Set the trigger alarm action
gptimer_set_alarm_action(gptimer, &alarm_config);
// Start the timer to begin working
gptimer_start(gptimer);
return queue;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
Write the following code in main.c.
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "bsp_timer.h"
static const char *TAG = "example";
void app_main(void)
{
int number = 0;
QueueHandle_t queue = 0;
// Initialize the timer; enter the callback function once every 1 second
queue = timerInitConfig(1000000,1000000);
while(1)
{
// Receive a piece of data from the queue; cannot be used in an interrupt service function
if (xQueueReceive(queue, &number, pdMS_TO_TICKS(2000)))
{
ESP_LOGI(TAG, "Timer stopped, count=%d", number);
} else {
ESP_LOGW(TAG, "Missed one count event");
}
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
In the above example code, FreeRTOS-related content is used. xQueueSendFromISR() is a function used in the FreeRTOS real-time operating system to send data to a queue from within an interrupt service routine (ISR). The prototype of this function is as follows:
BaseType_t xQueueSendFromISR(QueueHandle_t xQueue, const void *pvItemToQueue, BaseType_t *pxHigherPriorityTaskWoken);Parameter description:
xQueue: The queue to send data to.pvItemToQueue: Pointer to the data to be sent.pxHigherPriorityTaskWoken: A pointer to a variable of typeBaseType_t. During the data sending process, if a higher-priority task needs to be woken up, by passing a pointer to this variable, the kernel can be notified to wake up the task. Function return:- On success, returns
pdPASS, indicating that the data was successfully sent to the queue. - On failure, returns
errQUEUE_FULL, indicating that the queue is full and data cannot be sent. Note that thexQueueSendFromISR()function can only be called from an interrupt service routine, not from a normal task. This is because special operations are required when sending data to a queue from an interrupt service routine, and to ensure data synchronization and correct usage in a multitasking environment.
xQueueReceive() is a function used to receive data from a queue in the FreeRTOS real-time operating system. The prototype of this function is as follows:
BaseType_t xQueueReceive(QueueHandle_t xQueue, void *pvBuffer, TickType_t xTicksToWait);Parameter description:
xQueue: The handle of the queue to read data from.pvBuffer: Pointer to the buffer used to receive data.xTicksToWait: The timeout to wait for data to become available, in FreeRTOS clock ticks. If set toportMAX_DELAY, it means waiting indefinitely until data is available to read. Function return:- On success, returns a positive number, indicating the number of data items successfully received. On failure, returns
errQUEUE_EMPTY, indicating that the queue is empty and no data item can be obtained. Note that thexQueueReceive()function usually blocks waiting for data to be readable in the queue. After data becomes available, it retrieves the data and deletes that data from the queue. If the timeoutxTicksToWaitis set, it waits for data to become readable within the specified time and returns before the timeout. If no timeout is set, this function will block until data is available to read in the queue. In addition, you need to pay attention to memory safety when using thexQueueReceive()function. Since the parameters of this function include a pointer to a buffer, you need to ensure that the buffer size is sufficient to avoid memory out-of-bounds and data corruption issues.
7.6 Timer Verification Effect
After burning the above code, you can view the serial log through a serial assistant. Each message is sent at 1-second intervals.