基于 MX6UL 的 DHT11 温湿度传感器 驱动开发

DHT11 是低成本、单总线通信的温湿度传感器,广泛应用于物联网、工业控制等场景。本文基于 NXP MX6UL 处理器,从设备树配置、内核驱动开发、应用层调用三个维度,详细拆解 DHT11 的 Linux 驱动实现逻辑,结合实际代码片段分析关键技术点。

一、DHT11 通信原理基础

DHT11 采用单总线协议与主控通信,核心时序要求如下:

  1. 主机发起起始信号:拉低总线至少 18ms,然后释放总线(拉高),等待从机回应;
  2. 从机回应信号:DHT11 检测到起始信号后,拉低总线 80μs,再拉高 80μs,告知主机准备传输数据;
  3. 数据传输 :从机依次传输 40 位数据(5 字节),格式为:湿度整数字节 + 湿度小数字节 + 温度整数字节 + 温度小数字节 + 校验和字节
  4. 校验规则:前 4 字节之和等于第 5 字节,校验失败则数据无效。

注:DHT11 的小数位实际为 0,本文代码保留小数位解析逻辑,适配 DHT22(小数位有效)也可复用。

二、设备树配置解析

基于 MX6UL 的设备树需完成引脚复用设备节点两大配置,以下是核心片段分析:

1. 引脚复用配置(pinctrl_dht11)

dts

复制代码
pinctrl_dht11: putedht11 {
    fsl,pins = <
        MX6UL_PAD_JTAG_TDI__GPIO1_IO13          0x10B0
    >;
};
  • MX6UL_PAD_JTAG_TDI__GPIO1_IO13:将 JTAG_TDI 引脚复用为 GPIO1_IO13(DHT11 的单总线引脚),MX6UL 的引脚复用通过预定义宏实现,需匹配芯片手册;
  • 0x10B0:引脚电气属性配置,拆解为:
    • 0x10:速率配置(100MHz);
    • 0xB0:上下拉 / 驱动能力配置(默认上拉、驱动能力等级等),具体需参考 MX6UL 引脚配置手册。

2. 设备节点配置(putedht11)

dts

复制代码
putedht11 {
    compatible = "pute,putedht11";
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_dht11>;
    gpio-dht11 = <&gpio1 13 0>;
    status = "disable";
};
  • compatible = "pute,putedht11":驱动匹配关键标识,需与驱动中of_match_tablecompatible字段完全一致,是平台驱动与设备树匹配的核心;
  • pinctrl-names + pinctrl-0:绑定引脚复用配置,"default" 表示默认状态下使用pinctrl_dht11的引脚配置;
  • gpio-dht11 = <&gpio1 13 0>:指定 DHT11 使用的 GPIO 引脚:&gpio1(GPIO1 控制器)、13(引脚号)、0(默认低电平 / 无极性,无实际意义,仅为格式要求);
  • status = "disable":默认禁用,需改为"okay"才能让内核识别该设备节点。

三、内核驱动代码深度剖析

驱动基于 Linux平台驱动框架 + 混杂设备(miscdevice) 实现,既利用平台驱动适配设备树,又通过混杂设备简化字符设备注册(无需手动分配主设备号)。核心代码分模块解析如下:

1. 核心变量与初始化

c

运行

复制代码
static int gpiono = 0;          // 存储DHT11对应的GPIO编号
static spinlock_t lock;         // 自旋锁,保护时序临界区
  • 自旋锁用于保护 DHT11 时序操作(单总线时序要求严格,禁止中断打断),通过spin_lock_irqsave/spin_unlock_irqrestore实现中断上下文中的临界区保护。

2. DHT11 时序实现函数

(1)起始信号发送(dht11_init)

c

运行

复制代码
static void dht11_init(void)
{
    gpio_direction_output(gpiono, 1);
    gpio_set_value(gpiono, 0);
    msleep(20);                // 拉低至少18ms,满足DHT11时序要求
    gpio_set_value(gpiono, 1); // 释放总线,等待从机回应
    return;
}
  • 先将 GPIO 设为输出并拉高,再拉低 20ms(满足最低 18ms 要求),最后释放总线,触发 DHT11 回应。
(2)从机回应检测(dht11_reply)

c

运行

复制代码
static int dht11_reply(void)
{
    int cnt = 0;
    gpio_direction_input(gpiono);  // 切换为输入,检测从机回应
    // 等待从机拉低总线(超时保护:10*10μs=100μs)
    while (gpio_get_value(gpiono) == 1) {
        if (cnt++ > 10) {
            pr_info("dht11 no respond timeout\n");
            return -1;
        }
        udelay(10);
    }
    while(gpio_get_value(gpiono) == 0); // 等待从机拉低结束(80μs)
    return 0;
}
  • 切换 GPIO 为输入模式,检测从机是否拉低总线:若超时未检测到,返回错误;否则等待从机拉高总线(回应完成)。
(3)数据读取(dht11_read_data)

c

运行

复制代码
static int dht11_read_data(unsigned char *pdata, int datalen) 
{ 
    int i = 0, cnt = 0, j = 0;
    while(gpio_get_value(gpiono) == 1); // 等待从机拉高结束
    for(j = 0; j < datalen; j++) {      // 读取5字节数据
        for(i = 7; i >= 0; i--) {       // 逐位读取(高位先行)
            cnt = 0;
            while(gpio_get_value(gpiono) == 0); // 等待数据位起始(低电平)
            // 统计高电平持续时间:判断0/1(DHT11:0≈26-28μs,1≈70μs)
            while(gpio_get_value(gpiono) == 1) {
                cnt++;
                udelay(10);
            }
            if(cnt > 5) {               // 5*10μs=50μs,大于50μs判定为1
                pdata[j] |= (1 << i); 
            }
        }
    }
    return 0;
}
  • 核心逻辑:通过统计高电平持续时间区分 0/1(DHT11 的 0 对应高电平≈28μs,1 对应≈70μs);
  • 代码中cnt > 5(即 50μs)作为阈值,兼顾时序误差,工程上需根据实际硬件微调。
(4)数据校验(dht11_data_check)

c

运行

复制代码
static int dht11_data_check(unsigned char *pdata, int datalen)
{
    int i = 0, sum = 0;
    for(i = 0; i < datalen - 1; i++) {
        sum += pdata[i];       // 前4字节求和
    }
    if(sum == pdata[4]) {      // 与校验和对比
        return 0;
    }
    return -1;
}
  • 校验是 DHT11 数据有效性的关键,若校验失败,需重新读取(驱动中直接返回错误,由应用层重试)。

3. 混杂设备核心接口(read 函数)

c

运行

复制代码
static ssize_t dht11_read(struct file *fp, char __user *puser, size_t n, loff_t *off)
{
    unsigned long nret = 0;
    unsigned char data[5] = {0};
    int ret = 0;
    unsigned long flag = 0;

    dht11_init();                          // 发送起始信号
    spin_lock_irqsave(&lock, flag);        // 上锁,保护时序
    ret = dht11_reply();                   // 检测回应
    if (ret < 0) { spin_unlock_irqrestore(&lock, flag); return -1; }
    dht11_read_data(data, sizeof(data));   // 读取数据
    spin_unlock_irqrestore(&lock, flag);   // 解锁

    ret = dht11_data_check(data, sizeof(data)); // 校验数据
    if (ret < 0) { return -1; }
    
    nret = copy_to_user(puser, &data, sizeof(data)); // 数据拷贝到用户空间
    if (nret != 0) { return -1; }
    return 0;
}
  • copy_to_user:内核空间数据拷贝到用户空间的核心函数,返回值为未拷贝的字节数,需判断是否为 0;
  • 自旋锁仅包裹 "回应检测 + 数据读取"(时序敏感区),避免锁粒度太大影响系统性能。

4. 平台驱动与混杂设备注册

(1)文件操作集与混杂设备

c

运行

复制代码
static struct file_operations fops = {
    .owner = THIS_MODULE,
    .read = dht11_read,  // 绑定read接口
};
static struct miscdevice dht11_misc = {
    .minor = MISC_DYNAMIC_MINOR, // 动态分配次设备号
    .name = "dht11_misc",        // 设备节点名:/dev/dht11_misc
    .fops = &fops,
};
  • 混杂设备无需手动调用register_chrdev,通过misc_register即可完成设备注册,简化开发。
(2)平台驱动探针函数(probe)

c

运行

复制代码
static int dht11_probe(struct platform_device *pdevice)
{
    int ret = 0;
    // 1. 注册混杂设备
    ret = misc_register(&dht11_misc);
    if (ret != 0) { goto err_mis_register; }
    // 2. 从设备树获取GPIO编号
    gpiono = of_get_named_gpio(pdevice->dev.of_node, "gpio-dht11", 0);
    if (gpiono < 0) { goto err_find_resource; }
    // 3. 请求GPIO使用权(devm_前缀:自动释放,避免内存泄漏)
    ret = devm_gpio_request(dht11_misc.this_device, gpiono, "dht11_drv");
    if (ret != 0) { goto err_find_resource; }
    // 4. GPIO初始化(输出高电平)
    gpio_direction_output(gpiono, 1);
    // 5. 初始化自旋锁
    spin_lock_init(&lock);
    return 0;

err_find_resource:
    misc_deregister(&dht11_misc);
err_mis_register:
    return -1;
}
  • of_get_named_gpio:从设备树节点中读取gpio-dht11属性对应的 GPIO 编号;
  • devm_gpio_request:GPIO 请求的 "资源管理版",驱动卸载时自动释放 GPIO,避免手动释放遗漏;
  • 错误处理:采用 goto 语句,保证出错时反向释放已注册的资源(如混杂设备)。
(3)平台驱动匹配与注册

c

运行

复制代码
static const struct of_device_id dht11_of_match_table[] = {
    {.compatible = "pute,putedht11"}, // 匹配设备树compatible
    {},
};
static struct platform_driver dht11_drv = {
    .probe = dht11_probe,
    .remove = dht11_remove,
    .driver = {
        .name = "putedht11",
        .of_match_table = dht11_of_match_table,
    },
};
module_init(dht11_drv_init); // 注册平台驱动
module_exit(dht11_drv_exit); // 卸载平台驱动
  • of_match_table:设备树匹配表,内核启动时根据设备树节点的compatible字段匹配驱动,触发probe函数。

四、应用层代码解析

应用层通过操作/dev/dht11_misc设备文件与驱动交互,核心逻辑如下:

c

运行

复制代码
int main(void)
{
    int fd = 0;
    unsigned char data[5] = {0};
    // 打开混杂设备节点
    fd = open("/dev/dht11_misc", O_RDWR);
    if (-1 == fd) { perror("fail to open"); return -1; }
    // 循环读取温湿度
    while (1)
    {
        read(fd, data, sizeof(data)); // 触发驱动read函数
        // 解析数据:湿度(data[0].data[1])、温度(data[2].data[3])
        printf("temp:%d.%d   hum:%d.%d\n", data[2], data[3], data[0], data[1]);
        sleep(2); // DHT11采样周期至少1秒,这里设2秒
    }
    close(fd);
    return 0;
}
  • open:打开设备节点,内核触发file_operationsopen接口(本文未实现,使用默认);
  • read:调用一次触发驱动的dht11_read函数,读取 5 字节数据;
  • 数据解析:DHT11 小数位为 0,实际输出如temp:25.0 hum:60.0

五、工程测试与问题排查

1. 测试步骤

  1. 修改设备树 :将status = "disable"改为status = "okay",编译并烧写设备树;
  2. 编译驱动:编写 Makefile(指定内核源码路径),编译为.ko 模块;
  3. 加载驱动insmod dht11_drv.ko,查看内核日志dmesg,确认probe函数执行成功;
  4. 编译应用程序gcc dht11_app.c -o dht11_app
  5. 运行应用./dht11_app,查看温湿度输出。

2. 常见问题排查

表格

问题现象 可能原因 解决方案
设备打开失败(-1) 驱动未加载 / 设备树 status 未改 / 节点名不匹配 检查lsmod、设备树、/dev/dht11_misc是否存在
回应超时(reply timeout) 硬件接线错误 / GPIO 配置错误 / 时序延迟不准确 检查接线、GPIO 编号、udelay/msleep参数
数据校验失败 时序阈值不合理 / 总线干扰 / 传感器故障 微调cnt > 5的阈值、增加总线上拉电阻、更换传感器
copy_to_user 失败 用户空间缓冲区大小不足 / 权限问题 确保读取长度为 5 字节、以 root 权限运行应用

六、总结

本文基于 MX6UL 实现 DHT11 驱动,要点如下:

  1. 设备树 :重点关注compatible匹配、GPIO 编号、引脚复用配置;
  2. 驱动层:时序精准性(自旋锁保护、延迟函数选择)、数据校验、混杂设备简化注册;
  3. 应用层:通过标准字符设备接口与驱动交互,逻辑简洁。
相关推荐
charlie1145141913 小时前
嵌入式Linux驱动开发(8)——内存映射 I/O - 别拿物理地址当指针用
linux·开发语言·驱动开发·c·imx6ull
Wallace Zhang3 小时前
SimpleFOC源码学习09(v2.3.2) - 磁编码器MagneticSensorSPI.cpp与MagneticSensorSPI.h
驱动开发·stm32·simplefoc·foc电机控制
Freak嵌入式4 小时前
亲测可用!可本地部署的 MicroPython 开源仿真器
ide·驱动开发·嵌入式·仿真·micropython·upypi
进击的小头21 小时前
20_第20篇:嵌入式外设驱动开发基础:寄存器级开发与库函数开发对比实战
arm开发·驱动开发·单片机
低调小一21 小时前
BDD(行为驱动开发)入门:把“测试”写成“行为”,把“需求”写成“场景”
驱动开发·tdd·bdd
charlie1145141911 天前
嵌入式Linux驱动开发(7) 从虚拟设备到真实硬件 —— LED驱动硬件基础
linux·开发语言·驱动开发·内核·c
莎士比亚的文学花园1 天前
Linux驱动开发(2)——驱动编程
linux·运维·驱动开发
2601_949695591 天前
开源AI智能体OpenClaw接入DeepSeek V4全流程:从配置到成本
人工智能·驱动开发·ai·电脑
枳实-叶1 天前
【Linux驱动开发】第二天:内核模块生命周期+内存分配全解
linux·驱动开发