☰
STM32H747图像分割可视化:DMA2D显示链路实战指南
2026/9/29 18:13:36 网站建设 项目流程

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 放在 SDRAM,DMA2D 同时也在访问 SDRAM,两者之间会有总线竞争。遇到画面撕裂、刷新卡顿,不一定是 DMA2D 配置错了,很多时候是总线调度和刷新时序的问题。

2.2 软件配置

开发环境常见搭配是 STM32CubeMX + STM32CubeIDE。CubeMX 里需要打开这些外设:

  • DMA2D;
  • LTDC;
  • FMC,用于访问 SDRAM;
  • DCMI 或 SD 卡读取接口;
  • 串口,用于调试打印。

模型推理部分可以使用 STM32Cube.AI 转换后的网络,也可以使用 TFLite Micro。这里要记住一个原则:对显示链路来说,模型具体是 U-Net、SegNet、DeepLabV3 还是轻量级分割网络,影响并不大,DMA2D 只认最终输出的 label buffer 地址、宽度、高度和颜色格式。

所以调试阶段不要马上把模型推理接进去。先用一段固定假数据填充 label buffer,确认 DMA2D 可视化链路是通的,再让模型进来。很多项目把问题搞复杂,就是因为模型和显示同时调试,报错时根本分不清是推理错还是显示错。

2.3 显存规划是最容易低估的一步

在 DMA2D 可视化场景里,内存规划比写寄存器更重要。先按 640x360 分辨率做一个粗略估算,你就能明白为什么“看起来不大的图片”会在嵌入式里瞬间吃掉几百 KB。

表格:按 640x360 分辨率估算

缓冲区作用格式大小估算
原始图像帧摄像头采集或解码后的画面RGB565约 450 KB
Label Buffer模型输出的类别索引uint8约 225 KB
Overlay 颜色层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,原图当 background;
  • DMA2D 最终把混合结果写入目标 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 步:先用固定假标签图验证显示链路

拿到程序后,不要立刻跑模型。先用代码填入一块固定 label:

for (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 准备好后,把原来的原始图像作为 background,overlay 作为 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 是 ARGB8888,DMA2D 却按 RGB565 去读,颜色自然乱。建议在代码里固定用同一个宏定义,避免一个地方改成格式、另一个地方忘了同步。

5.3 位置不对或画面斜切,优先查行宽和步长

DMA2D 搬运的是矩形区域,需要同时告诉它一行的像素个数和总共多少行。如果只改了缓冲区总大小,却没有设置正确的 stride,画面会一条一条地斜着错位。

另外,某些行可能带 padding。比如你申请的缓冲区一行实际占 640 * 2 字节用于 RGB565,但 DMA2D 配置时把行偏移写错,就会出现错行。

排查这类问题,先打印每个缓冲区的首地址,确认内存没有覆盖。然后检查 DMA2D NLR 中 PL 和 NL 的配置。

5.4 刷新卡顿或闪屏,先看总线、缓存和同步

画面能出,但一跑实时视频就闪烁、卡顿,常见原因有四种:

  • DMA2D 和 LTDC 同时访问 SDRAM,造成带宽抢占;
  • framebuffer 切换没有在 VBlank 完成;
  • Cache 未做 Clean 或 Invalidate;
  • overlay 转换速度过慢,拖慢了整个周期。

尤其要提一下 Cache。CPU 写 overlay 后会先把数据留在 Cache 中,DMA2D 去读 SDRAM 时可能读到旧数据。解决方式是在 DMA2D 启动之前,对 overlay 所在地址范围做 Cache Clean:

SCB_CleanDCache_by_Addr((uint32_t *)overlay_layer, size);

如果是 DMA2D 把结果写进 framebuffer,再由 LTDC 读取,DMA2D 写入的内容也可能在 Cache 里不可见。这时需要做 Invalidate:

SCB_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 能力不够,而是前置环境和调试顺序没有整理清楚。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询