【第17期】裸机时间管理:SysTick与时间片轮询

核心目标:彻底抛弃阻塞式延时(Blocking Delay),构建**非阻塞(Non-blocking)**的协作式调度架构。

1. 为什么 Delay 是万恶之源?

先看一段典型的“代码”:

while (1) {
    HAL_GPIO_TogglePin(LED_PORT, LED_PIN);
    HAL_Delay(500);  // 罪魁祸首!
    
    if (HAL_GPIO_ReadPin(BTN_PORT, BTN_PIN) == 0) {
        // 处理按键...
    }
}

问题在哪里? HAL_Delay(500) 的本质是一个死循环。CPU 就像被点穴了一样,在那儿傻等 500 毫秒。 在这 500 毫秒里:

  • 用户按了按钮?检测不到(除非用中断,但中断不能做复杂逻辑)。

  • 串口来了数据?没空处理

  • 屏幕需要刷新?卡住不动

这就叫阻塞(Blocking)。只要系统里有一个这样的 Delay,整个系统的实时性就完了。


2. 解药:SysTick (系统滴答定时器)

要实现“非阻塞”,我们需要一个独立于主程序的时钟基准。ARM Cortex-M 内核自带了一个神级外设——SysTick

  • 身份:它是内核的一部分,不是芯片厂商的外设。这意味着你的代码在 STM32、GD32、NXP 上都能通用。

  • 原理:它是一个 24 位的倒计数器。通常我们会把它配置为每 1 毫秒产生一次中断。

  • 心跳变量:在 SysTick 的中断服务函数(ISR)里,我们维护一个全局变量 g_sys_tick

// 定义一个全局心跳计数器 (必须加 volatile)
volatile uint32_t g_sys_tick = 0;

// SysTick 中断服务函数 (每 1ms 触发一次)
void SysTick_Handler(void) {
    g_sys_tick++; 
}

// 获取当前系统时间的函数
uint32_t GetTick(void) {
    return g_sys_tick;
}

有了这个不断增长的 g_sys_tick,CPU 就不需要“傻等”了,它只需要“看表”。


3. 时间片轮询 (Time-Slicing Polling) 架构

我们可以把 while(1) 变成一个轮询调度器。每个任务都去检查:“现在时间到了吗?”

  • 如果到了,干活,更新下次时间。

  • 如果没到,立马退出,把 CPU 让给下一个任务

改造后的非阻塞代码:

// 定义任务的刷新间隔
#define LED_TASK_INTERVAL  500
#define KEY_TASK_INTERVAL  10

// 记录上次执行的时间点
uint32_t last_led_time = 0;
uint32_t last_key_time = 0;

while (1) {
    // ---------------------------------------
    // 任务 1:LED 闪烁 (每 500ms 执行一次)
    // ---------------------------------------
    if (GetTick() - last_led_time >= LED_TASK_INTERVAL) {
        // 1. 更新时间戳 (核心!)
        last_led_time = GetTick();
        
        // 2. 执行非阻塞业务
        HAL_GPIO_TogglePin(LED_PORT, LED_PIN);
    }

    // ---------------------------------------
    // 任务 2:按键扫描 (每 10ms 执行一次)
    // ---------------------------------------
    if (GetTick() - last_key_time >= KEY_TASK_INTERVAL) {
        last_key_time = GetTick();
        
        // 扫描按键,消抖逻辑也不用 Delay 了
        Scan_Key_NonBlocking(); 
    }
    
    // ... 可以无限添加任务 3, 4, 5 ...
}
 

效果: CPU 会以极快的速度在这些 if 之间空转。一旦某个任务时间到了,就进去执行一下。所有任务看起来就像是“同时”在运行!


4. 关键技术细节:如何处理计数器溢出?

有的细心工程师会问:

g_sys_tickuint32_t,大约 49 天后会溢出(从 4294967295 变成 0)。 那 GetTick() - last_time 这种减法会不会算错?

答案是:完全不会错,只要你用无符号减法。

这是 C 语言中模运算(Modulo Arithmetic)的神奇之处。

举例验证(用 8 位数简化模拟):

  • uint8_t 最大值是 255。

  • 假设 last_time = 250。

  • 现在时间流逝了 10ms,current_time 溢出变成了 4 (250 + 10 = 260 -> 260 % 256 = 4)。

  • 我们算算间隔:current - last = 4 - 250

在无符号二进制运算中:

  • 40000 0100

  • 2501111 1010

  • 做减法(借位被忽略),结果是 0000 1010,也就是 10

结论:不管 g_sys_tick 溢出多少圈,只要任务间隔小于 49 天,Current - Last >= Interval 这个公式永远成立。


5. 这种架构的局限性

虽然“时间片轮询”比“死延时”强了一万倍,但它依然是裸机系统,有两个致命弱点:

  1. 由于是串行执行,只要有一个任务卡死,所有任务都死。

    • 如果你在 LED 任务里偷偷写了个 while 死循环,按键任务也就永远得不到执行了。

  2. 实时性抖动

    • 如果 LED 任务特别繁重,耗时 20ms,那按键任务的检测就会被迫推迟 20ms。这在电机控制等高精度场景下是不可接受的。


归纳下本章关键点

  1. SysTick 是裸机的心跳。

  2. GetTick() - Last >= Interval 是裸机多任务的黄金公式。

  3. 无符号减法 自动解决了溢出回绕问题,不需要额外处理。

  4. 这种架构被称为 协作式调度 (Cooperative Scheduling) —— 任务之间互相礼让,谁也不占着茅坑不拉屎。

至此,并发与中断结束。 你已经掌握了裸机开发的最高阶形态。但正如刚才所说,这种架构在复杂系统下依然力不从心(特别是当某个任务必须耗时很久时)。

如何让任务之间既能并发,又能随时打断,还能自动挂起等待?

评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值