工业相机设置 ROI 后外部触发慢一帧(N−1):RKISP 帧完成边界与未决问题

先说结论:这次排查已经找到一个可以稳定解释并规避整帧 N−1 的内核边界,但还不能说问题已经从硬件机制上根治。

已确认的是,在本次 RK3576、Sony IMX296 和 RKISP V39 的组合上,设置或清除 ROI、重新配置采集链路以后,当前帧对应的 V4L2 buffer 没有在当前 XTRIG 周期内完成。下一次外部触发到来后,上一张 buffer 才交给用户态,于是形成稳定的 N−1。

当前内核方案在 RKISP 输出行计数到 1079 时启动提前完成流程,并额外等待 50ms,再把 buffer 交给应用。这个方案通过了当时的多场景测试,消除了整帧错位和底部旧行,但它本质上仍是根据现象选出的安全窗口:驱动没有读取一个能够明确表示“ISP 和 DMA 已经全部完成”的硬件状态。

因此,本文不再把“1079 行中断加 50ms”称为最终根治,而是把问题拆成三层:已经证实的 buffer 发布错误、已经验证有效的规避方案,以及仍然缺少硬件证据的物理完成条件。

系统背景:ROI 之后才容易出现的 N−1

硬件平台使用 RK3576,图像传感器是 Sony IMX296 全局快门传感器,图像经 RKCIF 进入 RKISP V39,再通过 V4L2 和 GStreamer 交给应用。

IMX296 工作在 Fast Trigger Mode。外部控制器向 XTRIG 输入一个低电平脉冲,传感器在 XTRIG 下降沿开始曝光,脉冲宽度决定本次曝光时间。根据 IMX296 datasheet,该模式只支持 Master mode,模式切换还需要经过 sensor standby。

数据链路可以简化为:

XTRIG
  → IMX296 曝光与读出
  → MIPI CSI-2
  → RKCIF 接收
  → RKISP V39 处理与写内存
  → V4L2 buffer 完成
  → GStreamer appsink
  → 应用取得图像

无 ROI、保持全幅采集时,测试基本正常。问题主要出现在设置 ROI、清除 ROI 或由此引起采集链路重启以后:

触发 1 → 没有图像,或者仍是进入触发模式前的图像
触发 2 → 收到触发 1 的图像
触发 3 → 收到触发 2 的图像

这不是普通的固定延迟。如果触发 N 的图像身份正确,只是晚 50ms 到达,那仍然是 N 对 N;N−1 则是触发与图像身份发生了整体错位。若下一次 XTRIG 不来,当前图像甚至可能一直留在内核中。

ROI 是主要复现条件,但这并不等于 ROI 坐标本身写错。ROI 会同时改变传感器读出区域、输出高度、帧时序,并经常伴随 stream off/on 和 Fast Trigger 模式恢复。它更像是把某个帧完成边界问题暴露出来,而不是直接说明裁剪结果错误。

为什么最初会误判成曝光晚一枪生效

早期测试通过交替修改曝光,让画面一亮一暗,再观察输出的亮暗顺序。结果看上去总是落后一枪,于是一度认为:IMX296 Fast Trigger 的曝光脉冲要到下一帧才生效。

这种实验只能看到最终图像的相位,却区分不了两种情况:

情况 A:曝光参数确实晚一次 XTRIG 生效
情况 B:曝光已经用于当前枪,但用户态拿到的是上一张 buffer

后续使用固定曝光、固定增益、固定 XTRIG 脉宽,只改变外部场景内容进行标记。结果表明:相邻触发之间不修改任何相机参数,ROI 后仍然可能出现 N−1;而修改曝光或增益后的首枪又可以正确体现新参数。

这推翻了“曝光参数必然下一帧生效”作为最终根因的判断。更可靠的原则是:

验证帧身份时,不要用正在被验证的相机参数给帧做标记。固定相机参数,使用外部光源、遮挡片、数字标牌或其他独立场景变化标记每一枪。

从 V4L2 buffer 生命周期理解“为什么是上一帧”

V4L2 不会因为内存中已经出现了像素就自动把图像交给应用。驱动必须明确完成当前 buffer,随后用户态的 dequeue、GStreamer appsink 和业务回调才有机会拿到它。

在 RKISP V39 的采集路径中,可以把 buffer 队列简化成当前 buffer、下一 buffer 和等待队列。正常情况下,一帧结束后驱动完成当前 buffer,再让下一块 buffer 接替它。

本次异常状态的时序更接近下面这样:

XTRIG N
  → 图像 N 进入 RKCIF 和 RKISP
  → 像素开始写入当前 buffer
  → 当前周期内没有出现可用于完成它的正常 frame-end
  → buffer N 仍由驱动持有

XTRIG N+1
  → 流水线再次推进
  → 上一周期缺失的完成条件出现
  → 驱动完成并发布 buffer N
  → 此时应用正在等待“触发 N+1 的结果”

所以应用不是从队列中随机拿错一张图,而是内核直到下一枪才公布上一枪。清空 GStreamer queue、关闭浏览器缓存或重启 JPEG 编码器都无法改变这个边界,因为错位发生在 V4L2 buffer 交给用户态之前。

这里还需要保持谨慎:现有最终资料确认了 RKISP/V4L2 完成边界是第一个能够解释并改变 N−1 的位置,但没有保存足够完整的最终跨层 trace,用来证明当前枪的 CSI frame-end、RKCIF frame-end 都已经到达,只有 RKISP frame-end 缺失。因此更严谨的表述是:

首个被实验确认的错误交付边界在 RKISP 的 buffer 完成路径;导致该完成事件跨到下一次 XTRIG 的最上游硬件条件,仍需通过 CSI、RKCIF 和 RKISP 同步 trace 继续确认。

原驱动并没有“1079”这个默认值

RKISP 原驱动提供了一个 wait_line 配置,用于在输出到指定行时提前完成 buffer。未通过模块参数或设备树配置时,它的初始值为 0,也就是不启用行中断提前完成。

原始逻辑可以简化为:

wait_line = configured_wait_line;   /* 默认 0 */

if (wait_line != 0) {
    program_line_irq(wait_line);
    enable_early_done();
}

因此,问题出现时原驱动不是“把阈值写成了错误的 1080”,而是根本没有为 IMX296 Fast Trigger 启用这条提前完成路径。它依赖正常帧结束事件来完成 buffer;在本次 ROI 后的异常状态中,该事件到下一次 XTRIG 才推进,于是形成 N−1。

1079 到底是怎么来的

这里最容易把三个不同概念混在一起:datasheet 中的推荐记录区域、驱动使用的完整像素阵列,以及 RKISP 内部输出行计数器。

IMX296 datasheet 常见的推荐记录区域是 1440 × 1080,但当前 Linux 传感器驱动定义的完整像素阵列是 1456 × 1088。本次全幅路径中,RKISP 协商得到的实际输出高度也是 1088。

实测发现,RKISP V39 的输出行计数器在这条路径上的可观察终值是 1080,比实际输出高度少 8:

ISP 实际输出高度         = 1088
输出行计数器可观察终值   = 1080
最后能够触发行中断的位置 = 1079

因此当前代码使用的关系是:

counter_terminal = output_height - 8
wait_line         = counter_terminal - 1

代入全幅高度:

1088 - 8 - 1 = 1079

这不是简单的“1080 行从 0 开始计数”。如果对实际输出高度 1088 直接做 height - 1,结果应该是 1087,而不是 1079。现有资料也没有给出 RKISP V39 寄存器手册,证明该比较寄存器统一要求写 height - 1

更准确地说,1079 是本次实验中“计数器终止以前,最后一个确实能够产生所需行中断的比较值”。其中的 8 和最后的减 1,都来自当前 V39 路径的实测行为,而不是可以直接推广到所有 RKISP、所有分辨率的公式。

1079 与 1080 实验真正证明了什么

1079:当前 buffer 能及时发布,但尾部仍不安全

把行中断阈值设为 1079,并在中断后立即执行 early-done,整帧 N−1 消失。这证明:只要不再等待下一次正常 frame-end,驱动可以在当前 XTRIG 周期中发布当前 buffer。

但图像底部出现了约 8 行上一帧内容:

当前帧主体
+ 上一帧底部残留

它证明 1079 行中断不能作为“buffer 已经可以安全交给 CPU 和用户态”的充分条件。可能仍未结束的环节包括 ISP 后级流水、Memory Interface 写回、DMA outstanding transaction、缓存同步或 buffer 状态切换。现有实验只能确认交付过早,不能只凭旧 8 行就断言唯一原因一定是“DMA 还差 8 行”。

1080:没有行中断,不能证明 1080 时 DMA 正好完成

将阈值从 1079 改为 1080 后,所需的行中断没有产生。驱动重新退回到等待正常 frame-end 的路径,buffer 到下一次 XTRIG 才发布,整帧 N−1 恢复;此时图像尾部是完整的。

这个实验经常被简化为“1080 时尾部已经写完”,但证据并不支持这么精确的结论。因为 1080 没有中断,驱动并没有在计数到 1080 的那个时刻把 buffer 交给用户态。实际交付发生在更晚的正常 frame-end,甚至已经到了下一次触发。

所以 1080 对照能够证明的是:

  • 1079 和 1080 位于 V39 行中断能否触发的边界;
  • N−1 与是否走 early-done 路径直接相关;
  • 不提前发布时,最终得到的图像尾部完整。

它不能证明:

  • 行计数器采用 0 基还是 1 基;
  • 1080 就是 DMA 的精确完成时刻;
  • 固定再等多少毫秒在其他分辨率下仍然安全;
  • 上游 MIPI CSI-2 和 RKCIF 的 frame-end 已经在当前枪正常到达。
设置 实际发生的完成路径 结果 能证明什么
不启用行中断 下一次正常 frame-end 整帧 N−1,尾部完整 默认完成路径跨了一个触发周期
1079 后立即完成 当前枪 early-done N−1 消失,尾部旧行 行中断足以改变帧身份,但不是安全完成条件
阈值写 1080 没有行中断,回到下一次正常 frame-end N−1 恢复,尾部完整 1080 不会触发所需中断,不能定位精确 DMA 完成点
1079 后等待 50ms 定时器到期后 early-done 已测场景通过 50ms 是当前测试窗口内的经验安全值

当前代码如何适配不同高度

当前修改没有直接把 1079 写死,而是在每次 ISP 启动时读取媒体链路协商后的实际输出高度:

height = isp_output_crop_height;

if (is_rkisp_v39 && is_imx296_fast_trigger) {
    wait_line = height > 9 ? height - 8 - 1 : 1;
    tail_wait = configured_tail_wait;
}

如果实际 ISP 输出高度为 720,并且“计数器终值始终比输出高度少 8”这个假设仍然成立,那么计算结果是:

terminal = 720 - 8 = 712
wait_line = 712 - 1 = 711

如果 ROI 对齐后实际输出高度不是请求的 720,而是其他值,驱动使用的是协商后的 out_crop.height,不是用户界面上的“720p”名称。这一点是正确的:行中断阈值必须基于 ISP 真正处理的高度。

但动态计算高度只解决了“不写死 1079”,并没有证明尾部差值 8 是通用硬件规律。当前实现仍然写死了两个经验量:

输出行计数尾差 = 8
early-done 等待 = 50ms

切换到不同 ROI 高度、缩放路径、像素格式、ISP 输出通道甚至另一个传感器后,这两个值都需要重新验证。其他传感器目前也不会自动启用这段 IMX296 专用逻辑。

为什么 50ms 仍然不是根治

10ms、20ms 下仍能看到旧尾行,30ms 在当轮测试中通过但接近临界,50ms 在多个场景下稳定。因此产品值选择 50ms 是合理的保守决定,但它只回答了“目前等多久没有复现”,没有回答“硬件什么时候真正完成”。

而且,“8 行数据需要 30~50ms 才写完”本身就值得继续怀疑。在正常像素时钟和内存带宽下,几十毫秒明显不只是几行像素的简单搬运时间。50ms 可能同时覆盖了 ISP 后级流水、MI 写回、定时器调度、正常中断竞争或其他尚未观察到的状态转换。

从当前实现还能看到两个必须补测的时序风险。

风险一:定时器没有绑定明确的帧身份

行中断发生后,驱动启动一个高精度定时器;定时器回调再执行 early-done。当前用于避免正常完成和提前完成重复处理的身份,来自回调执行时读取的全局 SOF 时间戳,而不是启动定时器时保存的那一帧身份。

如果下一次 XTRIG 在定时器到期前已经到达,全局 SOF 可能已经更新。此时定时器属于帧 N,但回调观察到的可能是帧 N+1 的时间戳。现有多场景测试没有证明这种短触发间隔下仍然不会误完成或错误去重。

风险二:一个定时器未结束时,后续行中断会被合并

驱动通过一个 pending 标志保证同一时刻只挂一个 early-done 定时器。如果定时器仍在等待,新的行中断不会重新安排截止时间。

因此必须验证:

最小 XTRIG 间隔
  > 50ms 等待
  + 中断与调度抖动
  + buffer 完成开销

如果外部触发周期短于这个窗口,帧 N+1 的行中断可能在帧 N 的定时器仍 pending 时到达。当前资料没有覆盖 20ms、30ms、40ms 等短周期连续触发,不能直接声称方案支持任意触发频率。

这两个风险是静态代码审查得到的待验证项,并不表示已经在设备上复现。但在完成短周期压力测试之前,它们足以说明 50ms 方案仍是受条件约束的 workaround。

真正根治需要先找到哪一个 frame-end 消失了

下一步不应该继续尝试 60ms、80ms,也不应该再丢一张首帧。需要把同一次 XTRIG 在每个硬件边界上的身份和时间串起来:

层级 必须记录的事件 要回答的问题
外部触发 trigger token、上升沿/下降沿时间 当前测试的是哪一枪
IMX296 / CSI-2 frame start、frame end 传感器是否在当前枪完整结束一帧
RKCIF FS、FE、送入 ISP 的 buffer/sequence CIF 是否已经收到并转交当前帧
RKISP SOF、ISP frame-end、MI frame interrupt、输出行计数 ISP 的哪个完成事件跨到了下一枪
V4L2 curr/next buffer、sequence、buffer done 时间 哪块内存在什么时候真正交给用户态
用户态 dequeue/appsink sequence、内容标记 内核交付与最终图像是否一致

根据第一个缺失事件,可以把根因分成三类:

当前枪连 CSI/RKCIF frame-end 都没有
  → 继续检查 IMX296 ROI 后的 VTR/VMAX、MIPI 帧边界和模式切换

CSI/RKCIF frame-end 当前枪正常,RKISP frame-end 到下一枪才出现
  → 检查 RKISP V39 输入帧边界识别、在线路径和 ROI 后配置更新

RKISP/MI 已经完成,V4L2 buffer 下一枪才 done
  → 检查 curr/next buffer 状态机和正常中断/early-done 竞争

只有完成这组跨层对齐,才能回答“物理根因究竟在传感器帧边界、CIF 到 ISP 的传递,还是 RKISP buffer 状态机”。目前文章能够确认的是最后一个方向已经暴露并可被 early-done 改变,但不能跳过前两个方向的最终证据。

更合理的长期修复应该是什么样

理想方案不依赖固定毫秒数,而是依赖明确的完成条件:

  1. 优先使用 RKISP Memory Interface 或 DMA 的真实写回完成中断;
  2. 如果硬件只提供状态位,在可睡眠的工作线程中进行有超时的状态等待,不能在硬中断里忙等几十毫秒;
  3. 每个等待对象保存自己的 SOF sequence、buffer 指针和截止时间,定时器不能只依赖回调时的全局帧状态;
  4. 下一次触发到来前若上一帧未完成,应明确选择阻止触发、排队或报告 overrun,不能静默合并完成事件;
  5. 行阈值必须根据实际输出高度和经过验证的硬件计数语义计算,并在启动日志中输出 height、terminal、wait_line;
  6. 从 Fast Trigger 切回自由运行或切换传感器时,显式恢复默认完成策略,避免沿用上一种模式的阈值。

如果最终确认 RKISP V39 没有可以使用的尾部完成信号,固定等待仍可以作为产品策略,但必须把它写成接口约束:支持的最大触发频率、适用的分辨率和 ISP 路径都要有明确边界,而不是只保留一个没有上下文的 50ms 参数。

回归测试不能只看“连续几枪不再 N−1”

完整验收至少应覆盖:

  • 无 ROI 全幅采集,作为正常基线;
  • 设置 ROI 后的第一枪和连续多枪;
  • 清除 ROI、恢复全幅后的第一枪;
  • 多种 ROI 高度,检查动态阈值是否正确;
  • 固定相机参数,只改变外部场景标记;
  • 曝光或增益修改后的首枪,但不能只靠亮暗判断帧身份;
  • 200ms、100ms、60ms、50ms、40ms、30ms、20ms 等不同触发间隔;
  • 确认每个 XTRIG 只对应一个 V4L2 buffer done;
  • 检查 missing、duplicate、N−1 和局部旧行;
  • 检查定时器是否跨越下一次 SOF,pending 状态是否吞掉后续行中断;
  • CPU、内存和 ISP 高负载下重复测试;
  • stream off/on、ROI set/reset 和异常停止时没有延迟回调访问已经释放的 buffer。

验收标准不能只是“画面看起来对了”,而应满足:

trigger_token N
  == CSI/RKCIF frame N
  == RKISP/V4L2 sequence N
  == userspace frame N
  == image_content_marker N

同时还要满足:当前帧完整、尾部没有上一帧数据,并且完成回调发生在下一次触发以前。

总结

这次排查最重要的收获不是 1079,也不是 50ms,而是把“慢一帧”从模糊的画面延迟,转化成了可以沿 buffer 生命周期追踪的帧身份问题。

已经证实的是:在本次 ROI 后的异常状态中,等待正常完成事件会让当前 buffer 跨到下一次 XTRIG 才发布;1079 行中断能够改变这个发布边界;立即发布又会暴露尾部未安全完成的问题。

尚未证实的是:1080 是否对应某种真实 DMA 完成状态、尾差 8 是否适用于所有高度、50ms 是否支持短周期连续触发,以及导致正常 frame-end 跨枪的最上游硬件条件究竟在哪里。

因此,当前方案可以称为“经过实测的规避方案”,不能称为所有场景下的根治。真正的根治不是继续 flush、sleep 或丢帧,而是给每次 XTRIG 建立端到端身份,找到第一个缺失的 frame-end,并让 V4L2 buffer 只在硬件明确完成当前帧以后发布。

Comments

No comments yet. Why don’t you start the discussion?

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注