1. Linux I2C子系统深度解析
作为一名嵌入式Linux开发者,我经常需要与各种传感器和外围设备打交道。I2C总线因其简单可靠的特点,成为连接这些设备的首选方案之一。今天我将分享Linux内核中I2C子系统的完整实现细节,从硬件接口到驱动开发,再到用户空间工具使用。
1.1 I2C总线基础概念
I2C(Inter-Integrated Circuit)总线是由Philips公司开发的一种简单、双向二线制同步串行总线。它只需要两根线即可在连接于总线上的器件之间传送信息。
总线拓扑结构:
- 主从式架构,由一个主设备(Master)和一个或多个从设备(Slave)组成
- 主设备负责发起和终止数据传输,控制时钟线(SCL)
- 从设备响应主设备的请求,每个从设备都有唯一的地址
电气特性:
- 标准模式:100 kbit/s
- 快速模式:400 kbit/s
- 高速模式:3.4 Mbit/s
- 超快速模式:5 Mbit/s
信号线:
- SDA(Serial Data Line):数据线,双向
- SCL(Serial Clock Line):时钟线,由主设备产生
实际开发中需要注意:I2C总线需要上拉电阻,典型值为4.7kΩ,具体值需要根据总线电容和传输速率调整。
1.2 I2C寻址机制
I2C使用7位地址空间,最多可寻址127个从设备。地址空间的前7位用于指定从设备,最后1位用于表示读/写方向。
地址格式:
code复制+------+------+------+------+------+------+------+------+
| MSB | | | | | | | LSB |
+------+------+------+------+------+------+------+------+
| A6 | A5 | A4 | A3 | A2 | A1 | A0 | R/W |
+------+------+------+------+------+------+------+------+
从设备地址通常可以在器件的数据手册中找到。例如,常见的EEPROM芯片24C02的地址是0x50(7位地址)。
2. Linux I2C子系统架构
Linux内核中的I2C子系统采用分层设计,主要分为三层:
2.1 I2C核心层
I2C核心层位于设备驱动层和适配器驱动层之间,主要功能包括:
- 提供统一的API接口供设备驱动使用
- 实现I2C总线类型(struct bus_type i2c_bus_type)
- 管理I2C适配器和设备
- 确保I2C时序完整性(防止中断打断关键时序)
核心层的关键数据结构:
c复制struct i2c_adapter; // 代表I2C控制器
struct i2c_algorithm; // 定义控制器通信方法
struct i2c_client; // 代表I2C从设备
struct i2c_driver; // I2C设备驱动
2.2 I2C适配器层
适配器层负责与具体硬件交互,主要功能:
- 实现硬件控制器驱动
- 提供底层传输函数(master_xfer)
- 处理硬件相关的时序和中断
常见的I2C控制器驱动包括:
- 处理器内置的I2C控制器(如RK3399的I2C控制器)
- GPIO模拟的I2C控制器(i2c-gpio)
- USB转I2C适配器等
2.3 I2C设备层
设备层面向用户空间和上层驱动,主要功能:
- 提供设备节点(/dev/i2c-x)
- 实现设备驱动模型
- 提供sysfs接口
- 实现字符设备接口供用户空间使用
3. I2C设备树配置详解
设备树是现代Linux内核中描述硬件的重要机制。下面以Rockchip RK3399平台为例,详细讲解I2C的设备树配置。
3.1 I2C控制器节点
dts复制i2c1: i2c@fe5a0000 {
compatible = "rockchip,rk3399-i2c";
reg = <0x0 0xfe5a0000 0x0 0x1000>;
clocks = <&cru CLK_I2C1>, <&cru PCLK_I2C1>;
clock-names = "i2c", "pclk";
interrupts = <GIC_SPI 47 IRQ_TYPE_LEVEL_HIGH>;
pinctrl-names = "default";
pinctrl-0 = <&i2c1_xfer>;
#address-cells = <1>;
#size-cells = <0>;
status = "disabled"; /* 初始状态为禁用 */
};
关键字段说明:
- compatible:驱动匹配字符串
- reg:寄存器地址范围
- clocks:使用的时钟
- interrupts:中断号
- pinctrl-0:引脚配置
- status:控制器状态
3.2 I2C设备节点
在控制器节点下添加从设备节点:
dts复制&i2c1 {
status = "okay"; /* 启用I2C1控制器 */
myft5x06: my-ft5x06@38 {
compatible = "my-ft5x06"; /* 驱动匹配名 */
reg = <0x38>; /* I2C设备地址:0x38 */
/* GPIO配置 */
reset-gpios = <&gpio0 RK_PB6 GPIO_ACTIVE_LOW>;
interrupt-parent = <&gpio3>;
interrupts = <RK_PA5 IRQ_TYPE_LEVEL_LOW>;
/* 引脚控制 */
pinctrl-names = "default";
pinctrl-0 = <&myft5x06_pins>;
};
};
引脚控制组配置:
dts复制&pinctrl {
myft5x06 {
myft5x06_pins: myft5x06-pins {
rockchip,pins =
<0 RK_PB6 RK_FUNC_GPIO &pcfg_pull_none>, /* 复位脚 */
<3 RK_PA5 RK_FUNC_GPIO &pcfg_pull_none>; /* 中断脚 */
};
};
};
4. I2C驱动开发实战
下面以FT5x06触摸芯片为例,完整讲解I2C驱动的开发过程。
4.1 驱动框架搭建
c复制#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/of_device.h>
/* 探测函数 */
static int ft5x06_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
printk("FT5x06 probe\n");
return 0;
}
/* 移除函数 */
static int ft5x06_remove(struct i2c_client *client)
{
return 0;
}
/* 设备树匹配表 */
static const struct of_device_id ft5x06_of_match[] = {
{ .compatible = "my-ft5x06" },
{ }
};
MODULE_DEVICE_TABLE(of, ft5x06_of_match);
/* I2C设备ID表 */
static const struct i2c_device_id ft5x06_id[] = {
{ "my-ft5x06", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, ft5x06_id);
/* I2C驱动结构体 */
static struct i2c_driver ft5x06_driver = {
.driver = {
.name = "my-ft5x06",
.of_match_table = ft5x06_of_match,
},
.probe = ft5x06_probe,
.remove = ft5x06_remove,
.id_table = ft5x06_id,
};
/* 模块初始化和退出 */
module_i2c_driver(ft5x06_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("FT5x06 I2C Touch Driver");
4.2 设备初始化和中断处理
c复制#include <linux/gpio/consumer.h>
#include <linux/interrupt.h>
static struct gpio_desc *reset_gpio;
static struct gpio_desc *irq_gpio;
static struct i2c_client *ft5x06_client;
/* 中断处理函数 */
static irqreturn_t ft5x06_irq_handler(int irq, void *dev_id)
{
/* 处理触摸中断 */
return IRQ_HANDLED;
}
static int ft5x06_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
int ret;
/* 保存client指针 */
ft5x06_client = client;
/* 获取复位GPIO */
reset_gpio = gpiod_get_optional(&client->dev, "reset", GPIOD_OUT_LOW);
if (IS_ERR(reset_gpio)) {
dev_err(&client->dev, "Failed to get reset GPIO\n");
return PTR_ERR(reset_gpio);
}
/* 硬件复位 */
gpiod_set_value(reset_gpio, 0);
msleep(5);
gpiod_set_value(reset_gpio, 1);
msleep(50);
/* 配置中断 */
irq_gpio = gpiod_get_optional(&client->dev, "interrupts", GPIOD_IN);
if (IS_ERR(irq_gpio)) {
dev_err(&client->dev, "Failed to get interrupt GPIO\n");
ret = PTR_ERR(irq_gpio);
goto err_reset_gpio;
}
ret = request_threaded_irq(client->irq, NULL, ft5x06_irq_handler,
IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
"ft5x06_irq", NULL);
if (ret) {
dev_err(&client->dev, "Failed to request IRQ\n");
goto err_irq_gpio;
}
return 0;
err_irq_gpio:
gpiod_put(irq_gpio);
err_reset_gpio:
gpiod_put(reset_gpio);
return ret;
}
4.3 I2C通信实现
Linux内核提供了多种I2C通信API,最常用的是i2c_transfer:
c复制/* 读取寄存器 */
static int ft5x06_read_reg(u8 reg, u8 *val)
{
struct i2c_msg msgs[2] = {
{
.addr = ft5x06_client->addr,
.flags = 0,
.len = 1,
.buf = ®,
},
{
.addr = ft5x06_client->addr,
.flags = I2C_M_RD,
.len = 1,
.buf = val,
}
};
int ret = i2c_transfer(ft5x06_client->adapter, msgs, 2);
if (ret < 0)
return ret;
return (ret == 2) ? 0 : -EIO;
}
/* 写入寄存器 */
static int ft5x06_write_reg(u8 reg, u8 val)
{
u8 buf[2] = {reg, val};
struct i2c_msg msg = {
.addr = ft5x06_client->addr,
.flags = 0,
.len = 2,
.buf = buf,
};
int ret = i2c_transfer(ft5x06_client->adapter, &msg, 1);
if (ret < 0)
return ret;
return (ret == 1) ? 0 : -EIO;
}
对于简单的读写操作,内核还提供了SMBus兼容的函数:
c复制/* 使用SMBus接口读取寄存器 */
static int ft5x06_read_reg_smbus(u8 reg)
{
return i2c_smbus_read_byte_data(ft5x06_client, reg);
}
/* 使用SMBus接口写入寄存器 */
static int ft5x06_write_reg_smbus(u8 reg, u8 val)
{
return i2c_smbus_write_byte_data(ft5x06_client, reg, val);
}
4.4 输入子系统集成
c复制#include <linux/input.h>
static struct input_dev *ft5x06_input;
static int ft5x06_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
/* ...之前的初始化代码... */
/* 初始化输入设备 */
ft5x06_input = input_allocate_device();
if (!ft5x06_input) {
ret = -ENOMEM;
goto err_irq;
}
ft5x06_input->name = "FT5x06 Touchscreen";
input_set_drvdata(ft5x06_input, client);
/* 设置支持的事件类型 */
__set_bit(EV_KEY, ft5x06_input->evbit);
__set_bit(BTN_TOUCH, ft5x06_input->keybit);
__set_bit(EV_ABS, ft5x06_input->evbit);
input_set_abs_params(ft5x06_input, ABS_X, 0, 1024, 0, 0);
input_set_abs_params(ft5x06_input, ABS_Y, 0, 1024, 0, 0);
/* 注册输入设备 */
ret = input_register_device(ft5x06_input);
if (ret) {
dev_err(&client->dev, "Failed to register input device\n");
goto err_input_alloc;
}
return 0;
err_input_alloc:
input_free_device(ft5x06_input);
err_irq:
free_irq(client->irq, NULL);
/* ...其他错误处理... */
}
4.5 工作队列处理触摸数据
c复制#include <linux/workqueue.h>
struct ft5x06_data {
struct i2c_client *client;
struct input_dev *input;
struct work_struct work;
};
static void ft5x06_work_handler(struct work_struct *work)
{
struct ft5x06_data *data = container_of(work, struct ft5x06_data, work);
u8 buf[6];
u16 x, y;
u8 status;
/* 读取触摸状态 */
if (ft5x06_read_reg(0x02, &status) < 0)
return;
status &= 0x0F; /* 只保留低4位 */
if (status == 0) { /* 无触摸 */
input_report_key(data->input, BTN_TOUCH, 0);
input_sync(data->input);
return;
}
/* 读取触摸坐标 */
if (i2c_smbus_read_i2c_block_data(data->client, 0x03, 4, buf) < 0)
return;
x = ((buf[0] & 0x0F) << 8) | buf[1];
y = ((buf[2] & 0x0F) << 8) | buf[3];
/* 上报触摸事件 */
input_report_key(data->input, BTN_TOUCH, 1);
input_report_abs(data->input, ABS_X, x);
input_report_abs(data->input, ABS_Y, y);
input_sync(data->input);
}
static irqreturn_t ft5x06_irq_handler(int irq, void *dev_id)
{
struct ft5x06_data *data = dev_id;
schedule_work(&data->work);
return IRQ_HANDLED;
}
5. 用户空间I2C访问
除了内核驱动,Linux还提供了用户空间访问I2C设备的接口。
5.1 通过/dev/i2c-x接口
c复制#include <linux/i2c-dev.h>
#include <sys/ioctl.h>
int i2c_read_reg(int fd, unsigned char addr, unsigned char reg, unsigned char *val)
{
unsigned char outbuf[1], inbuf[1];
struct i2c_msg msgs[2] = {
{
.addr = addr,
.flags = 0,
.len = 1,
.buf = outbuf,
},
{
.addr = addr,
.flags = I2C_M_RD,
.len = 1,
.buf = inbuf,
}
};
struct i2c_rdwr_ioctl_data msgset = {
.msgs = msgs,
.nmsgs = 2,
};
outbuf[0] = reg;
if (ioctl(fd, I2C_RDWR, &msgset) < 0)
return -1;
*val = inbuf[0];
return 0;
}
5.2 使用i2c-tools工具
i2c-tools是一组用户空间工具,用于调试I2C设备:
-
i2cdetect - 扫描I2C总线上的设备
bash复制i2cdetect -y 1 # 扫描I2C总线1 -
i2cget - 读取I2C寄存器
bash复制i2cget -y 1 0x38 0x00 # 从地址0x38读取寄存器0x00 -
i2cset - 写入I2C寄存器
bash复制i2cset -y 1 0x38 0x00 0x12 # 向地址0x38的寄存器0x00写入0x12 -
i2cdump - 显示所有寄存器内容
bash复制i2cdump -y 1 0x38 # 显示地址0x38的所有寄存器
6. GPIO模拟I2C驱动
在没有硬件I2C控制器的情况下,可以使用GPIO模拟I2C:
6.1 设备树配置
dts复制i2c_gpio: i2c@gpio {
compatible = "i2c-gpio";
gpios = <&gpio0 12 GPIO_ACTIVE_HIGH>, /* SDA */
<&gpio0 13 GPIO_ACTIVE_HIGH>; /* SCL */
i2c-gpio,delay-us = <5>; /* ~100 kHz */
#address-cells = <1>;
#size-cells = <0>;
};
6.2 内核配置
需要启用GPIO模拟I2C驱动:
code复制Device Drivers --->
I2C support --->
I2C Hardware Bus support --->
<*> GPIO-based bitbanging I2C
7. 调试技巧与常见问题
7.1 调试技巧
-
查看I2C适配器:
bash复制cat /sys/class/i2c-dev/i2c-0/name # 查看适配器名称 -
启用调试信息:
在内核配置中启用I2C调试:code复制CONFIG_I2C_DEBUG_CORE=y CONFIG_I2C_DEBUG_ALGO=y -
逻辑分析仪:使用逻辑分析仪抓取I2C波形,验证时序
7.2 常见问题
-
设备无响应:
- 检查设备地址是否正确
- 确认设备是否上电
- 检查总线是否有上拉电阻
- 使用示波器检查SCL/SDA信号
-
传输错误:
- 降低总线速度
- 检查总线电容是否过大
- 确保没有其他设备干扰总线
-
驱动加载失败:
- 检查设备树配置
- 确认compatible字符串匹配
- 检查依赖的模块是否加载
8. 性能优化建议
- 合理设置总线速度:根据设备能力选择最高支持的速度
- 减少传输次数:使用块传输代替单字节传输
- 使用DMA:对于大数据量传输,启用I2C控制器的DMA功能
- 中断优化:减少中断处理时间,使用工作队列处理耗时操作
- 电源管理:合理使用挂起/恢复功能,降低功耗
在实际项目中,I2C设备的稳定性和可靠性至关重要。通过深入理解Linux I2C子系统的架构和工作原理,结合本文提供的驱动开发方法和调试技巧,开发者可以快速实现各种I2C设备的驱动开发,并解决实际应用中遇到的各种问题。