3、I2C总线基础:I2C协议时序、Linux I2C子系统架构、I2C设备驱动编写模板
各位同学,咱们今天聊聊I2C总线。说实话,在触控驱动这个领域,I2C是绝对绕不开的。你想想看,市面上90%以上的触控芯片,跟主控之间的通信接口都是I2C。我最早接触I2C是在做一款电容式触摸屏驱动的时候,当时被时序问题折腾得够呛——嗯,今天就把这些经验都抖出来。
3.1 I2C协议时序:从物理层说起
I2C总线就两根线:SCL(时钟线)和SDA(数据线)。别小看这两根线,里面的门道可不少。我个人的习惯是,先把时序图画在纸上,再动手写代码。
核心时序要素:
- 起始条件(START):SCL高电平时,SDA从高变低
- 停止条件(STOP):SCL高电平时,SDA从低变高
- 数据采样:SCL低电平时改变SDA,高电平时采样数据
- 应答位(ACK):每8位数据后,接收方拉低SDA表示应答
说白了,I2C的通信就像两个人对话。主设备说“我要开始说话了”(START),然后报上自己的地址和操作类型,从设备听到是自己的地址就回一声“嗯”(ACK),然后开始传数据。传完了说“我说完了”(STOP)。
我曾经在一个项目中遇到过时序问题:触控芯片偶尔不响应。查了半天,发现是SCL频率设置得太高,芯片跟不上。后来把频率从400kHz降到100kHz,问题就解决了。所以啊,别一味追求高速,稳定才是王道。
3.2 Linux I2C子系统架构
Linux内核的I2C子系统分三层,我画个图给你看:
| 层级 | 组件 | 职责 |
|---|---|---|
| 应用层 | 用户空间程序 | 通过设备节点(/dev/i2c-X)访问 |
| 核心层 | i2c-core | 总线管理、设备注册、驱动匹配 |
| 适配器层 | i2c-adapter | 硬件相关的收发实现 |
这里有个关键点:i2c-core 负责把设备和驱动配对。你写一个I2C设备驱动,本质上就是实现一个 i2c_driver 结构体,然后告诉内核:“嘿,我支持这个厂商ID和设备ID的设备。”
我的经验:调试I2C驱动时,先用 i2cdetect 工具扫描总线,确认设备地址是否正确。我见过太多人一上来就写驱动,结果发现设备地址都写错了。
3.3 I2C设备驱动编写模板
好,咱们直接上代码。这是我多年总结出来的模板,你拿去改改就能用:
#include <linux/i2c.h>
#include <linux/module.h>
/* 设备结构体 */
struct my_touch_device {
struct i2c_client *client;
int irq;
};
/* 读寄存器函数 */
static int my_touch_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, "I2C read failed\n");
return -EIO;
}
return 0;
}
/* probe函数 */
static int my_touch_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
struct my_touch_device *touch;
u8 chip_id;
/* 分配设备结构体 */
touch = devm_kzalloc(&client->dev, sizeof(*touch), GFP_KERNEL);
if (!touch)
return -ENOMEM;
touch->client = client;
i2c_set_clientdata(client, touch);
/* 读取芯片ID确认设备存在 */
my_touch_read_reg(client, 0x00, &chip_id);
dev_info(&client->dev, "Chip ID: 0x%02x\n", chip_id);
/* 注册中断等操作... */
return 0;
}
/* 设备ID表 */
static const struct i2c_device_id my_touch_id[] = {
{ "my_touch", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, my_touch_id);
/* 设备树匹配表 */
static const struct of_device_id my_touch_of_match[] = {
{ .compatible = "mycompany,my-touch" },
{ }
};
MODULE_DEVICE_TABLE(of, my_touch_of_match);
/* I2C驱动结构体 */
static struct i2c_driver my_touch_driver = {
.driver = {
.name = "my_touch",
.of_match_table = my_touch_of_match,
},
.probe = my_touch_probe,
.id_table = my_touch_id,
};
module_i2c_driver(my_touch_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
避坑指南:我曾经犯过一个低级错误——在probe函数里用了 msleep()。要知道,probe函数是在原子上下文调用的,不能睡眠。后来改用 mdelay() 才解决问题。记住:probe里别睡觉。
3.4 设备树配置
现在的嵌入式平台都用设备树来描述硬件。你的I2C设备在设备树里长这样:
&i2c1 {
status = "okay";
clock-frequency = <100000>; /* 100kHz */
touch@38 {
compatible = "mycompany,my-touch";
reg = <0x38>; /* I2C地址 */
interrupt-parent = <&gpio1>;
interrupts = <5 IRQ_TYPE_EDGE_FALLING>;
reset-gpios = <&gpio2 10 GPIO_ACTIVE_LOW>;
};
};
这里要注意 reg 字段,它指定了I2C设备的从机地址。我见过有人把地址写成了7位格式,但内核用的是8位格式(左移一位),结果设备死活找不到。其实内核会自动处理这个转换,你只要写7位地址就行。
3.5 调试技巧
最后分享几个调试技巧:
- 用逻辑分析仪抓波形:我习惯用Saleae,便宜又好用。看START、STOP、ACK信号对不对,一目了然。
- 内核调试开关:打开
CONFIG_I2C_DEBUG_BUS和CONFIG_I2C_DEBUG_CORE,内核会打印详细的I2C通信日志。 - 模拟I2C:如果硬件I2C控制器有问题,可以用GPIO模拟I2C时序。虽然慢,但调试起来方便。
我的习惯:每次写I2C驱动前,先用 i2cget 和 i2cset 工具手动读写几个寄存器,确认硬件通路没问题。这一步能省下大量调试时间。
好了,I2C的基础就讲到这里。下一章咱们会深入触控芯片的通信协议,到时候你会看到这些基础知识的实际应用。记住:I2C看似简单,但细节决定成败。多动手,多抓波形,你很快就能成为I2C高手。