这次的 N−1 并不是在所有外部触发场景下都会出现。无 ROI、保持全幅采集时,测试基本正常;设置或清除 ROI、重新配置采集链路后,才比较容易复现:外部触发第 N 次,后端拿到的却是第 N−1 张图。
初期排查过浏览器缓存、MJPEG 预览、GStreamer 缓存队列,也怀疑过 IMX296 的曝光脉冲是否晚一次触发生效。清理队列、重启采集链路、修改缓存响应头都能影响表面现象,但没有稳定消除 N−1。
后续通过逐层对齐帧序号确认:ROI 变化是主要复现条件,但问题并不是简单的裁剪坐标错误。真正发生帧身份错位的位置,是 ROI 变化之后 RKISP 提交图像缓冲区的时机。
系统背景与问题现象
硬件是 RK3576(泰山派内核环境)加 Sony IMX296,使用 Rockchip 的 RKISP V39。IMX296 工作在 Fast Trigger Mode,外部控制器通过 XTRIG 输入触发拍照。
根据 Sony《IMX296LQR-C Datasheet》第 59、64 页,IMX296 使用全局快门;在 Fast Trigger Mode 下,曝光会在 XTRIG 下降沿立即开始,而且该模式只支持 Master mode,也就是由传感器提供图像读出的主同步时序。外部 XTRIG 低电平脉冲同时决定本次曝光时间。
实际数据路径是:
XTRIG
→ IMX296
→ RKCIF
→ RKISP V39
→ V4L2 buffer
→ GStreamer appsink
→ 业务、预览和存图
这几层可以简单理解为:RKCIF 负责接收传感器送来的原始图像,RKISP 负责图像处理并判断一帧何时结束,V4L2 buffer 是 Linux 内核交给应用的一块帧内存,GStreamer 再从这里取图。只要 RKISP 过早或过晚地宣布 buffer 完成,上面的业务层都会跟着拿错。
我们希望它严格满足“一枪一帧”:第 N 次 XTRIG 触发后,业务拿到第 N 张图。全幅采集时基本如此;但 ROI 设置或恢复全幅、链路重启以后,第 N 次触发可能拿到第 N−1 张图。
这里有个很重要的区别:如果 frame N 只是晚 50ms 到达,那是延迟;如果当前 trigger 拿到上一枪的图,那是帧身份错位。两者看起来都叫“慢一帧”,修法完全不同。
先排除曝光参数生效时序
早期排查时,通过交替修改曝光让画面一亮一暗。观察结果的亮暗相位落后一次触发,因此一度认为 IMX296 Fast Trigger 的曝光脉冲要到下一帧才生效。
后续实验说明,这个判断并不成立。
问题在于,亮暗交替只能说明最终看到的画面相位落后,却分不清下面两种情况:
情况 A:曝光参数确实晚一枪生效
情况 B:曝光已经生效,但后面交付的是上一张 buffer
后面我们不再用相机参数给帧做标记,而是在完成一次 ROI 设置或清除之后,固定曝光、增益、ROI(只读取图像中的指定区域)和 XTRIG 脉宽,只改变外部场景,例如切换光源或遮挡。与此同时,把外部触发编号、CIF/ISP 事件、V4L2 缓冲区序号、GStreamer 接收顺序、业务层采集序号和最终图片内容串起来看。
实验结果是:在 ROI 变化后的异常状态中,相邻两次触发完全不修改曝光和增益,N−1 仍然存在;而修改曝光或增益后的第一张图,又可以正确体现新参数。作为对照,无 ROI 的全幅采集没有稳定复现同样问题。
所以不能再把“曝光脉冲下一帧生效”当作最终根因。真正的问题是,内核晚了一次触发才把上一张图交给应用。
定位 RKISP 的帧完成时机
继续检查内核侧事件后,问题范围收敛到 RKISP V39 的帧完成时机。
通常情况下,ISP 会在一帧处理结束时产生 frame-end,也就是“本帧结束”信号,驱动随后把对应的 V4L2 图像缓冲区交给应用。
在这次 ROI 变化后出现异常的 Fast Trigger 状态中,常规 frame-end 可能要等下一次 XTRIG 到来、流水线再次向前推进时才出现。如果驱动一直等它,当前这张图就会被压到下一枪才交付。这里说的是本次平台和复现条件,并不表示 IMX296 Fast Trigger 在无 ROI 时一定会慢一帧。
第 N 次触发
→ 当前图像进入 ISP
→ 当前图像缓冲区没有及时完成
第 N+1 次触发
→ 常规帧结束信号到达
→ 第 N 次触发的图像才交给应用
这可以解释 ROI 后出现的 N−1:不是 GStreamer 取错,也不是前端显示错,而是图像在进入应用之前就已经晚了一次触发。
为了更早知道当前帧处理到了哪里,RKISP 还可以使用“输出行计数”。它类似一个进度条:ISP 每输出一行,计数就向前走;计数达到指定值时,可以产生 line IRQ,也就是行中断,通知驱动执行处理。
在这次 1080 行输出的实验里,1079 是能够产生所需行中断的最后一个计数位置,1080 是硬件计数终值。这两个数字不是 ROI 坐标,也不是什么神秘的传感器参数,它们只是我们用来选择“何时把这一帧交给应用”的两个相邻时间点。
为了不再等待下一次触发,我们尝试在计数到 1079、收到行中断时,提前把当前图像缓冲区标记为完成。修改后,整帧 N−1 消失,当前触发可以拿到当前图像。
与此同时出现了另一个现象:图像主体属于当前帧,但底部大约 8 行仍然是上一帧的内容。
这说明计数到 1079 只能证明 ISP 已经处理到接近最后一行,并不能证明 DMA 已把尾部数据全部写回内存。DMA 可以简单理解为硬件直接向帧内存搬运图像数据。此时立刻把图像交给应用,还是早了一点。
接着把阈值改成硬件终值 1080。底部旧行确实没了,说明像素数据已经完整写入;但 1080 不产生所需的行中断,驱动只能重新等待常规 frame-end,整帧 N−1 随即恢复。
这组对照将问题范围进一步限定在图像缓冲区的完成边界:
| 设置 | 结果 |
|---|---|
| 等常规帧结束信号 | 图像完整,但下一枪才发布,出现 N−1 |
| 计数到 1079 后立即交付 | 当前枪及时发布,但尾部约 8 行还没写完 |
| 等到计数终值 1080 | 尾部完整,但没有行中断,N−1 恢复 |
| 1079 行中断后再等尾部写完 | 当前枪及时发布,尾部也完整 |
浏览器缓存、JPEG、单纯的 ROI 裁剪坐标错误和曝光相位都无法解释这组现象。ROI 负责触发问题,而 1079/1080 对照说明:“RKISP 可以提交当前帧”和“DMA 已经写完全部像素”并不是同一个时刻。
最终修复:提交前等待尾部写入完成
最终方案仍然使用 1079 行中断,因为它能保证当前图像不会拖到下一枪;区别是在把图像交给应用之前,再给尾部数据留出写入时间。
对等待时间进行了逐档测试:
- 10ms:底部仍有旧行;
- 20ms:底部仍有旧行;
- 30ms:本轮测试可以通过,但接近临界值;
- 50ms:多种场景下稳定通过。
最终产品值采用 50ms。它会增加固定的提交延迟,但图像身份正确,也不会再混入上一帧的尾部。
50ms 只是 RK3576、RKISP V39、IMX296 和当前实现组合上的实测安全值。换分辨率、换 ISP 版本或换平台,都应该重新测,不能照抄。
长期方案应当寻找 ISP/DMA 的真实完成状态或硬件完成中断,用硬件完成条件代替固定等待。当前的 50ms 是经过实测的工程方案。
可复用的排查方法
遇到类似的“慢一帧”问题,可以先做两件事。
第一,不要先急着清队列或丢弃第一帧,而是给每一层增加可对齐的身份:外部触发编号、ISP 和 V4L2 缓冲区序号、GStreamer 接收顺序、业务层采集序号,以及最终图像的内容标记。找到第一个从 N 变成 N−1 的位置,后续排查范围会明显缩小。
第二,将相机参数与帧内容标记分开。曝光亮暗交替适合判断参数是否生效,但不能单独判断图像属于哪一次触发。验证帧身份时,应固定相机参数,只改变外部场景。
最终验收也不能只看某一次测试通过。无 ROI 的全幅采集要作为正常基线;然后分别验证设置 ROI、清除 ROI、ROI 后的第一枪和连续多枪,以及曝光或增益修改后的第一枪,同时检查 N−1、漏帧、重复帧和底部旧行。
这次 N−1 主要由 ROI 变化和链路重建触发,但不能因此只检查 ROI 参数。排查的关键是找到 ROI 之后第一次发生帧身份错位的层,并确认内核在什么时刻把图像标记为完成。