核心目标:彻底抛弃阻塞式延时(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_tick是uint32_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。
在无符号二进制运算中:
-
4是0000 0100 -
250是1111 1010 -
做减法(借位被忽略),结果是
0000 1010,也就是 10。
结论:不管 g_sys_tick 溢出多少圈,只要任务间隔小于 49 天,Current - Last >= Interval 这个公式永远成立。
5. 这种架构的局限性
虽然“时间片轮询”比“死延时”强了一万倍,但它依然是裸机系统,有两个致命弱点:
-
由于是串行执行,只要有一个任务卡死,所有任务都死。
-
如果你在 LED 任务里偷偷写了个
while死循环,按键任务也就永远得不到执行了。
-
-
实时性抖动。
-
如果 LED 任务特别繁重,耗时 20ms,那按键任务的检测就会被迫推迟 20ms。这在电机控制等高精度场景下是不可接受的。
-
归纳下本章关键点
-
SysTick 是裸机的心跳。
-
GetTick() - Last >= Interval 是裸机多任务的黄金公式。
-
无符号减法 自动解决了溢出回绕问题,不需要额外处理。
-
这种架构被称为 协作式调度 (Cooperative Scheduling) —— 任务之间互相礼让,谁也不占着茅坑不拉屎。
至此,并发与中断结束。 你已经掌握了裸机开发的最高阶形态。但正如刚才所说,这种架构在复杂系统下依然力不从心(特别是当某个任务必须耗时很久时)。
如何让任务之间既能并发,又能随时打断,还能自动挂起等待?
1190

被折叠的 条评论
为什么被折叠?



