在上一篇文章中,我们学习了互斥量,解决了共享资源保护与优先级反转问题。本篇将介绍 FreeRTOS 的软件定时器,它无需占用额外的硬件定时器资源,即可实现单次或周期性的定时回调。我们将通过实验演示如何创建、启动、停止软件定时器,并总结使用中的关键注意事项。

一、软件定时器与硬件定时器的区别
硬件定时器(如 TIM1、SysTick)直接由 MCU 外设实现,精度高、实时性强,但数量有限且配置相对复杂。
FreeRTOS 的软件定时器完全由内核通过一个守护任务(Timer Service Task)实现,它利用系统节拍来检查定时器是否超时,并执行对应的回调函数。其优点包括:
- 不占用额外硬件资源;
- 可以动态创建、删除;
- 回调函数运行在守护任务的上下文,可以安全调用大多数 FreeRTOS API(除部分阻塞函数外)。
当然,软件定时器的精度受系统节拍频率限制(通常为 1ms),且回调执行时间不确定(受守护任务优先级影响),不适用于微秒级硬实时需求。
二、软件定时器的关键 API
| 功能 | API 名称 | 说明 |
|---|---|---|
| 创建定时器 | xTimerCreate |
返回定时器句柄,需指定名称、周期、是否自动重载、ID、回调函数 |
| 启动定时器 | xTimerStart |
启动后开始计时 |
| 停止定时器 | xTimerStop |
停止计时,不触发回调 |
| 从 ISR 启动 | xTimerStartFromISR |
中断中使用的启动版本 |
| 从 ISR 停止 | xTimerStopFromISR |
中断中使用的停止版本 |
| 更改周期 | xTimerChangePeriod |
修改定时器的超时时间 |
| 获取定时器 ID | pvTimerGetTimerID |
返回创建时绑定的 ID 指针 |
| 删除定时器 | xTimerDelete |
释放定时器资源 |
使用软件定时器前,必须在
FreeRTOSConfig.h中将configUSE_TIMERS设置为 1,并配置configTIMER_TASK_PRIORITY和configTIMER_QUEUE_LENGTH等参数。这些在系列第一篇的配置文件中已经设置好,此处无需额外修改。
三、硬件准备与配置
本实验使用板载 PC13 LED,通过软件定时器实现不同频率的闪烁,无需额外硬件。BSP 文件沿用之前的 bsp_led.h 和 bsp_led.c,包含 LED_InitAll() 和 LED3_Toggle() 即可。
确保在 main() 开头调用了:
c
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
四、实验一:单次定时器
创建一个只运行一次的软件定时器,延时 3 秒后翻转一次 LED,之后不再触发。
4.1 代码实现
c
#include "stm32f10x.h"
#include "FreeRTOS.h"
#include "task.h"
#include "timers.h"
#include "bsp_led.h"
TimerHandle_t xOneShotTimer = NULL;
/* 定时器回调函数 */
void vOneShotCallback(TimerHandle_t xTimer)
{
LED3_Toggle(); // 3 秒后翻转一次 LED
}
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
LED_InitAll();
/* 创建单次定时器:
参数:名称、周期(3s)、自动重载(pdFALSE)、定时器 ID(0)、回调函数 */
xOneShotTimer = xTimerCreate(
"OneShot",
pdMS_TO_TICKS(3000),
pdFALSE, // 单次模式,不自动重载
(void *)0,
vOneShotCallback
);
if (xOneShotTimer != NULL)
{
xTimerStart(xOneShotTimer, 0); // 启动,超时时间 0 表示立刻开始计时
}
vTaskStartScheduler();
while (1);
}
4.2 实验现象
上电后 LED 保持初始状态(灭),3 秒后翻转一次,之后不再变化。单次定时器适合实现"延时执行"的功能,比在任务中使用 vTaskDelay 更简洁,且不占用额外的任务堆栈。
五、实验二:周期定时器
创建一个自动重载的周期定时器,每 500ms 翻转一次 LED,实现稳定的 1Hz 闪烁。
c
#include "stm32f10x.h"
#include "FreeRTOS.h"
#include "task.h"
#include "timers.h"
#include "bsp_led.h"
TimerHandle_t xPeriodicTimer = NULL;
/* 定时器回调函数 */
void vPeriodicCallback(TimerHandle_t xTimer)
{
LED3_Toggle();
}
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
LED_InitAll();
/* 创建周期定时器:500ms 周期,自动重载 */
xPeriodicTimer = xTimerCreate(
"Periodic",
pdMS_TO_TICKS(500),
pdTRUE, // 自动重载,周期触发
(void *)0,
vPeriodicCallback
);
if (xPeriodicTimer != NULL)
{
xTimerStart(xPeriodicTimer, 0);
}
vTaskStartScheduler();
while (1);
}
实验现象:LED 以 1Hz 频率稳定闪烁。这个闪烁完全由软件定时器的回调函数驱动,主任务甚至可以是空的。
六、动态控制定时器:启动、停止与修改周期
软件定时器可在运行时被其他任务或中断动态控制。我们通过一个按键任务来模拟:按一次按键,启动定时器;再按一次,停止定时器。按键处理仍使用之前的中断加队列方案。
6.1 中断服务函数(位于 stm32f10x_it.c 中)
c
#include "stm32f10x_it.h"
#include "FreeRTOS.h"
#include "queue.h"
extern QueueHandle_t xKeyQueue;
void EXTI0_IRQHandler(void)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
if (EXTI_GetITStatus(EXTI_Line0) != RESET)
{
uint8_t event = 1;
xQueueSendFromISR(xKeyQueue, &event, &xHigherPriorityTaskWoken);
EXTI_ClearITPendingBit(EXTI_Line0);
}
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
6.2 main.c 实现
c
#include "stm32f10x.h"
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "timers.h"
#include "bsp_led.h"
#include "bsp_exti.h" // 包含 EXTI0_Init()
TimerHandle_t xControlledTimer = NULL;
QueueHandle_t xKeyQueue = NULL;
/* 定时器回调 */
void vTimerCallback(TimerHandle_t xTimer)
{
LED3_Toggle();
}
/* 按键处理任务:根据按键状态切换定时器启停 */
void vKeyTask(void *pvParameters)
{
uint8_t key_val;
static uint8_t timer_running = 0;
while (1)
{
if (xQueueReceive(xKeyQueue, &key_val, portMAX_DELAY) == pdTRUE)
{
if (!timer_running)
{
xTimerStart(xControlledTimer, 0);
timer_running = 1;
}
else
{
xTimerStop(xControlledTimer, 0);
timer_running = 0;
}
}
}
}
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
LED_InitAll();
EXTI0_Init(); // PA0 按键中断
/* 创建队列,用于传递按键事件 */
xKeyQueue = xQueueCreate(5, sizeof(uint8_t));
if (xKeyQueue == NULL) while (1);
/* 创建周期定时器,默认不启动(后续由任务启动) */
xControlledTimer = xTimerCreate(
"CtrlTimer",
pdMS_TO_TICKS(200),
pdTRUE,
(void *)0,
vTimerCallback
);
if (xControlledTimer == NULL) while (1);
xTaskCreate(vKeyTask, "KeyTask", 128, NULL, 1, NULL);
vTaskStartScheduler();
while (1);
}
实验现象:
- 上电后定时器未启动,LED 不闪烁;
- 第一次按 PA0 按键,定时器启动,LED 以 2.5Hz(200ms 周期)闪烁;
- 再次按键,定时器停止,LED 保持当前状态不变;
- 后续每次按键交替切换启停状态。
七、软件定时器的注意事项
-
回调函数不要阻塞
回调函数运行在 Timer Service Task 中,如果在该任务中调用
vTaskDelay或等待信号量等阻塞操作,将导致整个定时器守护任务挂起,所有定时器都无法回调。务必保证回调函数快速执行完毕。 -
定时精度
软件定时器的精度为一个系统节拍(通常 1ms),并且回调实际执行时间受守护任务优先级影响。如需微秒级精确控制,请使用硬件定时器。
-
回调函数中可以调用的 API
大多数 FreeRTOS API 都可以在回调中使用,但需注意:不能调用可能导致守护任务阻塞的 API (如等待队列、信号量、互斥量等)。一般可以进行
xSemaphoreGive、xQueueSend等非阻塞操作来通知其他任务。 -
定时器的删除
如果不确定定时器是否正在运行,删除前应调用
xTimerStop确保其已停止。在回调函数中删除自己是不安全的行为,应避免。 -
与任务配合
软件定时器非常适合触发动作后立即将具体处理交给任务。例如在回调中释放一个信号量或发送队列消息,由专门的任务负责长时间操作,这样既保证了定时精度,又不会阻塞守护任务。
八、总结
本篇学习了 FreeRTOS 软件定时器的使用:
- 单次定时器用于延时执行;
- 周期定时器可替代硬件定时器,实现周期性回调;
- 定时器可被任务动态控制启停,灵活集成到应用逻辑中。
软件定时器为系统设计提供了极大的便利,在需要简单定时功能的场合应优先考虑。下一篇文章,我们将回到中断管理的话题,更系统地梳理 FreeRTOS 下中断优先级、临界区以及如何与任务高效协作。
下一篇:FreeRTOS 中断管理 ------ 优先级、临界区与任务通知。