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

按钮按一次为什么触发多次?给 ESP32 加上消抖

机械触点会在一次动作中来回跳变。本篇用30毫秒稳定窗口和下降沿计数,完成按钮切灯、30次计数与长按检查,避免用delay掩盖问题。

读完能做什么
按钮每次稳定按下只切换一次LED,并用日志完成30次计数验收。
开始之前
先运行P01,能确认外接LED的亮灭与GPIO4一致。
所需器材
P01的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#p02版本:Arduino-ESP32 3.3.12验证日期:
构建 / 配置已通过
docs/esp32-evidence/logs/esp32-button-debounce-esp32-button-debounce.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 / 编程实验

一次手指动作,为何产生多个电平变化

如果直接检测“GPIO是LOW就加一”,按住按钮的几百毫秒内loop会执行许多次;即使只检测下降沿,机械触点在接触的瞬间仍可能短暂断开再接通。一种错误来自持续电平,另一种来自触点抖动,需要分别处理。

本篇把原始电平、候选电平、已接受的稳定电平分开:原始值一变就重启30毫秒计时,只有候选值连续稳定足够久才更新稳定状态;稳定状态从HIGH变LOW时产生一次press。最终作品是按钮切灯器,长按只切换一次。

做到这里,你应该看到
能说明“低电平”与“一次稳定按下事件”为什么不是同一件事。

参考:[1] Arduino-ESP32 3.3.12:GPIO API与模式[3] Arduino:millis()返回类型与回绕

02 / 编程实验

上拉让松开的按钮也有明确状态

保留P01的GPIO4 LED电路。按钮使用常开触点,GPIO5(J1-5)接一侧,另一侧接GND,软件配置INPUT_PULLUP。松开时由内部上拉保持HIGH;按下时触点把输入接地,读到LOW。

常见四脚轻触按钮每组两脚内部已经相连。如果选了同一组的两脚,输入会始终接地;如果把电源与地接到内部相连的两脚,还会形成短路。先断电用通断挡确认两组,按下时才导通的组合才能作为按钮两侧。

本篇外接灯固定为Kingbright WP7113ID(规格书V.14A),使用330Ω、±5%、额定0.25W电阻;如果沿用F03的1kΩ电阻,也可保留,亮度更低,程序不变。更换电阻必须先断电。

节点连接预期电平
GPIO5(J1-5)按钮的一组触点INPUT_PULLUP
按钮另一组触点GND按下时输入LOW
GPIO4330Ω→LED阳极,阴极GND按下沿切换

参考:[1] Arduino-ESP32 3.3.12:GPIO API与模式[2] 乐鑫:ESP32-S3-DevKitC-1 v1.1,排针与RGB修订[4] Kingbright WP7113ID规格书 V.14A:极性、10mA条件压降与额定值

03 / 编程实验

用稳定窗口过滤抖动,释放也要经过同一规则

30毫秒是这份初学示例的初始阈值,不是所有按钮的抖动上限。候选值每次变化都更新changedAt,因此抖动结束前不会提前接受。松开也要稳定30毫秒才更新状态,避免在一次动作里反复接受按下和松开。

上电时以当前输入初始化candidate和stable,避免刚启动就把一个已按住的按钮当成新事件。测试前要先松开;计数只记录运行过程中新的稳定下降沿。很短的触碰可能被30毫秒规则过滤,这是定义的输入范围,应明确记录。

参考:[1] Arduino-ESP32 3.3.12:GPIO API与模式[3] Arduino:millis()返回类型与回绕

04 / 编程实验

完整代码:只有稳定下降沿改变LED

程序没有delay,也没有在每次扫描时打印,避免大量日志影响读入。press日志只在有效按下时增加,release日志用于确认松开确实被接受。单看LED亮灭不容易发现重复两次切换,计数日志是必要的第二条证据。

代码 / 配置
// 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, lamp = false;
uint32_t changedAt = 0, pressCount = 0;
void setup() {
  Serial.begin(115200);
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LOW);
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  candidate = stable = digitalRead(BUTTON_PIN);
  changedAt = millis();
  Serial.println("P02 ready; release button before 30-press test");
}
void loop() {
  const uint32_t now = millis();
  const bool raw = digitalRead(BUTTON_PIN);
  if (raw != candidate) {
    candidate = raw;
    changedAt = now;
  }
  if (candidate != stable && uint32_t(now - changedAt) >= DEBOUNCE_MS) {
    stable = candidate;
    if (stable == LOW) {
      lamp = !lamp;
      digitalWrite(LED_PIN, lamp ? HIGH : LOW);
      Serial.printf("press=%lu t=%lu led=%u\n", (unsigned long)++pressCount,
                    (unsigned long)now, lamp);
    } else {
      Serial.printf("release t=%lu\n", (unsigned long)now);
    }
  }
}

参考:[1] Arduino-ESP32 3.3.12:GPIO API与模式[3] Arduino:millis()返回类型与回绕

05 / 编程实验

用30次短按与一次长按检验结果

先按P01命令方式编译本篇目录并上传,打开115200串口,保持按钮松开再RESET。本篇下载包含与正文一致的源码、锁定profile和README。验收要求来自要解决的“重复事件”,不是看见灯能亮就结束。

  1. 记录初始LED灭、计数0。每次按住约0.2秒再松开约0.2秒,做30次,用纸或表格标记物理次数与press编号。
  2. 每次动作应只有一条新press,最后编号30;30次切换后LED应回到灭。保存实际串口日志。
  3. 再按住2秒,应只出现press=31,松开只出现release;LED不在长按中反复改变。
  4. 如果一次动作重复计数,先排接线,再保留原始电平/逻辑分析仪波形,根据实际抖动调整阈值,重新完整做30次。
做到这里,你应该看到
30次的实际事件数等于30,长按只计一次,且保存了实板记录。本站尚未执行该硬件检查。
06 / 编程实验

从“总是HIGH/LOW”开始定位

总是LOW:断电检查按钮是否接了同组脚、GPIO5是否直接碰地、按钮是否机械卡住。总是HIGH:查缺共地、导线未入孔或选错脚组。先量状态,不要先加更长delay。

日志增加但灯不切换:回到P01独立测试GPIO4。按一次出现多条press:确认没有另一段代码在累计,并检查是否在上电/松开后完整经过稳定窗口。长线和较强干扰可能需要合适的外部电路,单纯把阈值无限增大会让操作迟钝。

RESET会清空本例计数和灯状态。恢复原始30毫秒代码并重复验收,得到可信基线后,再进入多任务计时。

BACK TO THE SOURCE

资料来源与版本说明

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

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