以下是对您提供的博文内容进行深度润色与结构重构后的技术文章。整体风格更贴近一位资深嵌入式Linux内核开发者在技术社区中自然、扎实、有温度的分享——去AI痕迹、强逻辑流、重实战感、轻说教味,同时严格遵循您提出的全部优化要求(无模板化标题、无总结段、无缝融合知识点、口语化专业表达、关键点加粗提示等)。
I2C设备“活”起来之前,Linux内核到底做了什么?
你有没有遇到过这样的场景:
设备树里明明写了codec@1a,驱动也编译进去了,modprobe rt5682也成功了,可dmesg里就是看不到rt5682_probe()的打印,/sys/bus/i2c/devices/下空空如也?
或者更玄学一点:i2c-2是存在的,i2cdetect -y 2能扫出地址,但一读寄存器就超时,示波器上看SCL根本没动?
这不是驱动写错了,也不是硬件焊反了——而是你还没真正看懂:从.dts文件里那一行codec@1a { ... };到内核里一个能read_reg()的struct i2c_client *client,中间到底发生了多少层“翻译”和“握手”?
今天我们就以 ARM64 平台(RK3588 / i.MX93 / SM8550)为背景,不讲概念复读,不列函数调用栈,而是像拆解一台老式收音机那样,一层层拧开外壳,看看 Linux 内核是怎么把设备树里的静态文本,“点化”成运行时可用的 I2C 外设实例的。
设备树不是配置文件,是“硬件说明书”的二进制快照
很多人把.dts当作类似 U-Boot 环境变量的配置项——改个status = "okay"就完事。这其实是最大的误解。
设备树的本质,是启动早期由 bootloader 加载进内存的一块只读数据区(.dtb),它不参与编译,也不被链接进 vmlinux;内核拿到它之后,做的第一件事,是把它“摊平”成一棵struct device_node *组成的树状链表。这个过程叫unflatten_device_tree(),发生在setup_arch()里,比mm_init()还早。
所以,.dts中每个{}块,在内存里都对应一个实实在在的device_node结构体。而&i2c2 { ... }这种写法,本质是告诉内核:“这里有个叫i2c2的节点,它的phandle指向i2c@feac0000,请把它挂到根节点下面”。
⚠️ 关键提醒:
&i2c2是引用(reference),不是定义。真正的定义一定在.dtsi里,比如i2c@feac0000 { compatible = "rockchip,rk3588-i2c"; ... };——如果你只改了.dts里的&i2c2,却忘了.dtsi里对应节点的status或compatible,那整条链就断了。
再来看这段常见代码:
&i2c2 { status = "okay"; clock-frequency = <400000>; eeprom@50 { compatible = "atmel,24c02"; reg = <0x50>; pagesize = <16>; }; };它其实隐含了三层含义:
- 第一层:
i2c2是个platform device,要走platform_bus匹配; - 第二层:
eeprom@50是i2c2的子节点,但它不是 platform device,而是一个待注册的 I2C client; - 第三层:
compatible = "atmel,24c02"不是给i2c2看的,是留给i2c_bus匹配用的——也就是说,这个字符串,最终会去和drivers/misc/eeprom/at24.c里那个static const struct i2c_device_id at24_ids[]表做比对。
这就是为什么你不能把atmel,24c02写成atmel,24c02a——少一个字母,匹配就失败,probe()永远不会被调。
I2C控制器怎么“上线”?靠的是 platform_bus,不是 i2c_bus
很多初学者卡在这一步:明明i2c-2没出来,就急着去查at24.ko有没有加载。其实顺序完全反了。
I2C 总线上的设备能不能工作,第一道门槛是控制器本身得先“活”过来。
ARM64 上的 I2C 控制器(比如 RK3588 的 I2C2),在设备树里长得像这样:
i2c@feac0000 { compatible = "rockchip,rk3588-i2c"; reg = <0x0 0xfeac0000 0x0 0x1000>; interrupts = <GIC_SPI 56 IRQ_TYPE_LEVEL_HIGH>; #address-cells = <1>; #size-cells = <0>; clocks = <&cru PCLK_I2C2>; clock-names = "i2c"; power-domains = <&power RK3588_PD_PERI>; status = "okay"; };注意几个关键点:
compatible = "rockchip,rk3588-i2c"→ 这是platform_driver的匹配钥匙;reg描述的是控制器寄存器的物理地址范围,不是 I2C 设备地址;#address-cells = <1>和#size-cells = <0>是 I2C bus type 的硬性约定,意味着子节点的reg只取一个 u32 值作为设备地址(即0x50),不带长度字段。
当内核解析到这个节点,会把它当作一个platform_device注册到platform_bus上。随后触发匹配流程:
// drivers/i2c/busses/i2c-rk3x.c static const struct of_device_id rk3x_i2c_of_match[] = { { .compatible = "rockchip,rk3399-i2c" }, { .compatible = "rockchip,rk3588-i2c" }, // ← 就是它! { } }; MODULE_DEVICE_TABLE(of, rk3x_i2c_of_match); static struct platform_driver rk3x_i2c_driver = { .probe = rk3x_i2c_probe, .remove = rk3x_i2c_remove, .driver = { .name = "rk3x-i2c", .of_match_table = rk3x_i2c_of_match, // ← platform_bus 用它匹配 }, };rk3x_i2c_probe()干了四件大事:
devm_ioremap_resource():把0xfeac0000映射成虚拟地址;clk_prepare_enable():打开时钟,否则寄存器读写全返回 0;devm_request_irq():申请中断(有些 I2C 控制器用轮询,但 RK 系列基本都用中断);i2c_add_numbered_adapter():这才是最关键的一步——它把struct i2c_adapter注册进 I2C core,生成/sys/class/i2c-adapter/i2c-2,并触发后续 client 创建。
💡 小技巧:如果
i2c-2没出现,先dmesg | grep -i "rk3x\|i2c"看 probe 是否执行;如果没输出,说明 platform_driver 根本没匹配上——这时候回头检查compatible拼写、status状态、clocks 是否 enable。
client 不是“生”出来的,是 I2C core “造”出来的
很多人以为:我写了eeprom@50,内核就会自动创建一个i2c_client。但真相是:client 是 I2C core 主动构造的,不是设备树“生”出来的。
这个动作发生在i2c_add_numbered_adapter()返回之后,由 I2C core 主动调用of_i2c_register_devices(adap)完成。
我们来还原一下这个过程:
i2c_add_numbered_adapter()注册完adap后,会遍历它的dev.of_node->child链表;- 对每个子节点(比如
eeprom@50),调用of_i2c_register_device(); - 该函数内部:
- 解析reg = <0x50>→ 得到info.addr = 0x50;
- 解析compatible = "atmel,24c02"→ 填入info.type;
- 解析interrupts→ 调用of_irq_get()获取 Linux IRQ 编号;
- 最终调用i2c_new_client_device(adap, &info),生成struct i2c_client *client; i2c_new_client_device()内部会:
- 分配client内存;
- 设置client->adapter = adap;
- 调用device_register(&client->dev),将它挂到i2c_bus_type上;
- 触发i2c_bus.match(),开始找 driver。
看到没?整个过程里,设备树只是提供原始参数,真正干活的是 I2C core 的 C 代码。这也是为什么你可以用i2c_new_dummy_device()在 runtime 动态创建 client——设备树只是最常用的一种初始化方式,不是唯一方式。
🔍 调试线索:如果
i2c-2存在,但/sys/bus/i2c/devices/2-0050/没出现,说明of_i2c_register_devices()没执行或执行失败。此时可以加一句pr_info("creating client for %pOF\n", child);到of_i2c_register_devices()里确认是否走到这步。
为什么要有两套总线?platform_bus 和 i2c_bus 不是重复造轮子
这个问题问到了 Linux 设备模型的底层哲学。
简单说:platform_bus 管“谁来管总线”,i2c_bus 管“总线上挂谁”。它们服务的对象、生命周期、匹配逻辑,完全不同。
| 维度 | platform_bus | i2c_bus |
|---|---|---|
| 设备类型 | SoC 内部集成模块(UART / I2C 控制器 / PWM) | 外挂芯片(EEPROM / Codec / Sensor) |
| 设备来源 | of_platform_populate()扫描根节点及其子树 | of_i2c_register_devices()扫描 I2C controller 的子节点 |
| 驱动匹配依据 | .of_match_table(匹配compatible) | .id_table(匹配info.type,即compatible字符串) |
| 生命周期 | 内核启动期静态注册,不可热插拔 | 可热插拔(需 mux 支持),也可动态创建 |
| 地址空间 | 无统一地址空间(每个 controller 自己管) | 全局 I2C 地址空间(0x00–0x7F),需避免冲突 |
举个现实例子:你在 RK3588 板子上换了一颗新的 PMIC,型号从rk806换成rk809。
你只需要改设备树里pmic@20节点的compatible = "rockchip,rk809",并确保rk809驱动已编译——不用碰 I2C controller 驱动,也不用改任何 client 驱动。因为rk809依然是挂在i2c-1上的一个 client,匹配逻辑完全走i2c_bus。
反过来,如果你把 RK3588 换成 i.MX93,I2C controller 的 IP 核变了,那你只需更新platform_driver(比如换成imx-lpi2c),所有挂在它上面的 client 驱动——at24、rt5682、bme280——全都原封不动继续用。
这就是两级总线设计的真正价值:解耦。不是为了炫技,而是为了在芯片迭代、硬件变更、驱动维护时,把改动控制在最小范围内。
实战排障:三个高频问题,一句命令定位根源
❌ 问题1:dmesg里看不到at24_probe(),但i2c-2是存在的
→ 先跑这句:
dmesg | grep -E "(at24|2-0050|i2c.*[0-9]-[0-9a-f]{3})"- 如果看到
i2c i2c-2: new_device: can't create device at 0x50,说明地址冲突(另一个设备也在用 0x50); - 如果看到
at24 2-0050: failed to get page size,说明pagesize属性没被正确解析(检查.dts是否拼错); - 如果啥都没看到,说明
of_i2c_register_devices()根本没执行 → 检查i2c2节点下是否有status = "okay",且没有被其他节点 disable。
❌ 问题2:i2cdetect -y 2能扫出地址,但i2cget -y 2 0x50 0x00返回Error: Read failed
→ 这是典型的电气或时序问题。别急着改驱动,先看设备树:
&i2c2 { clock-frequency = <100000>; // 先降频试试 i2c-scl-falling-time-ns = <30>; i2c-scl-rising-time-ns = <150>; i2c-sda-falling-time-ns = <30>; };RK 系列驱动支持这些属性,会自动配置CON,CLKDIVL,CLKDIVH寄存器。如果硬件上用了 10k 上拉、线长 15cm,400kHz 很可能不稳定。
❌ 问题3:modprobe at24成功,但/sys/bus/i2c/devices/2-0050/eeprom不可读
→ 检查at24驱动是否启用了CONFIG_MISC_DEVICES和CONFIG_EEPROM_AT24;更重要的是,确认at24的id_table是否包含"atmel,24c02":
static const struct i2c_device_id at24_ids[] = { { "24c01", AT24_DEVICE_MAGIC(1, 8) }, { "24c02", AT24_DEVICE_MAGIC(2, 8) }, // ← 必须有这一行! { "atmel,24c02", AT24_DEVICE_MAGIC(2, 8) }, // ← 更推荐带 vendor 的写法 { } };很多旧版驱动只写了"24c02",不认"atmel,24c02",就会匹配失败。
最后一句实在话
设备树不是魔法,I2C 注册也不是黑箱。它是一套高度工程化的协作机制:
设备树负责“说清楚”,platform_bus 负责“搭好桥”,i2c_bus 负责“接上人”,I2C core 负责“发号施令”,而驱动,只是听命行事的那个“人”。
当你再遇到 probe 不执行、client 不创建、通信失败这些问题时,别急着翻驱动源码——先dmesg | grep i2c,再ls /sys/bus/i2c/devices/,然后打开.dts和drivers/i2c/对着看。你会发现,那些曾经神秘的宏、结构体、匹配逻辑,其实都有迹可循,有据可依。
如果你在实现过程中遇到了其他挑战,欢迎在评论区分享讨论。