拓冰建站拓冰建站
首页 / 资讯中心 / 正文

STM32MP2上libcamera配置非标分辨率引发bad_alloc的排查

1. 项目背景与问题现场STM32MP257F 上的视觉流配置踩坑1.1 这套硬件组合在做什么STM32MP257F 是 ST 新一代 MPU 里定位工业视觉和边缘 AI 的型号双核 Cortex-A35 加一个可选的 Cortex-M33芯片内部还集成了 NPU算力大概在 2 TOPS 这个量级。这种片子的典型用法是“轻量 AI 视觉采集”IMX335 通过 MIPI CSI-2 接进来libcamera 负责把 sensor 的数转化成应用层能直接消费的帧NPU 再对帧做检测、分类这些操作。IMX335 是个很经典的 500 万像素堆栈式 sensor原生分辨率 2592x19444:3 画幅。它在安防和工业相机里出现频率很高因为灵敏度不错、HDR 功能也够用而且成本控制得比较好。换到 STM32MP257F 这类 MPU 平台上IMX335 的出图管线通常走的是sensor - MIPI CSI-2 receiver - ISP或者是软件 ISP- 内存 - 应用。中间这层相机控制逻辑现在越来越多人用 libcamera 来接管。说实话libcamera 在 PC 和树莓派上已经比较成熟了但放到 STM32MP2 这种相对小众的嵌入式 BSP 里坑并不少。我这次就是在配置流的时候撞上了一个挺有代表性的问题应用启动时想要一路主码流和一路 640x640 的副码流结果 libcamera 直接抛了std::bad_alloc。这个异常扔出来的时候整个进程直接 terminate没有任何 recover 的机会非常让人头疼。1.2 报错现场和最小复现方式先说一下我当时的最小复现代码。核心就是 libcamera 的标准配置流程生成配置、改尺寸、配置 camera然后 allocate 缓冲#include libcamera/libcamera.h using namespace libcamera; int main() { CameraManager cm; cm.start(); auto camera cm.get(IMX335); if (!camera) return -1; auto config camera-generateConfiguration({ StreamRole::VideoRecording, StreamRole::Viewfinder, }); // 副码流640x640 config-at(1).size.width 640; config-at(1).size.height 640; config-at(1).bufferCount 4; auto status camera-configure(config.get()); if (status) { std::cerr configure failed: status \n; return -1; } FrameBufferAllocator allocator(camera.get()); for (StreamConfiguration cfg : *config) { Stream *stream cfg.stream(); if (allocator.allocate(stream) 0) { std::cerr allocate failed\n; return -1; } } std::cout configure ok\n; return 0; }代码看起来完全正常但编译跑起来之后终端输出的不是configure ok而是这样一行terminate called after throwing an instance of std::bad_alloc what(): std::bad_alloc说句实在话我第一次在嵌入式 Linux 上看到这个异常时第一反应就是“系统内存不够了”。但接着就会发现不对劲系统剩余内存明明还有几十 MB怎么配个相机流就能bad_alloc而且更奇怪的是把副码流改成 640x480 就很稳只有 640x640 会出问题。这个问题带出的线索非常多值得从 libcamera 的配置机制一层一层往下拆。2. libcamera 配置流时内存分配到底发生在哪一步2.1 从 generateConfiguration 到 configure 的完整路径要排查std::bad_alloc首先得搞清楚 libcamera 在配置流的整个过程中哪些操作会涉及内存分配。很多人以为只有FrameBufferAllocator::allocate()才会分配 buffer 内存但实际上在进入这一步之前系统已经在做大量的堆内存分配了。generateConfiguration()会创建Stream对象、用std::vector保存多个StreamConfiguration、还会在 camera 内部建立 pipeline handler 的上下文数据结构。这一阶段如果堆内存耗尽就会直接抛std::bad_alloc。接下来是camera-configure()。这是最重的阶段pipeline handler 要协商 sensor mode要调用 V4L2 subdev 和 video device 设置格式要给每个 stream 准备 FormatConverter 或者 ISP 的转换路径还要创建Request对象。每一步内部都涉及 STL 容器的扩容比如 controls 列表、metadata 区域的初始化。在这些阶段里std::bad_alloc一般不是malloc单块大内存失败而是std::vector的resize或std::map插入节点时new操作符抛出异常。所以如果你只盯着“是不是有一块巨型 buffer 没分出来”方向就偏了。2.2 三种 buffer 分配方式的内存来源libcamera 底层的 buffer 分配方式直接影响内存来自哪里、会不会失败。不同平台默认走的路子不一样常见有三种分配方式底层机制内存特性适用平台Mallocator普通 malloc非连续内存可能被 swap 到外部嵌入式一般没 swap默认万能方案PlatformFrameBufferAllocatordma-heap / ION一般是连续内存适合 DMA 设备推荐嵌入式平台使用V4L2 driver 内部 allocatevideobuf2、CMA/sg表由内核驱动管理失败时不一定抛 bad_alloc某些 pipeline 专用在 STM32MP257F 的 OpenSTLinux BSP 里默认的 pipeline 如果走的是软件转换或特定 vendor pipeline往往会通过 dma-heap 分配连续内存给 ISP 和 CSI-2 控制器使用。连续内存不够时dma_buf_alloc或者ioctl(DMA_HEAP_IOCTL_ALLOC)会返回错误码libcamera 通常会把错误包装成一个std::runtime_error或者返回 -1不一定直接抛std::bad_alloc。所以看到std::bad_alloc的时候我心里会先做个判断这更像是用户态堆分配失败而不是 DMA 连续内存分配失败。这在后面排查时非常关键。2.3 内存需求是怎么被放大的问题的核心其实是另一个东西sensor mode的选择。libcamera 的 pipeline handler 在配置多路流时会做一个关键决策——把 sensor 输出配置成什么分辨率。这个分辨率决定了“原始 capture buffer”的大小是内存占用的最大头。如果你以为副码流是 640x640那么内存占用就是 640x640 的两倍那就完全错了。一个典型的多流配置里内存结构是这样一个较大的 capture buffer从 sensor 读出来的原始数据分辨率等于 sensor mode而不是等于某个 stream 的输出分辨率。若干 conversion/output buffer经过 ISP 缩放或格式转换之后的结果分辨率等于各 stream 的配置值。我举个例子。假设 sensor mode 协商到了 2592x1944IMX335 输出 raw10 格式如果按常见的 raw10 unpacked每像素 16bit存进内存那这个 capture buffer 单帧大小就是2592 * 1944 * 2 10,077,696 字节 ≈ 9.6MB一次配 4 个 capture buffer就是接近 38.4MB 内存。相比之下640x640 的 output buffer 单帧才640 * 640 * 2 819200 字节 ≈ 0.8MB四路也就 3.2MB。如果这时候主码流又是 1920x1080 甚至更大整条管线的内存峰值很容易冲到 40MB 以上。这对于一个内存总量可能只有 512MB、还正在跑 NPU 推理和图形栈的嵌入式系统来说是一个不小的压力。所以要定位std::bad_alloc第一个要确认的问题就变成了当时的 sensor mode 到底协商到了多少3. 为什么 640x640 会踩中内存红线3.1 IMX335 的 sensor mode 与 libcamera 的选择策略IMX335 作为 4:3 sensor常见输出模式有完整的 2592x1944也有裁剪或者 binning 后的 1920x1080、1280x960、640x480 这些档位。但 640x640 这种 1:1 的正方形分辨率几乎不可能出现在 sensor 的原生 mode 列表里。libcamera 在配置多流时会计算所有 stream 中需要的最大宽和高然后尝试在 sensor 的 mode 列表里找一个“能完全覆盖这些 stream 需求”的模式。如果找不到严格匹配的就退而求其次选择更大的模式用 ISP 或者软件缩放把图像降下来。问题就出在这里。我当时增加了打印日志把StreamConfiguration里的最终尺寸和 pipeline handler 选择的 sensor format 打出来发现 sensor 输出被配置成了 2592x1944。也就是说为了给一路 640x640 的副码流做转换libcamera 强行拉起了 IMX335 全分辨率输出capture buffer 直接占了最大的档位。你可能会问为什么不选一个稍微小一点的 mode比如 1280x960然后缩到 640x640这就是 libcamera 内部策略的保守之处它首先保证所有流都能被满足宽比、高比、像素格式差异都考虑进去之后优先选那种肯定不缺尺寸的 mode。在某些 pipeline 实现里如果流配置里出现一个奇怪的宽高比它会干脆跳过“尝试匹配”的逻辑直接选最大分辨率减少出错概率。3.2 对照实验哪些配置正常哪些必现为了验证这个判断我做了几组对照实验。测试时保持主码流 1920x1080 不变只改副码流的分辨率主码流副码流sensor mode 实测结果1920x1080640x4801920x1080正常1920x1080800x6001920x1080正常1920x1080640x6402592x1944必现 bad_alloc1280x960640x6401280x960正常2592x1944640x6402592x1944必现 bad_alloc且更早崩溃这组实验结果非常有说服力。只要出现 640x640pipeline handler 就会把 sensor mode 拉到 2592x1944内存需求立刻暴涨。而像 640x480、800x600 这些常规分辨率sensor mode 保持在 1920x1080 就足够覆盖了内存压力小很多。这也能解释为什么是std::bad_alloc而不是“分配连续内存失败”因为用户态需要把多个 flow 的配置结构、请求队列、metadata 都放在堆里内存一下被大量的 capture buffer 挤占剩下的堆碎片无法满足后续某个容器的扩容请求C 运行时就抛了异常。3.3 非标分辨率在嵌入式视觉栈里的隐藏成本这件事让我意识到一个规律嵌入式视觉平台上非标分辨率尤其是 1:1、圆整到奇奇怪怪尺寸的额外成本是被很多人忽略的。你在 PC 上写代码显示器上最终显示 640x640 的窗口感觉不到底层发生了什么。但在嵌入式 Linux 上多流相机的内存布局是由 sensor mode 决定的而不是由你最终想要的那一路输出决定的。这个特性不只是 libcamera 有V4L2 多路复用、ISP 缩放管线基本都是这套逻辑。所以“配置一个 640x640 副码流”这个动作真实的内存成本可能是一个 2592x1944 的 raw capture buffer约 9.6MB主码流本身的 output buffer副码流经过 ISP 缩放后 640x640 的 output buffer约 0.8MB如果应用再开几路内存需求会线性叠加。这种藏在配置背后的隐性内存放大才是std::bad_alloc的真正导火索。4. 逐层排查从日志、gdb 到内核信息4.1 第一轮打开 libcamera 调试日志收集现场证据遇到这类问题第一件事应该是把 libcamera 的日志等级调高。libcamera 支持通过环境变量控制日志输出export LIBCAMERA_LOG_LEVELS*:DEBUG ./your_app日志打开后你会看到 pipeline handler 在配置阶段的详细输出。关键信息有两个StreamConfiguration最终被确认的尺寸和格式。V4L2VideoDevice::setFormat返回的实际图像尺寸包括 stride 和 sizeimage。这些日志能直接告诉你sensor 输出到底是什么分辨率、stride 对齐到了多少、每个 buffer 的 sizeimage 多大。我当时就是在日志里看到Size 2592x1944这个数字才意识到 capture buffer 被拉到了全分辨率。除此之外还能顺手确认系统内存的整体状况。std::bad_alloc出现不代表系统内存百分之百耗尽了有可能是瞬间峰值顶到了上限。所以同时要看几个关键指标cat /proc/meminfo | grep -E MemTotal|MemFree|MemAvailable|CmaTotal|CmaFree dmesg | grep -i cma如果MemAvailable已经掉到几十 MB 以下而CmaFree很低说明用户态堆和内核连续内存同时都吃紧了。4.2 第二轮用 gdb 抓异常抛出点std::bad_alloc在 C 里是异常可以被 gdb 捕获。不要直接run到崩溃再去看那样只能看到terminate的位置看不到真正抛异常的地方。正确做法是 catch throwgdb --args ./your_app (gdb) catch throw (gdb) run (gdb) btgdb 会在异常对象被抛出的那一刻停下来bt就是最原始的抛异常点。我这次的 backtrace 指向了libcamera内部的某个std::vector扩容操作具体是在 pipeline handler 保存流配置的容器里。这进一步证明了它不是某一块大 buffer 的分配失败而是在构建配置结构时堆内存紧张导致的。如果你在 gdb 里跑比较麻烦也可以把日志调成 DEBUG 后在 libcamera 源码里暂时加fprintf或者用gdb在operator new上打断点。但这只能作为临时手段生产环境不建议这么做。4.3 第三轮通过 media-ctl 确认实际 sensor format既然日志里已经看到了 2592x1944再用 media-ctl 确认一下当前的 media graph 状态media-ctl -p /dev/media0这个命令会把整个摄像头拓扑打印出来包括各个 entity 当前的 format。重点看 IMX335 对应的 subdev 节点当前的 mbus 格式是多少。如果显示的确实是 2592x1944那就可以坐实“sensor mode 被拉升到了全分辨率”这个判断。另外还要注意一个细节sensor 输出的 raw 格式可能是 raw1010bit但到了内存里往往按 16bit 对齐。不同驱动、不同 pipeline handler 对这个处理不一样所以sizeimage可能比理论计算值大。查看日志里V4L2VideoDevice返回的具体sizeimage字节数比你自己算更准确。4.4 第四轮减小 bufferCount 做压力测试为了进一步确认是“内存峰值过高”而不是“单块分配太大”我把 bufferCount 从 4 降到 2config-at(1).bufferCount 2;此时 bad_alloc 没再出现。再把系统 CMAsize 缩小一半bad_alloc 又回来了。反向再加cma128M情况好一些但应用只要跑满 4 路 buffer日志还是会出现内存告警。这套压力测试的结果说明问题本质是内存预算不够而不是某一路配置写错了参数。5. 解决方案与最终配置5.1 方案 A让 libcamera 使用接近目标分辨率的 sensor mode既然根因是 sensor mode 被拉到 2592x1944最直接的思路就是让 pipeline handler 不要选那么大。有两种做法一种是改应用层把副码流的目标尺寸调整成传感器支持且比例合适的 mode比如 1280x960 或 640x480。但这要看业务允不允许640x640 是算法侧要求的话就不能改。另一种是改 pipeline handler 的 mode 选择逻辑或者通过 media-ctl 预先把 sensor 的 crop 窗口设置好强制 sensor 输出一个更小的区域。IMX335 这类 sensor 一般都支持 ROI 裁剪通过 subdev 的 selection API 设置 crop 矩形让输出分辨率直接就是 640x640 附近的值。一个参考命令是这样的实际节点名和坐标要根据 media graph 来定# 先确认 sensor 的 subdev 节点名 media-ctl -p /dev/media0 # 设置 crop 窗口让 sensor 输出中心 640x640 区域 media-ctl -d /dev/media0 --set-v4l2 IMX335 0-001a [crop:(976,652)/640x640]不过需要提醒的是让 sensor 直接输出 640x640 对 STM32MP257F 的 CSI-2 接收器和 ISP 来说并不是所有版本 BSP 都支持得那么顺。有的 ISP 输入要求宽高对齐到 64 或者 256 的倍数640 虽然能对齐到 64但要对齐到 256 就麻烦了这时候反而要配更大的 sensor mode。所以采用这个方案前一定要先查 ISP 的输入对齐要求。5.2 方案 B给系统预留足够的内存预算在不能立刻改 pipeline 逻辑的情况下最稳妥的办法就是给系统多留内存。STM32MP257F 上调整 CMA 是通过内核命令行参数完成的。在 extlinux.conf 或者 u-boot 的 bootargs 里追加cma128M重启后确认dmesg | grep -i cma cat /proc/meminfo | grep -E CmaTotal|CmaFree如果默认 CMA 只有 64MB把 128M 设进去内存压力会缓解很多。需要注意CMA 是从 DDR 总量里划出来的加它会减少匿名内存和页缓存可用量。如果你的系统总共只有 256MB DDR划 128M 给 CMA用户态可用内存反而更少可能加剧std::bad_alloc。这时要权衡一下或者把 bufferCount 降下来或者优化其他模块的内存占用。我当时的实际经验是512MB DDR 的板子cma128MbufferCount3的组合能让 2592x1944 主码流 640x640 副码流稳定跑上一整晚不再出现 bad_alloc。但如果只调 CMA、不控制 buffer 数量峰值内存仍然偏高偶发问题还是会冒头。5.3 方案 C减少每个流的 buffer 数量减少 bufferCount 是最容易做、见效也最明显的手段但也最容易引入延迟问题。相机管线里 buffer 太少意味着生产者sensor/ISP和消费者NPU/应用之间没有足够的缓冲来吸收时间抖动。如果应用处理一帧的时间不稳定bufferCount2 就很容易出现等待空帧的情况表现就是应用卡顿、丢帧。所以我建议把 bufferCount 的调整当作“短期止血”不要当作长期配置。至少配 3~4 个 buffer再配合其他内存优化手段来解决。这里贴一个我最后能稳定运行的配置示例config-at(0).size { 1920, 1080 }; config-at(0).bufferCount 4; config-at(1).size { 640, 640 }; config-at(1).bufferCount 3;5.4 终极验证跑满所有 stream 后观察内存曲线修复之后我专门写了一个压力脚本让相机应用连续跑 12 小时每隔 30 秒记录一次/proc/meminfo和libcamera日志里sizeimage的变化。同时用top观察应用进程的 RSS 峰值。最终确认sensor mode 保持合理档位没有直接输出 2592x1944除非主码流本身需要CMA 剩余维持在 30MB 以上应用 RSS 峰值稳定在预期范围没有再出现突刺到这一步“配一个 640x640 副码流就 bad_alloc”的问题才算真正关闭。6. 嵌入式平台调 libcamera 与内存的避坑指南6.1 常见错误速查表现象可能原因处理方向std::bad_alloc用户态堆内存不足 / 内部容器扩容失败抓 backtrace、看 sensor mode、减 bufferCount、增大 CMAFailed to allocate buffersdma-heap 或 videobuf2 连续内存分配失败检查 CMA、确认 dma-heap 设备节点存在Invalid argumentstream 尺寸超出 sensor/ISP 能力查看 sensor mode 列表、调整 crop 或格式画面卡顿、丢帧bufferCount 太低或内存带宽不足增加 bufferCount、降低分辨率、检查 DDR 频率配置成功后首帧全黑ISP 参数未初始化或 pipeline 转换错误检查 ISP 日志和 V4L2 format 是否匹配6.2 排查这类问题的方法论遇到嵌入式相机栈的内存问题我的建议是按照下面这个顺序走而不是一上来就调内核参数先复现并尽量缩小范围。把所有能砍的流都砍掉只留触发问题的那个 640x640 流看是否必现。这样能分辨是单个流的问题还是多流叠加的问题。开 DEBUG 日志拿到sensor mode和sizeimage。这是第一手证据。用 gdb 抓异常抛出点判断是用户态堆分配失败还是连续内存分配失败。做对照实验改变分辨率、bufferCount、CMA 大小每次只变一个变量。等确认根因后再考虑是改应用、改内核参数还是改 pipeline handler。这套流程看起来很基础但很管用。我见过太多人在没搞清楚 sensor mode 实际值的情况下就疯狂调大 CMA结果系统内存反而更紧张问题更难查。6.3 两个容易被忽视的细节第一个是stride对齐。libcamera 配置流时bytesperline不一定等于width * bpp / 8V4L2 驱动会按平台的 burst 对齐要求做向上取整。如果 stride 从 2560 对齐到 2688那实际每帧大小会比理论值大 5% 左右。计算内存预算时一定要用sizeimage的真实返回值。第二个是 IMX335 的 raw 输出格式。某些 pipeline 会把它从 raw10 packed 变成 raw10 unpacked16bit/pixel内存占用直接翻倍。如果看到sizeimage比你按 10bit 算出来的大别惊讶这是常见操作。7. 最后的经验这件事最后修复完我最大的感触是嵌入式 Linux 相机栈里真正决定内存峰值的往往不是“你想输出的分辨率”而是“sensor 实际输出的分辨率”。640x640 这种非标宽高比在 PC 上只是一个窗口大小的问题在 libcamera 的配置流程里却被转换成了传感器全分辨率输出的额外负担。以后再碰到std::bad_alloc我建议先别急着加内存先把 sensor mode、sizeimage、bufferCount 这三个数字打出来看一眼很多时候根因就在这几行日志里。如果你跟我一样在 STM32MP257F 这类平台上做视觉开发建议把 libcamera 版本和 BSP 里对应的 pipeline handler 实现锁定下来不要轻易升级。这类平台上的 libcamera 支持还在快速迭代不同版本对 mode 选择的策略可能完全不同。锁版本能保证你在解决一个问题的同时不会因为栈的变动冒出新的问题。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门