FreeRTOS——软件定时器(基于百问网FreeRTOS教学视频)


在FreeRTOS里,我们也可以设置无数个"软件定时器",它们都是基于 系统滴答中断(Tick Interrupt)。

一、定时器的特性

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

  • 指定时间:启动定时器和运行回调函数,两者的间隔被称为定时器的周期(period)。

  • 指定类型,定时器有两种类型:

  • 一次性(One-shot timers):

​ 这类定时器启动后,它的回调函数只会被调用一次;可以手工再次启动它,但是不会自动启动它。

  • 自动加载定时器(Auto-reload timers ):

​ 这类定时器启动后,时间到之后它会自动启动它; 这使得回调函数被周期性地调用。 指定要做什么事,就是指定回调函数

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

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

二、软件定时器的上下文

2.1 守护任务

要理解软件定时器 API 函数的参数,特别是里面的 xTicksToWait,需要知道定时器执行的过程。

FreeRTOS中有一个Tick中断,软件定时器基于Tick来运行。在哪里执行定时器函数?第 一印象就是在Tick中断里执行:

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

FreeRTOS是实时操作系统,不允许在中断中执行不确定的代码,如果定时器函数很耗时,会影响整个系统,所以在FreeRTOS中,不在Tick中断中执行定时器函数。在RTOS守护任务(RTOS Damemon Task)中执行。

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

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

在这里插入图片描述

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

2.2 守护任务的调度

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

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

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

例子1:守护任务的优先性级较低

  • t1:Task1 处于运行态,守护任务处于阻塞态。 守护任务在这两种情况下会退出阻塞态切换为就绪态:命令队列中有数据、某个定时器超时了。 至于守护任务能否马上执行,取决于它的优先级。

  • t2:Task1 调用 xTimerStart() 。要注意的是,xTimerStart()只是把"start timer"的命令发给"定时器命令队列",使得守护任务退出阻塞态。 在本例中,Task1 的优先级高于守护任务,所以守护任务无法抢占 Task1。

  • t3:Task1 执行完 xTimerStart()。但是定时器的启动工作由守护任务来实现,所以 xTimerStart()返回并不表示定时器已经被启动了。

  • t4:Task1 由于某些原因进入阻塞态,现在轮到守护任务运行。 守护任务从队列中取出"start timer"命令,启动定时器。

  • t5:守护任务处理完队列中所有的命令,再次进入阻塞态。Idel 任务时优先级最高的就绪态任务,它执行。

  • 注意:假设定时器在后续某个时刻 tX 超时了,超时时间是"tX-t2",而非"tXt4",从 xTimerStart()函数被调用时算起。
    在这里插入图片描述

例子2:守护任务的优先性级较高

  • t1:Task1 处于运行态,守护任务处于阻塞态。 守护任务在这两种情况下会退出阻塞态切换为就绪态:命令队列中有数据、某个定时器超时了。 至于守护任务能否马上执行,取决于它的优先级。
  • 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"。
    在这里插入图片描述

2.3 回调函数

函数原型:

void ATimerCallback( TimerHandle_t xTimer ); 

定时器的回调函数是在守护任务中被调用的,守护任务不是专为某个定时器服务的,它还要处理其他定时器。

所以,调用定时器的回调函数有要求:

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

三、软件定时器的函数

根据定时器的状态转换图,就可以知道所涉及的函数:
在这里插入图片描述

3.1 创建函数

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

动态分配内存方式创建:

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回调函数
返回值成功则返回 TimerHandle_t, 否则返回 NULL

示例:

	/* 创建定时器 */
	g_TimerSound = xTimerCreate(g_TimerSound,200,pdFALSE,NULL,GameSoundTimer_Func);

静态分配内存方式原型:

TimerHandle_t xTimerCreateStatic(const char * const pcTimerName, 
					TickType_t xTimerPeriodInTicks, 
					UBaseType_t uxAutoReload, 
					void * pvTimerID, 
					TimerCallbackFunction_t pxCallbackFunction, 
					StaticTimer_t *pxTimerBuffer ); 

参数说明:

参数说明
pcTimerName定时器名字, 用处不大, 仅在调试时用到
xTimerPeriodInTicks周期, 以 Tick 为单位
uxAutoReload类型, pdTRUE 表示自动加载, pdFALSE 表示一次性
pvTimerID回调函数可以使用此参数, 比如分辨是哪个定时器
pxCallbackFunction回调函数
pxTimerBuffer传入一个 StaticTimer_t 结构体, 将在上面构造定时器
返回值成功则返回 TimerHandle_t, 否则返回 NULL

回调函数的类型是:

typedef void (* TimerCallbackFunction_t)( TimerHandle_t xTimer ); 

void ATimerCallback( TimerHandle_t xTimer ); 

3.2 删除函数

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

定时器的很多 API 函数,都是通过发送"命令"到命令队列,由守护任务来实现。

如果队列满了,"命令"就无法即刻写入队列。我们可以指定一个超时时间xTicksToWait, 等待一会。

3.3 启动/停止函数

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

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

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

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

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

xTicksToWait不是定时器本身的超时时,不是定时器本身的"周期"。 创建定时器时,设置了它的周期(period)。xTimerStart()函数是用来启动定时器。假设调用xTimerStart()的时刻是tX,定时器的周期是n,那么在tX+n时刻定时器的回调函数被 调用。如果定时器已经被启动,但是它的函数尚未被执行,再次执行xTimerStart()函数相当于执行xTimerReset(),重新设定它的启动时间。

注意,这些函数的 xTicksToWait 表示的是,把命令写入命令队列的超时时间。命令队列可能已经满了,无法马上把命令写入队列里,可以等待一会。

3.4 复位函数

从定时器的状态转换图可以知道,使用 xTimerReset()函数可以让定时器的状态从冬眠态转换为运行态,相当于使用 xTimerStart()函数。 如果定时器已经处于运行态,使用xTimerReset()函数就相当于重新确定超时时间。假设调用xTimerReset()的时刻是tX,定时器的周期是n,那么tX+n就是重新确定的超时时间。

复位函数的原型如下:

/* 复位定时器 
 * 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);

3.5 修改周期函数

从定时器的状态转换图可以知道,使用 xTimerChangePeriod()函数,处理能修改它的 周期外,还可以让定时器的状态从冬眠态转换为运行态。 修 改 定 时 器 的 周 期 时 , 会 使 用 新 的 周 期 重 新 计 算 它 的 超 时 时 间 。 假设调用xTimerChangePeriod()函数的时间tX,新的周期是n,则tX+n就是新的超时时间。 相关函数的原型如下:

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

示例:

    /* 启动定时器 */
    xTimerChangePeriod(g_TimerSound,time_ms,0);

3.6 定时器 ID

定时器的结构体如下,里面有一项 pvTimerID,它就是定时器 ID:
在这里插入图片描述

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

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

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

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

这两个函数不涉及命令队列,它们是直接操作定时器结构体。

函数原型如下:

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

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

参考:
百问网FreeRTOS入门与工程实践-基于STM32

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值