把“等一会儿”改成“下次再检查”
在闪灯程序里加入delay(500)后,主循环在这半秒里不会检查按钮。按钮在等待中按下又松开,程序可能完全没看到。解决方法不是缩短所有等待,而是保存时间戳,每次loop只问“现在是否到期”。
本篇同时完成三个任务:按钮切换闪烁间隔、LED按当前间隔翻转、每5秒打印最大循环间隔。它们共享一个loop,但各自有状态和上次执行时刻,任何一项都不负责等待下一次执行。
三个时钟与四个按钮状态各管自己的事
接线完全沿用P02:GPIO4经330Ω和LED接地,GPIO5经常开按钮接地,内部上拉。按钮的candidate、stable、changedAt保留原来的含义;闪灯另外使用lastBlink与intervalMs,不要拿同一个时间戳兼作消抖和闪灯。
一次稳定按下把间隔从500改成150毫秒,再次按下改回500。这里的间隔指翻转一次,完整亮灭周期分别约1秒与0.3秒。lastBlink=now在任务被拖慢后跳过错过的边沿,避免恢复时连续快速翻转;需要节拍累计时才另做有上限的追赶机制。
本篇外接灯固定为Kingbright WP7113ID(规格书V.14A),使用330Ω、±5%、额定0.25W电阻;如果沿用F03的1kΩ电阻,也可保留,亮度更低,程序不变。更换电阻必须先断电。
| 状态/时钟 | 职责 | 更新时机 |
|---|---|---|
| changedAt | 候选按钮值开始稳定的时刻 | 原始电平变化 |
| lastBlink | 上次真正翻转LED的时刻 | 闪灯到期 |
| reportAt | 上一组性能日志的时刻 | 5秒到期 |
| lastLoop/maxLoopGap | 观察循环最长间隔 | 每一轮/每次报告 |
参考:[1] Arduino:millis()返回类型与回绕[2] Arduino-ESP32 3.3.12:GPIO API与模式[4] Kingbright WP7113ID规格书 V.14A:极性、10mA条件压降与额定值
无符号时间差让回绕成为普通情况
millis的32位毫秒值约49.7天绕回0。若用now >= then + interval,右侧的加法回绕可能改变比较含义。把两个时间戳都保存为uint32_t,并比较uint32_t(now−then) >= interval,可以在跨零时得到经过时间。
本例立即检查一组人工时间戳:then=0xFFFFFFF0,now=0x00000020,差值为48。因此47毫秒阈值到期,49毫秒未到期。不必让板运行49天才能测试这个算术分支;但这种测试也不证明设备具备49天稳定性。
该方法仍有边界:不能让同一计时器隔完整的32位周期都不检查,间隔参数也须在设计范围。使用signed int或把时间戳压成uint16_t会破坏这里的假设。
完整状态程序与循环间隔日志
每轮只读一次now,让这一轮任务用同一时刻判断。稳定按下日志输出candidate与accepted两个时刻,二者差值反映最后一次电平变化后的稳定等待,不是手指动作到灯光的全部延迟。
max_loop_gap_ms记录两次loop入口间隔的最大毫秒值,它能发现明显阻塞;millis的分辨率与打印行为会限制测量精度。代码没有宣称响应一定小于某个硬实时阈值。
// SPDX-License-Identifier: MIT
#include <Arduino.h>
constexpr uint8_t LED_PIN = 4, BUTTON_PIN = 5;
constexpr uint32_t DEBOUNCE_MS = 30;
bool candidate, stable, led = false;
uint32_t changedAt = 0, lastBlink = 0, intervalMs = 500, pressCount = 0;
uint32_t lastLoop = 0, maxLoopGap = 0, reportAt = 0;
bool due(uint32_t now, uint32_t then, uint32_t interval) {
return uint32_t(now - then) >= interval;
}
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LOW);
pinMode(BUTTON_PIN, INPUT_PULLUP);
candidate = stable = digitalRead(BUTTON_PIN);
changedAt = lastBlink = lastLoop = reportAt = millis();
// No 49-day wait: unsigned arithmetic is checked with synthetic timestamps.
const uint32_t before = 0xFFFFFFF0u, after = 0x00000020u;
Serial.printf("wrap delta=%lu threshold47=%u threshold49=%u\n",
(unsigned long)(after - before), due(after, before, 47), due(after, before, 49));
}
void loop() {
const uint32_t now = millis();
const uint32_t gap = now - lastLoop;
lastLoop = now;
if (gap > maxLoopGap) maxLoopGap = gap;
const bool raw = digitalRead(BUTTON_PIN);
if (raw != candidate) { candidate = raw; changedAt = now; }
if (candidate != stable && due(now, changedAt, DEBOUNCE_MS)) {
stable = candidate;
if (stable == LOW) {
intervalMs = intervalMs == 500 ? 150 : 500;
Serial.printf("press=%lu accepted=%lu candidate=%lu debounce=%lu interval=%lu\n",
(unsigned long)++pressCount, (unsigned long)now, (unsigned long)changedAt,
(unsigned long)(now - changedAt), (unsigned long)intervalMs);
}
}
if (due(now, lastBlink, intervalMs)) {
lastBlink = now; // skip missed edges rather than burst after a stall
led = !led;
digitalWrite(LED_PIN, led ? HIGH : LOW);
}
if (due(now, reportAt, 5000)) {
reportAt = now;
Serial.printf("max_loop_gap_ms=%lu interval=%lu\n",
(unsigned long)maxLoopGap, (unsigned long)intervalMs);
maxLoopGap = 0;
}
}
参考:[1] Arduino:millis()返回类型与回绕[2] Arduino-ESP32 3.3.12:GPIO API与模式
在等待中按按钮,并留下十次证据
按下载README的固定FQBN构建本篇目录,上传后开115200串口。第一条wrap日志应为delta=48、threshold47=1、threshold49=0。该检查失败时先检查源码和整数类型,不要继续把结果归咎于硬件。
- RESET后确认初始500毫秒翻转;LED刚亮时按按钮,确认不用等原来的完整等待就能产生press日志。
- 间隔应在150和500之间切换。重复10次,保存每条candidate、accepted、debounce和interval。
- 每5秒保存max_loop_gap_ms。若循环间隔经常超过消抖窗口,检查长串口输出和新加入的阻塞函数。
- 保持按钮长按,闪灯应继续;按钮不会因长按反复改变间隔。
- 做到这里,你应该看到
- 日志包含10次可对应的物理动作、合成回绕检查和循环间隔;实板现象仍待读者执行。
没有delay,不等于所有操作都立即返回
串口缓冲区满时的输出、I2C事务、网络连接都可能等待。将来添加传感器时,应给事务设置超时,降低日志频率,并重新观察循环间隔。这里的协作调度让短任务彼此穿插,但不能抢占一个正在阻塞的函数。
按钮不响应时,先回到P02检查电平和计数,再看max_loop_gap_ms;灯过快或过慢时先区分翻转间隔与完整周期。按RESET恢复500毫秒,重新上传原文件可恢复基线。下一篇把输出从开/关扩展为PWM亮度,按钮和计时思路可以继续沿用。
资料来源与版本说明
资料与固定版本源码核对;构建、仿真与实板为分开的验证维度。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。