公司动态

libcamera在STM32MP257上配置双路流内存分配失败问题解析

📅 2026/8/31 22:02:20
libcamera在STM32MP257上配置双路流内存分配失败问题解析
1. 问题现象与现场环境如果你正在 STM32MP257F 平台上调 libcamera大概率对这个组合不陌生IMX335 做图像传感器跑 OpenSTLinux业务侧用 libcamera 同时开两路 stream主码流出 1080p 或者 5MP 全分辨率另一路想取 640x640 的小图喂给 AI 模型。而我遇到的情况是主码流配置一切正常只要一配置第二条 640x640 的流程序就在 configure 或 start 阶段直接抛std::bad_alloc整条进程崩溃没有任何 C 层源码提示。先说清楚环境方便你对照排查。主控是 STM32MP257F这颗料是 ST 新一代 MPU双核 Cortex-A35 Cortex-M33视觉这边带 DCMIPPDigital Camera Memory Interface Pixel Processor和 ISP跑 Linux 侧的时候 camera 栈走的是 OpenSTLinux 自带的 libcamera V4L2 media controller 这套。传感器是 Sony IMX335最大输出 2592x194430fps 没问题。我的业务场景是一路主码流输出 1920x1080 YUV420 用于编码显示第二路 secondary stream 配置成 640x640 RGB888 给 AI 推理。问题就卡在第二路。这个现象最麻烦的地方在于它不是一个稳定的崩溃点。同样的二进制偶尔启动能跑起来多启动几次必然挂把 secondary stream 改成 640x480 或者 800x600又一切正常。光凭这一点就能确定问题跟分辨率组合、内存分配时机、DCMIPP 内部 buffer 策略强相关而不是简单的代码逻辑写错了。下面我把整个定位过程、排查命令、最终方案完整写出来遇到同款问题的朋友可以直接抄作业。2. std::bad_alloc 到底是谁抛出来的2.1 libcamera 中异常的上抛路径先说一个容易被忽略的细节std::bad_alloc是 C 堆分配失败时抛出的异常但它不一定代表你的用户态代码new或malloc失败了。libcamera 这个框架从底层到上层全是 Cconfigure()、start()阶段会做大量容器操作比如std::vector扩容、std::map插入、std::unique_ptr创建对象这些操作一旦底层分配内存失败就会向上抛std::bad_alloc。但你如果只看异常栈第一层很容易被带到沟里。我当时的 backtrace 里出现的是std::length_error或std::bad_alloc紧接着下面是 libcamera 内部的V4L2VideoDevice::createBuffers()再往下是SimpleCameraConfiguration::configure()。换句话说异常不是随便什么时候抛出来的而是发生在分配 V4L2 buffer 的那一步。createBuffers()本质上是调用VIDIOC_CREATE_BUFS这个 ioctl 向内核申请 DMA buffer如果内核返回ENOMEMlibcamera 内部会把它包装成std::bad_alloc重新抛出。所以看到这个异常第一反应不要是修 C 层的 try-catch先怀疑底层内存分配。2.2 内核层内存分配失败如何传导到 C 层嵌入式 Linux 上 V4L2 的 buffer 分配不是简单的kmalloc它走的是 dma-buf 机制。V4L2 驱动通过VIDIOC_CREATE_BUFS要求驱动分配内存驱动内部通常从 DMA heap 或者 CMAContiguous Memory Allocator区域拿内存。IMX335 STM32MP257F 这条链路里DCMIPP 对内存有连续性和对齐要求尤其当 DCMIPP 的 descriptor 和 line buffer 需要物理连续内存时驱动只能从 CMA 里抠。CMA 区域是一种可回收的连续内存池平时普通进程可以用但当驱动需要大块连续内存时它会抢占回收。问题是如果系统里已经有一堆不可移动的页面占住了 CMA 区域或者 CMA 总量本身就不够那么dma_alloc_from_contiguous就会失败返回值体现在 V4L2 层就是ENOMEM。到了 libcamera 层V4L2BufferCache::get()或FrameBufferAllocator::allocate()发现 ioctl 失败就会把错误转成异常。具体实现里libcamera 的V4L2VideoDevice::createBuffers()会对 ioctl 返回值做检查失败就throw std::runtime_error(Failed to create buffers)而某些版本会走std::bad_alloc路径。所以你不用纠结为什么 libcamera 不抛一个更精确的异常它的底层设计就是把内核错误统一成 C 异常用catch (...)或者gdb catch throw能拿到更完整的上下文。建议第一时间不要直接去 try-catch 包住 configure()先用gdb catch throw看异常原发点或者把 libcamera 的日志级别开到 Debug看日志里是否有Failed to allocate、Cannot allocate memory等关键行。这一步能帮你区别是 CMA 不够还是用户态堆不够。3. 640x640 这个分辨率为什么是导火索3.1 IMX335 输出特性与方形画幅转换IMX335 的原生输出是 4:3 的 2592x1944这是一个典型的 5MP sensor。640x640 是 1:1 方形画幅从 4:3 到 1:1中间要经历 crop 和 scale。这里有个很多人忽视的问题libcamera 的 pipeline handler 在内部会先做 sensor crop再做 ISP/DCMIPP 的 scale。具体到 STM32MP257FDCMIPP 能同时出多路输出一路 main 走直通一路 aux 支持 downscale。但是这个 downscale 的缩放因子有约束不是任意比例都能直接做到。如果直接让 DCMIPP 把 2592x1944 缩到 640x640水平方向缩放因子约 4.05垂直方向约 3.04两个方向缩放因子不一致DCMIPP 通常的处理方式是先做一部分 crop 让宽高比一致再统一缩放。crop 到 640x640 对应 sensor 端是 640x640 的画幅从 2592 宽里裁掉大量像素这本身不消耗额外 buffer。但问题在于如果 DCMIPP 的 aux 输出不支持这个目标尺寸组合libcamera 会尝试用一个中间尺寸做过渡比如先生成 1280x1280 或 640x1944 的中间帧再经过一次换算输出最终 640x640。这就意味着配置 secondary stream 时实际需要分配的 buffer 峰值比你预期的高不少。3.2 DCMIPP 双输出与内存峰值模型我们算一笔账。主码流 1920x1080 YUV420一帧内存大概是 1920*1080*1.5 ≈ 3.1MB如果 bufferCount 是 8就是约 25MB。secondary stream 如果配置 640x640 RGB888一帧是 640*640*3 ≈ 1.2MB8 个 buffer 是 9.6MB。只看最终配置35MB 左右的 CMA 用量并不夸张。但 DCMIPP 的 buffer 分配不完全按最终输出算。如果 pipeline handler 内部需要 intermediate buffer或者 libcamera 给每个 stream 都额外预留了 padding 和对齐空间峰值可能翻倍。更关键的是DCMIPP 使用的连续内存不一定从通用 CMA 池里分配STM32MP2 系列有些 buffer 会从特定的 DDR 保留区或者 dedicated heap 里拿。不同 heap 的剩余空间各自独立即使系统总内存还有很多特定 heap 不足也会直接失败。640x640 之所以是导火索核心原因就在这里它处在 DCMIPP 能支持、但支持得很勉强的边界上。你换 640x480 或者 800x600缩放因子更常规DCMIPP 可以直接产出不需要额外的中间 buffer换 1920x1080虽然 buffer 更大但它是 sensor 原生比例的常规输出pipeline 走的是最直通路径反而稳定。还有个容易被忽略的因素是 bufferCount。libcamera 默认情况下每个 stream 可能配置 4 到 8 个 buffer如果你不显式设置它按硬件特性默认来。两个 stream 的 buffer 数量是累加的主码流加 secondary 流总共十几个 dma-buf每个 dma-buf 的地址对齐和 size 对齐都会在 heap 里形成碎片。CMA 是典型的“大块连续内存池”碎片越多越容易在大分配请求时失败。640x640 分配失败不一定是你缺那 1.2MB而是分配器找不到一块满足对齐要求的连续区域。4. 定位内存瓶颈的实操清单4.1 先查看内核 CMA 和 dma-buf 占用遇到std::bad_alloc第一步不是改代码而是建立一张“配置前 vs 配置后”的内存画像。这套流程我实测很好用五分钟就能定位是不是 CMA 不足。# 查看 CMA 总量和剩余量 cat /proc/meminfo | grep -i cma # 查看内存碎片化情况 cat /proc/buddyinfo # 查看当前 dma-buf 占用重点看 DCMIPP 和 sensor 相关的条目 cat /sys/kernel/debug/dma_buf/bufinfo # 查看内核日志里有没有 CMA 分配失败 dmesg | grep -i -E cma|contiguous|alloc failed|ENOMEM我在出问题时/proc/meminfo里 CmaTotal 是 128MBCmaFree 只剩不到 30000 kB而且 dmesg 里能看到cma: cma_alloc: failed这样的记录。这说明 CMA 在系统启动后已经被各种驱动占用了一大部分到了 libcamera 配置 second stream 的时候剩余连续内存已经撑不起新的分配请求。这里建议你对比两次输出一次在程序跑起来之前抓一次在程序 configure 失败前抓。如果你是在嵌入式板子上远程调可以先把/proc/buddyinfo和cat /sys/kernel/debug/dma_buf/bufinfo重定向到文件里再复现崩溃最后对比差异。重点看 dma-buf 里谁的 size 最大、谁的数量最多。4.2 用 gdb/catch throw 快速锁定异常源如果你不想完全靠猜直接用 gdb 抓异常。libcamera 是 C所有异常从__cxa_throw经过你可以让 gdb 在异常抛出时停下来。gdb ./your_camera_app (gdb) catch throw (gdb) run (gdb) bt这样能看到异常当时是从哪个源文件哪一行抛出来的。我的经验是最终栈会落在libcamera::V4L2VideoDevice::createBuffers或者libcamera::FrameBufferAllocator::allocate。看到这个栈基本可以判定问题发生在内核 buffer 分配层而不是业务代码里的std::vector扩容。如果 gdb 不方便还可以临时把 libcamera 的日志级别调到最高观察配置过程中的关键日志。OpenSTLinux 上通常通过环境变量或配置文件控制LIBCAMERA_LOG_LEVELS*:DEBUG是一个比较常用的设置。日志里如果出现Failed to allocate、Cannot allocate memory、out of memory这类字眼基本不用再看用户态了。4.3 监控配置前后内存变化还有一个实用的土办法不用太高深的工具。在第二个 stream 配置之前开一个后台循环每 100ms 抓一次内存快照while true; do date %T /tmp/mem_trace.log grep -i cma /proc/meminfo /tmp/mem_trace.log sleep 0.1 done配置失败后看最后一两秒的日志能非常直观地看到 CmaFree 从正常一路掉到接近 0 的过程。这个方法在调试 buffer 数量导致的峰值压力时特别有用你能确切知道是“总容量不够”还是“瞬间峰值太高”。5. 可落地的解决方案5.1 方案一调整 CMA 大小最直接的手段增大 CMA。STM32MP257F 平台的 kernel cmdline 或 device tree 里可以调整 CMA 大小。默认可能是 128MB对 IMX335 多路流来说确实偏紧。内核 cmdline 方式在启动参数里加cma256Mdevice tree 方式在 reserved-memory 节点里确认reserved-memory { #address-cells 2; #size-cells 2; ranges; linux,cma { compatible shared-dma-pool; reusable; size 0x0 0x10000000; /* 256MB */ linux,cma-default; }; };修改后重启再看/proc/meminfo确认 CmaTotal 变成了 256MB。需要注意CMA 不是越大越好。CMA 区域是可回收的但如果系统里长期有不可移动页CMA 的“可回收”能力会下降另一方面CMA 设置得太大会压缩普通内存池影响系统整体性能。我的建议是从 256MB 起步如果能稳定跑再逐步往下调找到临界值。对 IMX335 两路流来说256MB 通常是个安全值。5.2 方案二减小 bufferCount 压缩峰值如果不想动内核配置业务侧最容易做的是减少 buffer 数量。libcamera 里每个StreamConfiguration都有一个bufferCount字段默认值不一定适合你的内存预算。如果主码流和 secondary 流各配 8 个 buffer高峰期确实压力巨大改成 4 个内存占用量几乎减半。StreamConfiguration cfg stream-configuration(); cfg.bufferCount 4; // 默认可能是 8按需调小这里给一个参考值主码流 1920x1080 YUV420bufferCount 4占用约 12.4MBsecondary stream 640x640 RGB888bufferCount 4占用约 4.8MB。总量不到 20MB对 128MB CMA 来说就很轻松了。但 bufferCount 不能无限小。你至少要保证 ISP/DCMIPP 流水线能持续出帧通常 3 到 4 个 buffer 是最低安全线。少于 3 个DCMIPP 可能因为 buffer 周转不过来而丢帧。所以这个参数要结合你的实际帧率和使用场景调不要盲目追求最小。5.3 方案三把 second stream 格式改为 YUV420/NV12因为 640x640 RGB888 内存占用高而且 DCMIPP 不一定支持 aux 输出 RGB888。如果 DCMIPP 不支持libcamera 会在内部做一次颜色空间转换这通常意味着额外分配一块中间 buffer进一步增加内存压力。改成 YUV420 或者 NV12一个 640x640 的帧只有 640*640*1.5 ≈ 614KB比 RGB888 的 1.2MB 小了一倍。很多嵌入式视觉管线本身就在 YUV 域做预处理AI 输入前再转 RGB 也不迟。如果你的下游模型可以直接接收 YUV这个方案是最优的。cfg.pixelFormat formats::YUV420; // 或者 formats::NV12 cfg.size { 640, 640 };一个小技巧如果 DCMIPP 对 RGB888 的 aux 输出支持不明确你可以先用 V4L2 的media-ctl -p或者 libcamera 的 debug 日志确认实际 format。看到 pipeline 里有processing或格式转换节点十有八九多占了内存。让 DCMIPP 直接输出 YUV能省掉这一层。5.4 方案四强制走 DCMIPP 的硬件 downscaler有时候问题不是内存总量不够而是 DCMIPP 为了完成 4:3 到 1:1 的转换内部多走了一段路径产生额外的中间 buffer。这种情况可以通过调整 sensor 的 scaler crop让 DCMIPP 的缩放更“直接”一些。libcamera 里可以通过StreamConfiguration::scalerCrop控制。把 crop 设置成接近 1:1 的区域比如在 sensor 原生画幅上裁出一个约 1944x1944 或者 1920x1920 的区域再往下缩到 640x640DCMIPP 就不需要做比例先转换再缩放的绕路操作了。cfg.scalerCrop Rectangle(324, 0, 1944, 1944); // 在 2592x1944 中裁出中心方形区域不过 scalerCrop 不是所有 pipeline handler 都完全支持需要确认你的 libcamera 版本和 DCMIPP 驱动实现。如果支持这个方案对内存峰值的影响非常明显如果不支持你只能退回方案一和方案二。还有一个思路是从 sensor 出图格式上做文章。如果你确实只需要 640x640 的小图给 AI可以考虑让 sensor 直接输出一个低分辨率模式比如 IMX335 的 1280x720 模式然后裁 640x640。这样整个 pipeline 走了最直通路径几乎不会有多余 buffer。代价是你需要在 DTS 或 sensor 驱动里确认 IMX335 是否支持预期模式以及主码流是否也依赖同一 sensor 的全分辨率输出如果两路流都要这个方法就不适用了。5.5 方案五业务侧预分配内存和异常兜底即使内核层内存充足业务侧也可能因为std::vectorbool或者大量临时对象在 configure 瞬间同时分配内存导致踩到峰值。我的习惯是在程序启动早期把给每个 stream 准备的帧缓冲池提前分配好而不是等到 stream start 的时候才 new。auto bufferPool std::make_uniqueFrameBufferAllocator(camera); // 提前 allocate失败的话给出明确日志 if (bufferPool-allocate(stream) 0) { std::cerr allocate stream buffers failed std::endl; return -1; }同时程序里对std::bad_alloc要做兜底处理。虽然不能直接恢复但至少要确保日志里能看到是哪个阶段、哪个 buffer 分配失败而不是一个光秃秃的 terminate。try { camera-configure(config); } catch (const std::bad_alloc e) { std::cerr configure failed: bad_alloc: e.what() std::endl; // 打印当前 CMA 状态或者触发一次内存回收 return -1; }这里我给一个实测有效的附加操作在配置第二个 stream 之前主动触发一次内存回收把容易回收的页面先让出来。echo 3 /proc/sys/vm/drop_caches这在调试阶段很有效能临时验证“是不是 CMA 被缓存占住了”。不过生产环境不要随便用它会影响系统整体性能只作为排查手段。6. 常见问题速查表下面这个表格是我在多个平台上排查类似问题时总结的高频场景你可以对照自己的日志快速缩小范围现象可能原因处理方式dmesg 有CMA: failed to allocateCMA 总量不足或碎片严重增大 CMA减小 bufferCount释放不必要的 dma-buf异常栈在new[]或std::vector::resize用户态堆内存不足可能是 cgroup 限制或过度分配检查 cgroup memory limit减少并发 buffer预分配对象换成 640x480 正常640x640 崩溃DCMIPP aux 不支持该尺寸组合走中间转换路径使用 640x480 代替或强制 scalerCrop 为 1:1或改 YUV420PC 的 v4l2loopback 上没问题板子上崩溃嵌入式硬件 pipeline 对 buffer 有连续内存要求查看 DCMIPP driver 要求确认使用专用 dma heap单独配置一路 640x640 正常两路同时不行并发 buffer 峰值超过 CMA 或 heap 剩余调 bufferCount或先释放不使用的 DCMIPP buffer启动第一次偶尔成功第二次必挂前一次运行的 dma-buf 没有释放干净检查进程退出时是否调用release()配合 bufinfo 确认这里我想强调一个很多人容易忽略的隐藏项dma-buf 泄漏。如果前一次运行的程序没有正确释放 buffer后一次运行会继承一部分“死”的 dma-buf导致内存越跑越少。我遇到过的情况是程序崩溃后 driver 没有彻底释放所有 buffer必须在板子上重启一次 camera 服务或者卸载重载驱动模块才能恢复。排查时如果发现每次失败后 CmaFree 都在减少且重启后能恢复一定优先排查 buffer 释放路径而不是反复调 CMA 大小。7. 最后如果你也遇到类似的问题我的建议是先不要急着改代码。花二十分钟把 CMA、dma-buf、dmesg 三样东西抓出来原因基本就浮出水面了。std::bad_alloc在嵌入式 libcamera 场景下绝大多数时候是底层的连续内存分配失败而不是 C 代码写得不对。调试技巧方面我最推荐gdb catch throw配合/proc/meminfo对比。这两个手段能让你在一分钟内区分问题出在 CMA 还是用户态堆。另外一个小心得把第二条流的分辨率先换成 640x480 验证整个双路流程是否通然后把分辨率一步步调到 640x640这样你能观察到是哪个临界点触发了崩溃往往这个临界点就是 DCMIPP 的硬件限制边界。如果你最终解决了也可以把这套配置固化成几个模板参数后续换 sensor 或者换分辨率的时候直接复用这套内存规划逻辑。