FreeRTOS-软件定时器的使用
FreeRTOS软件定时器的实现原理,可以参考:FreeRTOS-软件定时器的实现原理
FreeRTOS 软件定时器概要
FreeRTOS的软件定时器不依赖于任何额外的硬件定时器,完全由内核通过软件来实现的。
使用FreeRTOS 的软件定时器(timer),需要在工程中包含 FreeRTOS/Source/timers.c
文件,并且在FreeRTOSConfig.h
文件中定义
1 |
FreeRTOS 为了实现软件定时器功能,在内核调度器启动时,会自动创建(当定义了configUSE_TIMERS = 1 时) 定时器服务任务 TimerTask,该任务常驻于内核中,并实际负责处理所有与软件定时器相关的工作。
当我们调用定时器相关的 API (例如,如启动定时器)时,实际是通过消息队列,发送了特定的消息(要起启动的定时器句柄+START命令打包成消息)给TimerTask任务,TimerTask任务根据发送过来的 定时器句柄(软件定时器可以创建多个,软件定时器模块内部通过句柄来识别特定定时器)和命令执行相应的动作。
如下图所示:在自定义任务app_task中,使用软件定时器 API xTimerCreate
创建一个软件定时器,该函数会返回一个用来识别当前创建的这个定时器的句柄 timer_A,当我们调用 xTimerStart
启动定时器 timer_A时,函数内部实际是将START命令 和 定时器句柄timer_A封装成一个消息体,并放入软件定时器模块专用的消息队列中,而定时器服务 任务TimerTask则从该消息队列中提取消息,并根据消息中的命令执行相应的动作。
也就是说,与软件定时器相关的还有一个定时器服务任务,该任务实际执行软件定时器相关工作;一个软件定时器模块专用的消息队列,该消息队列用来存储定时器相关命令。因此,我们还需要如下相关的配置:
1 |
configTIMER_TASK_PRIORITY:
- 用来配置定时器服务任务的优先级,一般将其设置的比 idle任务(内核自动创建的任务,可以参考idle hook中相关的描述) 的优先级大。
- 定时器启动后,当设置的时间到期后,会执行创建定时器时配置的回调函数,该回调函数是在 定时器服务任务TimerTask中被调用的。
- 因此,如果你的系统中,存在一个比定时器服务任务 优先级更高的 任务A,并且任务A保持一直运行,不进入阻塞态,那么定时器服务任务 就得不到运行(任务A优先级更高,内核总是运行就绪的最高优先级的任务)。这种情况下,即使你启动的 某个定时器时间超时了,相应的超时回调函数也不会执行(定时器服务任务都得不到机会运行,所以无法执行回调函数)。
configTIMER_TASK_STACK_DEPTH:
- 内核在创建定时器服务任务时,不仅需要设置该任务的优先级,还需要设置 该任务的任务栈大小,如果你的 定时器的回调函数中都不会有较大的临时数组,那么默认配置的大小都是够的。否则,就需要根据实际情况来设置一个更大的值。具体可以参考FreeRTOS如何确定任务栈的大小以及任务栈溢出检测
configTIMER_QUEUE_LENGTH:
- 定时器服务任务 使用的专用消息队列的大小,表示可以同时存在的消息个数。
- 该消息队列中的消息都有由 定时器服务任务TimerTask 来消费。
- 因此,如果系统中存在多个更高优先级的任务,并且都执行了 定时器相关API,那么命令发送到 消息队列中后是不会被立刻消费掉的(定时器服务任务优先级低,还未得到运行,则无法消费刚放进来的消息),因此就会缓存在消息队列中。 所以,这里配置的大小就是在上述场景出现时,消息队列可以缓存的最大消息个数(命令个数)。
- 所以该值需要根据你的系统情况来设置,如果存在多个更高优先级的任务,会频繁调用 定时器相关API,那么就需要考虑设置的更大。
FreeRTOS 软件定时器使用介绍
使用软件定时器,需要先创建出一个软件定时器实例,并将该实例和一个要延后执行(或周期执行)的函数进行绑定。
FreeRTOS 创建一个 软件定时器的 API如下:
1 | TimerHandle_t xTimerCreate( const char * const pcTimerName, |
pcTimerName:创建的软件定时器名字,可以方便调试的时候识别 不同的定时器。定时器创建成功后,可以使用
const char * pcTimerGetName( TimerHandle_t xTimer )
从定时器句柄中提取创建时设置的名字。xTimerPeriodInTicks:定时器启动后多久到期,需要使用 宏pdMS_TO_TICKS将时间转换成内核可以识别的tick个数,例如 1秒钟:pdMS_TO_TICKS(1000)
uxAutoReload:是否是周期定时器,传入pdTRUE则为周期定时器,定时器到期后会自动重新计时;传入pdFALSE,则只执行一次。例如,使用周期定时器实现周期为 1s 为翻转LED灯功能。
pvTimerID:该值是用户使用的,可以理解成 与这个创建的软件定时器关联的一个参数。创建的时候可以设置成任意的值,当你再想用这个值时,使用
void *pvTimerGetTimerID( TimerHandle_t xTimer )
API 就能提取出来。pxCallbackFunction:定时器启动后,到达超时时间后执行的回调函数,该函数的原型必须为:
void ATimerCallback( TimerHandle_t xTimer )
。例如,当你创建的定时器A ,启动后到达了超时时间,定时器服务任务就会执行创建定时器时设置的这个回调函数,并将定时器A 作为参数传进去。返回值: 如果由于堆内存不足(使用该api时,是由内核负责在api内部,动态申请定时器控制结构所需的内存),创建失败了就会返回 NULL。否则,返回的就是用来识别该定时器的句柄(handle)。
定时创建成功后,并没有实际启动,需要主动调用 start 函数后,定时器才开始实际倒计时:
1 | BaseType_t xTimerStart( TimerHandle_t xTimer, TickType_t xTicksToWait ); |
xTimer:即上文创建定时器API 返回的定时器句柄,用来唯一识别一个定时器。
xTicksToWait:命令发送到消息队列的最长等待时间。上文提到,定时器相关操作API是向定时器服务任务专用消息队列中发送操作命令,
xTimerStart
这个函数实际是个宏定义:1
2xTimerGenericCommand
函数内部实现,就是将命令和定时器xTimer打包成一个消息体,并发送给消息队列。但如果当前消息队列满了(上文提到的很多高优先级任务频繁调用定时器API的场景),那么这个发送消息操作就会让当前任务阻塞,以等待消息队列有空闲位置可以放下当前这个消息。这个等待消息队列有空闲位置的时间就是由xTicksToWait
决定的。时间数值同样也应该通过宏pdMS_TO_TICKS()
转换成内核可以识别的tick数值。返回值: 命令消息体成功发送到消息队列中则返回pdPASS。否则,就是等待了
xTicksToWait
设置的时间,但消息队列仍旧没有空闲位置可以放 命令消息体,此时就会超时返回pdFALSE。
周期定时器使用样例:
基于 xTimerCreate
和xTimerStart
,我们实现一个周期定时器的样例程序:创建两个定时器,实现让两个 LED 灯以不同的频率闪烁。
1 |
|
上述代码创建的两个定时器,使用了相同的回调函数led_flash
,所以我们通过参数 pvTimerID
来识别需要操作的LED。
1 | // 定时器超时后,内核就会调用该回调函数,并将超时的 定时器作为参数传入。 |
重置定时器使用样例:
FreeRTOS官网介绍了一个比较典型的,需要使用重置定时器的例子。
对于嵌入式设备来说,往往需要考虑功耗问题。例如一个带有 LCD的设备,一般都是在用户操作时,才亮起LCD背光灯,当用户一段时间不操作时就会自动熄灭背光等,从而节省功耗。例如,当用户持续5s都没有操作,就关闭LCD的背光灯。该功能可以通过定时器来实现,创建一个5s超时的定时器,每当用户操作时,则重置定时器让其重新开始计时,只有用户持续5s都没操作时,定时器才会超时,并触发关闭LCD背光灯的操作。
如下图所示:定时器在第 1秒时启动,此时超时会在第 6秒发生,但第 4秒时,用户按下了设备按键(操作设备),则重置定时器,定时器开始重新计时。如果之后再没有其它操作,定时器的将在第 9秒后超时,并触发关闭LCD背光灯的操作
FreeRTOS提供的定时器重置API为:
1 | BaseType_t xTimerReset( TimerHandle_t xTimer, TickType_t xTicksToWait ); |
- xTimer:定时器句柄,用来指示要重置哪一个定时器。
- xTicksToWait:如上文所述,该API同样是将定时器句柄+reset命令 封装成一个消息体,并发送到定时器模块专用的消息队列上,如果消息队列中满了,那么就需要等待其有空闲位置,xTicksToWait即表示这种情况下愿意等待多久。
实际上,FreeRTOS中的xTimerStart
和xTimerReset
在内部实现上只是发送的命令不一样(一个start,一个reset),但在接收命令的 定时器服务任务TimerTask中,对于这两个命令的处理是完全一样的。
我们实现一个5秒内没有按键按下,则关闭背光灯的功能,代码样例如下:
1 |
|
上面的程序主体,创建了一个超时时间为5s的定时器,初始化完成后,就会启动该定时器,如果5s内没有按键操作就会超时,并执行回调函数backlight_off
。回调函数中实现关闭背光灯的操作,如下所示:
1 | void backlight_off( TimerHandle_t timer ) { |
剩下的就是按键检测任务button_press_detect_task
,检测按键最简单的实现就是不停的轮训按键的引脚,看是否电平有变化,但是这种方式效率太低,即使没有按键时任务也要占用cpu,浪费cpu资源。这里我们使用信号量来作为按键按下通知信号,按键检测任务等待一个信号量可用,而信号量在按键中断处理程序中才会被设置,这样按键检测任务在没有按键按下时就是处于阻塞状态,并不会占用任何cpu资源。
其逻辑如下图所示:当按键按下时,触发硬件中断,硬件中断程序中检测到按键按下,则设置信号量。信号量一旦被设置,则等待该信号量的按键检测任务button_press_detect_task
就会恢复为就绪状态,在任务被重新调度后(获得cpu),就会执行后续的处理过程。
具体的代码实现如下,按键中断处理程序需要根据自己的硬件来实现:
1 | void GPIOTE_IRQHandler(void){ |
上述代码,当检测到按键按下后,设置信号量,由于是处于中断处理函数中,所以需要使用xSemaphoreGiveFromISR
(中断环境中必须使用带FromISR后缀的API)。
此外,在中断处理程序最后使用了 portYIELD_FROM_ISR
,这是因为我们在中断处理程序中使用xSemaphoreGiveFromISR
设置信号量后,如果另一个优先级更高的任务(比当前运行的任务优先级高,在这里就是button_press_detect_task
任务)在等待该信号量,那么由于信号量可用了,等待该信号量的高优先级任务(button_press_detect_task
)就会变为就绪状态(信号量可用,使得另一个等待该信号量的更高优先级的任务变为就绪态,则higher_task_woken
会被自动设置为pdTRUE
)。我们使用的是抢占式调度的实时操作系统,当存在比当前任务优先级更高的任务就绪时,应该让其立刻被调度,而portYIELD_FROM_ISR
的作用就是如果存在更高优先级任务就绪了,就会设置任务切换中断(PendSV中断),这样在中断服务程序GPIOTE_IRQHandler
结束后,就会立刻触发任务切换,使得更高优先级的就绪任务(button_press_detect_task
)被调度运行。
按键检测任务实现如下:
1 | void button_press_detect_task( void *pvParameters ) { |
更新定时器超时周期样例:
实际应用中,存在一种场景,例如设备存在一个状态指示灯,当设备在正常运转时,该指示灯以一个较低的频率闪烁;当设备出现故障时,该指示灯以一个较高的频率闪烁。FreeRTOS提供了 xTimerChangePeriod
API,来应对这种应用场景。
1 | BaseType_t xTimerChangePeriod( TimerHandle_t xTimer, |
- xTimer: 创建定时器时返回的句柄。
- xNewTimerPeriodInTicks:定时器新的超时周期
- xTicksToWait:该API 同样是发送命令到定时器模块专用消息队列中,所以也需要这个 命令发送到消息队列的最长等待时间 参数。
使用样例,这里就基于上面的按键例子进行修改。创建一个定时器用来反转LED的状态,并根据检测到的不同的按键按下事件,将定时器的超时周期修改成不同的值(即修改了LED的闪烁频率)
main 函数整体不变,创建定时器led1_flash_timer
,默认周期为1s。创建按键检测任务button_press_detect_task
。
1 | SemaphoreHandle_t semaphore = NULL; |
定时器led1_flash_timer
的超时回调函数就是反转 LED灯状态:
1 | void led_flash( TimerHandle_t timer ) { |
按键检测任务button_press_detect_task
修改成检测两个按键,检测到BUTTON_1按下时,将定时器led1_flash_timer
的周期更新为200ms,检测到BUTTON_2按下时,则将定时器led1_flash_timer
的周期更新为2000ms。
1 |
|
最后的中断处理程序,也是修改成检测两个按键:
1 | void GPIOTE_IRQHandler(void){ |
ps:需要注意文章代码中的日志输出函数,产品代码中如果需要使用的话,需要考虑线程安全性(多任务安全性),因为中断/任务切换可能发生在另一个任务正在输出日志但还未输出完的时候,这就可能造成日志错乱
FreeRTOS交流QQ群-663806972