写点什么

FreeRTOS 记录(八、用软件定时器?还是硬件定时器?)

作者:矜辰所致
  • 2022 年 9 月 15 日
    江苏
  • 本文字数:6829 字

    阅读完需:约 22 分钟

FreeRTOS记录(八、用软件定时器?还是硬件定时器?)
FreeRTOS软件定时器,相对前面的内容来说,软件定时器还是比较简单的,我们简单测试一下因为是简单介绍,所以原理和源码的分析不会那么详细,具体可以根据文中API查看源码使用起来记住创建,启动,回调函数核心步骤即可
复制代码


说明:FreeRTOS 专栏与我的 RT-Thread 专栏不同,我的 RT-Thread 专栏是从理论学习一步一步循序渐进,从 0 起步的 完整教学,而 FreeRTOS 更偏向于 我直接拿来使用,需要用到什么,然后引出知识点,在使用中发现问题,解然后再解决问题。


本 FreeRTOS 专栏记录的开发环境:

FreeRTOS 记录(一、熟悉开发环境以及 CubeMX 下 FreeRTOS 配置)

https://xie.infoq.cn/article/e82c7e10af242105b509bee2e

FreeRTOS 记录(二、FreeRTOS 任务 API 认识和源码简析)

https://xie.infoq.cn/article/250c67b1cc68b91241347f8e6

FreeRTOS 记录(三、RTOS 任务调度原理解析 _Systick、PendSV、SVC)

https://xie.infoq.cn/article/e0c671e5ca2d472ce40d272d4

FreeRTOS 记录(四、FreeRTOS 任务堆栈溢出问题和临界区)

https://xie.infoq.cn/article/d3f7d46aa262b2118a544f250

FreeRTOS 记录(五、FreeRTOS 任务通知)

https://xie.infoq.cn/article/bd3a7f06b5148b665893b6ac6

FreeRTOS 记录(六、FreeRTOS 消息队列—Enocean 模块串口通讯、RAM 空间不足问题分析))

https://xie.infoq.cn/article/273f300ca626097eb6b44d6cb

FreeRTOS 记录(七、FreeRTOS 信号量、事件标志组、邮箱和消息队列、任务通知的关系)

https://xie.infoq.cn/article/cb0c88aa2077212312cc552cc


问:什么时候使用软件定时器,什么时候使用硬件定时器?

软件定时器可以解决硬件定时器数量不够的问题,理论上软件定时器可以很多,每个芯片的定时器外设是有限的,如果硬件定时器不够用,可以使用软件定时器。


但是,软件定时器相对硬件定时器来说,精度没有那么高(为什么不高?因为它以系统时钟为基准,系统时钟中断优先级又是最低,容易被打断)。 对于需要高精度要求的场合,不建议使用软件定时器。


同时软件定时器 是需要占用一部分内存空间的,用到的软件定时器数量越多,内存占用越大。如果 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 中定时器配置:



添加软件定时器:



程序设计:


  1. 生成的代码中需要定义一下 ID(上图的 Parameter 是 NULL 的,后来我改成了 no1 和 no2,需要对 no1 和 no2 定义一下);

  2. 不能在调度前使用xTimerStart() 函数,所以单独创建了一个任务启动定时器,然后删除;

  3. 回调函数中发送任务通知给温湿度读取任务;

  4. 温湿度读取任务接收任务通知执行任务;


/*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,然后再运行上述代码:



测试结果如下<font color=#FF0033>(下图中红色部分的疑问暂时未解决):



上面测试看上去好像是 configTIMER_QUEUE_LENGTH 为 1 的话只能有一个任务了,为了确定确实是这样,继续测试,configTIMER_QUEUE_LENGTH 改为 2,添加第三个任务:




在回调函数中测试一下(下图定时器 3 的回调函数,printf 忘了在后面跟打印的变量了,这里不影响结果,后面的测试我添加上去了):



测试结果如下:



那最后的测试就是,把 configTIMER_QUEUE_LENGTH 改成 3,不出意外,3 个定时器就能正常运行了:



测试结果如下:



留一个问题:为什么 3 个定时器开启了,和 2 个定时器占用的栈空间一样?

发布于: 刚刚阅读数: 3
用户头像

矜辰所致

关注

CSDN、知乎、微信公众号: 矜辰所致 2022.08.02 加入

不浮夸,不将就,认真对待学知识的我们,矜辰所致,金石为开! 为了活下去的嵌入式工程师,画画板子,敲敲代码,玩玩RTOS,搞搞Linux ...

评论

发布
暂无评论
FreeRTOS记录(八、用软件定时器?还是硬件定时器?)_软件定时器_矜辰所致_InfoQ写作社区