前面我们已经用链表和队列构建了异步事件的骨架。但在协作式微内核中,最大的禁忌就是阻塞死等。软件定时器的核心使命,就是彻底消灭 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 指针的关键
typedef void(*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; // 存放所有正在运行的定时器
// 系统初始化时调用
void SoftTimer_Init() {
List_Init(&active_timers);
}
// 获取当前系统滴答 (STM32中通常封装 HAL_GetTick)
static uint32_t Get_System_Tick() {
return HAL_GetTick(); // 假设 1ms 一个 tick
}
2.2 启动与停止 (Start / Stop)
这就是面向对象的接口设计。用户只需要操作句柄。
// 启动定时器
void SoftTimer_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 必须 > 0
if (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);
}
// 停止定时器
void SoftTimer_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. 定时器回调
// 注意:这里我们演示“回调产生事件”的模式
void OnLogTimer(void* ctx){
// 上下文 ctx 可以传任何东西,这里演示传一个 ID
uint32_t timer_id = (uint32_t)ctx;
// 发送一个超时事件到队列
Event_Post(SIG_TIMEOUT, timer_id);
}
// 3. 之前的事件分发器
void Dispatch_Event(Event* e){
switch (e->sig) {
case SIG_TIMEOUT:
// 真正的业务逻辑在这里处理
printf("Timer %d Timeout! System Running...\n", e->param);
break;
// ... 其他 case
}
}
int main(){
HAL_Init();
SoftTimer_Init(); // 初始化定时器链表
// 4. 启动定时器:500ms,重复,回调是 OnLogTimer,参数是 1
SoftTimer_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,没有乱七八糟的标志位判断。
这种基于微内核和事件驱动的设计思路,为构建更复杂、响应更及时的嵌入式系统打下了坚实基础。如果你想了解更多关于操作系统内核设计的底层逻辑,或者探讨如何将其应用于具体项目,欢迎在云栈社区的嵌入式版块交流讨论。