news 2026/4/29 15:26:10

别再用 delay() 坑自己了!一文讲透“状态机”与非阻塞编程,裸机也能跑出 RTOS 的丝滑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
别再用 delay() 坑自己了!一文讲透“状态机”与非阻塞编程,裸机也能跑出 RTOS 的丝滑

前言:回想一下你写的第一行点灯代码,是不是这样的:点亮 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!

掌握了“时间戳”与“状态机”,你也就拿到了告别“单片机初学者”头衔的证书。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/29 15:22:25

数字散斑DIC技术:金属薄板焊接变形全场动态监测及工艺优化全解析

前言:在汽车、飞机、轮船等精密制造领域,金属薄壁材料在焊接高温下的力学性能研究,是保证焊接产品加工精度、外部形状和结构性能的关键,是工业生产中迫切需要解决的问题。传统接触式测量(千分表、应变片)无…

作者头像 李华
网站建设 2026/4/29 15:19:23

从游戏背包到任务队列:用C++ list的splice实战优化你的数据结构设计

从游戏背包到任务队列:用C list的splice实战优化你的数据结构设计 在游戏开发中,玩家背包系统经常需要处理物品移动、合并和排序操作;而在后台服务中,任务队列的优先级调整和动态重组也是常见需求。这些场景本质上都是对链表节点的…

作者头像 李华
网站建设 2026/4/29 15:14:24

51单片机IO口模式避坑指南:为什么你的按键检测不准、LED亮度不够?

51单片机IO口模式实战避坑:从现象反推配置错误的解决之道 当你的按键检测总是不稳定,LED亮度始终达不到预期,或者通信接口频繁出错时,问题很可能出在IO口模式的选择上。很多开发者虽然知道51单片机有四种IO模式,但在实…

作者头像 李华
网站建设 2026/4/29 15:14:22

如何用免费开源PCB查看器OpenBoardView快速定位电路板问题

如何用免费开源PCB查看器OpenBoardView快速定位电路板问题 【免费下载链接】OpenBoardView View .brd files 项目地址: https://gitcode.com/gh_mirrors/op/OpenBoardView 作为一名硬件工程师或电路维修人员,你是否经常面对复杂的电路板设计文件感到无从下手…

作者头像 李华
网站建设 2026/4/29 15:12:48

Android 14启动优化避坑指南:从Choreographer到RenderThread的常见性能陷阱

Android 14启动性能优化实战:关键路径分析与避坑指南 在移动应用体验中,启动速度是用户感知最直接的核心指标之一。随着Android 14的发布,系统在启动流程中引入了多项底层优化,但开发者仍需面对Choreographer调度、RenderThread协…

作者头像 李华