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 到期执行一次后自动重新计时一直循环直到 Stop1.2 一个周期的完整过程代码块①你调用 osTimerStart(定时器, 100)→ 投命令 tm; 周期 100 tick② Tmr Svc 处理命令 → 定时器进入活动状态记录到期时刻③ 每个 tickTmr 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 重启ResetCMSIS 没有必须原生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); /CMSISISR 里返回 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); /* CMSISISR 里返回 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 / xTimerStopFromISR7 重启Reset重新计时 ❌ 完全没有 xTimerReset / xTimerResetFromISR8 在中断里改周期 ❌ xTimerChangePeriodFromISR9 查周期 / 到期时刻 ❌ xTimerGetPeriod / xTimerGetExpiryTime10 把函数投递给 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);}