基于ESP32的桌面小屏幕实战[9]:定时器+消息队列
1. 基础知识
定时器和消息队列的组合在嵌入式系统开发中非常常见,尤其是在实时系统(RTOS)中。这种组合可以用于任务调度、事件触发、数据传输、定时事件管理等多个场景,提高系统的响应速度和数据处理效率。
定时器:定时器可以生成精确的时间间隔事件,通常用于周期性任务的触发或超时控制。定时器会在设定的时间间隔内溢出,触发中断,通知CPU执行指定的任务。
消息队列:消息队列是一种线程间或任务间的通信机制。它允许任务将消息放入队列中,也允许其他任务从队列中取出消息。这种机制在实时系统中常用于异步通信,可以确保信息传递的有序性和可靠性。
2. 源码
源码位置~/esp/esp-idf/examples/peripherals/timer_group
包含头文件
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "driver/timer.h"
宏定义
#define TIMER_DIVIDER (16) // Hardware timer clock divider
#define TIMER_SCALE (TIMER_BASE_CLK / TIMER_DIVIDER) // convert counter value to seconds
定义一个结构体 example_timer_info_t,它用来表示定时器的相关信息
typedef struct {
int timer_group; //定时器所属的组
int timer_idx; //定时器在组内的索引
int alarm_interval; //定时器的报警间隔时间
bool auto_reload; //指示定时器是否自动重载
} example_timer_info_t;
typedef用来为数据类型创建一个别名,简化代码。在这里,typedef struct {...} example_timer_info_t;的意思是为这个结构体类型起一个名字example_timer_info_t,这样在声明这个结构体的变量时不需要再写struct关键字。我们可以直接写example_timer_info_t类型的变量。
定义一个名为 example_timer_event_t 的结构体类型,用于从定时器中断服务程序(ISR)传递事件到任务中。中断服务程序(ISR,Interrupt Service Routine)是一段专门的代码,用来响应硬件或软件触发的中断事件。中断事件是一种异步信号,当特定的事件(如定时器溢出、I/O 设备数据准备就绪)发生时,会触发中断,从而暂停当前任务,转而执行 ISR 以处理该事件。处理完毕后,程序会恢复之前的任务。
typedef struct {
example_timer_info_t info; //结构体变量,里面包含定时器所属的组、组内索引、报警间隔时间、是否自动重载
uint64_t timer_counter_value; //存储定时器的计数值
} example_timer_event_t;
uint64_t timer_counter_value;:用于存储定时器的计数值,以uint64_t类型存储,可确保计数值在长时间运行中不会溢出。uint64_t是一种 无符号 64 位整数 类型,表示不带符号的整型数据。在嵌入式编程中,uint64_t特别适合用于存储精确的计数值,像定时器计数器,保证计数在长时间运行中不溢出。
定义一个静态队列句柄 s_timer_queue
static xQueueHandle s_timer_queue;
static:限定符,指明 s_timer_queue 的 作用域仅限于当前文件。其他文件无法访问此变量,使其在模块内具有私有性。xQueueHandle:这是一个 FreeRTOS 队列句柄 类型,用于引用创建的队列。通过该句柄,程序可以向队列中发送和接收消息。s_timer_queue:这是定义的队列句柄变量名称,通常用于在系统中传递时间信息或中断事件数据。
定义一个用于 打印定时器计数器值 的辅助函数 print_timer_counter。此函数将在主函数中被调用。
static void inline print_timer_counter(uint64_t counter_value)
{
/* 打印原始计数值 */
printf("Counter: 0x%08x%08x\r\n", (uint32_t) (counter_value >> 32),
(uint32_t) (counter_value));
/* 将计数值转换为秒并打印 */
printf("Time : %.8f s\r\n", (double) counter_value / TIMER_SCALE);
}
inline:提示编译器将此函数 内联展开,以减少函数调用的开销。print_timer_counter(uint64_t counter_value):此函数接收一个uint64_t类型的参数counter_value,表示定时器的原始计数值。(uint32_t) (counter_value >> 32), (uint32_t) (counter_value):通过 位移和类型转换 将 64 位的 counter_value 拆分为两个 32 位的整数。
counter_value = | 高32位 | 低32位 |- 获取高 32 位
(counter_value >> 32):将counter_value向右移 32 位,使得高 32 位部分移至低 32 位位置,而原低 32 位的部分被移出。(uint32_t)(counter_value >> 32):将移位后的结果 转换为uint32_t类型,只保留低 32 位(即原来的高 32 位)。
- 获取低 32 位
(uint32_t)(counter_value):直接将counter_value的低 32 位部分 转换为uint32_t,自动丢弃高 32 位。
- 获取高 32 位
TIMER_SCALE是一个缩放因子,用于将原始计数值转换为秒。- 使用
%.8f格式化输出,以浮点数形式(精确到小数点后 8 位)显示转换后的时间值。
定义定时器中断回调函数 timer_group_isr_callback 用于处理来自特定定时器的中断事件并将事件信息传递给其他任务。此函数将在初始化定时器时使用。
/* 定时器中断回调函数 */
static bool IRAM_ATTR timer_group_isr_callback(void *args)
/* 声明了一个返回 bool 类型的静态函数 timer_group_isr_callback,加上了 IRAM_ATTR 修饰,表示将函数放在内部 RAM 中执行,优化中断的速度。
* 参数 args 是一个指针,用于接收传入的中断上下文(在这里为 example_timer_info_t 结构的地址)。*/
{
BaseType_t high_task_awoken = pdFALSE; //记录是否需要在中断后进行任务切换
example_timer_info_t *info = (example_timer_info_t *) args; //info 指针指向传入的参数 args,并将其类型转换为 example_timer_info_t,使得我们可以通过 info 访问定时器信息。
/* 在中断中获取定时器的当前计数值,并将其存储在 timer_counter_value 变量中。*/
uint64_t timer_counter_value = timer_group_get_counter_value_in_isr(info->timer_group, info->timer_idx);
/* Prepare basic event data that will be then sent back to task
* 创建并初始化 example_timer_event_t 类型的结构体变量 evt,用于存储此次中断事件的信息。*/
example_timer_event_t evt = {
.info.timer_group = info->timer_group, //定时器组索引
.info.timer_idx = info->timer_idx, //组内定时器索引
.info.auto_reload = info->auto_reload, //自动重载标志
.info.alarm_interval = info->alarm_interval,//报警间隔
.timer_counter_value = timer_counter_value //计数器当前值
};
/* 判断是否启用了自动重载模式。
* 如果 auto_reload 未启用(即为 false),则手动更新下一次报警的触发时间。 */
if (!info->auto_reload) {
timer_counter_value += info->alarm_interval * TIMER_SCALE; // 增加报警间隔的计数值
timer_group_set_alarm_value_in_isr(info->timer_group, info->timer_idx, timer_counter_value);//重新设置报警值
}
/* Now just send the event data back to the main program task
* 将事件 evt 发送到队列 s_timer_queue,以便主任务可以接收到中断事件。
* xQueueSendFromISR 中的 &high_task_awoken 会在需要任务切换时设置为 pdTRUE。 */
xQueueSendFromISR(s_timer_queue, &evt, &high_task_awoken);
/* 返回 high_task_awoken == pdTRUE 的布尔值,
如果 high_task_awoken 为 pdTRUE,则通知 FreeRTOS 调度器需要在中断结束时进行任务切换。*/
return high_task_awoken == pdTRUE; // return whether we need to yield at the end of ISR
}
IRAM_ATTR是一个属性修饰符,用于告诉编译器将带有该属性的函数或变量放置在 IRAM(Instruction RAM,指令RAM)中,而不是存放在默认的Flash存储中。这种做法在实时性要求较高的嵌入式系统中很常见,尤其是像ESP32这类芯片。- 在代码
example_timer_event_t evt = { .info.timer_group = info->timer_group, ... }中:.info.timer_group = info->timer_group,.info.timer_group前面的点.是 C 语言中的一种初始化语法,称为“设计器初始化”(designated initializer)。这种方式指定了evt结构体中info成员的timer_group字段应被初始化为info->timer_group的值。- 这段代码会将传入的
info结构体(即example_timer_info_t类型的指针)中的timer_group成员的值赋给evt的info成员中的timer_group。
info->timer_group->是 C 语言中的结构体指针操作符,用于访问结构体指针info指向的结构体中的成员。info->timer_group意思是“访问info指针指向的结构体的timer_group成员”。
定义example_tg_timer_init()函数,用于初始化指定定时器。此函数将在主函数中调用。
static void example_tg_timer_init(int group, int timer, bool auto_reload, int timer_interval_sec)
{
/* Select and initialize basic parameters of the timer
* 配置定时器基本参数 */
timer_config_t config = {
.divider = TIMER_DIVIDER, //定时器分频系数 16 ,影响计数速度。
.counter_dir = TIMER_COUNT_UP, //计数方向,设置为计数增加。
.counter_en = TIMER_PAUSE, //初始状态设置为暂停。
.alarm_en = TIMER_ALARM_EN, //启用报警功能。
.auto_reload = auto_reload, //根据传入参数决定是否自动重载。
}; // default clock source is APB
/* 定时器初始化 */
timer_init(group, timer, &config);
/* 设置定时器初始计数值为 0
* 若 auto_reload 启用,则在每次报警事件后重载为此值。 */
timer_set_counter_value(group, timer, 0);
/* 配置报警值和中断 */
timer_set_alarm_value(group, timer, timer_interval_sec * TIMER_SCALE);//定时器计数达到报警值(timer_interval_sec * TIMER_SCALE)触发中断。
timer_enable_intr(group, timer);//启用该定时器的中断
/* 动态分配结构体 */
example_timer_info_t *timer_info = calloc(1, sizeof(example_timer_info_t));//分配内存
/* 保存定时器信息 */
timer_info->timer_group = group;
timer_info->timer_idx = timer;
timer_info->auto_reload = auto_reload;
timer_info->alarm_interval = timer_interval_sec;
/* 添加中断回调函数 */
timer_isr_callback_add(group, timer, timer_group_isr_callback, timer_info, 0);
/* 启动定时器 */
timer_start(group, timer);
}
主函数
void app_main(void)
{
/* 创建了一个 FreeRTOS 消息队列,并将其赋值给 s_timer_queue 变量。*/
s_timer_queue = xQueueCreate(10, sizeof(example_timer_event_t));
example_tg_timer_init(TIMER_GROUP_0, TIMER_0, true, 3);//指定定时器组0的定时器0,启用自动重载,报警间隔为 3 秒
example_tg_timer_init(TIMER_GROUP_1, TIMER_0, false, 5);//指定定时器组1的定时器0,不启用自动重载,报警间隔为 5 秒
while (1) {
example_timer_event_t evt;//定义一个类型为 example_timer_event_t 的结构体变量evt
xQueueReceive(s_timer_queue, &evt, portMAX_DELAY);//从队列 s_timer_queue 中接收一个消息,并将该消息存储到 evt 变量中
/* Print information that the timer reported an event
* 根据接收到的 evt 事件,打印定时器组(Timer Group)产生的事件信息 */
if (evt.info.auto_reload) { //判断当前定时器是否启用了自动重载模式
printf("Timer Group with auto reload\n");
} else {
printf("Timer Group without auto reload\n");
}
/* 打印产生报警事件的定时器组和定时器编号 */
printf("Group[%d], timer[%d] alarm event\n", evt.info.timer_group, evt.info.timer_idx);
/* Print the timer values passed by event */
printf("------- EVENT TIME --------\n");
print_timer_counter(evt.timer_counter_value);//timer_counter_value 保存了计时器的计数器值(一般为计时器发生报警事件时的数值)
/* Print the timer values as visible by this task */
printf("-------- TASK TIME --------\n");
uint64_t task_counter_value;//用于存储计时器的当前计数值
/* 获取当前计时器的计数值
* &task_counter_value 是一个指向 task_counter_value 的指针,用于接收函数返回的计数器值*/
timer_get_counter_value(evt.info.timer_group, evt.info.timer_idx, &task_counter_value);
print_timer_counter(task_counter_value);//打印当前计数器的值
}
}
3. 编译工程
在终端中输入
idf.py fullclean
idf.py clean
idf.py build
如果报错idf.py: command not found,需要重新安装编译链和设置环境。
cd ~/esp/esp-idf
export IDF_GITHUB_ASSETS="dl.espressif.com/github_assets"
./install.sh
. /home/xzh/esp/esp-idf/export.sh
设置完记得回到工程所在文件夹。
把板子接上电脑。在终端中输入ls /dev/ttyUSB*查看设备号
然后输入idf.py -p /dev/ttyUSB0 flash monitor
按住IO0,轻按RST,一起松开,下载程序。
出现下图情况按下RST

可以看到程序运行起来了

从这些打印内容中可以得出以下信息:
(1) 定时器组和自动重载设置:
- 每个事件的定时器组(
Group[0]或Group[1])和是否启用了自动重载功能都会显示在第一行。自动重载的定时器会在触发报警后自动重置计数,而非自动重载的定时器在触发报警后不会自动重置。 - 例如,
Timer Group with auto reload表示此定时器组启用了自动重载,Timer Group without auto reload则表示没有启用。
(2) 事件触发时的计数器值和时间:
EVENT TIME下显示了事件触发时的计数器值(以十六进制格式)和相对应的秒数。- 例如,
Counter: 0x00000000017d7852表示计数器值为0x17d7852,转换为秒数为5.00000360 s。可以看出,这是Group[1]的非自动重载定时器,设置了一个 5 秒的报警事件。
(3) 任务读取时的计数器值和时间:
TASK TIME下显示了任务读取计数器的值及其对应的秒数。- 任务时间稍微晚于事件时间,因为任务读取计数器的时间点在事件触发后。比如
TASK TIME下的计数器值比EVENT TIME下的计数器值略高。
(4) 自动重载定时器的重置情况:
- 自动重载定时器(如
Group[0])每次触发报警后都会重置为接近初始状态,例如显示的Counter: 0x0000000000000012,对应的时间为0.00000360 s。

浙公网安备 33010602011771号