目录
3. xTimerStart()启动一个已经通过 xTimerCreate() 创建好的定时器
5.xTimerStartFromISR()从中断中启动软件定时器
6.xTimerStopFromISR()从中断中停止一个已经启动的定时器
(5)xTimerReset()与xTimerStop() + xTimerStart()的区别
8.xTimerResetFromISR() —— 在中断中重置软件定时器
9.xTimerChangePeriod()修改软件定时器的周期
一. 基础概念
1.概念
FreeRTOS 软件定时器是通过系统 tick 中断实现的定时回调机制,可以在将来某个时刻或周期性地调用一个用户提供的函数,用于定时处理任务,而不需要你自己维护计数器。这个机制不需要你自己写计数器,也不使用芯片的硬件定时器外设。
2.FreeRTOS软件定时器实现的底层原理
FreeRTOS 中的软件定时器是一种由系统调度器控制的延时/周期性触发机制,它允许你在不创建额外任务的情况下,在某个时间点自动执行指定的“回调函数”。
1. 软件定时器依赖一个专用的 Timer 服务任务:FreeRTOS 内部有一个专门的系统任务(叫 Timer Service Task),它负责:
-
维护一个定时器链表(按超时时间排序);
-
根据系统 Tick 递增,判断哪个定时器到期;
-
到期后自动执行你传入的回调函数(callback);
【注意】这个回调函数不是在中断中执行的,是在 Timer Service Task 里执行的,所以不能太耗时,也不能阻塞。
2. 软件定时器的 Tick 源:使用的是系统节拍(SysTick)。每当系统节拍(比如 1ms)增加,FreeRTOS 会更新各个定时器的剩余时间;达到你设置的定时时间后,进入“超时队列”,由 Timer 服务任务调度。
软件定时器 = 一个后台任务维护的定时器列表,到时间了就帮你自动执行你提供的函数。
3.软件定时器的作用
在嵌入式系统中,你经常会遇到下面这些需求:
-
每 500ms 闪一次 LED
-
延迟 2 秒后关闭电机
-
每 10 秒采集一次传感器数据
-
启动后等待 3 秒再做某事
如果你用任务 + vTaskDelay() 来实现,会浪费任务资源、栈空间,而且效率低。
软件定时器解决这个问题:
-
不需要为这些“等时间”的小逻辑专门建一个任务;
-
FreeRTOS 会帮你管理 tick 并在时间到了调用你写的回调函数;
-
比写 while(1){ delay; 做事; } 更轻巧、结构更清晰;
4. FreeRTOS软件定时器优点与缺点
优点:
-
无需创建额外任务;
-
代码简单,适合定时触发类应用;
-
定时精度一般在 1 Tick 级别(通常是 1ms);
-
任务栈压力低(Timer callback 由系统统一调度)。
缺点:
-
不适合高精度定时(无法做到微秒级);
-
不能阻塞、不能耗时长操作;
-
所有回调都由同一个 Timer Service Task 处理,回调时间太久会阻塞其他定时器。
二.相关函数
1.软件定时器相关配置项
FreeRTOS 内部会自动创建一个“定时器服务任务”(Timer Service Task),用于运行所有软件定时器的回调函数。
所以不需要你再手动创建定时器的任务,你只要确保:
-
FreeRTOSConfig.h 中定义了宏 configUSE_TIMERS == 1
-
并在FreeRTOSConfig.h 配置了下面这些宏(一般系统默认就设置好了):
#define configUSE_TIMERS 1 // 启用软件定时器
#define configTIMER_TASK_PRIORITY 2 // 定时器服务任务的优先级
#define configTIMER_QUEUE_LENGTH 10 // 定时器命令队列的长度
#define configTIMER_TASK_STACK_DEPTH configMINIMAL_STACK_SIZE // 栈大小
这些选项告诉 FreeRTOS:
-
是否启用软件定时器
-
创建一个什么优先级的后台任务来处理软件定时器
-
定时器命令队列长度(用于支持多个启动/停止等请求)
2.xTimerCreate() 创建定时器
(1)函数原型
TimerHandle_t xTimerCreate(
const char *pcTimerName, // 定时器名称(可用于调试)
TickType_t xTimerPeriod, // 定时周期,单位是 Tick,这个参数定义定时器多久超时一次,一旦超时,就会调用你指定的回调函数;
UBaseType_t uxAutoReload, // 是否自动重装载(pdTRUE=周期性,pdFALSE=一次性,只执行一次)
void *pvTimerID, // 用户自定义参数(通过 xTimerGetTimerID 获取)
TimerCallbackFunction_t pxCallbackFunction // 到期时调用的函数
);
(2)参数解释
①const char *pcTimerName:定时器命名(调试用)
-
这个是定时器的“名字”,主要用于调试或追踪,不会影响功能本身。
-
可以在 FreeRTOS 提供的调试工具、日志、命令行调试接口中查看这个名字。
② TickType_t xTimerPeriodInTicks:定时器的超时时间,以 Tick 为单位
-
这个参数定义定时器多久“超时”一次。
-
一旦超时,就会调用你指定的回调函数。
-
若你希望用毫秒指定时间,可以用 pdMS_TO_TICKS(ms) 宏转换。
-
举例说明:
-
假设 configTICK_RATE_HZ = 1000(每秒 1000 tick,即 1 tick = 1ms)那么:
-
pdMS_TO_TICKS(1000) = 1000 tick = 1 秒
-
pdMS_TO_TICKS(500) = 500 tick = 0.5 秒
-
③ UBaseType_t uxAutoReload:是否自动重复执行
-
该参数决定定时器是:
-
pdTRUE:周期性定时器(超时后会自动重装载并重新计时)
-
pdFALSE:一次性定时器(只执行一次,之后就停了)
-
④void *pvTimerID:用户定义的 ID,可在回调函数中识别定时器来源
-
你可以传入任意指针(结构体指针、字符串、整数的指针等)。
-
然后在回调函数中用 xTimerGetTimerID() 取回,进行判断或处理。
-
如果你用一个回调函数处理多个定时器,这个字段尤其有用。
int timer_id = 1;
my_timer = xTimerCreate("MyTimer", pdMS_TO_TICKS(1000), pdFALSE, (void*)&timer_id, my_callback);
//然后在回调函数中:
void my_callback(TimerHandle_t xTimer) {
int* pID = (int*)xTimerGetTimerID(xTimer);
printf("Timer ID = %d\n", *pID);
//...处理程序
}
⑤ TimerCallbackFunction_t pxCallbackFunction定时器超时时要执行的回调函数
-
类型为 void (*TimerCallbackFunction_t)( TimerHandle_t xTimer )
-
每次定时器“超时”,这个函数就会被“定时器服务任务”自动调用。
-
函数参数是该定时器的句柄,你可以用它获取定时器 ID 或名字等。
-
【注意】回调函数应当快速返回,不能阻塞、不能等待、不能死循环。
(3)示例
TimerHandle_t my_timer;
void my_timer_callback(TimerHandle_t xTimer) {
printf("Timer triggered!\n");
}
void app_main(void) {
my_timer = xTimerCreate(
"MyTimer",
pdMS_TO_TICKS(1000), // 1000 ms
pdTRUE, // pdTRUE:表示每隔1秒调用一次回调函数(周期性) pdFALSE:只在1秒(你设定的第2个参数)后调用一次回调函数(一次性)
NULL,
my_timer_callback
);
if (my_timer == NULL) {
printf("Timer create failed!\n");
}
}
3. xTimerStart()启动一个已经通过 xTimerCreate() 创建好的定时器
(1)函数原型
BaseType_t xTimerStart( TimerHandle_t xTimer, TickType_t xTicksToWait );
(2)参数解释
-
参数1: xTimer :xTimerCreate()返回的定时器句柄;
-
参数2:xTicksToWait:如果定时器命令队列满了,等待多久(tick 为单位);
(3)返回值
-
pdPASS:命令成功放入队列,稍后将被执行
-
pdFAIL:队列已满,或者 Timer 服务任务没有启动
(4)示例
// 在某个任务中
if (xTimerStart(myTimerHandle, portMAX_DELAY) == pdPASS) {
printf("定时器启动成功!\n");
}
(5)注意
-
xTimerCreate() 创建定时器时并不会自动启动,必须手动 xTimerStart()。
-
如果是一次性定时器(uxAutoReload = pdFALSE),调用一次就用一次。
-
如果是周期性定时器(uxAutoReload = pdTRUE),会自动按设定周期反复执行。
-
xTimerStart()不可用于中断!
4. xTimerStop()停止一个软件定时器
请求停止一个已经运行的定时器。调用次函数后,定时器就不会再调用它的回调函数了;用于停止一个已启动的定时器,防止它触发回调函数;当你的逻辑上认为“这个动作不该再执行了”,就用它;
(1)函数原型
BaseType_t xTimerStop(TimerHandle_t xTimer, TickType_t xTicksToWait);
(2)参数解释
参数1:xTimer:要停止的定时器句柄
参数2: xTicksToWait:如果定时器服务队列满了,等待的最大 Tick 数,通常传 0 表示不等待;
(3)返回值
pdPASS :停止定时器的命令已成功发给定时器服务任务;
pdFAIL:定时器服务队列满,或定时器还没创建,或者未初始化定时器任务;
(4)使用场景
-
用户中断某个等待动作:如“按键长按3秒关机”,但中途松手了 ;
-
任务提前完成:如你设了一个超时定时器,但任务提前完成了,不想触发超时回调了
-
设备状态改变:比如定时LED自动熄灭,但用户手动熄灭了
(5)示例
//目标:实现用户如果 5 秒内没操作,就关闭 Wi-Fi 模块
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/timers.h"
#include <stdio.h>
TimerHandle_t timeoutTimer; // 超时定时器句柄
// 模拟:用户无操作超时的处理函数
void timeoutCallback(TimerHandle_t xTimer)
{
printf("超时未操作,关闭 Wi-Fi 模块!\n");
// 关闭 WiFi 的实际函数写这里
// esp_wifi_stop();
}
// 模拟:用户有操作(比如按键、串口输入)
void userActivityDetected()
{
BaseType_t xResult;
xResult = xTimerStop(timeoutTimer, 0); // 如果定时器正在运行,先停止
if (xResult == pdPASS)
{
printf("已停止原定时器\r\n");
}
xResult = xTimerStart(timeoutTimer, 0); // 重启定时器等待新的 5 秒
if (xResult == pdPASS)
{
printf("重启定时器,等待用户输入...\r\n");
}
}
void app_main()
{
// 创建一个一次性定时器,5 秒超时
timeoutTimer = xTimerCreate("TimeoutTimer",
pdMS_TO_TICKS(5000),
pdFALSE, // 一次性定时器
NULL,
timeoutCallback);
if (timeoutTimer == NULL)
{
printf("创建定时器失败\n");
return;
}
xTimerStart(timeoutTimer, 0); // 启动定时器,等待用户首次输入
// 模拟系统运行:检测用户操作
while (1)
{
vTaskDelay(pdMS_TO_TICKS(2000)); // 每 2 秒检查一次输入
printf("检测到用户有操作\n");
userActivityDetected(); // 每次用户输入完后都重启定时器
}
}
5.xTimerStartFromISR()从中断中启动软件定时器
中断函数内不能做复杂操作,比如不能调用 xTimerStart(),但你可以通过 xTimerStartFromISR() 安全地告诉系统“我想启动这个定时器”。FreeRTOS 会将请求放入定时器命令队列中,由后台的定时器服务任务异步处理。
(1) 函数原型:
BaseType_t xTimerStartFromISR( TimerHandle_t xTimer, BaseType_t *pxHigherPriorityTaskWoken );
//函数返回类型和第一个参数和xTimerStart()是一样的;
//参数2:pxHigherPriorityTaskWoken:指向一个变量,如果调用这个函数导致上下文切换,这个变量会被置为 pdTRUE;
(2)示例
TimerHandle_t myTimer;
void IRAM_ATTR gpio_isr_handler(void* arg)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xTimerStartFromISR(myTimer, &xHigherPriorityTaskWoken);
// 如果有更高优先级的任务被唤醒,则请求上下文切换
if (xHigherPriorityTaskWoken)
{
portYIELD_FROM_ISR();
}
}
6.xTimerStopFromISR()从中断中停止一个已经启动的定时器
在中断上下文里,如果要让某个软件定时器立即停止,就用这个函数,而不能用 xTimerStop()(因为它是线程上下文函数)。
(1)函数原型
BaseType_t xTimerStopFromISR(TimerHandle_t xTimer, BaseType_t *pxHigherPriorityTaskWoken);
(2)参数解释
参数1:xTimer:你要停止的定时器句柄
参数2:*pxHigherPriorityTaskWoken:指向一个布尔值的指针,用来告知中断退出时是否需要触发上下文切换(标准 ISR 机制);
(3)返回值
返回 pdPASS 表示操作成功,否则失败。
(4)示例
//你设置了一个定时器,如果用户 5 秒内没按按钮 就执行关机。
//但如果用户按了按钮(触发外部中断)——我们就在 ISR 里把这个定时器停掉:
//中断函数中:
void IRAM_ATTR gpio_isr_handler(void* arg)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xTimerStopFromISR(myShutdownTimer, &xHigherPriorityTaskWoken); // 检测到按键,就取消定时关机;
// 如果需要切换任务:
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
注意:ISR 中的操作尽量简洁,建议不要频繁调用定时器创建/删除,而是只做控制启停即可。
7.xTimerReset()任务中重启定时器(从头计时)
实现在任务上下文中将定时器“重置”(Restart),如果定时器尚未启动,这个函数的效果相当于 xTimerStart()
如果定时器已经在运行,它会被停止并重新开始计时,周期从当前时刻重新计算。
(1)函数原型
BaseType_t xTimerReset(TimerHandle_t xTimer, TickType_t xTicksToWait);
(2)参数解释
-
参数1:xTimer:要重置的定时器的句柄
-
参数2:xTicksToWait如果内部命令队列满了,允许等待的最大 tick 数。通常设为 0 或 portMAX_DELAY
(3)返回值
-
pdPASS:操作成功;
-
pdFAIL:操作失败(例如队列满了或定时器无效)
(4)示例
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/timers.h"
#include "esp_system.h"
//软件定时器句柄
TimerHandle_t shutdown_timer = NULL;
//模拟用户输入检测任务(串口收数据)
void user_input_task(void *param)
{
char ch;
while (1)
{
// 模拟串口接收字符:从 stdin 获取字符
if (scanf(" %c", &ch) == 1)
{
printf("[用户操作] 检测到输入字符 '%c'\n", ch);
// 既然已经检测到用户有操作,就重置定时器
if (xTimerReset(shutdown_timer, 0) == pdPASS)
{
printf("[系统] 关机倒计时被重置\n");
}
else
{
printf("[错误] 重置定时器失败!\n");
}
}
vTaskDelay(pdMS_TO_TICKS(100)); // 稍作延时,防止过快
}
}
//软件定时器的回调函数:关机操作
void shutdown_callback(TimerHandle_t xTimer)
{
printf("用户已达到5秒无操作,自动执行关机逻辑!\n");
// 这里可以加上关机处理,如关闭设备、切断供电等
}
//主函数入口
void app_main(void)
{
printf("系统启动,初始化...\n");
// 创建定时器:5秒后执行一次
shutdown_timer = xTimerCreate("ShutdownTimer", pdMS_TO_TICKS(5000), pdFALSE, NULL, shutdown_callback);
if (shutdown_timer == NULL)
{
printf("创建定时器失败!\n");
return;
}
xTimerStart(shutdown_timer, 0); // 启动定时器(第一次倒计时)
xTaskCreate(user_input_task, "UserInputTask", 2048, NULL, 1, NULL); // 启动任务模拟用户输入
}
(5)xTimerReset()与xTimerStop() + xTimerStart()的区别
方面
|
xTimerReset()
|
xTimerStop() + xTimerStart()
|
作用
|
重启定时器(相当于“重新倒计时”)
|
停止定时器,再重新启动(彻底重置状态)
|
前提条件
|
定时器必须已经“创建”并“启动过”
|
无需判断定时器状态,直接全流程处理
|
效率
|
更高,只执行一次动作
|
慢一些,涉及两个调用
|
使用限制
|
定时器未启动时调用,会失败返回 pdFAIL
|
总是能成功(只要定时器句柄正确)
|
推荐场景
|
明确知道定时器当前“已启动”
|
状态不确定、想保证可靠性
|
假设你设置了一个“5秒后自动关灯”的定时器:
-
xTimerReset() 相当于:“我知道定时器已经在倒计时了,现在我再按一下按钮,重新从5秒开始数。”
-
xTimerStop() + xTimerStart() 相当于:“我不管你在不在倒计时了,我就先把你停掉,然后再重新开始5秒倒计时。”
关键区别:
-
xTimerReset() 只能重启已在运行的定时器。
-
xTimerStop()+xTimerStart() 不管定时器是否运行,永远能重启倒计时。
实际开发中选哪一个?
-
你确认定时器已经启动过?→ xTimerReset()
-
你不确定定时器状态、或逻辑复杂?→ xTimerStop() + xTimerStart()
-
能用 xTimerReset() 的地方优先用它;但在定时器状态不确定或希望“彻底重启”时,xTimerStop()+xTimerStart() 是更安全的选择。
8.xTimerResetFromISR() —— 在中断中重置软件定时器
用于从中断服务例程(ISR)中调用,来重置一个一次性或周期性的软件定时器。比如我们使用一个 GPIO中断 检测按键:每次按下按键,就要重置“5秒倒计时定时器”。
(1)函数原型
BaseType_t xTimerResetFromISR(
TimerHandle_t xTimer,
BaseType_t *pxHigherPriorityTaskWoken
);
(2)参数解释
-
参数1:xTimer要重置的定时器句柄
-
参数2:pxHigherPriorityTaskWoken若定时器操作唤醒了一个更高优先级的任务,需设置:
*pxHigherPriorityTaskWoken = pdTRUE,之后调用 portYIELD_FROM_ISR()
(3)返回值
-
pdPASS:成功
-
pdFAIL:失败(例如定时器未创建、定时器命令队列满了)
(4)示例
// ISR 中使用的变量
extern TimerHandle_t timeout_timer;
void IRAM_ATTR gpio_isr_handler(void *arg) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
printf("检测到按键中断,重置定时器\n");
xTimerResetFromISR(timeout_timer, &xHigherPriorityTaskWoken);
if (xHigherPriorityTaskWoken) {
portYIELD_FROM_ISR(); // 触发任务切换(如果有更高优先级的任务被唤醒)
}
}
(5)注意
-
xTimerResetFromISR() 只能在 中断上下文 使用;
-
中断函数必须标记为 IRAM_ATTR;
-
必须使用 portYIELD_FROM_ISR() 来处理优先级切换;
9.xTimerChangePeriod()修改软件定时器的周期
修改定时器的周期,并自动重启定时器(即从新周期开始重新计时)。 所以 你不需要先 stop、再 set 周期、再 start,一步完成!
使用场景:
-
低功耗设备 → 根据信号强弱、连接状态切换定时周期;
-
传感器采样 → 白天/夜晚用不同采样率;
-
任务节拍 → 有人操作时频繁刷新,没人操作时变慢;
(1)函数原型
BaseType_t xTimerChangePeriod(
TimerHandle_t xTimer,
TickType_t xNewPeriod,
TickType_t xTicksToWait
);
(2)参数解释
-
参数1:xTimer要修改的定时器句柄
-
参数2: xNewPeriod 新的定时器周期(单位:tick)
-
参数3: xTicksToWait等待时间(通常填 portMAX_DELAY 就行)
(3)返回值
-
pdPASS:修改成功
-
pdFAIL:失败(比如定时器句柄无效)
(4)示例
//你在设备联网后,向服务器发心跳包:
//联网成功 → 心跳每 5 秒一次;
//联网失败 → 改为每 20 秒尝试一次连接;
// 定时器回调函数
void vHeartbeatTimerCallback(TimerHandle_t xTimer) {
if (is_wifi_connected()) {
printf("发送心跳包\n");
} else {
printf("WiFi断开,尝试重连...\n");
}
}
// 任务中动态调整周期
if (is_wifi_connected()) {
xTimerChangePeriod(heartbeat_timer, pdMS_TO_TICKS(5000), portMAX_DELAY);
} else {
xTimerChangePeriod(heartbeat_timer, pdMS_TO_TICKS(20000), portMAX_DELAY);
}
//设备默认每 2 秒打印一次“心跳”;
//如果用户按下按键,则将定时器周期改为 500 毫秒,更频繁地“刷新”;
//再次按键,又改回原周期(实现周期在两种值之间切换)。
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/timers.h"
#include "driver/gpio.h"
#define BUTTON_GPIO GPIO_NUM_0 // 按键接在 GPIO0
#define SHORT_PERIOD pdMS_TO_TICKS(500)
#define LONG_PERIOD pdMS_TO_TICKS(2000)
// 全局变量
TimerHandle_t heartbeat_timer;
volatile bool is_fast = false;
// 定时器回调函数
void heartbeat_timer_callback(TimerHandle_t xTimer) {
printf("心跳一次!当前周期:%s\n", is_fast ? "500ms" : "2000ms");
}
// 任务:监测按键并改变定时器周期
void button_task(void *pvParameters) {
gpio_set_direction(BUTTON_GPIO, GPIO_MODE_INPUT);
gpio_set_pull_mode(BUTTON_GPIO, GPIO_PULLUP_ONLY);
while (1) {
if (gpio_get_level(BUTTON_GPIO) == 0) { // 按键按下
vTaskDelay(pdMS_TO_TICKS(50)); // 简单防抖
if (gpio_get_level(BUTTON_GPIO) == 0) {
// 切换状态
is_fast = !is_fast;
// 修改定时器周期
xTimerChangePeriod(heartbeat_timer,
is_fast ? SHORT_PERIOD : LONG_PERIOD,
portMAX_DELAY);
printf("切换定时器周期为:%s\n", is_fast ? "500ms" : "2000ms");
// 等待按键释放
while (gpio_get_level(BUTTON_GPIO) == 0) {
vTaskDelay(pdMS_TO_TICKS(10));
}
}
}
vTaskDelay(pdMS_TO_TICKS(100)); // 检测间隔
}
}
void app_main() {
// 创建定时器,初始周期为 2000ms
heartbeat_timer = xTimerCreate("heartbeat", LONG_PERIOD,pdTRUE,NULL, heartbeat_timer_callback);
if (heartbeat_timer == NULL) {
printf("定时器创建失败!\n");
} else {
xTimerStart(heartbeat_timer, 0); // 启动定时器
}
// 创建监控按键的任务
xTaskCreate(button_task, "button_task", 2048, NULL, 10, NULL);
}
(5)注意
-
xTimerChangePeriod()必须在任务上下文中调用,不能在中断中用,如果你在中断中想修改周期,请用:xTimerChangePeriodFromISR()
-
修改周期后,定时器自动重启;
-
如果你想保留当前运行状态(比如暂不启动),不要用它;