基于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 位。
  • 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 成员的值赋给 evtinfo 成员中的 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
img

可以看到程序运行起来了
img

从这些打印内容中可以得出以下信息:

(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

推荐链接

posted @ 2025-11-19 09:44  茴香豆的茴  阅读(71)  评论(0)    收藏  举报