Day66 DHT11温湿度传感器驱动开发与单总线通信协议
day66 DHT11温湿度传感器驱动开发与单总线通信协议
一、DHT11传感器基础认知
- 名称:DHT11(Digital Humidity & Temperature Sensor)
- 功能:可同时测量环境中的温度(Temperature)和相对湿度(Relative Humidity, RH)
- 通信方式:单总线(Single Bus)——仅需1根数据线(除VCC、GND外)
- 工作模式:异步半双工(Asynchronous Half-duplex)通信
- 成本与普及度:
- 市场价格约 5~10元
- 属于入门级传感器,广泛用于教学和基础IoT项目
- 供电要求:
- 支持 3.3V 或 5V 电源
- 建议在数据线加 10kΩ 上拉电阻 以提升信号稳定性
✅ 提示:DHT11输出的温湿度均为整数,小数部分固定为0。
二、DHT11通信协议核心要点
1. 数据传输流程(三步走)
步骤 | 动作 | 说明 |
---|---|---|
① 主机发起请求 | 单片机发送起始信号 | 拉低总线 ≥18ms,再拉高20–40μs |
② DHT11响应 | DHT11返回响应信号 | 80μs低电平 + 80μs高电平 |
③ 数据输出 | DHT11连续发送40bit数据 | 高位在前,含校验位 |
2. 数据格式(共40 bit = 5字节)
字节序 | 内容 | 说明 |
---|---|---|
Byte 0 | 湿度高8位 | 整数部分(如 53 表示 53% RH) |
Byte 1 | 湿度低8位 | DHT11固定为 0 |
Byte 2 | 温度高8位 | 整数部分(如 24 表示 24℃) |
Byte 3 | 温度低8位 | DHT11固定为 0 |
Byte 4 | 校验位 | = (Byte0 + Byte1 + Byte2 + Byte3) 的低8位 |
✳️ 校验规则:若校验位 ≠ 前四字节之和的低8位 → 数据无效,应丢弃
✅ 正确示例:
接收数据:[0x35][0x00][0x18][0x00]
→ 和 = 0x35 + 0x00 + 0x18 + 0x00 = 0x4D
校验位 = 0x4D
→ 匹配 → 有效
→ 湿度 = 53% RH,温度 = 24℃
❌ 错误示例:
若校验位 ≠ 计算值(如校验位是
0x4C
而计算值是0x4D
),则放弃数据
3. 位(bit)时序区分
数据位 | 低电平 | 高电平 | 判断依据 |
---|---|---|---|
0 | ~50μs | ~26–28μs | 高电平短 |
1 | ~50μs | ~70μs | 高电平长 |
⚠️ 关键:通过测量高电平持续时间判断 bit 值,而非电压高低。
三、驱动开发关键步骤与代码实现
1. 设备树(Device Tree)配置
mydht11 {#address-cells = <1>;#size-cells = <1>;compatible = "mydht11";pinctrl-0 = <&pinctrl_mydht11>;gpio-dht11 = <&gpio1 1 1>; // 使用 GPIO1_1 引脚status = "okay";
};
📌 操作指令:
vim arch/arm/boot/dts/pt.dts # 编辑设备树
make pt.dtb # 编译设备树
cp arch/arm/boot/dts/pt.dtb ~/tftpboot # 拷贝到TFTP目录
2. 内核驱动代码(drivers/char/dht11_1.c
)
(1)头文件与宏定义
#include <linux/init.h>
#include <linux/printk.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <asm/io.h>
#include <asm/string.h>
#include <asm/uaccess.h>
#include <linux/miscdevice.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
#include <linux/delay.h>#define DEV_NAME "dht11"
static int gpio_dht11; // 存储从设备树获取的GPIO编号
✅ 说明:包含必要头文件,定义设备名和全局GPIO变量。
(2)发送起始信号 dht11_start()
static void dht11_start(void)
{gpio_direction_output(gpio_dht11, 1); // 设置为输出模式,初始高电平msleep(10); // 稳定高电平(可选)gpio_set_value(gpio_dht11, 0); // 拉低总线msleep(20); // 保持低电平 ≥18ms(实际20ms)gpio_set_value(gpio_dht11, 1); // 拉高总线udelay(40); // 保持高电平 20–40μs(此处40μs)gpio_direction_input(gpio_dht11); // 切换为输入模式,准备接收
}
🔧 功能:严格按照DHT11协议发送起始信号,并切换GPIO为输入以接收响应。
⏱️ 注意:msleep(20)
满足 ≥18ms 要求;udelay(40)
在20–40μs范围内。
(3)等待DHT11响应 dht11_wait_respon()
static int dht11_wait_respon(void)
{// 等待高电平结束(初始状态应为高,但保险起见)int time = 10;while(gpio_get_value(gpio_dht11) && time--){udelay(2); }if(time <= 0){printk("wait_respon 1..\n");return -1; // 超时:未检测到低电平开始}// 等待低电平结束(DHT11拉低80μs)time = 120; // 最多等待120μswhile((!gpio_get_value(gpio_dht11)) && time--){udelay(1); }if(time <= 0){printk("wait_respon 2..\n");return -2; // 超时:低电平过长}// 等待高电平结束(DHT11拉高80μs)time = 120;while(gpio_get_value(gpio_dht11) && time--){udelay(1); }if(time <= 0){printk("wait_respon 3..\n");return -3; // 超时:高电平过长}return 0; // 响应成功
}
🔍 功能:检测DHT11的80μs低 + 80μs高响应信号,每步设超时防止死循环。
🛑 错误码:-1/-2/-3 分别对应三阶段超时。
(4)读取单个bit dht11_get_bit()
static inline char dht11_get_bit(void)
{ // 等待低电平结束(约50μs)int time = 80; while((!gpio_get_value(gpio_dht11)) && time--){udelay(1); }if(time <= 0){printk("wait_respon 4..\n");return -4; // 低电平超时}udelay(35); // 延时35μs,此时若为"0"则已变低,"1"仍为高if(!gpio_get_value(gpio_dht11))return 0; // 高电平短 → bit=0// 等待高电平结束(为"1"时需等待70μs高电平结束)time = 55;while(gpio_get_value(gpio_dht11) && time--){udelay(1); }if(time <= 0){printk("wait_respon 5..\n");return -5; // 高电平超时}return 1; // bit=1
}
📏 原理:在低电平结束后延时35μs采样——
- 若此时为低 → 高电平仅26–28μs → bit=0
- 若此时为高 → 高电平将持续70μs → bit=1
✅ 此方法避免精确计时,提高鲁棒性。
(5)读取完整40bit数据 dht11_read_data()
static int dht11_read_data(unsigned char * data)
{int i = 0;int j = 0;for(i = 0; i < 5; i++) // 5字节{for(j = 0; j < 8; j++) // 每字节8位,高位在前{char tmp = dht11_get_bit();if(tmp < 0)return -6; // 读取bit出错data[i] <<= 1; // 左移腾出最低位data[i] |= tmp; // 将新bit填入最低位}}return 5; // 成功读取5字节
}
🧩 功能:按高位在前顺序组装5字节数据到
data[5]
数组。
⚠️ 若任一bit读取失败(返回负值),立即返回错误码-6
。
(6)文件操作接口(file_operations
)
static int open(struct inode * node, struct file * file)
{printk("kernel dht11 open ...\n");return 0;
}static ssize_t read(struct file * file, char __user * buf, size_t len, loff_t * loff)
{unsigned char data[5] = {0};int ret = 0;dht11_start(); // 发送起始信号ret = dht11_wait_respon(); // 等待响应if(ret < 0)return ret; // 响应失败,返回错误码ret = dht11_read_data(data); // 读取40bit数据if(ret < 0)return ret; // 读取失败ret = copy_to_user(buf, data, sizeof(data)); // 拷贝到用户空间printk("kernel dht11 read ...\n");return ret; // 返回未拷贝字节数(0表示成功)
}static ssize_t write(struct file * file, const char __user * buf, size_t len, loff_t * loff)
{return 0; // 不支持写操作
}static int close(struct inode * node, struct file * file)
{printk("kernel dht11 close ...\n");return 0;
}
📤 read函数流程:
- 发起通信
- 等待响应
- 读取数据
- 拷贝到用户空间
✅ 注意:未在校验失败时丢弃数据(需用户层校验)
(7)平台驱动与设备注册
static struct file_operations fops =
{.owner = THIS_MODULE,.open = open,.read = read,.write = write,.release = close
};static struct miscdevice misc =
{.minor = MISC_DYNAMIC_MINOR,.name = DEV_NAME,.fops = &fops
};static int probe(struct platform_device * pdev)
{struct device_node * node;int ret = misc_register(&misc); // 注册misc设备if(ret < 0)goto err_misc_register;node = of_find_node_by_path("/mydht11"); // 查找设备树节点if(NULL == node){ret = PTR_ERR(node);goto err_dts;}gpio_dht11 = of_get_named_gpio(node, "gpio-dht11", 0); // 获取GPIO编号if(gpio_dht11 < 0){ret = gpio_dht11;goto err_dts;}gpio_request(gpio_dht11, "gpiodht11"); // 申请GPIOgpio_direction_output(gpio_dht11, 1); // 初始化为输出高电平printk("dht11 probe ############################ ***\n");return 0;err_dts:
err_misc_register:misc_deregister(&misc);return ret;
}static int remove(struct platform_device * pdev)
{gpio_free(gpio_dht11);misc_deregister(&misc);printk("dht11 remove ############################\n");return 0;
}static const struct of_device_id dht11_table[] =
{{.compatible = "mydht11"},{}
};static struct platform_driver drv =
{.probe = probe,.remove = remove,.driver = {.name = DEV_NAME,.of_match_table = dht11_table}
};static int __init dht11_driver_init(void)
{int ret = platform_driver_register(&drv); if(ret < 0)goto err_driver_register;printk("dht11 platform_driver_register ...\n");return 0;err_driver_register:platform_driver_unregister(&drv);return ret;
}static void __exit dht11_driver_exit(void)
{platform_driver_unregister(&drv);printk("dht11 platform_driver_unregister ...\n");
}module_init(dht11_driver_init);
module_exit(dht11_driver_exit);
MODULE_LICENSE("GPL");
🧱 架构说明:
- 使用 miscdevice 简化设备注册(自动分配次设备号)
- 通过 设备树 获取GPIO引脚
- 标准 platform_driver 框架,支持热插拔
3. 编译与加载驱动
vim drivers/char/Makefile
# 新增:
obj-m += dht11_1.omake modules
cp drivers/char/dht11_1.ko ~/nfs/imx6/rootfs
📦 说明:将驱动编译为模块(
.ko
),拷贝到目标文件系统。
4. 用户空间测试程序(dht11_app.c
)
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <string.h>
#include <sys/ioctl.h>int main(int argc, const char *argv[])
{int fd = open("/dev/dht11", O_RDWR); // 打开设备节点if(fd < 0){perror("open dht11 ");return -1;}int i = 0;unsigned char data[5] = {0};while(1){int ret = read(fd, data, sizeof(data)); // 读取5字节printf("ret = %d\n", ret);perror("read dht11");for(i = 0; i < 5; i++){printf("%x\t", data[i]); // 打印十六进制}printf("\n");sleep(2); // DHT11要求 ≥1秒间隔,此处2秒更安全}close(fd);return 0;
}
🖥️ 编译与运行:
arm-linux-gnueabihf-gcc dht11_app.c -o dht11_app
# 目标板操作:
tftp 0x80800000 zImage
tftp 0x83000000 pt.dtb
bootz 0x80800000 - 0x83000000
insmod dht11_1.ko
./dht11_app
💡 理想运行结果:
kernel dht11 open ...
kernel dht11 read ...
ret = 0
read dht11: Success
32 0 18 5 4f
kernel dht11 read ...
ret = 0
read dht11: Success
32 0 18 8 52
...
📊 数据解析示例:
32 0 18 5 4f
→ 湿度=0x32=50% RH,温度=0x18=24℃,校验=0x32+0+0x18+5=0x4F → 有效32 0 18 8 52
→ 湿度=50%,温度=24℃,校验=0x32+0+0x18+8=0x52 → 有效
⚠️ 注意:驱动未做校验,需用户程序自行验证第5字节。
四、使用注意事项与调试建议
- 采样频率:DHT11最大采样率 1Hz,两次读取间隔 ≥1秒
- 超时机制:所有等待循环必须设超时,防止死锁
- GPIO模式切换:
- 发送起始信号 → 输出模式
- 接收响应和数据 → 输入模式
- 校验处理:驱动层未校验,用户程序必须验证校验位
- 常见问题排查:
- 返回
-1
/-2/-3:检查GPIO连接、上拉电阻、时序 - 数据全0或固定值:可能未正确切换输入模式
- 校验频繁失败:检查电源稳定性或读取间隔是否太短
- 返回
五、上下文使用规范(Linux驱动)
上下文类型 | 允许操作 | 禁止操作 |
---|---|---|
中断上下文 (ISR, softirq, tasklet) | 自旋锁、udelay | 休眠、msleep 、copy_to_user 、printk (部分) |
进程上下文 (open/read/write/workqueue) | 互斥锁、信号量、msleep 、copy_to_user 、printk | — |
✅ 本驱动中
read
属于进程上下文,可安全使用msleep
/udelay
/printk
。
✅ 总结
本日学习涵盖:
- DHT11单总线通信协议(起始信号、响应、数据格式、校验)
- bit时序判别原理(高电平长短区分0/1)
- Linux平台驱动开发(设备树、miscdevice、GPIO控制)
- 用户空间测试程序编写
- 驱动编译、加载与调试流程
💡 核心思想:硬件通信依赖精确时序,驱动需严格遵循协议,并加入超时保护与错误处理。