📚 训练目标:
- 从驱动模型出发,掌握 I2C 子系统的核心结构;
- 分析控制器与从设备的注册流程;
- 结合 AT24 EEPROM 驱动源码与设备树实例,理解 i2c_client 与 i2c_driver 的交互;
- 配套高质量练习题巩固理解。
一、I2C 子系统的本质:一个总线驱动子系统
在 Linux 内核中,I2C 是一个标准的总线型子系统(bus_type),不是 platform 设备模型。
I2C 子系统结构如下:
组件 | 结构体 | 描述 |
---|---|---|
I2C 总线 | bus_type i2c_bus_type |
内核为 I2C 提供的驱动模型 |
控制器(主机) | i2c_adapter |
所在 SoC 上的控制器,由 platform_driver 注册 |
从设备 | i2c_client |
掛载在总线上的外设设备(如 EEPROM、Codec) |
驱动程序 | i2c_driver |
针对某类设备的驱动程序 |
控制器驱动:使用 platform_driver 绑定 SoC 资源,注册 i2c_adapter。
设备驱动:使用 i2c_driver 注册,等待与 i2c_client 匹配。
二、源码分析:控制器是如何注册进来的?
以 NXP i.MX8MP 的 I2C 控制器为例,驱动路径:drivers/i2c/busses/i2c-imx.c
。
static int i2c_imx_probe(struct platform_device *pdev)
{
struct i2c_adapter *adapter;
struct imx_i2c_struct *i2c_imx;
// 1. 分配结构体
i2c_imx = devm_kzalloc(&pdev->dev, sizeof(*i2c_imx), GFP_KERNEL);
// 2. 获取寄存器资源、时钟、中断
i2c_imx->base = devm_ioremap_resource(&pdev->dev, res);
i2c_imx->clk = devm_clk_get(&pdev->dev, NULL);
i2c_imx->irq = platform_get_irq(pdev, 0);
// 3. 初始化 i2c_adapter 结构体
adapter = &i2c_imx->adapter;
adapter->owner = THIS_MODULE;
adapter->algo = &i2c_imx_algo;
adapter->dev.parent = &pdev->dev;
adapter->nr = pdev->id;
strlcpy(adapter->name, "i.MX I2C Adapter", sizeof(adapter->name));
// 4. 注册适配器到内核
return i2c_add_numbered_adapter(adapter);
}
该驱动注册的是 platform_driver
,但其核心工作是初始化并注册 i2c_adapter
。
三、设备是如何通过设备树挂载上来的?
设备树示例:
&i2c1 {
status = "okay";
eeprom@50 {
compatible = "atmel,24c02";
reg = <0x50>;
pagesize = <16>;
};
};
设备树加载过程:
- 控制器节点
i2c1
被 platform_driver 匹配; - probe 中调用
i2c_add_adapter()
,注册一条总线; - 子节点
eeprom@50
被of_i2c_register_devices()
扫描; - 创建
i2c_client
对象(地址为 0x50); - 内核查找合适的
i2c_driver
进行绑定。
四、i2c_driver 是如何匹配和驱动设备的?
以 AT24 EEPROM 驱动为例,路径为 drivers/misc/eeprom/at24.c
。
1. 定义驱动匹配表和结构体
static const struct i2c_device_id at24_ids[] = {
{ "24c02", 0 }, { /* more */ }, {}
};
static const struct of_device_id at24_of_match[] = {
{ .compatible = "atmel,24c02" },
{ /* more */ }, {}
};
static struct i2c_driver at24_driver = {
.driver = {
.name = "at24",
.of_match_table = at24_of_match,
},
.probe = at24_probe,
.remove = at24_remove,
.id_table = at24_ids,
};
module_i2c_driver(at24_driver);
2. 驱动入口:probe 函数
static int at24_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
// 打印设备信息
dev_info(&client->dev, "AT24 EEPROM detected at address 0x%x\n", client->addr);
// 挂载到 MTD、NVMEM 或字符设备(不同内核版本机制不同)
return devm_nvmem_register(...);
}
驱动完成初始化后,用户可通过 sysfs
或 i2c-tools
访问 EEPROM 内容。
五、如何访问设备?—— 使用 i2c_transfer
int i2c_transfer(struct i2c_adapter *adap,
struct i2c_msg *msgs, int num);
示例:读 EEPROM
u8 addr = 0x00;
u8 buf[16];
struct i2c_msg msgs[2] = {
{
.addr = client->addr,
.flags = 0,
.buf = &addr,
.len = 1,
},
{
.addr = client->addr,
.flags = I2C_M_RD,
.buf = buf,
.len = 16,
}
};
i2c_transfer(client->adapter, msgs, 2);
六、练习题目(含答案)
💡 题目 1:以下哪个结构体代表 I2C 总线控制器?
A. struct i2c_driver
B. struct i2c_client
C. struct i2c_adapter
D. struct i2c_msg
✅ 答案:C
i2c_adapter
表示控制器,负责发起读写。
💡 题目 2:哪一个函数用于驱动注册设备驱动(如 AT24)?
A. platform_driver_register()
B. i2c_add_adapter()
C. i2c_register_driver()
D. of_register_platform_driver()
✅ 答案:C
i2c_register_driver()
会将i2c_driver
注册到i2c_bus_type
下,由内核匹配。
💡 题目 3:设备树中 reg = <0x50>;
表示的含义是什么?
A. 设备在系统内的 IRQ 编号
B. I2C 控制器的总线编号
C. 设备的物理地址
D. 设备在 I2C 总线上的从地址
✅ 答案:D
在 I2C 总线中,
reg
对应从设备地址(如 0x50 即为 EEPROM 地址)。
七、实战建议与延伸学习
推荐进一步学习内容:
- 使用 i2c-tools 进行测试:
i2cdetect
,i2cget
,i2cset
- 理解
regmap
+i2c
的结合方式(如 codec 驱动) - 在 I2C 多路复用器(i2c-mux)场景下的子设备建模
八、结语
Linux 的 I2C 子系统是一个典型的“总线-设备-驱动”三段式模型。
控制器使用 platform_driver 驱动,而子设备由 i2c_driver 管理,匹配过程由 I2C 核心完成。
通过本文,我们不仅从结构上掌握了子系统的组织方式,也通过实际代码洞察了其内部运行机制,并借助练习巩固了要点。
视频教程请关注 B 站:“嵌入式 Jerry”