1、准备材料
正点原子stm32f407探索者开发板V2.4
STM32CubeMX软件(Version 6.10.0)
Keil µVision5 IDE(MDK-Arm)
野火DAP仿真器
XCOM V2.6串口助手
2、学习目标
本文主要学习 FreeRTOS 软件定时器的相关知识,包括软件定时器回调函数、属性、状态、运行原理和常见 API 函数等知识
3、前提知识
3.1、软件定时器回调函数
软件定时器的回调函数是一个返回值为 void 类型,并且只有软件定时器句柄一个参数的 C 语言函数,其函数的具体原型如下所述
/**
* @brief 软件定时器回调函数
* @param xTimer:软件定时器句柄
* @retval None
*/
void ATimerCallback(TimerHandle_t xTimer)
{
/* do something */
}
软件定时器回调函数会在定时器设定的时间到期时在 RTOS 守护进程任务中被执行,软件定时器回调函数从头到尾执行,并以正常方式退出
需要读者注意的是软件定时器的回调函数应尽可能简短,并且在该函数体内不能调用任何会使任务进入阻塞状态的 API 函数,但是如果设置调用函数的 xTicksToWait 参数为 0 ,则可以调用如 xQueueReceive() 等 API 函数
3.2、软件定时器属性和状态
3.2.1、周期
这个属性比较好理解,软件定时器的周期指的是 从软件定时器启动到软件定时器回调函数执行之间的时间,该属性是定时器不可或缺的重要属性
3.2.2、分类
软件定时器根据行为的不同分为了 单次定时器(One-shot timers) 和 周期定时器(Auto-reload timers) 两种类型,如下图展示了两种不同类型软件定时器的行为差异 (注释1)
3.2.3、状态
根据定时器是否正在运行可以将定时器分为 运行状态(Running) 和 休眠状态(Dormant) 两种不同状态,如下图所示展示了单次定时器和周期定时器在两种不同状态之间的转换过程
从图上可以看出以下几点内容
- 不管是单次定时器还是周期定时器,在定时器创建成功之后都处于休眠状态,一旦调用启动、复位或改变定时器周期的 API 函数就会使定时器从休眠状态转移到运行状态;
- 单次定时器定时时间到期之后执行一次回调函数就会自动转换为休眠状态,而周期定时器会一直处于运行状态;
- 当对处于运行状态的定时器调用停止 API 函数时,不管是哪种定时器都会转变为休眠状态
定时器的状态可以通过 xTimerlsTimerActive() API 函数查询,该函数具体声明如下所述
/**
* @brief 查询软件定时器是否处于运行或休眠状态
* @param xTimer:要查询的定时器句柄
* @retval 如果定时器处于休眠状态则返回pdFALSE,如果定时器处于运行状态则返回pdTRUE
*/
BaseType_t xTimerIsTimerActive(TimerHandle_t xTimer);
3.3、软件定时器运行原理
3.3.1、RTOS 守护进程任务
首先读者应该知道的一点是所有软件定时器的回调函数都在同一个 RTOS 守护进程任务的上下文中执行,这个 RTOS 守护进程任务和空闲任务一样,在调度器启动的时候会被自动创建, RTOS 守护进程任务的优先级和堆栈大小分别由 configTIMER_TASK_PRIORITY 和 configTIMER_TASK_STACK_DEPTH 两个参数设置(可在 STM32CubeMX 软件中配置)
”3.1、软件定时器回调函数“ 小节提到在回调函数中不能使用会使任务进入阻塞状态的 API 函数,这是因为调用会使任务进入阻塞状态的 API 函数会使 RTOS 守护进程任务进入阻塞状态,这是不被允许的
3.3.2、定时器命令队列
上面提到的软件定时器的启动、复位、改变定时器周期和停止等操作的 API 函数只是将控制定时器的命令从调用任务发送到称为 “定时器命令队列” 的队列上,然后由 RTOS 守护进程任务从定时器命令队列中取出命令对定时器实际操作
定时器命令队列是 FreeRTOS 里的一个标准队列,其也是在调度程序启动时被自动创建的,定时器命令队列的长度可以由 configTIMER_QUEUE LENGTH 参数设置
如下图所示为软件定时器 API 函数使用定时器命令队列与 RTOS 守护程序任务进行通信的示意图
3.3.3、守护进程任务调度
守护进程任务是一个 FreeRTOS 任务,所以其任务调度会遵循和其他任务一样的调度规则,当守护进程任务是能够运行的最高优先级任务时,它将会处理定时器队列中的命令或执行定时器的回调函数
守护进程任务的优先级在 STM32CubeMX 中默认为 2 ,当守护进程任务的优先级低于调用 xTimerStart() 等 API 函数的任务的优先级时,其会在任务结束之后轮到守护进程任务执行时对 “开始定时器” 命令进行处理,具体如下图所示
当守护进程任务的优先级高于调用 xTimerStart() 等 API 函数的任务的优先级时,一旦任务调用 xTimerStart() 等 API 函数将命令写入定时器命令队列,守护进程任务便可以抢占该任务,立即处理写入定时器命令队列的命令,处理完毕之后进入阻塞状态,处理器返回原任务继续执行,具体如下图所示
3.4、创建、启动软件定时器
同样,根据 FreeRTOS API 的惯例,创建软件定时器仍然提供了动态内存创建和静态内存创建两个不同的 API 函数,软件定时器可以在调度程序运行之前创建,也可以在调度程序启动后从任务创建,如下所示为两个 API 函数声明
/**
* @brief 动态分配内存创建软件定时器
* @param pcTimerName:定时器的描述性名称,辅助调试用
* @param xTimerPeriod:定时器的周期,参考 “3.2.1、周期” 小节
* @param uxAutoReload:pdTRUE表示周期软件定时器,pdFASLE表示单次软件定时器
* @param pvTimerID:定时器ID
* @param pxCallbackFunction:定时器回调函数指针,参考 “3.1、软件定时器回调函数” 小节
* @retval 创建成功则返回创建的定时器的句柄,失败则返回NULL
*/
TimerHandle_t xTimerCreate(const char * const pcTimerName,
const TickType_t xTimerPeriod,
const UBaseType_t uxAutoReload,
void * const pvTimerID,
TimerCallbackFunction_t pxCallbackFunction);
/**
* @brief 动态分配内存创建软件定时器
* @param pcTimerName:定时器的描述性名称,辅助调试用
* @param xTimerPeriod:定时器的周期,参考 “3.2.1、周期” 小节
* @param uxAutoReload:pdTRUE表示周期软件定时器,pdFASLE表示单次软件定时器
* @param pvTimerID:定时器ID
* @param pxCallbackFunction:定时器回调函数指针,参考 “3.1、软件定时器回调函数” 小节
* @param pxTimerBuffer:指向StaticTimer_t类型的变量,然后用该变量保存定时器的状态
* @retval 创建成功则返回创建的定时器的句柄,失败则返回NULL
*/
TimerHandle_t xTimerCreateStatic(const char * const pcTimerName,
const TickType_t xTimerPeriod,
const UBaseType_t uxAutoReload,
void * const pvTimerID,
TimerCallbackFunction_t pxCallbackFunction
StaticTimer_t *pxTimerBuffer);
创建完的软件定时器处于休眠状态,需要调用启动定时器或其他 API 函数才会进入运行状态,xTimerStart() 可以在调度程序启动之前调用,但是完成此操作后,软件定时器直到调度程序启动的时间才会真正启动,启动定时器的 API 函数如下所述
/**
* @brief 启动定时器
* @param xTimer:要操作的定时器句柄
* @param xBlockTime:参考 “3.4.1、xTicksToWait 参数” 小节
* @retval 参考 “3.4.2、函数返回值” 小节
*/
BaseType_t xTimerStart(TimerHandle_t xTimer,
TickType_t xTicksToWait);
/**
* @brief 启动定时器的中断安全版本
* @param xTimer:要操作的定时器句柄
* @param pxHigherPriorityTaskWoken:用于通知应用程序编写者是否应该执行上下文切换
* @retval 参考 “3.4.2、函数返回值” 小节
*/
BaseType_t xTimerStartFromISR(TimerHandle_t xTimer,
BaseType_t *pxHigherPriorityTaskWoken);
3.4.1、xTicksToWait 参数
xTimerStart() 使用定时器命令队列向守护进程任务发送 “启动定时器” 命令, xTicksToWait 指定调用任务应保持在阻塞状态以等待定时器命令队列上的空间变得可用的最长时间(如果队列已满),该参数需要注意以下几点
-
如果 xTicksToWait 为零且定时器命令队列已满,xTimerStart() 将立即返回,该参数以滴答定时器时间刻度为单位,可以使用宏 pdMS_TO_TICKS() 将以毫秒为单位的时间转换为以刻度为单位的时间,例如 pdMS_TO_TICKS(50) 表示阻塞 50ms
-
如果在 FreeRTOSConfig.h 中将 INCLUDE_vTaskSuspend 设置为 1,则将 xTicksToWait 设置为 portMAX_DELAY 将导致调用任务无限期地保持在阻塞状态(没有超时),以等待定时器命令队列中的空间变得可用
-
如果在启动调度程序之前调用 xTimerStart(),则 xTicksToWait 的值将被忽略,并且 xTimerStart() 的行为就像 xTicksToWait 已设置为零一样
3.4.2、xTimerStart() 函数返回值
有两种可能的返回值,分别为 pdPASS 和 pdFALSE ,具体如下所述
① 仅当 “启动定时器” 命令成功发送到定时器命令队列时,才会返回 pdPASS
- 如果守护程序任务的优先级高于调用 xTimerStart() 的任务的优先级,则调度程序将确保在 xTimerStart() 返回之前处理启动命令。这是因为一旦定时器命令队列中有数据,守护任务就会抢占调用 xTimerStart() 的任务,从而总是保证将命令成功发送到定时器命令队列
- 如果指定了阻塞时间(xTicksToWait 不为零),则调用任务可能会被置于阻塞状态,以等待定时器命令队列中的空间在函数返回之前变得可用,只要在阻塞时间到期之前命令已成功写入定时器命令队列,就可以返回 pdPASS
② 如果由于队列已满或超过阻塞时间等原因无法将 “启动定时器” 命令写入定时器命令队列,则将返回 pdFALSE
- 如果指定了阻塞时间(xTicksToWait 不为零),则调用任务将被置于阻塞状态以等待守护进程任务在定时器命令队列中腾出空间,但是指定的阻塞时间在等待定时器命令队列中腾出空间之前已过期,所以返回 pdFALSE
3.6、软件定时器 ID
每个软件定时器都有一个 ID ,它是一个标签值,应用程序编写者可以将其用于任何目的, ID 被存储在空指针中,因此可以直接存储整数值,指向任何其他对象,或用作函数指针
创建软件定时器时会为 ID 分配一个初始值,之后可以使用 vTimerSetTimerID() API 函数更新 ID,并且可以使用 pvTimerGetTimerID() API 函数查询 ID ,这两个 API 函数具体如下所示
/**
* @brief 设置定时器ID值
* @param xTimer:要操作的定时器句柄
* @param pvNewID:想要设置软件定时器的新ID值
* @retval None
*/
void vTimerSetTimerID(TimerHandle_t xTimer, void *pvNewID);
/**
* @brief 获取定时器ID值
* @param xTimer:要操作的定时器句柄
* @retval 正在查询的软件定时器ID
*/
void *pvTimerGetTimerID(TimerHandle_t xTimer);
注意:与其他软件定时器 API 函数不同,vTimerSetTimerID() 和 pvTimerGetTimerID() 直接访问软件定时器,它们不向定时器命令队列发送命令
如果创建了多个软件定时器,并且所有软件定时器均使用了同一个回调函数,则可以给软件定时器设置不同的 ID 值,然后在回调函数中通过 ID 值判断软件定时器触发的来源
3.7、改变软件定时器周期
创建软件定时器时就会为定时器周期设置初始值,后续也可以使用 xTimerChangePeriod() 函数动态更改软件定时器的周期,该函数具体声明如下所示
/**
* @brief 改变软件定时器的周期
* @param xTimer:要操作的定时器句柄
* @param xNewPeriod:软件定时器的新周期,以刻度为单位指定
* @param xBlockTime:参考 “3.4.1、xTicksToWait 参数” 小节
* @retval 参考 “3.4.2、xTimerStart() 函数返回值” 小节
*/
BaseType_t xTimerChangePeriod(TimerHandle_t xTimer,
TickType_t xNewPeriod,
TickType_t xBlockTime);
/**
* @brief 改变软件定时器周期的中断安全版本
* @param xTimer:要操作的定时器句柄
* @param xNewPeriod:软件定时器的新周期,以刻度为单位指定
* @param pxHigherPriorityTaskWoken:用于通知应用程序编写者是否应该执行上下文切换
* @retval 参考 “3.4.2、xTimerStart() 函数返回值” 小节
*/
BaseType_t xTimerChangePeriodFromISR(TimerHandle_t xTimer,
TickType_t xNewPeriod,
BaseType_t *pxHigherPriorityTaskWoken);
如果 xTimerChangePeriod() 用于更改已运行的定时器的周期,则定时器将使用新的周期值重新计算其到期时间,重新计算的到期时间是相对于调用 xTimerChangePeriod() 的时间,而不是相对于定时器最初启动的时间
如果使用 xTimerChangePeriod() 更改处于休眠状态(未运行的定时器)的定时器的周期,则定时器将计算到期时间,并转换到运行状态(定时器将开始运行)
另外如果希望查询一个定时器的定时周期,可以通过 xTimerGetPeriod() API 函数查询,具体函数声明如下所述
/**
* @brief 查询一个软件定时器的周期
* @param xTimer:要查询的定时器句柄
* @retval 返回一个软件定时器的周期
*/
TickType_t xTimerGetPeriod(TimerHandle_t xTimer);
3.8、重置软件定时器
重置软件定时器是指重新启动定时器,定时器的到期时间将根据重置定时器的时间重新计算,而不是相对于定时器最初启动的时间,如下图对此进行了演示,其中显示了一个定时器,该定时器启动的周期为 6,然后重置两次,最后到期并执行其回调函数
FreeRTOS中使用 xTimerReset() API 函数重置软件定时器,除此之外还可用于启动处于休眠状态的定时,该函数具体声明如下所述
/**
* @brief 重置软件定时器
* @param xTimer:要操作的定时器句柄
* @param xBlockTime:参考 “3.4.1、xTicksToWait 参数” 小节
* @retval 参考 “3.4.2、xTimerStart() 函数返回值” 小节
*/
BaseType_t xTimerReset(TimerHandle_t xTimer,
TickType_t xBlockTime);
/**
* @brief 重置软件定时器的中断安全版本
* @param xTimer:要操作的定时器句柄
* @param pxHigherPriorityTaskWoken:用于通知应用程序编写者是否应该执行上下文切换
* @retval 参考 “3.4.2、xTimerStart() 函数返回值” 小节
*/
BaseType_t xTimerResetFromISR(TimerHandle_t xTimer,
BaseType_t *pxHigherPriorityTaskWoken);
3.8、停止、删除软件定时器
/**
* @brief 停止软件定时器
* @param xTimer:要操作的定时器句柄
* @param xBlockTime:参考 “3.4.1、xTicksToWait 参数” 小节
* @retval 参考 “3.4.2、xTimerStart() 函数返回值” 小节
*/
BaseType_t xTimerStop(TimerHandle_t xTimer,
TickType_t xBlockTime);
/**
* @brief 删除软件定时器
* @param xTimer:要操作的定时器句柄
* @param xBlockTime:参考 “3.4.1、xTicksToWait 参数” 小节
* @retval 参考 “3.4.2、xTimerStart() 函数返回值” 小节
*/
BaseType_t xTimerDelete(TimerHandle_t xTimer,
TickType_t xBlockTime);
/**
* @brief 停止软件定时器的中断安全版本
* @param xTimer:要操作的定时器句柄
* @param pxHigherPriorityTaskWoken:用于通知应用程序编写者是否应该执行上下文切换
* @retval 参考 “3.4.2、xTimerStart() 函数返回值” 小节
*/
BaseType_t xTimerStopFromISR(TimerHandle_t xTimer,
BaseType_t *pxHigherPriorityTaskWoken);
3.9、其他 API 函数
/**
* @brief 将软件定时器的“模式”更新为 自动重新加载定时器 或 一次性定时器
* @param xTimer:要操作的定时器句柄
* @param uxAutoReload:设置为pdTRUE则将定时器设置为周期软件定时器,设置为pdFASLE则将定时器设置为单次软件定时器
* @retval None
*/
void vTimerSetReloadMode(TimerHandle_t xTimer,
const UBaseType_t uxAutoReload);
/**
* @brief 查询软件定时器是 单次定时器 还是 周期定时器
* @param xTimer:要查询的定时器句柄
* @retval 如果为周期软件定时器则返回pdTRUE,否则返回pdFALSE
*/
BaseType_t xTimerGetReloadMode(TimerHandle_t xTimer);
/**
* @brief 查询软件定时器到期的时间
* @param xTimer:要查询的定时器句柄
* @retval 如果要查询的定时器处于活动状态则返回定时器下一次到期的时间,否则未定义返回值
*/
TickType_t xTimerGetExpiryTime(TimerHandle_t xTimer);
4、实验一:软件定时器的应用
4.1、实验目标
- 创建一个周期软件定时器 TimerPeriodic 和一个单次软件定时器 TimerOnce
- 创建一个按键扫描任务 Task_KeyScan,根据不同按键实现不同响应
- 当按键 WK_UP 按下时,设置周期定时器以 500ms 周期执行;当按键 KEY2 按下时,设置单次定时器以 1s 周期执行一次;当按键 KEY1 按下时,对周期定时器进行复位操作;当按键 KEY0 按下时,停止 TimerPeriodic 周期定时器
4.2、CubeMX相关配置
首先读者应按照 "FreeRTOS教程1 基础知识" 章节配置一个可以正常编译通过的 FreeRTOS 空工程,然后在此空工程的基础上增加本实验所提出的要求
本实验需要初始化 USART1 作为输出信息渠道,具体配置步骤请阅读“STM32CubeMX教程9 USART/UART 异步通信”,如下图所示
本实验需要初始化开发板上 WK_UP、KEY2、KEY1 和 KEY0 用户按键做普通输入,具体配置步骤请阅读“STM32CubeMX教程3 GPIO输入 - 按键响应”,注意虽开发板不同但配置原理一致,如下图所示
单击 Middleware and Software Packs/FREERTOS ,在 Configuration 中单击 Tasks and Queues 选项卡,双击默认任务修改其参数,如下所示
单击 Timers and Semaphores ,在 Timers 中创建周期、单次两个软件定时器,如下所示
配置 Clock Configuration 和 Project Manager 两个页面,接下来直接单击 GENERATE CODE 按钮生成工程代码即可
4.3、添加其他必要代码
按照 “STM32CubeMX教程9 USART/UART 异步通信” 实验 “6、串口printf重定向” 小节增加串口 printf 重定向代码,具体不再赘述
首先应该在 freertos.c 中添加软件定时器的头文件和使用到的 printf 的头文件,如下所述
#include "timers.h"
#include "stdio.h"
然后实现按键扫描任务函数体,当按键 WK_UP 按下时启动周期软件定时器,当按键 KEY2 按下时启动单次软件定时器,当按键 KEY1 按下时对周期软件定时器进行复位操作,当按键 KEY0 按下时停止周期定时器,具体如下所述
void AppTask_KeyScan(void *argument)
{
/* USER CODE BEGIN AppTask_KeyScan */
uint8_t key_value = 0;
/* Infinite loop */
for(;;)
{
key_value = 0;
//按键WK_UP按下
if(HAL_GPIO_ReadPin(WK_UP_GPIO_Port,WK_UP_Pin) == GPIO_PIN_SET)
key_value = 4;
//按键KEY2按下
if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin) == GPIO_PIN_RESET)
key_value = 3;
//按键KEY1按下
if(HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin) == GPIO_PIN_RESET)
key_value = 2;
//按键KEY0按下
if(HAL_GPIO_ReadPin(KEY0_GPIO_Port,KEY0_Pin) == GPIO_PIN_RESET)
key_value = 1;
if(key_value != 0)
{
if(key_value == 4)
{
if(xTimerChangePeriod(TimerPeriodicHandle, 500, pdMS_TO_TICKS(500)) == pdTRUE)
{
printf("\r\nWK_UP PRESSED, TimerPeriodic Start!\r\n\r\n");
}
}
if(key_value == 3)
{
if(xTimerChangePeriod(TimerOnceHandle, 1000, pdMS_TO_TICKS(500)) == pdTRUE)
{
printf("\r\nKEY2 PRESSED, TimerOnce Start!\r\n\r\n");
}
}
else if(key_value == 2)
{
if(xTimerReset(TimerPeriodicHandle, pdMS_TO_TICKS(500)) == pdTRUE)
{
printf("\r\nKEY1 PRESSED, TimerPeriodic Reset!\r\n\r\n");
}
}
else if(key_value == 1)
{
if(xTimerStop(TimerPeriodicHandle, pdMS_TO_TICKS(500)) == pdTRUE)
{
printf("\r\nKEY0 PRESSED, TimerPeriod Stop!\r\n\r\n");
}
}
//有按键按下就进行按键消抖
osDelay(300);
}
else
osDelay(10);
}
/* USER CODE END AppTask_KeyScan */
}
最后实现单次/周期软件定时器的两个回调函数即可,回调函数内不做任何具体操作,仅通过串口输出提示信息,如下所述
/* appTimerPeriodic function */
void appTimerPeriodic(void *argument)
{
/* USER CODE BEGIN appTimerPeriodic */
printf("Into appTimerPeriodic Function\r\n");
/* USER CODE END appTimerPeriodic */
}
/* appTimerOnce function */
void appTimerOnce(void *argument)
{
/* USER CODE BEGIN appTimerOnce */
printf("Into appTimerOnce Function\r\n");
/* USER CODE END appTimerOnce */
}
4.4、烧录验证
烧录程序,打开串口助手后无任何信息输出,当按下开发板上的 WK_UP 按键之后,会启动以 500ms 为周期的周期软件定时器,此时周期软件定时器的回调函数会周期得到执行;当按下开发板上的 KEY2 按键之后,会启动 1s 为周期的单次软件定时器,此时单次软件定时器的回调函数会得到执行,并且只执行了一次就停止了执行;当按下开发板上的 KEY1 按键时,会复位周期定时器;当按下开发板上的 KEY0 按键时,会停止周期定时器,整个过程串口的输出信息如下图所示
5、注释详解
注释1:图片来源于 Mastering_the_FreeRTOS_Real_Time_Kernel-A_Hands-On_Tutorial_Guide.pdf
参考资料
STM32Cube高效开发教程(基础篇)
Mastering_the_FreeRTOS_Real_Time_Kernel-A_Hands-On_Tutorial_Guide.pdf
评论区