输入输出与编程 / 编程实验发布 内容更新

不用 delay 同时做两件事:让按钮和闪灯一起工作

把按钮消抖、两档闪烁和循环间隔日志放进同一个loop,用无符号时间差处理millis回绕。包含可立即执行的合成时间戳检查与响应记录方法。

读完能做什么
等待闪灯时仍能处理按钮,知道如何检查循环耗时与时钟回绕。
开始之前
先完成P02,理解按下沿与连续稳定窗口。
所需器材
P02的WP7113ID LED与按钮电路、可保存日志的串口工具
版本组合
Arduino CLI:1.5.1Arduino-ESP32:3.3.12 · 94afccf35fb1e401facddbcf9e13bcf7c76a31d8参考硬件:ESP32-S3-DevKitC-1 v1.1 · WROOM-1-N8R8FQBN:esp32:esp32:esp32s3:FlashSize=8M,PSRAM=opi,FlashMode=qio,PartitionScheme=default_8MB,CDCOnBoot=default,USBMode=hwcdc
适用范围

官方ESP32-S3-DevKitC-1 v1.1、ESP32-S3-WROOM-1-N8R8;Arduino CLI 1.5.1、Arduino-ESP32 3.3.12;USB-to-UART口。

这篇验证到了哪一步

资料核对已通过
docs/esp32-evidence/programming.md#p03版本:Arduino-ESP32 3.3.12验证日期:
构建 / 配置已通过
docs/esp32-evidence/logs/esp32-millis-state-machine-esp32-millis-state-machine.log对象:esp32:esp32:esp32s3:FlashSize=8M,PSRAM=opi,FlashMode=qio,PartitionScheme=default_8MB,CDCOnBoot=default,USBMode=hwcdc版本:Arduino CLI 1.5.1 / core 3.3.12验证日期:
仿真已通过
docs/esp32-evidence/host-simulation.json:主机HAL执行原样.ino,仅验证软件输入、时间戳与模拟PWM返回值;未模拟ESP32芯片、电气或物理外设验证日期:
本站实板待验证
本站未实板验证;正文提供可执行验收步骤

构建通过只说明指定环境中的代码或配置通过检查;实物功能、功耗和稳定性以对应的实板记录为准。

01 / 编程实验

把“等一会儿”改成“下次再检查”

在闪灯程序里加入delay(500)后,主循环在这半秒里不会检查按钮。按钮在等待中按下又松开,程序可能完全没看到。解决方法不是缩短所有等待,而是保存时间戳,每次loop只问“现在是否到期”。

本篇同时完成三个任务:按钮切换闪烁间隔、LED按当前间隔翻转、每5秒打印最大循环间隔。它们共享一个loop,但各自有状态和上次执行时刻,任何一项都不负责等待下一次执行。

参考:[1] Arduino:millis()返回类型与回绕

02 / 编程实验

三个时钟与四个按钮状态各管自己的事

接线完全沿用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条件压降与额定值

03 / 编程实验

无符号时间差让回绕成为普通情况

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会破坏这里的假设。

参考:[1] Arduino:millis()返回类型与回绕

04 / 编程实验

完整状态程序与循环间隔日志

每轮只读一次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与模式

05 / 编程实验

在等待中按按钮,并留下十次证据

按下载README的固定FQBN构建本篇目录,上传后开115200串口。第一条wrap日志应为delta=48、threshold47=1、threshold49=0。该检查失败时先检查源码和整数类型,不要继续把结果归咎于硬件。

  1. RESET后确认初始500毫秒翻转;LED刚亮时按按钮,确认不用等原来的完整等待就能产生press日志。
  2. 间隔应在150和500之间切换。重复10次,保存每条candidate、accepted、debounce和interval。
  3. 每5秒保存max_loop_gap_ms。若循环间隔经常超过消抖窗口,检查长串口输出和新加入的阻塞函数。
  4. 保持按钮长按,闪灯应继续;按钮不会因长按反复改变间隔。
做到这里,你应该看到
日志包含10次可对应的物理动作、合成回绕检查和循环间隔;实板现象仍待读者执行。
06 / 编程实验

没有delay,不等于所有操作都立即返回

串口缓冲区满时的输出、I2C事务、网络连接都可能等待。将来添加传感器时,应给事务设置超时,降低日志频率,并重新观察循环间隔。这里的协作调度让短任务彼此穿插,但不能抢占一个正在阻塞的函数。

按钮不响应时,先回到P02检查电平和计数,再看max_loop_gap_ms;灯过快或过慢时先区分翻转间隔与完整周期。按RESET恢复500毫秒,重新上传原文件可恢复基线。下一篇把输出从开/关扩展为PWM亮度,按钮和计时思路可以继续沿用。

BACK TO THE SOURCE

资料来源与版本说明

资料与固定版本源码核对;构建、仿真与实板为分开的验证维度。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。

  1. Arduino:millis()返回类型与回绕
  2. Arduino-ESP32 3.3.12:GPIO API与模式
  3. 乐鑫:ESP32-S3-DevKitC-1 v1.1,排针与RGB修订
  4. Kingbright WP7113ID规格书 V.14A:极性、10mA条件压降与额定值