1. Linux I2C子系统概述
I2C(Inter-Integrated Circuit)总线是一种由Philips公司开发的简单、双向二线制同步串行总线,广泛应用于嵌入式系统中连接低速外设。Linux内核提供了完整的I2C子系统框架,使得驱动开发人员能够高效地开发I2C设备驱动。
I2C子系统采用典型的分层架构设计,主要包含以下几个核心组件:
- I2C核心层:维护i2c_bus结构体,提供总线驱动和设备驱动的注册/注销接口,实现设备与驱动的匹配机制
- I2C适配器驱动层:与具体硬件I2C控制器交互,实现底层传输协议
- I2C设备驱动层:实现具体I2C设备的业务逻辑
这种分层设计使得硬件相关代码与硬件无关代码分离,提高了代码的复用性和可维护性。
2. I2C子系统核心数据结构解析
2.1 i2c_adapter结构体
i2c_adapter代表一个物理I2C总线控制器,其定义如下:
c复制struct i2c_adapter {
struct module *owner;
const struct i2c_algorithm *algo; /* 总线访问算法 */
void *algo_data;
/* 适配器编号 */
int nr;
char name[48];
struct device dev; /* 适配器设备 */
/* 其他成员省略... */
};
关键成员说明:
algo:指向i2c_algorithm结构体,包含该适配器的通信方法nr:适配器编号,用于区分系统中多个I2C控制器name:适配器名称,如"s3c2410-i2c"
2.2 i2c_algorithm结构体
i2c_algorithm定义了适配器的通信方法:
c复制struct i2c_algorithm {
/* 主传输函数 */
int (*master_xfer)(struct i2c_adapter *adap,
struct i2c_msg *msgs, int num);
/* 功能标志 */
u32 (*functionality)(struct i2c_adapter *adap);
};
master_xfer是实现I2C数据传输的关键函数,驱动开发者需要根据硬件特性实现该函数。
2.3 i2c_client结构体
i2c_client代表一个I2C从设备:
c复制struct i2c_client {
unsigned short flags; /* 标志位 */
unsigned short addr; /* 7位设备地址 */
char name[I2C_NAME_SIZE]; /* 设备名称 */
struct i2c_adapter *adapter; /* 所属适配器 */
struct device dev; /* 设备结构 */
/* 其他成员省略... */
};
设备地址addr是7位格式,不包含读写位。例如MPU6050的地址为0x68(二进制1101000)。
2.4 i2c_driver结构体
i2c_driver描述I2C设备驱动:
c复制struct i2c_driver {
int (*probe)(struct i2c_client *client,
const struct i2c_device_id *id);
int (*remove)(struct i2c_client *client);
struct device_driver driver;
const struct i2c_device_id *id_table;
};
关键成员说明:
probe:设备探测函数,当设备与驱动匹配时调用remove:设备移除时调用的清理函数id_table:支持的设备ID表
3. I2C总线驱动开发实战
3.1 适配器驱动实现步骤
以S3C2410 I2C控制器为例,开发适配器驱动的主要步骤:
- 定义适配器结构体:
c复制struct s3c24xx_i2c {
struct i2c_adapter adap;
void __iomem *regs; /* 寄存器基地址 */
struct clk *clk; /* 时钟 */
int irq; /* 中断号 */
/* 其他硬件相关成员... */
};
- 实现传输算法:
c复制static const struct i2c_algorithm s3c24xx_i2c_algorithm = {
.master_xfer = s3c24xx_i2c_xfer,
.functionality = s3c24xx_i2c_func,
};
- 实现master_xfer函数:
c复制static int s3c24xx_i2c_xfer(struct i2c_adapter *adap,
struct i2c_msg *msgs, int num)
{
struct s3c24xx_i2c *i2c = adap->algo_data;
int ret;
/* 初始化硬件 */
s3c24xx_i2c_init(i2c);
/* 处理每条消息 */
for (i = 0; i < num; i++) {
if (msgs[i].flags & I2C_M_RD) {
ret = s3c24xx_i2c_read(i2c, &msgs[i]);
} else {
ret = s3c24xx_i2c_write(i2c, &msgs[i]);
}
if (ret < 0)
goto out;
}
ret = num;
out:
return ret;
}
- 实现probe函数:
c复制static int s3c24xx_i2c_probe(struct platform_device *pdev)
{
struct s3c24xx_i2c *i2c;
struct resource *res;
int ret;
/* 分配内存 */
i2c = devm_kzalloc(&pdev->dev, sizeof(*i2c), GFP_KERNEL);
/* 获取IO资源 */
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
i2c->regs = devm_ioremap_resource(&pdev->dev, res);
/* 获取中断 */
i2c->irq = platform_get_irq(pdev, 0);
/* 初始化适配器 */
strlcpy(i2c->adap.name, "s3c2410-i2c", sizeof(i2c->adap.name));
i2c->adap.owner = THIS_MODULE;
i2c->adap.algo = &s3c24xx_i2c_algorithm;
i2c->adap.algo_data = i2c;
/* 注册适配器 */
ret = i2c_add_numbered_adapter(&i2c->adap);
return ret;
}
3.2 适配器驱动注册流程
- 调用
platform_driver_register注册平台驱动 - 在probe函数中:
- 初始化硬件(时钟、GPIO等)
- 设置i2c_algorithm
- 调用
i2c_add_adapter或i2c_add_numbered_adapter注册适配器
3.3 关键函数说明
i2c_add_adapter:动态分配总线号注册适配器i2c_add_numbered_adapter:指定总线号注册适配器i2c_del_adapter:注销适配器
4. I2C设备驱动开发实战
4.1 设备驱动实现步骤
以MPU6050加速度计为例:
- 定义设备驱动:
c复制static struct i2c_driver mpu6050_driver = {
.driver = {
.name = "mpu6050",
.owner = THIS_MODULE,
},
.probe = mpu6050_probe,
.remove = mpu6050_remove,
.id_table = mpu6050_id,
};
- 实现probe函数:
c复制static int mpu6050_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
struct mpu6050_data *data;
/* 分配设备数据结构 */
data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
/* 初始化设备 */
data->client = client;
i2c_set_clientdata(client, data);
/* 配置硬件 */
mpu6050_init_device(data);
/* 注册输入设备、中断等 */
/* ... */
return 0;
}
- 实现设备操作函数:
c复制static int mpu6050_read_reg(struct i2c_client *client, u8 reg, u8 *val)
{
struct i2c_msg msg[2] = {
{
.addr = client->addr,
.flags = 0,
.len = 1,
.buf = ®,
},
{
.addr = client->addr,
.flags = I2C_M_RD,
.len = 1,
.buf = val,
}
};
return i2c_transfer(client->adapter, msg, 2);
}
4.2 设备驱动注册流程
- 调用
i2c_add_driver注册驱动 - 内核通过设备树或ACPI匹配设备与驱动
- 匹配成功后调用probe函数初始化设备
4.3 关键函数说明
i2c_transfer:执行I2C传输(组合消息)i2c_master_send:发送数据i2c_master_recv:接收数据i2c_smbus_*:SMBus协议相关函数
5. 设备树配置
现代Linux内核推荐使用设备树描述硬件配置。I2C设备通常在设备树中如下配置:
5.1 适配器节点
dts复制i2c1: i2c@400a0000 {
compatible = "fsl,imx6q-i2c", "fsl,imx21-i2c";
reg = <0x400a0000 0x4000>;
interrupts = <0 38 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks 125>;
#address-cells = <1>;
#size-cells = <0>;
status = "disabled";
};
5.2 设备节点
dts复制&i2c1 {
clock-frequency = <100000>;
status = "okay";
mpu6050@68 {
compatible = "invensense,mpu6050";
reg = <0x68>;
interrupt-parent = <&gpio1>;
interrupts = <18 IRQ_TYPE_EDGE_RISING>;
};
};
关键属性说明:
compatible:驱动匹配字符串reg:I2C设备地址clock-frequency:总线时钟频率(可选)
6. 调试与问题排查
6.1 常用调试工具
-
i2c-tools工具包:
i2cdetect:扫描I2C总线上的设备i2cget:读取I2C寄存器i2cset:写入I2C寄存器i2cdump:dump寄存器内容
-
sysfs接口:
/sys/bus/i2c/devices/:查看已注册的I2C设备/sys/bus/i2c/drivers/:查看已注册的I2C驱动
6.2 常见问题及解决方案
-
设备无响应:
- 检查物理连接(SDA/SCL上拉电阻)
- 确认设备地址是否正确(7位地址)
- 使用示波器检查信号质量
-
传输错误:
- 确认总线速度配置是否正确
- 检查时序是否符合设备要求(建立/保持时间)
- 增加传输重试机制
-
驱动加载失败:
- 检查设备树配置是否正确
- 确认驱动是否编译进内核或正确加载
- 检查内核日志(dmesg)获取详细错误信息
7. 性能优化技巧
-
减少传输开销:
- 合并多个寄存器读写为单个传输
- 使用
i2c_transfer代替多次i2c_smbus_*调用
-
合理设置总线速度:
- 低速设备(如RTC)使用100kHz
- 高速设备(如传感器)可尝试400kHz或更高
-
使用DMA传输(如果硬件支持):
- 大数据量传输时启用DMA
- 实现
i2c_algorithm中的master_xfer_dma回调
-
电源管理优化:
- 实现
pm_ops进行低功耗管理 - 非活动期降低总线速度或关闭总线
- 实现
8. 高级话题
8.1 regmap框架
regmap是Linux内核提供的统一寄存器访问框架,可以简化I2C设备驱动的开发:
c复制static const struct regmap_config mpu6050_regmap_config = {
.reg_bits = 8,
.val_bits = 8,
.max_register = 0x75,
};
static int mpu6050_probe(struct i2c_client *client)
{
struct regmap *regmap;
regmap = devm_regmap_init_i2c(client, &mpu6050_regmap_config);
/* 使用regmap读写寄存器 */
regmap_read(regmap, REG_WHO_AM_I, &val);
regmap_write(regmap, REG_PWR_MGMT_1, 0x01);
}
8.2 I2C多设备仲裁
当多个主机尝试同时控制总线时,需要进行仲裁:
- 硬件自动处理总线冲突
- 驱动应实现错误处理和重试机制
8.3 10位地址支持
某些设备使用10位地址扩展地址空间:
- 在
i2c_msg.flags中设置I2C_M_TEN - 适配器需支持
I2C_FUNC_10BIT_ADDR
9. 实战案例:MPU6050驱动实现
完整实现一个MPU6050加速度计/陀螺仪驱动:
c复制#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/input.h>
#define MPU6050_ADDR 0x68
#define REG_WHO_AM_I 0x75
#define REG_PWR_MGMT_1 0x6B
#define REG_ACCEL_XOUT_H 0x3B
struct mpu6050_data {
struct i2c_client *client;
struct input_dev *input;
};
static int mpu6050_read_data(struct mpu6050_data *data)
{
u8 buf[6];
s16 x, y, z;
int ret;
/* 读取加速度计数据 */
ret = i2c_smbus_read_i2c_block_data(data->client,
REG_ACCEL_XOUT_H,
sizeof(buf), buf);
if (ret < 0)
return ret;
x = (buf[0] << 8) | buf[1];
y = (buf[2] << 8) | buf[3];
z = (buf[4] << 8) | buf[5];
/* 上报输入事件 */
input_report_abs(data->input, ABS_X, x);
input_report_abs(data->input, ABS_Y, y);
input_report_abs(data->input, ABS_Z, z);
input_sync(data->input);
return 0;
}
static int mpu6050_probe(struct i2c_client *client)
{
struct mpu6050_data *data;
int ret;
/* 验证设备 */
ret = i2c_smbus_read_byte_data(client, REG_WHO_AM_I);
if (ret != 0x68) {
dev_err(&client->dev, "WHO_AM_I check failed\n");
return -ENODEV;
}
/* 初始化数据结构 */
data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
data->client = client;
/* 配置设备 */
i2c_smbus_write_byte_data(client, REG_PWR_MGMT_1, 0x01);
/* 注册输入设备 */
data->input = devm_input_allocate_device(&client->dev);
data->input->name = "MPU6050 Accelerometer";
input_set_abs_params(data->input, ABS_X, -32768, 32767, 0, 0);
input_set_abs_params(data->input, ABS_Y, -32768, 32767, 0, 0);
input_set_abs_params(data->input, ABS_Z, -32768, 32767, 0, 0);
ret = input_register_device(data->input);
return ret;
}
static const struct i2c_device_id mpu6050_id[] = {
{ "mpu6050", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, mpu6050_id);
static struct i2c_driver mpu6050_driver = {
.driver = {
.name = "mpu6050",
},
.probe = mpu6050_probe,
.id_table = mpu6050_id,
};
module_i2c_driver(mpu6050_driver);
10. 总结与最佳实践
通过本文的详细分析,我们可以总结出Linux I2C驱动开发的最佳实践:
- 分层设计:严格区分总线驱动和设备驱动
- 设备树优先:尽可能使用设备树描述硬件配置
- 错误处理:完善所有可能的错误路径处理
- 性能考量:合理使用批量传输和DMA
- 电源管理:实现适当的电源状态切换
- 调试友好:提供足够的调试信息和sysfs接口
在实际项目中,建议:
- 参考内核已有驱动(如drivers/i2c/busses/)
- 使用regmap框架简化寄存器访问
- 编写全面的设备树绑定文档
- 实现自动测试用例验证驱动稳定性
掌握Linux I2C子系统对于嵌入式Linux开发至关重要,希望本文能为开发者提供全面的技术参考和实践指导。
