2、GPIO基础与数字传感器:数字输入输出原理,按键消抖,LED控制,人体红外传感器(PIR)实战

各位同学,欢迎来到第二章。上一章我们聊了ESP32的整体架构,今天咱们来点实在的——直接上手操作GPIO。

GPIO,全称是General Purpose Input Output,通用输入输出口。说白了,它就是芯片和外界打交道的“手脚”。你想让LED亮?GPIO输出高电平。你想知道按键按没按?GPIO读输入电平。就这么简单。

但简单归简单,坑也不少。我刚开始做项目时,就因为在GPIO上栽过跟头,导致整块板子重启。嗯,今天咱们就把这些坑一个个填平。

2.1 数字输入输出原理

先讲原理。ESP32的GPIO引脚,内部其实是一个数字逻辑门。输出模式下,你写1,引脚就是3.3V;写0,引脚就是0V。输入模式下,你读引脚,得到的就是外部电平——高电平是1,低电平是0。

这里有个关键点:ESP32的GPIO容忍电压是3.3V。千万别直接接5V,否则芯片会烧。我见过有人把5V传感器直接怼到GPIO上,结果冒烟了。切记!

警告:ESP32的GPIO最大输入电压为3.3V。如果需要连接5V设备,必须使用电平转换电路或分压电阻。

另外,有些GPIO引脚有特殊功能。比如GPIO6-11默认连接了Flash,你如果把它们当普通IO用,程序会跑飞。我踩过这个坑,当时调试了一整天,最后发现是引脚冲突。

引脚 特殊功能 建议
GPIO6-11 连接Flash 不要用作普通IO
GPIO0 下载模式选择 上拉或悬空,避免低电平
GPIO2 下载时上拉 注意外部电路影响

2.2 按键消抖——一个被忽视的细节

按键,看起来最简单。按下,读0;松开,读1。对吧?

但实际不是这样。机械按键在按下和松开的瞬间,会产生一连串的抖动信号。你想想看,一个按键按下去,实际上会在几毫秒内产生几十次电平跳变。如果你直接读,程序会认为你按了几十次。

为什么会这样?因为金属触点接触时会有弹性反弹。这个现象叫“抖动”。

消抖的方法有两种:硬件消抖和软件消抖。

硬件消抖:在按键两端并联一个电容(通常0.1uF),利用电容的充放电特性滤除抖动。优点是简单,缺点是增加元件成本。

软件消抖:检测到电平变化后,延时10-20ms再读一次。如果电平稳定,才认为是有效按键。我个人习惯用软件消抖,因为灵活,而且不增加硬件成本。

核心要点:软件消抖的延时时间取决于按键的机械特性。一般10-20ms足够,但劣质按键可能需要50ms。我曾经在一个项目中用了廉价按键,抖动长达80ms,最后不得不调整延时参数。

来看代码:

// 软件消抖示例
#define BUTTON_PIN 0
#define DEBOUNCE_DELAY 20  // 毫秒

int lastButtonState = 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;
}

这段代码的核心逻辑是:只有电平稳定超过20ms,才认为按键状态发生了变化。你想想看,如果抖动只有5ms,它根本不会触发状态更新。

2.3 LED控制——从闪烁到呼吸

LED控制是GPIO输出的经典案例。最简单的就是点亮和熄灭:

pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, HIGH);  // 点亮
digitalWrite(LED_PIN, LOW);   // 熄灭

但如果你想让LED有呼吸效果,就需要PWM(脉冲宽度调制)。ESP32的LEDC外设可以生成PWM信号,频率和占空比都可调。

我建议你使用LEDC库,它比analogWrite更灵活:

// LEDC呼吸灯示例
#define LED_PIN 2
#define LEDC_CHANNEL 0
#define LEDC_TIMER 8  // 8位分辨率,0-255
#define LEDC_FREQ 5000 // 5KHz

void setup() {
  ledcSetup(LEDC_CHANNEL, LEDC_FREQ, LEDC_TIMER);
  ledcAttachPin(LED_PIN, LEDC_CHANNEL);
}

void loop() {
  // 渐亮
  for (int dutyCycle = 0; dutyCycle <= 255; dutyCycle++) {
    ledcWrite(LEDC_CHANNEL, dutyCycle);
    delay(10);
  }
  // 渐暗
  for (int dutyCycle = 255; dutyCycle >= 0; dutyCycle--) {
    ledcWrite(LEDC_CHANNEL, dutyCycle);
    delay(10);
  }
}

小技巧:LEDC的通道数量有限(ESP32有16个通道),但可以复用。如果你需要控制多个LED,记得合理分配通道。另外,PWM频率不要低于1KHz,否则人眼能感觉到闪烁。

2.4 人体红外传感器(PIR)实战

PIR传感器,全称Passive Infrared Sensor,被动红外传感器。它检测人体发出的红外辐射,当有人经过时,输出高电平。

常用的型号是HC-SR501。它有三个引脚:VCC(5V)、GND、OUT(数字输出)。注意,它的输出是3.3V,可以直接接ESP32的GPIO。

但有个坑:HC-SR501上电后需要30-60秒的稳定时间。在这段时间内,它会频繁触发。我曾经以为传感器坏了,换了三个才发现是稳定时间的问题。

接线很简单:

  • VCC → ESP32 5V(注意不是3.3V)
  • GND → GND
  • OUT → GPIO4(任意数字引脚)

代码实现:

#define PIR_PIN 4

void setup() {
  pinMode(PIR_PIN, INPUT);
  Serial.begin(115200);
  Serial.println("PIR传感器初始化中,请等待30秒...");
  delay(30000);  // 等待传感器稳定
  Serial.println("初始化完成");
}

void loop() {
  int pirState = digitalRead(PIR_PIN);
  
  if (pirState == HIGH) {
    Serial.println("检测到人体移动!");
    // 这里可以触发LED亮、蜂鸣器响等动作
    delay(1000);  // 避免重复触发
  } else {
    // 无人状态
  }
}

实战经验:PIR传感器的检测距离和灵敏度可以通过背后的两个电位器调节。顺时针旋转,检测距离变远(最远约7米),灵敏度变高。但注意,过高的灵敏度会导致误触发,比如宠物经过也会触发。我在智能家居项目中,通常把灵敏度调到中等,然后配合延时电路使用。

另外,PIR传感器有个特性:检测到人体后,输出会保持高电平一段时间(默认约2-3秒)。这个时间也可以通过电位器调节。如果你需要更长的保持时间,可以在代码中自己实现延时逻辑。

2.5 综合实战:智能感应灯

好了,我们把今天学的知识串起来,做一个智能感应灯:当PIR检测到人时,LED呼吸点亮;人离开后,LED缓慢熄灭。

#define PIR_PIN 4
#define LED_PIN 2
#define LEDC_CHANNEL 0
#define LEDC_TIMER 8
#define LEDC_FREQ 5000

int pirState = LOW;
unsigned long lastMotionTime = 0;
const unsigned long timeout = 10000;  // 10秒无人后熄灭

void setup() {
  pinMode(PIR_PIN, INPUT);
  ledcSetup(LEDC_CHANNEL, LEDC_FREQ, LEDC_TIMER);
  ledcAttachPin(LED_PIN, LEDC_CHANNEL);
  Serial.begin(115200);
  delay(30000);  // PIR稳定时间
}

void loop() {
  pirState = digitalRead(PIR_PIN);
  
  if (pirState == HIGH) {
    lastMotionTime = millis();
    // 呼吸点亮
    for (int i = 0; i <= 255; i++) {
      ledcWrite(LEDC_CHANNEL, i);
      delay(5);
    }
  }
  
  // 如果超过10秒无人,缓慢熄灭
  if (millis() - lastMotionTime > timeout) {
    for (int i = 255; i >= 0; i--) {
      ledcWrite(LEDC_CHANNEL, i);
      delay(10);
    }
  }
}

这个项目虽然简单,但包含了GPIO输入、输出、PWM、延时控制等核心知识点。你可以在家里试试,把它做成一个走廊感应灯。

嗯,今天的内容就到这里。下一章我们会讲ADC和模拟传感器,到时候你会学到如何读取温度、光照等模拟信号。记得把今天的代码跑一遍,有问题随时问我。

课后练习:

  1. 修改按键消抖代码,实现长按和短按的区分
  2. 用两个LED实现红绿灯效果,要求使用PWM控制亮度
  3. 将PIR传感器与按键结合:按键按下后,感应灯进入常亮模式;再次按下恢复自动模式

公众号:蓝海资料掘金营,微信deep3321