2. GPIO与外部中断:数字输入输出、内部上拉下拉、中断触发模式、消抖处理实战

好,咱们进入第二章。这一章讲的是GPIO和外部中断,说白了就是ESP32跟外界打交道的最基本方式。你想想看,一个摄像头项目,总得知道门磁有没有被触发、人体红外传感器有没有检测到人吧?这些信号怎么读进来,怎么处理,就是本章要解决的问题。

我个人习惯把GPIO比作芯片的「手脚」——手用来输出信号(比如控制LED亮灭),脚用来感知外部状态(比如读取按键电平)。ESP32有34个GPIO,但不是所有都能随便用,这个后面会讲到。

2.1 数字输入输出基础

先说说最基本的。GPIO作为输出时,我们可以用digitalWrite()把它拉高(3.3V)或拉低(0V)。作为输入时,用digitalRead()读取引脚电平。

这里有个坑——浮空输入。什么意思呢?就是引脚什么都没接,电平不确定。我在项目中遇到过这种情况:按键没按下时,读到的电平忽高忽低,导致程序误判。嗯,这就是典型的浮空问题。

核心概念:数字信号只有两种状态——高电平(1)和低电平(0)。ESP32的工作电压是3.3V,所以高电平就是3.3V,低电平就是0V。千万别直接接5V,会烧芯片。

2.2 内部上拉与下拉电阻

解决浮空问题,最直接的办法就是加上拉或下拉电阻。ESP32很贴心,内部集成了这些电阻,我们只需要在代码里配置一下就行。

我举个例子。假设你接了一个按键,一端接GPIO,另一端接地。按键按下时,GPIO读到低电平。但按键松开时呢?GPIO就悬空了。这时候就需要内部上拉电阻,把引脚默认拉到高电平。

// 配置GPIO为输入,并启用内部上拉电阻
pinMode(4, INPUT_PULLUP);
// 或者启用内部下拉电阻
pinMode(4, INPUT_PULLDOWN);

你可能会问:上拉和下拉有什么区别?简单说:

  • 上拉(PULLUP):默认高电平,按下变低。适合按键接地的情况。
  • 下拉(PULLDOWN):默认低电平,按下变高。适合按键接VCC的情况。

我的经验:在安防项目中,我习惯用上拉模式。因为大多数传感器(如门磁、PIR)都是输出低电平触发,上拉模式更符合直觉——没触发时是高电平,触发了变低。

2.3 外部中断触发模式

接下来是重头戏——外部中断。为什么要用中断?你想想看,如果主程序一直在轮询读取按键状态,CPU就被占死了,啥也干不了。中断就不一样了——它像门铃,按了才响,不按的时候CPU该干嘛干嘛。

ESP32支持以下几种触发模式:

模式 触发条件 适用场景
LOW 低电平触发 持续报警信号
HIGH 高电平触发 持续触发信号
CHANGE 电平变化触发 按键、开关
RISING 上升沿触发(低→高) 按键松开检测
FALLING 下降沿触发(高→低) 按键按下检测

我个人最常用的是FALLINGRISING。为什么?因为CHANGE模式会在上升和下降都触发,容易产生重复响应。而边沿触发只响应一次,配合消抖处理,效果很好。

// 配置外部中断,下降沿触发
attachInterrupt(digitalPinToInterrupt(4), buttonISR, FALLING);

void buttonISR() {
  // 中断服务函数——要短!要快!
  // 一般只设置标志位,不要做复杂操作
  interruptFlag = true;
}

重要提醒:中断服务函数(ISR)里不要用delay(),不要做串口打印,不要做复杂计算。我在项目中见过有人直接在ISR里写Serial.println(),结果系统频繁崩溃。ISR的原则就是——快进快出,只设标志位。

2.4 消抖处理实战

好,终于到了消抖。为什么需要消抖?你按一下机械按键,实际上在几毫秒内会弹跳好几次,产生多个边沿信号。如果不处理,一次按键可能会触发多次中断。

我曾经在做一个门禁系统时,就因为这个吃了大亏。按键按一次,门开了又关、关了又开,活像在跳机械舞。从那以后,我再也不敢忽略消抖了。

消抖有两种主流方法:

方法一:硬件消抖

在按键两端并联一个0.1μF的电容。电容可以吸收弹跳产生的毛刺。优点是简单可靠,缺点是多了一个元件。

方法二:软件消抖

这是嵌入式开发中最常用的方法。核心思想就是——检测到边沿后,等一段时间再确认电平状态。

// 软件消抖示例
volatile bool interruptFlag = false;
unsigned long lastDebounceTime = 0;
const unsigned long debounceDelay = 50; // 50ms消抖

void buttonISR() {
  // 记录中断发生的时间
  lastDebounceTime = millis();
  interruptFlag = true;
}

void loop() {
  if (interruptFlag) {
    // 等待消抖时间过去
    if ((millis() - lastDebounceTime) > debounceDelay) {
      // 再次读取引脚状态确认
      if (digitalRead(4) == LOW) {
        // 确认是有效按键
        Serial.println("按键被按下!");
        // 执行你的安防逻辑...
      }
    }
    interruptFlag = false; // 清除标志位
  }
}

消抖参数选择:一般机械按键的弹跳时间在5-20ms之间。我建议消抖延时设在30-50ms。太短消不干净,太长影响响应速度。50ms是个比较稳妥的值,人感觉不到延迟。

2.5 实战:安防门磁检测

最后,咱们把今天学的串起来,做一个门磁检测的实战例子。假设门磁传感器接在GPIO4上,门关时输出高电平,门开时输出低电平。

#define DOOR_SENSOR 4
#define ALARM_LED 2

volatile bool doorOpened = false;
unsigned long lastInterruptTime = 0;

void setup() {
  Serial.begin(115200);
  pinMode(DOOR_SENSOR, INPUT_PULLUP);
  pinMode(ALARM_LED, OUTPUT);
  
  // 下降沿触发——门打开时触发
  attachInterrupt(digitalPinToInterrupt(DOOR_SENSOR), doorISR, FALLING);
  Serial.println("门磁监控已启动");
}

void doorISR() {
  // 简单的消抖处理
  if ((millis() - lastInterruptTime) > 50) {
    doorOpened = true;
    lastInterruptTime = millis();
  }
}

void loop() {
  if (doorOpened) {
    Serial.println("⚠️ 门被打开了!");
    digitalWrite(ALARM_LED, HIGH);
    delay(3000); // 报警3秒
    digitalWrite(ALARM_LED, LOW);
    doorOpened = false;
  }
  // 其他任务...
}

这个例子虽然简单,但包含了今天讲的所有核心知识点:GPIO配置、内部上拉、外部中断、消抖处理。你可以在ESP32上跑一下试试,感受一下中断的实时性。

扩展思考:在实际的安防摄像头项目中,门磁中断触发后,我们通常不会只亮个LED,而是会唤醒摄像头拍照、发送通知到手机、甚至启动录像。这些功能我们会在后面的章节逐步实现。

好了,这一章就到这里。记住一句话:GPIO是基础,中断是灵魂,消抖是良心。下一章我们会讲PWM和ADC,到时候你会看到ESP32更多的「隐藏技能」。