2、I2C子系统详解:从协议到调试,打通传感器通信的任督二脉
传感器驱动开发,说白了就是跟I2C打交道。RK平台上的加速度计、陀螺仪、温湿度传感器,十有八九都挂在I2C总线上。这一章咱们就把I2C子系统彻底吃透——从总线协议回顾,到RK平台控制器驱动,再到设备驱动模板,最后是调试技巧。我做了这么多年驱动,踩过的I2C坑少说也有十几个,今天一并分享给你。
2.1 I2C总线协议回顾:那些容易忽略的细节
I2C协议本身不复杂,但细节决定成败。咱们快速过一遍核心要点。
| 协议要素 | 关键参数 | 常见坑点 |
|---|---|---|
| 起始条件 | SCL高电平时SDA下降沿 | 时序不满足导致从机不响应 |
| 从机地址 | 7位或10位,左对齐 | 地址移位错误(常见!) |
| 读写位 | 0写1读 | 与地址组合成8位时搞反 |
| 应答机制 | 每字节后ACK/NACK | 从机忙时拉低SCL时钟拉伸 |
| 时钟频率 | 标准100kHz,快速400kHz | RK平台默认400kHz,部分传感器不支持 |
| 数据格式 | MSB先行,大端 | 寄存器地址和数据顺序搞混 |
再说说时钟拉伸(Clock Stretching)。有些慢速传感器在收到数据后,会拉低SCL线,告诉主机“我还没准备好,你等等”。RK平台的I2C控制器默认支持这个特性,但如果你用的传感器不支持,反而会出问题。我建议你在设备树里显式配置这个参数。
2.2 RK平台I2C控制器驱动:硬件怎么工作的
RK平台的I2C控制器,本质上是一个状态机。它负责把软件发来的消息转换成SCL和SDA上的时序波形。咱们来看看它的核心架构。
控制器驱动主要处理这几件事:
- 时钟配置: 根据设备树里的clock-frequency计算分频系数
- 消息拆分: 把一次I2C传输拆成起始、地址、数据、停止等阶段
- 中断处理: 处理传输完成、错误、超时等中断
- DMA传输: 大块数据时启用DMA,减轻CPU负担
我个人习惯在调试新传感器时,先把DMA关掉,用PIO模式。为什么?因为DMA涉及缓存一致性、描述符管理等复杂逻辑,出了问题不好定位。等PIO模式调通了,再切到DMA提升性能。
2.3 I2C设备驱动编写模板:照着写就能用
下面这个模板,是我在多个RK项目里提炼出来的。你写传感器驱动时,直接套用这个框架就行。
#include <linux/i2c.h>
#include <linux/module.h>
struct sensor_data {
struct i2c_client *client;
int irq;
/* 你的传感器状态变量 */
};
static int sensor_read_reg(struct i2c_client *client, u8 reg, u8 *val)
{
struct i2c_msg msg[2];
int ret;
msg[0].addr = client->addr;
msg[0].flags = 0; /* 写 */
msg[0].len = 1;
msg[0].buf = ®
msg[1].addr = client->addr;
msg[1].flags = I2C_M_RD; /* 读 */
msg[1].len = 1;
msg[1].buf = val;
ret = i2c_transfer(client->adapter, msg, 2);
if (ret != 2) {
dev_err(&client->dev, "read reg 0x%02x failed\n", reg);
return ret < 0 ? ret : -EIO;
}
return 0;
}
static int sensor_probe(struct i2c_client *client)
{
struct sensor_data *data;
u8 chip_id;
/* 1. 分配私有数据 */
data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
data->client = client;
i2c_set_clientdata(client, data);
/* 2. 读取芯片ID验证连接 */
sensor_read_reg(client, 0x0F, &chip_id);
if (chip_id != EXPECTED_ID) {
dev_err(&client->dev, "chip id mismatch: 0x%02x\n", chip_id);
return -ENODEV;
}
/* 3. 初始化传感器配置 */
/* ... */
/* 4. 注册输入设备或IIO设备 */
/* ... */
return 0;
}
static const struct of_device_id sensor_of_match[] = {
{ .compatible = "vendor,sensor-model" },
{ }
};
MODULE_DEVICE_TABLE(of, sensor_of_match);
static struct i2c_driver sensor_driver = {
.driver = {
.name = "sensor_drv",
.of_match_table = sensor_of_match,
},
.probe = sensor_probe,
.remove = sensor_remove,
.id_table = sensor_id_table,
};
module_i2c_driver(sensor_driver);
2.4 I2C通信调试技巧:三板斧解决问题
调试I2C通信,说白了就是三板斧:看波形、抓日志、测时序。咱们一个一个说。
2.4.1 逻辑分析仪看波形
这是最直接的方法。把SCL和SDA接到逻辑分析仪上,抓一段通信波形。你想想看,地址对不对?ACK有没有?数据对不对?一眼就能看出来。我建议你买一个带I2C解码的,比如Saleae或者国产的Kingst,几百块钱,能省你几天时间。
2.4.2 内核I2C调试接口
Linux内核提供了i2c-tools工具集,在RK平台上可以直接用。我最常用的是i2cdetect和i2cget/i2cset。
# 扫描总线上的设备
i2cdetect -y 2
# 读取设备寄存器
i2cget -y 2 0x44 0x0F
# 写入设备寄存器
i2cset -y 2 0x44 0x10 0x01
这里有个技巧:i2cdetect扫描时,如果地址显示UU,说明该地址已经被内核驱动占用了。如果显示--,说明没有设备响应。如果显示数字(比如44),说明有设备但没驱动。这个信息对排查很有用。
2.4.3 动态调试打印
打开I2C子系统的动态调试,可以看到每次传输的详细信息。
# 打开I2C核心调试
echo 'file drivers/i2c/i2c-core-base.c +p' > /sys/kernel/debug/dynamic_debug/control
# 打开RK平台控制器调试
echo 'file drivers/i2c/busses/i2c-rk3x.c +p' > /sys/kernel/debug/dynamic_debug/control
打开后,dmesg里会打印每次I2C传输的地址、数据、状态。嗯,这里要注意,生产环境别开这个,日志量太大。
2.5 本章知识体系总览
下面这张图,把I2C子系统的核心脉络画出来了。你写驱动时,脑子里要有这张图。
这张图把I2C子系统分成了五层:核心层、控制器驱动、设备驱动、调试技巧、常见问题。你写驱动时,从上往下看:先搞清楚核心层的接口,再写控制器配置,然后套设备驱动模板,最后用调试技巧验证。遇到问题,去最下面那层找答案。