2. ESP32 GPIO基础与LED控制:GPIO工作模式详解、数字输出控制LED、数字输入读取按键状态、按键消抖处理(软件消抖)

好,咱们开始第二章。这一章,我会带你从最基础的GPIO玩起。

你想想看,智能门锁的核心是什么?无非就是传感器输入(指纹、按键)和执行器输出(电机、电磁铁)。而这些,都离不开GPIO。说白了,GPIO就是芯片和外界打交道的“手脚”。

2.1 GPIO工作模式详解

ESP32的GPIO,功能很强大。但咱们做门锁,最常用的就两种模式:数字输出数字输入

我个人习惯,拿到一个新板子,先看数据手册的GPIO表格。ESP32有些引脚是“特殊”的,比如GPIO6-11默认接Flash,你拿来当普通IO用,程序就跑飞了。嗯,这里要注意。

GPIO的工作模式,本质上就是配置内部电路的通路:

  • 输出模式:内部推挽电路接通,可以输出高电平(3.3V)或低电平(0V)。驱动LED、继电器等。
  • 输入模式:内部施密特触发器接通,读取外部电平。读取按键、传感器信号。
  • 上拉/下拉:输入模式下,可以内部拉高或拉低,避免引脚悬空导致电平不确定。

核心要点:ESP32的GPIO默认是输入模式,且内部无上下拉。如果你直接读一个悬空引脚,得到的数据是随机的。我在项目中遇到过,新手把按键一端接GPIO,另一端悬空,结果按键状态乱跳。

2.2 数字输出控制LED

先来个最简单的:点亮LED。这是嵌入式开发的“Hello World”。

电路连接很简单:LED正极接GPIO(比如GPIO2),负极串一个220Ω电阻到GND。为什么串电阻?防止电流过大烧坏LED和GPIO。ESP32的GPIO最大输出电流约40mA,LED一般20mA就够了。

代码怎么写?看下面:

// 定义LED引脚
#define LED_PIN 2

void setup() {
    // 设置GPIO2为输出模式
    pinMode(LED_PIN, OUTPUT);
}

void loop() {
    // 点亮LED
    digitalWrite(LED_PIN, HIGH);
    delay(1000); // 延时1秒
    
    // 熄灭LED
    digitalWrite(LED_PIN, LOW);
    delay(1000);
}

这段代码,让LED每隔1秒闪烁一次。很简单对吧?但这里有个坑:delay()函数会阻塞程序。在门锁项目中,你不可能让CPU一直傻等。后面我们会用定时器或非阻塞方式替代。

我的经验:调试时,我习惯用LED做“心跳指示”。比如系统正常运行时,LED慢闪;有按键按下时,快闪;报警时,常亮。一眼就能看出系统状态。

2.3 数字输入读取按键状态

接下来,读取按键。这是门锁的“触发源”。

电路连接:按键一端接GPIO(比如GPIO4),另一端接GND。同时,GPIO内部启用上拉电阻。这样,按键未按下时,GPIO读到高电平;按下时,读到低电平。

代码示例:

#define BUTTON_PIN 4
#define LED_PIN    2

void setup() {
    pinMode(BUTTON_PIN, INPUT_PULLUP); // 输入模式,内部上拉
    pinMode(LED_PIN, OUTPUT);
    Serial.begin(115200);
}

void loop() {
    int buttonState = digitalRead(BUTTON_PIN);
    
    if (buttonState == LOW) {
        Serial.println("按键按下!");
        digitalWrite(LED_PIN, HIGH);
    } else {
        digitalWrite(LED_PIN, LOW);
    }
}

运行后,按下按键,LED亮,串口打印信息。松开,LED灭。

但是,你实际测试会发现:有时候按一次,串口打印了好几条“按键按下!”。这就是按键抖动。

2.4 按键消抖处理(软件消抖)

为什么会抖动?机械按键内部是金属弹片,按下和释放时,弹片会反弹几次,产生多个脉冲。这个时间很短,大约5-20ms。但对CPU来说,已经足够执行几十万条指令了。

我曾经在一个项目中,没做消抖,结果按键一次触发,门锁开了又关、关了又开,差点把门锁搞坏。从那以后,我再也不敢忽略消抖了。

软件消抖的原理很简单:检测到电平变化后,等一段时间(通常10-30ms),再读取一次。如果状态一致,才确认是有效按键。

看代码:

#define BUTTON_PIN 4
#define DEBOUNCE_DELAY 30 // 消抖延时30ms

int lastButtonState = HIGH;  // 上一次的按键状态
int buttonState = HIGH;      // 当前按键状态
unsigned long lastDebounceTime = 0; // 上次状态变化的时间

void setup() {
    pinMode(BUTTON_PIN, INPUT_PULLUP);
    Serial.begin(115200);
}

void loop() {
    int reading = digitalRead(BUTTON_PIN);
    
    // 如果电平发生了变化,记录当前时间
    if (reading != lastButtonState) {
        lastDebounceTime = millis();
    }
    
    // 如果电平稳定时间超过了消抖延时,更新状态
    if ((millis() - lastDebounceTime) > DEBOUNCE_DELAY) {
        // 如果状态确实改变了
        if (reading != buttonState) {
            buttonState = reading;
            
            // 按键按下(低电平有效)
            if (buttonState == LOW) {
                Serial.println("按键按下(已消抖)");
                // 这里可以执行开锁动作
            }
        }
    }
    
    lastButtonState = reading;
}

这段代码用了millis()函数,是非阻塞的。它不会像delay()那样卡住程序。你想想看,门锁在等待按键的同时,还要处理指纹识别、蓝牙通信,如果用了delay(),其他任务就全停了。

重要提醒:消抖时间不是越长越好。太短,消抖不彻底;太长,按键响应迟钝。我一般取20-30ms。如果按键质量差,可以适当增加到50ms。但超过50ms,用户体验就变差了。

另外,还有一种更简单的消抖方法:连续采样多次,如果结果一致,就确认。比如采样10次,每次间隔1ms,如果10次都是低电平,才认为按键按下。这种方法在中断服务函数里很常用。

好了,这一章的内容就这些。总结一下:

  • GPIO有输入和输出两种基本模式
  • 数字输出控制LED,注意限流电阻
  • 数字输入读取按键,使用内部上拉
  • 按键必须消抖,软件消抖用millis()非阻塞方式

下一章,我们会把这些知识串起来,做一个简单的门锁控制逻辑。到时候,你会看到按键控制电磁锁的完整流程。

课后练习:用两个按键控制一个LED。按键1按下,LED亮;按键2按下,LED灭。要求:两个按键都要做消抖处理。试试看,能不能自己写出来?