前言:回想一下你写的第一行点灯代码,是不是这样的:点亮 LED ->delay_ms(1000)-> 熄灭 LED ->delay_ms(1000)。 这看起来毫无问题。但是,随着项目变大,你需要同时闪烁 LED、扫描按键、并在 OLED 屏上刷动画。这时你突然发现:动画卡成了 PPT,按键按下去经常没反应!
为什么?因为delay_ms()是阻塞式(Blocking)的。当单片机执行delay_ms(1000)时,CPU 实际上是在原地疯狂执行while(1)的空循环,长达 1 秒钟。在这 1 秒内,CPU 变成了“瞎子”和“聋子”,什么事都干不了。
想要让单片机同时干多件事(并发),你必须抛弃延时,拥抱“状态机(FSM)”与“时间戳”机制!
一、 降维打击:用“时间戳”取代“死等”
生活中的例子:假设你要烤一个披萨(需要 15 分钟),同时还要回一封邮件。
阻塞式(delay):你把披萨放进烤箱,然后搬个小板凳坐在烤箱前死死盯着(死等 15 分钟),披萨烤好后,你再去回邮件。这太蠢了。
非阻塞式(时间戳):你把披萨放进烤箱,看一眼手表(记录当前时间戳)。然后你回到了电脑前开始写邮件。每写两句话,你就抬头看一眼手表,如果过了 15 分钟,就去拿披萨;没到时间,就继续写邮件。
在单片机中,这个“手表”就是SysTick(滴答定时器)提供的全局变量,比如uint32_t uwTick(HAL 库自带)。
❌ 菜鸟的死等代码:
// 极其糟糕的写法,导致整个大循环卡顿 void LED_Task(void) { LED_ON(); delay_ms(500); // CPU 罢工 0.5 秒 LED_OFF(); delay_ms(500); // CPU 罢工 0.5 秒 }✅ 老鸟的非阻塞代码:
void LED_Task_NonBlock(void) { static uint32_t last_time = 0; // 记录上一次翻转的时间 // 获取当前手表时间,看看有没有过 500ms? if (HAL_GetTick() - last_time >= 500) { LED_TOGGLE(); // 翻转 LED last_time = HAL_GetTick(); // 重新对表 } // 如果没到 500ms,函数瞬间执行完毕并退出,绝不占用 CPU! }二、 核心杀器:有限状态机(FSM)
时间戳解决了简单的延时问题,但如果是一个多步骤的复杂流程呢? 比如操作一个超声波模块:1. 发送触发信号(拉高 10us);2. 等待回波引脚变高;3. 等待回波引脚变低并计算时间。 如果用死等,整个单片机都会被这个模块拖死。
这时候,我们需要引入状态机(State Machine)。状态机的核心是:每次进入函数,只干当前状态该干的一点点小事,干完立刻退出交出 CPU ;下次再进来时,根据状态标志位,接着往下干。
【干货实战:超声波非阻塞状态机】
typedef enum { STATE_IDLE = 0, // 空闲状态 STATE_TRIGGER, // 触发状态 STATE_WAIT_ECHO_H, // 等待高电平 STATE_WAIT_ECHO_L // 等待低电平 } SonicState_t; void Sonic_Task_FSM(void) { static SonicState_t state = STATE_IDLE; static uint32_t start_time = 0; switch (state) { case STATE_IDLE: // 主程序如果想测距,把状态改为 TRIGGER 即可 break; case STATE_TRIGGER: TRIG_PIN_HIGH(); start_time = HAL_GetTick(); // 记录当前微秒/毫秒 state = STATE_WAIT_ECHO_H; // 切换到下一状态 break; case STATE_WAIT_ECHO_H: // 这里用时间戳取代了 delay_us(10) if (HAL_GetTick() - start_time >= 1) { TRIG_PIN_LOW(); // 检查是否出现高电平... if (ECHO_PIN == HIGH) { start_time = HAL_GetTick(); state = STATE_WAIT_ECHO_L; } } break; case STATE_WAIT_ECHO_L: // 等待电平落下的同时,可以做超时保护,防止死锁! if (ECHO_PIN == LOW) { Calculate_Distance(); // 计算距离 state = STATE_IDLE; // 完美收工,回到空闲 } else if (HAL_GetTick() - start_time > 50) { // 如果等了 50ms 还没落下,说明超声波坏了,强制退出防卡死 state = STATE_IDLE; } break; } }三、 总结
在main()的while(1)大循环中,依次调用LED_Task_NonBlock()和Sonic_Task_FSM(),再加入按键扫描。 你会发现,没有任何一个函数会卡住 CPU 超过 1 微秒。单片机的 CPU 就像一个极其高效的快递分配员,在无数个任务之间疯狂穿梭。明明没有用 RTOS 操作系统的多线程,但表现出来的丝滑度和并发能力,却完全不逊于 RTOS!
掌握了“时间戳”与“状态机”,你也就拿到了告别“单片机初学者”头衔的证书。