3、传感器数据采集基础:GPIO与I2C协议、读取温湿度传感器(DHT22)、数据格式化与时间戳标记

好,咱们进入第三章。这一章我打算带大家真正把手弄脏——去跟硬件打交道。你想想看,物联网终端如果连传感器数据都读不出来,那还叫啥智能设备?

我个人习惯,每次开始一个新项目,第一件事就是先搞定传感器。为啥?因为数据是上层应用的命根子。今天咱们就拿最常见的温湿度传感器 DHT22 开刀,把 GPIO、I2C 这些底层通信协议彻底搞明白。

3.1 GPIO 基础:管脚不是你想用,想用就能用

GPIO,全称 General Purpose Input Output,通用输入输出口。说白了,就是芯片上那些可以编程控制的引脚。

我在项目中遇到过不少新手,上来就把传感器往 GPIO 上一插,然后代码跑不通。嗯,这里要注意:GPIO 有几种工作模式,你得先搞清楚。

模式 说明 典型用途
推挽输出 可以输出高电平或低电平 控制 LED、继电器
开漏输出 只能输出低电平,高电平靠上拉 I2C 总线、多设备共享线
浮空输入 引脚电平由外部决定 读取按键状态
上拉/下拉输入 内部有电阻拉高或拉低 防抖、默认电平确定
⚠️ 我曾经踩过一个坑:用浮空输入去读一个悬空的引脚,结果电平随机跳变,程序像抽风一样。后来才明白,浮空输入必须配合外部上拉电阻,或者直接用内部上拉模式。

3.2 I2C 协议:两根线搞定一切

I2C 协议,Inter-Integrated Circuit,集成电路间通信。你想想看,只用两根线——SCL(时钟线)和 SDA(数据线)——就能挂载几十个设备,是不是很神奇?

其实原理不复杂。SCL 负责同步时钟,SDA 负责传输数据。每次通信开始,主设备会发一个起始信号,然后带上从设备的地址。从设备收到自己的地址后,回复一个 ACK(应答信号)。

我建议你记住这个流程:

  1. 起始条件:SCL 高电平时,SDA 从高变低
  2. 发送地址:7 位或 10 位地址 + 读写位
  3. 等待应答:从设备拉低 SDA 表示收到
  4. 数据传输:每 8 位数据后跟一个应答位
  5. 停止条件:SCL 高电平时,SDA 从低变高

核心要点:I2C 是半双工通信,同一时刻只能一个方向传数据。但好处是节省引脚,两根线能带一堆传感器。

3.3 读取 DHT22:单总线协议的倔强

DHT22 这个传感器有点特别。它不用 I2C,也不用 SPI,而是用自己的一套单总线协议。说白了,就是一根数据线既当爹又当妈,既要传时钟又要传数据。

为什么会这样?因为便宜啊!少一根线,成本就降一点。但代价是时序要求非常严格,差几微秒就读不出来。

DHT22 的数据格式是这样的:

  • 40 位数据:16 位湿度 + 16 位温度 + 8 位校验和
  • 湿度:实际值 = 原始值 / 10,比如 512 就是 51.2%
  • 温度:实际值 = 原始值 / 10,比如 256 就是 25.6°C
  • 校验和:前 4 个字节相加,取低 8 位,等于第 5 个字节

读取时序大致分三步:

  1. 主机拉低总线至少 18ms,然后释放
  2. DHT22 拉低 80μs 作为响应,再拉高 80μs 准备发送
  3. 逐位读取 40 位数据,每位开始是 50μs 低电平,然后高电平长度决定 0 或 1

💡 我的经验:DHT22 的时序对微秒级延迟很敏感。在 FreeRTOS 或 Linux 下,建议关掉中断再读,或者用硬件定时器精确控制。我曾经在树莓派上用软件延时读 DHT22,结果十次有八次超时,后来改用 wiringPi 的微秒延时函数才稳定。

3.4 代码实战:读取 DHT22 并格式化数据

好,理论说完了,咱们直接上代码。下面是一个在 ESP32 上用 Arduino 框架读取 DHT22 的例子:

#include <DHT.h>

#define DHTPIN 4        // GPIO4 接数据线
#define DHTTYPE DHT22   // 传感器型号

DHT dht(DHTPIN, DHTTYPE);

void setup() {
  Serial.begin(115200);
  Serial.println("DHT22 测试开始");
  dht.begin();
}

void loop() {
  // 读取温湿度
  float humidity = dht.readHumidity();
  float temperature = dht.readTemperature();

  // 检查读取是否成功
  if (isnan(humidity) || isnan(temperature)) {
    Serial.println("读取失败!检查接线");
    return;
  }

  // 数据格式化
  char buffer[128];
  snprintf(buffer, sizeof(buffer),
           "时间戳:%lu,温度:%.1f°C,湿度:%.1f%%",
           millis(), temperature, humidity);

  Serial.println(buffer);
  delay(2000);  // 每 2 秒读一次
}

这段代码做了三件事:初始化传感器、读取数据、格式化输出。注意那个 snprintf,我习惯用它来拼接字符串,比 String 类更省内存,也更安全。

3.5 时间戳标记:让数据有「时间感」

光有温湿度数据还不够。你想想看,如果不知道数据是什么时候采集的,那分析起来就毫无意义。所以,时间戳是必须的。

在嵌入式系统里,时间戳通常有两种来源:

来源 精度 适用场景
系统滴答(millis) 毫秒级 本地记录、短时间分析
RTC 实时时钟 秒级 需要绝对时间、断网场景
NTP 网络时间 毫秒级 联网设备、云端同步

我个人习惯,在本地调试时用 millis() 就够了。但产品上线后,一定要用 RTC 或 NTP。为啥?因为设备重启后 millis() 会归零,你根本不知道数据是今天还是昨天的。

数据格式化建议:我推荐用 JSON 格式,比如:{"ts": 1700000000, "temp": 25.6, "hum": 51.2}。这样后续传到云端,解析起来非常方便。

3.6 避坑指南:我踩过的那些雷

做传感器采集这么多年,我踩过的坑真不少。挑几个典型的说说:

  • 上拉电阻忘加:DHT22 的数据线需要 4.7kΩ 上拉到 VCC。我第一次用的时候没加,结果数据全是 0。后来查手册才发现,单总线协议要求外部上拉。
  • 读取间隔太短:DHT22 的采样周期是 2 秒。如果你 1 秒读一次,它根本来不及更新数据,返回的全是旧值。
  • 校验和忽略:有些同学图省事,不校验校验和。结果数据偶尔跳变,还以为是传感器坏了。其实只要加上校验,就能过滤掉大部分错误。
  • 时序被中断打乱:在 RTOS 环境下,如果读取过程中被高优先级任务打断,时序就乱了。我建议在读取时关中断,或者用 DMA 方式。
⚠️ 特别提醒:DHT22 对电源纹波很敏感。如果供电不稳,读数会忽高忽低。我建议在 VCC 和 GND 之间加一个 100μF 的电解电容,能有效滤除噪声。

3.7 小结:从数据到信息

这一章我们干了三件事:搞懂了 GPIO 和 I2C 协议、用代码读取了 DHT22、给数据加上了时间戳。你想想看,从一根根管脚到一串串有意义的温湿度数据,这个过程其实就是物联网终端最核心的能力——把物理世界的信号,变成数字世界的信息。

下一章,咱们会把这些数据通过网络发出去,让终端真正「联网」。到时候你会发现,前面这些基础打得越扎实,后面就越顺手。

嗯,今天就到这儿。去动手试试吧,别光看代码,把传感器接上,跑一遍,你会有不一样的体会。