I3C Target发送完成回调不触发?FIFO下溢与中断处理陷阱深度排查 那段时间我在调试日志里反反复复看到一句话I3C target transmit does not trigger a callback upon completion。I3C target 的发送方向数据确实已经送到了总线上对端 controller 也正确收走了逻辑分析仪上波形清清楚楚可软件这边注册的完成回调就是不来。程序卡在等待超时上状态机推不动看起来就像整个系统在装死。这种数据通了事件没通的问题比单纯的不通更难受。如果你现在也正在调 I3C target 模式或者刚把某个带 I3C 控制器的 SoC 跑起来准备做双向通信这篇文章应该能帮你少走弯路。我会把完整的排查过程、根因分析和修复方案都写出来包括一段非常隐蔽的中断处理逻辑问题以及 I3C target 模式下 FIFO 时序为什么比 I2C 苛刻得多。1. 问题现场波形正常、数据正确回调就是不来1.1 I3C target transmit 到底在跑什么先对齐一下背景。I3C 是 MIPI 联盟定义的串行总线协议可以把它理解成 I2C 的高性能升级版保留了两线制 SDA/SCL、多设备挂载同时加入了动态地址分配DAA、带内中断IBI、更高传输速率SDR 模式最高 12.5MHzHDR 模式更高等特性。在这套体系里controller 是总线主控方target 是从设备。所谓target transmit指的是 controller 发起了对某个 target 的私有读操作controller 在总线上产生时钟target 负责把数据放到 SDA 上一个 bit 一个 bit 地送出去。数据方向是从 target 流向 controller。我负责的模块运行在某颗集成了第三方 I3C 控制器 IP 的 SoC 上固件跑在 RTOS 环境里。controller 端是另一个团队用 FPGA 逻辑做的验证平台两边通过 I3C 总线对接。我们要做的是让 target 设备响应 controller 的读请求把一段传感器数据发回去。1.2 复现步骤和表面现象问题复现路径非常稳定Controller 通过 DAA 分配好动态地址之后周期性发起私有读请求。Target 侧驱动收到读请求中断调用发送接口i3c_target_send(buf, len, cb)把数据填进硬件 FIFO。发送接口返回成功。逻辑分析仪看到总线上数据完全正确controller 也正常接收并 ACK。但是cb这个回调函数在设定的超时时间内一次都没有被调用。上层模块超时后上报I3C_ERR_TX_TIMEOUT任务卡死。最开始我以为是接口使用方式的问题比如回调注册错了地方、上下文参数传错或者上层在发送前把回调清掉了。对着代码查了几遍这些常规嫌疑都被排除。函数确实被调用了参数也没问题硬件也把数据发出去了可回调就是静悄悄的。1.3 为什么这个问题比完全不通更难查如果数据根本没出去排查方向会很聚焦查地址、查配置、查中断使能、查波形。麻烦就麻烦在数据已经正确收到了这会让所有先看链路通不通的常规手段全部失效。更隐蔽的是上层状态机依赖这个回调来推进。回调不来就意味着发送方永远等不到这针对端已经收完也不知道该释放 buffer、推进状态。整个软件栈被卡在一个硬件已经成功软件认为失败的撕裂状态里。碰到这种情况我个人的习惯是不猜直接去看硬件的原始现场。2. 第一轮排查寄存器说事件发生了可驱动的 ISR 没消费它2.1 嫌疑一硬件根本没产生完成事件既然是发送完成回调不触发第一步就应该确认硬件到底有没有产生发送完成这个事件。别只看封装好的驱动函数直接读中断状态寄存器。I3C 控制器 IP 虽然各家实现略有差异但基本都会有一组中断状态寄存器命名类似INTR_STATUS/RAW_INTR_STATUS。我在 ISR 入口处加了一行调试打印把触发中断后的原始状态值完整 dump 出来uint32_t status readl(priv-base INTR_STATUS); dev_dbg(priv-dev, ISR status0x%08x\n, status);跑一次复现日志里出现了ISR status0x0000_00A8对照芯片手册拆开这个值bit 7TX_COMPLETE是 1bit 5TX_UNDERRUN也是 1bit 3TX_REQUEST也是 1。第一个结论立刻明确硬件层面发送完成事件确实产生了。事件没消失只是没有被软件消费。注意这里我用的是消费这个词——中断事件就像一封信硬件把信送到邮箱里了但读信的人可能没把它当作需要回应的信。2.2 嫌疑二中断被 MASK 挡在门外事件产生了但有没有可能它压根没有触发 CPU 中断只是状态寄存器里默默挂在那查中断使能寄存器INTR_MASK。大多数 IP 的设计是 mask 对应位写 1 表示允许该事件上报。我读到的INTR_MASK中 TX_COMPLETE 位确实是 1没有被屏蔽。另外如果事件没触发中断ISR 根本不会跑进来。但上面那条ISR status0x...的日志能打印出来就说明 ISR 已经在执行了中断通道是通的。而且bit 3TX_REQUEST置位也符合预期——controller 请求读数据时硬件会先通知 CPU我这边需要发送数据。2.3 嫌疑三ISR 执行了但根本没走对分支既然 ISR 进来了、事件也置位了问题就聚焦到 ISR 内部的判断逻辑上。打开驱动代码我看到了这样的经典结构static irqreturn_t i3c_target_isr(int irq, void *data) { struct i3c_target_priv *priv data; uint32_t status readl(priv-base INTR_STATUS); if (status STATUS_TX_UNDERRUN) { /* 先处理下溢错误 */ writel(CTRL_TX_ABORT, priv-base CTRL); i3c_target_abort(priv); return IRQ_HANDLED; /* 注意这里直接 return 了 */ } if (status STATUS_TX_COMPLETE) { writel(status STATUS_TX_COMPLETE, priv-base INTR_STATUS); if (priv-tx_busy) { priv-tx_busy false; if (priv-tx_cb) priv-tx_cb(priv-tx_ctx, 0); } return IRQ_HANDLED; } if (status STATUS_TX_REQUEST) { i3c_target_refill_fifo(priv); } return IRQ_HANDLED; }这段代码的逻辑是错误优先一旦发现TX_UNDERRUN立刻 abort 并 return。如果同一时刻还有TX_COMPLETE事件挂在那TX_COMPLETE分支完全没有机会执行。看到这里问题基本定位了一半。但还有一半的问题没有解决为什么会有TX_UNDERRUN如果这是一次正常的发送FIFO 里有数据controller 时钟过来数据一个个发出去最后 controller 发 STOP 结束硬件应该只置TX_COMPLETE而不是同时出现下溢错误。除非TX_UNDERRUN的出现不是偶发而是必然。既然状态值里同时出现了TX_COMPLETE和TX_UNDERRUN这两件事很可能在一个传输周期内同时发生。这也就意味着target 在发送过程中FIFO 曾经空过导致控制器那边看到了不完整的数据时序最终以异常方式结束了这笔传输。但数据在逻辑分析仪上看起来正确说明异常只发生在最后若干 bit 或者 STOP 阶段没有破坏主要数据内容。这个推测如果成立那么TX_COMPLETE到底代表什么不同厂商 IP 对完成的定义不一样。有些 IP 认为只要 FIFO 里的数据被硬件消费完就算发送完成会置TX_COMPLETE有些则认为要检测到总线上的 STOP 条件才算完成。如果 IP 在 FIFO 被硬件取空时就置位TX_COMPLETE但此时 controller 还在继续产生时钟而 FIFO 已空硬件可能同时报TX_UNDERRUN这两个事件就在同一时刻叠加了。我翻了一下手里的 IP 手册上面明确写着TX_COMPLETE会在发送 FIFO 中最后一个字节被移出时置位如果 FIFO 空而 controller 仍在传输硬件会产生TX_UNDERRUN错误事件并视总线状态决定是否等待 STOP。也就是说在数据刚好发完、controller 还没发 STOP的那个窗口里两个事件叠加是完全可能发生的。那为什么 FIFO 会空这才是真正的幕后元凶。3. 真正的元凶FIFO 下溢和那条过早的 return3.1 I3C target 发送与 I2C 的本质区别不能拉伸时钟要理解 FIFO 为什么会空必须回到 I3C 和 I2C 的一个本质差异上。I2C 的 target 在来不及准备数据时可以通过拉低 SCL时钟拉伸让 controller 停下来等自己。这是 I2C 协议里的一个保护机制很多工程师都习以为常。但 I3C 协议里clock stretching 被严格限制甚至直接不适用——时钟始终由 controller 产生target 没有资格让总线时钟停下来。这意味着什么意味着当 controller 发起读请求后target 硬件必须保证在 controller 下一个时钟沿到来时SDA 上已经有正确的电平。CPU 侧没法通过慢一点来争取时间只能在硬件需要数据之前就提前把 FIFO 填满。对照到我们的硬件TX FIFO 深度只有 16 字节。在 SDR 12.5MHz 速率下一个 bit 周期约 80ns。每个字节实际传输需要 9 个 bit8 位数据 1 位奇偶校验 T 位合起来大约 0.72us。如果 FIFO 里已经预存了 16 字节数据硬件可以连续发送约 11.5us 而不需要 CPU 介入。11.5us 听起来不短但在 RTOS 环境里一个高优先级中断的响应时间加上关临界区保护很容易超过这个窗口。更要命的是如果驱动收到读请求中断后才开始往 FIFO 写而此前 FIFO 是空的那留给软件的时间窗口是硬件检测到地址匹配到第一个数据 bit 送出之间的那点时间可能只有几个 us甚至更短。在这种条件下凡是经过 RTOS 调度、锁保护、打印日志等路径的代码都可能在第一个字节上就翻车。所以答案逐渐浮出水面不是回调没事件可用而是发送请求到来得很快FIFO 却没有提前准备硬件发一会儿就空了产生了TX_UNDERRUN随后驱动在错误分支里直接 return把本应执行的完成分支跳过了。3.2 中断处理里的逻辑缺陷一个 return 吞掉了一个事件回到 ISR 代码那个 return 真的太伤了硬件在发送过程中发现 FIFO 空了置TX_UNDERRUN。几乎同时控制器还在继续读数据最终发送了 STOP。硬件检测到 STOP置位TX_COMPLETE。ISR 被触发读取 status看到TX_UNDERRUN和TX_COMPLETE同时为 1。代码先检查TX_UNDERRUN进入 error 分支调用 abort 后直接 return。TX_COMPLETE事件就这样被留在状态寄存器里没人处理回调自然不可能被调用。很多同学会问TX_UNDERRUN真的应该在正常传输路径里出现吗按理说如果 target 有足够的数据就不该出现。但在真实系统里FIFO 深度是有限的CPU 响应时间是不确定的硬件对总线事件的判定又有自己的时钟粒度所以正常 异常同时发生并不是天方夜谭恰恰是嵌入式系统里最常见的 bug 温床。3.3 用数据算一下时序窗口为了验证FIFO 空窗理论我抓了一组实际时序数据I3C SDR 速率12.5MHzbit 周期 80ns一个数据字节含 T 位约 0.72usTX FIFO 深度16 字节从 FIFO 被消耗到完全变空的极限时间11.5usCPU 从收到中断到完成写入 16 字节的实测时间在关中断的情况下约 1.2us在 RTOS 环境下如果中间有临界区、任务切换实测可以达到 8~15us注意最后一行当 CPU 响应延迟超过 11.5usFIFO 就一定会下溢。我甚至在日志里看到过TX_UNDERRUN和TX_COMPLETE同时出现而那时 controller 还在等待最后一个字节。把这条链路连起来看整个问题就完全清楚了。硬件层面的发送完成事件和发送错误事件同时发生而驱动代码用了一个错误分支优先 直接 return的策略把正常完成回调永久丢弃了。上层一直在等回调可回调所在的分支连执行机会都没有。4. 修复落地重构中断处理路径与 FIFO 填充策略4.1 中断处理代码错误分支不能吞掉完成分支修复的核心思路是中断处理里不管遇到什么错误都不应该直接 return 而让其他 pending 事件失去被处理的机会。事件产生以后该消费的必须消费该通知上层的必须通知哪怕通知的内容是这次出错了。我重写了 ISR把错误的处理从提前 return改成先记录错误再继续检查其他事件最后统一执行恢复动作static irqreturn_t i3