第五章 . 软件时间任务
章节介绍和范围
软件时间用于计划未来的某个时间执行某个函数,固定周期的运行某个函数。被软件时间管理器执行的函数叫做软件时间管理回调函数。
软件时间任务的实现是在FreeRTOS内核控制中的。它不以来硬件支持,也不依赖硬件时间管理和硬件计数。
注意,在FreeRTOS哲学中要求用创新的设计实现高效的性能。软件时间管理器只有在执行回调函数时才会使用处理器时间。
软件时间管理功能是可选的,使用下列方法包含时间管理:
- 编译FreeRTOS中的源文件FreeRTOS/Source/timers.c as 到你的项目中。
- 在FreeRTOSConfig.h文件中设置
configUSE_TIMERS
为1
范围
这个章节指在让读者学会:
- 软件时间管理特点和任务管理特点比较
- 实时操作系统守护任务
- 时间管理和队列比较
- 瞬时时间任务和周期时间任务的不同点
- 如何创建,开始,重启和改变周期时间任务
软件时间任务回调函数
软件时间任务回调函数是用C语言实现的。唯一特别的是它的原型。它返回空,传递一个软件时间句柄。列表74展示了软件时间任务回调函数原型。
void ATimerCallback( TimerHandle_t xTimer );
软件时间任务回调函数从开始执行到结尾,以普通方式返回。它应当尽量短,不可以进入阻塞状态。
注意,正如接下来我们所见到的,软件时间任务运行在一个任务上下文中,这个任务上下文是调度器开启后自动创建的。一个基础认知必须知道,软件时间任务回调函数没有调用FreeRTOS中的任何会让调用任务进入阻塞的函数。可以调用例如xQueueReceive这样的函数,但函数中的xTicksToWait必须设置为0,因为这个参数指定函数最大可阻塞时间。不可以调用xTaskDelay()函数,因为它总是让调用者进入阻塞状态。
软件时间任务属性和状态
软件时间任务时期是指软件时间任务开始到软件时间任务回调函数运行中间的时间段。
一次和自动重载时间任务
有两种时间任务。
- 一次性时间任务:一旦开始,一个一次性时间任务只会执行一次它的回调函数。一个一次性时间任务可以被重启,但不能自己重启。
- 自动重载时间任务:一旦开始,自动重载任务会按照希望的时间自动重启。导致周期性执行它的回调函数。
图38消失了一次性时间任务和自动重载时间任务的不同之处。最后一行是tick中断发生的时间。
6
|<timer1时段>|
Timer1(一次性) | - |
Timer2(自动重载) | - - |
t1t2t3t4t5t6t7t8t9t10t11
|<timer2时段>|
5
图38解释:
时间任务1是一个一次性时间任务,时间段为6个ticks。t1开启时间任务,他会在6个ticks后执行回调任务,即t7时刻。因为是一次性任务,所以后面时间回调函数不会再次执行。
时间任务2是一个自动重载任务,时间段是5个ticks。t1时刻开启任务,之后每5个ticks即t6,t11时刻执行回调任务函数。
时间任务状态
时间任务可以处于以下两个状态中的1个:
- 休眠
一个休眠任务是存在的,可以用它的句柄引用。因为没有在运行,所以回调函数没有执行。 - 运行
自时间任务进入运行状态或被重置开始,消逝的时间等于时间任务时间段,时间任务会执行他的回调函数。
图39和图40展示了一次性时间任务和自动重载时间任务休眠态和运行态之间转换可能。两个图之间的关键差异是时间到期后的状态。自动重载时间任务执行它的回调函数后再次进入运行状态,一次性时间任务执行它的回调函数后进入休眠状态。
// 自动重载时间任务状态切换。图39
-------- --------
| 起点 |--调用xTimerCreate()-->| 休眠 |
-------- --------
| ^
| |
调用 调用
xTimerStart(), xTimerStop()
xTimerReset(), |
xTimerChangePeriod() |
| |
v |
----------
----->| 运行 |----
/ ---------- \
| |
\ 时间到调用时间回调 /
--------------------
// 一次性时间任务状态切换。图40
-------- --------
| 起点 |--调用xTimerCreate()-->| 休眠 |<--------
-------- -------- \
| ^ \
| | \
调用 调用 |
xTimerStart(), xTimerStop() /
xTimerReset(), | /
xTimerChangePeriod() | /
| | 时间到调用回调函数
v | /
---------- /
| 运行 |------
----------
软件时间任务上下文
实时系统守护(时间服务)任务
所有的时间任务回调函数都在相同的实时系统守护(时间服务)任务上下文执行。
这个守护任务是一个标准的FreeRTOS任务,它是调度器开启的时候自动创建的。它的优先级和栈大小由FreeRTOSConfig.h文件中的configTIMER_TASK_PRIORITY
和configTIMER_TASK_STACK_DEPTH
指定。
时间回调函数中不能调用会阻塞的FreeRTOS函数,因为如果时间回调函数中有阻塞函数,就会使守护进程任务进入阻塞状态。
时间任务命令队列
软件时间任务接口函数 通过一个队列从调用任务发送命令到守护任务。这个队列称作时间任务命令队列。图41有展示。时间任务命令有:开启一个时间任务,停止一个时间任务,重置一个时间任务等。
时间任务队列是一个标准的FreeRTOS队列,它也是开启调度器后自动创建的。它的长度由FreeRTOSConfig.h中的configTIMER_QUEUE_LENGTH
指定。
// 时间任务函数使用时间任务命令队列和实时系统守护任务沟通。图41
/* 首先是一个程序任务实现函数 */
void vAFunction( void ){
/* 这里写函数代码 */
/* 某些位置调用xTimerReset()函数。xTimerReset()实现会写时间任务命令队列 */
xTimerReset();
/* 写一些函数复位的代码 */
}
/* FreeRTOS守护任务伪代码,不是实际代码。 */
void prvTimerTask(...){
for(;;){
/* 写一些命令 */
xQueueReceive();
/* 处理命令 */
}
}
守护任务调度
守护任务调度和其他的FreeRTOS任务一样;它只会处理命令或者执行时间任务回调函数,当它是最高优先级任务时,就可以运行。图42和图43展示了configTIMER_TASK_PRIOTITY
配置项如何影响执行顺序。
图42展示了当守护任务优先级比调用xTimerStart()函数任务优先级低时的执行情况。
# 调用xTimerCreate()任务优先级比守护任务优先级高时执行顺序。图42
守护任务 | -- |
任务1 |------------ |
空闲任务 | ------|
t1 t2t3 t4t5
# t2:任务1调用xTimerStart()
# t3:xTimerStart()返回
# t4:守护任务开始处理开启时间任务命令
# t5:守护任务进入阻塞状态
图42中,任务1的优先级高于守护任务,守护任务优先级高于空闲任务。
在t1时刻:任务1进入运行状态,守护任务在阻塞状态。如果发送一个命令给守护任务,它就会离开阻塞状态,而等待处理这个命令。或者一个时间任务到期,就开始执行时间任务回调函数。
在t2时刻:任务1调用xTimerStart()函数。xTimerStart()会发送一个命令给时间命令队列,导致守护任务离开阻塞状态。任务1优先级高于守护任务,因此守护任务不会抢占任务1。任务1仍然处于运行状态,守护任务离开阻塞态,进入就绪态。
在t3时刻:任务1执行xTimerCreate()完成,任务1从xTimerStart()开始执行到函数结尾,不会离开运行状态。
在t4时刻:任务1调用函数进入阻塞状态。守护任务现在是就绪态中优先级最高的任务,所以调度器选择守护任务进入运行态。守护任务开始处理由任务1发送给时间命令列表的命令。
注意:是从命令发送到时间命令队列的时候开始计算时间任务时间段的,而不是从守护任务接收到"开启时间任务"命令时。
在t5时刻:守护任务完成处理任务1发送给时间命令队列的命令,而且会试图接收很多的命令从时间命令队列。由于时间命名队列是空的,因此守护任务再次进入阻塞态。如果时间命令队列再次接收到命令或软件时间任务到期,守护任务就会再次离开阻塞状态。
目前空闲任务成为就绪态中优先级最高的任务,所以调度器选择空闲任务执行。
图43展示了一个和图42类似的情况,只是这次守护任务优先级比调用xTimerStart()函数任务优先级高。
# 调用xTimerCreate()优先级比守护任务优先级低执行顺序。图43
守护任务 | -- |
任务 1 |----- ----- |
空闲任务 | -----|
t1 t2t3t4 t5
# t2:任务1调用xTimerCreate()函数。
# t2:守护任务处理"开启时间任务"指令。
# t3:守护任务进入阻塞状态。
# t4:调用xTimerCreate()返回
在图43中,守护任务优先级比任务1优先级高,任务1优先级又比空闲任务优先级高。
在t1时刻:任务1进入运行态,守护任务处于阻塞状态。
在t2时刻:任务1调用xTimerCreate()。xTimerCreate()发送一个命令给时间命令队列,导致守护任务离开阻塞状态。守护任务优先级高于任务1优先级,所以调度器选择守护任务执行。守护任务开始处理任务1发送到时间命令队列。
在t3时刻:守护任务处理完任务1发送给时间命令队列的命令,试图从时间命令队列读取更多数据。而时间命令队列是空的,所以守护任务再次进入阻塞状态。任务1再次成为就绪状态最高优先级任务,被调度器选取执行。
在t4时刻:任务1在执行完成xTimerCreate()之前被守护任务抢占,它只有再次进入运行状态后才能退出xTimerCreate()。
在t5时刻:任务1调用可以进入阻塞态的函数。空闲任务成为就绪态最高优先级任务,调度器选中空闲任务执行。
和图42展示的一样,任务1传递一个命令到时间命令队列,守护任务读取这个命令。在图43展示中,守护任务在任务1从发送命令函数返回前,接收和处理了从任务1发送到时间命令队列的命令。
发送到时间命令队列的任务包含一个时间戳,这个时间戳用来度量任务什么时候发送的命令。比如,一个"开启时间任务"命令发送开始时间段是10个tick数,时间戳用于确保在发送这个命令后10个tick数量后到期,而不是守护任务处理命令后10个tick数。
xTimerCreate()函数
FreeRTOS V9.0.0也包含了xTimerCreateStatic()函数,它会在编译的时候静态的分配需要的内存。一个软件时间任务在使用之前需要明确的创建。
软件时间任务使用TimerHandle_t
进行引用。xTimerCreate()函数用于创建一个软件时间任务,它会返回一个由它创建的TimerHandle_t
软件时间任务引用。
软件时间任务可以在调度器开启前创建,也可以在调度器开启后在任务中创建。
第0章有介绍数据格式和命名规则
// xTimerCreate()原型。列表73
TimerHandle_t xTimerCreate(const char * const pcTimerName, TickType_t xTimerPeroidInTicks, BaseType_t uxAutoReload, void *pvTimerID, TimerCallbackFunction_t pxCallbackFunction);
// 参数
/* pcTimerName: 时间任务的名字。FreeRTOS中没有任何形式使用。只是为了调试方便,人类识别名字比句柄更加容易,仅此而已。
* xTimerPeroidInTicks: 时间任务时间段,以tick数量指定。可以用pdMS_TO_TICKS()将时钟时间的ms转换成tick数量。
* uxAutoReload: 设置uxAutoReload为pdTRUE会创建可重载时间任务,设置为pdFALSE创建一次性时间任务。
* pvTimerID: 每个时间任务都有一个ID值,这个ID值是一个空指针,可以被程序员行为某些原因使用。这个ID在同一个回调函数用于多个时间任务时,因为它可以用作特定时间存储。这个章节就有一个时间任务ID的例子。pvTimerID用于创建时初始化时间任务ID值。
* pxCallbackFunction: 软件时间任务回调函数是一个见到的C函数,他的原型在列表72中有列出。这里的pxCallbackFunction只是一个函数指针(实际上就是函数名字)。
* 返回值: 如果返回NULL,软件时间任务不能被创建,因为FreeRTOS没有足够的堆空间用于创建时间任务需要的数据结构。返回一个非NULL的值表示创建时间任务成功。返回值是创建时间任务句柄。第2章提供了更多堆管理信息。
*/
xTimerStart()函数
xTimerStart()用于开启一个休眠态的任务或者重置一个运行态的任务。xTimerStop()用于停止一个运行态时间任务,停止一个软件时间任务意味着将它转换成休眠态。
xTimerStart()可以在调度器开启前调用,但调用后时间任务不是真的开启了,还需要等待调度器开启。
注意不要在中断处理函数中使用xTimerStart(),而应该使用中断安全版本xTimerStartFromISR()。
// xTimerStart()原型。列表74
BaseType_t xTimerStart(TimerHandle_t xTimer, TickType_t xTicksToWait);
/* 参数
* xTimer: 被开启或重置的软件时间任务句柄,这个句柄是创建软件时间任务时调用xTimerCreate()时返回的。
* xTicksToWait: xTimerStart()使用时间命令队列发送"开启时间任务"命令到守护任务。xTicksToWait用于指定时间命令队列满时进入阻塞状态的最长阻塞时间。
* 如果xTicksToWait为0, xTimerCreate()会立即返回。
* 这个阻塞时间是用tick数量指定的,因此具体时间依赖于tick的频率。可以使用pdMS_TO_TICKS()将时钟时间转换为tick数量。
* 如果在FreeRTOSConfig.h文件中的INCLUDE_vTaskSuspend设置为1,那么设置xTicksToWait为portMAX_DELAY会使调用者,在时间命令队列有可用数据前一直处于阻塞状态。
* 如果在调度器开启之前调用xTimerStart(),xTicksToWait的值会被忽略,相当于xTicksToWait设置为0。
* 返回值:
* 有两个可能的返回值
* pdPASS:如果成功发送"开启一个时间任务"命令到时间命令队列,就返回pdPASS。
* 如果守护任务的优先级比调用xTimerStart()任务高,那么调度器会确保处理"开启时间任务"命令在xTimerStart()返回前处理。这是因为一旦数据发送到时间命令队列,守护任务就会立刻抢占调用xTimerStart()的任务。
* 如果阻塞时间不是0,那么调用任务可能会进入阻塞,等待时间命令队列数据可用,在xTimerCreate()返回之前。但必须在阻塞时间到xTicksToWait之前,数据就被写入到时间命令队列之中。
* pdFALSE: 如果时间命令队列已经满了,"开启时间任务"命令不能被写入到时间命令队列之中就会返回pdFALSE。
* 如果阻塞时间xTicksToWait不为0,那么调用任务就会进入阻塞态,等待守护任务让时间命令队列有可用空间。最后如果在xTicksToWait时间到期后,队列还没有可用空间就返回pdFALSE。
*/
例13,创建一个一次性和自动重载时间任务
这个例子创建和启动以后一次性和自动重载时间任务,代码在列表75中展示。
// 例13创建和开启时间任务。列表75
/* 用做一次性时间任务的时间段3.333秒,和用于可重载时间任务的时间段0.5秒*/
#define mainONE_SHOT_TIMER_PERIOD pdMS_TO_TICKS(3333)
#define mainAUTO_RELOAD_TIMER_PERIOD pdMS_TO_TICKS(500)
int main( void ){
TimerHandle_t xAutoReloadTimer, xOneShotTimer;
BaseType_t xTimer1Started, xTimer2Started;
/* 创建一次性时间任务,引用句柄存放在xOneShotTimer中 */
xOneShotTimer = xTimerCreate(
/* 时间任务名字,不会被FreeRTOS使用 */
"OneShot",
/* 时间软件tick周期数量 */
mainONE_SHOT_TIMER_PERIOD,
/* 设置uxAutoReload为pdFALSE,表示一次性时间任务*/
pdFALSE,
/* 这个时间任务不使用任务ID */
0,
/* 时间任务回调函数 */
prvOneShotTimerCallback
);
/* 创建自动重载时间任务,引用句柄存放在xAutoReloadTimer中*/
xAutoReloadTimer = xTimerCreate(
/* 时间任务名字,不会被FreeRTOS使用 */
"AutoReload",
/* 时间软件tick周期数量 */
mainAUTO_RELOAD_TIMER_PERIOD,
/* 设置uxAutoReload为pdFALSE,表示一次性时间任务*/
pdTRUE,
/* 这个时间任务不使用任务ID */
0,
/* 时间任务回调函数 */
prvAutoReloadTimerCallback
);
/* 检查两个时间任务是否创建 */
if(xOneShotTimer != NULL && xAutoReloadTimer != NULL){
/* 开启软件时间任务,这里的阻塞时间为0。未开启调度器阻塞时间参数无用*/
xTimer1Started = xTimerStart( xOneShotTimer, 0 );
xTimer2Started = xTimerStart( xAutoReloadTimer, 0 );
/* xTimerStart()实现使用了时间命令队列,如果时间命令队列已满xTimerStart()会调用失败。调度器开启之前时间守护任务不会被创建。因此在调度器开启之前,所有的发送给时间命令队列的命令都会被保存在队列中。检查2个时间任务是否创建成功。*/
if( (xTimer1Started == pdPASS ) && ( xTimer2Started == pdPASS))
/* 开启调度器 */
vTaskStartScheduler();
}
/* 如果一切顺利,这里的代码不会运行 */
for(;;);
}
每次时间任务回调函数只会打印一个字符串信息。一次性时间任务的回调函数在列表76中列出。自动重载时间任务回调函数在列表77中列出。
// 例13,一次性时间任务回调函数。列表76
static void prvOneShotTimerCallback( TimerHandle_t xTimer ){
TickType_t xTimeNow;
/* 包含当前tick计数 */
xTimeNow = xTaskGetTickCount();
/* 打印这个回调函数正在执行 */
vPrintStringAndNumber( "One-shot timer callback executing", xTimeNow );
/* 任务执行次数变量 */
ulCallCount++;
}
// 例13,自动重载时间任务回调函数,列表77
static void prvAutoReloadTimerCallback( TimerHandle_t xTimer ){
TickType_t xTimeNow;
/* 包含当前tick计量 */
xTimeNow = xTaskGetTickCount();
/* 打印这个回调函数正在执行 */
vPrintStringAndNumber( "Auto-reload timer callback executing", xTimeNow );
/* 任务执行次数变量 */
ulCallCount++;
}
运行这个例子会打印出图44的内容。图44显示自动重载时间任务回调函数每500个ticks执行1次,因为列表75中mainAUTO_RELOAD_TIMER_PERIOD
设定为500,一次性时间任务回调函数只执行了一次,就是当tick数3333到期时。
# 实例13运行输出。图44
Auto-reload timer callback executing 500
Auto-reload timer callback executing 1000
Auto-reload timer callback executing 1500
Auto-reload timer callback executing 2000
Auto-reload timer callback executing 2500
Auto-reload timer callback executing 3000
One-shot timer callback executing 3333
Auto-reload timer callback executing 3500
Auto-reload timer callback executing 4000
Auto-reload timer callback executing 4500
Auto-reload timer callback executing 5000
Auto-reload timer callback executing 5500
时间任务ID
每个软件时间任务都有一个ID。它是一个标签值,可以给程序员使用。这个ID可以放在一个空指针之中(void *),所以也可以存放一个整数在里面,指向其它目标,或只是用做一个函数指针。
当软件时间任务创建时会设定一个时间任务ID的初始值,之后可以使用vTimerSetTimerID()函数设置这个ID值,也可以使用pvTimerGetTimerID()函数获取。
和其它软件时间任务函数不同,vTimerSetTimerID()和pvTimerGetTimerID()会直接访问软件时间任务,不会发送命令给时间命令队列。
// vTimerSetTimerID()函数原型。列表78
void vTimerSetTimerID( const TimerHandle_t xTimer, void *pvNewID );
/* 参数
* xTimer: 将被更新ID值软件时间任务句柄。这个句柄是使用xTimerCreate()函数创建的软件时间任务返回的句柄。
* pvNewID: 软件时间任务要被设置成的ID。
*/
// pvTimerGetTimerID()原型。列表79
void *pvTimerGetTimerID( TimerHandle_t xTimer );
/* 参数
* xTimer: 要获取的软件时间任务句柄。这个句柄是使用xTimerCreate()函数创建的软件时间任务返回的句柄。
* 返回值:返回这个软件时间任务的ID值。
*/
例14,时间任务回调函数和ID使用
同一个回调函数可以用于多个软件时间任务。这个时候,回调函数的参数用于确定是那个软件时间任务。
例13用了两个不同的回调函数 ;一个用于一次性时间任务,另一个用于自动重载时间任务。例14创建相似的函数 ,但两个时间任务只使用一个回调函数。
例14的main()函数和例13的基本类似。唯一不同的时创建软件时间任务代码。它们的不同之处已经在列表80中列出,两个时间任务都将prvTimerCallback()用做回调函数。
// 例14,创建2个时间任务。列表80
/* 创建一次性时间任务,保存句柄在xOneShotTimer */
xOneShotTimer = xTimerCreate( "OneShot",
mainONE_SHOT_TIMER_PERIOD,
pdFALSE,
0,
prvTimerCallback
);
/* 创建自动重载时间任务,保存句柄在xAutoReloadTimer */
xAutoReloadTimer = xTimerCreate( "AutoReload",
mainAUTO_RELOAD_TIMER_PERIOD,
pdTRUE,
0,
prvTimerCallback
);
prvTimerCallback()会在各自的时间到期时执行。prvTimerCallback()的实现使用函数参数决定调用它的是一次性时间任务还是自动重载时间任务。
prvTimerCallback()中也展示了如何使用时间任务ID这个特别的变量;每个软件时间任务都会保存一个任务数量计数在自己的ID中,自动重载时间任务使用这个ID值在15个tick后停止自己。
列表81中列出了prvTimerCallback()函数的实现。
// 便14,时间任务回调函数。列表81
static void prvTimerCallback( TimerHandle_t xTimer ){
TickType_t xTimerNow;
uint32_t ulExecutionCount;
/* 这个软件时间任务希望的时间计数保存在时间任务ID中。包括这个ID,增加它,然后保存新的ID值。这个ID是空指针类型,因此强制转换为uint32_t */
ulExecutionCount = (uint32_t)pvTimerGetTimerID(xTimer);
ulExecutionCount++;
vTimerSetTimerID( xTimer, (void *)ulExecutionCount );
/* 取出当前tick计数 */
xTimerNow = xTaskGetTickCount();
/* 当创建一次性时间任务后,一次性时间任务句柄保存在xOneShotTimer中。将传递给当前函数的句柄和xOneShotTimer比较决定它是一次性时间任务,还是版重载时间任务。然后用一个字符串显示这个函数执行的时点*/
if( xTimer == xOneShotTimer )
vPrintStringAndNumber("One-shot timer callback executing", xTimerNow);
else{
/* xTimer不等于xOneShotTimer, 因此调用当前函数的时间只能是自动重载时间任务 */
vPrintStringAndNumber("Auto-reload timer callback executing", xTimerNow);
if(ulExecutionCount == 5){
/* 自动重载时间任务执行5次之后停止。回调函数在实时系统守护任务上下文执行,因此不会调用可能进入阻塞状态的函数。所以这里的阻塞时间是0。*/
xTimerStop( xTimer, 0 );
}
}
}
图45显示了例14的输出。可以看出自动重载时间任务只执行了5次。
# 例14执行输出。图45
Auto-reload timer callback executing 500
Auto-reload timer callback executing 1000
Auto-reload timer callback executing 1500
Auto-reload timer callback executing 2000
Auto-reload timer callback executing 2500
One-shot timer callback executing 3333
改变时间任务周期
每个FreeRTOS官方接口都提供了一个或多个实例项目。大部分项目会自检,LED灯用于反馈项目状态;如果自检通过,LED会慢慢闪烁,如果自检错误,那么LED灯会快速闪烁。
一些实例项目在任务中执行自检,例用vTaskDelay()控制LED闪烁的速度。其它一些实例用一个软件时间任务实现自检,使用时间任务周期控制LED闪烁的速度。
xTimerChangePeriod()函数
软件时间任务周期可以使用xTimerChangePeriod()函数改变。
如果用xTimerChangePeriod()改变已经运行的时间任务周期,那么这个时间任务会使用一个新的周期值重新诸它的期满时间。这个重新计算期满时间和什么时候调用xTimerChangePeriod()函数有关,和时间任务什么时候开启无关。
如果用xTimerChangePeriod()函数改变休眠态的时间任务,这个时间任务没有在运行状态,那么这个时间任务会计算一个期满时间,并切换为运行状态,就是说这个时间任务会进入运行态。
注意:不要在中断处理中调用xTimerChangePeriod()函数,要在中断处理函数中使用中断安全版本xTimerChangePeriodFromISR()代替。
// xTimerChangePeriod()函数原型。列表82
BaseType_t xTimerChangePeriod( TimerHandle_t xTimer,
TickType_t xNewTimerPeriodInTicks,
TickType_t xTicksToWait );
/* 参数
* xTimer: 将被更新为一个新的周期值的时间任务句柄。这个句柄是通过xTimerCreate()函数创建软件时间任务的句柄。
* xNewTimerPeriodInTicks: 新的软件时间任务周期值,以tick数量方式指定。可以使用pdMS_TO_TICKS()宏将时钟时间ms转换为tick数量。
* xTicksToWait: xTimerChangePeriod()会通过时间命令队列发送一个"改变周期"命令给守护任务。当时间命令队列已满时,xTicksToWait指定一个调用任务可以保持在阻塞状态,等待这个队列有可用空间的最大的tick数量。
* 如果时间命令队列已满,xTicksToWait为0时,xTimerChangePeriod()会立刻返回。
* 可以使用pdMS_TO_TICKS()宏将ms的时钟周期转换为特定的tick数量
* 如果FreeRTOSConfig.h中的INCLUDE_vTaskSuspend设置为1。可以将xTicksToWait设置为portMAX_DELAY,让xTimerChangePeriod()函数在时间命令队列有空闲位置之前一起阻塞下去,就是不会因为时间到期而强制返回。
* 如果在开启调度器之前调用这个函数,那么xTicksToWait就没有作用,相当于xTicksToWait为0。
* 返回值
* 可能有两种返回值
* pdPASS: 如果命令成功发送给时间命令队列就返回pdPASS.
* 如果阻塞时间不为0,那么这个函数返回前调用它的任务可能进入阻塞状态,等待时间命令队列有可用空间。但最后队列会在到期前成功将数据写入队列。
* pdFALSE: 如果因为时间命令队列已满,"改变周期"命令无法写入队列就会返回pdFALSE。
* 如果阻塞时间不为0,那么调用任务就可能进入阻塞态,等待守护任务让时间命令队列有空闲空间。但阻塞的时间会在队列有空间空间发生前到期。
*/
列表83展示了包含自检功能的FreeRTOS实例,如何在软件时间回调函数中,使用xTimerChangePeriod()函数增加LED灯闪烁的速度表达出错的。软件时间任务用于自检叫做自检时间任务。
// xTimerChangePeriod()使用
/* 自检时间任务用一个3S周期创建,所以LED灯3S闪烁一次。如果自检功能出错,自检时间任务周期就会改变为200ms,引起led闪烁速度大增加*/
const TickType_t xHealthyTimerPeriod = pdMS_TO_TICKS(3000);
const TickType_t xErrorTimerPeriod = pdMS_TO_TICKS(200);
/* 这个回调函数用做自检时间任务 */
static void prvCheckTimerCallbackFunction( TimerHandle_t xTimer ){
static BaseType_t xErrorDetected = pdFALSE;
if( xErrorDetected == pdFALSE )
{
/* 没有错误发生,再次运行自检函数。这个函数要求每个由实例创建的任务报告自身状态,也会检查所有运行的任务,因此可以报告它们的状态。*/
if( CheckTasksAreRunningWithoutError() == pdFAIL ){
/* 一个或更多任务报告了不希望的状态。可能发生了一个错误。在这个回调函数中增加自检时间任务周期,就相当于增加了LED灯闪烁的速度。这个回调函数执行上正文是在实时系统守护任务中,不能调用可能进行阻塞态的函数,所以阻塞时间设定为0。*/
xTimerChangePeriod( xTimer, xErrorTimerPeriod, 0);
}
/* 标记已经发生了错误 */
xErrorDetected = pdTRUE;
}
/* 翻转LED灯。LED闪烁的速度依赖于这个回调函数调用的周期,这是由自检时间任务周期决定的。这个任务周期在CheckTasksAreRunningWithoutError()返回pdFAIL时会从3000ms降至200ms。*/
ToggleLED();
}
重置软件时间任务
重置软件时间任务意味着重启这个时间任务;这个时间任务关联的期望到期时间会在重置时重新计算,而不是时间任务初始化时。图46显示了重置时间任务时序图,图中的时间任务开始时周期是6个ticks,然后在调用它的回调函数前,重置了2次。
# 开启和重置一个周期为6个ticks的时间任务。图46
|<----期满时间计算----->|
t7(t1 + 6)
|<----期满时间计算----->|
t11(t5+6)
|<----期满时间计算----->|
t15(t9+6)
t1 t2 t3 t4 t5 t6 t7 t8 t9 t10 t11 t12 t13 t14 t15
# t1: 开启时间任务
# t5: 重置时间任务
# t9: 重置时间任务
# t15: 执行时间任务
图46说明:
- 时间任务1在t1时刻开启。它的周期是6,因此时间任务1初始化后计算出期满时间是t7,就是6个ticks后。
- 时间任务1在t7前重置,因此是在它执行回调函数前。时间任务1在t5时刻重置,因此在这个时候时间任务1回调函数执行时间重新计算后是t11,就是在重置后的6个ticks。
- 时间任务1在t11前重置,因此是在它执行回调函数前。时间任务1在t9时刻重置,因此在这个时候任务1回调函数执行时间重新计算后是t15,就是在重置后的6个ticks。
- 时间任务1没有再次重置,t15时间期满,执行它的回调函数。
xTimerReset()函数
时间任务可以使用xTimerReset()重置。
xTimerReset()也可以用来重置一个休眠态的时间任务。
注意:不要在中断处理函数中使用xTimerReset(),而应当使用中断安全版本xTimerResetFromISR()代替。
// xTimerReset()函数原型。列表84
BaseType_t xTimerReset( TimerHandle_t xTimer, TickType_t xTicksToWait );
/* 参数
* xTimer: 将被重置或开启的软件时间任务句柄。这个句柄是调用xTimerCreate()创建软件时间任务的句柄引用。
* xTicksToWait: xTimerReset()函数用时间命令队列将"重置"命令发送给守护任务。xTicksToWait指定了当时间命令队列已经满时,调用xTimerReset()函数的任务最长可阻塞的tick数量,用于等待时间队列任务有空闲空间。
* 如果xTicksToWait为0,xTimerReset()函数会立即返回。
* 如果FreeRTOSConfig.h文件中的INCLUDE_vTaskSuspend设置为1。可以将 xTicksToWait设置为portMAX_DELAY,使xTimerReset()函数一直等待时间命令函数出现空闲空间,也就是说阻塞时间无限,时间命令队列一直没有空闲空间,这个函数就一直等待下去。
* 返回值
* 可能有两种返回值
* 1. pdPASS: 如果数据成功写入到时间命令参数,就返回pdPASS。
* 如果阻塞时间不为0,调用这个函数的任务可能进入阻塞状态,等待时间命令队列中有空闲空间可用。但在阻塞时间期满前数据会成功写入时间命令队列,也会返回pdPASS。
* 2. pdFALSE: 因为时间命令队列已经满,"重置"命令无法写入到时间命令队列,就会返回pdFALSE。
* 如果阻塞时间不为0,那么调用这个函数的任务可能进入阻塞状态,等待时间命令队列中有空闲空间可用。但在阻塞时间期满后时间命令队列中方才出现可用空间。
*/
例15,重置软件时间任务
这个例子的行为和手机背景灯行为类似,这个背景灯:
- 当按一个键时打开
- 一个确定时间周期内又有键按下,就保持在打开状态。
- 一个确定时间周期内无键按下,就关闭打开的背景灯
用一个一次性时间任务实现这个功能: - 当按下一个键时打开背景灯,在回调函数中关闭这个背景灯。
- 这个软件时间任务每次按下一个键时会重置时间任务。
- 如果不按下一个按键,背景灯就会被关闭,这里的必需按下按键的最长时间就等于软件时间周期;如果时间任务到期前没有按下重置键,软件时间任务回调函数就会关闭这个背景灯。
xSimulatedBacklightOn变量保存了背景灯的状态。如果背景灯打开xSimulatedBacklightOn设置为pdTRUE,否则设置为pdFALSE。
列表85列出了这个软件时间任务回调函数。
// 例15一次性软件时间任务回调函数。列表85
static void prvBacklightTimerCallback( TimerHandle_t xTimer ){
TickType_t xTimerNow = xTaskGetTickCount();
/* 背景灯时间任务期满,关闭背景灯 */
xSimulatedBacklightOn = pdFALSE;
/* 打印关闭背景灯的时间 */
vPrintStringAndNumber("Timer expired, turning backlight OFF at timer\t\t", xTimeNow);
}
例15创建一个轮询按键的任务。这个任务在列表86中列出,这里的代码只是为了描述。而不是为了提供一个最优的方式。
用FreeRTOS允许你的程序是事件驱动的。事件驱动模式非常有效的使用处理器时间,因为只有在事件发生时才会使用处理器时间,面不是浪费在轮询没有发生的事件上。但例15没有使用事件驱动,因为使用FreeRTOS的windows接口时不建议处理按键中断,所以只能使用低效的轮询技术代替。如果列表86是一个中断处理程序,那么要使用xTimerResetFromISR()代替。
** 在Windows终端打印和从Windows终端读按键都要执行Windows的系统调。Windows系统调用包括Windows终端使用,硬盘,TCP/IP栈。这些都可能会对FreeRTOS的Windows端口产生不利影响,通常应当避免。
// 例15任务用作重置软件时间任务。列表86
static void vKeyHitTask(void *pvParameters){
const TickType_t xShortDelay = pdMS_TO_TICKS(50);
TickType_t xTimeNow;
vPrintString( "Press a key to turn the backlight on.\r\n ");
/* 实际应用应当是事件驱动,用一个中断处理按键。在FreeRTOS的Windows端口中不建议使用按键中断,这里只能使用轮询来检测按键 */
for(;;){
/* 是否按下一个按键 */
if(_kbhit() != 0){
/* 已经按下按键,记录时间 */
xTimeNow = xTaskGetTickCount();
if(xSimulatedBacklightOn == pdFALSE){
/* 背景灯是关闭的,打开背景灯,打印打开时间 */
xSimulatedBacklightOn = pdTRUE;
vPrintStringAndNumber("Key pressed, reseting software timer at time\t\t", xTimeNow);
}
else{
/* 背景灯已经打开,打印一个字符串提示时间任务即将被重置 */
vPrintStringAndNumber("Key pressed, reseting software timer at time\t\t", xTimeNow);
}
/* 重置软件时间任务。如果之前背景灯是关闭的,那么这个调用会打开时间任务。如果背景灯已经打开,那么这个调用会重置时间任务。一个实际的应用可能会在中断中读取按键值。如果这个函数是一个中断服务程序,那么应当使用xTimerResetFromISR()代替xTimerReset()。*/
xTimerReset(xBacklightTimer, xShortDelay);
/* 读取并丢弃按键值,这个例子中不会用到 */
(void) _getchar();
}
}
例15运行输出如图47。下面是对图47的说明:
- 第一个按键按下发生在tick计数为812。这时背景灯打开,一次性时间任务开启。
- tick计数是1813,3114,4015和5016时又有按键按下。所有这些按键按下都会使时间任务在期满前重置时间任务。
- tick计数在10016时时间任务期满。这时背景灯关闭。
# 例15输出。图47
Press a key to turn the backlight on.
Key pressed, turning backlight On at time 812
Key pressed, resetting software timer at time 1013
Key pressed, resetting software timer at time 3114
Key pressed, resetting software timer at time 4015
Key pressed, resetting software timer at time 5016
Timer expired, turning backlight OFF at time 10016
可以看到图47中的时间任务周期是5000个ticks;在最后一个按键按下后5000个ticks背景灯被关闭,因此时间任务重置后5000个ticks时间任务期满。