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>这个头文件。

  1. 主机向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);
  1. 主机接收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;
    }

  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高电平的应答信号,以及后面的一点点数据位,验算了一下,示波器显示的结果和代码中显示的结果一样。
在这里插入图片描述

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐