手把手教你写dht11温湿度传感器驱动——Linux驱动
dht11是通过单总线协议同主机通讯,其对时序的要求非常严格!!!(划重点,后面会因为这个踩坑)。dht11的时序,其实也蛮简单。首先,主机拉低18ms,再拉高20-40us,作为启动dht11的信号(如下方第一张图所示)。然后,dht11发送80us的低电平和80us的高电平作为应答信号(如下方第一张图所示)。最后dht11开始发送数据。50us的低电平,30us左右的高电平为数据0(下方第二张
0.背景
背景是我给别人复刻智能家居项目,发现dht11的读取数值很奇怪,温度才10几度,不符合现在夏天房间的基本温度。这个驱动是我根据开源的智能家居基础上修改的。后来改成源码上的驱动,发现也只能成功获取几次,后续居然能读出121%湿度的值。于是我打算重整旗鼓,自己写一套dht11的驱动程序(急性子可以拉到最下面的代码讲解)。
1. dht11简介以及数据格式
dht11是通过单总线协议同主机通讯,其对时序的要求非常严格!!!(划重点,后面会因为这个踩坑)。dht11的时序,其实也蛮简单。
首先,主机拉低18ms,再拉高20-40us,作为启动dht11的信号(如下方第一张图所示)。
然后,dht11发送80us的低电平和80us的高电平作为应答信号(如下方第一张图所示)。
最后dht11开始发送数据。50us的低电平,30us左右的高电平为数据0(下方第二张图);50us的低电平,70us左右的高电平为数据1(下方第三张图)。总共会发送40位数据,湿度高8位+湿度低8位+温度高8位+温度低8位+8bit校验位,校验位等于前几位的和。


懒的找图了,这几张图是从TmCmT的博客《DHT11读写时序详解(时序+上板实测)》里拿的,链接是
https://blog.csdn.net/weixin_53042232/article/details/141131210?fromshare=blogdetail&sharetype=blogdetail&sharerId=141131210&sharerefer=PC&sharesource=qq_53772633&sharefrom=from_link
2.代码
2.1 硬件环境以及设备树
我使用的是正点原子的阿尔法板,芯片是imx6ull。设备树部分的代码如下所示。这些配置是啥意思,可以自己用AI搜搜,重点主要是放在时序的代码上面。
hc_dht11 {
compatible = "hc-dht11";
pinctrl-name = "default";
pinctrl-0 = <&pinctrl_dht11>;
gpios = <&gpio1 2 GPIO_ACTIVE_HIGH>;
my_name = "dht11";
status = "okay";
};
/* hc dht11 */
pinctrl_dht11: dht11grp {
fsl,pins = <
MX6UL_PAD_GPIO1_IO02__GPIO1_IO02 0x0001B0B0 /* dht11 00111000010110000 70b0 */
>;
};
2.2 dht11代码解析
其实在Linux下编程,还是有蛮多好处,有很多已经封装好的api。比如ktime_get_ns();,可以获取当前的系统时间,是以ns的单位,需要引入#include <linux/ktime.h>这个头文件。
- 主机向dht11发送启动信号
首先是主机向dht11发生启动信号,将gpio_direction_output(DHT11_PIN, 1) 将DHT11_PIN更改为输出模式,默认输出电平为高,相当于一个复位信号。接下来,gpio_set_value(DHT11_PIN, 0)拉低,延时20ms,再将其拉高20us,表示启动信号结束。然后速度将连接dht11的gpio设置为输入模式,等待dht11的应答信号。
这里曾存在一些问题,原本在gpio_direction_input(DHT11_PIN)代码下面是有一个20us的延时的,在这行代码上面的延时是40us,不是现在的20us,但我发现,这样子的话,会导致在读取dht11应答信号中的低电平信号时,只有40us的低电平,后来我将原本这行代码上面的40us改为20us,再将这行代码的下面的延时20us给去掉,发现就能检测到80us的低电平。猜测是在运行这些api函数本身也需要时间,过长的延时,会导致无法接收到完整的低电平信号。举个例子就是,假设,dht11在系统时间为8000us的时候发送了80us的低电平应答信号,但由于我运行api函数的延时,实际我在8040us才去接收这个低电平信号,自然就导致了只接收到了40us。
/* 1. 发送高脉冲启动DHT11 */
/* reset */
gpio_direction_output(DHT11_PIN, 1);
/* start */
mdelay(2);
gpio_set_value(DHT11_PIN, 0);
mdelay(20);
gpio_set_value(DHT11_PIN, 1);
udelay(20);
// 配置引脚为输入,准备接收DHT11传来的数据
gpio_direction_input(DHT11_PIN);
- 主机接收dht11的应答信号
接下来是主机接收dht11的应答信号,先是记录当前时间在current_time里,主要的目的是防止接收应答信号超时,没有接收到信号,一直在下面的while循环卡着,会导致程序崩溃。第一个while循环等待低电平信号,如果等到了,跳出while循环,记录当前时间在low_start,即低电平开始时间;如果没等到,循环等待,等不到,那就报错。第二个while循环,等到低电平的结束时间,如果等到了,那立即用当前时间减去low_start得到low_duration,即低电平持续时间。当低电平小于我设定的最小时间和最大时间,则报错,理想情况下应该是80us。
紧接着,同样的逻辑,记录当前时间为high_start,因为上述的低电平结束时间,其实就是高电平的开始时间,所以我先记录下这个高电平开始时间。第三个while循环,等待高电平结束,就可以计算出高电平的持续时间high_dutation。如果高电平的持续时间,也没有问题,那就说明主机接收到了dht11的应答信号,接下来主机可以接收dht11发送来的数据了。
这边有个细节,我的printk全是只有出错才打印,没啥问题我不打印。我之前在测试时,为了看看低电平和高电平持续时间是多少,用prink打印了些信息出来,但后面的读取dht11数据的信息就出错了。后来我又想在应答信号这边看看读取的电平是什么样子,发现原本没问题的接收应答信号代码,也出错了,我把prink打印信息一注释掉,就好了。后续查了一下,发现prink并非原子操作,它是要进去内核的,他的执行时间大概是ms级别的,对于dht11这种us级别,对时序敏感就容易出问题。
/* 2. 等待DHT11就绪(说不定高低高才是正确顺序) */
current_time = ktime_get_ns();
// 2.1 等待低电平开始(DHT11应答)
while (gpio_get_value(DHT11_PIN)) {
if (ktime_get_ns() - current_time > TIMEOUT_NS(100)) {
printk("错误:未检测到低电平应答开始\n");
return -1;
}
}
low_start = ktime_get_ns();
// 2.2 等待低电平结束(高电平开始)
while (!gpio_get_value(DHT11_PIN)) {
if (ktime_get_ns() - low_start > TIMEOUT_NS(100)) {
printk("错误:低电平持续时间过长\n");
return -1;
}
}
low_duration = ktime_get_ns() - low_start;
if (low_duration < MIN_DURATION_NS || low_duration > MAX_DURATION_NS) {
printk("错误:低电平时间 %lld ns 异常\n", low_duration);
return -1;
}
// 2.3 记录高电平开始时间
high_start = ktime_get_ns();
// 2.4 等待高电平结束
while (gpio_get_value(DHT11_PIN)) {
if (ktime_get_ns() - high_start > TIMEOUT_NS(100)) {
printk("错误:高电平持续时间过长\n");
return -1;
}
}
// 2.5. 计算高电平持续时间
high_duration = ktime_get_ns() - high_start;
if (high_duration < MIN_DURATION_NS || high_duration > MAX_DURATION_NS) {
printk("错误:高电平时间 %lld ns 异常\n", high_duration);
return -1;
}
- 开始接收数据
接收数据代码如下所示,接收5个数据,一个数据8位。这边的逻辑是两个for循环的嵌套。dht11通过高电平的时长来判断数据0和1。无论0和1,都有一段低电平,我首先是读取低电平,然后看看低电平有没有超时,没有超时的话,再去获取高电平,计算高电平持续时间,当高电平持续时间大于50us,那就将1左移j位,存储起来。
/* 3. 读5字节数据 */
for (i = 0; i < 5; i++) {
int j;
for (j = 7; j >= 0; j--) {
//先获取低电平
current_time = ktime_get_ns();
while (gpio_get_value(DHT11_PIN)) {
if (ktime_get_ns() - current_time > TIMEOUT_NS(50)) {
printk("错误:高电平持续时间过长1\n");
return -1;
}
}
low_start = ktime_get_ns();
// 等待高电平开始
while (!gpio_get_value(DHT11_PIN)) {
if (ktime_get_ns() - low_start > TIMEOUT_NS(70)) {
printk("错误:低电平持续时间过长\n");
return -1;
}
}
high_start = ktime_get_ns();
// 等待高电平结束
while (gpio_get_value(DHT11_PIN)) {
if (ktime_get_ns() - high_start > TIMEOUT_NS(100)) {
printk("错误:高电平持续时间过长\n");
return -1;
}
}
high_duration = ktime_get_ns() - high_start;
//calculate how long 1
if (high_duration > 50000){
data[i] |= (1 << j);
}
}
}
2.3 完整代码
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/gpio.h>
#include <linux/uaccess.h>
#include <linux/string.h>
#include <linux/interrupt.h>
#include <linux/irqreturn.h>
#include <linux/of_gpio.h>
#include <linux/slab.h>
#include <linux/device.h>
#include <linux/fs.h>
#include <linux/kernel.h>
#include <linux/wait.h>
#include <linux/sched.h>
#include <linux/timer.h>
#include <linux/gpio/consumer.h>
#include <linux/delay.h>
#include <linux/timekeeping.h>
#include <linux/wait.h>
#include <linux/irqflags.h>
#include <linux/ktime.h>
static int major;
static struct class * class;
//static int irq;
static struct gpio_desc *dht11_desc;
static int dht11_gpio;
static DECLARE_WAIT_QUEUE_HEAD(waitqueue);
static struct timer_list dht11_timer;
static int DHT11_PIN;
static u8 data[5] = {0};
static int read_dht11_data(void);
#define TIMEOUT_NS(us) ((us) * 1000) // 微秒转纳
#define MIN_DURATION_NS 40000 // 60µs
#define MAX_DURATION_NS 100000 // 100µs
static int dht11_probe(struct platform_device *pdev)
{
/* get gpio_pin from device tree */
struct device_node *node = pdev->dev.of_node;
dht11_gpio = of_get_gpio(node, 0);
dht11_desc = gpio_to_desc(dht11_gpio);
DHT11_PIN = dht11_gpio; //在源代码中修改的,多此一举
printk("DHT11_PIN = %d, dht11_gpio = %d\n", DHT11_PIN, dht11_gpio);
gpiod_direction_output(dht11_desc, 1);
printk("dht11 probe run\n");
return 0;
}
static int dht11_remove(struct platform_device *dev)
{
return 0;
}
static const struct of_device_id dht11_of_match[] = {
{ .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;
}
static int read_dht11_data(void)
{
memset(data, 0, sizeof(data)); // 每次进入函数时重置
u8 checksum = 0;
int i;
int timeout = 200;
u64 current_time;
u64 last_time;
u64 low_start, low_end, high_start, low_duration, high_duration;
/* 1. 发送高脉冲启动DHT11 */
/* reset */
gpio_direction_output(DHT11_PIN, 1);
/* start */
mdelay(2);
gpio_set_value(DHT11_PIN, 0);
mdelay(20);
gpio_set_value(DHT11_PIN, 1);
udelay(20);
// 配置引脚为输入,准备接收DHT11传来的数据
gpio_direction_input(DHT11_PIN);
/* 2. 等待DHT11就绪(说不定高低高才是正确顺序) */
current_time = ktime_get_ns();
// 2.1 等待低电平开始(DHT11应答)
while (gpio_get_value(DHT11_PIN)) {
if (ktime_get_ns() - current_time > TIMEOUT_NS(100)) {
printk("错误:未检测到低电平应答开始\n");
return -1;
}
}
low_start = ktime_get_ns();
// 2.2 等待低电平结束(高电平开始)
while (!gpio_get_value(DHT11_PIN)) {
if (ktime_get_ns() - low_start > TIMEOUT_NS(100)) {
printk("错误:低电平持续时间过长\n");
return -1;
}
}
low_duration = ktime_get_ns() - low_start;
if (low_duration < MIN_DURATION_NS || low_duration > MAX_DURATION_NS) {
printk("错误:低电平时间 %lld ns 异常\n", low_duration);
return -1;
}
// 2.3 记录高电平开始时间
high_start = ktime_get_ns();
// 2.4 等待高电平结束
while (gpio_get_value(DHT11_PIN)) {
if (ktime_get_ns() - high_start > TIMEOUT_NS(100)) {
printk("错误:高电平持续时间过长\n");
return -1;
}
}
// 2.5. 计算高电平持续时间
high_duration = ktime_get_ns() - high_start;
if (high_duration < MIN_DURATION_NS || high_duration > MAX_DURATION_NS) {
printk("错误:高电平时间 %lld ns 异常\n", high_duration);
return -1;
}
/* 3. 读5字节数据 */
for (i = 0; i < 5; i++) {
int j;
for (j = 7; j >= 0; j--) {
//先获取低电平
current_time = ktime_get_ns();
while (gpio_get_value(DHT11_PIN)) {
if (ktime_get_ns() - current_time > TIMEOUT_NS(50)) {
printk("错误:高电平持续时间过长1\n");
return -1;
}
}
low_start = ktime_get_ns();
// 等待高电平开始
while (!gpio_get_value(DHT11_PIN)) {
if (ktime_get_ns() - low_start > TIMEOUT_NS(70)) {
printk("错误:低电平持续时间过长\n");
return -1;
}
}
high_start = ktime_get_ns();
// 等待高电平结束
while (gpio_get_value(DHT11_PIN)) {
if (ktime_get_ns() - high_start > TIMEOUT_NS(100)) {
printk("错误:高电平持续时间过长\n");
return -1;
}
}
high_duration = ktime_get_ns() - high_start;
//calculate how long 1
if (high_duration > 50000){
data[i] |= (1 << j);
}
}
}
//读取结束,拉高回到默认状态
gpio_direction_output(DHT11_PIN, 1);
mdelay(2);
//gpio_free(DHT11_PIN);
checksum = data[0] + data[1] + data[2] + data[3];
if (checksum != data[4]) {
// printk(KERN_ERR "DHT11 data checksum error\n");
// printk(KERN_INFO "DHT11 Temperature: %d°C, Humidity: %d%%\n", data[2], data[0]);
return 1;
}
// printk(KERN_INFO "DHT11 Temperature: %d°C, Humidity: %d%%\n", data[2], data[0]);
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();
local_irq_restore(flags);
if(res == 0){
// printk(KERN_INFO "DHT11 Temperature: %d°C, Humidity: %d%%\n", data[2], data[0]);
res = copy_to_user(buf, data, size);
return 0;
} else if(res == 1){
printk(KERN_ERR "DHT11 data checksum error\n");
return -1;
} else {
printk(KERN_ERR "DHT11 data read error\n");
return -2;
}
}
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)
{
major = register_chrdev(0 , "dht11", &dht11_ops);
class = class_create(THIS_MODULE, "dht11_class");
device_create(class, NULL, MKDEV(major, 0), NULL, "dht11");
platform_driver_register(&dht11_platform_driver);
return 0;
}
static void dht11_exit(void)
{
platform_driver_unregister(&dht11_platform_driver);
device_destroy(class, MKDEV(major, 0));
class_destroy(class);
unregister_chrdev(major, "dht11");
}
module_init(dht11_init);
module_exit(dht11_exit);
MODULE_LICENSE("GPL");
注意事项
在每次读取前,必须先将data清0,memset(data, 0, sizeof(data))。 不清零的话,如果发生问题,会导致新旧数据都在data数组里,使得计算校验值的时候发生问题。为什么提到这个注意事项?是我发现我的代码,只有第一次是有效的,后面就是报错校验和有误。后来我将清0的步骤也放在了开源的智能家居源码上,发现也同样能够回归正常,原作者在这个地方可能是存在了一些疏忽。
示波器验证
示波器是真好玩啊,捣鼓了一下,波形就出来了,图片中对应着正是主机发送的启动信号,dht11的80us低电平和80us高电平的应答信号,以及后面的一点点数据位,验算了一下,示波器显示的结果和代码中显示的结果一样。
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐


所有评论(0)