DHT11 温湿度传感器驱动学习笔记
对应代码:dht11_ioctl.c / pt.dts
平台:i.MX6ULL,数据线接 GPIO1_IO04
一、DHT11 硬件基础
1.1 引脚说明
| Pin | 名称 | 说明 |
|---|---|---|
| 1 | VDD | 供电 3~5.5V |
| 2 | DATA | 单总线数据线(需 5K 上拉电阻) |
| 3 | NC | 悬空 |
| 4 | GND | 接地 |
1.2 关键参数
| 参数 | 值 |
|---|---|
| 测温范围 | 0~50°C,精度 ±2°C,分辨率 1°C |
| 测湿范围 | 20~90%RH,精度 ±5%RH,分辨率 1%RH |
| 采样间隔 | ≥ 1 秒(强制要求,否则数据不稳定) |
| 上电等待 | 上电后等待 1 秒才能开始通信 |
| 单次通信 | 约 4ms,传输 40 bit 数据 |
二、通信协议与完整时序讲解
DHT11 采用单总线协议(1-Wire),同一根线既发又收,分三个阶段。
2.1 总线空闲状态
总线空闲时由上拉电阻维持高电平。DHT11 处于低功耗模式,不主动发数据。
总线空闲:
DATA ─────────────────────── 高电平(上拉电阻维持)
2.2 阶段一:
主机(MCU)主动拉低总线,通知 DHT11 开始工作。
主机发起信号时序:
DATA ──┐ ┌─────┐
│ │ │
└────────────────────┘ └── 切为输入,等 DHT11
← 拉低 ≥ 18ms(代码用20ms)→ ↑ ←20~40us→
拉高
对应代码:
c
static void dht11_start(void)
{
gpio_set_value(dht11_gpio, 0); // ① 拉低总线
msleep(20); // ② 保持低电平 20ms(>18ms 即可)
gpio_set_value(dht11_gpio, 1); // ③ 拉高,结束起始信号
udelay(30); // ④ 等待 20~40us
// ⑤ 函数返回后,调用方切换为输入模式等待响应
}
为什么要拉低 ≥18ms? 这是 DHT11 能检测到起始信号的最短时间,低于 18ms DHT11 不会响应。
2.3 阶段二:DHT11 响应信号(代码:dht11_response())
主机拉高后,DHT11 检测到起始信号,发出响应:先拉低 80us,再拉高 80us,然后再次拉低,表示数据开始。
DHT11 响应时序:
主机拉高 数据开始
↓ ↓
DATA ──────┐ ┌──────────┐ ┌── 第1个bit低电平50us
│ │ │ │
└────┘ └───────────┘
←20~40us→← 80us低 →← 80us高 →
主机信号 DHT11响应低 DHT11响应高
对应代码:
c
static int dht11_response(void)
{
int time = 10;
gpio_direction_input(dht11_gpio); // ① 切为输入模式
// ② 等待 DHT11 拉低(响应的 80us 低电平开始)
while (gpio_get_value(dht11_gpio) && time--)
udelay(10);
if (time <= 0) { return -1; } // 超时:DHT11 没有响应
// ③ 等待 DHT11 拉高(80us 低电平结束,80us 高电平开始)
time = 10;
while (!dht11_pin_check() && time--)
udelay(10);
if (time <= 0) { return -2; }
// ④ 等待 DHT11 再次拉低(80us 高电平结束,第一个数据 bit 开始)
while (dht11_pin_check());
return 0;
}
注意 :步骤 ①
gpio_direction_input写在dht11_response()里,而不是dht11_start()里。这是因为dht11_start()执行完后要有 30us 的等待,这 30us 间 GPIO 还是输出模式(高电平),进入dht11_response()后才切换为输入。
2.4 阶段三:数据传输(40 bit)
DHT11 发送 40 bit 数据,高位(MSB)先出。
数据格式:
第1字节 第2字节 第3字节 第4字节 第5字节
湿度整数 湿度小数 温度整数 温度小数 校验和
8 bit 8 bit 8 bit 8 bit 8 bit
当前 DHT11 小数部分固定为 0(保留扩展),实际只用整数部分。
bit 编码规则
每个 bit 以 50us 低电平开始,后面跟的高电平时长决定是 0 还是 1:
bit = 0 的波形:
DATA ─┐ ┌──────┐
│ │ │
└─────────┘ └─ 下一个bit低电平
← 50us低 →←26~28us高→
bit = 1 的波形:
DATA ─┐ ┌──────────────────┐
│ │ │
└─────────┘ └─ 下一个bit低电平
← 50us低 →← 70us高 →
采样判断方法(延迟 60us 采样)
等 50us 低电平结束(上升沿到来)
↓
延迟 60us
↓
此时采样:
- 若还是高电平 → 高电平持续 70us > 60us → bit = 1
- 若已经是低电平 → 高电平只有 26~28us < 60us → bit = 0
对应代码:
c
for (j = 0; j < 5; j++) {
for (i = 0; i < 8; i++) {
while (!dht11_pin_check()); // 等待 50us 低电平结束(上升沿)
udelay(60); // 延迟 60us 后采样
if (dht11_pin_check())
dht_dat[j] |= (1 << (7 - i)); // 还是高 → bit=1(MSB 先出)
while (dht11_pin_check()); // 等待低电平,准备下一个 bit
}
}
(7 - i)的含义:第一个收到的 bit 是最高位(bit7),所以 i=0 时存到第 7 位,i=7 时存到第 0 位,这就是"高位先出"的处理方式。
2.5 阶段四:数据结束
最后一个 bit 传完后,DHT11 拉低总线 50us,然后由上拉电阻将总线拉回高电平,恢复空闲状态。代码里在 dht11_read() 结尾调用 gpio_direction_output(dht11_gpio, 1) 主动拉高,准备下次采集。
2.6 完整时序总览
总线空闲(高电平)
│
▼
【主机发起】
拉低 ≥18ms → 拉高 → 等 20~40us → 切为输入
│
▼
【DHT11 响应】
DHT11 拉低 80us → DHT11 拉高 80us → DHT11 拉低(数据开始)
│
▼
【传输 40 bit】(每个 bit:50us低 + 高电平)
bit7 bit6 bit5 ... bit0 │ bit7 ... bit0 │ ... │ 校验和
湿度整数(8bit) 湿度小数(8bit) 温度整数 温度小数 校验
│
▼
【结束】
DHT11 拉低 50us → 上拉电阻拉高 → 总线空闲
三、校验和机制
计算方式:前 4 字节之和,取低 8 位
c
checksum = dht_dat[0] + dht_dat[1] + dht_dat[2] + dht_dat[3];
if (checksum != dht_dat[4]) {
// 数据有误,丢弃
}
遇到的 Bug:校验一直失败
现象:加上校验代码后一直报错,删掉校验就能正常读数。
原因 :dht_dat 是全局数组,每次读取前没有清零。数据读取用的是 |=(按位或),只能把 0 变成 1,无法把 1 变回 0。第一次读完的数据留在数组里,第二次读进来的新 bit 和旧数据叠加,校验和必然出错。
c
// 错误原因示意:
// 第一次读完:dht_dat = {0x55, 0x00, 0x1C, 0x00, 0x71} 正确
// 第二次读取,|= 叠加后:某些 bit 永远是 1,校验和不对
dht_dat[j] |= (1 << (7 - i)); // |= 只能置 1,不能清 0
修复 :在 dht11_read() 开头加一行清零
c
static int dht11_read(void)
{
memset(dht_dat, 0, sizeof(dht_dat)); // ← 加这一行,每次采集前清零
// ... 后面不变
}
为什么删掉校验"看起来正常" :删掉校验后直接使用 dht_dat[0](湿度)和 dht_dat[2](温度),温湿度变化缓慢,叠加的旧数据影响不明显,所以"看起来"能读出数据。但实际上数据已经因累积错误而不准确了------校验是在暴露 bug,而不是制造问题。
四、DTS 配置(需新增到 pt.dts)
DHT11 使用 GPIO1_IO04(原 DTS 中 PWM3 为 disabled,该引脚空闲可用)。
第一处:在 &iomuxc { imx6ul-evk { 内添加引脚配置
dts
pinctrl_dht11: dht11grp {
fsl,pins = <
/* GPIO1_IO04,使能内部上拉(0x1b0b0)
* DHT11 数据线需要上拉,保证总线空闲时为高电平
* 0x1b0b0 = HYS+PUS+PUE+PKE+SPEED_100MHz+DSE_48OHM
*/
MX6UL_PAD_GPIO1_IO04__GPIO1_IO04 0x1b0b0
>;
};
第二处:在根节点 / { 内添加设备节点
dts
pt_dht11 {
compatible = "pt-dht11";
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_dht11>;
dht11-gpio = <&gpio1 4 GPIO_ACTIVE_HIGH>;
};
五、数据读取流程图
应用层:read(fd, buf, 4)
↓
内核 read()
↓
dht11_read()
├─ memset(dht_dat, 0) 清零,防止旧数据干扰
├─ dht11_start() 主机发起:拉低20ms → 拉高 → 等30us
├─ dht11_response() 等DHT11响应:低80us → 高80us → 低
├─ 读 40 bit 循环 5字节 × 8bit,60us 延迟采样
├─ 校验和验证 前4字节之和 == 第5字节?
└─ copy_to_user(buf, dht_dat, 4) 返回4字节给用户
↓
应用层获得:
buf[0] = 湿度整数
buf[1] = 湿度小数(当前固定0)
buf[2] = 温度整数
buf[3] = 温度小数(当前固定0)
六、编译与验证步骤
bash
# 1. 修改 DTS,重新编译 DTB
make pt.dtb
# 把 pt.dtb 部署到开发板,重启生效
# 2. 编译驱动模块
make
# 生成 dht11.ko
# 3. 加载驱动,确认 probe 执行
insmod dht11.ko
dmesg | tail # 看到 "probe dht11 misc_register"
ls /dev/dht11 # 设备节点存在
# 4. 编写应用层测试程序
# read(fd, buf, 4) 读出 4 字节
# buf[0]=湿度整数 buf[2]=温度整数
# 交叉编译后传到开发板运行
arm-linux-gnueabihf-gcc dht11_app.c -o dht11_app
./dht11_app
# 5. 卸载
rmmod dht11
注意事项:
- DHT11 上电后必须等待 1 秒 才能通信(代码里 open 函数有
msleep(5),实际上建议应用层上电后等 1s 再 open)- 两次 read 之间至少间隔 1 秒,否则 DHT11 来不及完成下一次采样
- 时序对抖动敏感,Linux 非实时系统有调度延迟,如遇偶发错误属正常现象