【微内核 第03章】软件定时器 (SoftTimer)
前言
前面我们已经用链表和队列构建了异步事件的骨架。但在协作式微内核中,最大的禁忌就是阻塞死等。软件定时器的核心使命,就是彻底消灭 delay。它将所有的“延时”和“周期性动作”转化为非阻塞的“时间事件”,通过异步回调或发布事件的方式,实现多任务的周期性调度与超时控制。
很多人刚开始写单片机代码,最喜欢用 HAL_Delay(1000)。
-
后果:CPU 在这 1 秒内是在“发呆”,如果有串口数据来了,或者有紧急按键,系统完全没反应。这就是阻塞(Blocking)。
这一章,我们要实现第三章:软件定时器 (SoftTimer) —— 给系统装上心跳。
我们要达到的目标是:在整个工程里,禁止出现任何 Delay 函数。
1. 核心设计思想:定时器即对象
回顾“句柄到对象”的思路。定时器不应该是一个全局数组里的索引(比如 Timer[0]),而应该是一个独立的对象。谁需要定时,谁就自己定义一个定时器对象。
1.1 定义定时器对象
我们在 framework_timer.h 中定义:
#include"utils_list.h"#include<stdint.h>#include<stdbool.h>// 1. 定义回调函数原型// context: 又是它!解决 C 语言没有 this 指针的关键typedefvoid(*TimerCallback)(void* context);// 2. 定时器对象typedef struct {// 【继承】侵入式链表节点// 只要把这个结构体挂到链表上,它就“活”了ListNode node;// 属性uint32_t timeout_tick; // 到期的时间点 (Absolute Time)uint32_t interval; // 周期时间 (0 表示单次)bool is_running; // 运行状态// 行为TimerCallback cb; // 时间到了干什么?void* context; // 传给回调的参数} SoftTimer;// 句柄定义typedef SoftTimer* SoftTimerHandle;
2. 核心逻辑实现 (The Manager)
2.1 初始化与基准时间
在 framework_timer.c 中:
static List active_timers; // 存放所有正在运行的定时器// 系统初始化时调用voidSoftTimer_Init() {List_Init(&active_timers);}// 获取当前系统滴答 (STM32中通常封装 HAL_GetTick)static uint32_t Get_System_Tick() {return HAL_GetTick(); // 假设 1ms 一个 tick}
2.2 启动与停止 (Start / Stop)
这就是面向对象的接口设计。用户只需要操作句柄。
// 启动定时器voidSoftTimer_Start(SoftTimerHandle h, uint32_t ms, bool repeat, TimerCallback cb, void* ctx){// 1. 如果已经在运行,先从链表移除,避免重复添加破坏链表结构SoftTimer_Stop(h);// 2. 填充属性h->interval = ms;h->timeout_tick = Get_System_Tick() + ms; // 计算绝对到期时间h->cb = cb;h->context = ctx;// 3. 标记重复模式(如果 interval > 0 也可以认为重复,看具体设计)// 这里为了逻辑清晰,如果 repeat 为 true,则 interval 必须 > 0if (repeat && h->interval == 0) h->interval = 1;h->interval = repeat ? h->interval : 0;h->is_running = true;// 4. 【关键】加入链表// 这里简单起见插到队尾。// 进阶优化:可以按 timeout_tick 排序插入,这样检查时只要看第一个,效率更高。List_AddTail(&active_timers, &h->node);}// 停止定时器voidSoftTimer_Stop(SoftTimerHandle h){if (h->is_running) {List_Del(&h->node); // 利用侵入式链表,O(1) 删除h->is_running = false;}}
2.3 心跳驱动 (Update)
这是整个定时器系统的引擎。它需要在主循环中被频繁调用。
注意点:在遍历链表时删除节点(Callback 可能会调用 Stop)是危险的。所以我们要用安全的遍历方法。
// 放在主循环里调用void SoftTimer_Update() {uint32_t current_tick = Get_System_Tick();ListNodeHandle pos, next;SoftTimerHandle timer;// 使用 safe 模式遍历:在处理当前节点前,先保存 next 指针// 这样即使当前节点被删除了,循环也能继续for (pos = active_timers.head.next, next = pos->next;pos != &active_timers.head;pos = next, next = pos->next) {// 还原对象指针timer = container_of(pos, SoftTimer, node);// 检查是否到期 (这里忽略了 49 天溢出翻转问题,实际工程需处理)if (current_tick >= timer->timeout_tick) {// 1. 执行回调if (timer->cb) {timer->cb(timer->context);}// 2. 处理周期性if (timer->interval > 0) {// 如果是周期的,更新下一次到期时间timer->timeout_tick += timer->interval;} else {// 如果是单次的,停止它(从链表移除)SoftTimer_Stop(timer);}}}}
3. 定时器 + 事件系统
现在,我们把第三章(定时器)和第二章(事件)结合起来。
最佳实践:不要在 SoftTimer 的回调函数里做繁重的业务逻辑,也不要直接操作硬件(除非是极其简单的 LED 翻转)。推荐模式:定时器回调只负责 Event_Post。
实战演示:每 500ms 打印一次日志
// 1. 定义定时器对象 (放在全局或静态区,保证生命周期)static SoftTimer log_timer;// 2. 定时器回调// 注意:这里我们演示“回调产生事件”的模式voidOnLogTimer(void* ctx){// 上下文 ctx 可以传任何东西,这里演示传一个 IDuint32_t timer_id = (uint32_t)ctx;// 发送一个超时事件到队列Event_Post(SIG_TIMEOUT, timer_id);}// 3. 之前的事件分发器voidDispatch_Event(Event* e){switch (e->sig) {case SIG_TIMEOUT:// 真正的业务逻辑在这里处理printf("Timer %d Timeout! System Running...\n", e->param);break;// ... 其他 case}}intmain(){HAL_Init();SoftTimer_Init(); // 初始化定时器链表// 4. 启动定时器:500ms,重复,回调是 OnLogTimer,参数是 1SoftTimer_Start(&log_timer, 500, true, OnLogTimer, (void*)1);// 进入主循环while (1) {// 驱动定时器 (检查有没有到期的)SoftTimer_Update();// 驱动事件 (检查有没有新事件)Event e;if (Event_Get(&e)) {Dispatch_Event(&e);}}}
4. 进阶:为什么要这么麻烦?
你可能会问:“我直接在 TimerCallback 里 printf 不行吗?为什么要 Post Event 绕一圈?”
原因主要因为:
-
栈深度与并发安全:虽然
SoftTimer_Update在 Main 里跑,但如果你的业务逻辑很复杂(比如写 Flash、复杂的计算),直接在 Callback 里做会卡住SoftTimer_Update的循环,导致后续的定时器全部延迟。 -
统一入口:通过
Event_Post,所有的业务逻辑都汇聚到了Dispatch_Event。哪怕以后你把SoftTimer_Update移到了一个高优先级的定时器中断里去跑(为了更高精度),你的业务逻辑代码(printf)一行都不用改,因为它依然是在主循环处理事件。这就是解耦!
5. 本章总结
现在,我们的微内核框架已经具备了雏形:
-
数据层:
ListNode,RingBuffer。 -
通信层:
Event_Post,Event_Get。 -
时间层:
SoftTimer,SoftTimer_Update。
你的主循环现在看起来非常干净:
while(1) {SoftTimer_Update(); // 心跳if(Event_Get(&e)) Dispatch(&e); // 大脑}
没有 HAL_Delay,没有乱七八糟的标志位判断。
往期精选
【第09期】C语言的嵌入式特化 (三) —— 函数指针与回调 (Callback):解耦神器
C语言从句柄到对象 (二) —— 极致的封装:不透明指针与 SDK 级设计
/******************************************
* 这是一场主动降频后,回归初心的思考与整理,对过往的嵌入式经验重新校准与复盘。
* 内容以专题系列文的形式持续更新,声明仅代表个人理解与思考,欢迎留言、讨论、补充,共同进步!
* 觉得有共鸣,关注我!并设为星标⭐,欢迎转发分享❤️!让更多人看见!
******************************************/
夜雨聆风