灯在闪,按钮为什么不响应?|单片机动画课 04

我们已经知道怎样读取按钮,也知道怎样过滤抖动。现在让 LED 每秒翻转一次,顺便读取按钮。结果却是:灯正常闪,短按经常没反应。
问题可能不在按钮,而在程序这一秒里有没有机会去读它。电路状态一直在变化,程序却不一定一直在观察。
本文以 Uno R3 的普通主循环为例。动画为原创时序示意,非芯片仿真、非实测;读取间隔和执行耗时经过夸张,方便观察。按钮信号先假定已经稳定,暂不混入消抖细节。
程序在等,按钮不会等
常见闪灯写法是:点亮,delay(1000),熄灭,再 delay(1000)。对于只闪灯的任务,这很好理解。但如果按钮读取也放在同一个主循环中,等待期间就没有执行那一行读取代码。

看动图:主循环刚读完按钮,进入等待。按钮随后按下,又在等待结束前松开。等程序再读时,眼前又是“没按”。仅凭这两次读取,它无法知道中间发生过什么。
如果按钮一直按到等待结束,通常还能被下一次读取发现,只是响应晚了。所以“有时有效、有时无效”的表现,可能与按压时机和持续时间有关。
delay() 也不是让整颗芯片冻结。在这里,当前主程序在等待,输出保持原状态;中断以及已经运行的某些硬件外设仍可能工作。我们今天讨论的限制,是同一段主循环里的按钮轮询没有继续执行。
把等待改成记时间
换一个安排:翻转灯后记下当前时间,然后继续读按钮。每次循环顺便看看,距离上次翻转有没有满一秒。没满就略过,满了才翻转灯并更新时间。

动画把每次检查画得很宽,真实程序通常快得多。它也不是同时执行两条指令,而是在很短的时间内轮流完成不同的小任务。
下面是核心片段,放在 loop() 中。last 预先定义为 unsigned long 并初始化为 0,led 为保存灯状态的变量,D8 已配置为 OUTPUT。省略按钮处理部分,避免把重点藏进完整程序。
unsigned long now = millis();
if (now - last >= 1000UL) {
last = now;
led = !led;
digitalWrite(8, led);
}
// 本轮接着读取按钮并更新消抖状态
millis() 给出毫秒计数;这里比较的是两个无符号时间值的差。相比直接比较 now >= last + 1000,这种写法适合处理计数回绕。前提是使用正确的无符号类型,并在合理周期内持续检查,而不是任意长时间停止运行。
状态保存在变量里
以前,程序“停在某个 delay 里”隐含了灯正在等待什么。现在它不在原地停留,就需要明确保存两件事:灯目前亮还是灭,上次改变是什么时候。

把动画中的两个框当作小纸条:led 记住状态,last 记住时间。每次绕回循环,程序读纸条、看时钟,再决定要不要改变输出。这就是理解状态机的一个小入口,无需一开始就背复杂名词。
这个版本到期后把 last 更新为本轮时间,所以如果循环被其他工作拖慢,翻转也会推迟;它不是精密的硬件定时器。后面的定时器一课会解释,如何把稳定计数交给专用硬件。
一个常见误区
“只要删掉所有 delay,就一定不会漏按键。”仍然不对。长时间计算、等待网络、阻塞读取,也会让主循环迟迟不回来。轮询还存在自己的时间分辨率,短于两次有效读取间隔的脉冲仍可能被漏掉。
非阻塞组织的关键,是让每个任务尽快做完本轮该做的一小步,把运行机会交给其他任务;消抖和非阻塞需要配合,而不是互相替代。
看完再猜
一盏灯每秒翻转,另一盏每 300ms 翻转,需要两个 CPU 吗?
不需要。分别保存两盏灯的状态和时间,每一轮各检查一次就可以。理解了这种“快速轮流照看”,下一课我们再观察另一种变化:LED 看上去慢慢变亮,输出引脚到底在做什么?
资料:Arduino Blink Without Delay、Arduino AVR 核心计时实现。
发现更多好玩的开源项目
专注 Arduino、开源硬件、端侧 AI 和值得收藏的开发者工具。
长按二维码关注「GitStar」
所有动画为教学原理示意,非芯片仿真、非硬件实测。网页发布:2026-09-21。