3、按键输入与中断:数字输入、上拉/下拉电阻、外部中断编程

按键,是嵌入式系统里最基础、也最常用的人机交互方式。你想想看,一个智能家居项目,总得有个开关吧?不管是控制灯光、切换模式,还是触发某个场景,按键几乎无处不在。

这一章,我们就来彻底搞懂 ESP32 的按键输入。从最基础的 GPIO 数字读取,到上拉/下拉电阻的原理,再到高效的中断编程。我会结合我实际做项目时踩过的坑,帮你把这块知识吃透。

3.1 数字输入基础:GPIO 读取

ESP32 的 GPIO 引脚,既可以做输出(控制 LED、继电器),也可以做输入(读取按键、传感器)。做输入时,我们通过读取引脚的电平状态(高电平 3.3V 或低电平 0V),来判断外部设备的状态。

按键的典型接法很简单:一端接 GPIO,另一端接 GND(低电平有效)或 VCC(高电平有效)。实际项目中,低电平有效更常见,因为 GND 更稳定,抗干扰能力强。

核心概念:数字输入就是读取引脚上的电压,判断是逻辑 1(高电平)还是逻辑 0(低电平)。

在 ESP32 上,读取按键的代码非常直接:

// 配置 GPIO 为输入模式
pinMode(KEY_PIN, INPUT);

// 读取引脚电平
int buttonState = digitalRead(KEY_PIN);

if (buttonState == LOW) {
  // 按键按下(低电平有效)
  Serial.println("Button Pressed!");
}

嗯,这里要注意。直接用 INPUT 模式,引脚是“浮空”的。什么意思?就是当按键没按下时,引脚没有明确的电平,它会像天线一样感应周围的电磁干扰,导致读数随机跳变。我在一个环境监测项目里就吃过这个亏,按键没按,数据却乱跳,排查了半天才发现是引脚浮空的问题。

3.2 上拉与下拉电阻:解决浮空问题

解决浮空问题,就需要上拉或下拉电阻。说白了,就是给引脚一个“默认状态”。

  • 上拉电阻:电阻一端接 VCC,一端接 GPIO。默认引脚为高电平,按键按下拉低。
  • 下拉电阻:电阻一端接 GND,一端接 GPIO。默认引脚为低电平,按键按下拉高。

传统做法需要外接电阻,但 ESP32 体贴地内置了上拉和下拉电阻,我们可以通过软件直接启用:

// 启用内部上拉电阻(默认高电平)
pinMode(KEY_PIN, INPUT_PULLUP);

// 启用内部下拉电阻(默认低电平)
pinMode(KEY_PIN, INPUT_PULLDOWN);

我的习惯:我个人偏好使用 INPUT_PULLUP + 低电平有效。原因很简单:ESP32 的内部上拉电阻约 45kΩ,功耗低,而且大多数开发板上的按键默认接 GND,这样连线最方便。

这里有个对比表格,帮你快速选择:

模式 默认电平 按键按下 适用场景
INPUT_PULLUP 高电平 (3.3V) 低电平 (0V) 按键接 GND,最常用
INPUT_PULLDOWN 低电平 (0V) 高电平 (3.3V) 按键接 VCC,特殊场景
INPUT (无) 浮空 (不稳定) 取决于外部电路 不推荐单独使用

3.3 按键消抖:为什么需要它?

你按下按键时,你以为是一次干净利落的接触。但实际上,机械按键在按下和释放的瞬间,会产生一连串的物理弹跳,导致电平在几毫秒内多次跳变。如果不处理,程序可能会误判为按了多次。

我曾经在一个智能门锁项目里,因为没做消抖,按键按一次,门锁却执行了三次开锁动作。用户反馈说“这锁是不是疯了?”——嗯,从那以后,我再也不敢省略消抖了。

消抖有两种主流方式:

  1. 硬件消抖:在按键两端并联一个 0.1μF 电容。利用电容的充放电特性,平滑电平跳变。简单可靠,适合量产。
  2. 软件消抖:检测到电平变化后,延时 10-50ms 再读取一次,确认状态稳定。灵活,无需额外元件。

软件消抖的代码示例:

// 软件消抖函数
bool debounce(int pin) {
  if (digitalRead(pin) == LOW) {        // 检测到按下
    delay(20);                          // 等待 20ms
    if (digitalRead(pin) == LOW) {      // 再次确认
      return true;                      // 确认按下
    }
  }
  return false;
}

注意:delay() 会阻塞程序执行。在复杂项目中,建议使用定时器或状态机来实现非阻塞消抖。我们会在后续章节深入讲解。

3.4 外部中断:让 CPU 更高效

如果使用轮询(在 loop() 里不断读取),CPU 大部分时间都在做无用功。尤其当系统有多个任务时,轮询会浪费大量资源。

外部中断解决了这个问题。它让引脚在电平变化时,主动“通知”CPU 去处理。CPU 平时可以安心做其他事,只有按键按下时才响应。

ESP32 几乎所有 GPIO 都支持外部中断。配置起来也很简单:

// 中断服务函数(ISR)
void IRAM_ATTR buttonISR() {
  // 注意:ISR 内要简短,避免 delay() 和 Serial.print()
  buttonPressed = true;  // 设置一个标志位
}

void setup() {
  pinMode(KEY_PIN, INPUT_PULLUP);
  // 配置中断:下降沿触发(高→低)
  attachInterrupt(digitalPinToInterrupt(KEY_PIN), buttonISR, FALLING);
}

void loop() {
  if (buttonPressed) {
    buttonPressed = false;  // 清除标志位
    // 在这里处理按键逻辑
    Serial.println("Interrupt triggered!");
  }
}

这里有几个关键点:

  • 触发模式:RISING(上升沿)、FALLING(下降沿)、CHANGE(任意变化)。低电平有效按键,用 FALLING 最合适。
  • ISR 要短:中断服务函数里不要做复杂操作,比如 Serial.print() 或 delay()。设置一个全局标志位,在 loop() 里处理,这是标准做法。
  • IRAM_ATTR:这个关键字把 ISR 放在 IRAM(指令 RAM)中,保证中断响应速度。不加也行,但加了更保险。

3.5 知识体系:按键输入与中断全景图

下面这张 SVG 图,帮你把本章的核心知识点串起来。从物理按键到 CPU 响应,整个链路一目了然。

按键输入与中断知识体系 按键输入与中断知识体系 物理按键 上拉/下拉电阻 INPUT_PULLUP / INPUT_PULLDOWN 消抖处理 硬件电容 / 软件延时 GPIO 读取(轮询) digitalRead() / loop() 外部中断 attachInterrupt() / ISR CPU 响应:执行任务 核心路径:物理按键 → 上拉电阻 → 消抖 → 中断/轮询 → CPU 响应 中断方式更高效,适合多任务系统;轮询方式简单,适合单任务原型

3.6 实战建议与避坑指南

最后,分享几个我在项目中积累的经验:

  • 优先用中断:只要项目超过两个任务,就别用轮询。中断让系统更流畅,功耗也更低。
  • ISR 里别干重活:设置标志位就好。我在一个项目中试图在 ISR 里直接驱动电机,结果系统频繁崩溃。后来改成标志位 + loop 处理,问题解决。
  • 内部上拉够用:除非环境特别恶劣(强电磁干扰),否则 ESP32 的内部上拉电阻完全够用,省一个电阻是一个。
  • 消抖不能省:不管是硬件还是软件,必须做。我曾经因为偷懒没做消抖,被测试同事追着改 bug,教训深刻。

小技巧:调试时,可以用 Serial.println() 打印按键状态,观察是否有抖动。确认稳定后,再上中断。

好了,这一章的内容就到这里。按键输入看似简单,但上拉、消抖、中断这些细节,决定了项目的稳定性和效率。把这些基础打牢,后面的智能家居项目才能走得稳。