tags: [source-summary] type: source source: "尚硅谷嵌入式技术之FreeRTOS实时操作系统 V1.0.3 — 第17~18章 + 配套代码19/20" author: "尚硅谷研究院" date: 2026-07-12 url: ""
用生活理解:软件定时器就像你手机上的"闹钟"——
- 一次性定时器:设一个闹钟,响了就没了(比如"30分钟后关火")
- 周期性定时器:设一个重复闹钟,每天这个点都响(比如"每天早上7点起床")
精度限制:软件定时器的精度由 tick 周期决定(如 1ms 节拍 → 最大精度 1ms),无法达到微秒级——这是与硬件定时器的本质区别。
由专用任务管理:软件定时器由调度器启动时自动创建的定时器服务任务(
prvTimerTask)负责管理。所有定时器命令(启动/停止/复位)通过命令队列发送给该任务。回调函数在该任务上下文中执行,因此不能调用任何阻塞 API,否则会卡住整个定时器服务。
| 特性 | 硬件定时器 | 软件定时器 |
|---|---|---|
| 本质 | 芯片内部真实计数器 | 用RTOS tick模拟的"假定时器" |
| 精度 | 微秒级 | 受任务调度影响(毫秒级) |
| 数量 | 有限(STM32F1有几个TIM) | 无限(只要内存够) |
| 回调 | 在中断中执行 | 在"定时器服务任务"中执行 |
#include "timers.h"
TimerHandle_t timer1_handle; // 一次性
TimerHandle_t timer2_handle; // 周期性
void freertos_start(void)
{
// 创建一次性定时器:500ms后执行一次
timer1_handle = xTimerCreate(
"timer1", // 名字(调试用)
500, // 周期 = 500个tick
pdFALSE, // pdFALSE = 一次性,pdTRUE = 周期性
(void *)1, // 定时器ID(可用于区分多个定时器)
timer1_callback); // 超时回调函数
// 创建周期性定时器:每1000ms执行一次
timer2_handle = xTimerCreate(
"timer2", 1000, pdTRUE, (void *)2, timer2_callback);
xTaskCreate(/* task1 */);
vTaskStartScheduler();
}
/* ========== 控制任务 ========== */
void task1(void *pvParameters)
{
uint8_t key = 0;
while (1)
{
key = Key_Detect();
if (key == KEY1_PRESS)
{
xTimerStart(timer1_handle, portMAX_DELAY); // 启动一次性
xTimerStart(timer2_handle, portMAX_DELAY); // 启动周期性
}
else if (key == KEY2_PRESS)
{
xTimerStop(timer1_handle, portMAX_DELAY); // 停止
xTimerStop(timer2_handle, portMAX_DELAY);
}
vTaskDelay(500);
}
}
/* ========== 回调函数 ========== */
void timer1_callback(TimerHandle_t xTimer)
{
static uint16_t count = 0;
printf("一次性定时器触发了第%d次\r\n", ++count);
// 一次性定时器:只会打印一次
}
void timer2_callback(TimerHandle_t xTimer)
{
static uint16_t count = 0;
printf("周期性定时器触发了第%d次\r\n", ++count);
// 周期性定时器:一直打印下去
}
| 函数 | 作用 | 类比 |
|---|---|---|
xTimerCreate() |
创建定时器 | 买一个闹钟 |
xTimerStart() |
启动 | 拧上发条开始计时 |
xTimerStop() |
停止 | 按掉闹钟 |
xTimerReset() |
复位(重新开始计时) | 把闹钟重设为当前时间+N |
xTimerChangePeriod() |
修改周期 | 把闹钟改成不同的时间响 |
定时器回调函数在定时器服务任务中执行,不能调用阻塞 API:
// ❌ 错误:在回调中调用阻塞函数
void timer_callback(TimerHandle_t xTimer)
{
vTaskDelay(100); // 禁止!会阻塞整个定时器服务任务
xQueueReceive(queue, &val, portMAX_DELAY); // 禁止!
xSemaphoreTake(sem, portMAX_DELAY); // 禁止!
}
// ✅ 正确做法:在回调里只做快速处理
void timer_callback(TimerHandle_t xTimer)
{
printf("超时了!\r\n"); // 可以:printf通常很快
LED_Toggle(LED1_Pin); // 可以:GPIO操作极快
xQueueSend(queue, &val, 0); // 可以:超时=0不阻塞
}
用生活理解:普通模式下,系统像一个人"每秒醒来看一次有没有事做"。Tickless 模式下,没事做就直接睡大觉,直到有事了才被叫醒。省电!
普通模式: 醒→看→没事→睡→醒→看→没事→睡→醒→看→有事→干→醒→看...
↑ ↑ ↑
1秒醒一次 1秒醒一次 1秒醒一次(浪费电)
Tickless: 醒→看→没事→睡大觉→有事→醒了→干活
↑ ↑
睡很久 有人叫醒(省电!)
核心机制:当所有任务都阻塞时,系统可以预测空闲任务的持续时长(从最近的阻塞任务超时时间计算),然后在此时间段内禁用 SysTick 中断,醒来时一次性补偿漏掉的 tick 计数。
睡觉被手机吵醒类比:"好不容易有半个小时要睡一会儿,结果你的手机一分钟响一次"——这就是不禁用 SysTick 的感受。Tickless 模式相当于睡前把手机调成静音,睡醒了再看所有未接来电。
最小空闲时长配置:可设置进入低功耗所需的最短空闲时间——如果空闲时间太短,"进入退出的开销得不偿失",就不进低功耗模式。
// FreeRTOSConfig.h
#define configUSE_TICKLESS_IDLE 1 // 开启低功耗
// 定义"睡前处理"和"醒后处理"
#define configPRE_SLEEP_PROCESSING(x) PRE_SLEEP_PROCESSING()
#define configPOST_SLEEP_PROCESSING(x) POST_SLEEP_PROCESSING()
// 睡前:关掉所有外设时钟以省电
void PRE_SLEEP_PROCESSING(void)
{
__HAL_RCC_GPIOA_CLK_DISABLE();
__HAL_RCC_GPIOB_CLK_DISABLE();
__HAL_RCC_GPIOC_CLK_DISABLE();
// ... 关掉当前不需要的外设
}
// 醒后:重新打开外设时钟
void POST_SLEEP_PROCESSING(void)
{
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_GPIOC_CLK_ENABLE();
// ... 重新打开外设
}
| 模式 | 功耗 | 唤醒方式 | 醒来速度 | Tickless用哪个? |
|---|---|---|---|---|
| 睡眠(Sleep) | 中 | 任何中断 | 最快 | ✅ 默认用这个 |
| 停机(Stop) | 低 | EXTI/RTC/USB | 中等 | ⚠️ 需额外配置 |
| 待机(Standby) | 极低 | 复位/WKUP/RTC | 最慢 | ❌ 用不了 |
code/19_软件定时器/code/20_低功耗模式/后续章节:[[11-内存管理]]