RK3576平台I3C总线实战:从I2C加速到DTS配置与驱动调试

最近两年在 RK3576 这类新平台的板子上,经常会遇到一个情况:以前用 I2C 挂得好好的传感器,BSP 里默认给你拉成了 I3C 控制器,驱动和 DTS 配置的方式也跟着变了。一开始我也觉得,不就是个串行总线嘛,I2C 也没慢到哪里去。直到认真把 I3C 规范和 RK3576 的 I3C 控制器撸了一遍,才发现这个接口确实是来取代 I2C 的,而且“快 10 倍”这个说法不但不夸张,拿 I2C 标准模式来比的话,它甚至快了 100 倍不止。

这篇文章我就以 RK3576 为例,把 I3C 到底快在哪里、和 I2C 的本质区别是什么、硬件设计上要注意什么、板级 DTS 怎么配、内核驱动怎么跑通,以及我在调试过程中踩过的几个坑,完整地串一遍。适合正在做 RK3576 平台 BSP、Linux 驱动,或者在传感器、AIoT 设备上遇到 I3C 地址分配和时序问题的同学参考,看完可以直接照着配、照着查。

1. I3C 比 I2C 快 10 倍?先把账算清楚

1.1 标准速率对照

“快 10 倍”这个数字不是营销话术,但要看跟谁比。I2C 从 1982 年诞生到现在,物理层协议几乎没有变过,只是通过模式扩展把时钟频率往上提。而 I3C(Improved Inter-Integrated Circuit)是 MIPI 联盟在 2017 年左右推出的新定义,向下兼容 I2C,但物理层换成了推挽驱动,速率上限彻底拉开。先看一张最直接的对比表:

总线模式标准名称标称时钟频率大致吞吐
I2C Standard Mode标准模式100 kHz100 Kbps
I2C Fast Mode快速模式400 kHz400 Kbps
I2C Fast Mode Plus快速模式+1 MHz1 Mbps
I2C High Speed Mode高速模式3.4 MHz3.4 Mbps
I3C SDR单倍数据率12.5 MHz12.5 Mbps
I3C HDR-DDR双倍数据率12.5 MHz 基频约 25 Mbps

能看到,I3C 的 SDR 模式标称 12.5 Mbps。拿它跟 I2C 最常见的 400 kHz 快速模式比,快了大约 31 倍;跟 1 MHz 的快速模式+比,正好是 12.5 倍。所以“快 10 倍”这个说法,其实是对标了 I2C 的 Fast Mode Plus 取整说的,属于保守估计。

但这里有个容易被忽略的前提:12.5 MHz 是规范给定的大气层物理上限,不是每块板子都能跑到的实际值。我见过不少 RK3576 板子,I3C 总线上挂了两个传感器加一个触摸屏,走线超过 5 厘米、电容负载一上,12.5 MHz 直接超时或者 CRC 报错。实际调试时把i3c-scl-hz配置成 12.5 MHz 能通过的板子,线短、负载轻、PCB 阻抗可控,属于比较理想的情况;大多数量产方案跑到 5 MHz 或者 10 MHz 就足够稳了。

1.2 为什么 I2C 注定跑不快

I2C 速度上不去,根源在于开漏输出加外部上拉电阻的结构。开漏的意思就是设备只能把总线拉低,不能主动拉高,高电平完全靠外部上拉电阻把线上电容充起来。充放电是一个 RC 过程,阻值和容值越大,上升沿越慢。总线电容超过 400 pF 以后,高速模式基本就是摆设。I3C 的做法简单粗暴:物理层改成推挽驱动,主设备在 SDR 模式下主动把 SCL 和 SDA 拉高拉低,不再依赖上拉电阻充电。上升沿陡了,速度自然就上去了。这也是为什么 I3C 总线在设计合理的 PCB 上能跑到十几 MHz 的根本原因。

除了速度,I2C 还有几个没法回避的痛点:7 bit 地址空间小,同型号传感器一多了就撞地址;中断引脚是独立的,一个传感器一根线,多个传感器就把 SoC 的 GPIO 吃光了;总线上的设备不支持热加入,上电前就得规划好地址。这些问题在手机、平板这种传感器数量爆炸的平台上早就绷不住了,I3C 就是为解决这些问题被推出来的,地址动态分配、带内中断、热加入这些能力,我会在下一节展开讲。

2. I3C 不只是快,机制上做了哪些改变

2.1 动态地址分配(DAA)怎么解决地址冲突

I2C 时代,如果板子上有两颗相同型号的传感器,它们的 7 bit 从机地址是一样的,只能通过另外一路 I2C 总线或者 En 使能脚来区分。I3C 直接把这个问题解决了,机制叫动态地址分配(Dynamic Address Assignment,DAA)。

I3C 控制器上电后,会发送一个ENTDAA(Enter Dynamic Address Assignment)公共命令。总线上所有尚未分配地址的 I3C 目标设备,会通过 SDA 线上逐 bit 仲裁的方式,把自己唯一的 Provisional ID 发出来。这套仲裁逻辑有点像 CAN 总线,多个设备同时发送时,按位比较,优先级高的先被识别出来。控制器识别到一个设备后,给它分配一个当前空闲的 7 bit 动态地址,然后继续仲裁下一个设备,直到总线上的所有 I3C 设备都拿到地址。

对驱动开发者来说,这个机制的直观好处是:设备树里 I3C 设备的 reg 地址填 0 就行,表示“我没有固定地址,等控制器动态分配”。内核 I3C 框架会在总线初始化阶段自动完成 DAA,给每个设备分配的地址会写到设备的 info 结构体里,你在驱动里通过i3c_device_get_info()就能查到实际广播地址。如果你拿到的是带静态地址的 I3C 设备,也可以在 reg 里直接填静态地址值,控制器会优先尝试用静态地址,分配失败再走 DAA。

2.2 带内中断(IBI)和热加入

I2C 时代传感器需要中断通知主控时,只能拉一根独立的 GPIO 中断线。传感器一多,SoC 的引脚资源就紧张。I3C 提供带内中断(In-Band Interrupt,IBI),目标设备可以在总线上主动触发一个中断请求,控制器收到后在当前事务的间隙插入对目标设备的访问,直接读取它的中断状态寄存器,不需要额外引脚。

IBI 的优先级还可以配置,不同类型的中断可以设定不同优先级,控制器按顺序处理。这个特性在实际调板中非常好用,比如一个六轴传感器加一个接近传感器挂同一路 I3C,两个都配置了 IBI 后,释放出来的 GPIO 可以直接用于安卓或者 RTC 等更重要的功能。

热加入(Hot Join)跟 IBI 是配套的,目标设备可以在总线运行中上电,通过一个特殊的时序主动请求加入总线,控制器收到热加入请求后,会为它执行一次 DAA,给它分配动态地址,整个过程不用断电重启总线。这个特性对模块化硬件设计极其友好,比如摄像头模组、扩展板卡这类可热插拔的场景,设备插上即被识别。

2.3 从开漏到推挽的电气差异

I2C 设备是开漏结构,总线空闲时依赖上拉电阻把 SCL/SDA 拉到高电平。I3C 在 SDR 模式下,控制器用推挽输出主动驱动总线,目标设备可以继续用开漏方式应答或者传输。这样一个最大的好处是不需要依赖外部上拉电阻来获得高电平,总线空闲时电平是明确由控制器维持的,不会有 RC 充电拖后腿。

但要注意,I3C 总线上如果同时挂着传统 I2C 设备,上拉电阻依然需要保留。因为传统 I2C 设备不认识 I3C 的推挽时序,它在兼容模式阶段还是按开漏那套逻辑工作。在 RK3576 的板级设计里,常见的做法是:如果总线上只有 I3C 设备,可以不放上拉;如果有 I2C 设备共存,保留上拉但阻值尽量选大一些,比如 4.7 kΩ 到 10 kΩ,避免在上拉电阻上浪费太多功耗,也减少对 I3C 推挽驱动的负担。

3. RK3576 的 I3C 控制器资源与硬件设计注意点

3.1 控制器 IP 与 BSP 里的节点

以 RK3576 的 SDK 为例,I3C 控制器在设备树里通常以i3c0、i3c1这样的标签导出,实际节点 compatible 带snps,dw-i3c-master,也就是 Synopsys 的 DesignWare I3C 控制器 IP。不同版本的 BSP 可能兼容字符串略有差异,有的会带rockchip,rk3576-i3c,但核心驱动路径都在 Linux 内核的drivers/i3c/目录下面,框架是统一的。

I3C 控制器资源跟 I2C 控制器是独立的,但硬件引脚往往会复用同一组 IO。也就是说,同一组 SCL/SDA 引脚,你可以配置成 I2C 功能,也可以配置成 I3C 功能,但不能同时又是 I2C 又是 I3C。这个复用关系在 Rockchip 的 TRM(Technical Reference Manual)里有明确的 pinmux 表格,板级 DTS 里通过pinctrl-0引用对应的引脚组节点。

3.2 引脚复用与上拉电阻选择

在用 RK3576 做板子时,硬件原理图上画的是 I3C,软件 DTS 里也要对应使能 I3C 控制器。以我的经验,比较容易翻车的点是 pinctrl 配置。有些 SDK 的 dtsi 里 I3C 的 pinctrl 节点名叫i3c0_xfer,有些叫i3c0_m0_pins,还有的直接借用i2c0_xfer。这个没有统一规律,最靠谱的办法是在 SDK 的 dtsi 里搜i3c关键字的pinctrl节点,确认实际引脚组,然后照抄到板级 dts。

上拉电阻方面分两种情况。如果这一路总线只挂 I3C 设备,选i3c-scl-hz跑高速时,外部上拉电阻可以去掉,I3C 模式靠推挽输出工作。如果总线上还混挂了 EEPROM、RTC 这类传统 I2C 设备,就必须保留上拉。我之前有一块板子,I3C 挂了一颗六轴传感器,同时被测板上的 EEPROM 也要共用总线,上拉从 2.2 kΩ 改成 10 kΩ 后,I3C 模式才算稳定,虽然 I2C 兼容模式下的 EEPROM 速率差一点,但整体可用性高了很多。

3.3 时钟拉伸和 I2C 从设备的兼容问题

I2C 协议允许从设备通过拉低 SCL 来插入等待周期,叫做时钟拉伸。I3C 控制器在兼容 I2C 设备时同样要支持这个行为,否则挂混在总线上的 I2C 设备在慢速模式下一拉伸,控制器就直接超时了。Linux I3C 框架对时钟拉伸的处理是,在 I2C 兼容传输阶段依然按 I2C 时序工作,目标设备拉伸就等待,不会当作错误处理。但这也意味着,如果一条总线上混挂了很多会频繁拉伸时钟的 I2C 老设备,实际吞吐会被这些慢速设备拖累,总线不可能一直维持 12.5 MHz。板级设计时尽量让高速 I3C 传感器单独走一条总线,把 EEPROM、RTC 这类低速设备放另一条,这也是性能设计上很现实的一条建议。

0.9 寸 OLED、SSD1306 这类常见显示模块,很多不支持 I3C 协议,只有 I2C 接口。把它们挂在 I3C 控制器下是可以工作的,但只能走 I2C 兼容模式,无法参与 I3C 的动态地址分配。如果你在 DTS 里把它当作 I3C 设备配置了 reg = <0>,驱动会一直等 DAA,而它根本不会响应,表现就是 I2C 探测设备失败或者 i2cdetect 只能在某个地址看到设备。正确做法是把它当传统 I2C 设备挂在 I3C master 节点下,reg 填实际 I2C 地址。

4. RK3576 上 I3C 的 DTS 配置实操

4.1 I3C 控制器通用配置

RK3576 BSP 里的 I3C 控制器节点,SoC dtsi 一般已经定义好了寄存器地址、中断号、时钟。我们在板级 dts 里只需要补齐 state、pinctrl、速率属性和挂在下面的设备节点。一个典型的 I3C 控制器配置长这样:

&i3c0 { status = "okay"; pinctrl-names = "default"; pinctrl-0 = <&i3c0_xfer>; /* I3C 模式下总线目标频率,SDR 模式最大 12.5 MHz */ i3c-scl-hz = <12500000>; /* 与 I2C 从设备通信时使用的 I2C 模式频率 */ i2c-scl-hz = <400000>; };

这里有几个关键属性需要解释:

  • i3c-scl-hz:I3C 模式下的 SCL 时钟频率。可以填 12500000(12.5 MHz)拉满,也可以按板子实际负载填 10000000 或 5000000。建议首次调试先用 1 MHz 甚至 400 kHz 起步,等波形确认没问题再往上拉。
  • i2c-scl-hz:控制器切换到 I2C 兼容模式时的频率。这个值跟 I3C 速率没关系,它是给传统 I2C 从设备准备的,按你板上的 I2C 外设规格填,常见的 100000 或 400000 都行。
  • pinctrl-names和pinctrl-0:引用 I3C 引脚复用组,具体名称查 dtsi。

status = "okay"是必须的,默认 SoC dtsi 里 I3C 节点多半是disabled,不定制成 okay,控制器不会使能。

4.2 挂一个 I3C 传感器节点的正确姿势

在 I3C 控制器下添加 I3C 目标设备,和普通 I2C 设备子节点写法是有区别的。I3C 设备的 reg 可以是 0,表示不指定静态地址,由控制器在 DAA 阶段动态分配:

&i3c0 { status = "okay"; pinctrl-names = "default"; pinctrl-0 = <&i3c0_xfer>; i3c-scl-hz = <12500000>; i2c-scl-hz = <400000>; /* I3C 六轴传感器,地址由 DAA 动态分配 */ gyro@0 { compatible = "vendor,lsm6dso-i3c"; reg = <0>; }; };

要注意的是,compatible必须能匹配到内核里注册的i3c_driver,匹配不上设备是不会被挂载的。在驱动侧,这个匹配由i3c_device_id表提供,后面驱动章节会细说。

如果你的 I3C 目标设备自带静态地址,比如某些器件出厂有 0x68 的默认 I2C 地址,那么在 DTS 里可以直接填:

gyro@68 { compatible = "vendor,lsm6dso-i3c"; reg = <0x68>; };

控制器在总线初始化时会优先让设备使用静态地址,不需要 DAA。这个方式在同时保留 I2C 时兼容性更好,因为传统 I2C 主机也能用 0x68 找到它。但要注意,静态地址不能跟同一条总线上其他传统 I2C 设备的地址冲突,否则总线初始化会报地址冲突。

4.3 I2C 传统从设备怎么挂进 I3C 总线

I3C 控制器向下兼容 I2C,意味着传统 I2C 从设备可以直接挂在 I3C 控制器的子节点下,不需要任何中间转换。配置方法就是按普通 I2C 设备节点写,reg 填 I2C 地址:

&i3c1 { status = "okay"; pinctrl-names = "default"; pinctrl-0 = <&i3c1_xfer>; i2c-scl-hz = <400000>; /* 传统 I2C EEPROM,不需要 I3C 能力 */ eeprom@50 { compatible = "atmel,24c02"; reg = <0x50>; pagesize = <8>; }; };

在内核眼里,I3C master 会导出一个 I2C adapter,这个 EEPROM 仍然会被挂在 I2C 子系统的适配器上,原有i2c_driver不需要任何修改就能工作。这也解释了为什么很多只写了 I2C 驱动的传感器,在 I3C 总线上依然能被识别,前提是它支持 I2C 时序且挂在控制器兼容段下。

4.4 用 I2C 兼容模式做最省事的验证

在刚拿到一块新的 RK3576 板子、I3C 驱动还没完全就绪时,有个非常实用的过渡方案:如果硬件上这组引脚既能当 I2C 又能当 I3C,先切换到 I2C 控制器配置,把总线上所有设备当成传统 I2C 设备去探测和验证。这样可以用 i2cdetect、i2cget、i2cset 这些成熟工具确认接线、地址、上拉电阻是否正常。

验证通过以后,再切回 I3C 控制器,把需要参与 DAA 的设备配成 reg = <0>,一步步把速率提上去。这个“先 I2C 打通硬件,再切 I3C 验证协议”的策略,能帮你把硬件问题和协议问题分开排查,节省大量调试时间。

5. 驱动侧:Linux I3C 子系统的使用

5.1 I3C 驱动框架和 I2C 框架的关系

Linux 内核从 4.x 时代就开始合入 I3C 框架,核心代码在/drivers/i3c/目录下。框架分为两层:上层是i3c master core,负责总线初始化、DAA、IBI 处理等总线级事务;下层是具体控制器驱动,比如 DesignWare I3C 驱动。对我们写设备驱动的人来说,接触最多的是struct i3c_driver和它的 probe/remove 接口,跟i2c_driver的写法非常像。

I3C master 控制器在注册时会同时注册一个i2c_adapter。所以传统 I2C 设备在 I3C 总线上依然以i2c_client的形式存在,它们的驱动不需要任何改动。而真正的 I3C 目标设备,则必须以i3c_device的形式挂到 I3C bus 上,对应i3c_driver。

5.2 写一个最简单的 I3C 设备驱动

假设我们在 I3C 0 总线上挂了一个虚构的六轴传感器vendor,lsm6dso-i3c,驱动骨架如下:

#include <linux/i3c/device.h> #include <linux/i3c/master.h> #include <linux/module.h> #include <linux/slab.h> struct my_sensor_data { struct i3c_device *dev; /* 传感器私有数据 */ }; static int my_sensor_probe(struct i3c_device *i3cdev) { struct device *dev = &i3cdev->dev; struct my_sensor_data *data; struct i3c_device_info info; u8 reg = 0x0F; u8 val = 0; struct i3c_priv_xfer xfers[] = { { .data = &reg, .len = 1, .rnw = 0, }, { .data = &val, .len = 1, .rnw = 1, }, }; int ret; data = kzalloc(sizeof(*data), GFP_KERNEL); if (!data) return -ENOMEM; >static void my_sensor_ibi_handler(struct i3c_device *i3cdev, struct i3c_ibi_meta_data *meta) { struct device *dev = &i3cdev->dev; /* 中断到来,通常是状态寄存器有变化,可以主动读一次 */ dev_info(dev, "IBI received!\n"); } static int my_sensor_probe(struct i3c_device *i3cdev) { struct i3c_ibi_setup setup = { .handler = my_sensor_ibi_handler, .max_payload_len = 4, .num_slots = 4, }; int ret; /* ... */ ret = i3c_device_request_ibi(i3cdev, &setup); if (ret) dev_warn(&i3cdev->dev, "request IBI failed\n"); else i3c_device_enable_ibi(i3cdev); /* ... */ }

IBI 的好处是省掉了一根 GPIO 中断线。RK3576 这类 SoC 引脚本来就紧张,传感器、触摸、充电、邮件提醒全都要中断脚,用 IBI 能释放不少资源。

5.4 验证 I3C 是否挂成功的几种方法

驱动 probe 成功不代表 I3C 整条链路就稳了。我喜欢用三种方式交叉验证:

  • 看内核日志:dmesg 里搜i3c、dwc_i3c、dma等关键字,能看到 master 注册和 DAA 分配过程。
  • 查 sysfs:I3C 设备挂在/sys/bus/i3c/devices/下面,通常能看到总线上每个目标的目录,目录里info文件可以看到动态地址。
  • 逻辑分析仪:这是最硬核的验证手段。不过要提醒一句,普通支持 I2C 解码的逻辑分析仪不一定能正确解 I3C 的帧,因为 I3C 帧里带 CCC、DAA、IBI 这些特殊机制。部分分析软件更新了 I3C 解码协议,抓之前记得先确认解码器支持。

6. 调板踩坑记录:RK3576 上 I3C 的那些问题

6.1 DAA 过程一直卡住,传感器不响应

这算是 I3C 调板最经典的问题。现象是 dmesg 里 master 初始化时报DAA timeout或者no device found,总线上明明挂着传感器,但控制器就是识别不出来。

排查思路按概率排序:先是引脚复用没配对,pinctrl 指向的引脚组和原理图不一致,时钟偶合都没对上;再查传感器供电和复位引脚是否正常,很多传感器模组把 RST 脚默认拉低了,导致上电后一直处于复位状态;最后看传感器的 I3C 模式寄存器是否需要手动打开,部分设备默认上电是 I2C 模式,要由控制器发特殊命令切到 I3C 模式才能参与 DAA。这种时候,先用 I2C 兼容模式把设备地址读出来,确认硬件通路完整,再切 I3C 模式做 DAA,能省去大量来回试错。

6.2 I3C 模式下读寄存器全是 0xFF

寄存器读出来的数据全是 0xFF,通常意味着总线通信根本没建立,SDA 上没有设备应答,返回的就是线被拉高的那个值。排查时先降频,把i3c-scl-hz从 12.5 MHz 降到 400 kHz,如果恢复正常,说明是信号完整性问题:PCB 走线太长、过孔太多、或者上拉电阻太小导致推挽驱动电流不够。这种场景下我倾向于在 DTS 里把速率稳定在 5 MHz 左右,优先保证量产可复现性。

另一种可能是目标设备驱动的私有传输帧格式不对,写的寄存器地址长度、读写标志位填错,导致设备没有正确应答。可以先发一个最基础的读 WHOAMI 寄存器,逐个字节确认。

6.3 和传统 I2C 设备共用总线时出现地址探测异常

I3C 控制器挂 I2C 传统设备时,如果 i2cdetect 探测出来的地址和你 DTS 里配置的不一致,或者总是探测不到,先检查是不是 I3C 控制器内部把地址做了二次映射。I3C 的动态地址和 I2C 静态地址是两套逻辑,传统 I2C 设备走的是 I2C 兼容段,地址应该原样保留。如果探测不到,优先怀疑i2c-scl-hz配置过高,传统设备跟不上,把 I2C 兼容模式降速到 100 kHz 再试。I3C 总线控制器总线初始化时会做 DAA,传统 I2C 设备不参与,但初始化过程会占用总线一段时间,慢速 I2C 设备此时不能响应任何命令,所以不要在总线初始化阶段拿 i2cdetect 去并发探测。

6.4 I3C 设备休眠后总线卡死,无法唤醒

这个问题跟 I3C 的动态地址有关。设备休眠后,如果控制器这边没有正确维护 DAA 表,重新唤醒时设备可能已经丢了动态地址,总线通信就卡住了。I3C 规范有RSTACT(Reset Action)这类公共命令,设备可以通过它重新回到已知状态,或者重新执行 DAA。内核 I3C 框架在 PM 回调里会做一部分恢复工作,但如果 BSP 实现不完整,就会出现休眠唤醒后要手动 reset 的情况。

我在实际项目里的处理思路是:设备驱动里添加 runtime PM 回调,suspend 时记录当前状态,resume 时判断是否需要重新执行ENTDAA,如果控制器不支持自动恢复,就驱动主动切一次总线复位流程。这件事没有统一的解法,每个终端设备对RSTACT的响应行为也不同,需要在调试中一次一次验证。

6.5 检查清单速查表

把常见问题的排查要点整理成一张表,方便现场调板对着查:

现象优先检查次级检查
DAA 超时pinctrl 引脚组设备供电/复位、I3C 模式寄存器
读寄存器全 FF频率降级测试私有传输帧格式、上拉电阻
与 I2C 设备冲突I2C 地址映射I2C 兼容模式频率
休眠唤醒卡死PM 回调处理RSTACT 复位流程
分析仪解不出帧解码器版本改用 I2C 模式预验证

最后再分享一个经验:I3C 调试初期,不要一上来就追 12.5 MHz 和 HDR 模式。先把整条链路在 400 kHz 的 I2C 兼容模式下跑通,确保每个设备都能正确寻址和通信,然后切到 I3C SDR 模式 1 MHz 起步验证 DAA,再逐步升频到 5 MHz、10 MHz、12.5 MHz。每次升频都要配合逻辑分析仪确认边沿质量,一步一个脚印。这样一来,遇到问题时你能很清楚地判断是哪个环节出的问题,而不是在一堆不确定因素里面猜。