DHT11温湿度传感器驱动开发全解析
文章目录
-
- 概要
- DHT11原理
- 节点添加与驱动编写
- 测试程序编写(test)
- 实验测试
参考文章: https://blog.csdn.net/qq_37858386/article/details/115573565
【IMX6ULL驱动开发学习】05.IMX6ULL驱动开发_编写第一个hello驱动【熬夜肝】_imx6ull怎样开发驱动程序-CSDN博客
概要
本文介绍一下在imx6ull开发板上dht11驱动开发思路,以及对应的设备树节点与驱动代码,最后使用应用测试程序,打印出温湿度数据,驱动会写整体流程图方便读者观看,NXP官方其实已经写好了该驱动,但是作为初学者还是有必要动手写一下,了解整个设计思路,笔者发一个开源项目(多气体传感器检测,具体设计蓝牙,ADC的俩通道,LED灯与蜂鸣器,QT开发,部署,可视化动态波形图表,下面就是链接)
imx6ull项目:气体传感器检测-CSDN博客
DHT11原理介绍
下图为一次完整的传输示例,其中深黑色信号表示由主机驱动,即主机向DHT11发信号,浅灰色信号表 示DHT11驱动,即DHT11发向主机发信号。
-
主机准备发送信号
主机将连接 DHT11 的 GPIO 管脚配置为输出模式,为发送开始信号做准备。 -
主机发送开始信号
开始信号由 “低脉冲 + 高脉冲” 组成:- 低脉冲:持续时间至少 18ms;
- 高脉冲:持续 20~40μs。
-
主机切换为接收模式
发送开始信号后,主机将 GPIO 管脚切换为输入模式,准备接收 DHT11 的响应。此时总线由上拉电阻拉高,等待 DHT11 回应。 -
DHT11 发送响应信号
接收到开始信号后,DHT11 返回响应信号,同样由 “低脉冲 + 高脉冲” 组成:- 低脉冲:持续 80μs;
- 高脉冲:持续 80μs。
-
DHT11 发送数据信号
响应信号结束后,DHT11 开始传输数据,每一位数据均以 “低脉冲 + 高脉冲” 的形式表示:- 数据 0:低脉冲持续 50μs,高脉冲持续 26~28μs;
- 数据 1:低脉冲持续 50μs,高脉冲持续 70μs。
-
DHT11 发送结束信号
最后一位数据传输完成后,DHT11 将总线拉低 50μs,随后释放总线。总线在了你上拉电阻的作用下拉高,回到空闲状态,本次通信结束。
节点添加与驱动编写
1、节点添加:将上面俩段代码复制到设备树.dts中,这里自定义dht11节点,pintrl_dht11是引脚复用信息,注意如果在设备树中有其它MX6ULL_PAD_GPIO1_IO02有复用为其它功能引脚的话,最好将其屏蔽掉或者“disable”,pintrl_dht11里面意思就是复用为普通引脚GPIO1_IO02,当然使用其他引脚也可以,因为dht11为单总线型,普通引脚即可,添加节点完成后重新编译make dtbs生成设备树.dtb。
dht11 {
compatible = \"hc-dht11\";
pinctrl-name = \"default\";
pinctrl-0 = ;
gpios = ;
my_name = \"dht11\";
status = \"okay\";
};
pinctrl_dht11: dht11grp {
fsl,pins = <
MX6UL_PAD_GPIO1_IO02__GPIO1_IO02 0x000010B0 /* dht11 00111000010110000 70b0 */
>;
};
2、驱动代码编写
代码主要包含以下几个部分,代码部分我都加了注释,编写的是.c文件,之后读者需要用交叉编译器后形成.ko文件:
1. 头文件
2. 全局变量定义
3. 平台设备驱动相关:probe、remove、设备树匹配表、平台驱动结构体
4. 字符设备操作:open、read、release
5. DHT11数据读取函数:read_dht11_data
6. 模块初始化和退出函数
2.1代码部分dh11_drv_dtb.c:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
// 全局变量定义
static int major; // 字符设备主设备号
static struct class *class; // 设备类指针
static struct gpio_desc *dht11_desc; // GPIO描述符(用于操作GPIO)
static int dht11_gpio; // DHT11连接的GPIO编号
static int DHT11_PIN; // DHT11连接的GPIO管脚(实际使用的编号)
static u8 data[5] = {0}; // 存储DHT11读取的5字节数据(湿度整数、湿度小数、温度整数、温度小数、校验和)
// 函数声明
static int read_dht11_data(void); // 读取DHT11传感器数据的核心函数
static int dht11_probe(struct platform_device *pdev)
{
/* 从设备树中获取GPIO管脚编号 */
struct device_node *node = pdev->dev.of_node; // 获取设备节点
dht11_gpio = of_get_gpio(node, 0); // 从设备树读取GPIO编号(第0个属性)
dht11_desc = gpio_to_desc(dht11_gpio); // 将GPIO编号转换为GPIO描述符
DHT11_PIN = dht11_gpio; // 保存实际使用的GPIO管脚
printk(\"DHT11_PIN = %d\\n\", DHT11_PIN ); // 打印GPIO编号,用于调试
// 初始化GPIO为输出模式,默认输出高电平(总线空闲状态)
gpiod_direction_output(dht11_desc, 1);
printk(\"dht11 probe run\\n\"); // 提示probe函数执行完成
return 0;
}
static int dht11_remove(struct platform_device *dev)
{
return 0;
}
// 设备树匹配表(用于驱动与设备的匹配)
static const struct of_device_id dht11_of_match[] = {
{ .compatible = \"hc-dht11\" }, // 匹配设备树中compatible属性为\"hc-dht11\"的设备
{ } // 终止符
};
// 平台驱动结构体(定义驱动的核心信息)
static struct platform_driver dht11_platform_driver = {
.driver = {
.name = \"my_dht11\", // 驱动名称
.of_match_table = dht11_of_match, // 设备树匹配表
},
.probe = dht11_probe, // 探测函数
.remove = dht11_remove, // 移除函数
};
static int dht11_open (struct inode *node, struct file *filp)
{
return 0; // 直接返回成功
}
/**
* read_dht11_data - 读取DHT11传感器数据
*
* 功能:实现主机与DHT11的通信时序,读取40位数据(5字节),并验证校验和。
* 步骤:1. 发送开始信号;2. 等待DHT11响应;3. 读取40位数据;4. 校验和验证。
* 返回:0-读取成功;1-读取失败(响应超时或校验和错误)。
*/
static int read_dht11_data(void)
{
u8 checksum = 0; // 校验和
int i, j; // 循环变量(i:5字节数据;j:每字节的8位)
// 1. 主机发送开始信号
gpio_direction_output(DHT11_PIN, 0); // GPIO切换为输出,拉低总线
mdelay(18); // 低脉冲持续18ms(满足DHT11时序要求)
gpio_set_value(DHT11_PIN, 1); // 拉高总线
udelay(40); // 高脉冲持续40us(满足20-40us要求)
// 2. 主机切换为输入模式,准备接收DHT11响应
gpio_direction_input(DHT11_PIN);
// 3. 等待DHT11的响应信号(第一步:低脉冲80us)
if (gpio_get_value(DHT11_PIN)) { // 若总线仍为高电平,说明DHT11无响应
printk(KERN_ERR \"DHT11 sensor not responding\\n\");
return 1;
}
udelay(80); // 等待响应信号的低脉冲结束(80us)
// 4. 等待DHT11的响应信号(第二步:高脉冲80us)
if (!gpio_get_value(DHT11_PIN)) { // 若总线仍为低电平,说明DHT11响应异常
printk(KERN_ERR \"DHT11 sensor not responding\\n\");
return 1;
}
udelay(80); // 等待响应信号的高脉冲结束(80us)
// 5. 读取40位数据(5字节,每字节8位)
for (i = 0; i < 5; i++) { // 循环读取5字节
data[i] = 0; // 初始化当前字节
for (j = 7; j >= 0; j--) { // 循环读取8位(从高位到低位)
u32 bit; // 存储当前位的值(0或1)
// 等待当前位的低脉冲结束(DHT11会先拉低总线50us)
while (!gpio_get_value(DHT11_PIN));
// 低脉冲结束后,等待50us(此时高脉冲开始)
udelay(50);
// 读取高脉冲电平:若为高,说明当前位是1;否则是0
bit = gpio_get_value(DHT11_PIN);
if (bit) { // 高脉冲持续时间>50us(实际70us),判定为1
data[i] |= (1 << j); // 将当前位设置为1
udelay(70); // 等待高脉冲结束(70us - 已等待的50us = 20us,实际按需调整)
}
// 若bit为0,高脉冲持续26-28us(已在udelay(50)中覆盖,无需额外等待)
}
}
// 6. 校验和验证(前4字节之和应等于第5字节)
checksum = data[0] + data[1] + data[2] + data[3];
if (checksum != data[4]) { // 校验和不匹配,数据无效
return 1;
}
return 0; // 数据读取成功
}
static ssize_t dht11_read (struct file *filp, char __user *buf, size_t size, loff_t *offset)
{
int res; // 临时变量
unsigned long flags; // 用于保存中断状态
/* DHT11时序敏感,需进入临界区(关闭中断)防止内核调度干扰 */
local_irq_save(flags); // 保存中断状态并关闭中断
res = read_dht11_data(); // 读取DHT11数据
local_irq_restore(flags); // 恢复中断状态
// 若读取成功,将数据复制到用户空间
if (res == 0) {
res = copy_to_user(buf, data, size); // 复制数据到用户空间(返回0表示成功)
return size; // 返回实际读取的字节数(5字节)
}
return -EIO; // 读取失败,返回I/O错误
}
static int dht11_release (struct inode *node, struct file *filp)
{
return 0;
}
// 文件操作结构体(定义设备的操作接口)
static struct file_operations dht11_ops = {
.owner = THIS_MODULE, // 模块所有者
.open = dht11_open, // 打开设备
.read = dht11_read, // 读取设备
.release = dht11_release, // 关闭设备
};
static int dht11_init(void)
{
// 1. 注册字符设备(动态分配主设备号,设备名为\"dht11\")
major = register_chrdev(0 , \"dht11\", &dht11_ops);
// 2. 创建设备类(用于在/sys/class下生成目录)
class = class_create(THIS_MODULE, \"dht11_class\");
// 3. 在/dev下创建设备文件(设备名为\"dht11\")
device_create(class, NULL, MKDEV(major, 0), NULL, \"dht11\");
// 4. 注册平台驱动(用于与设备树中的设备匹配)
platform_driver_register(&dht11_platform_driver);
return 0;
}
static void dht11_exit(void)
{
// 1. 注销平台驱动
platform_driver_unregister(&dht11_platform_driver);
// 2. 删除/dev下的设备文件
device_destroy(class, MKDEV(major, 0));
// 3. 删除设备类
class_destroy(class);
// 4. 注销字符设备
unregister_chrdev(major, \"dht11\");
}
// 模块加载和卸载函数指定
module_init(dht11_init);
module_exit(dht11_exit);
MODULE_LICENSE(\"GPL\"); // 模块许可证(GPL兼容)
2.2 以下是DHT11驱动代码的大致流程图及关键步骤说明(以文本形式描述):
1. 模块初始化流程 (dht11_init
)
start
│
├─ 注册字符设备 → register_chrdev()
│ │
│ └─ 动态分配主设备号
│
├─ 创建设备类 → class_create()
│ │
│ └─ 生成 /sys/class/dht11_class
│
├─ 创建设备文件 → device_create()
│ │
│ └─ 生成 /dev/dht11
│
└─ 注册平台驱动 → platform_driver_register()
│
└─ 等待设备树匹配 (compatible = \"hc-dht11\")
end
2. 平台设备匹配流程 (dht11_probe
)
start (设备树匹配成功)
│
├─ 获取GPIO编号 → of_get_gpio()
│
├─ 转换为GPIO描述符 → gpio_to_desc()
│
└─ 初始化GPIO为输出高电平 → gpiod_direction_output()
│
└─ 总线空闲状态
end
3. 用户读取数据流程 (dht11_read
)
start (用户调用 read())
│
├─ 进入临界区 → local_irq_save()
│ │
│ └─ 禁止中断(确保时序严格)
│
├─ 读取DHT11数据 → read_dht11_data()
│ │
│ ├─ 发送开始信号(拉低18ms → 拉高40μs)
│ │
│ ├─ 切换为输入模式,等待DHT11响应(低80μs → 高80μs)
│ │
│ ├─ 循环读取5字节数据(每位:低50μs + 高26-70μs)
│ │
│ └─ 校验和验证(前4字节之和 = 第5字节)
│
├─ 退出临界区 → local_irq_restore()
│
└─ 复制数据到用户空间 → copy_to_user()
│
└─ 成功返回5字节,失败返回-EIO
end
4. 模块卸载流程 (dht11_exit
)
start
│
├─ 注销平台驱动 → platform_driver_unregister()
│
├─ 删除设备文件 → device_destroy()
│
├─ 删除设备类 → class_destroy()
│
└─ 注销字符设备 → unregister_chrdev()
end
关键时序图(read_dht11_data
内部)
主机操作:
拉低18ms → 拉高40μs → 切换为输入 → 等待DHT11响应
│ │ │
└─ 开始信号 └─ 主机准备接收 └─ DHT11响应:
低80μs → 高80μs
数据读取(每位):
等待低电平结束 → 等待50μs → 检测高电平持续时间:
│ │ │
└─ 位起始信号 └─ 判断位值 │
高>50μs → 1
高<50μs → 0
测试程序编写(test.c)
#include
#include
#include
#include
#include
int main(int argc, char *argv[])
{
int fd;
int res;
char buf[5];
fd = open(\"/dev/dht11\", O_RDONLY);
if(fd < 0)
{
printf(\"dht11 open failed\\n\");
return 0;
}
/* DHT11上电后,要等待1秒以越过不稳定状态,在此期间不能发送任何指令。 */
sleep(1);
while(1){
/* DHT11属于低速传感器,两次通信请求之间的间隔时间不能太短,一般不低于1秒。 */
res = read(fd, buf, 5);
if(res == 0)
{
printf(\"read success\\n\");
printf(\"DHT11 Temperature: %d°C, Humidity: %d%%\\n\", buf[2], buf[0]);
}
else
{
printf(\"read failed\\n\");
}
sleep(3);
}
close(fd);
return 0;
}
实验测试
最后加载驱动程序.ko文件,执行测试程序./test(这里的test也是test.c用编译器编译后形成的test),可以看到温度28,湿度25