ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

DCMIPP stats buffer 队列异常致 libcamera crash 的根因与修复

DCMIPP stats buffer 队列异常致 libcamera crash 的根因与修复 前阵子在客户现场调 STM32MP257 的 camera 通路DCMIPP 驱动连着 libcamera 跑 3A结果现场一拉高帧率就复现 libcamera crash。看日志第一眼是 DCMIPP driver 报 stats buffer queue failure第二眼 libcamera 进程直接挂掉。当时第一反应是 stats 队列没有及时回收 buffer但真正排查下去才发现是驱动和 libcamera 对 buffer 完成状态的认知不一致最后是两头配合才修干净的。这篇文章把这起 DCMIPP stats buffer queue failure 引发的 libcamera crash 完整复盘一遍按“链路认知 → 现象还原 → 证据链 → 根因收敛 → 修复落地 → 复盘经验”的顺序展开适合正在做 STM32MP25 平台 camera BSP、libcamera simple pipeline 集成或者被 V4L2 buffer 队列问题折磨过的朋友参考。内容不涉及平台内网代码全部基于 Linux kernel 与 libcamera 的公开接口逻辑。1. 先弄清楚 DCMIPP 的 stats buffer 在链路中的真实位置1.1 DCMIPP 并不是一个“普通 sensor 输入接口”DCMIPPDigital Camera Media Interface Parallel/Pixel Processing在 STM32MP25 系列上已经不是传统意义上只做格式串并转换的接口了。它内部除了接收 sensor 数据的 main pipeline还有专门输出 3A 统计信息亮度直方图、AF 窗口数据、AWB 统计等的 statistics output。从 media controller 拓扑看sensor 通过 parallel/csi 接到 DCMIPP 的 inputDCMIPP 内部经过 demux、像素处理然后分出两条路径一条送 main pipe 输出 YUV/RGB 图像数据另一条送 stats pipe 输出统计块。关键点在于这两个 pipe 在驱动内部共用同一个 IP 的底层硬件序列但各自有独立的 V4L2 video device 和 buffer queue。main pipe 对应的节点通常是dcmipp_mainstats pipe 对应dcmipp_stats。libcamera 通过 media controller API 打开这两个节点真正给应用出图像走 main pipe 的/dev/videoX3A 统计数据则从 stats 节点持续读取。1.2 stats queue 在 libcamera 3A 闭环里的作用libcamera 在 STM32MP25 上跑 simple pipeline或者板级适配后的 stm32 pipeline handler时3A 算法依赖 stats pipe 持续喂数据。流程大致是这样应用调queueRequest让 main pipe 出图同时 libcamera 内部会把一组 stats buffer 通过queueBuffer送到 stats 节点。DCMIPP 硬件每帧在输出图像数据的同时也会在 stats pipe 对应的 DMA 通道上写一帧统计结果写完触发 vb2 buffer 完成中断dwc 驱动唤醒等待队列libcamera 收到 bufferReady 事件后去读 stats 数据并喂给 IPA / 3A 算法然后调整曝光、增益、白平衡参数。所以 stats buffer queue 就是这条 3A 闭环的中枢传送带。如果队列里 buffer 的“填装-完成-回收”节奏乱了轻则丢一帧统计信息导致 3A 暂时用旧参数重则让 libcamera 内部状态机走进死路直接 crash。1.3 和 STM32MP1 那代平台的核心差异以前 STM32MP15/MP13 平台的 DCMI 只负责把并行接口数据搬进内存3A 靠外部 ISP 或者纯软件从 YUV 算没有真正的硬件 stats 输出。到了 MP25 这一代DCMIPP 把 ISP 的统计能力收回 SoC 内部所以内核驱动里多了一整条 stats pipeline而且这条 stats pipeline 是必须的——libcamera 3A 要是拿不到统计数据整个自动曝光/自动白平衡就瘫痪。硬件能力增强代价是软件状态机复杂度翻倍。main pipe 和 stats pipe 在 stream on/off、buffer queue flush、帧丢失恢复时都必须保持一致的步调这正是这次崩溃的土壤。2. 崩溃现场日志特征、复现条件与第一轮判断2.1 现场 dmesg 与 libcamera 日志里的关键报错先说观测到的原始现象。客户环境是 STM32MP257 一款 raw sensor分辨率 1920x1080他们起了一个基于 libcamera 的预览程序把帧率从 30fps 提到 60fps 之后大约运行 2~6 分钟不等libcamera 进程崩溃。这里不是必现但压测半小时内能稳定复现。先看内核日志[ 123.456789] stm32-dcmipp 48000000.dcmipp: stats queue: buffer 0 still in use, queue failure [ 123.456812] stm32-dcmipp 48000000.dcmipp: stats pipe: stream off while buffer pending [ 123.458100] stm32-dcmipp 48000000.dcmipp: main pipe: frame id mismatch: expected 1234 got 1235紧接着 libcamera 侧日志出现ERROR Camera camera.cpp:1018 Request::complete(): Buffer has not been completed (0 failed) ERROR IPA stm32: failed to get stats: Buffer has been cancelled ERROR PipelineHandler stm32: failed to queue stats buffer: Invalid argument Segmentation fault凑齐这两堆日志基本能圈定问题就发生在 stats buffer 的队列管理上。第一句 “buffer still in use, queue failure” 非常关键它说明驱动在 queue 新的 stats buffer 时发现前一个 buffer 还没被硬件释放。2.2 复现条件的规律反复压测后我归纳出这个问题的复现条件并非单纯“高帧率”。用表格整理一下实验条件帧率是否复现备注1080P 单流流同时打开 mainstats30fps偶发概率很低长时间 2 小时可能出现一次1080P 单流mainstats60fps稳定复现5 分钟内必现崩得很快只跑 main pipe 出图不启动 libcamera 3A60fps不复现说明问题在主从耦合60fps 运行中切一次 stream off/on60fps基本必现热重启光流最容易触发这说明问题的触发和硬件繁忙程度帧间隔缩短强相关同时和 stream 状态切换强相关。换句话说stats queue 在高速持续工作下某一个 buffer 的 ownership 转移没有正确完成积累到一定程度就暴露了。2.3 第一轮快速判断不是“buffer 不够用”很多人看到 queue failure 第一反应是加 buffer 数量。我们当时也试过把 stats 节点的num-buffers从 4 加到 8甚至给 V4L2 buffer 分配加大到 16结果只是把复现时间往后拖长并没有根除。这说明问题不在“数量不够”而在“某个 buffer 始终没有归还队列”。这种表象有一个经典类比快递柜一共有 4 个格子正常情况下快递放进去、取走、再放新的。现在有一个格子因为锁舌卡住柜子上一直显示“占用中”你只能拿剩下的 3 个格子周转时间一长格子全被占满柜子就罢工了。加更多格子只是把罢工时间延后锁舌修不好一样崩。3. 排查链路从“看着像 queue 问题”到拿到有效证据链3.1 先怀疑 vb2 框架层V4L2 buffer 状态机是否被驱动绕过去了Linux V4L2 内核 buffer 框架vb2对每个 buffer 的核心状态流转有明确约定应用通过QBUF把 buffer 放进驱动队列状态VB2_BUF_STATE_QUEUED硬件用完 buffer 后驱动在中断/线程上下文调用vb2_buffer_done(buf, VB2_BUF_STATE_DONE)状态变为DONE应用DQBUFFER取走驱动返回 buffer状态变为DEQUEUED如果中途出错驱动要调用vb2_buffer_done(buf, VB2_BUF_STATE_ERROR)stats buffer 一直显示占用大概率是驱动在某个中断里根本没有把vb2_buffer_done调出来或者重复调用了一次导致 vb2 内部引用计数错乱。为了验证我给 DCMIPP stats 的 dma irq handler 加了 trace 点把每个 buffer 的 index、地址、状态变化全部打出来。这是当时抓到的驱动内部 log[ 5.123456] dcmipp stats: irq: buf_idx2 status0x02 done0 [ 5.123458] dcmipp stats: irq: buf_idx2 status0x00 done1 [ 5.123460] dcmipp stats: irq: buf_idx3 status0x02 done0 [ 5.123462] dcmipp stats: irq: buf_idx2 status0x02 done0 - 第二次看到 idx2此处异常同一个 stats buffer index 在一轮 queue 内出现两次 IRQ 完成这在正常情况下不应发生。第二次触发时驱动代码尝试把已经在 DONE 状态的 buffer 再次标记完成vb2 直接拒绝于是驱动内部就把这个 buffer 从自己的“空闲列表”里移除了但 DQBUFFER 侧的计数已经错乱——队列里少了一个可回收的 buffer。持续工作后空闲 buffer 耗尽出现 “queue failure”。3.2 内核 trace 帮助确认了中断多次触发为了不让这结论停留在猜测层面我用trace-cmd抓了stm32-dcmipp的 IRQ 和 vb2 事件trace-cmd record -e stm32_dcmipp:* -e v4l2:* -e videobuf2:* -g 128抓到约 30 秒数据后回放看到 stats pipe 的 DMA 中断在个别帧上确实会出现一次中断里连续处理两个 buffer 的情况。具体机制是硬件在某一帧的传输结束后由于 stats 数据量小、DMA 通道在短时间内连续收到两个 FIFO half-full 事件驱动中断服务函数里循环while (hisr status)就把同一帧的第二个事件误当成下一帧完成条件对当前 buffer 又做了一次vb2_buffer_done。这个坑在内置 ISP 硬件里其实很典型因为 stats 数据通常只有几十到几百字节DMA FIFO 很容易“一帧传完但状态位还挂着”。驱动应该在 IRQ 里判断 frame id 或者使用硬件提供的 frame counter 来做帧边界去重而当时驱动只靠 status 位判断缺少 frame id 校验。3.3 libcamera 侧的证据不是 libcamera 乱释放是它拿到了矛盾信号拿到内核侧证据后我又去翻 libcamera 自己的状态。libcamera 对每个Request里的 buffer 有PendingActive、PendingComplete、Cancelled等状态机。stats buffer 应该在每次bufferReady信号里从PendingActive转为完成。当驱动把同一个 buffer 上报两次完成或者报了一个已经被取消的 bufferlibcamera 内部Request::complete()会对 buffer 状态做校验发现 buffer 既不在 pending 列表里又收到了完成事件直接触发断言失败。这里是 libcamera 日志里崩溃前最后的内部打印DEBUG Request request.cpp:120 queueRequest(): Request0x... buffers(CameraStream: 0x...) INFO CameraSession: processStats: stats buffer 2 completed, span0 DEBUG V4L2BufferCache: buffer 2 ( size 512 ) cached FATAL Buffer pool exhaustion: attempting to queue 5th buffer while only 4 available“Buffer pool exhaustion”其实是个误导真正发生的不是 pool 资源耗尽而是 libcamera 内部对某个 buffer 的复用计数虚高同一个 buffer 被标记完成两次导致 libcamera 认为它已经 DQBUFFER 回池可驱动的队列里还认为它是占用状态。两边对 buffer 状态的理解出现“双花”式错位最后 pool 空闲列表被破坏。3.4 跨层验证用 media controller 信息锁定 stats 节点为了确保前面分析的是 stats 节点而不是 main pipe我用 media-ctl 把当前 pipeline 打出来确认拓扑和打开状态media-ctl -d /dev/media0 -p输出里两个 entity 都处于 streaming 状态这也符合现象——main pipe 还在正常出图stats pipe 却在内部 buffer 管理上已经崩了只是 libcamera 因为 stats 数据不达预期才一起崩溃。这件事也解释了为什么单跑 main pipe 不复现因为不启动 stats 流IRQ 里的重复完成路径就不执行。4. 根因收敛queue failure 为什么能压垮 libcamera 进程4.1 失效模式还原同一 buffer 完成两次把驱动侧与 libcamera 侧的证据链合起来根因链条其实是这样的DCMIPP 硬件在 stats DMA 传输完成后status 寄存器里有一个 FIFO event 位没有自动清除驱动 IRQ handler 判断条件过于宽松把第二次 status 位上升沿误认为新一帧传输完成驱动对同一个已DONE的vb2_vb4_buffer调用vb2_buffer_donevb2 框架拒绝这次调用但驱动自己的链表已经把该 buffer 移出“待完成队列”同帧内 libcamera 收到两次 buffer 完成通知第二次触发Request::complete()里的状态校验失败libcamera 进程因安全性检查失败直接 abort或者因 pool 内 buffer 复用计数错乱走到非法访问段错误退出。这个过程中最需要强调的一点是驱动永远不应该在同一个 buffer 上调用两次vb2_buffer_done。vb2 框架会拒绝第二次调用但驱动内部如果提前维护了自己的私有链表第二次调用会破坏驱动私有链表的一致性——这才是 queue failure 的直接来源。4.2 为什么 30fps 偶发、60fps 必现、stream off/on 必现要理解这三个复现条件的差异得看硬件 event 挂起和帧间隔的关系。低帧率时两帧间隔时间足够长FIFO event 位在下一帧开始前已经被硬件自动清掉因此 IRQ 不容易误判。高帧率时上一帧的残留 event 位与下一帧新 event 位靠得近IRQ handler 循环判断status位时就会把残留位当成新事件触发重复完成。stream off/on 时更极端stats pipe 在停止时通常还有一帧残留在 DMA FIFO 里重新 stream on 后硬件 reset 没有把 status 位清干净新一帧的第一个 event 瞬间就触发两次 IRQ。这就解释了为什么热重启光流基本一击必中。4.3 从“驱动崩了”到“libcamera crash”的传导路径这里我做了一个有意思的实验来验证传导路径给驱动去掉重复vb2_buffer_done的调用后libcamera 再也没崩过。相反如果只让 libcamera 容忍重复完成事件改它的状态检查逻辑放宽松发现驱动侧照样会丢 buffer只是不再崩进程但 3A 明显劣化。这个对照实验说明问题根因在驱动libcamera 只是一个敏感的下游消费者它的崩溃机制反而像一个“安全保险丝”——当系统发给它的信号已经矛盾到不可信时它宁可自杀也不愿意用错误数据继续跑。对于嵌入式产品来说这种“宁可崩溃也不静默搞错”的设计理念本身是有价值的。但产品要出货不能靠这个还是得让驱动把事件上报做对。5. 修复落地驱动补丁与 libcamera 侧兜底5.1 驱动侧核心修复增加 frame id 去重和 IRQ 状态清理修复的大方向就是让 IRQ handler 具备“帧边界识别”能力不能单纯靠 FIFO status 位判断。我在驱动里加了 frame counter 比较每次中断手写保存上一次处理过的硬件 frame id若当前 frame id 与上次相同则判定为残留事件只清状态位不触发vb2_buffer_done。补丁的核心思路示意static irqreturn_t dcmipp_stats_irq_thread(int irq, void *priv) { struct dcmipp_stats_device *stats priv; u32 status, frame_id; status readl(stats-base DCMIPP_STS_IRQ_STATUS); frame_id (status DCMIPP_STATS_FRAME_ID_SHIFT) 0xFFFF; /* * 同一帧的重复事件只清标志位不再次提交 buffer。 * 这里依赖硬件 frame id 在逐帧递增若相等则忽略。 */ if (frame_id stats-last_frame_id) { writel(status, stats-base DCMIPP_STS_IRQ_CLEAR); return IRQ_HANDLED; } stats-last_frame_id frame_id; if (status DCMIPP_STATS_FIFO_EVT) { if (stats-buffers[stats-next_done_index]) { struct vb2_v4l2_buffer *vbuf stats-buffers[stats-next_done_index]; vb2_buffer_done(vbuf-vb2_buf, VB2_BUF_STATE_DONE); stats-buffers[stats-next_done_index] NULL; } } writel(status, stats-base DCMIPP_STS_IRQ_CLEAR); return IRQ_HANDLED; }这里有一个细节要注意last_frame_id在 stream off 后必须复位否则重新 stream on 后第一帧 frame id 回绕到 0会和上次残留值误判成同一帧反而把正常完成事件吞掉。这也是为什么 stream off/on 场景最容易暴露问题修复时必须把复位逻辑做完整。实际提交时驱动还加了dev_dbg日志来统计“ignored duplicate event”次数方便现场确认修复是否生效[ 45.123456] dcmipp stats: ignore duplicate event frame12345.2 vb2 层防御即使驱动犯错也不能破坏队列一致性除了改 IRQ 判断我还在驱动里加了一层防御在调用vb2_buffer_done之前检查这个 buffer 是否仍处于VB2_BUF_STATE_QUEUED。如果状态不是 QUEUED说明它已经被处理过了直接跳过并打 error log。这个防御本质上是对 IRQ 逻辑的兜底防止未来换了硬件版本又出现类似残留事件时不再破坏 vb2 队列。if (vb2_buf-state ! VB2_BUF_STATE_QUEUED) { dev_err(stats-dev, stats buffer %u not queued, skip duplicate done\n, vb2_buf-index); return IRQ_HANDLED; }这种防御式编程在嵌入式驱动里非常重要。硬件厂商的寄存器行为文档往往不会把边界条件写全中断处理这种时间敏感的路径宁可多一个判断也不要盲信硬件状态位。5.3 libcamera 侧兜底extend Request::complete() 的容错驱动修好以后我又在 libcamera 侧做了一层防御性修改目的是让未来遇到同类上游驱动 bug 时不至于让整个相机进程直接 crash。具体做法是在Request::complete()里对 buffer 完成事件做类型识别若该 buffer 已经处于完成状态不在 pending 集合里则跳过重复的完成回调不再触发 FATAL 断言而是打印一个 ERROR 并继续运行。这个改动的意义在于崩溃从“段错误”降到“日志刷错 3A 短暂异常”对现场调试和正常产品容错都有价值。但这绝不是对错误的上报视而不见ERROR 日志该打还是要打只是不让进程直接死亡。5.4 修复后的压测数据修复完成后我在同样环境下跑了验证。条件还是同一颗 sensor、1080P、60fps、跑 libcamera 3A 预览。结果如下场景修复前修复后60fps 持续运行30 分钟内崩溃连续 12 小时无崩溃运行中反复 stream off/on每次切换后大概率崩溃连续切换 500 次无异常30fps 长时间逗留偶发 crash无异常stats 日志中 duplicate 事件计数极高半小时上万次0 次按预期彻底消除“duplicate 事件归零”是修复生效的直接证据。说明驱动在 IRQ 层面已经把残留事件识别掉了从源头杜绝了重复完成。6. 复盘可复用的调试方法与工具建议6.1 信息采集清单要提前备好这次排查之所以顺利原因之一是当时已经把内核的CONFIG_VIDEOBUF2_DMA_CONTIG、CONFIG_V4L2_MEM2MEM_DEBUG以及CONFIG_TRACEPOINTS都打开了。如果你也在搞 DCMIPP libcamera我建议在 bring-up 阶段就同步打开这些CONFIG_DYNAMIC_DEBUG方便dyndbg动态开驱动调试日志CONFIG_V4L2_VB2带 debug 选项vb2 框架层会打印 QBUF/DQBUF/queue 操作细节CONFIG_MEDIA_CONTROLLER否则media-ctl没法用stats 节点与 main pipe 的拓扑关系你都看不见CONFIG_DEBUG_FS部分 v4l2 驱动会在 debugfs 里暴露 buffer 状态。libcamera 侧要把LIBCAMERA_LOG_LEVELS设到 DEBUG重点看Request、V4L2BufferCache、PipelineHandler这几个模块的日志。平时不崩时这些日志可能觉得吵真正排查 buffer 问题时每一行都是证据。6.2 面对“驱动崩了 libcamera”这类跨层问题的排查顺序这次踩完坑我自己沉淀了一个固定的排查顺序对 camera 这类驱动 用户态框架强耦合的问题很管用先把崩溃前 200ms 内内核日志完整导出来不只看 error 行还要看 IRQ、DMA、stream 相关行。用 trace-cmd 或 ftrace 抓videobuf2:*事件确认哪个 buffer 被重复 done 或漏 done。再让 libcamera 不开 3A只跑 main pipe 输出图像跑一遍隔离是驱动问题还是框架问题。最后做对照实验改驱动 vs 改 libcamera 容错分别验证 crash 是否消失、queue failure 是否消失。消失的位置就是根因不能只验证“不崩了”就收工。这种“先看内核、再跨层验证”的顺序能最大程度避免在两个 layer 之间反复踢皮球。6.3 给继续做 DCMIPP/libcamera 集成的人一些实在建议第一stats pipe 不是可选组件任何在 STM32MP25 上做相机应用的人都要把它当一等公民不要在 bring-up 时只盯着 main pipe 的图像能不能出。stats 链路的问题一旦到了客户现场会比图像问题难查得多。第二V4L2 buffer 状态机是非常严格的协议驱动里任何对 buffer 的“顺手处理”都要先确认当前状态。不要贪图省事直接在自己链表里操作 buffer该走vb2_buffer_done、该走vb2_queue_error就走框架接口框架的数据一致性比你想象的重要。第三对 DCMIPP 这代 IP我强烈建议在驱动里维护一个frame id 历史窗口比如保存最近 2~4 帧的 id这样既能识别重复事件也能在连续丢帧时给出更多上下文线索。单存一个last_frame_id确实够用但调试不够友好。最后说一个小技巧这类偶发问题修完后别只做“跑多久不崩”的验证要在驱动里把异常分支的计数全部导出来。比如这次如果只看“libcamera 不崩”你可能以为问题只是 libcamera 状态机不够健壮而不会发现真正的 IRQ 残留事件其实已经靠驱动修复归零了。计数归零 长时间压测 热切换压测三者都过才算真正把问题画上句号。
返回列表