2、GPIO基础与数字传感器:GPIO模式配置、数字输入输出、按键消抖、读取DHT11温湿度传感器

各位同学,欢迎来到第二章。上一章我们把ESP32的开发环境搭好了,也点亮了那颗经典的LED。从这一章开始,我们要真正跟硬件打交道了。

GPIO,全称叫通用输入输出口。说白了,它就是芯片伸出来的那些引脚,你可以让它们输出高电平或低电平,也可以读取外部是高还是低。我刚开始学单片机时,觉得GPIO不就是0和1嘛,有啥好学的?后来踩的坑多了才发现,这里面的门道真不少。

本章核心目标:掌握ESP32的GPIO模式配置,学会数字输入输出,搞定按键消抖,最后读取DHT11温湿度传感器。一步一个脚印,都是实战干货。

GPIO 知识体系与信号流向 ESP32 芯片 GPIO 输出模式 推挽输出 / 开漏输出 控制 LED、蜂鸣器、继电器 GPIO 输入模式 浮空输入 / 上拉 / 下拉 读取按键、传感器电平 数字传感器(如 DHT11)同时使用输入和输出引脚进行通信

2.1 GPIO模式配置:别让引脚"裸奔"

ESP32的GPIO模式,我习惯把它分成三大类:输出模式、输入模式和特殊功能模式。输出模式里又分推挽输出和开漏输出;输入模式里分浮空输入、上拉输入和下拉输入。

你可能会问:为啥要分这么细?直接输出高低电平不就行了?

嗯,这里有个坑。我曾经在一个项目里,直接用推挽输出驱动一个I2C总线,结果总线拉不下来,通信一直失败。后来才发现,I2C协议要求开漏输出,因为多个设备要共享一根线,谁都不能强行拉高,只能拉低。

模式 配置函数 典型用途 注意事项
推挽输出 pinMode(pin, OUTPUT) LED、蜂鸣器、继电器 高电平驱动能力强,但多个输出不能并联
开漏输出 pinMode(pin, OUTPUT_OPEN_DRAIN) I2C总线、电平转换 必须外接上拉电阻
浮空输入 pinMode(pin, INPUT) 读取外部明确电平 引脚悬空时电平不确定,容易误读
上拉输入 pinMode(pin, INPUT_PULLUP) 按键读取(按下为低) 内部上拉电阻约40kΩ,噪声敏感时需外接更小电阻
下拉输入 pinMode(pin, INPUT_PULLDOWN) 按键读取(按下为高) ESP32部分引脚不支持内部下拉,需查手册

我的习惯:读取按键时,我几乎只用 INPUT_PULLUP。为啥?因为高电平默认状态稳定,按下拉低,逻辑清晰。而且内部上拉省一个电阻,板子能小一点。

2.2 数字输入输出:从LED到按键

数字输出,就是 digitalWrite(pin, HIGH/LOW)。数字输入,就是 digitalRead(pin)。听起来简单,但实际用起来有几个细节要注意。

先看一个完整的例子:用按键控制LED。

const int ledPin = 2;      // 板载LED
const int buttonPin = 0;   // BOOT按键

void setup() {
  pinMode(ledPin, OUTPUT);
  pinMode(buttonPin, INPUT_PULLUP);  // 内部上拉
  Serial.begin(115200);
}

void loop() {
  int buttonState = digitalRead(buttonPin);
  
  if (buttonState == LOW) {  // 按键按下
    digitalWrite(ledPin, HIGH);
    Serial.println("Button pressed!");
  } else {
    digitalWrite(ledPin, LOW);
  }
  
  delay(50);  // 简单延时,后面会讲消抖
}

这段代码能跑,但有个问题。你按下按键时,串口可能会打印好几条"Button pressed!"。这就是按键抖动。

2.3 按键消抖:别让抖动坑了你

按键抖动的本质是什么?机械触点在闭合和断开的瞬间,会弹跳几次,每次持续几毫秒到十几毫秒。如果不处理,一次按下会被误读成多次。

我遇到过最离谱的一次,是一个工业控制面板,操作员按一次启动键,机器启动了三次。查了两天才发现是消抖没做好,生产线差点停摆。

消抖有两种主流方法:

  1. 硬件消抖:在按键两端并联一个0.1μF电容,或者加一个RC低通滤波器。简单粗暴,适合量产。
  2. 软件消抖:检测到电平变化后,延时10-20ms再读一次。如果状态一致,才确认有效。

我个人更推荐软件消抖,因为不改硬件,灵活调整。下面是我常用的消抖函数:

bool debounce(int pin) {
  if (digitalRead(pin) == LOW) {      // 第一次检测到按下
    delay(20);                         // 等待20ms,避开抖动期
    if (digitalRead(pin) == LOW) {    // 再次确认
      while (digitalRead(pin) == LOW); // 等待松开
      return true;                     // 返回有效按下
    }
  }
  return false;
}

注意:上面的 while 等待松开会阻塞程序。在复杂项目中,建议用状态机或定时器实现非阻塞消抖。我们后面章节会讲。

2.4 读取DHT11温湿度传感器:实战开始

DHT11,可能是物联网入门最常用的数字温湿度传感器。它用单总线协议通信,说白了就是一根数据线既当输入又当输出,靠时序来区分数据位。

DHT11的数据格式是这样的:

字节 内容 范围
Byte 0 湿度整数部分 0-100%RH
Byte 1 湿度小数部分 0-9
Byte 2 温度整数部分 -20~60°C
Byte 3 温度小数部分 0-9
Byte 4 校验和 前4字节之和的低8位

读取DHT11的时序,说白了就是三步:

  1. 主机发起信号:拉低数据线至少18ms,然后拉高20-40μs。
  2. DHT11响应:拉低80μs,再拉高80μs,表示准备好了。
  3. 读取40位数据:每一位开始都是50μs的低电平,然后高电平持续26-28μs表示0,持续70μs表示1。

听起来复杂?其实用库就简单了。但我建议你至少自己手写一次,才能真正理解单总线协议。

下面是用DHT11库的代码:

#include <DHT.h>

#define DHTPIN 4       // 数据线接GPIO4
#define DHTTYPE DHT11  // DHT11型号

DHT dht(DHTPIN, DHTTYPE);

void setup() {
  Serial.begin(115200);
  dht.begin();
  Serial.println("DHT11 Ready!");
}

void loop() {
  delay(2000);  // DHT11采样间隔至少1秒,建议2秒
  
  float h = dht.readHumidity();
  float t = dht.readTemperature();
  
  if (isnan(h) || isnan(t)) {
    Serial.println("Failed to read from DHT sensor!");
    return;
  }
  
  Serial.print("Humidity: ");
  Serial.print(h);
  Serial.print(" %\t");
  Serial.print("Temperature: ");
  Serial.print(t);
  Serial.println(" *C");
}

关键提醒:DHT11的采样周期是1秒,也就是说你最快1秒读一次。读太快会返回NAN。我习惯在 loop() 里加 delay(2000),稳定可靠。

2.5 避坑指南:我踩过的那些坑

  • 引脚选择:ESP32的GPIO6-11默认接Flash,别乱用。GPIO34-39只能输入,不能输出。我一开始没看手册,用GPIO34驱动LED,死活不亮。
  • 上拉电阻:DHT11的数据线必须外接4.7k-10k上拉电阻到3.3V。忘了接?数据读出来全是0。别问我怎么知道的。
  • 电源稳定:DHT11对电源噪声敏感,最好在VCC和GND之间加一个100μF电解电容。特别是用面包板时,接触不良会导致读数跳变。
  • 线长限制:DHT11的数据线不要超过20米。超过这个长度,信号会畸变。我试过用50米网线接DHT11,结果读出来湿度120%,温度-40°C。

我的调试习惯:第一次读取DHT11时,先用逻辑分析仪抓一下波形。看看起始信号对不对,响应信号有没有,数据位的宽度是否在范围内。波形对了,代码基本就对了。

好了,这一章的内容就到这里。GPIO配置、数字输入输出、按键消抖、DHT11读取,这些都是嵌入式开发的"基本功"。练好了,后面的项目才能走得稳。