4、传感器驱动开发:编写温湿度传感器(DHT22)驱动、光照传感器(BH1750)驱动

好,咱们进入实战环节。前面讲了那么多理论,今天直接上手写驱动。我选了两个最常用的传感器:DHT22 和 BH1750。一个测温湿度,一个测光照。这两个家伙在智能家居、环境监测项目里几乎天天见。

写驱动这事儿,说白了就是跟芯片的数据手册打交道。你想想看,传感器厂商已经把硬件做好了,我们只需要按照它规定的时序去读数据就行。嗯,这里要注意,不同传感器的通信协议差别很大,有的用单总线,有的用 I2C。咱们一个一个来。

4.1 DHT22 温湿度传感器驱动

DHT22 用的是单总线协议。什么叫单总线?就是一根数据线既当输入又当输出,还得靠时序来区分。我刚开始搞这个的时候,总觉得这玩意儿不靠谱,一根线能传啥?后来发现,人家靠的是精确的时序控制。

4.1.1 硬件连接

DHT22 有四个引脚,但实际只用三个:VCC、GND、DATA。我建议在 DATA 引脚上拉一个 4.7kΩ 电阻到 VCC,这样信号更稳定。我在项目中遇到过,不上拉电阻的话,长线传输时数据经常出错。

DHT22 引脚 连接目标
VCC(1脚) 3.3V 或 5V
DATA(2脚) GPIO(带 4.7kΩ 上拉)
NC(3脚) 悬空
GND(4脚) GND

4.1.2 通信时序

DHT22 的时序其实不复杂,但要求很严格。大致流程是这样的:

  1. 主机拉低总线至少 18ms,然后释放
  2. DHT22 响应:拉低 80μs,再拉高 80μs
  3. 然后开始传输 40 位数据(16位湿度 + 16位温度 + 8位校验)

每个数据位用高低电平的持续时间来区分。逻辑 0 是 50μs 低电平 + 26μs 高电平,逻辑 1 是 50μs 低电平 + 70μs 高电平。我曾经因为定时器精度不够,读出来的数据全是乱的。后来换了硬件定时器才搞定。

注意:DHT22 的读取间隔不能小于 2 秒。你如果读得太快,它会不理你。我刚开始调试时,在循环里一直读,结果传感器直接罢工了。

4.1.3 代码实现

下面是我个人习惯的写法,用状态机来管理时序,这样不会阻塞系统。你想想看,如果直接用 delay 函数,整个系统都得等,多浪费。

// DHT22 驱动代码片段
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <wiringPi.h>

#define DHT22_PIN 7

int readDHT22(float *humidity, float *temperature) {
    uint8_t data[5] = {0};
    
    // 主机发送开始信号
    pinMode(DHT22_PIN, OUTPUT);
    digitalWrite(DHT22_PIN, LOW);
    delay(20);  // 拉低至少 18ms
    digitalWrite(DHT22_PIN, HIGH);
    delayMicroseconds(40);
    
    // 切换为输入,等待响应
    pinMode(DHT22_PIN, INPUT);
    
    // 等待 DHT22 拉低总线(响应信号)
    int timeout = 100;
    while(digitalRead(DHT22_PIN) == HIGH) {
        if(timeout-- == 0) return -1;
        delayMicroseconds(1);
    }
    
    // 读取 40 位数据
    for(int i = 0; i < 40; i++) {
        // 等待低电平结束
        timeout = 100;
        while(digitalRead(DHT22_PIN) == LOW) {
            if(timeout-- == 0) return -2;
            delayMicroseconds(1);
        }
        
        // 延时 30μs 后判断电平
        delayMicroseconds(30);
        if(digitalRead(DHT22_PIN) == HIGH) {
            data[i/8] |= (1 << (7 - (i % 8)));
        }
        
        // 等待高电平结束
        timeout = 100;
        while(digitalRead(DHT22_PIN) == HIGH) {
            if(timeout-- == 0) return -3;
            delayMicroseconds(1);
        }
    }
    
    // 校验
    uint8_t checksum = data[0] + data[1] + data[2] + data[3];
    if(checksum != data[4]) return -4;
    
    // 解析数据
    *humidity = ((data[0] << 8) | data[1]) / 10.0;
    *temperature = ((data[2] << 8) | data[3]) / 10.0;
    
    return 0;
}
小技巧:校验和是前四个字节相加的低 8 位。如果校验失败,我建议直接丢弃这组数据,不要做任何修正。因为温湿度数据一旦出错,偏差可能很大。

4.2 BH1750 光照传感器驱动

BH1750 就友好多了。它用的是 I2C 协议,地址固定为 0x23(ADDR 引脚接低电平)或 0x5C(接高电平)。我个人习惯用 0x23,因为大多数模块默认就是这个地址。

4.2.1 硬件连接

I2C 只需要两根线:SCL 和 SDA。加上 VCC 和 GND,一共四根线。注意,I2C 总线需要上拉电阻,一般 4.7kΩ 就行。很多开发板已经内置了上拉,你直接接就行。

BH1750 引脚 连接目标
VCC 3.3V 或 5V
GND GND
SCL I2C 时钟线
SDA I2C 数据线
ADDR GND(地址 0x23)或 VCC(地址 0x5C)

4.2.2 工作模式

BH1750 有好几种测量模式,我常用的就两种:

  • 连续高分辨率模式(0x10):精度 1 lux,测量时间 120ms。适合大多数场景。
  • 连续低分辨率模式(0x13):精度 4 lux,测量时间 16ms。适合快速采样。

为什么会这样?高分辨率模式内部做了多次采样取平均,所以更准但更慢。低分辨率模式就是快,但精度差点。我在做智能窗帘项目时,用的就是低分辨率模式,因为需要快速响应光线变化。

4.2.3 代码实现

I2C 驱动比单总线简单多了。你只需要发送指令,然后读数据就行。下面是我常用的代码:

// BH1750 驱动代码片段
#include <stdio.h>
#include <wiringPiI2C.h>

#define BH1750_ADDR 0x23
#define BH1750_POWER_ON 0x01
#define BH1750_CONTINUOUS_HIGH_RES 0x10

int bh1750_fd;

int bh1750_init() {
    bh1750_fd = wiringPiI2CSetup(BH1750_ADDR);
    if(bh1750_fd < 0) {
        printf("BH1750 初始化失败\n");
        return -1;
    }
    
    // 上电
    wiringPiI2CWrite(bh1750_fd, BH1750_POWER_ON);
    delay(10);
    
    // 设置为连续高分辨率模式
    wiringPiI2CWrite(bh1750_fd, BH1750_CONTINUOUS_HIGH_RES);
    delay(180);  // 等待首次测量完成
    
    return 0;
}

float bh1750_read_lux() {
    uint8_t data[2];
    
    // 读取 2 字节数据
    data[0] = wiringPiI2CRead(bh1750_fd);
    data[1] = wiringPiI2CRead(bh1750_fd);
    
    // 计算光照强度
    uint16_t raw = (data[0] << 8) | data[1];
    float lux = raw / 1.2;
    
    return lux;
}
关键点:BH1750 返回的原始值除以 1.2 才是 lux 值。这个系数是数据手册里写的,我建议你直接写死,不要用变量,省得后面改出问题。

4.3 两个驱动的对比

写到这里,你可能会问:这两个驱动哪个更难?说实话,DHT22 的时序要求更严格,对定时器精度要求高。BH1750 用 I2C 协议,只要总线没问题,基本一次过。

特性 DHT22 BH1750
通信协议 单总线(自定义时序) I2C(标准协议)
数据长度 40 位(含校验) 16 位
测量间隔 ≥ 2 秒 16ms ~ 120ms
精度 ±0.5°C, ±2% RH ±1 lux(高分辨率)
调试难度 较高(时序敏感) 较低(I2C 稳定)

4.4 避坑指南

最后,分享几个我踩过的坑:

  • DHT22 上电后要等 1 秒:传感器刚上电时不稳定,我建议等 1 秒后再发第一次读取指令。我曾经一上电就读,结果返回的全是 0。
  • BH1750 的地址别搞错:0x23 是 7 位地址,在 Linux 下用 wiringPi 时,函数会自动左移一位。如果你自己写 I2C 驱动,记得左移。
  • 长线传输加电容:如果传感器离主板超过 20cm,我建议在 VCC 和 GND 之间加一个 100μF 的电解电容。我遇到过,不加电容时,数据偶尔会跳变。

好了,这两个驱动就讲到这里。你回去可以自己试试,先调 DHT22,再调 BH1750。相信我,调通之后你会觉得传感器驱动也就那么回事儿。