4. I2C总线驱动:从控制器到从设备的完整链路
好,咱们今天聊I2C总线驱动。说实话,I2C这玩意儿在嵌入式系统里太常见了——传感器、触摸屏、音频Codec、电源管理芯片,十有八九都挂在I2C总线上。MTK8678这颗芯片也不例外,它内部集成了多个I2C控制器,每个控制器可以挂载多个从设备。
我个人习惯把I2C驱动分成三层来看:控制器驱动(也就是适配器驱动)、核心层(Linux内核帮你搞定的通用框架)、以及从设备驱动(你真正需要写的业务代码)。今天咱们重点讲控制器驱动和从设备驱动怎么写,核心层我简单带过,因为内核已经封装得很好了。
4.1 I2C控制器驱动框架:Linux是怎么管的?
Linux内核的I2C子系统,说白了就是一套总线-设备-驱动模型。你想想看,I2C控制器是硬件,它需要被抽象成一个struct i2c_adapter;挂在总线上的从设备,被抽象成struct i2c_client;而驱动代码,就是struct i2c_driver。
这三者的关系,我画个简图你感受一下:
I2C Core (核心层)
├── i2c_adapter (控制器/适配器)
│ ├── 算法: struct i2c_algorithm
│ │ ├── master_xfer() // 主模式传输
│ │ └── functionality() // 查询支持的功能
│ └── 物理设备: platform_device / device tree node
│
├── i2c_client (从设备)
│ ├── 地址: 7位或10位
│ └── 挂载在哪个adapter上
│
└── i2c_driver (从设备驱动)
├── probe() / remove()
└── id_table 匹配规则
嗯,这里要注意:控制器驱动负责实现底层的硬件操作,比如怎么发START信号、怎么产生时钟、怎么读写数据。而从设备驱动只管调用i2c_transfer()接口,不用关心底层时序细节。这种分层设计,让驱动开发变得清爽很多。
核心要点:控制器驱动提供"怎么发",从设备驱动只管"发什么"。两者通过i2c_adapter和i2c_algorithm解耦。
4.2 MTK I2C适配器实现:从寄存器到框架
好,咱们来点硬核的。MTK8678的I2C控制器,硬件上长什么样?
每个I2C控制器有一组寄存器,包括控制寄存器、状态寄存器、数据寄存器、时钟分频寄存器等等。我挑几个关键的说说:
| 寄存器名称 | 偏移地址 | 作用 |
|---|---|---|
| I2C_CON | 0x0000 | 控制寄存器:使能、中断、ACK/NACK设置 |
| I2C_STA | 0x0004 | 状态寄存器:传输完成、总线忙、错误标志 |
| I2C_DAT | 0x0008 | 数据寄存器:读写数据都走这里 |
| I2C_CLK_DIV | 0x000C | 时钟分频:决定SCL频率 |
| I2C_SLAVE_ADDR | 0x0010 | 从设备地址寄存器 |
在MTK平台上,I2C控制器通常被注册为platform_driver。驱动入口长这样:
static const struct of_device_id mtk_i2c_of_match[] = {
{ .compatible = "mediatek,mt8678-i2c", },
{ /* sentinel */ }
};
static struct platform_driver mtk_i2c_driver = {
.probe = mtk_i2c_probe,
.remove = mtk_i2c_remove,
.driver = {
.name = "mtk_i2c",
.of_match_table = mtk_i2c_of_match,
},
};
module_platform_driver(mtk_i2c_driver);
在probe()函数里,我们要做几件事:
- 获取硬件资源:从设备树里拿到寄存器基地址、中断号、时钟。
- 初始化硬件:复位控制器、设置时钟分频、使能中断。
- 填充i2c_adapter:设置算法、名称、设备节点。
- 注册适配器:调用
i2c_add_adapter(),让内核知道这个控制器存在。
我贴一段核心代码,你感受一下:
static int mtk_i2c_probe(struct platform_device *pdev)
{
struct mtk_i2c *i2c;
struct i2c_adapter *adap;
int ret;
i2c = devm_kzalloc(&pdev->dev, sizeof(*i2c), GFP_KERNEL);
if (!i2c)
return -ENOMEM;
// 1. 获取寄存器基地址
i2c->base = devm_platform_ioremap_resource(pdev, 0);
if (IS_ERR(i2c->base))
return PTR_ERR(i2c->base);
// 2. 获取时钟
i2c->clk = devm_clk_get(&pdev->dev, "main");
if (IS_ERR(i2c->clk))
return PTR_ERR(i2c->clk);
clk_prepare_enable(i2c->clk);
// 3. 硬件初始化:设置100KHz标准模式
mtk_i2c_set_clk_div(i2c, 100000);
// 4. 填充适配器
adap = &i2c->adap;
adap->owner = THIS_MODULE;
adap->class = I2C_CLASS_DEPRECATED;
adap->algo = &mtk_i2c_algorithm;
adap->dev.of_node = pdev->dev.of_node;
adap->nr = pdev->id;
snprintf(adap->name, sizeof(adap->name), "MTK I2C %d", pdev->id);
// 5. 注册适配器
ret = i2c_add_numbered_adapter(adap);
if (ret) {
dev_err(&pdev->dev, "failed to add i2c adapter\n");
clk_disable_unprepare(i2c->clk);
return ret;
}
platform_set_drvdata(pdev, i2c);
return 0;
}
个人经验:我在MTK平台上调试I2C时,踩过最大的坑是时钟分频算错了。MTK的时钟树比较复杂,APB总线时钟和I2C模块时钟可能不同源。我建议你在mtk_i2c_set_clk_div()里加个调试打印,把实际算出来的SCL频率打出来,用示波器量一下确认。
接下来是算法实现,也就是struct i2c_algorithm里的两个回调:
static const struct i2c_algorithm mtk_i2c_algorithm = {
.master_xfer = mtk_i2c_master_xfer,
.functionality = mtk_i2c_functionality,
};
master_xfer()是核心函数,它接收一个struct i2c_msg数组,每个msg描述了一次传输的方向、地址、数据和长度。MTK控制器的硬件逻辑是:写一个字节到数据寄存器,然后等待传输完成中断,再写下一个字节。读操作类似,只是方向反过来。
我简化一下实现逻辑:
static int mtk_i2c_master_xfer(struct i2c_adapter *adap,
struct i2c_msg *msgs, int num)
{
struct mtk_i2c *i2c = i2c_get_adapdata(adap);
int i, ret;
for (i = 0; i < num; i++) {
// 设置从设备地址
writel(msgs[i].addr << 1, i2c->base + I2C_SLAVE_ADDR);
if (msgs[i].flags & I2C_M_RD) {
// 读操作
ret = mtk_i2c_read_bytes(i2c, msgs[i].buf, msgs[i].len);
} else {
// 写操作
ret = mtk_i2c_write_bytes(i2c, msgs[i].buf, msgs[i].len);
}
if (ret < 0)
return ret;
}
return num;
}
注意:MTK的I2C控制器在传输完成后,需要检查状态寄存器里的NACK标志位。如果从设备没有应答,硬件会置位NACK位。我曾经遇到一个情况:从设备地址写错了,但代码没检查NACK,结果一直读到0xFF,排查了半天才发现是地址问题。
4.3 从设备驱动编写:以温度传感器为例
好了,控制器驱动写好了,现在该写从设备驱动了。假设我们挂了一个温度传感器,比如LM75,它的I2C地址是0x48(7位地址)。
从设备驱动的套路很固定:
- 定义
struct i2c_driver,填充probe()、remove()和id_table。 - 在
probe()里,拿到struct i2c_client,然后注册一个字符设备或者输入设备。 - 实现读写函数,调用
i2c_smbus_read_byte_data()或i2c_transfer()。
代码示例:
static const struct i2c_device_id lm75_ids[] = {
{ "lm75", 0 },
{ }
};
static int lm75_probe(struct i2c_client *client)
{
struct lm75_data *data;
int ret;
data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
data->client = client;
i2c_set_clientdata(client, data);
// 读取温度寄存器(0x00),验证设备是否存在
ret = i2c_smbus_read_word_swapped(client, 0x00);
if (ret < 0) {
dev_err(&client->dev, "failed to read temperature\n");
return ret;
}
// 注册一个sysfs接口或者字符设备
// ...
return 0;
}
static struct i2c_driver lm75_driver = {
.driver = {
.name = "lm75",
},
.probe = lm75_probe,
.remove = lm75_remove,
.id_table = lm75_ids,
};
module_i2c_driver(lm75_driver);
这里有个细节:i2c_smbus_read_word_swapped()是SMBus协议里的函数,它自动处理了字节序。LM75的温度寄存器是16位,高字节在前,低字节在后,所以用swapped版本刚好。
我个人的习惯:在probe()里先做一次读操作,验证设备是否真的在线。如果读不到数据,直接返回-ENODEV,避免后面操作一个不存在的设备。这个习惯帮我省了不少调试时间。
4.4 设备树配置:把硬件描述清楚
在MTK平台上,I2C控制器和从设备的信息都在设备树里描述。控制器节点长这样:
&i2c0 {
pinctrl-names = "default";
pinctrl-0 = <&i2c0_pins>;
status = "okay";
clock-frequency = <100000>;
lm75@48 {
compatible = "lm75";
reg = <0x48>;
};
};
注意reg = <0x48>,这个地址是7位地址左移0位后的值。有些平台要求左移1位,但Linux内核里i2c_client的地址是7位原始地址,不需要移位。嗯,这个细节容易搞混,我建议你查一下内核文档Documentation/i2c/instantiating-devices.rst。
4.5 调试技巧:没有示波器怎么搞?
说实话,I2C调试最靠谱的工具是示波器或逻辑分析仪。但有时候手边没有,怎么办?
我分享几个软件层面的调试方法:
- 打开内核I2C调试:编译内核时开启
CONFIG_I2C_DEBUG_BUS和CONFIG_I2C_DEBUG_ALGO,内核会打印每次传输的细节。 - 使用i2c-tools:在用户空间用
i2cdetect -y 0扫描总线,用i2cget和i2cset直接读写寄存器。这个工具在MTK平台上可以直接用。 - 加调试打印:在
master_xfer()里打印每个msg的地址、长度、方向和数据。我一般会加个动态调试开关,避免影响性能。
避坑指南:我曾经遇到一个I2C总线锁死的问题——从设备拉低了SCL线,导致总线一直忙。排查后发现是从设备进入了异常状态,需要给它一个复位信号。从那以后,我在每个I2C控制器的probe()里都加了总线恢复逻辑:先拉高SCL 9个时钟脉冲,让从设备释放总线。
4.6 小结
I2C总线驱动,说白了就是三件事:
- 控制器驱动:操作寄存器,实现
master_xfer(),把硬件能力暴露给内核。 - 从设备驱动:调用
i2c_transfer(),实现具体的设备功能。 - 设备树:描述硬件拓扑,让驱动和设备匹配起来。
在MTK8678上,I2C控制器驱动已经由MTK官方提供了,大部分情况下你只需要写从设备驱动。但理解底层实现,能帮你更快地定位问题。下次遇到I2C通信失败,你知道该查时钟分频、查NACK标志、还是查设备树地址——这就是系统思维的价值。
好,这一章就到这儿。下一章咱们聊SPI总线驱动,那个比I2C稍微复杂一点,但套路是相通的。