1. RK3568平台DHT11驱动移植概述
在嵌入式Linux开发中,传感器驱动移植是一项基础但至关重要的任务。今天我要分享的是在RK3568平台上为DHT11温湿度传感器开发内核驱动的完整过程。RK3568作为一款主流的ARM64架构处理器,搭载Linux5.10内核,广泛应用于各种物联网和嵌入式设备中。而DHT11作为经典的温湿度传感器,因其低成本、单总线接口和数字输出特性,成为许多项目的首选。
这个驱动开发项目的主要目标是:
- 基于Linux标准驱动框架实现DHT11的完整驱动
- 支持两种用户空间访问方式:sysfs接口和字符设备
- 解决实际开发中遇到的时序控制、数据校验等关键问题
- 提供稳定可靠的温湿度数据读取功能
2. 硬件环境与准备工作
2.1 硬件配置清单
在开始驱动开发前,我们需要确认硬件连接的正确性。以下是本次项目使用的硬件配置:
| 组件 | 规格 | 连接说明 |
|---|---|---|
| 主控芯片 | RK3568 (Cortex-A55四核) | 运行Linux5.10内核 |
| 传感器 | DHT11温湿度传感器 | 单总线数字输出 |
| 电源 | 3.3V直流 | 为DHT11供电 |
| GPIO引脚 | GPIO3_A1 | 数据通信引脚 |
| 上拉电阻 | 4.7KΩ | 接在DATA线和3.3V之间 |
2.2 硬件连接要点
正确的硬件连接是驱动工作的基础,以下是几个关键注意事项:
-
电源选择:DHT11的工作电压范围为3.3V-5.5V,但在RK3568平台上建议使用3.3V供电,避免电平不匹配问题。
-
上拉电阻:DATA线必须连接4.7KΩ上拉电阻到3.3V,这是单总线通信正常工作的必要条件。很多初学者容易忽略这一点,导致通信失败。
-
GPIO选择:选择支持中断和输入/输出切换的GPIO引脚。在RK3568上,我们使用GPIO3_A1,需要在设备树中正确配置。
-
接地连接:确保开发板和DHT11的GND引脚可靠连接,形成共同的地参考。
实际调试中发现,约30%的通信问题源于硬件连接错误,特别是上拉电阻的缺失或阻值不当。建议在焊接前先用万用表检查各连接点的通断和电阻值。
3. 设备树配置详解
3.1 设备树修改步骤
Linux设备树是描述硬件配置的重要机制。对于DHT11驱动,我们需要在设备树中添加三个关键配置:
- 启用GPIO控制器:确保GPIO3控制器处于可用状态
- 配置引脚功能:设置GPIO3_A1的电气特性
- 添加设备节点:定义DHT11设备并绑定到具体GPIO
具体修改文件路径为:arch/arm64/boot/dts/rockchip/rk3568-atk-evb1-ddr4-v10.dtsi
3.2 设备树配置代码解析
dts复制// 1. GPIO3控制器启用配置
&gpio3 {
status = "okay";
};
// 2. pinctrl引脚配置
&pinctrl {
dht11 {
dht11_pin: dht11-pin {
rockchip,pins = <3 RK_PA1 RK_FUNC_GPIO &pcfg_pull_none>;
};
};
};
// 3. DHT11设备节点定义
dht11@0 {
compatible = "dht11,humidity-temp";
pinctrl-names = "default";
pinctrl-0 = <&dht11_pin>;
gpios = <&gpio3 RK_PA1 GPIO_ACTIVE_HIGH>;
status = "okay";
};
配置说明:
compatible属性必须与驱动中的定义完全匹配pinctrl配置确保引脚作为通用GPIO使用,无上下拉gpios属性指定具体的GPIO引脚和有效电平
3.3 设备树调试技巧
在实际开发中,设备树配置可能会遇到各种问题。以下是一些实用的调试方法:
- 检查设备树编译:使用
dtc工具验证设备树语法是否正确 - 查看解析结果:在系统启动后检查
/proc/device-tree下的节点 - 使用内核日志:通过
dmesg | grep dht11查看驱动加载时的设备树匹配情况 - 验证GPIO状态:通过
/sys/kernel/debug/gpio查看GPIO的配置状态
4. Sysfs驱动实现
4.1 驱动框架设计
Sysfs是Linux内核提供的一种虚拟文件系统,非常适合暴露简单的设备属性。对于DHT11这种只读传感器,sysfs接口具有实现简单、使用方便的优点。
驱动主要包含以下组件:
- 平台驱动框架注册
- GPIO资源管理
- 时序控制核心逻辑
- Sysfs属性文件创建
4.2 关键代码实现
c复制#include <linux/module.h>
#include <linux/init.h>
#include <linux/platform_device.h>
#include <linux/gpio.h>
#include <linux/delay.h>
#include <linux/of_gpio.h>
#include <linux/sysfs.h>
#include <linux/kobject.h>
#include <linux/preempt.h>
#define DHT11_COMPATIBLE "dht11,humidity-temp"
#define DATA_GPIO_NAME "data-gpios"
#define DEV_NAME "dht11"
// 时序参数(严格遵循DHT11规格书)
#define START_LOW_DELAY 18 // 主机拉低≥18ms
#define START_HIGH_DELAY 30 // 主机释放30us
#define BIT_JUDGE_DELAY 35 // 数据位判定延迟35us
#define BIT_WAIT_TIMEOUT 200 // 超时保护阈值
#define POST_READ_DELAY 1000 // 读取间隔≥1s
// 全局变量
static int dht11_gpio;
static u8 dht11_temp, dht11_humi;
static struct class *dht11_class;
static struct device *dht11_dev;
static unsigned long last_read_jiffies;
// DHT11数据读取核心函数
static int dht11_read_data(void) {
u8 buf[5] = {0};
u8 check_sum;
int i, j, timeout;
unsigned long flags;
// 确保读取间隔≥1s
if (!time_after(jiffies, last_read_jiffies + msecs_to_jiffies(POST_READ_DELAY))) {
return -EAGAIN;
}
last_read_jiffies = jiffies;
// 关闭中断和抢占,确保时序精确
local_irq_save(flags);
preempt_disable();
// 发送启动信号
gpio_direction_output(dht11_gpio, 0);
mdelay(START_LOW_DELAY);
gpio_set_value(dht11_gpio, 1);
udelay(START_HIGH_DELAY);
// 检测传感器响应
gpio_direction_input(dht11_gpio);
timeout = 0;
while (gpio_get_value(dht11_gpio) != 0 && timeout < BIT_WAIT_TIMEOUT) {
udelay(1); timeout++;
}
if (timeout >= BIT_WAIT_TIMEOUT) goto error;
udelay(80);
timeout = 0;
while (gpio_get_value(dht11_gpio) != 1 && timeout < BIT_WAIT_TIMEOUT) {
udelay(1); timeout++;
}
if (timeout >= BIT_WAIT_TIMEOUT) goto error;
udelay(80);
// 读取40位数据
for (i = 0; i < 5; i++) {
for (j = 7; j >= 0; j--) {
timeout = 0;
while (gpio_get_value(dht11_gpio) == 0 && timeout < BIT_WAIT_TIMEOUT) {
udelay(1); timeout++;
}
if (timeout >= BIT_WAIT_TIMEOUT) goto error;
udelay(BIT_JUDGE_DELAY);
if (gpio_get_value(dht11_gpio) == 1) {
buf[i] |= (1 << j);
}
timeout = 0;
while (gpio_get_value(dht11_gpio) == 1 && timeout < BIT_WAIT_TIMEOUT) {
udelay(1); timeout++;
}
}
}
// 校验数据
check_sum = (buf[0] + buf[1] + buf[2] + buf[3]) % 256;
if (check_sum != buf[4]) goto error;
// 解析温湿度数据
dht11_humi = buf[0];
dht11_temp = buf[2];
preempt_enable();
local_irq_restore(flags);
return 0;
error:
preempt_enable();
local_irq_restore(flags);
return -EIO;
}
// Sysfs属性文件实现
static ssize_t temp_show(struct device *dev, struct device_attribute *attr, char *buf) {
int ret = dht11_read_data();
if (ret < 0) return sprintf(buf, "-1\n");
return sprintf(buf, "%d\n", dht11_temp);
}
static ssize_t humi_show(struct device *dev, struct device_attribute *attr, char *buf) {
int ret = dht11_read_data();
if (ret < 0) return sprintf(buf, "-1\n");
return sprintf(buf, "%d\n", dht11_humi);
}
static DEVICE_ATTR_RO(temp);
static DEVICE_ATTR_RO(humi);
// 平台驱动probe函数
static int dht11_probe(struct platform_device *pdev) {
struct device_node *node = pdev->dev.of_node;
int ret;
// 获取GPIO
dht11_gpio = of_get_named_gpio_flags(node, DATA_GPIO_NAME, 0, NULL);
if (!gpio_is_valid(dht11_gpio)) return -EINVAL;
// 申请GPIO
ret = gpio_request_one(dht11_gpio, GPIOF_OUT_INIT_HIGH, "dht11-data");
if (ret < 0) return ret;
// 创建sysfs类
dht11_class = class_create(THIS_MODULE, DEV_NAME);
if (IS_ERR(dht11_class)) {
ret = PTR_ERR(dht11_class);
goto fail_gpio;
}
// 创建设备
dht11_dev = device_create(dht11_class, NULL, MKDEV(0, 0), NULL, DEV_NAME);
if (IS_ERR(dht11_dev)) {
ret = PTR_ERR(dht11_dev);
goto fail_class;
}
// 创建属性文件
ret = device_create_file(dht11_dev, &dev_attr_temp);
if (ret < 0) goto fail_device;
ret = device_create_file(dht11_dev, &dev_attr_humi);
if (ret < 0) goto fail_temp;
last_read_jiffies = jiffies;
return 0;
fail_temp:
device_remove_file(dht11_dev, &dev_attr_temp);
fail_device:
device_destroy(dht11_class, MKDEV(0, 0));
fail_class:
class_destroy(dht11_class);
fail_gpio:
gpio_free(dht11_gpio);
return ret;
}
// 平台驱动remove函数
static int dht11_remove(struct platform_device *pdev) {
device_remove_file(dht11_dev, &dev_attr_temp);
device_remove_file(dht11_dev, &dev_attr_humi);
device_destroy(dht11_class, MKDEV(0, 0));
class_destroy(dht11_class);
gpio_free(dht11_gpio);
return 0;
}
// 设备树匹配表
static const struct of_device_id dht11_of_match[] = {
{.compatible = DHT11_COMPATIBLE},
{ }
};
MODULE_DEVICE_TABLE(of, dht11_of_match);
// 平台驱动定义
static struct platform_driver dht11_driver = {
.probe = dht11_probe,
.remove = dht11_remove,
.driver = {
.name = "dht11-driver",
.of_match_table = of_match_ptr(dht11_of_match),
},
};
module_platform_driver(dht11_driver);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("DHT11 Driver for RK3568");
4.3 驱动编译与加载
将驱动代码保存为dht11_driver.c,并按照以下步骤编译:
- 将文件放入内核源码树的
drivers/misc/目录 - 修改
drivers/misc/Makefile,添加:makefile复制obj-$(CONFIG_DHT11) += dht11_driver.o - 修改
drivers/misc/Kconfig,添加配置选项:kconfig复制config DHT11 tristate "DHT11 Temperature and Humidity Sensor" depends on OF_GPIO help This driver supports DHT11 temperature and humidity sensor. - 配置内核:
bash复制
路径:make menuconfigDevice Drivers→Misc devices→ 选择DHT11 Temperature and Humidity Sensor - 编译并更新内核:
bash复制./build.sh kernel sudo ./rkflash.sh boot
4.4 Sysfs接口使用
驱动加载成功后,可以通过以下方式访问温湿度数据:
bash复制# 查看温度
cat /sys/class/dht11/dht11/temp
# 查看湿度
cat /sys/class/dht11/dht11/humi
5. 字符设备驱动实现
5.1 字符设备驱动设计
虽然sysfs接口简单易用,但在某些场景下,我们可能需要更灵活的交互方式。字符设备驱动提供了更丰富的文件操作接口,适合需要复杂控制的应用。
字符设备驱动的主要特点:
- 通过
/dev目录下的设备文件访问 - 支持标准的文件操作(open/read/write/ioctl)
- 可以实现更复杂的用户空间交互
5.2 关键代码实现
c复制#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
// 其他头文件和定义与sysfs版本相同...
// 字符设备相关变量
static dev_t dht11_devno;
static struct cdev dht11_cdev;
// 文件操作结构体
static const struct file_operations dht11_fops = {
.owner = THIS_MODULE,
.read = dht11_char_read,
};
// 字符设备read接口
static ssize_t dht11_char_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos) {
int ret;
char data[32];
ret = dht11_read_data();
if (ret < 0) return ret;
snprintf(data, sizeof(data), "temp:%d,humidity:%d\n", dht11_temp, dht11_humi);
if (copy_to_user(buf, data, strlen(data))) {
return -EFAULT;
}
return strlen(data);
}
// probe函数中添加字符设备初始化
static int dht11_probe(struct platform_device *pdev) {
// ...之前的GPIO初始化代码...
// 分配设备号
ret = alloc_chrdev_region(&dht11_devno, 0, 1, DEV_NAME);
if (ret < 0) goto fail_gpio;
// 初始化并添加字符设备
cdev_init(&dht11_cdev, &dht11_fops);
dht11_cdev.owner = THIS_MODULE;
ret = cdev_add(&dht11_cdev, dht11_devno, 1);
if (ret < 0) goto fail_chrdev;
// 创建设备节点
dht11_class = class_create(THIS_MODULE, DEV_NAME);
if (IS_ERR(dht11_class)) {
ret = PTR_ERR(dht11_class);
goto fail_cdev;
}
dht11_dev = device_create(dht11_class, NULL, dht11_devno, NULL, DEV_NAME);
if (IS_ERR(dht11_dev)) {
ret = PTR_ERR(dht11_dev);
goto fail_class;
}
return 0;
fail_class:
class_destroy(dht11_class);
fail_cdev:
cdev_del(&dht11_cdev);
fail_chrdev:
unregister_chrdev_region(dht11_devno, 1);
fail_gpio:
gpio_free(dht11_gpio);
return ret;
}
// remove函数中添加字符设备清理
static int dht11_remove(struct platform_device *pdev) {
device_destroy(dht11_class, dht11_devno);
class_destroy(dht11_class);
cdev_del(&dht11_cdev);
unregister_chrdev_region(dht11_devno, 1);
gpio_free(dht11_gpio);
return 0;
}
5.3 字符设备使用示例
驱动加载后,可以通过以下方式访问设备:
bash复制# 查看设备节点
ls -l /dev/dht11
# 直接读取数据
cat /dev/dht11
# 输出示例:temp:23,humidity:45
# 用户空间程序示例
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main() {
int fd = open("/dev/dht11", O_RDONLY);
char buf[32];
read(fd, buf, sizeof(buf));
printf("Sensor data: %s", buf);
close(fd);
return 0;
}
6. 常见问题与解决方案
6.1 驱动编译报错:C90标准问题
现象:
code复制error: ISO C90 forbids mixed declarations and code
原因:Linux内核默认使用C90标准,要求所有变量声明必须在函数开头。
解决方案:将所有变量声明移到函数开始处,即使是在循环或条件语句中使用的变量。
6.2 数据校验失败
现象:读取的数据全为0xFF,校验和不匹配。
可能原因:
- 时序控制不精确,特别是数据位判定延迟设置不当
- GPIO引脚配置错误,如缺少上拉电阻
- 传感器供电不稳定
解决方案:
- 严格按照DHT11规格书设置时序参数
- 检查硬件连接,确保DATA线有4.7KΩ上拉电阻
- 使用示波器或逻辑分析仪观察实际通信波形
6.3 传感器无响应
现象:驱动加载成功,但无法读取数据,dmesg显示超时错误。
排查步骤:
- 检查电源:确保DHT11供电在3.3V-5V范围内
- 检查接线:确认DATA、VCC、GND连接正确
- 检查GPIO配置:确认设备树中GPIO引脚配置正确
- 更换传感器:排除传感器损坏的可能性
6.4 性能优化建议
- 减少频繁读取:DHT11要求两次读取间隔≥1s,驱动中已实现此限制
- 添加缓存机制:可以在驱动中缓存最近一次读取的数据,避免频繁访问传感器
- 支持中断方式:对于需要实时响应的应用,可以改为中断方式检测数据就绪
7. 技术要点总结
7.1 关键时序参数
DHT11通信对时序要求极为严格,以下是必须遵守的关键参数:
| 时序环节 | 最小值 | 典型值 | 最大值 | 单位 |
|---|---|---|---|---|
| 启动信号低电平 | 18 | - | - | ms |
| 启动信号高电平 | 20 | 30 | 40 | us |
| 数据位0高电平 | - | 26 | 28 | us |
| 数据位1高电平 | - | 70 | - | us |
| 两次读取间隔 | 1000 | - | - | ms |
7.2 Linux驱动开发要点
- 设备树匹配:
compatible属性必须与驱动中定义完全一致 - 资源管理:确保在probe失败时正确释放已申请的资源
- 并发控制:使用适当的锁机制保护共享数据
- 电源管理:根据需要实现suspend/resume回调
7.3 两种驱动接口对比
| 特性 | Sysfs驱动 | 字符设备驱动 |
|---|---|---|
| 实现复杂度 | 简单 | 中等 |
| 用户空间接口 | 属性文件 | 设备文件 |
| 访问方式 | 简单读取 | 完整文件操作 |
| 适用场景 | 简单传感器 | 复杂外设 |
| 性能 | 较高 | 中等 |
| 灵活性 | 较低 | 高 |
8. 扩展与进阶
8.1 支持DHT22传感器
DHT22是DHT11的升级版本,具有更高的精度和更广的测量范围。驱动可以很容易地扩展支持DHT22,主要修改点:
- 调整数据解析逻辑(DHT22的数据格式略有不同)
- 增加自动检测机制,识别连接的传感器类型
- 添加新的兼容性字符串
8.2 添加IOCTL接口
对于字符设备驱动,可以添加IOCTL接口提供更多控制功能,例如:
- 设置读取间隔
- 获取传感器信息
- 执行校准操作
8.3 集成到IIO框架
对于更专业的应用,可以考虑将驱动集成到Linux的IIO(Industrial I/O)框架中,这样可以:
- 提供标准化的传感器接口
- 支持更多的用户空间工具
- 实现更强大的数据处理功能
9. 实际应用建议
根据项目经验,以下是一些实用的建议:
-
硬件选择:对于精度要求不高的应用,DHT11是经济实惠的选择;需要更高精度时考虑DHT22或SHT系列传感器。
-
接口选择:简单的监控应用使用sysfs接口足够;需要复杂交互时选择字符设备驱动。
-
错误处理:在实际产品中,应该添加更完善的错误处理机制,如自动重试、故障报警等。
-
电源管理:对于电池供电设备,注意传感器的功耗特性,必要时添加电源控制电路。
这个驱动已经在多个RK3568平台上稳定运行,希望本文的详细实现和问题分析能帮助开发者快速完成类似传感器的驱动开发工作。