一、软件定时器的概念
1.1、软件定时器的概念
FreeRTOS 软件定时器的时基是基于系统时钟节拍实现的,之所以叫软件定时器是因为它的实现不需要额外使用硬件定时器,而且可以创建很多个,综合这些因素,这个功能就被称之为软件定时器组。既然是定时器,那么它实现的功能与硬件定时器也是类似的。在硬件定时器中,我们是在定时器中断中实现需要的功能,而使用软件定时器时,我们是在创建软件定时器时指定软件定时器的回调函数,在回调函数中实现相应的功能。
1.2、FreeRTOS 提供的软件定时器支持如下功能:
① 裁剪,可通过宏关闭软件定时器功能
② 软件定时器创建
③ 软件定时器启动
④ 软件定时器停止
⑤ 软件定时器复位
⑥ 软件定时器删除
软件定时器的使用相当于扩展了定时器的数量,允许创建更多的定时任务。
1.3、单次模式与周期模式
FreeRTOS 提供的软件定时器支持单次模式和周期性模式,单次模式就是用户创建了定时器并启动了定时器后,定时时间到将不再重新执行,这就是单次模式软件定时器的含义。周期模式就是此定时器会按照设置的时间周期重复去执行,这就是周期模式软件定时器的含义。另外就是单次模式或者周期模式的定时时间到后会调用定时器的回调函数,用户可以回调函数中加入需要执行的工程代码。
1.4、定时器守护任务
FreeRTOS 通过一个 prvTimerTask 任务 (也叫作守护任务(Daemon)) 管理软件定时器,它是在启动调度器时自动创建的,以满足用户定时需求。pryTimerTask 任务会在其执行期间检查用户启动的时间周期溢出的定时器,并调用其回调函数。只有设置 FreeRTOSConfig.h 中的宏定义 configUSE_TIMERS 为 1,将相关代码编译进来,才能正常使用软件定时器相关功能。
FreeRTOS 定时器组的大部分 API 函数都是通过消息队列给定时器任务发消息,在定时器任务里面执行实际的操作。为了更好的说明这个问题,我们将官方在线版手册中的这个截图贴出来进行说明:
左侧图是用户应用程序,右侧是定时器任务。在用户应用程序里面调用了定时器组 API 函数 xTimerReset,这个函数会通过消息队列给定时器任务发消息,在定时器任务里面执行实际操作。消息队列在此处的作用有一个专门的名字:Timer command queue,即专门发送定时器组命令的队列。
二、软件定时器的应用
2.1、应用场景
在很多应用中,我们需要用到一些定时器任务,硬件定时器受硬件的限制,数量上不足以满足用户的实际需求,无法提供更多的定时器,那么可以采用软件定时器来完成,由软件定时器任务代替硬件定时器任务。但需要注意的是,软件定时器的精度是无法和硬件定时器相比的,因为在软件定时器的定时过程中极有可能被其他中断所打断,这是由于软件定时器的执行上下文环境是任务 (prvTimerTask 任务)。所以,软件定时器更适用于对时间精度要求不高的任务,或一些辅助型的任务。
2.2、软件定时器的精度
在操作系统中,通常软件定时器以系统节拍周期为计时单位。系统节拍是系统的心跳节拍,表示系统时钟的频率,类似人的心跳 1s 能跳动多少下。系统节拍配置为 configTICK_RATE HZ,该宏在 FreeRTOSConfig.h 中有定义,默认是 1000。那么系统的时钟节拍周期就为 1ms(1s 跳动 1000 下,每一下时长就为 1ms)。软件定时器的所定时数值必须是这个节拍周期的整数倍,例如节拍周期是 10ms,那么上层软件定时器定时数值只能是 10ms、20ms、100ms 等,而不能取值为 15ms。由于节拍定义了系统中定时器能够分辨的精确度,系统可以根据实际 CPU 的处理能力和实时性需求设置合适的数值,系统节拍周期的值越小,精度越高,但是系统开销也将越大,因为这代表在 1s 中系统进入时钟中断的次数也就越多。
2.3、回调函数
在 prvTimerTask 任务中检测软件定时器,一旦定时时间到,将执行回调函数 (被作为参数传递的函数,间接调用),以完成任务。
详情如下面的图片:
2.4、使用时注意事项
① prvTimerTask 任务的优先级设置高些,以便及时处理软件定时器的相关指令;
② 定时器回调函数是在定时器任务中执行的,实际应用中不可在定时器回调函数中调用任何将定时器任务挂起的函数,比如 vTaskDelay(), vTaskDelayUntil() 以及非零延迟的消息队列和信号量相关的函数。将定时器任务挂起,会导致定时器任务负责的相关功能都不能正确执行了。
三、软件定时器的 API 函数
3.1、使用软件定时器的典型流程如下:
- 创建软件定时器
- 启动软件定时器
- 停止软件定时器
- 删除软件定时器
- 获取软件定时器 ID
3.2、常用 API 函数如下:
- xTimerCreate()
- xTimerStart()
- xTimerStop()
- xTimerDelete()
- pvTimerGetTimerID()
3.3、软件定时器的创建与删除
软件定时器控制块 (句柄)
结构体成员变量说明:
- 软件定时器的名字,一般用于调试,因为控制定时器是通过句柄
- 软件定时器的列表项,用于插入定时器链表
- 软件定时器的周期,单位为系统节拍 (tick)
- 软件定时器是否自动重置,pdFALSE-> 单次模式;pdTRUE-> 周期模式
- 软件定时器的数字 ID,典型用法是多个定时器共用一个回调函数时,通过 ID 辨别
- 软件定时器的回调函数,当定时时间到就会调用这个函数
创建软件定时器
函数原型:
TimerHandle_t xTimerCreate( const char * const pcTimerName, /* 定时器名字 */
const TickType_t xTimerPeriod, /* 定时器周期,单位系统时钟节拍 */
const UBaseType_t uxAutoReload, /* 选择单次模式或者周期模式 */
void * const pvTimerID, /* 定时器 ID */
TimerCallbackFunction_t pxCallbackFunction ); /* 定时器回调函数 */
函数描述:
函数 xTimerCreate 用于创建软件定时器。
-
第 1 个参数是定时器名字,用于调试目的,方便识别不同的定时器。
-
第 2 个参数是定时器周期,单位系统时钟节拍。
-
第 3 个参数是选择周期模式还是单次模式,pdFALSE-> 单次模式;pdTRUE-> 周期模式
-
第 4 个参数是定时器 ID,当不同的定时器使用相同的回调函数时,在回调函数中通过不同的 ID 号来区分不同的定时器。
-
第 5 个参数是定时器回调函数。
-
返回值,创建成功返回定时器的句柄,由于 FreeRTOSCongfig.h 文件中 heap 空间不足,或者定时器周期设置为 0,会返回 NULL。
使用这个函数要注意以下问题:
- 在 FreeRTOSConfig.h 文件中使能宏定义:
#define configUSE_TIMERS 1
应用举例:
删除软件定时器
函数原型:
TimerHandle_t xTimerDelede(TimerHandle_t xTimer, /* 定时器句柄 */
TickType_t xBlockTime ); /* 定时器队列消息发送超时时间 */
函数描述:
函数 xTimerCreate 用于创建软件定时器。
-
第 1 个参数是定时器句柄
-
第 2 个参数定时器队列消息发送超时间,定时器组的大部分 API 函数不是直接运行的,而是通过消息队列给定时器任务发消息来实现的,此参数设置的等待时间就是当消息队列已经满的情况下,等待消息队列有空间时的最大等待时间。
-
返回值,返回 pdFAIL 表示此函数向消息队列发送消息失败,返回 pdPASS 表示此函数向消息队列发送消息成功。定时器任务实际执行消息队列发来的命令依赖于定时器任务的优先级,如果定时器任务是高优先级会及时得到执行,如果是低优先级,就要等待其余高优先级任务释放 CPU 权才可以得到执行。
应用举例:
xTimerDelede(MyTimer01Handle, 100);
3.4、启动软件定时器
函数原型:
BaseType_t xTimerStart( TimerHandle_t xTimer, /* 定时器句柄 */
TickType_t xBlockTime ); /* 定时器队列消息发送超时时间 */
函数描述:
函数 xTimerStart 用于启动软件定时器。
-
第 1 个参数是定时器句柄。
-
第 2 个参数定时器队列消息发送超时间,定时器组的大部分 API 函数不是直接运行的,而是通过消息队列给定时器任务发消息来实现的,此参数设置的等待时间就是当消息队列已经满的情况下,等待消息队列有空间时的最大等待时间。
-
返回值,返回 pdFAIL 表示此函数向消息队列发送消息失败,返回 pdPASS 表示此函数向消息队列发送消息成功。定时器任务实际执行消息队列发来的命令依赖于定时器任务的优先级,如果定时器任务是高优先级会及时得到执行,如果是低优先级,就要等待其余高优先级任务释放 CPU 权才可以得到执行。
使用这个函数要注意以下问题:
- 使用前一定要保证定时器组已经通过函数 xTimerCreate 创建了。
- 对于已经被激活的定时器,即调用过函数 xTimerStart 进行启动,再次调用此函数相当于调用了函数 xTimerReset 对定时器时间进行了复位。
- 如果在启动 FreeRTOS 调度器前调用了此函数,定时器是不会立即执行的,需要等到启动了 FreeRTOS 调度器才会得到执行,即从此刻开始计时,达到 xTimerCreate 中设置的单次或者周期性延迟时间才会执行相应的回调函数。
应用举例:
3.5、停止软件定时器
函数原型:
BaseType_t xTimerStop( TimerHandle_t xTimer, /* 定时器句柄 */
TickType_t xBlockTime ); /* 定时器队列消息发送超时时间 */
函数描述:
函数 xTimerStart 用于停止软件定时器。
-
第 1 个参数是定时器句柄。
-
第 2 个参数定时器队列消息发送超时间
-
返回值,返回 pdFAIL 表示此函数向消息队列发送消息失败,返回 pdPASS 表示此函数向消息队列发送消息成功。才可以得到执行。
应用举例:
3.6、获取软件定时器 ID
函数原型:
void *pvTimerGetTimerID(TimerHandle_t xTimer); /* 定时器句柄 */
函数描述:
函数 pvTimerGetTimerID 用于获取软件定时器 ID。
-
第 1 个参数是定时器句柄。
-
返回值,返回定时器 ID。
使用这个函数要注意以下问题:
-
使用前一定要保证定时器组已经通过函数 xTimerCreate 创建了。
-
创建不同的定时器时,可以对定时器使用相同的回调函数,在回调函数中通过此函数获取是哪个定时器的时间到了,这个功能就是此函数的主要作用。
应用举例:
四、软件定时器的应用编程
4.1、实验现象
4.2、STM32CubeMX 初始化
需要使用软件定时器,必须使能定时器组