十九、软件定时器(software timer)

1、前言

(1)软件定时器就是“闹钟”,你可以设置闹钟。

  • 在30分钟后开始学习
  • 每个1小时例行检查机器运行情况

(2)软件定时器也可以完成两类事情:

  • 在“未来”某个时间点,运行函数。
  • 周期性地运行函数。

(3)日常生活中我们可以定无数个“闹钟”,这无数的“闹钟”要基于一个真实的闹钟。在FreeRTOS里,我们也可以设置无数个“软件定时器”,它们都是基于系统滴答中断(Tick Interrupt)。

(4)本文涉及内容

  • 软件定时器的特性
  • Daemon Task(守护进程的任务)
  • 定时器命令队列
  • 一次性定时器、周期性定时器的差别
  • 怎么操作定时器:创建、启动、复位、修改周期

2、软件定时器的特性

(1)我们在手机上添加闹钟时,需要指定时间、指定类型(一次性的,还是周期性的)、指定做什么事;还有一些过时的、不再使用的闹钟。如下图所示:

(2)使用定时器跟使用手机闹钟是类似的:

  • 指定时间:启动定时器和运行回调函数,两者的间隔被称为定时器的周期(period)。
  • 指定类型,定时器有两种类型:
    • 一次性(One-shot timers):这类定时器启动后,它的回调函数只会被调用一次;可以手动再次启动它,但是不会自动启动它。
    • 自动加载定时器(Auto-reload timers):这类定时器启动后,时间到了之后它会自动启动它;这使得回调函数被周期性地调用。
  • 指定要做什么事,就是指定回调函数。

(3)实际的闹钟分为:有效、无效两类。软件定时器也是类似的,它有两种状态:

  • 运行(Running、Active):运行态的定时器,当指定时间到达之后,它的回调函数会被调用。
  • 冬眠(Dormant):冬眠态的定时器还可以通过句柄来访问它,但是它不再运行,它的回调函数不会被调用。

(4)定时器运行情况示例如下:

  • Timer1:它是一次性的定时器,在t1启动,周期是6个Tick。经过6个tick后,在t7执行回调函数。它的回调函数只会被执行一次,然后该定时器进入冬眠状态。
  • Timer2:它是自动加载的定时器,在t1启动,周期是5个Tick。每经过5个tick它的回调函数都被执行,比如在t6、t11、t16都会执行。

3、软件定时器的上下文

3.1、守护任务

(1)要理解软件定时器API函数的参数,特别是里面的 xTicksToWait ,需要知道定时器执行的过程。FreeRTOS中有一个Tick中断,软件定时器基于Tick来运行。在哪里执行定时器函数?第一印象是在Tick中断里执行:

  • 在Tick中断中判断定时器是否超时
  • 如果超时了,调用它的回调函数

(2)FreeRTOS是RTOS,它不允许在内核、在中断中执行不确定的代码;如果定时器函数很耗时,会影响整个系统。所以,FreeRTOS中,不在Tick中断中执行定时器函数。

(3)在哪里执行定时器函数呢?在某个任务里执行,这个任务就是:RTOS Damemon Task,RTOS守护任务。以前被称为“Timer server”,但是这个任务要做的并不仅仅是定时器相关,所以改名为:RTOS Damemon Task。

(4)当FreeRTOS的配置项 configUSE_TIMERS 被设置为1时,在启动调度器时,会自动创建RTOS
Damemon Task。

(5)我们自己编写的任务函数要使用定时器时,是通过“定时器命令队列”(timer command queue)和守护任务交互,如下图所示:

(6)守护任务的优先级为:configTIMER_TASK_PRIORITY;定时器命令队列的长度为
configTIMER_QUEUE_LENGTH。

3.2、守护任务的调度

(1)守护任务的调度,跟普通的任务并无差别。当守护任务是当前优先级最高的就绪态任务时,它就可以运行。它的工作有两类:

  • 处理命令:从命令队列里取出命令、处理。
  • 执行定时器的回调函数。

(2)能否及时处理定时器的命令、能否及时执行定时器的回调函数,严重依赖于守护任务的优先级。下面使用两个例子来演示。

(3)例子1:守护任务的优先级较低。

  • t1:Task1处于运行态,守护任务处于阻塞态。守护任务在这两种情况下会退出阻塞态切换为就绪态:命令队列中有数据、某个定时器超时了。至于守护任务能否马上执行,取决于它的优先级。
  • t2:Task1调用 xTimerStart(),需要注意的是, xTimerStart() 只是把"start timer"的命令发给"定时器命令队列",使得守护任务退出阻塞态。在本例中,Task1的优先级高于守护任务,所以守护任务无法抢占Task1。
  • t3:Task1执行完 xTimerStart(),但是定时器的启动工作由守护任务来实现,所以 xTimerStart() 返回并不表示定时器已经被启动了。
  • t4:Task1由于某些原因进入阻塞态,现在轮到守护任务运行。守护任务从队列中取出“start timer”命令,启动定时器。
  • t5:守护任务处理完队列中所有的命令,再次进入阻塞态。Idel任务时优先级最高的就绪态任务,它执行。
  • 注意:假设定时器在后续某个时刻tX超时了,超时时间是"tX-t2",而非"tX-t4",从
    xTimerStart() 函数被调用时算起。

(4)例2:守护任务的优先级较高

  • t1:Task处于运行态,守护任务处于阻塞态。守护任务在这两种情况下会退出阻塞态切换为就绪态:命令队列中有数据、某个定时器超时了。至于守护任务能否马上执行,取决于它的优先级。
  • t2:Task1调用 xTimerStart()。需要注意的是, xTimerStart() 只是把"start timer"的命令发给"定时器命令队列",使得守护任务退出阻塞态。在本例中,守护任务的优先级高于Task1,所以守护任务抢占Task1,守护任务开始处理命令队列。Task1在执行 xTimerStart() 的过程中被抢占,这时它无法完成此函数。
  • t3:守护任务处理完命令队列中所有的命令,再此进入阻塞态。此时Task1是优先级最高的就绪态任务,它开始执行。
  • t4:Task1之前被守护任务抢占,对 xTimerStart() 的调用尚未返回。现在开始继续运行该函
    数、返回。
  • t5:Task1由于某些原因进入阻塞状态,进入阻塞状态。Idel任务是优先级最高的就绪态任务,它执行。
  • 注意,定时器的超时时间是基于调用 xTimerStart() 的时刻tX,而不是基于守护任务处理命令的时刻tY。假设超时时间是10个Tick,超时时间是"tX+10",而非"tY+10"。

3.3、回调函数

(1)定时器回调函数的原型如下:

void ATimerCallback( TimerHandle_t xTimer );

(2)定时器的回调函数是在守护任务中被调用的,守护任务不是专为某个定时器服务的,它还要处理其他定时器。所以定时器的回调函数不要影响其他人:

  • 回调函数要尽快执行,不能进入阻塞状态。
  • 不要调用会导致阻塞的API函数,比如vTaskDelay()。
  • 可以调用 xQueueReceive() 之类的函数,但是超时时间要设为0:即刻返回,不可阻塞。

4、软件定时器的函数

4.1、定时器状态转换图

根据定时器的状态转换图,就可以知道所涉及的函数:

4.2、创建

(1)要使用定时器,需要先创建它,得到它的句柄。

(2)有两种方法创建定时器:动态分配内存、静态分配内存。函数原型如下:

/* 使用动态分配内存的方法创建定时器
 * pcTimerName:定时器名字, 用处不大, 会在调试时用到
 * xTimerPeriodInTicks: 周期, 以Tick为单位
 * uxAutoReload: 类型, pdTRUE表示自动加载, pdFALSE表示一次性
 * pvTimerID: 回调函数可以使用此参数, 比如分辨是哪个定时器
 * pxCallbackFunction: 回调函数
 * 返回值: 成功则返回TimerHandle_t, 否则返回NULL
 */
TimerHandle_t xTimerCreate( const char * const pcTimerName,
                            const TickType_t xTimerPeriodInTicks,
                            const UBaseType_t uxAutoReload,
                            void * const pvTimerID,
                            TimerCallbackFunction_t pxCallbackFunction );


/* 使用静态分配内存的方法创建定时器
 * pcTimerName:定时器名字, 用处不大, 尽在调试时用到
 * xTimerPeriodInTicks: 周期, 以Tick为单位
 * uxAutoReload: 类型, pdTRUE表示自动加载, pdFALSE表示一次性
 * pvTimerID: 回调函数可以使用此参数, 比如分辨是哪个定时器
 * pxCallbackFunction: 回调函数
 * pxTimerBuffer: 传入一个StaticTimer_t结构体, 将在上面构造定时器
 * 返回值: 成功则返回TimerHandle_t, 否则返回NULL
 */
TimerHandle_t xTimerCreateStatic( const char * const pcTimerName,
                                  TickType_t xTimerPeriodInTicks,
                                  UBaseType_t uxAutoReload,
                                  void * pvTimerID,
                                  TimerCallbackFunction_t pxCallbackFunction,
                                  StaticTimer_t *pxTimerBuffer );

(3)回调函数的类型是:

void ATimerCallback( TimerHandle_t xTimer );
typedef void (* TimerCallbackFunction_t)( TimerHandle_t xTimer );

4.3、删除

(1)动态分配的定时器,不再需要时可以删除掉以回收内存。删除函数原型如下:

/* 删除定时器
 * xTimer: 要删除哪个定时器
 * xTicksToWait: 超时时间
 * 返回值: pdFAIL表示"删除命令"在xTicksToWait个Tick内无法写入队列
 * pdPASS表示成功
 */
BaseType_t xTimerDelete( TimerHandle_t xTimer, TickType_t xTicksToWait );

(2)定时器的很多API函数,都是通过发送"命令"到命令队列,由守护任务来实现。如果队列满了,"命令"就无法即刻写入队列。我们可以指定一个超时时间 xTicksToWait ,等待一会。

4.4、启动

(1)启动定时器就是设置它的状态为运行态(Running、Active)。

(2)函数原型如下:

/* 启动定时器
 * xTimer: 哪个定时器
 * xTicksToWait: 超时时间
 * 返回值: pdFAIL表示"启动命令"在xTicksToWait个Tick内无法写入队列
 *        pdPASS表示成功
 */
BaseType_t xTimerStart( TimerHandle_t xTimer, TickType_t xTicksToWait );


/* 启动定时器(ISR版本)
 * xTimer: 哪个定时器
 * pxHigherPriorityTaskWoken: 向队列发出命令使得守护任务被唤醒,
 *                            如果守护任务的优先级比当前任务的高,
 *                            则"*pxHigherPriorityTaskWoken = pdTRUE",
 *                            表示需要进行任务调度
 * 返回值: pdFAIL表示"启动命令"无法写入队列
 *         pdPASS表示成功
 */
BaseType_t xTimerStartFromISR( TimerHandle_t xTimer,
                               BaseType_t *pxHigherPriorityTaskWoken );

4.5、停止

(1)停止定时器就是设置它的状态为冬眠(Dormant),让它不能运行。

(2)函数原型如下:

/* 停止定时器
 * xTimer: 哪个定时器
 * xTicksToWait: 超时时间
 * 返回值: pdFAIL表示"停止命令"在xTicksToWait个Tick内无法写入队列
 * pdPASS表示成功
 */
BaseType_t xTimerStop( TimerHandle_t xTimer, TickType_t xTicksToWait );


/* 停止定时器(ISR版本)
 * xTimer: 哪个定时器
 * pxHigherPriorityTaskWoken: 向队列发出命令使得守护任务被唤醒,
 *                            如果守护任务的优先级比当前任务的高,
 *                            则"*pxHigherPriorityTaskWoken = pdTRUE",
 *                            表示需要进行任务调度
* 返回值: pdFAIL表示"停止命令"无法写入队列
*        pdPASS表示成功
*/
BaseType_t xTimerStopFromISR( TimerHandle_t xTimer,
                              BaseType_t *pxHigherPriorityTaskWoken );

(3)注意,xTimerStart() 和 xTimerStop() 函数的参数 xTicksToWait 表示的是:把命令写入命令队列的超时时间。命令队列可能已经满了,无法马上把命令写入队列里,可以等待一会。

(4)xTicksToWait不是定时器本身的超时时间,不是定时器本身的“周期”。创建定时器时,设置了它的周期(period)。 xTimerStart() 函数是用来启动定时器。假设调用xTimerStart() 的时刻是tX,定时器的周期是n,那么在 tX+n 时刻定时器的回调函数被调用。

(5)如果定时器已经被启动,但是它的函数尚未被执行,再次执行 xTimerStart() 函数相当于执行
xTimerReset() ,重新设定它的启动时间。

4.6、复位

(1)从定时器的状态转换图可以知道,使用 xTimerReset() 函数可以让定时器的状态从冬眠态转换为运行态,相当于使用 xTimerStart() 函数。

(2)如果定时器已经处于运行态,使用 xTimerReset() 函数就相当于重新确定超时时间。假设调用xTimerReset() 的时刻是tX,定时器的周期是n,那么 tX+n 就是重新确定的超时时间。

(3)复位函数原型如下:

/* 复位定时器
 * xTimer: 哪个定时器
 * xTicksToWait: 超时时间
 * 返回值: pdFAIL表示"复位命令"在xTicksToWait个Tick内无法写入队列
 *        pdPASS表示成功
 */
BaseType_t xTimerReset( TimerHandle_t xTimer, TickType_t xTicksToWait );


/* 复位定时器(ISR版本)
 * xTimer: 哪个定时器
 * pxHigherPriorityTaskWoken: 向队列发出命令使得守护任务被唤醒,
 *                            如果守护任务的优先级比当前任务的高,
 *                            则"*pxHigherPriorityTaskWoken = pdTRUE",
 *                            表示需要进行任务调度
 * 返回值: pdFAIL表示"停止命令"无法写入队列
 *        pdPASS表示成功
 */
BaseType_t xTimerResetFromISR( TimerHandle_t xTimer,
                               BaseType_t *pxHigherPriorityTaskWoken );

4.7、修改周期

(1)从定时器的状态转换图可知,使用 xTimerChangePeriod() 函数,除了能修改它的周期外,还可以让定时器的状态从冬眠状态转换为运行态。

(2)修改定时器的周期时,会使用新的周期重新计算它的超时时间。假设调用 xTimerChangePeriod() 函数的时间tX,新的周期是n,则 tX+n 就是新的超时时间。

(3)函数原型如下:

/* 修改定时器的周期
 * xTimer: 哪个定时器
 * xNewPeriod: 新周期
 * xTicksToWait: 超时时间, 命令写入队列的超时时间
 * 返回值: pdFAIL表示"修改周期命令"在xTicksToWait个Tick内无法写入队列
 *        pdPASS表示成功
 */
BaseType_t xTimerChangePeriod( TimerHandle_t xTimer,
                               TickType_t xNewPeriod,
                               TickType_t xTicksToWait );

/* 修改定时器的周期(ISR)
 * xTimer: 哪个定时器
 * xNewPeriod: 新周期
 * pxHigherPriorityTaskWoken: 向队列发出命令使得守护任务被唤醒,
 *                            如果守护任务的优先级比当前任务的高,
 *                            则"*pxHigherPriorityTaskWoken = pdTRUE",
 *                            表示需要进行任务调度
* 返回值: pdFAIL表示"修改周期命令"在xTicksToWait个Tick内无法写入队列
*        pdPASS表示成功
*/
BaseType_t xTimerChangePeriodFromISR( TimerHandle_t xTimer,
                                      TickType_t xNewPeriod,
                                      BaseType_t *pxHigherPriorityTaskWoken );

4.8、定时器ID

(1)定时器的结构体如下,里面有一项 pvTimerID ,它就是定时器ID:


(2)怎么使用定时器ID,完全由程序来决定:

  • 可以用来标记定时器,表示自己是什么定时器;
  • 可以用来保存参数,给回调函数使用。

(3)pvTimerID 的初始值在创建定时器时由 xTimerCreate() 这类函数传入,后续可以使用这些函数来操作:

  • 更新ID:使用 vTimerSetTimerID() 函数。
  • 查询ID:查询 pvTimerGetTimerID() 函数。

(4)更新ID和查询ID,这两个函数不涉及命令队列,它们是直接操作定时器结构体。

(5)函数原型如下:

/* 获得定时器的ID
 * xTimer: 哪个定时器
 * 返回值: 定时器的ID
 */
void *pvTimerGetTimerID( TimerHandle_t xTimer );

/* 设置定时器的ID
 * xTimer: 哪个定时器
 * pvNewID: 新ID
 * 返回值: 无
 */
void vTimerSetTimerID( TimerHandle_t xTimer, void *pvNewID );