3、I2C总线驱动:I2C控制器驱动框架、设备树配置、I2C从设备驱动编写与调试
I2C总线,在嵌入式开发里太常见了。传感器、触摸屏、eeprom、音频codec……几乎每个板子上都能找到它的身影。RK平台上的I2C控制器驱动,其实内核已经帮我们写好了,我们更多的工作是配置设备树,然后编写从设备驱动。
说白了,你不需要从零写一个I2C控制器驱动。但你要搞清楚它的框架,否则出了问题你都不知道从哪查起。我刚开始搞RK的时候,就犯过这个错——以为I2C不通就是硬件问题,结果折腾半天发现是时钟配置错了。
3.1 I2C控制器驱动框架
RK平台的I2C控制器,本质上是一个总线控制器。它负责产生SCL时钟、处理SDA数据、处理起始/停止条件、应答等等。内核里,这部分代码在 drivers/i2c/busses/i2c-rk3x.c。
这个驱动主要干了三件事:
- 初始化硬件:设置时钟频率、使能控制器、配置中断。
- 实现master_xfer:这是核心,负责发送和接收数据。
- 中断处理:处理传输完成、错误等中断。
我们来看一下它的核心结构体:
static const struct i2c_algorithm rk3x_i2c_algorithm = {
.master_xfer = rk3x_i2c_xfer,
.functionality = rk3x_i2c_func,
};
这个 master_xfer 就是总线控制器对外提供的接口。你调用 i2c_transfer() 的时候,最终就会走到这里。
关键点:RK的I2C控制器支持标准模式(100kHz)和快速模式(400kHz)。但实际能跑多快,跟你的PCB走线、上拉电阻、从设备能力都有关系。我遇到过一块板子,400kHz死活调不通,降到200kHz就稳了。别死磕频率。
3.2 设备树配置
设备树配置,是RK平台I2C开发里最常打交道的地方。你不需要改内核代码,改dts就行。
一个典型的I2C节点长这样:
&i2c1 {
status = "okay";
clock-frequency = <400000>;
pinctrl-names = "default";
pinctrl-0 = <&i2c1_xfer>;
/* 从设备节点 */
touchscreen@38 {
compatible = "edt,edt-ft5406";
reg = <0x38>;
interrupt-parent = <&gpio0>;
interrupts = <RK_PB5 IRQ_TYPE_EDGE_FALLING>;
};
};
这里有几个关键属性:
| 属性 | 说明 | 注意事项 |
|---|---|---|
clock-frequency |
I2C总线时钟频率 | 不要超过从设备支持的最大值 |
pinctrl-0 |
引脚复用配置 | 必须与硬件原理图一致 |
reg |
从设备地址(7位) | 注意左对齐还是右对齐,RK用7位地址 |
status |
使能状态 | "okay" 或 "disabled" |
我曾经踩过的坑:设备树里 reg 填的是7位地址,但有些芯片手册给的是8位地址(带读写位)。比如手册写0x70,实际7位地址是0x38。一定要确认清楚,否则设备根本枚举不到。
3.3 I2C从设备驱动编写
这部分才是我们真正要写的代码。一个I2C从设备驱动,本质上就是一个 i2c_driver。
核心结构体:
static const struct i2c_device_id ft5406_id[] = {
{ "edt,edt-ft5406", 0 },
{ }
};
static struct i2c_driver ft5406_driver = {
.driver = {
.name = "edt-ft5406",
.of_match_table = ft5406_of_match,
},
.probe = ft5406_probe,
.remove = ft5406_remove,
.id_table = ft5406_id,
};
驱动加载流程:
- 匹配:内核根据设备树里的
compatible和驱动的of_match_table进行匹配。 - probe:匹配成功后调用
probe函数。在这里做初始化、注册中断、创建sysfs节点等。 - 读写:通过
i2c_smbus_read_byte_data()、i2c_master_send()等API与设备通信。
一个简单的读操作示例:
static int ft5406_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)
return 0;
else
return -EIO;
}
我的习惯:调试I2C设备时,先用 i2c-tools 在用户空间验证通信是否正常。命令 i2cdetect -y 1 可以扫描总线上的设备,i2cget 和 i2cset 可以直接读写寄存器。等用户空间调通了,再写内核驱动,能省很多时间。
3.4 调试技巧与常见问题
I2C调试,说白了就是看波形、看日志、看返回值。
- 看波形:逻辑分析仪是神器。抓一下SCL和SDA,看看起始条件、地址、数据、应答是否正常。我习惯用Saleae,便宜好用。
- 看日志:打开内核的I2C调试日志:
echo 7 > /sys/module/i2c_core/parameters/debug。能看到每次传输的细节。 - 看返回值:驱动里每个
i2c_transfer的返回值都要检查。返回负数就是出错,返回小于消息数量也是出错。
常见问题:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 设备枚举不到 | 地址错误、硬件没上电、引脚复用不对 | 量电压、查原理图、确认设备树 |
| 通信超时 | 时钟频率太高、从设备没应答、总线被拉死 | 降频率、抓波形、复位从设备 |
| 数据错位 | 地址左对齐/右对齐搞错、寄存器地址长度不对 | 仔细看芯片手册的时序图 |
| 中断不触发 | 中断号配置错、中断类型不对、GPIO没配成中断模式 | 检查设备树interrupts属性 |
重要提醒:I2C总线如果被拉死(SCL或SDA一直低电平),很多时候是因为从设备异常。可以尝试给从设备硬件复位,或者重新上电。如果还不行,检查上拉电阻是否焊好。我遇到过一例,是PCB上拉电阻虚焊,折腾了两天。
3.5 I2C驱动框架与数据流
下面这张图,是我自己总结的I2C数据流。从用户空间到硬件,每一层都有对应的接口。
从这张图可以看得很清楚:你的从设备驱动工作在I2C核心层之上,通过 i2c_client 与控制器驱动交互。控制器驱动再操作硬件寄存器。设备树则贯穿整个配置过程。
3.6 实战:编写一个简单的I2C从设备驱动
假设我们要驱动一个eeprom(AT24C02),设备地址0x50。驱动代码框架如下:
#include <linux/i2c.h>
#include <linux/module.h>
static int at24_probe(struct i2c_client *client)
{
dev_info(&client->dev, "AT24 eeprom probed\n");
/* 在这里注册misc设备或sysfs接口 */
return 0;
}
static void at24_remove(struct i2c_client *client)
{
dev_info(&client->dev, "AT24 eeprom removed\n");
}
static const struct of_device_id at24_of_match[] = {
{ .compatible = "atmel,24c02" },
{ }
};
MODULE_DEVICE_TABLE(of, at24_of_match);
static struct i2c_driver at24_driver = {
.driver = {
.name = "at24",
.of_match_table = at24_of_match,
},
.probe = at24_probe,
.remove = at24_remove,
};
module_i2c_driver(at24_driver);
设备树里加上:
&i2c2 {
status = "okay";
clock-frequency = <100000>;
eeprom@50 {
compatible = "atmel,24c02";
reg = <0x50>;
};
};
编译、烧录、加载驱动。用 i2cdetect -y 2 应该能看到地址0x50的UU标记(表示有驱动绑定)。
小技巧:调试的时候,可以在 probe 函数里加 dev_err 打印,这样即使驱动加载失败,你也能看到错误信息。我习惯在probe开头和结尾各加一句打印,确认流程走到了哪里。
嗯,I2C这部分内容其实不难,但细节很多。设备树配对了,驱动框架搭好了,剩下的就是读写时序的问题。多抓波形,多看手册,慢慢就熟了。