FreeRTOS软件定时器,相对前面的内容来说,软件定时器还是比较简单的,我们简单测试一下
因为是简单介绍,所以原理和源码的分析不会那么详细,具体可以根据文中API查看源码
使用起来记住创建,启动,回调函数核心步骤即可
说明:FreeRTOS 专栏与我的 RT-Thread 专栏不同,我的 RT-Thread 专栏是从理论学习一步一步循序渐进,从 0 起步的 完整教学,而 FreeRTOS 更偏向于 我直接拿来使用,需要用到什么,然后引出知识点,在使用中发现问题,解然后再解决问题,
本 FreeRTOS 专栏记录的开发环境:
FreeRTOS记录(一、熟悉开发环境以及CubeMX下FreeRTOS配置)
FreeRTOS记录(二、FreeRTOS任务API认识和源码简析)
FreeRTOS记录(三、RTOS任务调度原理解析_Systick、PendSV、SVC)
FreeRTOS记录(四、FreeRTOS任务堆栈溢出问题和临界区)
FreeRTOS记录(五、FreeRTOS任务通知)
FreeRTOS记录(六、FreeRTOS消息队列—Enocean模块串口通讯、RAM空间不足问题分析)
FreeRTOS记录(七、FreeRTOS信号量、事件标志组、邮箱和消息队列、任务通知的关系)
问:什么时候使用软件定时器,什么时候使用硬件定时器?
软件定时器可以解决硬件定时器数量不够的问题,理论上软件定时器可以很多,每个芯片的定时器外设是有限的,如果硬件定时器不够用,可以使用软件定时器。
但是,软件定时器相对硬件定时器来说,精度没有那么高(为什么不高?因为它以系统时钟为基准,系统时钟中断优先级又是最低,容易被打断)。 对于需要高精度要求的场合,不建议使用软件定时器。
同时软件定时器 是需要占用一部分内存空间的,用到的软件定时器数量越多,内存占用越大。 如果RAM空间不够用,不能使用软件定时器。
当然,使用软件定时器的程序还有一个好处就是方便移植,不同芯片的硬件定时器的设置代码是不一样的,在条件允许的情况下使用软件定时器,那么不同平台之间的的代码移植起来相对方便一点。
一、FreeRTOS软件定时器基础
1.1 时钟来源
系统的时钟周期,对于FreeRTOS而言,就是 TICK_RATE_HZ 对应的值。在以上的测试我们使用都是设置为默认的1000,那么系统的时钟节拍周期就为 1ms(1s 跳动 1000 下,每一下就为 1ms)。
1.2 运行原理
FreeRTOS 所创建的软件定时器共用一个任务prvTimerTask
(也叫守护任务 Daemon)和队列,定时器处理API函数最终都是通过给队列发送信息,在任务中接收处理。
创建prvTimerTask
任务:
在文章 FreeRTOS记录(六、FreeRTOS消息队列—Enocean模块串口通讯、RAM空间不足问题分析) 中的 第 5小结:5 、RAM空间不足问题 中讲到过只要使能了使用定时器,系统就会自动产生一个任务,占用内存空间,说的就是这个任务:
prvTimerTask
任务:
- 优先级为我们定时器的配置 configTIMER_TASK_PRIORITY
- 任务的堆栈大小为 configTIMER_TASK_STACK_DEPTH
队列:
- 队列的长度由配置 configTIMER_QUEUE_LENGTH 决定
prvTimerTask
任务会在其执行期间检查用户启动的时间周期溢出的定时器,并调用其回调函数。在任务中最后会调用prvProcessReceivedCommands();
函数:
定时器消息队列的命令在这个函数中进行处理:
在定时器创建好了以后,定时器并不会运行。在prvTimerTask
任务中,如果暂时没有运行中的定时器,任务会进入阻塞态等待命令。 使用xTimerStart
才会开始定时器的运行,启动函数通过“定时器命令队列” 向定时器任务发送一个启动命令,这个命令最终就在prvProcessReceivedCommands();
函数中解析,定时器任务获得命令就解除阻塞,然后执行启动软件定时器命令。
## 1.3 使用注意事项
- 软件定时器的定时时间必须是系统时钟周期的整数倍,如果我们定义的 TICK_RATE_HZ 为100,系统的时钟周期为10ms,必须为10的整数倍。那么5ms,15ms的延时是无法实现的。
- 软件定时器使用了系统的一个队列和一个任务资源,软件定时器任务的优先级默认为configTIMER_TASK_PRIORITY,为了更好响应,该优先级应设置为所有任务中最高的优先级
- 软件定时器的回调函数中应快进快出,切不可在定时器回调函数中调用任何将定时器任务挂起的函数,比如vTaskDelay(), vTaskDelayUntil()以及非零延迟的消息队列和信号量相关的函数,也绝对不允许出现死循环。
二、API介绍
FreeRTOS软件定时器的API所有的都可以在FreeRTOS驱动文件timers.h
文件中找到,这里介绍常用的基本的几个:
2.1 创建定时器
xTimerCreate()
和xTimerCreateStatic()
创建以 xTimerCreate()
为例介绍:
TimerHandle_t xTimerCreate
( const char * const pcTimerName, /* 定时器名字,方便识别不同的定时器 */
const TickType_t xTimerPeriod, /* 定时器周期,单位系统时钟节拍 */
const UBaseType_t uxAutoReload, /* 若参数为 pdTRUE,则表示选择周期模式,
若参数为pdFALSE,则表示选择单次模式 */
void * const pvTimerID, /* 创建不同的定时器,但使用相同的回调函数时,
在回调函数中通过不同的ID 号来区分不同的定时器。 */
TimerCallbackFunction_t pxCallbackFunction ); /* 定时器回调函数 */
...
void pxCallbackFunction (xTimerHandle pxTimer){...}
在CubeMX中封装后的函数为osTimerCreate
:
2.2 开始定时器(为什么开启定时器还需要等待时间)
xTimerStart()
和xTimerStartFromISR()
xTimerStart():
BaseType_t xTimerStart( TimerHandle_t xTimer, /* 定时器句柄 */
TickType_t xBlockTime ); /* 成功启动定时器前的最大等待时间设置,单位系统时钟节拍
如果在 FreeRTOS 调度器开启之前调用 xTimerStart(),该形参将不起作用*/
说明:为什么开启定时器还需要等待时间?
前面 运行原理 已经介绍过,FreeRTOS的软件定时器是通过消息队列给定时器任务发消息来实现的,此参数设置的等待时间就是当消息队列已经满的情况下,等待消息队列有空间时的最大等待时间。
定时器任务实际执行消息队列发来的命令依赖于定时器任务的优先级,如果定时器任务是高优先级会及时得到执行,如果是低优先级,就要等待其余高优先级任务释放 CPU 权才可以得到执行。
对于已经被激活的定时器,即调用过函数 xTimerStart
进行启动,再次调用此函数相当于调用了函数xTimerReset
对定时器时间进行了复位。
如果在启动 FreeRTOS 调度器前调用了此函数,定时器是不会立即执行的,需要等到启动了 FreeRTOS调度器才会得到执行,即从此刻开始计时,达到xTimerCreate
中设置的单次或者周期性延迟时间才 会执行相应的回调函数。
xTimerStartFromISR():
/*******************************************************************************************************
*@ 函数功能:在中断中启动一个软件定时器。
*@ 函数参数:xTimer:软件定时器句柄
pxHigherPriorityTaskWoken:定时器守护任务的大部分时间都在阻塞态等待定时器命令队列的命令。
调用函数 xTimerStartFromISR()将会往定时器的命令队列发送一个启动命令,这很有可能会将定时器任务从阻塞除。
如果调用函数xTimerStartFromISR()让定时器任务脱离阻塞态,
且定时器守护任务的优先级大于或者等于当前被中断的任务的优先级,
那么 pxHigherPriorityTaskWoken 的值会在函数xTimerStartFromISR()内部设置为 pdTRUE,
然后在中断退出之前执行一次上下文切换。
pxHigherPriorityTaskWoken:pxHigherPriorityTaskWoken 在使用之前必须初始化成pdFALSE。
调用xEventGroupSetBitsFromISR()会给守护任务发送一个消息,
如果守护任务的优先级高于当前被中断的任务的优先级的话
(一般情况下都需要将守护任务的优先级设置为所有任务中最高优先级),
pxHigherPriorityTaskWoken 会被置为 pdTRUE, 然后在中断退出前执行一次上下文切换。
*@ 返回值:如果启动命令无法成功地发送到定时器命令队列则返回 pdFAILE,成功发送则返回pdPASS。
软件定时器成功发送的命令是否真正的被执行也还要看定时器守护任务的优先级,
其优先级由宏 configTIMER_TASK_PRIORITY 定义。
*******************************************************************************************************/
#define xTimerStartFromISR( xTimer, pxHigherPriorityTaskWoken ) xTimerGenericCommand( ( xTimer ), tmrCOMMAND_START_FROM_ISR,( xTaskGetTickCountFromISR() ),( pxHigherPriorityTaskWoken ), 0U )
2.3 获取定时器ID
pvTimerGetTimerID
void *pvTimerGetTimerID( const TimerHandle_t xTimer ) /* 定时器句柄 */
2.4 停止/删除定时器
xTimerStop()
和xTimerStopFromISR()
xTimerStop():
/*******************************************************************************************************
*@ 函数功能:停止一个软件定时器, 让其进入休眠态。
*@ 函数参数:xTimer:软件定时器句柄
xBlockTime:用户指定超时时间, 单位为系统节拍周期(即 tick)。
如果在 FreeRTOS 调度器开启之前调用 xTimerStart(),形参将不起作用。
*@ 返回值:
如果启动命令在超时时间之前无法成功地发送到定时器命令队列则返回 pdFAILE,成功发送则返回 pdPASS。
软件定时器成功发送的命令是否真正的被执行也还要看定时器守护任务的优先级,
其优先级由宏 configTIMER_TASK_PRIORITY 定义。
*******************************************************************************************************/
BaseType_t xTimerStop( TimerHandle_t xTimer, TickType_t xBlockTime );
xTimerStopFromISR():
/*******************************************************************************************************
*@ 函数功能:在中断中停止一个软件定时器, 让其进入休眠态。
*@ 函数参数:xTimer:软件定时器句柄
pxHigherPriorityTaskWoken:定时器守护任务的大部分时间都在阻塞态等待定时器命令队列的命令。
调用函数 xTimerStopFromISR()将会往定时器的命令队列发送一个停止命令,
这很有可能会将定时器任务从阻塞态移除 。
如果调用函数xTimerStopFromISR()让定时器任务脱离阻塞态,
且定时器守护任务的优先级大于或者等于当前被中断的任务的优先级,
那么 pxHigherPriorityTaskWoken 的值会在函数xTimerStopFromISR()内部设置为 pdTRUE,
然后在中断退出之前执行一次上下文切换。
*@ 返回值:
如果停止命令在超时时间之前无法成功地发送到定时器命令队列则返回pdFAILE,成功发送则返回 pdPASS。
软件定时器成功发送的命令是否真正的被执行也还要看定时器守护任务的优先级,
其优先级由宏 configTIMER_TASK_PRIORITY 定义。
*******************************************************************************************************/
BaseType_t xTimerStopFromISR(TimerHandle_t xTimer,BaseType_t *pxHigherPriorityTaskWoken);
xTimerDelete()
/*******************************************************************************************************
*@ 函数功能:删除一个已经被创建成功的软件定时器
*@ 函数参数:xTimer:软件定时器句柄
xBlockTime:用户指定的超时时间, 单位为系统节拍周期(即 tick),
如果在 FreeRTOS调度器开启之前调用 xTimerStart(), 该形参将不起作用。
*@ 返回值:如果删除命令在超时时间之前无法成功地发送到定时器命令队列则返回 pdFAILE, 成功发送则返回 pdPASS。
*******************************************************************************************************/
#define xTimerDelete( xTimer, xTicksToWait ) xTimerGenericCommand( ( xTimer ),tmrCOMMAND_DELETE,0U, NULL, ( xTicksToWait ) )
三、测试Demo
3.1 简单测试
在CubeMX中定时器配置:
添加软件定时器:
程序设计:
- 生成的代码中需要定义一下 ID(上图的Parameter是NULL的,后来我改成了no1 和 no2,需要对no1 和 no2定义一下);
- 不能在调度前使用
xTimerStart()
函数,所以单独创建了一个任务启动定时器,然后删除; - 回调函数中发送任务通知给温湿度读取任务;
- 温湿度读取任务接收任务通知执行任务;
/*1.定义一下 ID*/
#define no1 1
#define no2 2
..
/* Create the timer(s) */
/* definition and creation of myTimer01 */
osTimerDef(myTimer01, myTimeCallback);
myTimer01Handle = osTimerCreate(osTimer(myTimer01), osTimerOnce, (void*) no1);
/* definition and creation of myTimer02 */
osTimerDef(myTimer02, myTimeCallback);
myTimer02Handle = osTimerCreate(osTimer(myTimer02), osTimerPeriodic, (void*) no2);
...
/*2.创建了一个任务启动定时器*/
/* USER CODE END Header_StartTimerTask */
void StartTimerTask(void const * argument)
{
/* USER CODE BEGIN StartTimerTask */
/* Infinite loop */
for(;;)
{
osTimerStart(myTimer01Handle,5000);
osTimerStart(myTimer02Handle,3000);
vTaskDelete(NULL);
osDelay(1);
}
/* USER CODE END StartTimerTask */
}
...
/*
myTimeCallback function
3.回调函数中发送任务通知给温湿度读取任务
测试用,实际使用不能加printf在回调函数
*/
void myTimeCallback(void const * argument)
{
/* USER CODE BEGIN myTimeCallback */
uint8_t myTimerID;
myTimerID = (uint8_t)pvTimerGetTimerID(argument);
if (myTimerID == no1)
{
osSignalSet(THreadHandle,test_signal1);
printf("ulTimerID = %d,send a Signal_1 to threadtask!\r\n",myTimerID);
}
else if(myTimerID == no2){
osSignalSet(THreadHandle,test_signal2);
printf("ulTimerID = %d,send a Signal_2 to threadtask!\r\n",myTimerID);
}
/* USER CODE END myTimeCallback */
}
/*4、温湿度读取函数,还是老样子,和第FreeRTOS记录(5、任务通知)中的函数一样*/
...
3.1.1 再遇溢出问题
本来是个简单的测试,测试结束完结散花,没想到又遇到了溢出问题:
遇到这个问题那可就不能不管了,为了项目中能够更加合理的分配RAM空间,问题必须深究到底!!!
问题一点一点剥开来测试!!!
我们看一下溢出情况下 定时器控制任务的 情况:
在CubeMX中能看到2个定时器需要的内存大小(目前的设置的configTIMER_TASK_STACK_DEPTH 的大小为 128 字,512Bytes)这样子看的话足够用的啊?:
接下来测试,把 configTIMER_TASK_STACK_DEPTH 改成 256 字后,运行起来是正常的:
此时再来看一下任务栈剩余情况(开始剩余2字,多了128字,剩余130,这点OK!):
那我始终觉得128字,应该是足够了,我把 configTIMER_TASK_STACK_DEPTH 改回128字,把myTimeCallback
函数中的 printf 语句去掉,因为正常使用肯定不能带的(快进快出):
测试是能够正常工作的(128字去掉printf的情况下)看任务栈剩余结果:
还有测试Demo中我们创建了2个定时器,定时器创建完了就占用了内存空间,其中有一个单次的定时器,虽然只运行一次,但是他只是出于休眠状态,随时可以启动运行(虽然每次都是运行一次),如果我们只想他开机运行一次,那么我们可以在回调函数中删除此定时器,那么其所占用的内存就会释放:
测试结果:
通过上面我们也可以算出,定时器1占用了 78-26=52 字的空间。
3.1.2 定时器数量问题
可创建的软件定时器的数量是由什么决定的呢?
这个问题推测是和定义的队列长度有关,因为一个队列可以控制一个定时器。
我们做如下测试,把队列的长度,configTIMER_QUEUE_LENGTH 改为1,然后再运行上述代码:
测试结果如下(下图中红色部分的疑问暂时未解决):
上面测试看上去好像是 configTIMER_QUEUE_LENGTH 为1的话只能有一个任务了,为了确定确实是这样,继续测试,configTIMER_QUEUE_LENGTH 改为2,添加第三个任务:
在回调函数中测试一下(下图定时器3的回调函数,printf忘了在后面跟打印的变量了,这里不影响结果,后面的测试我添加上去了):
测试结果如下:
那最后的测试就是,把 configTIMER_QUEUE_LENGTH 改成3,不出意外,3个定时器就能正常运行了:
测试结果如下:
问题:为什么3个定时器开启了,和2个定时器占用的栈空间一样?(未解决)