Linux内核驱动——DTH11温湿度传感器

一、DHT11 传感器基础

DHT11 采用单总线串行通信方式,仅需一根数据线即可完成温湿度数据的双向传输,无需额外的时钟线,硬件连接简单。其通信逻辑由主机(MCU / 开发板)主动发起,从机(DHT11)响应并返回 40 位数据。

数据格式为:8 位湿度整数 + 8 位湿度小数 + 8 位温度整数 + 8 位温度小数 + 8 位校验和,其中 DHT11 的湿度小数和温度小数位始终为 0,实际有效数据为湿度整数和温度整数

二、DHT11原理图

1.电路原理图

DHT11 温湿度传感器的单总线连接电路,DATA 引脚通过 4.7K 上拉电阻接 VCC,确保总线空闲时为高电平,VCC 和 GND 分别接电源正负极。

2.通信时许

  • 主机起始:主机将数据线拉低至少 18ms,再拉高 20~40us,向 DHT11 发送起始指令;
  • 从机响应:DHT11 检测到起始信号后,会将数据线拉低 80us,再拉高 80us,完成响应;

3.读取0和1的时序

从机通过数据线高低电平的持续时间表示二进制位,低电平 50us 后,高电平 26~28us 表示 0,依次传输 40 位数据。

从机通过数据线高低电平的持续时间表示二进制位,低电平 50us 后,高电平 70us 表示 1,依次传输 40 位数据。

三、驱动代码核心实现解析

3.1 宏定义与全局变量

cs 复制代码
#define DEV_NAME "dht11"
static int dht11_gpio;
  • 定义设备名dht11,作为杂项设备和平台驱动的标识;
  • 全局变量dht11_gpio存储从设备树解析到的 DHT11 数据线对应的 GPIO 编号,实现 GPIO 资源的动态获取。

3.2 核心时序操作函数

3.2.1 起始信号函数dht11_start
cs 复制代码
static void dht11_start(void)
{
	gpio_direction_output(dht11_gpio, 1);
	mdelay(2);
	gpio_set_value(dht11_gpio, 0);
	msleep(20);
	gpio_set_value(dht11_gpio, 1);
	udelay(20);
}

该函数实现主机向 DHT11 发送起始信号,严格遵循 DHT11 时序要求:

  1. 先将 GPIO 配置为输出模式并拉高 2ms,保证数据线初始状态稳定;
  2. 拉低数据线 20ms(满足至少 18ms 的要求),发送起始指令;
  3. 拉高数据线 20us,等待 DHT11 的响应信号。
3.2.2 响应等待函数dht11_wait_respon
cs 复制代码
static int dht11_wait_respon(void)
{
	int time = 0;
	gpio_direction_input(dht11_gpio);
    // 等待数据线拉低
	time = 30;
	while(gpio_get_value(dht11_gpio) && time--) { udelay(1); }
	if(time < 0) return -1;
    // 等待从机响应低电平结束
	time = 100;
	while((!gpio_get_value(dht11_gpio)) && time--) { udelay(1); }
	if(time < 0) return -2;
    // 等待从机响应高电平结束
	time = 100;
	while(gpio_get_value(dht11_gpio) && time--) { udelay(1); }
	if(time < 0) return -3;
	return 0;
}

主机发送起始信号后,将 GPIO 配置为输入模式,等待 DHT11 的响应信号,通过超时计数避免死循环:

  1. 等待 DHT11 将数据线拉低,超时 30us 返回错误;
  2. 等待 DHT11 响应的 80us 低电平结束,超时 100us 返回错误;
  3. 等待 DHT11 响应的 80us 高电平结束,超时 100us 返回错误;
  4. 无超时则返回 0,表示响应成功。
3.2.3 位数据读取函数dht11_get_bit
cs 复制代码
static char dht11_get_bit(void)
{
	int time = 60;
	while(gpio_get_value(dht11_gpio) && time--) { udelay(1); }
	if(time < 0) return -4;
	time = 80;
	while((!gpio_get_value(dht11_gpio)) && time--) { udelay(1); }
	if(time < 0) return -5;
	udelay(35);
	if(!gpio_get_value(dht11_gpio)) return 0;
	time = 50;
	while(gpio_get_value(dht11_gpio) && time--) { udelay(1); }
	if(time < 0) return -6;
	return 1;
}

该函数实现单个二进制位的读取,基于 DHT11 的位时序特征:

  1. 先等待数据位的起始低电平结束,超时则返回错误;
  2. 延时 35us 后检测数据线电平:若为低,表示该位为 0;若为高,表示该位为 1;
  3. 对高电平做超时判断,避免总线异常导致的死循环。
3.2.4 完整数据读取函数dht11_read_data
cs 复制代码
static int dht11_read_data(unsigned char * data)
{
	int i = 0;
	int j = 0;
	for(j = 0; j < 5; j++)
	{
		for(i = 0; i < 8; i++)
		{
			char bit = dht11_get_bit();	
			if(bit < 0) return bit;
			data[j] <<= 1;
			data[j] |= bit;
		}
	}
	return 5;
}

循环读取 40 位(5 个字节)数据,通过位运算将读取到的二进制位拼接为完整的字节数据:

  1. 外层循环遍历 5 个字节(湿度整、湿度小、温度整、温度小、校验和);
  2. 内层循环遍历每个字节的 8 位,通过左移和或运算拼接位数据;
  3. 若位读取失败,立即返回错误码;读取成功返回 5,表示获取到 5 个字节有效数据。

3.3 文件操作接口实现

cs 复制代码
static int open(struct inode * inode, struct file * file)
{
	printk("dht11  open\n");
	return 0;
}
static ssize_t read(struct file * file, char __user * buf, size_t size, loff_t * loff)
{
	int ret = 0;
	unsigned char data[5];
	dht11_start();
	ret = dht11_wait_respon();
	if(ret < 0) return ret;
	ret = dht11_read_data(data);
	if(ret < 0) return ret;
	ret = copy_to_user(buf, data, sizeof(data));
	printk("dht11  read\n");
	return 5;
}
static ssize_t write(struct file * file, const char __user * buf, size_t size, loff_t * loff)
{
	printk("dht11  write\n");
	return 0;
}
static int close(struct inode * inode, struct file * file)
{
	printk("dht11  close\n");
	return 0;
}
static struct file_operations fops = 
{
	.owner = THIS_MODULE,
	.open = open,
	.read = read,
	.write = write,
	.release = close
};

read:核心接口,用户态调用read函数读取设备文件时,触发该接口:

  • 依次执行起始信号、响应等待、数据读取;
  • 通过copy_to_user将内核态的 5 字节温湿度数据拷贝到用户态缓冲区;
  • 数据传输成功后返回 5,表示读取到 5 个字节数据。

3.4 杂项设备注册

cs 复制代码
static struct miscdevice misc_dev =
{
	.minor = MISC_DYNAMIC_MINOR, // 动态分配次设备号
	.name = DEV_NAME,            // 设备名,对应/dev/dht11
	.fops = &fops                // 绑定文件操作接口
};

在平台驱动的probe函数中通过misc_register(&misc_dev)完成注册,驱动卸载时通过misc_deregister(&misc_dev)注销。

3.5.1 设备树匹配与 probe/remove 函数
cs 复制代码
static int probe(struct platform_device * pdev)
{
	struct device_node * pdts;
	int ret = misc_register(&misc_dev);
	if(ret) goto err_misc_register;
    // 解析设备树节点
	pdts = of_find_node_by_path("/dht11");
	if(NULL == pdts) { ret = PTR_ERR(pdts); goto err_of_find; }
    // 获取GPIO编号
	dht11_gpio = of_get_named_gpio(pdts, "ptdht11-gpio", 0);	
	if(dht11_gpio < 0) { ret = dht11_gpio; goto err_of_find; }
    // 申请GPIO资源
	ret = gpio_request(dht11_gpio, "dht11");
	if(ret < 0) goto err_gpio_request;
	gpio_direction_output(dht11_gpio, 1);
	printk("#########################  dht11_driver  probe\n");
	return 0;
    // 错误处理流程
err_gpio_request:
err_of_find:
	misc_deregister(&misc_dev);
err_misc_register:
	printk("#########################  dht11_driver misc register ret = %d\n", ret);
	return ret;
}
static int remove(struct platform_device * pdev)
{
	gpio_free(dht11_gpio);
	misc_deregister(&misc_dev);
	printk("#########################  dht11_driver  remove\n");
	return 0;
}
// 设备树匹配表
static struct of_device_id dht11_table[] = 
{
	{.compatible = "pt-dht11"},
	{}
};
// 平台驱动结构体
static struct platform_driver pdrv = 
{
	.probe = probe,
	.remove = remove,
	.driver = 
	{
		.name = DEV_NAME,
		.of_match_table = dht11_table
	}
};
  1. probe 函数:平台驱动匹配成功后执行的核心函数,完成设备初始化:
  2. remove 函数:驱动卸载时执行,释放 GPIO 资源并注销杂项设备,避免资源泄漏。
  3. 设备树匹配表:通过compatible属性(pt-dht11)与设备树中的设备节点匹配,实现驱动的自动加载。
3.5.2 模块入口与出口
cs 复制代码
static int __init dht11_driver_init(void)
{
	int ret = platform_driver_register(&pdrv);
	if(ret) goto err_platform_driver_register;
	printk("dht11 platform_driver_register success\n");
	return 0;
err_platform_driver_register:
	printk("dht11 platform_driver_register failed\n");
	return ret;
}
static void __exit dht11_driver_exit(void)
{
	platform_driver_unregister(&pdrv);
	printk("dht11 platofrm_driver_unregister\n");
}
module_init(dht11_driver_init);
module_exit(dht11_driver_exit);
MODULE_LICENSE("GPL");
  • 模块入口dht11_driver_init:注册平台驱动,驱动加载时执行;
  • 模块出口dht11_driver_exit:注销平台驱动,驱动卸载时执行;
  • MODULE_LICENSE("GPL"):声明模块遵循 GPL 协议,避免内核加载时的版权警告。

四、应用层程序

cs 复制代码
int main(int argc, const char* argv[])
{
    // 1. 打开驱动生成的设备节点
    int fd = open("/dev/dht11", O_RDONLY);
    if (fd < 0)
    {
        perror("open dht11 failed");
        return 1;
    }

    unsigned char data[5] = { 0 };
    while (1)
    {
        // 2. 读取 5 字节数据
        if (read(fd, data, sizeof(data)) == 5)
        {
            // data[0]: 湿度整数, data[1]: 湿度小数
            // data[2]: 温度整数, data[3]: 温度小数
            float h = data[0] + data[1] * 0.1;
            float t = data[2] + data[3] * 0.1;

            printf("Humidity = %.1f %%, Temperature = %.1f C\n", h, t);
        }
        else
        {
            printf("Read error or checksum failed\n");
        }

        // 3. 每 3 秒读一次(DHT11 硬件限制,不能读太快)
        sleep(3);
    }

    close(fd);
    return 0;
}

五、总结

DHT11 温湿度传感器的单总线接口电路:

  • DHT11 的 VCC 和 GND 分别接电源正负极,为传感器供电。
  • DATA 引脚通过 4.7K 上拉电阻接 VCC,保证总线空闲时为高电平,并通过接口 P2 与 MCU 通信。
  • 整个电路实现了单总线双向通信,用于 MCU 采集温湿度数据。
相关推荐
安科士andxe4 小时前
深入解析|安科士1.25G CWDM SFP光模块核心技术,破解中长距离传输痛点
服务器·网络·5g
小白同学_C7 小时前
Lab4-Lab: traps && MIT6.1810操作系统工程【持续更新】 _
linux·c/c++·操作系统os
今天只学一颗糖7 小时前
1、《深入理解计算机系统》--计算机系统介绍
linux·笔记·学习·系统架构
2601_949146537 小时前
Shell语音通知接口使用指南:运维自动化中的语音告警集成方案
运维·自动化
儒雅的晴天8 小时前
大模型幻觉问题
运维·服务器
Gofarlic_OMS9 小时前
科学计算领域MATLAB许可证管理工具对比推荐
运维·开发语言·算法·matlab·自动化
通信大师9 小时前
深度解析PCC策略计费控制:核心网产品与应用价值
运维·服务器·网络·5g
dixiuapp9 小时前
智能工单系统如何选,实现自动化与预测性维护
运维·自动化
不做无法实现的梦~9 小时前
ros2实现路径规划---nav2部分
linux·stm32·嵌入式硬件·机器人·自动驾驶
Elastic 中国社区官方博客9 小时前
如何防御你的 RAG 系统免受上下文投毒攻击
大数据·运维·人工智能·elasticsearch·搜索引擎·ai·全文检索