自学内容网 自学内容网

S1-11 定时器

定时器

定时器是一种计时器件,它可以在一定时间间隔内产生一个或多个事件。在嵌入式系统中,定时器通常用于处理周期性的任务、事件触发和控制系统时间等场景。定时器又分为软件定时器和硬件定时器,硬件定时器在芯片中数量是有限的,在 ESP32-S3 中也仅有两个硬件定时器(其中有一个还被用作了FreeRTOS的Tick精确计时和任务调度),而软件定时器在一个系统中就可以有无数个,软件定时器和硬件定时器都是定时器的实现方式,它们的区别主要体现在实现方式和精度上。
软件定时器是通过 CPU 软件实现的定时器,不需要特殊的硬件支持。在 FreeRTOS 中,开发者可以使用 xTimerCreate 和 xTimerStart 函数创建和启动软件定时器。软件定时器通常采用时钟中断来实现定时功能,其精度受到系统负载和执行时间等因素的影响,一般精度较低,适用于对时间精度要求不高的场景。

为什么不用任务(Task)解决?

定时器是一个不属于核心 FreeRTOS 内核的可选功能, 由 定时器服务(或守护进程)任务提供。
FreeRTOS中的定时器和任务虽然都可以用来完成一些周期性操作,但是它们的设计思路和实现方式有很大的不同。

定时器主要是为了在系统中间隔一段时间后执行某个特定的动作。而通常情况下,定时器并不需要占用CPU资源,比如每秒钟更新一下系统时钟,定时触发一些事件等等。在FreeRTOS中,可以通过软件定时器(xTimer)和硬件定时器来实现定时器功能。

任务则是一种独立运行的程序单元,相当于是操作系统调度的最小单位。它拥有一定的运行优先级、堆栈空间、代码和数据等资源,并且可以与其他任务交互和共享资源。任务通常会被安排在可用的处理器时间片中,以完成某种特定的系统功能,例如计算、通信、数据采集和控制等等。

对于任务和定时器的选择,一般建议按照以下原则进行:

  1. 如果需要高精度的时间控制,或者需要处理实时事件,那么应该优先选择任务。
  2. 如果只需要简单的周期性操作,例如延迟、LED闪烁等,可以使用软件定时器。
  3. 如果需要对外部设备进行周期性的通讯或者控制,可以使用硬件定时器。

在实际应用中,通常需要根据具体的场景和需求来选择任务或定时器。需要注意的是,在使用定时器和任务时,一定要合理利用系统资源并避免占用过多的CPU时间,以保证系统的稳定性和实时响应性。

软件定时器例程

之前我们在任务中实现了LED灯每秒两灭一次,在实际项目开发中 LED 灯这样简单的器件不可能单独为其开放一个任务操作,这样太浪费资源了,一般这种闪烁的 LED 都会被放在定时器中。
本例中需要求如下:

  1. 1号按键开关控制 LED 闪烁,按下一次后启动闪烁,再按一次关闭闪烁,LED 灯每500ms闪烁一次
  2. 2号按键开关控制 LED 闪烁,按下一次后启动闪烁,再按一次关闭闪烁,LED 灯每1000ms闪烁一次
    代码共享位置:https://wokwi.com/projects/364597601972463617
    优化过的代码:https://wokwi.com/projects/364602232901689345
#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);
}

原文地址:https://blog.csdn.net/suolong123/article/details/135616587

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!