1. 引言
在嵌入式 Linux 开发中,传感器驱动是常见的开发任务。DHT11 是一款数字温湿度传感器,采用单总线通信协议;而 I2C 子系统是 Linux 内核中用于管理 I2C 总线设备的标准化驱动框架。本文分别介绍 DHT11 驱动的实现思路,以及基于 Linux I2C 子系统编写设备驱动的完整流程。
2. DHT11 驱动开发
2.1 DHT11 简介
DHT11 是一款含有已校准数字信号输出的温湿度复合传感器,采用单总线(Single-Wire)协议与主机通信。其测量范围:湿度 20%-90% RH,温度 0-50 摄氏度,精度为湿度 ±5% RH、温度 ±2 摄氏度。
2.2 单总线时序分析
DHT11 通信时序分为以下几个阶段:
- 起始信号:主机拉低总线至少 18ms,然后释放总线,等待 DHT11 响应。
- 响应信号:DHT11 拉低总线 80us,再拉高 80us,表示准备发送数据。
- 数据发送:每 bit 以 50us 低电平开始,随后高电平时间长短决定该位是 0 还是 1。
- 数据格式:40 位数据,依次为湿度整数、湿度小数、温度整数、温度小数、校验和。
2.3 驱动代码实现
下面给出一个基于 GPIO 的 DHT11 字符设备驱动示例,使用内核定时器完成时序控制。
c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/gpio.h>
#include <linux/delay.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/cdev.h>
#include <linux/device.h>
#define DHT11_GPIO 17
#define DHT11_DATA_LEN 5
static int major;
static struct cdev dht11_cdev;
static struct class *dht11_class;
static unsigned char dht11_data[DHT11_DATA_LEN];
static int dht11_read_bit(void)
{
int timeout = 100;
while (gpio_get_value(DHT11_GPIO) == 0 && timeout--) {
udelay(1);
}
timeout = 100;
while (gpio_get_value(DHT11_GPIO) == 1 && timeout--) {
udelay(1);
}
return timeout > 0 ? 1 : 0;
}
static int dht11_read_data(void)
{
int i, j;
unsigned char byte = 0;
gpio_direction_output(DHT11_GPIO, 0);
mdelay(20);
gpio_set_value(DHT11_GPIO, 1);
udelay(30);
gpio_direction_input(DHT11_GPIO);
while (gpio_get_value(DHT11_GPIO) == 1) {
udelay(1);
}
while (gpio_get_value(DHT11_GPIO) == 0) {
udelay(1);
}
while (gpio_get_value(DHT11_GPIO) == 1) {
udelay(1);
}
for (i = 0; i < DHT11_DATA_LEN; i++) {
byte = 0;
for (j = 0; j < 8; j++) {
byte <<= 1;
if (dht11_read_bit()) {
byte |= 1;
}
}
dht11_data[i] = byte;
}
return (dht11_data[0] + dht11_data[1] + dht11_data[2] + dht11_data[3]) == dht11_data[4] ? 0 : -EIO;
}
static ssize_t dht11_read(struct file *filp, char __user *buf, size_t len, loff_t *off)
{
char temp[32];
int ret;
ret = dht11_read_data();
if (ret < 0) {
return ret;
}
snprintf(temp, sizeof(temp), "hum=%d.%d temp=%d.%d\n",
dht11_data[0], dht11_data[1], dht11_data[2], dht11_data[3]);
return simple_read_from_buffer(buf, len, off, temp, strlen(temp));
}
static struct file_operations dht11_fops = {
.owner = THIS_MODULE,
.read = dht11_read,
};
static int __init dht11_init(void)
{
int ret;
ret = gpio_request(DHT11_GPIO, "dht11");
if (ret) {
return ret;
}
major = register_chrdev(0, "dht11", &dht11_fops);
if (major < 0) {
gpio_free(DHT11_GPIO);
return major;
}
dht11_class = class_create(THIS_MODULE, "dht11");
device_create(dht11_class, NULL, MKDEV(major, 0), NULL, "dht11");
return 0;
}
static void __exit dht11_exit(void)
{
device_destroy(dht11_class, MKDEV(major, 0));
class_destroy(dht11_class);
unregister_chrdev(major, "dht11");
gpio_free(DHT11_GPIO);
}
module_init(dht11_init);
module_exit(dht11_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("DHT11 temperature and humidity sensor driver");
2.4 驱动测试
编译并加载驱动后,通过读取设备节点即可获取温湿度数据:
bash
sudo insmod dht11.ko
cat /dev/dht11
3. I2C 子系统驱动开发
3.1 I2C 子系统架构
Linux I2C 子系统采用三层架构:
- I2C 核心层:提供总线管理、设备注册、地址分配等通用逻辑。
- I2C 总线驱动:对应具体硬件控制器,负责底层时序收发。
- I2C 设备驱动:针对挂在总线上的具体外设,如 EEPROM、触摸屏、传感器等。
3.2 设备驱动核心结构
I2C 设备驱动主要实现两个核心结构体:
c
static const struct i2c_device_id xxx_id[] = {
{ "xxx_chip", 0 },
{ }
};
static struct i2c_driver xxx_driver = {
.driver = {
.name = "xxx_chip",
.of_match_table = xxx_of_match,
},
.probe = xxx_probe,
.remove = xxx_remove,
.id_table = xxx_id,
};
3.3 完整驱动示例
下面以一款虚拟 I2C 传感器为例,演示从注册到数据读取的完整流程。
c
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/delay.h>
#include <linux/slab.h>
#define XXX_REG_DATA 0x00
#define XXX_REG_CTRL 0x01
struct xxx_dev {
struct i2c_client *client;
int value;
};
static int xxx_read_value(struct xxx_dev *dev)
{
struct i2c_client *client = dev->client;
int ret;
ret = i2c_smbus_read_byte_data(client, XXX_REG_DATA);
if (ret < 0) {
return ret;
}
dev->value = ret;
return 0;
}
static int xxx_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
struct xxx_dev *dev;
int ret;
dev = kzalloc(sizeof(*dev), GFP_KERNEL);
if (!dev) {
return -ENOMEM;
}
dev->client = client;
i2c_set_clientdata(client, dev);
ret = i2c_smbus_write_byte_data(client, XXX_REG_CTRL, 0x01);
if (ret < 0) {
goto err_free;
}
ret = xxx_read_value(dev);
if (ret < 0) {
goto err_free;
}
dev_info(&client->dev, "xxx sensor probed, value=%d\n", dev->value);
return 0;
err_free:
kfree(dev);
return ret;
}
static void xxx_remove(struct i2c_client *client)
{
struct xxx_dev *dev = i2c_get_clientdata(client);
kfree(dev);
}
static const struct of_device_id xxx_of_match[] = {
{ .compatible = "vendor,xxx-sensor" },
{ }
};
MODULE_DEVICE_TABLE(of, xxx_of_match);
static const struct i2c_device_id xxx_id[] = {
{ "xxx-sensor", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, xxx_id);
static struct i2c_driver xxx_driver = {
.driver = {
.name = "xxx_sensor",
.of_match_table = xxx_of_match,
},
.probe = xxx_probe,
.remove = xxx_remove,
.id_table = xxx_id,
};
module_i2c_driver(xxx_driver);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Example I2C sensor driver");
3.4 设备树配置
在设备树中描述 I2C 设备,驱动加载时内核会自动匹配:
dts
&i2c1 {
status = "okay";
xxx_sensor@48 {
compatible = "vendor,xxx-sensor";
reg = <0x48>;
};
};
4. 两种驱动开发方式对比
| 对比项 | DHT11 驱动 | I2C 子系统驱动 |
|---|---|---|
| 通信协议 | 单总线,需软件模拟时序 | 标准 I2C 协议,硬件控制器支持 |
| 驱动框架 | 字符设备驱动,直接操作 GPIO | I2C 子系统框架,遵循总线-设备-驱动模型 |
| 时序控制 | 依赖 udelay/mdelay 精确延时 | 由 I2C 控制器硬件完成 |
| 设备匹配 | 手动注册,无自动匹配机制 | 支持设备树和 id_table 自动匹配 |
| 适用场景 | 低成本、低速简单传感器 | 标准总线外设,多设备共享总线 |
5. 总结
DHT11 驱动开发的核心在于精确控制单总线时序,适合理解底层 GPIO 操作和时序逻辑;而 I2C 子系统驱动则充分利用内核标准框架,代码结构清晰、可移植性强。实际项目中,应根据传感器接口类型选择合适的驱动开发方式,并优先复用内核提供的子系统框架。