一次 I2C 时钟门控把整块板锁死了——从现象到根因的完整排查
约 2 分钟 阅读 30给 ZynqMP 的 R5 核写驱动,第一次访问 PS 的 I2C0 控制器,整台板子死了——不是 R5 死,是整板:A53 四核失联、串口冻结、ping 不应,连 JTAG DAP 都一起瘫掉。这篇记录从锁死到结案的完整取证过程。
现场
新固件推进去,R5 正常启动,自检一路 PASS——直到第一个 I2C 传输开始,R5 再没输出过一个字符。控制台、SSH、ICMP 全部失联。
第一反应是 R5 崩了拖累系统?不对。JTAG 读 DDR 里的共享内存环:R5 固件好好的,自检输出停在那条 I2C 用例上,没有 fault 报告。halt R5 和 A53——全部超时。JTAG 复位——目标选择直接挂死。连调试器的路都死了,只能物理断电。
取证
恢复后抓到决定性证据(锁死前一刻的系统状态):
ff020000.i2c的runtime_status=suspended,power/control=auto;- 时钟树里
i2c0_ref enable_count=0——I2C0 的时钟被门控了;对照组 UART0 是 1。
然后做了一次"自杀式"复现:A53 上用 /dev/mem 直接读 I2C0 寄存器(绕开驱动)——整板再次锁死。机制坐实。
根因
一句话:Linux 内核的 cdns-i2c 驱动在运行时 PM 下把空闲的 I2C0 时钟关了;任何绕过驱动的直接访问(R5 固件、A53 /dev/mem)命中一个"没有时钟的 APB 从机",LPD APB 互连永不应答——访问它的 CPU 核死在一条 ldr 指令上,而互连锁死又拖死了整片 LPD 外设和 JTAG DAP。
两个推论值得记住:
- 软件超时救不了你。R5 卡在总线等待里,
ldr永不退休,超时判断代码永远执行不到。所有"先访问再说、错了超时兜底"的写法在这种故障面前全部失效——唯一有效的防护是"确保从机可被访问"。 - 内核自己的
i2cdetect/i2cget反而是安全的:走驱动路径会先 runtime resume 把时钟打开。所以故障只在"裸机核直访"或"/dev/mem"这种绕开驱动的路径上触发——这也是它为什么潜伏期这么难被发现。
修复(三层,纵深防护)
| 层 | 措施 |
|---|---|
| 根治(Linux 侧) | power/control=on,systemd 持久化(keep-i2c0-clock.service),时钟永不门控 |
| 驱动侧 | R5 的 I2C 驱动每次传输前重断言时钟使能位(CRL CLKACT)——防 Linux 中途再关 |
| 代码门禁 | 驱动加"武装"标志:未武装时收发一律拒绝(-EPERM),boot 自动路径在代码层不可能碰到 I2C0;调试命令默认拦截该地址区,要加 force 才放行 |
板验:时钟常开后同一地址连读连过,i2csmoke 5/5(EEPROM 读写回环/页写/RTC/NAK 负路径),boot 自检全绿。
带走的经验
- 时钟门控 × 绕过驱动的直访 = 致命组合。只要系统里有一个核/一条路径绕开 Linux 驱动直接摸硬件,就必须把对应外设的运行时 PM 钉死为常开。
- 互连锁死级的故障,取证窗口只有"锁死前那一刻"——
runtime_status和时钟计数器是我们能抓到的最后现场。 - 防护要分层:根治一层、驱动一层、代码门禁一层。任何一层失守都不至于再回到"物理断电"。