1. RK3568平台I2C驱动开发概述
在嵌入式Linux开发中,I2C总线是最常用的外设接口之一。RK3568作为瑞芯微推出的中高端处理器,其I2C控制器驱动架构具有典型的Linux I2C子系统特征。对于驱动开发者而言,理解I2C子系统的分层设计和工作原理至关重要。
I2C驱动开发的核心在于掌握"总线-驱动-设备"模型。在Linux内核中,I2C子系统被划分为以下几个关键组件:
- i2c_client:代表具体的I2C设备
- i2c_driver:设备对应的驱动程序
- i2c_adapter:硬件I2C控制器抽象
- i2c_algorithm:控制器通信协议实现
- i2c_core:核心层,负责组件间的协调
提示:硬件I2C和软件模拟I2C(bit-banging)有本质区别。硬件I2C由SoC控制器实现,性能更好;软件I2C则需要完全自行实现时序控制。
2. I2C子系统架构深度解析
2.1 核心组件分工与协作
i2c_core作为中间层,主要职责包括:
- 提供注册/注销接口给adapter、driver和client
- 实现总线匹配逻辑(i2c_device_match)
- 处理数据传输的中间转换
- 维护i2c_bus_type总线实例
i2c_adapter和i2c_algorithm通常由SoC厂商提供。以RK3568为例,其驱动代码位于drivers/i2c/busses/i2c-rk3x.c。关键结构体如下:
c复制struct rk3x_i2c {
struct i2c_adapter adap;
struct device *dev;
void __iomem *regs;
/* 其他硬件相关字段 */
};
static const struct i2c_algorithm rk3x_i2c_algorithm = {
.master_xfer = rk3x_i2c_xfer,
.functionality = rk3x_i2c_func,
};
2.2 设备注册流程揭秘
RK3568的I2C控制器注册过程比较特殊:
- 通过platform_driver注册(而非直接使用i2c_bus_type)
- 在probe函数中初始化i2c_adapter
- 调用i2c_add_adapter注册控制器
c复制static int rk3x_i2c_probe(struct platform_device *pdev)
{
struct rk3x_i2c *i2c;
// 资源分配和初始化
i2c->adap.algo = &rk3x_i2c_algorithm;
ret = i2c_add_adapter(&i2c->adap);
// ...
}
这种设计是因为I2C控制器本身也是平台设备,需要先通过platform总线匹配。
2.3 设备树与client创建
I2C设备的注册流程如下:
- 设备树定义I2C设备节点
- 内核解析设备树生成i2c_client
- 驱动通过i2c_driver注册
- 总线完成driver和client的匹配
典型设备树示例:
dts复制i2c1: i2c@fdd40000 {
compatible = "rockchip,rk3568-i2c";
reg = <0x0 0xfdd40000 0x0 0x1000>;
// 控制器配置...
sensor@19 {
compatible = "vendor,sensor-model";
reg = <0x19>;
// 设备特定配置...
};
};
3. 驱动开发实战指南
3.1 标准I2C驱动开发流程
- 定义设备ID表(用于匹配)
c复制static const struct of_device_id sensor_dt_ids[] = {
{ .compatible = "vendor,sensor-model" },
{ }
};
MODULE_DEVICE_TABLE(of, sensor_dt_ids);
- 实现驱动核心结构体
c复制static struct i2c_driver sensor_driver = {
.driver = {
.name = "sensor-device",
.of_match_table = sensor_dt_ids,
},
.probe = sensor_probe,
.remove = sensor_remove,
.id_table = sensor_id,
};
- 编写probe函数(设备初始化)
c复制static int sensor_probe(struct i2c_client *client)
{
struct device *dev = &client->dev;
// 验证设备
if (!i2c_check_functionality(client->adapter,
I2C_FUNC_SMBUS_BYTE_DATA)) {
dev_err(dev, "I2C功能不支持");
return -EIO;
}
// 设备初始化和注册
// ...
}
3.2 数据传输操作详解
Linux提供了多种I2C数据传输API:
- 基础读写函数:
c复制i2c_smbus_read_byte(client);
i2c_smbus_write_byte(client, value);
- 寄存器访问:
c复制i2c_smbus_read_byte_data(client, reg);
i2c_smbus_write_byte_data(client, reg, value);
- 复合传输:
c复制i2c_transfer(adapter, &msg, num);
注意:i2c_transfer需要自行构造i2c_msg,更灵活但更复杂。
3.3 调试技巧与工具
- 用户空间调试工具:
bash复制# 列出所有I2C总线
i2cdetect -l
# 扫描总线上的设备
i2cdetect -y 1
# 读取寄存器值
i2cget -y 1 0x19 0x00
- 内核调试手段:
c复制// 启用动态调试
echo "file i2c-* +p" > /sys/kernel/debug/dynamic_debug/control
// 查看I2C总线信息
cat /sys/bus/i2c/devices/i2c-1/name
4. 常见问题与解决方案
4.1 典型问题排查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| probe函数未调用 | 设备树匹配失败 | 检查compatible字符串 |
| 数据传输失败 | 时钟配置错误 | 检查设备树时钟配置 |
| 设备无响应 | 电源未开启 | 检查设备树电源相关属性 |
| 偶尔通信失败 | 上拉电阻不足 | 检查硬件上拉电阻值 |
4.2 设备树配置要点
- 时钟配置必须准确:
dts复制i2c1: i2c@fdd40000 {
clocks = <&cru CLK_I2C1>, <&cru PCLK_I2C1>;
clock-names = "i2c", "pclk";
};
- 引脚复用配置:
dts复制pinctrl_i2c1: i2c1 {
rockchip,pins = <0 RK_PB0 1 &pcfg_pull_none>,
<0 RK_PB1 1 &pcfg_pull_none>;
};
4.3 性能优化建议
- 使用DMA传输(如果控制器支持):
c复制i2c->adap.algo_data = &dma_config;
- 调整I2C频率:
dts复制i2c1: i2c@fdd40000 {
clock-frequency = <400000>; /* 400kHz */
};
- 减少锁竞争:
c复制/* 避免在原子上下文中进行I2C操作 */
5. 高级话题与扩展
5.1 i2c-dev接口的妙用
i2c-dev驱动为用户空间提供了直接访问I2C设备的接口:
- 启用配置:
bash复制# 加载模块
modprobe i2c-dev
# 创建设备节点
mknod /dev/i2c-1 c 89 1
- 用户空间示例:
c复制int fd = open("/dev/i2c-1", O_RDWR);
ioctl(fd, I2C_SLAVE, 0x19);
i2c_smbus_write_byte_data(fd, REG_ADDR, value);
注意:直接用户空间访问会绕过内核驱动模型,慎用于生产环境。
5.2 多地址设备处理
对于支持多地址的设备,可以使用dummy驱动:
c复制static const unsigned short normal_i2c[] = { 0x19, 0x1a, I2C_CLIENT_END };
static struct i2c_client_address_data addr_data = {
.normal_i2c = normal_i2c,
};
static int dummy_probe(struct i2c_client *client)
{
/* 根据client->addr区分具体设备 */
}
5.3 与用户空间通信
常用用户空间通信方式:
- sysfs接口:
c复制static DEVICE_ATTR(value, 0644, show_value, store_value);
static int sensor_probe(struct i2c_client *client)
{
device_create_file(&client->dev, &dev_attr_value);
}
- IIO框架(适合传感器):
c复制static const struct iio_info sensor_info = {
.read_raw = sensor_read_raw,
};
static int sensor_probe(struct i2c_client *client)
{
struct iio_dev *indio_dev;
indio_dev = devm_iio_device_alloc(&client->dev, sizeof(*data));
indio_dev->info = &sensor_info;
return devm_iio_device_register(&client->dev, indio_dev);
}
在实际项目中,我发现RK3568的I2C控制器对时钟稳定性要求较高,当使用非标准频率时容易出现通信错误。建议在设备树中明确指定时钟源,并避免使用边缘频率值。另外,对于高可靠性要求的应用,建议在驱动中添加CRC校验和重试机制,这能显著提高通信稳定性。