软件定时器¶
软件定时器让你可以在指定时间后执行一个回调函数,而不需要占用硬件定时器资源。FreeRTOS 的软件定时器由专门的守护任务(Timer Service Task)管理,适合实现超时检测、周期性操作等场景。
软件定时器 vs 硬件定时器¶
| 特性 | 硬件定时器(TIM) | 软件定时器 |
|---|---|---|
| 精度 | 极高(微秒级) | 依赖 tick 精度(默认 1ms) |
| 数量 | 有限(取决于芯片) | 几乎无限(受内存限制) |
| 资源占用 | 占用硬件外设 | 仅占用少量内存 |
| 执行上下文 | 在中断中执行 | 在定时器守护任务中执行 |
| 适用场景 | PWM、精确计时、编码器 | 超时、周期性检查、延迟操作 |
什么时候用软件定时器?
- 不需要微秒级精度的定时操作
- 硬件定时器资源不够用
- 需要大量不同周期的定时器
- 需要动态创建/销毁定时器
基本概念¶
定时器类型¶
| 类型 | 行为 | 适用场景 |
|---|---|---|
| 单次定时器(One-shot) | 到期执行一次回调,然后自动停止 | 超时检测、延迟执行 |
| 周期定时器(Periodic) | 到期执行回调后自动重置,周而复始 | 心跳包、LED 闪烁、定时巡检 |
gantt
title 定时器类型对比
dateFormat X
axisFormat %s
section 单次定时器
等待 :a1, 0, 100
回调执行 :crit, a2, 100, 105
停止 :a3, 105, 200
section 周期定时器
等待 :b1, 0, 100
回调执行 :crit, b2, 100, 105
等待 :b3, 105, 205
回调执行 :crit, b4, 205, 210
等待 :b5, 210, 310
回调执行 :crit, b6, 310, 315
定时器守护任务¶
所有软件定时器的回调都在同一个任务(Timer Service Task / Daemon Task)中执行:
graph TD
T1[定时器1] -->|到期| D[定时器守护任务<br>Timer Daemon]
T2[定时器2] -->|到期| D
T3[定时器3] -->|到期| D
D -->|执行| CB1[回调函数1]
D -->|执行| CB2[回调函数2]
D -->|执行| CB3[回调函数3]
回调函数中的限制
因为所有定时器回调都在同一个任务中串行执行:
- 不能调用阻塞 API(如
osDelay()、osMessageQueueGet(osWaitForever)) - 回调应尽量短,否则会影响其他定时器的精度
- 如果需要长时间处理,应在回调中释放信号量,由专门的任务去处理
CubeMX 中配置软件定时器¶
配置步骤¶
- Middleware → FREERTOS → Timers and Semaphores 标签页
- 在 Timers 区域点击 Add
- 配置参数:
| 参数 | 说明 | 示例 |
|---|---|---|
| Timer Name | 定时器名称 | HeartbeatTimer |
| Callback | 回调函数名 | HeartbeatCallback |
| Type | 单次/周期 | osTimerPeriodic |
CubeMX 配置定时器守护任务参数¶
在 Config parameters 中:
| 参数 | 说明 | 建议值 |
|---|---|---|
USE_TIMERS |
启用软件定时器 | 1(启用) |
TIMER_TASK_PRIORITY |
守护任务优先级 | 2(默认,高于 Idle) |
TIMER_TASK_STACK_DEPTH |
守护任务栈大小 | 256(默认,按需调整) |
TIMER_QUEUE_LENGTH |
定时器命令队列长度 | 10(默认) |
生成的代码¶
/* 自动生成的定时器定义 */
osTimerId_t HeartbeatTimerHandle;
const osTimerAttr_t HeartbeatTimer_attributes = {
.name = "HeartbeatTimer"
};
void MX_FREERTOS_Init(void)
{
/* 创建周期定时器 */
HeartbeatTimerHandle = osTimerNew(HeartbeatCallback,
osTimerPeriodic,
NULL,
&HeartbeatTimer_attributes);
}
/* 回调函数(需要在 USER CODE 区域实现) */
void HeartbeatCallback(void *argument)
{
/* USER CODE BEGIN HeartbeatCallback */
// 定时器到期时执行的代码
/* USER CODE END HeartbeatCallback */
}
软件定时器 API¶
创建定时器¶
osTimerId_t osTimerNew(
osTimerFunc_t func, // 回调函数
osTimerType_t type, // osTimerOnce 或 osTimerPeriodic
void *argument, // 传给回调函数的参数
const osTimerAttr_t *attr // 属性(名称等)
);
启动 / 停止 / 重启¶
// 启动定时器(设置周期 1000ms)
osTimerStart(HeartbeatTimerHandle, 1000);
// 停止定时器
osTimerStop(HeartbeatTimerHandle);
// 重启定时器(重新计时,周期不变)
// 用 osTimerStart 再次调用即可重置计时
osTimerStart(HeartbeatTimerHandle, 1000);
// 检查定时器是否在运行
uint32_t running = osTimerIsRunning(HeartbeatTimerHandle);
// 删除定时器(释放资源)
osTimerDelete(HeartbeatTimerHandle);
定时器创建后不会自动启动
osTimerNew() 只是创建定时器,必须调用 osTimerStart() 才开始计时。
实战案例¶
案例 1:LED 心跳灯¶
最常见的用法——用周期定时器实现 LED 闪烁:
/* 回调函数:每次到期翻转 LED */
void HeartbeatCallback(void *argument)
{
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
}
/* 在某个任务中启动定时器 */
void System_Task(void *argument)
{
/* 启动心跳定时器,500ms 周期 */
osTimerStart(HeartbeatTimerHandle, 500);
for (;;)
{
// 做其他事情,LED 自动在后台闪烁
osDelay(1000);
}
}
心跳灯的意义
嵌入式开发中,心跳灯是最简单的"系统还活着"指示器。如果 LED 停止闪烁,说明系统可能死机了。
案例 2:通信超时检测¶
使用单次定时器实现串口通信的超时机制:
osTimerId_t TimeoutTimerHandle;
volatile uint8_t timeout_flag = 0;
/* 超时回调 */
void TimeoutCallback(void *argument)
{
timeout_flag = 1; // 标记超时
}
/* 创建单次定时器 */
void Init_Timeout(void)
{
const osTimerAttr_t attr = { .name = "TimeoutTimer" };
TimeoutTimerHandle = osTimerNew(TimeoutCallback, osTimerOnce,
NULL, &attr);
}
/* 通信任务 */
void Comm_Task(void *argument)
{
for (;;)
{
/* 发送请求 */
HAL_UART_Transmit(&huart1, tx_data, sizeof(tx_data), 100);
/* 启动 500ms 超时定时器 */
timeout_flag = 0;
osTimerStart(TimeoutTimerHandle, 500);
/* 等待回复 */
while (!rx_complete && !timeout_flag)
{
osDelay(1);
}
if (timeout_flag)
{
/* 超时处理:重发或报错 */
printf("通信超时!\r\n");
}
else
{
/* 收到回复,停止定时器 */
osTimerStop(TimeoutTimerHandle);
process_response();
}
osDelay(1000);
}
}
案例 3:按键消抖¶
使用单次定时器实现按键消抖,避免在任务中使用 osDelay 消抖造成的延迟:
osTimerId_t DebounceTimerHandle;
volatile uint8_t button_confirmed = 0;
/* 消抖定时器回调(30ms 后确认按键状态) */
void DebounceCallback(void *argument)
{
/* 30ms后再次读取按键状态 */
if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET)
{
button_confirmed = 1; // 确认按下
}
}
/* 外部中断回调 */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == GPIO_PIN_0)
{
/* 按键触发中断,启动消抖定时器 */
osTimerStart(DebounceTimerHandle, 30);
}
}
/* 按键处理任务 */
void Button_Task(void *argument)
{
for (;;)
{
if (button_confirmed)
{
button_confirmed = 0;
/* 执行按键功能 */
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
}
osDelay(10);
}
}
案例 4:多定时器管理¶
同时使用多个定时器实现不同周期的操作:
osTimerId_t Timer_1s_Handle;
osTimerId_t Timer_5s_Handle;
osTimerId_t Timer_30s_Handle;
/* 1 秒定时器:更新显示 */
void Timer_1s_Callback(void *argument)
{
update_display();
}
/* 5 秒定时器:保存数据 */
void Timer_5s_Callback(void *argument)
{
save_data_to_flash();
}
/* 30 秒定时器:发送心跳包 */
void Timer_30s_Callback(void *argument)
{
send_heartbeat();
}
/* 初始化并启动所有定时器 */
void Start_All_Timers(void)
{
const osTimerAttr_t attr1 = { .name = "Timer1s" };
const osTimerAttr_t attr2 = { .name = "Timer5s" };
const osTimerAttr_t attr3 = { .name = "Timer30s" };
Timer_1s_Handle = osTimerNew(Timer_1s_Callback,
osTimerPeriodic, NULL, &attr1);
Timer_5s_Handle = osTimerNew(Timer_5s_Callback,
osTimerPeriodic, NULL, &attr2);
Timer_30s_Handle = osTimerNew(Timer_30s_Callback,
osTimerPeriodic, NULL, &attr3);
osTimerStart(Timer_1s_Handle, 1000);
osTimerStart(Timer_5s_Handle, 5000);
osTimerStart(Timer_30s_Handle, 30000);
}
定时器回调中的注意事项¶
不能做的事¶
void BadCallback(void *argument)
{
osDelay(100); // ❌ 不能阻塞!
osSemaphoreAcquire(sem, 1000); // ❌ 不能长时间等待!
osMessageQueueGet(q, &d, NULL,
osWaitForever); // ❌ 不能无限等待!
heavy_computation(); // ⚠️ 避免耗时操作
}
正确做法:回调通知,任务处理¶
osSemaphoreId_t ProcessSemHandle;
/* 定时器回调:仅释放信号量 */
void TimerCallback(void *argument)
{
osSemaphoreRelease(ProcessSemHandle); // ✅ 快速,非阻塞
}
/* 专门的处理任务 */
void Process_Task(void *argument)
{
for (;;)
{
if (osSemaphoreAcquire(ProcessSemHandle, osWaitForever) == osOK)
{
// ✅ 在任务中做复杂处理
heavy_computation();
HAL_UART_Transmit(&huart1, data, len, 1000);
}
}
}
常见问题¶
定时器精度不够怎么办?
软件定时器精度取决于 TICK_RATE_HZ(默认 1000 = 1ms)。如果需要更高精度:
- 增大
TICK_RATE_HZ(如 10000 = 0.1ms),但会增加上下文切换开销 - 使用硬件定时器(TIM)进行精确计时
- 如果定时器回调中有耗时操作,会影响后续定时器的触发时间
定时器太多会影响性能吗?
- 创建大量定时器本身不会影响性能(不运行时不占 CPU)
- 但如果大量定时器同时到期且回调执行时间较长,守护任务会排队处理
- 建议:短回调 + 信号量通知模式
定时器回调和任务回调有什么区别?
定时器不是独立任务。所有定时器回调都在同一个守护任务中执行。如果你需要每个定时操作独立运行、互不影响,应该创建独立的任务并使用 osDelay 或 osDelayUntil。