> 技术文档 > DHT11温湿度传感器驱动开发全解析

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发向主机发信号。

  1. 主机准备发送信号
    主机将连接 DHT11 的 GPIO 管脚配置为输出模式,为发送开始信号做准备。

  2. 主机发送开始信号
    开始信号由 “低脉冲 + 高脉冲” 组成:

    • 低脉冲:持续时间至少 18ms;
    • 高脉冲:持续 20~40μs。
  3. 主机切换为接收模式
    发送开始信号后,主机将 GPIO 管脚切换为输入模式,准备接收 DHT11 的响应。此时总线由上拉电阻拉高,等待 DHT11 回应。

  4. DHT11 发送响应信号
    接收到开始信号后,DHT11 返回响应信号,同样由 “低脉冲 + 高脉冲” 组成:

    • 低脉冲:持续 80μs;
    • 高脉冲:持续 80μs。
  5. DHT11 发送数据信号
    响应信号结束后,DHT11 开始传输数据,每一位数据均以 “低脉冲 + 高脉冲” 的形式表示:

    • 数据 0:低脉冲持续 50μs,高脉冲持续 26~28μs;
    • 数据 1:低脉冲持续 50μs,高脉冲持续 70μs。
  6. 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