ESP32 工程实践:用状态机实现按键消抖与长按识别
机械按键按下时,触点往往会在短时间内反复通断。如果直接把每次电平变化都当成一次点击,菜单可能跳过多个选项。加入 delay(20) 虽然容易理解,却会暂停主循环里的网络维护与其他任务。本篇用状态机把“输入采样”和“确认事件”分开。
示例面向经典 ESP32 开发板和 Arduino-ESP32 3.x。代码是教学示例,未在本文中完成实机验证;消抖时间与接线请根据实际硬件调整。
接线与输入约定
按键一端连接 GPIO27,另一端连接 GND,配置为 INPUT_PULLUP。松开读到 HIGH,按下读到 LOW。这里使用普通外接按键;其他型号或带外设的开发板应先核对 GPIO27 是否可用。
长线和强干扰场景中,内部上拉可能不足,应结合外部上拉、布线与必要的硬件滤波处理。不要把 5 V 信号直接接入 ESP32 的 GPIO。
状态机如何工作
保存原始电平、确认后的稳定电平,以及原始电平最后一次变化的时间。只有新电平持续 25 ms,才更新稳定状态。按下确认时记录起点;持续按下 800 ms 产生一次长按事件;松开时,如果此前没有产生长按,才产生短按事件。
这样一次按压最多产生一种事件。注意这里的时间从“确认按下”开始,而不是从触点第一次闭合开始。
完整最小示例
#include <Arduino.h>
constexpr uint8_t KEY_PIN = 27;
constexpr uint32_t DEBOUNCE_MS = 25;
constexpr uint32_t LONG_MS = 800;
bool rawLevel = HIGH;
bool stableLevel = HIGH;
bool longSent = false;
uint32_t rawChangedAt = 0;
uint32_t pressedAt = 0;
void setup() {
Serial.begin(115200);
pinMode(KEY_PIN, INPUT_PULLUP);
rawChangedAt = millis();
}
void loop() {
const uint32_t now = millis();
const bool sample = digitalRead(KEY_PIN);
if (sample != rawLevel) {
rawLevel = sample;
rawChangedAt = now;
}
if (rawLevel != stableLevel &&
uint32_t(now - rawChangedAt) >= DEBOUNCE_MS) {
stableLevel = rawLevel;
if (stableLevel == LOW) {
pressedAt = now;
longSent = false;
} else if (!longSent) {
// 即使循环偶尔延迟,也按持续时间区分松开事件。
if (uint32_t(now - pressedAt) >= LONG_MS) {
Serial.println("LONG");
} else {
Serial.println("SHORT");
}
}
}
if (stableLevel == LOW && rawLevel == LOW && !longSent &&
uint32_t(now - pressedAt) >= LONG_MS) {
longSent = true;
Serial.println("LONG");
}
delay(1); // 让出调度机会,不承担消抖计时。
}
时间比较使用无符号减法,避免把 millis() 回绕直接误判为超时。该写法仍要求计时间隔合理,不能用于跨越一个完整计数周期的持续时间测量。
如何验证
在串口监视器以 115200 波特率观察:连续短按应每次出现一个 SHORT;按住超过 800 ms 应只出现一个 LONG,松开后不追加 SHORT。重点测试临界时长、快速连按以及上电时一直按住。
代码会把上电时持续按住的按键视为一次按压。如果产品不希望开机触发动作,需要先等待一次稳定松开,再允许事件识别。
本例依赖主循环持续采样。若主循环阻塞几秒,软件无法还原期间发生的多次按压;应先消除耗时操作,或把输入扫描放入独立任务。涉及任务间传递时,把事件送入队列,让业务层决定动作,避免在输入扫描里直接写文件或访问网络。
浙公网安备 33010602011771号