当前位置: 首页 > news >正文

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函数流程

  1. 发起通信
  2. 等待响应
  3. 读取数据
  4. 拷贝到用户空间
    注意未在校验失败时丢弃数据(需用户层校验)

(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字节。


四、使用注意事项与调试建议

  1. 采样频率:DHT11最大采样率 1Hz,两次读取间隔 ≥1秒
  2. 超时机制:所有等待循环必须设超时,防止死锁
  3. GPIO模式切换
    • 发送起始信号 → 输出模式
    • 接收响应和数据 → 输入模式
  4. 校验处理:驱动层未校验,用户程序必须验证校验位
  5. 常见问题排查
    • 返回 -1/-2/-3:检查GPIO连接、上拉电阻、时序
    • 数据全0或固定值:可能未正确切换输入模式
    • 校验频繁失败:检查电源稳定性或读取间隔是否太短

五、上下文使用规范(Linux驱动)

上下文类型允许操作禁止操作
中断上下文
(ISR, softirq, tasklet)
自旋锁、udelay休眠、msleepcopy_to_userprintk(部分)
进程上下文
(open/read/write/workqueue)
互斥锁、信号量、msleepcopy_to_userprintk

✅ 本驱动中 read 属于进程上下文,可安全使用 msleep/udelay/printk


✅ 总结

本日学习涵盖:

  • DHT11单总线通信协议(起始信号、响应、数据格式、校验)
  • bit时序判别原理(高电平长短区分0/1)
  • Linux平台驱动开发(设备树、miscdevice、GPIO控制)
  • 用户空间测试程序编写
  • 驱动编译、加载与调试流程

💡 核心思想:硬件通信依赖精确时序,驱动需严格遵循协议,并加入超时保护错误处理

http://www.dtcms.com/a/506820.html

相关文章:

  • 【代码管理】在本地使用github和gitee之后,可能存在冲突,导致再次提交代码时提示Couldn‘t connect to server
  • bash:**:pip:***python: 错误的解释器: 没有那个文件或目录
  • OpenCV(十二):Mat
  • iOS 智能应用开发实践:从模型集成到场景化交互
  • 更好的网站制作系统平台
  • 佛山市手机网站建设网站建设管理工作情况的通报
  • ThinkPad 安装 Ubuntu 系统教程
  • 《未来的 AI 操作系统(四)——AgentOS 的内核设计:调度、记忆与自我反思机制》
  • Platform Health Management 与 EXM/STM 的集成实现方式
  • lambda怎么遍历集合
  • 国外客户推广网站做羞羞事的网站
  • 安装好PySide6后如何找到Qt Designer(pyside6-designer.exe)可执行文件
  • EIT/ERT技术在机器人触觉传感的硬件及电路实现
  • h5游戏免费下载:公园停车
  • FPGA 49 ,Xilinx Vivado 软件术语解析(Vivado 界面常用英文字段详解,以及实际应用场景和注意事项 )
  • 自动化漏洞利用技术颠覆传统:微软生态暴露的攻防新变局
  • Annals of Neurology | EEG‘藏宝图’:用于脑电分类、聚类与预测的语义化低维流形
  • 中小学网站建设有什么好处管理系统软件开发
  • uniapp canvas实现手写签字功能(包括重签,撤回等按钮)
  • 大语言模型如何精准调用函数—— Function Calling 系统笔记
  • 商业智能BI 浅谈数据孤岛和数据分析的发展
  • Chrome 浏览器扩展图片 提取大师
  • Uniapp微信小程序开发:修改了数据,返回父页面时,父页面数据重新加载
  • etcd-问题-调优-监控
  • 【国内电子数据取证厂商龙信科技】手机版Chrome调试方法
  • 做企业网站要怎么设计方案信产部网站备案
  • 成都爱站网seo站长查询工具上海跨境电商网站制作
  • Linux网络编程:Socket编程TCP
  • 库周报 | 25亿融资!天兵科技冲刺IPO;双十一3D打印机价格战打响;拓竹、爱乐酷等发新品
  • 替代传统电脑的共享云服务器如何实现1拖8SolidWorks设计办公