news 2026/10/29 11:10:53

FreeRTOS:软件定时器

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
FreeRTOS:软件定时器

一、软件定时器的概念
1.1 是什么
软件定时器 = 内核用一个"定时器服务任务(Tmr Svc / Daemon Task)"来统一管理的一组定时器。 它不占用任何硬件定时器,只靠 SysTick 的 tick 计数。

代码块

┌─────────────── 你的任务 ───────────────┐ │ osTimerStart() / osTimerStop() ... │ 只是往"命令队列"里投一条命令 └───────────────────┬────────────────────┘ ↓ 命令队列(configTIMER_QUEUE_LENGTH = 10) ┌───────────────────────────────────────┐ │ Tmr Svc 定时器服务任务 │ 优先级 configTIMER_TASK_PRIORITY = 2 │ (内核自动创建,栈 256 字) │ │ ① 处理命令:启动/停止/改周期/删除 │ │ ② 每个 tick 检查有没有定时器到期 │ │ ③ 到期 → 调用你的回调函数 │ ← 回调跑在这里! └───────────────────────────────────────┘

两种类型(osTimerType_t):

类型 枚举值 行为
单次 osTimerOnce 到期执行一次就自动停止(对象还在,可以再 Start)
周期 osTimerPeriodic 到期执行一次后自动重新计时,一直循环直到 Stop
1.2 一个周期的完整过程
代码块
①你调用 osTimerStart(定时器, 100)→ 投命令 tm; 周期 = 100 tick
② Tmr Svc 处理命令 → 定时器进入"活动"状态,记录到期时刻
③ 每个 tick:Tmr Svc 检查 → 到期?
④到期 → 调用你的回调函数(在 Tmr Svc 任务里执行!)
⑤周期定时器 → 自动重新计时;单次 → 自动停止
1.3 最关键的一个认知:回调跑在"任务"里,不是中断里(中断不要放时间长的处理)
这一点决定了回调里能写什么、不能写什么:

事实 后果
回调在 Tmr Svc 任务上下文执行 可以用 osThreadFlagsSet / xTaskNotifyGive / osMessageQueuePut 等任务版 API(不需要 FromISR)
回调阻塞 = 整个定时器系统停摆 回调里禁止 vTaskDelay/osDelay、禁止带超时等信号量、禁止 printf 大量输出、禁止死等
回调的栈来自 configTIMER_TASK_STACK_DEPTH 回调里放大数组会同时影响所有定时器
Tmr Svc 优先级只有 configTIMER_TASK_PRIORITY(本项目 2) 有高优先级任务忙的时候,回调会被推迟 → 定时器不保证硬实时精度。

1.4 依赖的配置(本项目已在 CubeMX 里配好)
宏 本项目值 作用
configUSE_TIMERS 1 总开关,为 0 时所有定时器 API 都不存在
configTIMER_TASK_PRIORITY 2 Tmr Svc 的优先级**(很低 → 容易被挤,可以调高)**
configTIMER_QUEUE_LENGTH 10 命令队列长度;同时启动/停止太多会失败
configTIMER_TASK_STACK_DEPTH 256(字) Tmr Svc 的栈,回调的局部变量都吃它
INCLUDE_xTimerPendFunctionCall 1 要用 xTimerPendFunctionCall 时必须开
成本提醒:只要 configUSE_TIMERS = 1,内核就会创建 Tmr Svc 任务 + 命令队列,即使你一个定时器都没用,也要吃掉约 1.3 KB 的 RTOS 堆(栈 1024 B + TCB + 队列)。对小 RAM 芯片(F103C8 只有 20KB SRAM)来说,不用定时器就把它关掉,能省一大块。

二、软件定时器的操作

操作 谁可以做 CMSIS 封装 FreeRTOS 原生

1 创建 任务 osTimerNew() xTimerCreate() / xTimerCreateStatic()
2 启动 任务 osTimerStart(定时器, ticks) xTimerStart()
2b 启动 中断 ❌ osErrorISR xTimerStartFromISR()
3 停止 任务 osTimerStop() xTimerStop()
3b 停止 中断 ❌ osErrorISR xTimerStopFromISR()
4 重启(Reset,重新计时) 任务 / 中断 ❌ 完全没有 xTimerReset() / xTimerResetFromISR()
5 改周期 任务 用 osTimerStart() 间接实现 xTimerChangePeriod()
5b 改周期 中断 ❌ xTimerChangePeriodFromISR()
6 查询是否在运行 任务 osTimerIsRunning() xTimerIsTimerActive()
7 取名字 任务 osTimerGetName() pcTimerGetName()
8 查周期 / 到期时刻 任务 ❌ xTimerGetPeriod() / xTimerGetExpiryTime()
9 删除 任务 osTimerDelete() xTimerDelete()
10 把任意函数投递给 Tmr Svc 执行 任务 / 中断 ❌ xTimerPendFunctionCall() / …FromISR()
三、软件定时器的函数(先 CMSIS,后原生)
3.1 创建
CMSIS 封装:osTimerNew()

osTimerId_t osTimerNew(osTimerFunc_t func, /* 回调函数/
osTimerType_t type, /
osTimerOnce / osTimerPeriodic */
voidargument, /传给回调的参数(也是 timer ID) */
const osTimerAttr_tattr);/NULL = 动态分配 */
回调函数签名固定:

void 回调函数(voidargument); /注意是 void*,不是 void */
返回:成功=句柄;失败(堆不够 / 回调为 NULL / 在中断里调用)=NULL。
必须在任务上下文创建(osTimerNew 里 if (!IS_IRQ()) 才干活,ISR 里直接返回 NULL)。
内部会额外 pvPortMalloc(sizeof(TimerCallback_t)) 存"回调 + 参数",所以 CMSIS 定时器比原生多一次 malloc(约多 16 字节)。

static osTimerId_t g_ledTimer;

void LED回调(voidargument)
{
/
注意:这里跑在 Tmr Svc 任务里,不能阻塞! */
}

void App_Timer_Init(void)
{
g_ledTimer = osTimerNew(LED回调, osTimerPeriodic, NULL, NULL);
if (g_ledTimer == NULL) { for(;😉; }
}

FreeRTOS 原生:xTimerCreate() / xTimerCreateStatic()

TimerHandle_t xTimerCreate(const char * const pcTimerName, /* 名字(调试用)/
const TickType_t xTimerPeriodInTicks, /
周期(tick)/
const UBaseType_t uxAutoReload, /
pdTRUE=周期 pdFALSE=单次/
void * const pvTimerID, /
传给回调的参数/
TimerCallbackFunction_t pxCallbackFunction); /
void (*)(TimerHandle_t) */
注意回调签名不一样!

回调签名
CMSIS void f(void *argument)
原生 void f(TimerHandle_t xTimer) ← 收到的是定时器句柄,参数要用 pvTimerGetTimerID() 取

/* 原生:回调里通过 pvTimerGetTimerID 拿回自己的参数 */
void LED回调原生(TimerHandle_t xTimer)
{
uint32_t id = (uint32_t)pvTimerGetTimerID(xTimer);
(void)id;
}

TimerHandle_t t = xTimerCreate(“led”, pdMS_TO_TICKS(500), pdTRUE, (void*)1, LED回调原生);
if (t == NULL) { for(;😉; }
结论
优先用 osTimerNew() —— 回调签名 void f(void*) 更简单,参数直接传(不用 pvTimerGetTimerID)。只有在完全不使用 CMSIS 层的工程里才用 xTimerCreate。

3.2 启动
CMSIS 封装:osTimerStart()

osStatus_t osTimerStart(osTimerId_t timer_id, uint32_t ticks); /* ticks,不是毫秒! */
返回:osOK / osErrorParameter(句柄 NULL)/ osErrorISR(在中断里调用) / osErrorResource(命令队列满)。
底层是 xTimerChangePeriod(hTimer, ticks, 0) —— 这意味着:
⚠️ 每次调用 osTimerStart() 都会把周期改成新值,而原生 xTimerStart() 不会改周期。 想要"按创建时的周期重新启动",CMSIS 必须重新传一遍周期;原生直接 xTimerStart(t, 0) 就行。

osTimerStart(g_ledTimer, pdMS_TO_TICKS(500)); /* 500 ms 周期,启动/
osTimerStart(g_ledTimer, pdMS_TO_TICKS(200)); /
再调一次 = 改成 200ms 并重启 */
FreeRTOS 原生:xTimerStart()

BaseType_t xTimerStart(TimerHandle_t xTimer, TickType_t xTicksToWait);
第 2 个参数不是定时器周期,而是"等命令队列有空位的最大 tick 数"(0 = 不等,队列满就返回 pdFAIL)。
若定时器已在运行,xTimerStart 等价于 xTimerReset(重新从 0 计时,周期不变)。
中断里启动:只能用原生

void xxx_IRQHandler(void)
{
BaseType_t woken = pdFALSE;
xTimerStartFromISR(g_timer, &woken); /* ★ 必须原生,CMSIS 在 ISR 里返回 osErrorISR */
portYIELD_FROM_ISR(woken);
}
3.3 停止

/* CMSIS/
osStatus_t osTimerStop(osTimerId_t timer_id);
/
返回 osErrorResource 表示"当前没在运行"(内部先 xTimerIsTimerActive 判断) */

/* 原生(任务) */
BaseType_t xTimerStop(TimerHandle_t xTimer, TickType_t xTicksToWait);

/* 原生(中断)★ */
BaseType_t xTimerStopFromISR(TimerHandle_t xTimer, BaseType_t *pxHigherPriorityTaskWoken);
3.4 重启(Reset):CMSIS 没有,必须原生
xTimerReset = 把定时器重新从 0 开始计时(周期不变),不管它当前是否在运行。 这是"软件看门狗"、按键消抖、"多久没动作就超时"这类逻辑的标准工具。

/* 原生,任务里 */
#include “timers.h”
xTimerReset(g_timer, 0);

/* 原生,中断里(★ 只有原生才有 FromISR 版) */
BaseType_t woken = pdFALSE;
xTimerResetFromISR(g_timer, &woken);
portYIELD_FROM_ISR(woken);
典型用法:“数据来了就喂一次狗”

/* 创建一个 1 秒的单次定时器,回调里报"超时" */
g_linkTimer = osTimerNew(链路超时回调, osTimerOnce, NULL, NULL);
osTimerStart(g_linkTimer, pdMS_TO_TICKS(1000));

/* 每收到一个字节就重置它 —— CMSIS 做不到,必须原生 xTimerReset/
void 收到字节(void)
{
xTimerReset(g_linkTimer, 0); /
★ 必须原生 */
}
3.5 改周期

/* CMSIS:没有独立函数,靠 osTimerStart(它内部就是 ChangePeriod) */
osTimerStart(g_timer, pdMS_TO_TICKS(250));

/* 原生(任务) */
xTimerChangePeriod(g_timer, pdMS_TO_TICKS(250), 0);

/* 原生(中断)★ */
xTimerChangePeriodFromISR(g_timer, pdMS_TO_TICKS(250), &woken);
3.6 查询

/* 是否在运行/
osTimerIsRunning(g_timer); /
CMSIS;ISR 里返回 0/
xTimerIsTimerActive(g_timer); /
原生 */

/* 名字/
osTimerGetName(g_timer); /
CMSIS/
pcTimerGetName(g_timer); /
原生 */

/* 周期与到期时刻(只有原生有)/
TickType_t period = xTimerGetPeriod(g_timer);
TickType_t expiry = xTimerGetExpiryTime(g_timer); /
到期时对应的 tick 值 */
3.7 删除

osTimerDelete(g_timer); /* CMSIS;ISR 里返回 osErrorISR/
xTimerDelete(g_timer, 0); /
原生;第 2 参数是等命令队列的 tick */
不要在定时器自己的回调里删除自己 —— 回调正跑在 Tmr Svc 里,删除后对象内存被回收,行为很难保证。稳妥做法:回调里置个标志/发条通知,让普通任务去删。

3.8 把任意函数"投递"给 Tmr Svc 执行(CMSIS 没有)
这是软件定时器附赠的一个很有用的机制:在中断里想干点"必须在任务上下文做、但又不值得单独开任务"的活,可以投给 Tmr Svc。

/* 原型:void 函数(void *参数1, uint32_t 参数2); */
#include “timers.h”

void 我的任务函数(voidp1, uint32_t p2) { /在 Tmr Svc 里执行 */ }

/* 任务里投递 */
xTimerPendFunctionCall(我的任务函数, 参数1, 参数2, 0);

/* 中断里投递 */
BaseType_t woken = pdFALSE;
xTimerPendFunctionCallFromISR(我的任务函数, 参数1, 参数2, &woken);
portYIELD_FROM_ISR(woken);
依赖 INCLUDE_xTimerPendFunctionCall = 1(本项目已开)。 ⚠️ 同样的禁忌:这个函数也跑在 Tmr Svc 里,不能阻塞。

能力 CMSIS 必须原生

1 创建(动态/静态) ✅ osTimerNew —
2 启动(任务里) ✅ osTimerStart —
3 停止(任务里) ✅ osTimerStop —
4 删除 ✅ osTimerDelete —
5 查询是否运行 / 名字 ✅ —
6 在中断里启动/停止定时器 ❌ 全部 osErrorISR xTimerStartFromISR / xTimerStopFromISR
7 重启(Reset,重新计时) ❌ 完全没有 xTimerReset / xTimerResetFromISR
8 在中断里改周期 ❌ xTimerChangePeriodFromISR
9 查周期 / 到期时刻 ❌ xTimerGetPeriod / xTimerGetExpiryTime
10 把函数投递给 Tmr Svc ❌ xTimerPendFunctionCall / …FromISR
一句话:CMSIS 能覆盖"任务里创建/启动/停止/删除"这条最常用的主干;但只要涉及"中断里改定时器"和"重新计时(Reset)",就必须原生。

五、三个角色怎么写(主函数 / 回调 / 消费者任务)

软件定时器没有"生产者任务","生产者"就是定时器回调(它产生"时间到了"这个事件)。

角色 谁 干什么 调什么
主函数 / 初始化 MX_FREERTOS_Init() 的 USER CODE BEGIN RTOS_TIMERS创建定时器 + 启动它 osTimerNew() / osTimerStart()
生产者定时器回调函数 只做"通知",把活交给任务osThreadFlagsSet() / xTaskNotifyGive() / osMessageQueuePut()
消费者普通任务 等通知→ 干活 osThreadFlagsWait() / ulTaskNotifyTake() / osMessageQueueGet()
5.1 主函数(初始化)——写在这里
CubeMX 已经给你留好了段:

void MX_FREERTOS_Init(void) {
/* USER CODE BEGIN RTOS_MUTEX/
/
USER CODE END RTOS_MUTEX/
/
USER CODE BEGIN RTOS_SEMAPHORES/
/
USER CODE END RTOS_SEMAPHORES */

/* USER CODE BEGIN RTOS_TIMERS/
/
start timers, add new ones, …/ ← ★ 定时器就写在这里
/
USER CODE END RTOS_TIMERS */

/* USER CODE BEGIN RTOS_QUEUES/
/
USER CODE END RTOS_QUEUES */
…
}

/* USER CODE BEGIN RTOS_TIMERS/
/
1. 先建消费者任务,拿到句柄(回调里要用它发通知) */
g_ledTaskHandle = osThreadNew(LEDTask, NULL, &LEDTask_attributes);
if (g_ledTaskHandle == NULL) { for(;😉; }

/* 2. 创建定时器 */
g_ledTimer = osTimerNew(LED定时回调, osTimerPeriodic, NULL, NULL);
if (g_ledTimer == NULL) { for(;😉; }

/* 3. 启动定时器(ticks 单位,用 pdMS_TO_TICKS 换算)/
osTimerStart(g_ledTimer, pdMS_TO_TICKS(500));
/
USER CODE END RTOS_TIMERS */
顺序建议:先建任务拿句柄 → 再创建定时器 → 最后启动。这样回调第一次触发时,句柄已经有效。

5.2 生产者:定时器回调(只做通知,绝不干活)

/* 回调跑在 Tmr Svc 任务里:不能阻塞、不能延时、不能 printf 大输出 */
static void LED定时回调(void *argument)
{
(void)argument;

/* 只做一件事:通知消费者(这里用 CMSIS 的任务通知,任务版 API 可用) */ if (g_ledTaskHandle != NULL) { osThreadFlagsSet(g_ledTaskHandle, (1UL << 0)); /* 置 bit0 */ } /* ❌ 不要在这里写:osDelay()、带超时等信号量、printf、长计算、死循环 */

}
5.3 消费者:普通任务(等通知 → 干活)
static void LEDTask(void *argument)
{
(void)argument;

for (;;) { /* 等定时器回调的通知;默认自动清除 */ uint32_t f = osThreadFlagsWait((1UL << 0), osFlagsWaitAny, osWaitForever); if (f & osFlagsError) { continue; } HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0); /* 真正的活儿放在任务里 */ }

}
为什么要"回调 → 通知任务"这么绕? 因为回调里不能阻塞;
而很多活(写 Flash、发串口、算滤波)都需要阻塞或耗时长。回调只负责"敲钟",任务负责"干活",这是软件定时器的标准用法。

5.4 完整可抄代码
/* ===================== app_timer.h ===================== */
#ifndef APP_TIMER_H
#define APP_TIMER_H

#include “cmsis_os.h”

#define FLAG_LED_TICK (1UL << 0)

extern osTimerId_t g_ledTimer;
extern osThreadId_t g_ledTaskHandle;

void App_Timer_Init(void);

#endif
/* ===================== app_timer.c =====================/
#include “app_timer.h”
#include “timers.h” /
★ xTimerReset 等原生 API */

osTimerId_t g_ledTimer = NULL;
osThreadId_t g_ledTaskHandle = NULL;

/* ---------- 消费者任务 ---------- */
static void LEDTask(void *argument)
{
(void)argument;

for (;;) { uint32_t f = osThreadFlagsWait(FLAG_LED_TICK, osFlagsWaitAny, osWaitForever); if (f & osFlagsError) { continue; } HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0); /* 慢活、阻塞活都放这里 */ }

}

/* ---------- 生产者:定时器回调 ---------- */
static void LED定时回调(voidargument)
{
(void)argument;
if (g_ledTaskHandle != NULL)
{
osThreadFlagsSet(g_ledTaskHandle, FLAG_LED_TICK); /
只敲钟 */
}
}

/* ---------- 单次定时器示例:超时保护 ---------- */
static osTimerId_t g_timeoutTimer = NULL;

static void 超时回调(voidargument)
{
(void)argument;
if (g_ledTaskHandle != NULL)
{
osThreadFlagsSet(g_ledTaskHandle, (1UL << 1)); /
bit1 = 超时 */
}
}

/* 外部每收到一个字节就调用一次(喂狗),★ 必须原生/
void App_Feed_Watchdog(void)
{
if (g_timeoutTimer != NULL)
{
xTimerReset(g_timeoutTimer, 0); /
CMSIS 没有这个函数 */
}
}

/* ---------- 初始化 ---------- */
void App_Timer_Init(void)
{
static const osThreadAttr_t LED属性 = {
.name = “LEDTask”, .stack_size = 128 * 4,
.priority = (osPriority_t)osPriorityNormal,
};

/* ① 先建任务,拿句柄 */ g_ledTaskHandle = osThreadNew(LEDTask, NULL, &LED属性); if (g_ledTaskHandle == NULL) { for(;;); } /* ② 周期定时器 → 每 500ms 敲一次钟 */ g_ledTimer = osTimerNew(LED定时回调, osTimerPeriodic, NULL, NULL); if (g_ledTimer == NULL) { for(;;); } osTimerStart(g_ledTimer, pdMS_TO_TICKS(500)); /* ③ 单次定时器 → 1 秒没喂狗就报超时 */ g_timeoutTimer = osTimerNew(超时回调, osTimerOnce, NULL, NULL); if (g_timeoutTimer == NULL) { for(;;); } osTimerStart(g_timeoutTimer, pdMS_TO_TICKS(1000));

}
/* ===================== freertos.c =====================/
void MX_FREERTOS_Init(void)
{
/
… CubeMX 生成 …/
/
USER CODE BEGIN RTOS_TIMERS/
App_Timer_Init(); /
← 只加这一行/
/
USER CODE END RTOS_TIMERS */
}
5.5 在中断里启动/重启定时器(必须原生)
c

/* 场景:外部中断来了,才开始 5 秒超时计时 */
void EXTI0_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);

BaseType_t woken = pdFALSE; /* ★ CMSIS 的 osTimerStart/osTimerStop 在中断里返回 osErrorISR,只能用原生 */ xTimerStartFromISR(g_timeoutTimer, &woken); /* 若只是"重新计时":xTimerResetFromISR(g_timeoutTimer, &woken); */ portYIELD_FROM_ISR(woken);

}

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/29 11:08:05

HDMI 无线传输:会议摄像头高清信号无线延伸方案实测(易视讯)

在会议室音视频项目调试中&#xff0c;经常会遇到 HDMI 高清信号远距离布线难题。很多 PTZ 会议摄像头输出 HDMI 信号&#xff0c;但主机、矩阵、采集端和摄像头距离较远&#xff0c;长距离 HDMI 线缆穿管、走线槽施工麻烦&#xff0c;已经装修完成的会场改造更是困难。HDMI 无…

作者头像 李华
网站建设 2026/10/29 11:07:52

Windows 电脑上安装 Docker Desktop

1、下载地址 下载地址:https://www.docker.com/products/docker-desktop/ 2、安装步骤 针对 x64 处理器的 Windows 电脑,选择下载 “Download for Windows - AMD64” 。 点击下载的 “Docker Desktop Installer.exe”,会按默认路径进行安装,默认路径是安装在C盘,没办法手…

作者头像 李华
网站建设 2026/10/29 11:07:07

AI时代,程序员如何从执行者转型?收藏这份保命指南!

春招数据显示传统开发岗位需求萎缩&#xff0c;AI相关岗位需求激增&#xff0c;但并非所有AI岗位都能受益。文章指出&#xff0c;初级开发、测试等执行型岗位面临淘汰风险&#xff0c;而定义问题、掌握核心技能的AI岗位则更受青睐。建议程序员从“执行者”向“定义者”转型&…

作者头像 李华
网站建设 2026/10/29 11:06:48

给问数平台开一个只读账号,我是这么把这条线焊死的

不替换原系统&#xff0c;只加问数入口&#xff0c;这句话落地靠一条底线&#xff0c;问数平台连的是一个只读账号。原系统照常记房源、记合同、记账单&#xff0c;问数平台连进去只能看&#xff0c;改不了也删不了。把这条线怎么焊死的过程记下来&#xff0c;给同样在接园区的…

作者头像 李华
网站建设 2026/10/29 11:06:45

learn-claude-code为什么会反复压缩上下文?一次已合并的 PR 实践

我给一个接近 8 万 Star 的开源项目提交了 PR&#xff0c;最近已经通过审核并合并&#xff01; 这个项目就是最近爆火的 learn-claude-code&#xff0c; 这次我主要对他的上下文压缩模块的snip_compact函数进行了修改&#xff0c;引入滞回阈值&#xff0c;给压缩操作留出缓冲&…

作者头像 李华