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 的时序其实不复杂,但要求很严格。大致流程是这样的:
- 主机拉低总线至少 18ms,然后释放
- DHT22 响应:拉低 80μs,再拉高 80μs
- 然后开始传输 40 位数据(16位湿度 + 16位温度 + 8位校验)
每个数据位用高低电平的持续时间来区分。逻辑 0 是 50μs 低电平 + 26μs 高电平,逻辑 1 是 50μs 低电平 + 70μs 高电平。我曾经因为定时器精度不够,读出来的数据全是乱的。后来换了硬件定时器才搞定。
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;
}
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;
}
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。相信我,调通之后你会觉得传感器驱动也就那么回事儿。