深入浅出解析 FreeRTOS 软件定时器 定时器服务任务:机制、API 详解及实践应用
1. FreeRTOS 软件定时器
1.1 简介
FreeRTOS 软件定时器是基于任务调度器实现的一种时间管理工具,可用于定时执行回调函数。它不依赖硬件资源,灵活性高,但受调度影响,精度低于硬件定时器
软件定时器与硬件定时器的主要区别如下:
软件定时器 | 硬件定时器 |
---|---|
依赖 FreeRTOS 任务调度器 | 由 MCU 提供,独立运行 |
精度受任务调度影响 | 具有更高的精度 |
不占用硬件资源,可能增加 CPU 负载 | 需占用硬件资源,不增加 CPU 负载 |
1.2 单次定时器和周期定时器
在 FreeRTOS 中,软件定时器主要有两种类型:一次性定时器和周期性定时器。
- 一次性定时器(One-shot Timer): 这种定时器在触发一次超时后就会停止,不再执行。适用于只需在特定时间执行一次任务或动作的场景(xAutoReload为pdFALSE 0)。
- 周期性定时器(Periodic Timer): 这种定时器会在每个超时周期都触发一次,循环执行。适用于需要在固定的时间间隔内重复执行任务或动作的场景(xAutoReload为pdTRUE 1)。
1.3 必要宏定义
要使用软件定时器,必须 使能软件定时器功能 并 配置相关参数,FreeRTOSConfig.h
文件中相关配置,如下:
/* 使能软件定时器相关 */
#define configUSE_TIMERS 1 /* 1: 启用 FreeRTOS 软件定时器功能 */
#define configTIMER_TASK_PRIORITY (configMAX_PRIORITIES - 1) /* 软件定时器任务的优先级 */
#define configTIMER_QUEUE_LENGTH 5 /* 软件定时器队列长度(存放定时器事件的队列深度) */
#define configTIMER_TASK_STACK_DEPTH (configMINIMAL_STACK_SIZE * 2) /* 定时器任务的栈大小 */
详细介绍:
configTIMER_TASK_PRIORITY
该任务的优先级越高,软件定时器的回调执行越及时,但过高的优先级可能影响其他任务的执行
configTIMER_QUEUE_LENGTH
由于其他优先级任务会可能会抢占软件定时器服务任务的执行,所以定义软件定时器服务队列来存储待执行的命令。软件定时器命令队列的最大长度(存放定时器操作请求,如xTimerStart()
、xTimerStop()
等)
configTIMER_TASK_STACK_DEPTH
该任务负责 检测定时器超时,并执行定时器回调函数,需要一定的栈空间
1.4 软件定时器与定时器服务任务的关系
在学习 FreeRTOS 的软件定时器时,容易混淆 “定时器” 和 “定时器服务任务” 之间的关系。实际上,FreeRTOS 采用 单个定时器服务任务 来统一管理 所有的软件定时器。所有 xTimerCreate()
创建的软件定时器都由一个单独的定时器服务任务管理,定时器本身不是一个独立的任务。
FreeRTOS 定时器管理机制:软件定时器(xTimerCreate()
)只是一个数据结构,存储了定时器的周期、回调函数等信息,并不是真正的任务,需要由定时器服务任务来管理和执行。
1.5 相关 API 函数
API 函数 | 描述 |
---|---|
xTimerCreate() | 动态创建一个新的软件定时器对象 |
xTimerCreateStatic() | 静态创建一个新的软件定时器对象 |
xTimerStart() | 启动定时器(从零开始计时) |
xTimerStop() | 停止定时器(不会自动重新开始) |
xTimerChangePeriod() | 修改定时器的周期 |
xTimerReset() | 重置定时器(重新开始计时) |
xTimerDelete() | 删除定时器 |
xTimerIsTimerActive() | 判断定时器是否在运行 |
pvTimerGetTimerID() | 获取定时器的 ID |
xTimerPendFunctionCall() | 将一个函数发送到定时器服务任务的执行队列中,执行一次 |
这些 API 函数中的
xTicksToWait
参数含义为:调用任务在定时器服务任务队列已满的情况下,为等待队列空间空出而保持阻塞状态的时间量。
1.6 实验
- start_task:用来创建 task1 任务,并创建周期性定时器。
- 定时器1:创建 scan_keypad 函数,软件定时器周期回调执行,用来扫描键盘进行消抖。
- 定时器2:创建 timer2_callback 函数,回调执行周期为 1 秒。
- task1:用于按键状态判断,对软件定时器2进行开启、停止、修改回调周期操作;将一个函数加入定时器服务任务,执行一次。
1 ) 启动任务配置
/* 启动任务配置 */
#define TASK_START_STACK_SIZE 128
#define TASK_START_PRIORITY 1
TaskHandle_t Task_start_Handle; // 如果后续不需要操作这个任务(删除,挂起等),可以省略这句
StackType_t start_task_stack[TASK_START_STACK_SIZE];
StaticTask_t start_task_tcb;
void task_start(void *pvParameters);
/* 任务1配置 */
#define TASK1_STACK_SIZE 128
#define TASK1_PRIORITY 2
TaskHandle_t Task1_Handler;
void task1(void *pvParameters);
/* 软件定时器配置 */
#define TIMER_TASK_STAK_SIZE configTIMER_TASK_STACK_DEPTH
StaticTask_t timer_task_tcb;
StackType_t timer_task_stack[TIMER_TASK_STAK_SIZE];
TimerHandle_t timer1, timer2;
void FreeRTOS_start()
{
Task_start_Handle = xTaskCreateStatic((TaskFunction_t)task_start, // 任务函数指针
(char *)"task_start", // 任务名称
(configSTACK_DEPTH_TYPE)TASK_START_STACK_SIZE, // 任务栈大小
(void *)NULL, // 传递给任务函数的参数
(UBaseType_t)TASK_START_PRIORITY, // 任务优先级
(StackType_t *)start_task_stack, // 任务栈指针(用户手动分配)
(StaticTask_t *)&start_task_tcb); // 任务控制块(TCB)指针(用户手动分配)
/* 启动任务调度器:自动创建空闲任务 */
vTaskStartScheduler();
}
void task_start(void *pvParameters)
{
/* 进入临界区:保护临界区里的代码不会被打断 */
taskENTER_CRITICAL();
/* 创建任务 */
xTaskCreate((TaskFunction_t)task1,
(char *)"task1",
(configSTACK_DEPTH_TYPE)TASK1_STACK_SIZE,
(void *)NULL,
(UBaseType_t)TASK1_PRIORITY,
(TaskHandle_t *)&Task1_Handler);
timer1 = xTimerCreate((char *)"timer1",
(TickType_t)10,
(BaseType_t)pdTRUE,
(void *)0,
scan_keypad);
timer2 = xTimerCreate((char *)"timer2",
(TickType_t)1000,
(BaseType_t)pdTRUE,
(void *)0,
timer2_callback);
if (timer1 != NULL && timer2 != NULL)
printf("软件定时器创建成功\r\n");
else
printf("软件定时器创建失败\r\n");
xTimerStart(timer1, portMAX_DELAY);
/* 启动任务只需要执行一次即可,用完就删除自己 */
vTaskDelete(NULL);
/* 退出临界区 */
taskEXIT_CRITICAL();
}
2 ) 按键扫描函数(这里为4*4矩阵键盘)
// 键盘消抖扫描函数
void scan_keypad(TimerHandle_t xTimer)
{
// 依次扫描每一行
for (int row = 0; row < 4; row++)
{
// 将当前行置为低电平,其他行置为高电平
HAL_GPIO_WritePin(ROW_GPIO_PORT, ROW1_PIN, GPIO_PIN_SET);
HAL_GPIO_WritePin(ROW_GPIO_PORT, ROW2_PIN, GPIO_PIN_SET);
HAL_GPIO_WritePin(ROW_GPIO_PORT, ROW3_PIN, GPIO_PIN_SET);
HAL_GPIO_WritePin(ROW_GPIO_PORT, ROW4_PIN, GPIO_PIN_SET);
HAL_GPIO_WritePin(ROW_GPIO_PORT, (row == 0) ? ROW1_PIN : (row == 1) ? ROW2_PIN
: (row == 2) ? ROW3_PIN
: ROW4_PIN,
GPIO_PIN_RESET);
// 检查每一列的状态
for (int col = 0; col < 4; col++)
{
key[(row * 4) + col].state = HAL_GPIO_ReadPin(COL_GPIO_PORT, (col == 0) ? COL1_PIN : (col == 1) ? COL2_PIN
: (col == 2) ? COL3_PIN
: COL4_PIN);
}
}
for (int i = 0; i < 16; i++)
{
switch (key[i].judge)
{
case 0:
{
if (key[i].state == 0)
{
key[i].judge = 1;
}
}
break;
case 1:
{
if (key[i].state == 0)
{
key[i].judge = 2;
}
}
break;
case 2:
{
if (key[i].state == 1)
{
key[i].flag = 1;
key[i].judge = 0;
}
break;
}
}
}
}
3 ) task1
void task1(void *pvParameters)
{
BaseType_t err = pdFAIL;
while (1)
{
if (key[0].flag)
{
err = xTimerStart(timer2, portMAX_DELAY);
if (err == pdTRUE)
printf("开启软件定时器2成功 \r\n");
else
printf("开启软件定时器2失败 \r\n");
key[0].flag = 0;
}
if (key[1].flag)
{
err = xTimerStop(timer2, portMAX_DELAY);
if (err == pdTRUE)
printf("关闭软件定时器2成功 \r\n");
else
printf("关闭软件定时器2失败 \r\n");
key[1].flag = 0;
}
if (key[2].flag)
{
err = xTimerChangePeriod(timer2,(TickType_t) 500 ,portMAX_DELAY);
if (err == pdTRUE)
printf("修改软件定时器2回调周期成功 \r\n");
else
printf("修改软件定时器2回调周期失败 \r\n");
key[2].flag = 0;
}
if (key[3].flag)
{
err = xTimerPendFunctionCall((PendedFunction_t)fun,NULL,0,portMAX_DELAY);
if (err == pdTRUE)
printf("将函数 fun 发送到定时器服务任务中执行成功 \r\n");
else
printf("将函数 fun 发送到定时器服务任务中执行中失败 \r\n");
key[3].flag = 0;
}
vTaskDelay(100);
}
}
4 ) 发送到定时器服务任务中的函数
void fun(void);
void fun(){
printf("fun runing...\r\n");
}
5 ) timer2 回调函数
void timer2_callback(TimerHandle_t xTimer)
{
static unsigned int time = 0;
printf("timer2 运行次数为: %d \r\n", ++time);
}