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 字符设备驱动示例,使用内核定时器完成时序控制。
#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 驱动测试
编译并加载驱动后,通过读取设备节点即可获取温湿度数据:
sudo insmod dht11.ko cat /dev/dht113. I2C 子系统驱动开发
3.1 I2C 子系统架构
Linux I2C 子系统采用三层架构:
- I2C 核心层:提供总线管理、设备注册、地址分配等通用逻辑。
- I2C 总线驱动:对应具体硬件控制器,负责底层时序收发。
- I2C 设备驱动:针对挂在总线上的具体外设,如 EEPROM、触摸屏、传感器等。
3.2 设备驱动核心结构
I2C 设备驱动主要实现两个核心结构体:
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 传感器为例,演示从注册到数据读取的完整流程。
#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 设备,驱动加载时内核会自动匹配:
&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 子系统驱动则充分利用内核标准框架,代码结构清晰、可移植性强。实际项目中,应根据传感器接口类型选择合适的驱动开发方式,并优先复用内核提供的子系统框架。