☰
RK3576 从 I2C 到 I3C:总线提速的真实收益与 DTS 配置陷阱
2026/10/1 14:52:34 网站建设 项目流程

把 I2C 换到 I3C 之后,我的第一反应是“总线快了,传感器读取肯定更顺了”。但把这个直觉落到 RK3576 开发板上,结果只对了一半。我这次把一颗气压计和一颗 9 轴 IMU 从原来的 i2c4 总线迁到 i3c0,改完 DTS 后系统确实能把设备枚举出来,但连续读数据的延迟并没有想象中那种“10 倍碾压”——读小寄存器时甚至和 400kHz 的 I2C 差不多。后来我把总线时序、DTS 节点、驱动挂载方式全部捋了一遍,才搞清楚这个“快 10 倍”到底在什么条件下成立,以及 RK3576 这类 SoC 上配置 I3C 有哪些坑值得避开。这篇文章就把这些内容完整写出来,适合正在 RK35 系列上做传感器接入、屏幕显示或调试总线性能的 Linux 开发者参考。

1. 所谓“快 10 倍”,先弄清楚 I3C 的速率标称与真实吞吐

先说结论:I3C 的标称速率确实比 I2C 高一个数量级,但“10 倍”是一个宣传话术,真实能拿到多少倍取决于你传输的数据量、控制器实现和总线上挂着多少老器件。

1.1 I2C 快不上去的原因不在协议,而在开漏物理层

I2C 为什么这么多年一直停留在几百 Kbps?不是协议复杂,而是它的物理层是开漏(open-drain)结构。总线上所有器件只能把 SDA 拉低,释放后靠外部上拉电阻把电平拉高。这个下拉到上拉的翻转过程受 RC 时间常数限制,总线电容越大、上拉电阻越大,上升沿就越慢。为了保证时序能对齐,I2C 只能把时钟压下来。

常见的 I2C 速率档位是标准模式 100kbps、快速模式 400kbps、快速+模式 1Mbps,高速模式 3.4Mbps 虽然存在,但在嵌入式系统和传感器上用得很少。所以拿 I3C 默认的 SDR 模式 12.5Mbps 去对比 I2C 快速模式 400kbps,确实是 31 倍;拿它对比快速+的 1Mbps,也差不多是 12.5 倍。这就是“快 10 倍”说法的来源。

1.2 I3C 的 SDR、DDR 和 HDR 模式到底是怎么回事

I3C 由 MIPI 联盟制定,物理层不再要求所有信号都开漏。它的 SDR 单数据速率模式里,SCL/SDA 在数据阶段可以用推挽方式驱动,上升沿不再受上拉电阻限制,所以能把时钟直接拉到 12.5MHz。DDR 双沿采样模式可以到 25Mbps,HDR 模式还能更高。

但要注意,普通 ARM SoC 的 I3C 控制器不一定把 HDR 模式全部实现。RK3576 的驱动我实际看过,最常见的是 SDR 和 DDR 模式;HDR-TSP/HDR-TSL 这类更高速的模式要看 BSP 内核版本和 IP 授权情况,写 DTS 时不要默认它一定支持。

模式标称速率说明
I2C 标准模式100 kbps开漏结构决定,兼容性最好
I2C 快速模式400 kbps多数传感器和 OLED 的上限
I2C 快速+1 Mbps部分 PMIC/EEPROM 支持
I3C SDR12.5 Mbps推挽驱动,单沿采样,最常见
I3C DDR25 Mbps双沿采样,支持度看驱动
I3C HDR更高控制器未必实现完整

1.3 为什么实测吞吐不一定有 10 倍

I3C 标称速率高,但协议开销也在。I3C 地址不再是固定的 7 位从机地址,而是通过 DAA(动态地址分配)在总线枚举阶段给每个设备分配动态地址;此外还有 CCC 通用命令、IBI 带内中断、CRC 校验这些新机制。每一条命令都比老 I2C 多了不少握手步骤。

实际测下来,如果读取的是一个 64 字节的 FIFO,I3C SDR 模式相对 I2C 400kHz 能快 12 倍左右,这个差距很明显;但如果只读一个 2 字节的芯片 ID,I2C 大约 125us,I3C 大约 45us,差距只有 3 倍上下。因为小包传输里,地址阶段、切换方向和时序开销占了大部分时间,总线位速率再高也救不回来。理解这一点,才不会在迁移后对性能抱有不切实际的期待。

2. RK3576 上的 I3C 到底在哪:资源、引脚与内核支持

I3C 不是随便一个引脚就能跑的。写 DTS 之前,必须先在 SoC 的 TRM 或厂商提供的 dtsi 里找到 I3C 控制器对应哪组引脚、有没有和 I2C/SPI 复用,否则改出来的节点要么起不来,要么和别的外设抢资源。

2.1 先查 TRM 再写 DTS,别照抄其它平台的节点

RK3576 是 Rockchip 比较新的 SoC,芯片手册里 I3C 控制器一般会标注为 i3c0、i3c1,也可能同时存在多路 I2C。不同核心板、不同商用的开发板,引出的 I3C 引脚位置可能不一样。我手上的板子把 i3c0 引到了某个 40pin 排针,另一块同型号的板子却默认把它复用成 I2C 了。所以最靠谱的办法是直接打开厂商 BSP 里的 rk3576.dtsi,搜索i3c关键字,看它定义了哪些节点,再从板级 dts 里找&i3c0 { ... }这种覆盖节点。

我看到有的网友直接拿 RK3568 或 RK3588 的 I3C 节点往 RK3576 上套,结果 pinctrl 名字对不上,编译都过不了。每颗 SoC 的 iomux 名字不同,例如 RK3576 里常见&i3c0_xfer这种 pinctrl 定义,最好以 BSP 自带的.dtsi里实际存在的名字为准。

2.2 I3C 控制器很可能与 I2C 共用引脚

RK3576 这类 SoC 的引脚复用非常灵活,I3C 控制器和 I2C 控制器经常共用同一组 SCL/SDA 引脚。DTS 里一旦同时把&i2c0和&i3c0都设置为status = "okay",后加载的驱动就会因为引脚已经被占用而申请 pinctrl 失败。表现是内核日志里出现pin already requested之类的报错,但总线本身没有坏。

正确的做法是先确认板级 dts 里默认打开了哪一路。如果硬件设计上传感器接在 I3C 引脚上,就把对应 I2C 节点关闭,或者反过来。我习惯用cat /sys/kernel/debug/gpio和cat /sys/kernel/debug/pinctrl/pinctrl-handles查看当前引脚被谁占着,比翻原理图更快。

2.3 Linux 内核的 I3C 子系统支持情况

Linux 内核从很早的版本开始就把 I3C 作为一个独立子系统放在drivers/i3c/,里面分了 master 驱动和 device 驱动。RK3576 的 BSP 如果用的是较新内核,一般会带CONFIG_I3C以及对应的 Rockchip/DesignWare I3C 主控驱动。但 I3C 器件驱动本身还不多,很多传感器厂商只提供 I2C 驱动,不提供原生 I3C 客户端驱动。

这里要注意:I3C 主控驱动在 Linux 里往往同时注册为 I2C adapter,目的是让挂在上面的传统 I2C 设备还能继续用原来的 i2c 驱动。所以即使你完全不用 I3C 新特性,也可以先把它当作一个高速 I2C 控制器来用。这是 RK3576 上比较讨巧的一种玩法:DTS 写成 I3C 节点,但总线只挂老 I2C 器件,并降低时钟频率运行。

3. DTS 配置实战:从 i2c 节点改造成 i3c 节点

DTS 改动看起来很简单,就是把&i2c3换成&i3c0,但真正落板时会有很多细节。下面用我这次迁移气压计和 IMU 的实际节点来演示。

3.1 改造前:原有 I2C 节点长什么样

原来的设备树大概是这样的:

&i2c4 { status = "okay"; clock-frequency = <400000>; pinctrl-0 = <&i2c4_xfer>; pinctrl-names = "default"; pressure_sensor: pressure@48 { compatible = "vendor,pressure-sensor"; reg = <0x48>; interrupt-parent = <&gpio1>; interrupts = <13 IRQ_TYPE_LEVEL_LOW>; }; };

这里关键信息是子节点的reg = <0x48>,也就是 I2C 7 位从机地址。驱动通过地址匹配来绑定,地址错误直接导致i2c detect扫不到设备。

3.2 改造后:I3C 节点和子设备节点怎么挂

改成 I3C 总线后,节点会长这样:

&i3c0 { status = "okay"; clock-frequency = <12500000>; pinctrl-0 = <&i3c0_xfer>; pinctrl-names = "default"; /* 传统 I2C 器件:reg 就是静态 7 位地址 */ pressure_sensor: pressure@48 { compatible = "vendor,pressure-sensor"; reg = <0x48>; }; /* 原生 I3C 器件:reg 通常用于动态地址协商,也可用 assigned-address 预设 */ imu: imu@0 { compatible = "vendor,imu-i3c"; reg = <0x0>; assigned-address = <0x6a>; }; };

需要特别说明的是,原生 I3C 器件挂载方式和传统 I2C 不太一样。I3C 总线会在启动时执行 DAA 流程给每个原生设备分配动态地址,所以子节点的reg不一定代表最终地址。有些内核版本要求你把reg写成设备预设的动态地址,有些版本则允许写0让控制器自动分配。这个细节在不同 BSP 里差异很大,最稳妥的方法是查看内核源码里的Documentation/devicetree/bindings/i3c/目录,对应器件的 binding 文档会写清楚。

3.3 时钟频率、pinctrl 和 status 三个常见的坑

第一个坑是clock-frequency的取值。I3C 控制器驱动不一定支持你随便写的任意频率,常见支持档位是 100kHz、400kHz、1MHz、6.25MHz、12.5MHz 和 25MHz。如果你写一个 5MHz,驱动可能会就近映射到 6.25MHz 或者直接初始化失败。建议先查 BSP 驱动代码里的时钟分频表,或者用cat /sys/kernel/debug/clk/clk_summary | grep i3c看实际输出频率。

第二个坑是 pinctrl 名字不匹配。RK3576 的.dtsi里可能同时存在i3c0_xfer、i3c0_gpio或者i3c0_sleep等多组 pinctrl 定义。如果没有在板级 dts 里正确引用,内核会照样 probe 但不发数据。

第三个坑是 status 开关。如果硬件上传感器已经连接到 I3C 引脚,但板级 dts 默认把这一路配置成了 I2C 并挂载了某个触摸屏节点,你需要先把原来的 I2C 节点整体关掉或改到别的总线,否则 I3C 节点 probe 后引脚冲突,总线完全无法通信。

4. 总线上的兼容性陷阱:I3C 控制器挂老 I2C 器件

I3C 设计时考虑了向后兼容,理论上可以在同一根总线上同时挂原生 I3C 器件和传统 I2C 器件。但“理论兼容”和“实际稳定跑起来”是两回事,尤其是遇到便宜的小屏幕模块时。

4.1 开漏和推挽的物理差异会直接影响老器件

传统 I2C 器件是为开漏总线设计的,输入端的施密特触发器、内部钳位和毛刺滤波都针对缓慢上升沿做了优化。I3C 切换到推挽驱动后,信号边沿非常陡,对于老器件来说等于输入信号变成了一个高频噪声源。最典型的情况是:正常 I2C 上能稳定工作的 0.9 寸 OLED(内部基本是 SSD1306 这类老控制芯片),挂到 I3C 总线上之后,要么 ACK 不稳定,要么屏幕初始化到一半就花屏。

这不是总线坏了,而是你让一个只认 I2C 400kHz 时序的芯片去面对 12.5MHz 的高速推挽信号。解决方法是把这条总线的clock-frequency调到 400kHz,同时让控制器以兼容模式运行。有些 BSP 有私有属性,比如rockchip,i2c-fallback-mode,可以强制主控只用开漏兼容时序;如果没有这个属性,就把这个 OLED 放到单独的一路 I2C 上,别和 I3C 传感器混在一起。

4.2 I3C 总线里混入多个老 I2C 器件时要小心地址空间

I3C 原生设备使用动态地址,通常在 0x08 到 0x7f 之间分配,而传统 I2C 设备占的是静态地址。如果总线上同时挂了多个使用固定地址的老 I2C 器件,它们和 I3C 动态地址之间可能有重叠风险。控制器在 DAA 枚举时会尽量避开静态地址,但不同厂商实现不一样,老器件越多,越容易出现设备地址被分走导致通信断掉的情况。

我实际体验是:一条 I3C 总线上挂两个原生 I3C 传感器再加一个老 EEPROM,基本没问题;挂三个以上老 I2C 设备再加两个原生 I3C 设备,板级调测时就很容易出现偶发 NACK。结论是不要为了省一路 I2C 把所有东西都塞进 I3C 总线,混合总线适合“少量老器件+若干新器件”的场景。

4.3 上拉电阻要重新算,不能沿用 I2C 的 4.7k 习惯

I2C 设计里上拉电阻的选择受最小灌电流和最大上升时间双重约束,社区里最常见的取值是 4.7k。I3C 主要靠推挽驱动,开漏只用来处理启动、停止、ACK 和仲裁阶段,所以上拉电阻不再是决定沿速率的关键。

但总线上如果还有老 I2C 器件,上拉电阻又必须保留。我目前在 RK3576 的 I3C 混合总线上用的是 2.2k 上拉到 3.3V,配合 12.5MHz 时钟跑原生设备,同时用 400kHz 模式驱动一个老 EEPROM,实测波形没有明显振铃。如果你的板子电容较大,或者线材很长,建议压到 1k 到 2.2k 之间,并尽量缩短跳线,因为 I3C 的高速边沿对寄生电容非常敏感。

4.4 休眠唤醒后遇到总线被拉低,试试九拍恢复

我在调试中遇到过一个很经典的问题:系统 suspend 再唤醒后,I3C 总线的 SDA 一直保持低电平,导致后续所有读写都卡死。原因是传感器在低功耗模式下没有释放总线,而 I3C 控制器的自动恢复机制对这个场景不起作用。老练的做法是手动用 GPIO 模拟 SCL 时钟,在 SDA 释放的间隙连续送出 9 个脉冲,把从机状态机强行复位,然后再恢复正常通信。

如果你的板子上传感器预留了复位 GPIO,最好在驱动 resume 回调里先复位传感器,再重新初始化 I3C 动态地址和寄存器配置。否则每次休眠唤醒都可能随机复现总线锁死,而且不是每次都能抓到日志。

5. 实测验证与调试手段

配置改完之后,不能只看i2cdetect里有没有设备就收工。I3C 的很多问题要等到连续读取数据时才暴露,所以验证环节要上点手段。

5.1 逻辑分析仪和示波器采样率不能太低

I3C SDR 12.5MHz 意味着一个 bit 只有 80ns,逻辑分析仪采样率至少要达到 50MSPS 才能看清眼图。如果你只有 24MHz 采样率的入门逻辑分析仪,看起来波形会是一堆锯齿,甚至会把 I3C 信号误判成 I2C 的毛刺。条件允许的话用示波器看 SCL 和 SDA 的上升沿,探头要尽量靠近器件引脚,别用几十厘米的杜邦线去接,不然测到的全是反射振铃。

另外,大部分逻辑分析仪的软件能自动识别 I2C 解码,但对 I3C 的 CCC 命令和动态地址分配认识有限。我实测过,I3C 的地址阶段在波形上和 I2C 很像,逻辑分析仪能勉强解码出设备地址,但数据阶段的奇偶校验位会被当成数据。如果软件里没有 I3C 解码器,重点是看 ACK、NACK 的位置和 SCL 频率是否接近配置值,别过分依赖解出来的数据值。

5.2 用 sysfs 和 i3ctools 查看动态地址分配结果

RK3576 的内核如果开启了 I3C 子系统,可以在/sys/bus/i3c/devices/下看到总线上的设备列表。传统 I2C 器件会以类似0-legacy@50的形式出现,原生 I3C 设备会有一个动态地址编号。多试几次就会发现,原生设备的动态地址不一定和 DTS 里写的一致,这说明 DAA 流程确实接管了地址分配。

拿到动态地址后,可以用带 i3ctools 的 BSP 直接读写设备寄存器,或者在内核驱动里通过i3c_device_do_priv_xfers发起私有传输。如果你的 BSP 没有这个工具,就老老实实看驱动日志,用dev_err把每次读写的地址、长度和返回状态打印出来,定位问题更快。

我在调 IMU 时发现,DAA 分配的动态地址每次冷启动都可能变化,所以 DTS 里千万别把某个硬件寄存器地址写死成动态地址。需要固定地址的话,就用assigned-address属性预设,并在驱动里确认控制器是否真的采用了预设值。

5.3 一组留档的吞吐对比数

我这块 RK3576 板上,I2C 400kHz 读 64 字节 FIFO 的整包时间大约 1.5ms,I3C SDR 12.5MHz 跑同样的读取操作大约 118us,差距 13 倍左右。只读 2 字节芯片 ID 时,I2C 大约是 125us,I3C 是 45us,差距只有 2.7 倍。这里测量的都是设备枚举完成后的稳定通信阶段,不包含开机时的 DAA 过程。

这个数据说明两点:第一,I3C 的优势在批量数据读取上非常明显,IMU 连续读 FIFO、气压计读传感器数据和 EEPROM 批量读写都能吃到红利;第二,小数据量的寄存器读写,优化收益有限,甚至不如直接优化驱动里每次 access 的次数更有效。

6. 我的结论与选型偏好

抛开 RK3576 本身不谈,I3C 是不是值得迁移,答案完全取决于你总线上挂的是什么。我的建议是:如果项目里传感器数量多、有大量 FIFO 批量读取需求、并且驱动支持原生 I3C 客户端,那值得迁移,收益会非常直接。如果只是挂一块 0.9 寸 OLED、一颗温湿度传感器,I2C 400kHz 完全够用,强行切到 I3C 只会增加 DTS 排查难度和兼容性风险。

场景我的建议
0.9 寸 OLED 等老屏显设备保留 I2C,不要混进 I3C 总线
IMU / 气压计 / 触控等多传感器优先迁移到 I3C,尤其在批量读 FIFO 场景
需要省 GPIO 中断引脚尝试使用 I3C IBI,但要先确认驱动支持
大批量 EEPROM 读写I3C 提升明显,建议评估
老项目升级,驱动已锁死 I2C不必强迁,继续 I2C 更稳

如果你也在 RK3576 上做 I3C 迁移,建议保留一路 I2C 给调试屏幕或者低速设备,另一路 I3C 专门给传感器。这样既能看到性能提升,又不会因为一条混合总线上的偶发兼容问题拖住整个项目。我在实际调板中的体会是:I3C 的收益是真实的,但代价是 DTS、驱动和硬件设计三方面都要跟上,而不是简单把i2c关键字换成i3c就万事大吉。

最后再分享一个小技巧:遇到 I3C 总线上设备枚举不稳定,先别急着怀疑硬件,翻一下 BSP 内核的驱动源码,看看clock-frequency被分频后实际是多少。我调试过的案例里,有一半以上的“不稳定”都源于时钟树里父时钟频率不对,导致 I3C 实际跑在了一个非标准速率上。用clk_summary确认时钟,再调assigned-clock-rates把它拉回标准档位,很多玄学问题就自然消失了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询