
STM32H747 上做图像分割模型结果可视化能不能把分割区域直接看得懂关键往往不在模型推理而在 DMA2D 这一条显示链路是否已经理顺。很多刚接触嵌入式视觉的人会默认“模型输出就是一张图”其实不是。语义分割模型返回的是逐像素的分类编号不是 RGB 画面这些编号必须经过颜色映射、图层叠加、格式转换最后才能落到屏幕上。如果这些工作全让 CPU 干帧率会很难看如果 DMA2D 用对了CPU 的负担会小很多画面也能稳定刷新。这个主题适合正在做 STM32 视觉应用、嵌入式模型部署或者想搞懂 DMA2D 在实际项目中怎么用的人。下面我会按实际落地顺序拆一遍包括硬件条件、显存估算、DMA2D 配置思路、验证方式以及最常踩的几个坑。1. 先定位问题DMA2D 在这个可视化场景里到底解决什么1.1 图像分割模型的输出和摄像头画面有本质差异摄像头采集到的是一帧 RGB 或 YUV 图像屏幕显示它的时候不需要太多额外解释因为每个像素都已经有明确颜色。图像分割模型则不同。模型对输入图像做像素级分类最终输出通常是一个二维标签图。图上每个位置的数值代表“这个像素属于哪个类别”比如 0 代表背景1 代表道路2 代表车辆3 代表行人。对嵌入式端来说为了控制内存和带宽部署时一般会把模型输出转成 8bit 或 16bit 的 label buffer而不是保留一整份浮点概率图。问题出现在这里屏幕不理解类别编号它只关心每个像素的颜色。如果把 label buffer 直接当 RGB 数据去显示结果大概率是乱码或者一屏灰阶人眼无法快速区分目标区域。所以我一直觉得嵌入式图像分割的可视化本质上不是“把结果显示出来”而是“把类别索引翻译成人眼能识别的颜色”。DMA2D 在这里扮演的就是搬像素、换颜色、做透明叠加重构的核心角色。1.2 可视化需要完成三段转换先别急着写代码。把可视化链路拆开其实只有三段第一段把 label buffer 里的类别编号映射成颜色。这一步可以靠 RGB 调色板完成。每一类分配一个 ARGB8888 色值例如0背景半透明黑色1类别 A半透明绿色2类别 B半透明蓝色。第二段把颜色图层和原始摄像头帧做叠加。叠加通常采用 alpha blending。默认 alpha 设置为 0x7F 左右也就是约一半透明度这样既能看到语义分割色块也能看到原始画面里的真实物体边界。第三段把混合结果放到 LTDC 正在扫描的 framebuffer 中。这一步可以由 DMA2D 直接完成。如果不做分层也可以直接把混合结果写进屏幕缓冲区然后让 LTDC 刷出来。流程看起来很简单但实际工程里的内存布局、缓存一致性、行偏移、颜色格式、DMA2D 寄存器配置每一步都可能让结果完全反直觉。1.3 DMA2D 不只是“贴图加速器”DMA2D 在很多人印象里是“图像搬运工”甚至会把它理解成一种简单的 DMA。真正用起来会发现它比普通 DMA 强在三个点上支持像素格式转换支持内存到内存的混合能够在搬运的同时处理透明度。拿图像分割可视化来说DMA2D 拿到的不一定是一张完整 RGB 图而是一块 label 数据或调色板后的 overlay 数据。它可以作为 foreground 层和 background 层自动做混合最终输出到目标内存。整个过程不需要 M7 核逐像素去算CPU 只要填好寄存器、启动 DMA2D、等待完成中断就行。换句话说DMA2D 在这里是“像素级流水线”解决的是 CPU 在逐像素循环里浪费大量主频的问题。主频省下来了才能留出时间给下一帧模型推理。2. 跑一次完整 Demo 需要的基础条件2.1 硬件层面如果目标平台是 STM32H747最好确认板上具备这些条件主控带有 DMA2D 和 LTDC这是显示链路的基础有一块 LCD 屏幕分辨率建议从 320x240 或 640x360 级别起步外部 SDRAM 容量足够至少 4MB 会比较从容摄像头或静态图片输入可以通过 DCMI、SD 卡或预先放在外部 Flash 的测试图完成。有人会问STM32H747 内部不是有 RAM 吗能不能不接 SDRAM如果只是做很小的分割图内部 RAM 确实可能够但只要把摄像头帧缓冲、模型输入、label buffer、overlay buffer、LTDC framebuffer 全部算上几百 KB 的真实需求会瞬间逼近芯片内部 RAM 上限。所以外部 SDRAM 几乎是必须的。显示部分需要注意LTDC 是一个持续扫描的显示控制器它会不断从 framebuffer 读取数据。如果 framebuffer 放在 SDRAMDMA2D 同时也在访问 SDRAM两者之间会有总线竞争。遇到画面撕裂、刷新卡顿不一定是 DMA2D 配置错了很多时候是总线调度和刷新时序的问题。2.2 软件配置开发环境常见搭配是 STM32CubeMX STM32CubeIDE。CubeMX 里需要打开这些外设DMA2DLTDCFMC用于访问 SDRAMDCMI 或 SD 卡读取接口串口用于调试打印。模型推理部分可以使用 STM32Cube.AI 转换后的网络也可以使用 TFLite Micro。这里要记住一个原则对显示链路来说模型具体是 U-Net、SegNet、DeepLabV3 还是轻量级分割网络影响并不大DMA2D 只认最终输出的 label buffer 地址、宽度、高度和颜色格式。所以调试阶段不要马上把模型推理接进去。先用一段固定假数据填充 label buffer确认 DMA2D 可视化链路是通的再让模型进来。很多项目把问题搞复杂就是因为模型和显示同时调试报错时根本分不清是推理错还是显示错。2.3 显存规划是最容易低估的一步在 DMA2D 可视化场景里内存规划比写寄存器更重要。先按 640x360 分辨率做一个粗略估算你就能明白为什么“看起来不大的图片”会在嵌入式里瞬间吃掉几百 KB。表格按 640x360 分辨率估算缓冲区作用格式大小估算原始图像帧摄像头采集或解码后的画面RGB565约 450 KBLabel Buffer模型输出的类别索引uint8约 225 KBOverlay 颜色层label 映射后的半透明色块ARGB8888约 900 KB混合输出 / LTDC 帧缓冲最终显示画面RGB565 或 ARGB8888约 450 KB ~ 900 KB这里还不是完整工程的全部数据因为模型推理过程中通常还有输入张量和中间层张量。如果再做双缓冲、三缓冲内存总量会继续往上涨。所以建议第一步先把分辨率定下来再根据格式计算每块缓冲的大小然后规划地址。一个比较稳妥的做法是把 RAM 按 Bank 划分模型权重和中间张量放一边显示类缓冲区放另一边避免模型推理的大块 DMA 访问和 DMA2D 读写互相干扰。是否一定要分开取决于芯片内部 RAM 和 SDRAM 的分配方式但至少逻辑上要有清晰的物理地址边界。3. DMA2D 可视化的三个核心动作3.1 Label 到颜色层不同芯片可以走不同路径把 label buffer 变成颜色层有两条路路径 A如果当前芯片的 DMA2D 支持索引色或 LUT 转换你可以尝试把 label buffer 作为输入让 DMA2D 查调色板后输出 ARGB8888。这条路最省 CPU但不是所有系列的所有型号都保证支持使用前必须查对应参考手册看 DMA2D 是否支持 L8/L4 输入及调色板寄存器。路径 B用 CPU 查表生成颜色层。这是最稳妥的通用做法。代码如下typedef struct { uint8_t a; uint8_t r; uint8_t g; uint8_t b; } ARGB_Color; static const ARGB_Color palette[4] { {0x00, 0x00, 0x00, 0x00}, // 0: 背景全透明 {0xA0, 0x00, 0xE0, 0x00}, // 1: 类别1半透明绿 {0xA0, 0x00, 0x00, 0xF0}, // 2: 类别2半透明蓝 {0xA0, 0xF0, 0x00, 0x00}, // 3: 类别3半透明红 }; void convert_label_to_overlay(const uint8_t *label, uint32_t *overlay, uint32_t pixel_count) { for (uint32_t i 0; i pixel_count; i) { uint8_t idx label[i]; if (idx 4) { idx 0; } overlay[i] (palette[idx].a 24) | (palette[idx].r 16) | (palette[idx].g 8) | palette[idx].b; } }如果你有摄像头图像后面可以把背景 alpha 设成 0x00这样不会遮挡原始画面如果希望背景也能覆盖一层颜色做“高亮”可以改成 0x60 之类的半透明值。这里需要注意overlay 是 ARGB8888 时按 640x360 估算约 900 KB。如果内存紧张也可以改成 RGB565半透明效果会有所损失同时 DMA2D 混合时 Alpha 通道还需要单独处理。建议第一次跑通优先用 ARGB8888稳定后再做内存优化。3.2 颜色层和原图的半透明叠加得到 overlay 颜色层后下一步就是把 overlay 和原始图像帧合成。DMA2D 在这种场景下非常适合做 foreground/background 混合。DMA2D 混合模式的原理不复杂两块输入一块作为 foreground一块作为 background。DMA2D 对每个像素按公式计算最终颜色 foreground颜色 * alpha系数 background颜色 * (1 - alpha系数)在实现时有两种选择把 overlay 当 foreground原图当 backgroundDMA2D 最终把混合结果写入目标 framebuffer。配置时只需要指定 foreground 地址、background 地址、目标地址、行宽、行数以及格式。整体流程类似这样DMA2D-CR DMA2D_M2M_BLEND; // 示意具体值以芯片参考手册为准 DMA2D-FGMAR (uint32_t)overlay_layer; DMA2D-BGMAR (uint32_t)camera_frame; DMA2D-OMAR (uint32_t)display_layer; // 设置 foreground 格式和 alpha // 设置 background 格式和 alpha // 设置 NLR像素行长度 行数 DMA2D-CR | DMA2D_CR_START; while ((DMA2D-ISR DMA2D_FLAG_TC) 0) { }C 代码里的寄存器字段名在不同 STM32 系列之间有差异甚至同一系列不同型号也有差异。所以不要直接照抄网上的某段配置拿到板子后先看参考手册或者用 CubeMX 生成的 HAL 驱动代码作为基础再改 DMA 输入地址和格式。生成好的 overlay 会直接叠加在原图上。模型认为属于某个目标的像素会变成半透明色块不是目标的区域保持原图颜色。这样展示出来的结果比较直观也方便在评测阶段对比模型输出和真实物体的边缘。3.3 理解内存搬运的代价DMA2D 看起来“很快”但它的本质仍然是内存到内存的搬运和计算。在 SDRAM 上做 ARGB8888 图层混合每一帧都要读写几 MB 数据。如果还开了多个 framebuffer那么 CPU、DMA2D、LTDC 三者在同一时刻抢总线速度会下降。理解这一点对调试特别有用。有些人不断降低模型输入分辨率发现画面还是很卡最后定位才发现瓶颈不在模型而在 DMA2D 把一块很大的 overlay 从 SDRAM 读出来又写回去了。此时更合理的做法是降低显示缓冲区数量、选择更小的 overlay 尺寸或者把不需要的图层裁掉。4. 从模型输出到屏幕的操作顺序4.1 第 1 步先用固定假标签图验证显示链路拿到程序后不要立刻跑模型。先用代码填入一块固定 labelfor (uint32_t i 0; i LABEL_PIXEL_COUNT; i) { label_buffer[i] 0; } // 在某个矩形区域内标成类别 1 for (uint32_t y 60; y 120; y) { for (uint32_t x 80; x 240; x) { label_buffer[y * width x] 1; } }如果显示链路的 DMA2D 混合和调色板正确屏幕中央会看到一个半透明色块位置、颜色都不会偏差。这一步通过以后再接入模型推理。我的习惯是先不放大模型。设置一张 RGB565 原图固定画几个色块作为“假摄像头帧”然后动态修改 label 区域观察半透明叠加是否实时更新。4.2 第 2 步确认调色板和模型类别一一对应很多分割项目会犯同一个错误模型训练时定义了 6 个类别代码里的调色板却有 8 个颜色或者类别顺序反了。看起来好像“分割错了”其实是可视化 LUT 和训练标签对不上。所以第二步要把模型训练时的 class mapping 列出来再和代码中的 palette 对齐。比如0背景1人2车辆3道路。那么 palette 下标 0~3 就得分别对应这四个类别。写一个串口打印函数统计 label buffer 中出现了哪些类别 ID以及每一类的像素数量。如果模型输出里出现了 palette 范围之外的 ID第一步就把它归零或标记出来千万不要让它越界访问调色板。4.3 第 3 步将 label buffer 转成 overlay这一步既可以用 DMA2D 的索引转换能力也可以用 CPU 查表。从工程稳定度来讲第一次跑通建议用 CPU 查表因为代码简单、可定位、没有那么多寄存器依赖。转换时可以拆成小段避免对超大 buffer 一次性循环。比如按行循环每处理 16 行后让另一个缓冲区先送 DMA2D这样能降低长阻塞。不过如果只是验证功能一次性处理 640x360 的 buffer 问题也不大。4.4 第 4 步触发 DMA2D 混合当 overlay 准备好后把原来的原始图像作为 backgroundoverlay 作为 foreground执行 DMA2D 混合。如果显示模块本身支持多个图层也可以不混合直接把两层配置到 LTDC 的不同 layer让 LTDC 在扫描时实时混合。需要注意LTDC 层混合和 DMA2D 混合在视觉结果上类似但使用场景略有不同。DMA2D 混合适合“生成一帧结果之后再做其他图像处理或保存”LTDC 多层则更适合实时预览不占用额外 framebuffer。资源紧张时可以优先考虑用 LTDC 的 layer 0 显示原图、layer 1 显示 overlay。4.5 第 5 步解决刷新撕裂问题显示图像分割结果时如果 DMA2D 正在修改 framebuffer而 LTDC 同时正在扫描同一块内存就会产生上半个屏幕是上一帧、下半个屏幕是下一帧的情况。解决办法是使用帧同步等 LTDC 进入 VBlank 周期后再切换 DMA2D 的目标 buffer或者先写到后台 buffer等 VBlank 到来再把后台 buffer 地址交给 LTDC。如果只画单帧做静态验证可以不考虑撕裂。一旦要实时显示模型分割结果就必须把双缓冲、Ping-Pong Buffer 和帧同步做成标配。5. 怎么判断结果是否正常以及排查顺序5.1 正确结果应该长什么样判断可视化链路正常可以从这几个维度看原画面内容仍然可见没有整片黑屏、花屏被模型判定为目标的区域覆盖了半透明颜色颜色映射与调色板预设一致色块边缘和原图中真实对象的边缘对齐连续刷新时没有明显撕裂或残留拖影。只要有一条不满足就需要按顺序排查。5.2 颜色不对先查调色板和格式如果画面已经能显示但颜色和预设完全对不上优先看三处label buffer 里的类别 ID 是否越界palette 数组的顺序和训练类别顺序是否一致DMA2D 输入格式和实际 buffer 格式是否一致。比较常见的情况是输入格式写错。CPU 生成的 overlay 是 ARGB8888DMA2D 却按 RGB565 去读颜色自然乱。建议在代码里固定用同一个宏定义避免一个地方改成格式、另一个地方忘了同步。5.3 位置不对或画面斜切优先查行宽和步长DMA2D 搬运的是矩形区域需要同时告诉它一行的像素个数和总共多少行。如果只改了缓冲区总大小却没有设置正确的 stride画面会一条一条地斜着错位。另外某些行可能带 padding。比如你申请的缓冲区一行实际占 640 * 2 字节用于 RGB565但 DMA2D 配置时把行偏移写错就会出现错行。排查这类问题先打印每个缓冲区的首地址确认内存没有覆盖。然后检查 DMA2D NLR 中 PL 和 NL 的配置。5.4 刷新卡顿或闪屏先看总线、缓存和同步画面能出但一跑实时视频就闪烁、卡顿常见原因有四种DMA2D 和 LTDC 同时访问 SDRAM造成带宽抢占framebuffer 切换没有在 VBlank 完成Cache 未做 Clean 或 Invalidateoverlay 转换速度过慢拖慢了整个周期。尤其要提一下 Cache。CPU 写 overlay 后会先把数据留在 Cache 中DMA2D 去读 SDRAM 时可能读到旧数据。解决方式是在 DMA2D 启动之前对 overlay 所在地址范围做 Cache CleanSCB_CleanDCache_by_Addr((uint32_t *)overlay_layer, size);如果是 DMA2D 把结果写进 framebuffer再由 LTDC 读取DMA2D 写入的内容也可能在 Cache 里不可见。这时需要做 InvalidateSCB_InvalidateDCache_by_Addr((uint32_t *)display_layer, size);如果实际项目开启了 MPU并且把 SDRAM 设成了 Cacheable这个环节尤其容易踩坑。无输出、画面花、颜色奇怪很多都和 Cache 一致性问题有关。6. 从 Demo 走向实际项目几点工程化建议6.1 把显示链路和模型推理彻底解耦图像分割结果可视化这个 Demo最容易出现的问题不是 DMA2D 配置难而是把模型、摄像头、显示捆在一起调试。建议从第一天就把工程拆成三个模块数据源模块输出一帧 RGB565推理模块输入一帧图像输出 label buffer显示模块输入 label buffer 和 RGB565输出屏幕画面。模块之间用内存指针连接。这样即使模型还没部署显示模块也一样可以开发。真正常态运行时也方便定位如果画面卡住先看推理模块是否超时再看显示模块是否在等待 DMA2D。6.2 尽量先让显示区域和模型输出分辨率解耦模型输出分辨率未必等于显示分辨率。比如模型只在 96x96 的标签图上输出可显示区域是 640x360。这时候不要直接把 96x96 的 label buffer 接到 DMA2D 上做全屏混合否则画面会很小密集的区域都挤在屏幕一角。更合理的做法是在推理后先做一次最近邻插值或双线性插值把 label 放大到显示区域大小再执行调色板转换和 DMA2D 叠加。插值会增加 CPU 负担。如果目标屏幕分辨率大而模型输出小性能瓶颈很可能会从 DMA2D 转移到放大插值这一步。选择算法时优先考虑最近邻因为它最省时间如果视觉质量要求高再尝试双线性。6.3 不同分割模型显示流程可以复用不管底层用 U-Net、SegNet、DeepLabV3 还是轻量化 MobileNet 变体DMA2D 看到的只是 label buffer。实际项目中医学图像分割、广告牌区域分割、工业缺陷分割等场景显示链路都很类似。区别只在于调色板颜色、类别数量、是否需要叠加原图、是否需要在边缘描边。所以第一版流程一旦跑通后续换模型、换任务改动成本会很低。如果你做的场景是医学图像分割通常更关注病灶区域的轮廓和透明度类颜色可以采用红黄这类高对比方案。如果你做的是道路场景分割背景 alpha 要低一些避免遮挡车道线。6.4 更复杂效果的实现思路有些项目希望分割区域更明显可以再给 overlay 增加 1~2 像素的描边。描边的实现方式通常是在空域找 label 边缘然后把边缘像素改成白或黑。这个边缘检测在小型 MCU 上可以用简单梯度判断当前像素和右边像素类别不同则当前像素标记为边缘当前像素和下边像素类别不同则当前像素标记为边缘。标记结果可以直接写成一个边缘 mask再通过 DMA2D 把它叠加到已有画面上。效果类似很多图像分割工具里的高亮描边但实现成本不高。如果帧率仍然有富余也可以做一些平滑处理让半透明色块的锯齿边界更柔和。整体优化顺序建议是先分层再缓存最后填色。最后说几句很多人第一次在 STM32H747 上做图像分割可视化会以为难点在模型部署。实际跑一轮就会明白模型把 label 输出后真正的工程细节才开始调色板要稳定、格式要对齐、Cache 要刷新、VBlank 要同步只要其中一个环节没处理好画面要么乱、要么闪、要么直接黑屏。我个人的建议还是先把单个帧链路跑通用一块假 label 测出稳定结果再接模型。DMA2D 本身并不神秘它只是把像素操作从 CPU 手里接了过来。真正要花时间准备的是内存规划、输入输出约束和整套显示链路的一致性。踩过几次坑之后你会发现很多问题不是 DMA2D 能力不够而是前置环境和调试顺序没有整理清楚。