DHT11 是低成本、单总线通信的温湿度传感器,广泛应用于物联网、工业控制等场景。本文基于 NXP MX6UL 处理器,从设备树配置、内核驱动开发、应用层调用三个维度,详细拆解 DHT11 的 Linux 驱动实现逻辑,结合实际代码片段分析关键技术点。
一、DHT11 通信原理基础
DHT11 采用单总线协议与主控通信,核心时序要求如下:
- 主机发起起始信号:拉低总线至少 18ms,然后释放总线(拉高),等待从机回应;
- 从机回应信号:DHT11 检测到起始信号后,拉低总线 80μs,再拉高 80μs,告知主机准备传输数据;
- 数据传输 :从机依次传输 40 位数据(5 字节),格式为:
湿度整数字节 + 湿度小数字节 + 温度整数字节 + 温度小数字节 + 校验和字节; - 校验规则:前 4 字节之和等于第 5 字节,校验失败则数据无效。
注:DHT11 的小数位实际为 0,本文代码保留小数位解析逻辑,适配 DHT22(小数位有效)也可复用。
二、设备树配置解析
基于 MX6UL 的设备树需完成引脚复用 和设备节点两大配置,以下是核心片段分析:
1. 引脚复用配置(pinctrl_dht11)
dts
pinctrl_dht11: putedht11 {
fsl,pins = <
MX6UL_PAD_JTAG_TDI__GPIO1_IO13 0x10B0
>;
};
MX6UL_PAD_JTAG_TDI__GPIO1_IO13:将 JTAG_TDI 引脚复用为 GPIO1_IO13(DHT11 的单总线引脚),MX6UL 的引脚复用通过预定义宏实现,需匹配芯片手册;0x10B0:引脚电气属性配置,拆解为:0x10:速率配置(100MHz);0xB0:上下拉 / 驱动能力配置(默认上拉、驱动能力等级等),具体需参考 MX6UL 引脚配置手册。
2. 设备节点配置(putedht11)
dts
putedht11 {
compatible = "pute,putedht11";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_dht11>;
gpio-dht11 = <&gpio1 13 0>;
status = "disable";
};
compatible = "pute,putedht11":驱动匹配关键标识,需与驱动中of_match_table的compatible字段完全一致,是平台驱动与设备树匹配的核心;pinctrl-names + pinctrl-0:绑定引脚复用配置,"default" 表示默认状态下使用pinctrl_dht11的引脚配置;gpio-dht11 = <&gpio1 13 0>:指定 DHT11 使用的 GPIO 引脚:&gpio1(GPIO1 控制器)、13(引脚号)、0(默认低电平 / 无极性,无实际意义,仅为格式要求);status = "disable":默认禁用,需改为"okay"才能让内核识别该设备节点。
三、内核驱动代码深度剖析
驱动基于 Linux平台驱动框架 + 混杂设备(miscdevice) 实现,既利用平台驱动适配设备树,又通过混杂设备简化字符设备注册(无需手动分配主设备号)。核心代码分模块解析如下:
1. 核心变量与初始化
c
运行
static int gpiono = 0; // 存储DHT11对应的GPIO编号
static spinlock_t lock; // 自旋锁,保护时序临界区
- 自旋锁用于保护 DHT11 时序操作(单总线时序要求严格,禁止中断打断),通过
spin_lock_irqsave/spin_unlock_irqrestore实现中断上下文中的临界区保护。
2. DHT11 时序实现函数
(1)起始信号发送(dht11_init)
c
运行
static void dht11_init(void)
{
gpio_direction_output(gpiono, 1);
gpio_set_value(gpiono, 0);
msleep(20); // 拉低至少18ms,满足DHT11时序要求
gpio_set_value(gpiono, 1); // 释放总线,等待从机回应
return;
}
- 先将 GPIO 设为输出并拉高,再拉低 20ms(满足最低 18ms 要求),最后释放总线,触发 DHT11 回应。
(2)从机回应检测(dht11_reply)
c
运行
static int dht11_reply(void)
{
int cnt = 0;
gpio_direction_input(gpiono); // 切换为输入,检测从机回应
// 等待从机拉低总线(超时保护:10*10μs=100μs)
while (gpio_get_value(gpiono) == 1) {
if (cnt++ > 10) {
pr_info("dht11 no respond timeout\n");
return -1;
}
udelay(10);
}
while(gpio_get_value(gpiono) == 0); // 等待从机拉低结束(80μs)
return 0;
}
- 切换 GPIO 为输入模式,检测从机是否拉低总线:若超时未检测到,返回错误;否则等待从机拉高总线(回应完成)。
(3)数据读取(dht11_read_data)
c
运行
static int dht11_read_data(unsigned char *pdata, int datalen)
{
int i = 0, cnt = 0, j = 0;
while(gpio_get_value(gpiono) == 1); // 等待从机拉高结束
for(j = 0; j < datalen; j++) { // 读取5字节数据
for(i = 7; i >= 0; i--) { // 逐位读取(高位先行)
cnt = 0;
while(gpio_get_value(gpiono) == 0); // 等待数据位起始(低电平)
// 统计高电平持续时间:判断0/1(DHT11:0≈26-28μs,1≈70μs)
while(gpio_get_value(gpiono) == 1) {
cnt++;
udelay(10);
}
if(cnt > 5) { // 5*10μs=50μs,大于50μs判定为1
pdata[j] |= (1 << i);
}
}
}
return 0;
}
- 核心逻辑:通过统计高电平持续时间区分 0/1(DHT11 的 0 对应高电平≈28μs,1 对应≈70μs);
- 代码中
cnt > 5(即 50μs)作为阈值,兼顾时序误差,工程上需根据实际硬件微调。
(4)数据校验(dht11_data_check)
c
运行
static int dht11_data_check(unsigned char *pdata, int datalen)
{
int i = 0, sum = 0;
for(i = 0; i < datalen - 1; i++) {
sum += pdata[i]; // 前4字节求和
}
if(sum == pdata[4]) { // 与校验和对比
return 0;
}
return -1;
}
- 校验是 DHT11 数据有效性的关键,若校验失败,需重新读取(驱动中直接返回错误,由应用层重试)。
3. 混杂设备核心接口(read 函数)
c
运行
static ssize_t dht11_read(struct file *fp, char __user *puser, size_t n, loff_t *off)
{
unsigned long nret = 0;
unsigned char data[5] = {0};
int ret = 0;
unsigned long flag = 0;
dht11_init(); // 发送起始信号
spin_lock_irqsave(&lock, flag); // 上锁,保护时序
ret = dht11_reply(); // 检测回应
if (ret < 0) { spin_unlock_irqrestore(&lock, flag); return -1; }
dht11_read_data(data, sizeof(data)); // 读取数据
spin_unlock_irqrestore(&lock, flag); // 解锁
ret = dht11_data_check(data, sizeof(data)); // 校验数据
if (ret < 0) { return -1; }
nret = copy_to_user(puser, &data, sizeof(data)); // 数据拷贝到用户空间
if (nret != 0) { return -1; }
return 0;
}
copy_to_user:内核空间数据拷贝到用户空间的核心函数,返回值为未拷贝的字节数,需判断是否为 0;- 自旋锁仅包裹 "回应检测 + 数据读取"(时序敏感区),避免锁粒度太大影响系统性能。
4. 平台驱动与混杂设备注册
(1)文件操作集与混杂设备
c
运行
static struct file_operations fops = {
.owner = THIS_MODULE,
.read = dht11_read, // 绑定read接口
};
static struct miscdevice dht11_misc = {
.minor = MISC_DYNAMIC_MINOR, // 动态分配次设备号
.name = "dht11_misc", // 设备节点名:/dev/dht11_misc
.fops = &fops,
};
- 混杂设备无需手动调用
register_chrdev,通过misc_register即可完成设备注册,简化开发。
(2)平台驱动探针函数(probe)
c
运行
static int dht11_probe(struct platform_device *pdevice)
{
int ret = 0;
// 1. 注册混杂设备
ret = misc_register(&dht11_misc);
if (ret != 0) { goto err_mis_register; }
// 2. 从设备树获取GPIO编号
gpiono = of_get_named_gpio(pdevice->dev.of_node, "gpio-dht11", 0);
if (gpiono < 0) { goto err_find_resource; }
// 3. 请求GPIO使用权(devm_前缀:自动释放,避免内存泄漏)
ret = devm_gpio_request(dht11_misc.this_device, gpiono, "dht11_drv");
if (ret != 0) { goto err_find_resource; }
// 4. GPIO初始化(输出高电平)
gpio_direction_output(gpiono, 1);
// 5. 初始化自旋锁
spin_lock_init(&lock);
return 0;
err_find_resource:
misc_deregister(&dht11_misc);
err_mis_register:
return -1;
}
of_get_named_gpio:从设备树节点中读取gpio-dht11属性对应的 GPIO 编号;devm_gpio_request:GPIO 请求的 "资源管理版",驱动卸载时自动释放 GPIO,避免手动释放遗漏;- 错误处理:采用 goto 语句,保证出错时反向释放已注册的资源(如混杂设备)。
(3)平台驱动匹配与注册
c
运行
static const struct of_device_id dht11_of_match_table[] = {
{.compatible = "pute,putedht11"}, // 匹配设备树compatible
{},
};
static struct platform_driver dht11_drv = {
.probe = dht11_probe,
.remove = dht11_remove,
.driver = {
.name = "putedht11",
.of_match_table = dht11_of_match_table,
},
};
module_init(dht11_drv_init); // 注册平台驱动
module_exit(dht11_drv_exit); // 卸载平台驱动
of_match_table:设备树匹配表,内核启动时根据设备树节点的compatible字段匹配驱动,触发probe函数。
四、应用层代码解析
应用层通过操作/dev/dht11_misc设备文件与驱动交互,核心逻辑如下:
c
运行
int main(void)
{
int fd = 0;
unsigned char data[5] = {0};
// 打开混杂设备节点
fd = open("/dev/dht11_misc", O_RDWR);
if (-1 == fd) { perror("fail to open"); return -1; }
// 循环读取温湿度
while (1)
{
read(fd, data, sizeof(data)); // 触发驱动read函数
// 解析数据:湿度(data[0].data[1])、温度(data[2].data[3])
printf("temp:%d.%d hum:%d.%d\n", data[2], data[3], data[0], data[1]);
sleep(2); // DHT11采样周期至少1秒,这里设2秒
}
close(fd);
return 0;
}
open:打开设备节点,内核触发file_operations的open接口(本文未实现,使用默认);read:调用一次触发驱动的dht11_read函数,读取 5 字节数据;- 数据解析:DHT11 小数位为 0,实际输出如
temp:25.0 hum:60.0。
五、工程测试与问题排查
1. 测试步骤
- 修改设备树 :将
status = "disable"改为status = "okay",编译并烧写设备树; - 编译驱动:编写 Makefile(指定内核源码路径),编译为.ko 模块;
- 加载驱动 :
insmod dht11_drv.ko,查看内核日志dmesg,确认probe函数执行成功; - 编译应用程序 :
gcc dht11_app.c -o dht11_app; - 运行应用 :
./dht11_app,查看温湿度输出。
2. 常见问题排查
表格
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 设备打开失败(-1) | 驱动未加载 / 设备树 status 未改 / 节点名不匹配 | 检查lsmod、设备树、/dev/dht11_misc是否存在 |
| 回应超时(reply timeout) | 硬件接线错误 / GPIO 配置错误 / 时序延迟不准确 | 检查接线、GPIO 编号、udelay/msleep参数 |
| 数据校验失败 | 时序阈值不合理 / 总线干扰 / 传感器故障 | 微调cnt > 5的阈值、增加总线上拉电阻、更换传感器 |
| copy_to_user 失败 | 用户空间缓冲区大小不足 / 权限问题 | 确保读取长度为 5 字节、以 root 权限运行应用 |
六、总结
本文基于 MX6UL 实现 DHT11 驱动,要点如下:
- 设备树 :重点关注
compatible匹配、GPIO 编号、引脚复用配置; - 驱动层:时序精准性(自旋锁保护、延迟函数选择)、数据校验、混杂设备简化注册;
- 应用层:通过标准字符设备接口与驱动交互,逻辑简洁。