FreeRTOS学习笔记(十二)
在STM32F407ZGT6中使用软件定时器
前言
之前我们提到了FreeRTOS的核心工作机制是围绕任务调度和系统时钟节拍展开的。现在我们开始围绕系统时钟节拍方面的内容开始阐述。
这篇内容我们主要阐述相对延时和绝对延时函数的区别并介绍软件定时器的概念。最后举一个使用软件定时器的例子,来看看在程序中如何实现软件定时功能。
-
如何在FreeRTOS中实现时间管理?
FreeRTOS中的时间管理主要依赖于时钟滴答和软件定时器。
时钟滴答是定期发生的事件,用于任务调度和时间函数的执行。系统时钟滴答率可以通过configTICK_RATE_HZ配置,它定义了每秒钟发生的滴答次数。
软件定时器则用于实现定时任务,可以通过xTimerCreate()创建,并设置到期时间和回调函数。当定时器到期时,回调函数将被执行。
-
FreeRTOS的延时函数有哪些?如何使用?
FreeRTOS提供了两种延时函数,相对延时函数vTaskDelay()和绝对延时函数vTaskDelayUntil()
这两种延时函数都会使调用他们的任务进入阻塞态,待延时结束后再恢复到就绪态,是高优先级任务主动让出CPU使用权的一种有效方法。
相对延时函数vTaskDelay()
函数 vTaskDelay()用于对任务进行延时,延时的时间单位为系统时钟节拍,如需使用子函 数,需要的 FreeRTOSConfig.h 文件中将配置项 INCLUDE_vTaskDelay 配置为 1。
此函数在 task.c 文件中有定义,具体的代码如下所示:
c#if ( INCLUDE_vTaskDelay == 1 ) void vTaskDelay( const TickType_t xTicksToDelay ) { BaseType_t xAlreadyYielded = pdFALSE; /* 只有在延时时间大于 0 的时候, * 才需要进行任务阻塞, * 否则相当于强制进行任务切换,而不阻塞任务 */ if( xTicksToDelay > ( TickType_t ) 0U ) { configASSERT( uxSchedulerSuspended == 0 ); vTaskSuspendAll(); { /* 挂起任务调度器 */ traceTASK_DELAY(); /* 将任务添加到阻塞态任务列表中 */ prvAddCurrentTaskToDelayedList( xTicksToDelay, pdFALSE ); } /* 恢复任务调度器运行, * 调用此函数会返回是否需要进行任务切换 */ xAlreadyYielded = xTaskResumeAll(); } else { mtCOVERAGE_TEST_MARKER(); } /* 根据标志进行任务切换 */ if( xAlreadyYielded == pdFALSE ) { portYIELD_WITHIN_API(); } else { mtCOVERAGE_TEST_MARKER(); } } #endif /* INCLUDE_vTaskDelay */形参xTicksToDelay为要延时的系统时钟节拍数,范围为1~portMAX_DELAY。对于STM32,portMAX_DELAY为0xFFFFFFFF。函数没有返回值。
在程序设计中,有时使用系统时钟数不够直观方便。FreeRTOS提供了将延时时间转换为系统时钟节拍的宏pdMS_TO_TICKS(),可以方便将要延时的以ms为单位的时间转换为系统时钟节拍。
绝对延时函数vTaskDelayUntil()
绝对延时是指每隔指定的系统时钟节拍运行一次调用vTaskDelayUntil()函数的任务。也就是说,任务以固定的频率运行。绝对延时函数的原型如下:
cvoid xTaskDelayUntil( TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement );形参 说明 pxPreviousWakeTime 指向用于保存上次任务退出阻塞态时的变量 xTimeIncrement 周期性的延时值 函数没有返回值。
与相对延时函数xTaskDelay()不同,绝对延时函数vTaskDelayUntil()增加了一个参数pxPreviousWakeTime,用于指向一个变量,该变量保存上次任务退出阻塞态的时间。这个变量在任务开始时必须被设置成当前系统时钟节拍值,在这之后vTaskDelayUntil()函数在内部会自动更新这个变量。
-
如何在FreeRTOS中使用定时器(Timer)?与任务延时的区别是什么?
软件定时器是FreeRTOS的一个可选功能,基于系统时钟节拍实现。它的实现不需要使用任何硬件定时器资源,并且不受数量限制,只需要系统资源许可。
在 FreeRTOS 中,软件定时器和任务延时(如
vTaskDelay)虽然都能实现"等待一段时间"的效果,但它们的设计机制和适用场景有着本质区别。简单来说:任务延时是"让任务自己暂停",而软件定时器是"让系统在指定时间通知你"。维度 任务延时 ( vTaskDelay)软件定时器 (Software Timer) 执行上下文 在用户任务的上下文中执行 在定时器服务任务(守护任务)的上下文中执行 资源开销 较高,每个定时任务都需要独立的栈空间和TCB 较低,多个定时器共享一个服务任务,无需为每个定时器创建任务 阻塞行为 可以安全地执行阻塞操作(如等待队列、信号量) 严禁阻塞,回调函数必须快速执行完毕 精度与抖动 相对精确,受任务优先级和调度影响 精度较低,易受服务任务优先级和系统负载影响,可能出现抖动 适用场景 执行周期性、有复杂逻辑或需要阻塞的任务 执行轻量级、快速的通知、超时检测或事件触发。 FreeRTOS 提供了许多软件定时器相关的 API 函数,这些 API 函数,大部分都是往定时器 的队列中写入消息(发送命令),这个队列叫做软件定时器命令队列,是提供给 FreeRTOS 中的软件定时器使用的,用户是不能直接访问的。
软件定时器命令队列的操作过程如下图所示:

上图中,左侧的代码为应用程序中用户任务的代码,而右侧的代码为软件定时器服务任务的代码。当用户任务需要操作软件定时器时,就需要调用软件定时器相关的 API 函数。
例如,图中调用了函数vTaskStart()启动软件定时器的定时,而函数vTaskStart()实际上会往软件定时器命令队列写入一条消息(发送消息),这条消息就包含了待操作的定时器对象以及操作的命令(启动软件定时器),软件定时器服务任务就会去读取软件定时器命令队列中的消息(接收命令),并处理这些消息(处理命令)。
FreeRTOS 提供了软件定时器的一些相关操作函数,其中常用的软件定时器相关 API 函数, 如下表所示:
函数 描述 xTimerCreate() 动态方式创建软件定时器 xTimerCreateStatic() 静态方式创建软件定时器 xTimerStart() 开启软件定时器定时 xTimerStartFromISR() 在中断中开启软件定时器定时 xTimerStop() 停止软件定时器定时 xTimerStopFromISR() 在中断中停止软件定时器定时 xTimerReset() 复位软件定时器定时 xTimerResetFromISR() 在中断中复位软件定时器定时 xTimerChangePeriod() 更改软件定时器的定时超时时间 xTimerChangePeriodFromISR() 在中断中更改软件定时器的定时超时时间 xTimerDelete() 删除软件定时器 1.创建软件定时器
FreeRTOS 提供了两种创建软件定时器的方式,分别为动态方式创建软件定时器和静态方式 创建软件定时器。
xTimerCreate()函数:动态方式创建软件定时器
函数原型如下所示:
cTimerHandle_t xTimerCreate(const char * const pcTimerName, const TickType_t xTimerPeriodInTicks, const UBaseType_t uxAutoReload, void * const pvTimerID, TimerCallbackFunction_t pxCallbackFunction);参数说明如下:
形参 描述 pcTimerName 软件定时器名 xTimerPeriodInTicks 定时超时时间,单位:系统时钟节拍 uxAutoReload 定时器模式,pdTRUE:周期定时器,pdFALSE:单次定时器 pvTimerID 软件定时器 ID,用于多个软件定时器公用一个超时回调函数 pxCallbackFunction 软件定时器超时回调函数 返回值:创建成功返回所创建的软件定时器句柄,创建失败返回NULL。
xTimerCreateStatic()函数:静态方式创建软件定时器
函数原型如下所示:
cTimerHandle_t xTimerCreateStatic(const char * const pcTimerName, const TickType_t xTimerPeriodInTicks, const UBaseType_t uxAutoReload, void * const pvTimerID, TimerCallbackFunction_t pxCallbackFunction, StaticTimer_t * pxTimerBuffer);参数说明如下:
形参 描述 pcTimerName 软件定时器名 xTimerPeriodInTicks 定时超时时间,单位:系统时钟节拍 uxAutoReload 定时器模式,pdTRUE:周期定时器,pdFALSE:单次定时器 pvTimerID 软件定时器 ID,用于多个软件定时器公用一个超时回调函数 pxCallbackFunction 软件定时器超时回调函数 pxTimerBuffer 创建软件定时器所需的内存空间 返回值:创建成功返回所创建的软件定时器句柄,创建失败返回NULL。
2.启动软件定时器
FreeRTOS 提供了两个用于开启软件定时器定时的 API 函数,这个两个函数分别用于在任 务和在中断中开启软件定时器定时。
xTimerStart()函数:开启软件定时器定时
函数原型如下所示:
cBaseType_t xTimerStart( TimerHandle_t xTimer, const TickType_t xTicksToWait);参数说明如下:
形参 描述 xTimer 待开启的软件定时器的句柄 xTicksToWait 发送命令到软件定时器命令队列的最大等待时间 返回值:软件定时器启动成功返回pdPASS,启动失败则返回pdFAIL。
xTimerStartFromISR()函数:在中断中开启软件定时器定时
函数原型如下所示:
cBaseType_t xTimerStartFromISR( TimerHandle_t xTimer, BaseType_t * const pxHigherPriorityTaskWoken);参数说明如下:
形参 描述 xTimer 待开启的软件定时器的句柄 pxHigherPriorityTaskWoken 用于标记函数退出后是否需要进行任务切换 返回值:软件定时器启动成功返回pdPASS,启动失败则返回pdFAIL。
3.停止软件定时器
FreeRTOS 提供了两个用于停止软件定时器定时的 API 函数,这个两个函数分别用于在任 务和在中断中停止软件定时器定时。
xTimerStop()函数:停止软件定时器定时
函数原型如下所示:
cBaseType_t xTimerStop( TimerHandle_t xTimer, const TickType_t xTicksToWait);参数说明如下:
形参 描述 xTimer 待停止的软件定时器的句柄 xTicksToWait 用于标记函数退出后是否需要进行任务切换
返回值:软件定时器停止成功返回pdPASS,停止失败则返回pdFAIL。
xTimerStopFromISR()函数:在中断中停止软件定时器定时
中断函数的原型、参数和返回值可以参考上面的xTimerStartFromISR()函数,这里不再罗列。
接下来,我们设计一个示例来演示在项目中软件定时器是如何实现的。
本示例创建两个软件定时器,一个是单次软件定时器,周期为2000,系统时钟节拍(2s);另一个是周期软件定时器,周期为1000,系统时钟节拍(1s)。通过appStartTask()函数创建两个任务。
单次软件定时器回调函数为singleTimerCallBack(),其功能是对单次软件定时器使用次数进行计数,并将通过信息通过串口进行发送,同时让LED0亮灭状态改变一次。
周期软件定时器回调函数为cycleTimerCallBack(),其功能是对周期软件定时器时间到达次数进行计数,并将通过信息通过串口进行发送,同时让LED1亮灭状态改变一次。
具体代码如下:
c
/*********************************************FreeRTOS*************************************************/
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "semphr.h"
#include "timers.h"
/*FreeRTOS配置*/
/* printTASK 任务 配置
* 包括: 任务句柄 任务优先级 堆栈大小 创建任务
*/
static void printTASK(void *pvParameters); /* 任务函数 */
static TaskHandle_t printTask_Handler = NULL; /* 任务句柄 */
#define printTask_PRIO 3 /* 任务优先级 */
#define printTask_STK_SIZE 128 /* 任务堆栈大小 */
/* keyTASK 任务 配置
* 包括: 任务句柄 任务优先级 堆栈大小 创建任务
*/
static void keyTASK(void *pvParameters); /* 任务函数 */
static TaskHandle_t keyTask_Handler = NULL; /* 任务句柄 */
#define keyTask_PRIO 4 /* 任务优先级 */
#define keyTask_STK_SIZE 128 /* 任务堆栈大小 */
static char pcToPrint[80]; /* 串口打印内容缓冲区*/
xQueueHandle xQueuePrint; /* 用于创建队列及其他函数*/
TimerHandle_t tmrSingleHander; /* 单次软件定时器句柄*/
TimerHandle_t tmrCycleHander; /* 周期软件定时器句柄*/
/******************************************************************************************************/
/**
* @brief appStartTask FreeRTOS入口函数
* @param 无
* @retval 无
*/
void appStartTask(void)
{
/*创建一个长度为2,队列项大小足够容纳待输出字符的队列*/
xQueuePrint = xQueueCreate(2, sizeof(pcToPrint));
/*创建两个软件定时器:一个单次软件定时器,一个周期软件定时器*/
/* 单次定时器 */
tmrSingleHander = xTimerCreate((const char* )"SingleTimer", /* 定时器名 */
(TickType_t )2000, /* 定时器超时时间 */
(UBaseType_t )pdFALSE, /* 单次定时器 */
(void* )1, /* 定时器ID */
(TimerCallbackFunction_t)SingleTimerCallback); /* 定时器回调函数 */
/* 周期定时器 */
tmrCycleHander = xTimerCreate((const char* )"CycleTimer", /* 定时器名 */
(TickType_t )1000, /* 定时器超时时间 */
(UBaseType_t )pdTRUE, /* 周期定时器 */
(void* )2, /* 定时器ID */
(TimerCallbackFunction_t)CycleTimerCallback); /* 定时器回调函数 */
if(xQueuePrint && tmrSingleHander && tmrCycleHander)
{
taskENTER_CRITICAL(); /*进入临界段,关中断*/
xTaskCreate((TaskFunction_t )printTASK, /* 任务函数 */
(const char* )"printTASK", /* 任务名称 */
(uint16_t )printTask_STK_SIZE, /* 任务堆栈大小 */
(void* )NULL, /* 传入给任务函数的参数 */
(UBaseType_t )printTask_PRIO, /* 任务优先级 */
(TaskHandle_t* )&printTask_Handler); /* 任务句柄 */
xTaskCreate((TaskFunction_t )keyTASK, /* 任务函数 */
(const char* )"keyTASK", /* 任务名称 */
(uint16_t )keyTask_STK_SIZE, /* 任务堆栈大小 */
(void* )NULL, /* 传入给任务函数的参数 */
(UBaseType_t )keyTask_PRIO, /* 任务优先级 */
(TaskHandle_t* )&keyTask_Handler); /* 任务句柄 */
taskEXIT_CRITICAL(); /*退出临界段,开中断*/
vTaskStartScheduler(); /*开启任务调度器*/
}
}
/**
* @brief 单次定时器超时回调函数
* @param xTimer : 传入参数(未用到)
* @retval 无
*/
void SingleTimerCallback(TimerHandle_t xTimer)
{
static uint16_t cnt = 0; /*保存软件定时器回调次数*/
xTimer = xTimer;
cnt++;
LED0_TOGGLE();
sprintf(pcToPrint,"单次软件定时器,运行%d次\r\n",cnt);
xQueueSendToBack(xQueuePrint,pcToPrint,0);
}
/**
* @brief 周期定时器超时回调函数
* @param xTimer : 传入参数(未用到)
* @retval 无
*/
void CycleTimerCallback(TimerHandle_t xTimer)
{
static uint16_t cnt = 0; /*保存软件定时器回调次数*/
xTimer = xTimer;
cnt++;
LED1_TOGGLE();
sprintf(pcToPrint,"周期软件定时器,运行%d次\r\n",cnt);
xQueueSendToBack(xQueuePrint,pcToPrint,0);
}
/**
* @brief printTASK
* @param pvParameters : 传入参数
* @retval 无
*/
static void printTASK(void *pvParameters)
{
char pcToWrite[80];
while(1)
{
xQueueReceive(xQueuePrint,pcToWrite,portMAX_DELAY);
printf("%s",pcToWrite);
}
}
/**
* @brief keyTASK
* @param pvParameters : 传入参数
* @retval 无
*/
static void keyTASK(void *pvParameters)
{
uint8_t keyValue; //键值
while(1)
{
keyValue = key_scan(); //获取键值
if(keyValue == KEY0_PRES)
{
if(xTimerIsTimerActive(tmrSingleHander) == pdFALSE)
{
sprintf(pcToPrint,"key0按键按下,启动单次定时器.....\r\n");
xQueueSendToBack(xQueuePrint,pcToPrint,0);
xTimerStart(tmrSingleHander,0); /*启动单次软件定时器*/
}
else
{
sprintf(pcToPrint,"key0按键按下,单次定时器已启动.....\r\n");
xQueueSendToBack(xQueuePrint,pcToPrint,0);
}
}
else if(keyValue == KEY1_PRES)
{
if(xTimerIsTimerActive(tmrCycleHander) == pdFALSE)
{
sprintf(pcToPrint,"key1按键按下,启动周期定时器.....\r\n");
xQueueSendToBack(xQueuePrint,pcToPrint,0);
xTimerStart(tmrCycleHander,0); /*启动周期软件定时器*/
}
else
{
sprintf(pcToPrint,"key1按键按下,周期定时器已启动.....\r\n");
xQueueSendToBack(xQueuePrint,pcToPrint,0);
}
}
else if(keyValue == WKUP_PRES)
{
sprintf(pcToPrint,"WAKEUP按键按下,停止所有定时器.....\r\n");
xQueueSendToBack(xQueuePrint,pcToPrint,0);
xTimerStop(tmrSingleHander,0); /*停止单次软件定时器*/
xTimerStop(tmrCycleHander,0); /*停止周期软件定时器*/
}
vTaskDelay(pdMS_TO_TICKS(100));
}
}
实验结果
