乐于分享
好东西不私藏

【微内核 第03章】软件定时器 (SoftTimer)

【微内核 第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 必须 > 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);}// 停止定时器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 posnext;    SoftTimerHandle timer;    // 使用 safe 模式遍历:在处理当前节点前,先保存 next 指针    // 这样即使当前节点被删除了,循环也能继续    for (pos = active_timers.head.next, next = pos->next         pos != &active_timers.head;          pos = nextnext = 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 可以传任何东西,这里演示传一个 ID    uint32_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,参数是 1    SoftTimer_Start(&log_timer, 500true, OnLogTimer, (void*)1);    // 进入主循环    while (1) {        // 驱动定时器 (检查有没有到期的)        SoftTimer_Update();        // 驱动事件 (检查有没有新事件)        Event e;        if (Event_Get(&e)) {            Dispatch_Event(&e);        }    }}

4. 进阶:为什么要这么麻烦?

你可能会问:“我直接在 TimerCallback 里 printf 不行吗?为什么要 Post Event 绕一圈?”

原因主要因为

  1. 栈深度与并发安全:虽然 SoftTimer_Update 在 Main 里跑,但如果你的业务逻辑很复杂(比如写 Flash、复杂的计算),直接在 Callback 里做会卡住 SoftTimer_Update 的循环,导致后续的定时器全部延迟。

  2. 统一入口:通过 Event_Post,所有的业务逻辑都汇聚到了 Dispatch_Event。哪怕以后你把 SoftTimer_Update 移到了一个高优先级的定时器中断里去跑(为了更高精度),你的业务逻辑代码(printf)一行都不用改,因为它依然是在主循环处理事件。这就是解耦!

5. 本章总结

现在,我们的微内核框架已经具备了雏形:

  1. 数据层ListNodeRingBuffer

  2. 通信层Event_PostEvent_Get

  3. 时间层SoftTimerSoftTimer_Update

你的主循环现在看起来非常干净:

while(1) {    SoftTimer_Update(); // 心跳    if(Event_Get(&e)) Dispatch(&e); // 大脑}

没有 HAL_Delay,没有乱七八糟的标志位判断。

往期精选

【第09期】C语言的嵌入式特化 (三) —— 函数指针与回调 (Callback):解耦神器

【第33期】协议设计:帧头、帧尾与转义(Stuffing)

第40期:Bootloader(二):跳转App

C语言从句柄到对象 (二) —— 极致的封装:不透明指针与 SDK 级设计

/******************************************

 * 这是一场主动降频后,回归初心的思考与整理,对过往的嵌入式经验重新校准与复盘。

 * 内容以专题系列文的形式持续更新,声明仅代表个人理解与思考,欢迎留言、讨论、补充,共同进步!

 * 觉得有共鸣关注我!并设为星标⭐,欢迎转发分享❤️!让更多人看见!

******************************************/

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » 【微内核 第03章】软件定时器 (SoftTimer)

猜你喜欢

  • 暂无文章