定时器是一种计时器件,它可以在一定时间间隔内产生一个或多个事件。在嵌入式系统中,定时器通常用于处理周期性的任务、事件触发和控制系统时间等场景。定时器又分为软件定时器和硬件定时器,硬件定时器在芯片中数量是有限的,在 ESP32-S3 中也仅有两个硬件定时器(其中有一个还被用作了FreeRTOS的Tick精确计时和任务调度),而软件定时器在一个系统中就可以有无数个,软件定时器和硬件定时器都是定时器的实现方式,它们的区别主要体现在实现方式和精度上。
软件定时器是通过 CPU 软件实现的定时器,不需要特殊的硬件支持。在 FreeRTOS 中,开发者可以使用 xTimerCreate 和 xTimerStart 函数创建和启动软件定时器。软件定时器通常采用时钟中断来实现定时功能,其精度受到系统负载和执行时间等因素的影响,一般精度较低,适用于对时间精度要求不高的场景。
定时器是一个不属于核心 FreeRTOS 内核的可选功能, 由 定时器服务(或守护进程)任务提供。
FreeRTOS中的定时器和任务虽然都可以用来完成一些周期性操作,但是它们的设计思路和实现方式有很大的不同。
定时器主要是为了在系统中间隔一段时间后执行某个特定的动作。而通常情况下,定时器并不需要占用CPU资源,比如每秒钟更新一下系统时钟,定时触发一些事件等等。在FreeRTOS中,可以通过软件定时器(xTimer)和硬件定时器来实现定时器功能。
任务则是一种独立运行的程序单元,相当于是操作系统调度的最小单位。它拥有一定的运行优先级、堆栈空间、代码和数据等资源,并且可以与其他任务交互和共享资源。任务通常会被安排在可用的处理器时间片中,以完成某种特定的系统功能,例如计算、通信、数据采集和控制等等。
对于任务和定时器的选择,一般建议按照以下原则进行:
在实际应用中,通常需要根据具体的场景和需求来选择任务或定时器。需要注意的是,在使用定时器和任务时,一定要合理利用系统资源并避免占用过多的CPU时间,以保证系统的稳定性和实时响应性。
之前我们在任务中实现了LED灯每秒两灭一次,在实际项目开发中 LED 灯这样简单的器件不可能单独为其开放一个任务操作,这样太浪费资源了,一般这种闪烁的 LED 都会被放在定时器中。
本例中需要求如下:
#define KEY_1_PIN 4
#define KEY_2_PIN 5
#define KEY_3_PIN 6
#define LED_PIN ? 40
#define KEY1_EVENT 1
#define KEY2_EVENT 2
#define KEY3_EVENT 4
EventGroupHandle_t key_event; ? ? ? ? // 按键事件组
volatile TickType_t keyDeounce = 0; ? // 按下按钮的时间
TimerHandle_t flash_timer = NULL; ? ? // 闪烁用的定时器
// 定时器回调函数
void vTimerCallback( TimerHandle_t xTimer ){
? digitalWrite(LED_PIN, !digitalRead(LED_PIN));
}
// 按键时间处理函数
void key_event_task_entry(void *params){
? printf("安检服务程序启动...\n");
? EventBits_t uxBits;
? uint16_t period=0; ? ? // 闪烁间隔时间
? while(1){
? ? uxBits= xEventGroupWaitBits(key_event, ? ? ? ? ? ? ? ?// 事件组句柄
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? KEY1_EVENT | KEY2_EVENT | KEY3_EVENT, ?// 等待事件
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? pdTRUE, ? ? ? ? ? ? ? ? ? // 退出后清空所有位
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? pdFALSE, ? ? ? ? ? ? ? ? ?// 任何一位到达都触发
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? portMAX_DELAY); ? ? ? ? ? // 无限期等待(50天)
? ? if(uxBits>0 && ((xTaskGetTickCount() - keyDeounce) < 200)){
? ? ? // 如果不是超时
? ? ? switch(uxBits){
? ? ? ? case KEY1_EVENT:
? ? ? ? ? period = 1000;
? ? ? ? ? break;
? ? ? ? case KEY2_EVENT:
? ? ? ? ? period = 500;
? ? ? ? ? break;
? ? ? ? case KEY3_EVENT:
? ? ? ? ? // 暂停或恢复闪烁
? ? ? ? ? if(flash_timer !=NULL){
? ? ? ? ? ? if(xTimerIsTimerActive(flash_timer)){
? ? ? ? ? ? ? // 启动中,暂停
? ? ? ? ? ? ? if(xTimerStop(flash_timer, 0) == pdPASS){
? ? ? ? ? ? ? ? printf("定时器停止成功!\n");
? ? ? ? ? ? ? }else{
? ? ? ? ? ? ? ? printf("错误,定时器无法停止!\n");
? ? ? ? ? ? ? }
? ? ? ? ? ? }else{
? ? ? ? ? ? ? // 暂停中,启动
? ? ? ? ? ? ? if(xTimerReset(flash_timer, 0) == pdPASS){ ?// Reset后会重启,暂停时间也会归零
? ? ? ? ? ? ? ? printf("定时器重置成功!\n");
? ? ? ? ? ? ? }else{
? ? ? ? ? ? ? ? printf("错误,定时器无法重置!\n");
? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? ? }else{
? ? ? ? ? ? printf("还没有创建定时器!\n");
? ? ? ? ? }
? ? ? ? ? goto exit;
? ? ? ? ? break;
? ? ? }
? ? ? if(flash_timer == NULL){
? ? ? ? // 创建新的定时器
? ? ? ? flash_timer = xTimerCreate("Timer_Flash", ? // 定时器的名字
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? period, ? ? ? ? ? // 调用间隔时间
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? pdTRUE, ? ? ? ? ? // 是否重复运行
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? NULL, ? ? ? ? ? ? // 定时器标识符,相当于参数
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? vTimerCallback);
? ? ? ? if(xTimerStart(flash_timer,0) == pdPASS){ ? //立即启动
? ? ? ? ? printf("开启闪烁,间隔 %d ms\n", period);
? ? ? ? }else{
? ? ? ? ? printf("错误,启动闪烁失败!");
? ? ? ? }
? ? ? }else{
? ? ? ? // 如果定时器存在,则停止并删除定时器
? ? ? ? if(xTimerIsTimerActive(flash_timer)){
? ? ? ? ? // 定时器正在运行中,先停止,这一步可以不操作,直接删除效果也是一样的
? ? ? ? ? if(xTimerStop(flash_timer, 0) != pdPASS){
? ? ? ? ? ? printf("错误,定时器无法停止!\n");
? ? ? ? ? }
? ? ? ? }
? ? ? ? if(xTimerDelete(flash_timer,0) == pdPASS){ ?// 立即删除
? ? ? ? ? flash_timer = NULL;
? ? ? ? ? printf("停止闪烁!\n");
? ? ? ? ? digitalWrite(LED_PIN, LOW);
? ? ? ? }else{
? ? ? ? ? printf("错误,定时器无法删除!\n");
? ? ? ? }
? ? ? }
exit:
? ? ? delay(500);
? ? ? xEventGroupGetBits(key_event); ?// 去抖动
? ? }
? }
}
// 中断按键
void IRAM_ATTR KEY1_ISR(){
? keyDeounce = xTaskGetTickCountFromISR(); ? ?// 记录下按下的时间,用于放抖动,正式开发中不要这样写,有Bug
? xEventGroupSetBitsFromISR(key_event, KEY1_EVENT, NULL);
}
void IRAM_ATTR KEY2_ISR(){
? keyDeounce = xTaskGetTickCountFromISR(); ? ?// 记录下按下的时间,用于放抖动,正式开发中不要这样写,有Bug
? xEventGroupSetBitsFromISR(key_event, KEY2_EVENT, NULL);
}
void IRAM_ATTR KEY3_ISR(){
? keyDeounce = xTaskGetTickCountFromISR(); ? ?// 记录下按下的时间,用于放抖动,正式开发中不要这样写,有Bug
? xEventGroupSetBitsFromISR(key_event, KEY3_EVENT, NULL);
}
void setup() {
? // put your setup code here, to run once:
? Serial.begin(115200);
? Serial.println("Hello, ESP32-S3!");
? // 初始化LED
? pinMode(LED_PIN, OUTPUT);
? // 初始化事件组
? key_event = xEventGroupCreate();
? // 安装中断
? pinMode(KEY_1_PIN, INPUT_PULLUP);
? pinMode(KEY_2_PIN, INPUT_PULLUP);
? pinMode(KEY_3_PIN, INPUT_PULLUP);
? attachInterrupt(KEY_1_PIN, KEY1_ISR, FALLING);
? attachInterrupt(KEY_2_PIN, KEY2_ISR, FALLING);
? attachInterrupt(KEY_3_PIN, KEY3_ISR, FALLING);
? // 启动按键服务线程
? xTaskCreate(key_event_task_entry, "KEY_SERVICE", 10240, NULL, 1, NULL);
? vTaskDelete(NULL);
}
void loop() {
}
定时器代码中综合运用了事件组和中断。
在 setup 函数中,首先初始化了 LED 控制引脚和按键引脚,并为其增加了中断服务函数,为优化的代码中,每个按键一个中断服务函数,优化过的代码中,三个中断使用了一个服务函数,并在其中判断是哪个按键被按下了,结果都是一样的。
本例程的重点在 key_event_task_entry 任务中,白任务中首先使用 xEventGroupWaitBits 等待一个事件的到达,等待的事件是任意三个按键事件中一个。
当事件到达后,首先判断是哪个按键,如果是一号二号按键,则是用于设置 LED 闪烁的,如果闪烁存在,则删除这个闪烁,如果不存在则创建一个定时器执行闪烁。
创建定时器函数运行如下:
TimerHandle_t xTimerCreate
( const char * const pcTimerName,
const TickType_t xTimerPeriod,
const UBaseType_t uxAutoReload,
void * const pvTimerID,
TimerCallbackFunction_t pxCallbackFunction );
参数 | 描述 |
---|---|
pcTimerName | 分配给定时器的可读文本名称。 这样做纯粹是为了协助 调试。 RTOS 内核本身只通过句柄引用定时器, 而从不通过名字引用。 |
xTimerPeriod | 定时器的周期。 以 tick 为单位指定此周期,宏 pdMS_TO_TICKS() 可用于将以毫秒为单位指定的时间转换为以 tick 为单位指定的时间。 例如, 如果定时器必须要在 100 次 tick 后到期,那么只需将 xTimerPeriod 设置为 100。 或者,如果定时器 必须在 500 毫秒后到期,则需要将 xTimerPeriod 设置为 pdMS_TO_TICKS( 500 )。 使用 pdMS_TO_TICKS() 的唯一前提条件是?configTICK_RATE_HZ?小于或等于 1000。 定时器周期必须大于 0。 |
uxAutoReload | 如果 uxAutoReload 设置为 pdTRUE ,则定时器将按 xTimerPeriod 参数设置的频率重复到期。 如果 uxAutoReload 设置为 pdFALSE,则此定时器为一次性定时器, 它会在到期后进入休眠状态。 |
pvTimerID | 分配给正在创建的定时器的标识符。 通常,此标识符用于定时器回调函数: 当同一个回调函数分配给了多个定时器时,此标识符可以识别哪个定时器已到期。 或者此标识符可与?vTimerSetTimerID()?和?pvTimerGetTimerID()?API 函数一起使用, 以便保存调用 定时器回调函数之间的值。 |
pxCallbackFunction | 定时器到期时调用的函数。 回调函数必须有 TimerCallbackFunction_t 定义的原型,即:void vCallbackFunction( TimerHandle_t xTimer );。 |
返回值 | 如果定时器创建成功, 则返回新创建的定时器的句柄。 如果由于剩余的 FreeRTOS 堆不足以分配定时器结构体而无法创建定时器, 则返回 NULL。 |
我们利用 xTimerCreate 创建了一个重复执行的函数,并通过 xTimerStart 将定时器启动起来,第二个参数为等待事件,因为定时器在启动、停止、重置、删除等操作的时候仍然有其他动作正在执行中,需要有个时间等待,我们这里不等待,如果有其他动作执行则马上返回,并报告错误。
定时器启动后,会重复调用 vTimerCallback 回掉函数,如果在创建定时器的时候设置过 pvTimerID 参数,则该参数会传递给这个回掉函数,在回调函数中,不需要做大循环,因为定时器会按照设置( uxAutoReload 为 true )自动重复调用。
单击第一个和第二个按钮的时候,如果发现定时器存在,则调用 xTimerDelete 删除定时器,在删除之前,还进行了定时器是否活跃中的判断,如果活跃中,则先停止再删除,这一步对于删除来说是多余的,这里我们仅用于演示。
第三个按钮演示了定时器的启动和停止,值得一提的是,启动定时器必须使用 xTimerStart,如果直接使用 xTimerReset则会报错,两者最本质上的区别就是 xTimerStart 是启动一个未运行的定时器,而 xTimerReset 是重新启动一个已运行或已超时的定时器,xTimerStart 函数会启动一个已经创建但是被停止的定时器,并开始倒计时。如果该定时器已经在运行状态或者尚未被创建,则该函数不会产生任何作用。需要注意的是,如果一个定时器启动后超时,则仅执行一次定时器回调函数,如果需要周期性执行该回调函数,需要在回调函数中再次启动该定时器。xTimerReset 函数会重新启动一个已经创建并正在运行或已经超时的定时器,这意味着重新开始倒计时。如果该定时器尚未被创建,则将不会有任何作用,也不会启动该定时器。与 xTimerStart 不同的是,xTimerReset 可以用于周期性定时器,每次重新启动定时器时都会触发回调函数执行。
上个例程中,在使用 xTimerCreate 创建定时器的时候,uxAutoReload 传入的是 pdTRUE,表示重复执行,如果这个函数传入的是 pdFALSE,则表示不重复执行,定时器时间到后,执行一次回调函数则会退出,退出后通过 xTimerIsTimerActive 可以看到定时器确实已经停止了,这时候可以再次调用xTimerStart 启动定时器。
下面一个例程中,演示了按下开后2秒钟之后再打开和关闭LED的例子,但在本例中有个小小的BUG,printf函数似乎有冲突,所以在演示的时候把输出改成了黄色 LED 表示。
代码共享位置:https://wokwi.com/projects/364603793995498497
#define KEY_PIN 20
#define LED_PIN 14
#define WAR_PIN 39
SemaphoreHandle_t led = NULL; ? ? ? ? // 二进制信号量
volatile TickType_t keyDeounce = 0; ? // 按下按钮的时间
TimerHandle_t timer = NULL; ? ? ? ? ? // 定时器句柄
// 定时器回调函数
void vTimerCallback( TimerHandle_t xTimer ){
? digitalWrite(LED_PIN, !digitalRead(LED_PIN));
}
void led_task(void *param_t){
? pinMode(LED_PIN, OUTPUT);
? pinMode(WAR_PIN, OUTPUT);
? while(1){
? ? // 这种去抖方式是很Low的,正确的方式要使用定时器。
? ? if((xSemaphoreTake(led, 1000) == pdTRUE) && ((xTaskGetTickCount() - keyDeounce) < 200)){
? ? ? // 创建定时器并启动他
? ? ? if(timer == NULL){
? ? ? ? timer = xTimerCreate("Timer_LED", ? ? ? // 定时器的名字
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 2000, ? ? ? ? ? ? // 调用间隔时间
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? pdFALSE, ? ? ? ? ?// 只运行一次
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? NULL, ? ? ? ? ? ? // 定时器标识符,相当于参数
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? vTimerCallback);
? ? ? }
? ? ? if(xTimerIsTimerActive(timer) == pdFALSE){
? ? ? ? xTimerStart(timer,0);
? ? ? ? // printf("按了开关...\n");
? ? ? }else{
? ? ? ? // printf("迷瞪中...\n");
? ? ? ? digitalWrite(WAR_PIN, !digitalRead(WAR_PIN));
? ? ? }
? ? ? vTaskDelay(500);
? ? }
? }
}
// 中断服务函数
void IRAM_ATTR ISR() {
? keyDeounce = xTaskGetTickCountFromISR(); ? ?// 记录下按下的时间,用于放抖动,正式开发中不要这样写,有Bug
? xSemaphoreGiveFromISR(led, NULL);
}
void setup() {
? Serial.begin(115200);
? led = xSemaphoreCreateBinary(); ? ? ? ? ? //创建二进制信号量
? xTaskCreate(led_task, "LED-DSP", 1024, NULL, 1, NULL);
? // 安装中断
? pinMode(KEY_PIN, INPUT_PULLUP);
? attachInterrupt(KEY_PIN, ISR, FALLING);
}
void loop() {
? delay(10);
}