STM32F767 + RT-Thread + LVGL:DMA2D「渲染 + 传输」双加速踩坑实录(根因剖析 + 性能实测)
硬件平台:野火 挑战者 STM32F767(Cortex-M7 @ 216MHz)+ 5 寸 800×480 RGB 电容屏 软件平台:RT-Thread + LVGL v8.3 + SDRAM 帧缓冲 + LTDC + DMA2D(Chrom-ART)
本文记录一次完整的 DMA2D 加速实验:单独开启「渲染加速」或「传输加速」一切正常,两者同时开启却出现画面闪烁、对角斜带、色块变形。文中从寄存器层面剖析根因、给出修复方案,并用 2×2 对照实验定量测出四种组合的性能差异。
目录
一、显示链路与实验背景
先从整体上理清这块板子的图形链路,后面所有问题都发生在这条链路上:

每一帧的完整生命周期分两个阶段:
- 渲染阶段:LVGL 把控件画进绘制缓冲(draw_buf),涉及大量的矩形填充、图片搬运、透明度混合——这是「画」的过程;
- 传输阶段:把绘制缓冲的内容搬进 LTDC 正在扫描的帧缓冲(front_buf / back_buf),然后切换 CFBAR 并在垂直消隐期间(VBR 重载)生效——这是「送显」的过程。
这两个阶段都可以让 DMA2D 代劳,这正是本实验的主题:分别开、同时开,各是什么效果?
测试界面如下(正弦滚动波形 + 红/绿/蓝三色条 + 底部白线,三色条用于检查颜色通道是否错位,白线用于检查闪烁):

二、DMA2D 关键原理速览
DMA2D(Chrom-ART 加速器)是 ST 内置于部分 MCU 的 2D 图形加速器,它挂在与 CPU 同级的 AHB 总线上,能独立完成矩形填充、图像拷贝、颜色格式转换和 Alpha 混合。理解后面的问题,只需要抓住两张表。
2.1 四种工作模式(CR 寄存器 MODE 字段)
| R2M | 寄存器→存储器,纯色填充 | 清屏、画纯色矩形 |
| M2M | 存储器→存储器,原样搬运 | flush 时把 draw_buf 拷进帧缓冲 |
| M2M_PFC | 搬运 + 像素格式转换 | RGB565 图片贴到 ARGB8888 画布 |
| M2M_BLEND | 前景层与背景层混合后输出 | 带透明度的贴图、UI 控件叠加 |
2.2 与本文强相关的寄存器
| CR | 模式(MODE)、启动位(START) | HAL_DMA2D_Init() 写 MODE |
| OPFCCR | 输出颜色格式 | HAL_DMA2D_Init() |
| OOR | 输出行偏移(Output Offset) | HAL_DMA2D_Init() |
| FGPFCCR / FGOR | 前景层输入格式 / 输入行偏移 | HAL_DMA2D_ConfigLayer(h, 1) |
| BGPFCCR / BGOR | 背景层输入格式 / 输入行偏移 | HAL_DMA2D_ConfigLayer(h, 0) |
| FGMAR / BGMAR / OMAR | 前景 / 背景 / 输出地址 | HAL_DMA2D_Start() 只写这三个 + NLR |
| NLR | 每行像素数(PL)、行数(NL) | HAL_DMA2D_Start() |
这张表是后面根因分析的钥匙,请先记住两个事实:
事实 1:HAL_DMA2D_Start() / HAL_DMA2D_Start_IT() 只写 NLR、OMAR、FGMAR 三个寄存器,然后置 START 位。它不会重写 MODE、输出格式、行偏移——这些寄存器里保留的是上一次操作留下的值。
事实 2:HAL 库中 图层 0 = 背景层(DMA2D_BACKGROUND_LAYER),图层 1 = 前景层(DMA2D_FOREGROUND_LAYER)。M2M 模式的源地址由 Start() 写入 FGMAR(前景层),所以源侧的格式和行偏移必须用 HAL_DMA2D_ConfigLayer(&hdma2d, 1) 配置——这个「1 是前景」的反直觉设定是 ST 官方 HAL 头文件里白纸黑字的定义,也是本案例埋的坑之一。
2.3 行偏移(OOR / FGOR)机制——理解"斜带"的关键
DMA2D 传输的是二维矩形,但内存是一维的。DMA2D 每传完一行,地址自动跳到下一行起点:
输出地址增量 = (NLR.PL + OOR) × 每像素字节数
↑每行有效像素 ↑行与行之间需要跳过的像素
当把一个 W×H 的脏矩形拷贝到宽度为 SCREEN_W 的帧缓冲时,正确配置应为:
- 源(draw_buf 是紧凑矩形):FGOR = 0
- 目的(帧缓冲每行还有 SCREEN_W−W 个像素不属于本矩形):OOR = SCREEN_W − W
如果 OOR 配错了会怎样? 假设需要 OOR=0 而寄存器残留着 40:每传一行,输出地址就多前进 40 个像素,第 N 行整体右移 40×N 像素——一帧 480 行累积下来偏移超过整个屏幕宽度,直接越界踩到别的内存。宏观表现就是画面被切成一条条对角斜带。记住这个画面,第六节它会真实出现。
三、两个加速点:渲染加速与传输加速
3.1 渲染加速:LV_USE_GPU_STM32_DMA2D

打开 LV_USE_GPU_STM32_DMA2D 后,LVGL 绘制过程中的纯色填充(lv_draw_fill→R2M)和带混合的贴图(lv_draw_map/blend→M2M_BLEND)会交给 DMA2D 完成,CPU 得以从大规模像素搬运中解放出来。
关键伏笔:LVGL v8.3 的 lv_gpu_stm32_dma2d.c 实现不走 HAL 库,而是直接裸写寄存器:
/* LVGL v8.3 GPU 内部实现(示意) */
DMA2D->CR = DMA2D_M2M_BLEND; /* 直接改模式 */
DMA2D->OPFCCR = CM_ARGB8888; /* 直接改输出格式 */
DMA2D->OOR = draw_buf_stride – w; /* 直接改输出行偏移 */
DMA2D->FGPFCCR = ...; DMA2D->FGOR = ...;
DMA2D->FGMAR = src; DMA2D->OMAR = dst;
DMA2D->NLR = h | (w << 16);
DMA2D->CR |= DMA2D_CR_START;
while (DMA2D->CR & DMA2D_CR_START); /* 轮询等待 */
DMA2D->IFCR = 0x3FU; /* 清标志 */
它用完就走,MODE、OPFCCR、OOR、FGOR 全部停留在 LVGL 最后一次操作的状态;而 HAL 句柄 hdma2d 里的软件状态机(State、ErrorCode)它一概不知。同一块硬件,两套"驾驶员"——这就是后面所有冲突的总根源。
3.2 传输加速:USE_DMA2D_TRANSFER

这是驱动层自定义的开关:flush 时不再用 CPU 逐行 rt_memcpy,而是配置 DMA2D 为 M2M 模式,一次把整个脏矩形搬进帧缓冲。800×480×ARGB8888 的一帧有 1.5 MB,全靠 CPU 搬确实吃力。
两个开关组合出 2×2 = 4 种实验场景,这正是实验设计的骨架。
四、实验设计:2×2 对照矩阵
| 1 | 0(CPU) | 0(CPU memcpy) | 纯软件基线 |
| 2 | 0(CPU) | 1(DMA2D M2M) | 只加速送显 |
| 3 | 1(DMA2D) | 0(CPU memcpy) | 只加速绘制 |
| 4 | 1(DMA2D) | 1(DMA2D M2M) | 双加速 |
4.1 测试 UI
界面刻意设计成「能暴露问题」的样子:
static void timer_cb(lv_timer_t *t)
{
phase += 0.2f;
int val = (int)(50 + 40 * sinf(phase));
lv_chart_set_next_value(chart, ser, val); /* 每 100ms 推入新数据点 */
}
void lv_test_ui(void)
{
lv_obj_t *scr = lv_scr_act();
lv_obj_set_style_bg_color(scr, lv_color_hex(0x000000), 0);
lv_obj_t *title = lv_label_create(scr);
lv_label_set_text(title, "DISP TEST 800×480");
lv_obj_set_style_text_color(title, lv_color_hex(0xffffff), 0);
lv_obj_set_style_text_font(title, &lv_font_montserrat_24, 0);
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 15);
/* ========== 波形图区域 ========== */
chart = lv_chart_create(scr);
lv_obj_set_size(chart, 760, 320);
lv_obj_align(chart, LV_ALIGN_TOP_MID, 0, 55);
/* 图表背景:深灰蓝,和纯黑桌面明显区分 */
lv_obj_set_style_bg_color(chart, lv_color_hex(0x1a2332), 0);
lv_obj_set_style_border_width(chart, 3, 0);
lv_obj_set_style_border_color(chart, lv_color_hex(0x334455), 0);
lv_chart_set_type(chart, LV_CHART_TYPE_LINE);
lv_chart_set_range(chart, LV_CHART_AXIS_PRIMARY_Y, 0, 100);
lv_chart_set_point_count(chart, 200);
lv_obj_set_style_line_width(chart, 2, LV_PART_MAIN);
lv_obj_set_style_line_color(chart, lv_color_hex(0x2a3a4a), LV_PART_MAIN);
lv_obj_set_style_size(chart, 0, LV_PART_INDICATOR);
ser = lv_chart_add_series(chart, lv_color_hex(0xff3333), LV_CHART_AXIS_PRIMARY_Y);
/* 初始化一条中线,之后 timer_cb 会动态更新 */
for (int i = 0; i < 200; i++) {
lv_chart_set_next_value(chart, ser, 50);
}
/* ========== 大色块检测区 ========== */
lv_obj_t *r = lv_obj_create(scr);
lv_obj_set_size(r, 240, 50);
lv_obj_set_pos(r, 20, 400);
lv_obj_set_style_bg_color(r, lv_color_hex(0xff0000), 0);
lv_obj_set_style_border_width(r, 2, 0);
lv_obj_set_style_border_color(r, lv_color_hex(0xffffff), 0);
lv_obj_t *g = lv_obj_create(scr);
lv_obj_set_size(g, 240, 50);
lv_obj_set_pos(g, 280, 400);
lv_obj_set_style_bg_color(g, lv_color_hex(0x00ff00), 0);
lv_obj_set_style_border_width(g, 2, 0);
lv_obj_set_style_border_color(g, lv_color_hex(0xffffff), 0);
lv_obj_t *b = lv_obj_create(scr);
lv_obj_set_size(b, 240, 50);
lv_obj_set_pos(b, 540, 400);
lv_obj_set_style_bg_color(b, lv_color_hex(0x0000ff), 0);
lv_obj_set_style_border_width(b, 2, 0);
lv_obj_set_style_border_color(b, lv_color_hex(0xffffff), 0);
/* ========== 检测白线 */
lv_obj_t *line = lv_obj_create(scr);
lv_obj_set_size(line, 760, 8); /* 够粗! */
lv_obj_align(line, LV_ALIGN_BOTTOM_MID, 0, –15);
lv_obj_set_style_bg_color(line, lv_color_hex(0xffffff), 0);
lv_obj_set_style_radius(line, 0, 0); /* 不要圆角,拍出来边缘锐 */
/* ========== 底部状态条 ========== */
lv_obj_t *status = lv_label_create(scr);
lv_label_set_text(status, "MODE: DMA2D | BUF: DOUBLE");
lv_obj_set_style_text_color(status, lv_color_hex(0xaaaaaa), 0);
lv_obj_set_style_text_font(status, &lv_font_montserrat_16, 0);
lv_obj_align(status, LV_ALIGN_BOTTOM_MID, 0, 5);
/* 启动定时器 */
lv_timer_create(timer_cb, 100, NULL);
}
设计意图:
- 滚动波形保证每个刷新周期都有大面积脏区域,性能数据才有区分度;
- 三色条用于肉眼校验 R/G/B 通道是否错位、Alpha 是否异常;
- 白线是撕裂与闪烁的"试纸"。
4.2 性能测量:monitor_cb 回调
LVGL 的显示驱动提供了 monitor_cb 回调,每次刷新周期结束时会汇报耗时和像素量:
static void disp_monitor(struct _lv_disp_drv_t *disp_drv, uint32_t time, uint32_t px)
{
rt_kprintf("Elapsed: %dms, Pixel: %d, Bytes:%d\\n", time, px, px * sizeof(lv_color_t));
}
本实验每次全屏刷新 Pixel = 800×480 = 384000、Bytes = 1536000(ARGB8888),串口持续输出 Elapsed 即单帧耗时。
五、单独开启:一切正常
5.1 序号 1:CPU 渲染 + CPU 传输(基线)

全屏刷新单帧耗时稳定在 324~329 ms(均值约 326 ms),等效帧率约 3.1 FPS。1.5 MB 的数据全靠 CPU 逐行 memcpy,加上 LVGL 全软件渲染,这就是纯软件方案的天花板。
5.2 序号 2:CPU 渲染 + DMA2D 传输

耗时降到 235~239 ms(均值约 236 ms),单帧快了约 90 ms。此时 LVGL 用 CPU 渲染、不碰 DMA2D 寄存器,flush 时 DMA2D 保持着 CubeMX 初始化后的状态(M2M + OOR 残留值恰好为全屏搬运所需的 0),画面完全正常。
5.3 序号 3:DMA2D 渲染 + CPU 传输

耗时降到 264~270 ms(均值约 267 ms),单帧快了约 59 ms。此时 flush 走 rt_memcpy,完全不碰 DMA2D;LVGL 自己配置、自己使用、自己恢复,画面同样正常。
三个场景全部正常,性能提升也符合预期——于是自然地,把两个开关同时打开。
六、双开翻车:画面闪烁、斜带、变形
先看现象(实拍):

把画面拆解成三类典型痕迹:
| 对角斜带 / 画面整体剪切 | 每行像素的落点逐行偏移 → 典型的输出行偏移 OOR 错误 |
| 色块错位、颜色异常 | 传输的工作模式或颜色格式不是预期的纯拷贝 |
| 半透明残影、忽明忽暗闪烁 | 输出经过了Alpha 混合而非直拷 |
注意串口日志——Elapsed 只有 187 ms 左右,性能数据看着是"最好"的一组,画面却完全不可用。性能正常而画面异常,这个组合本身就是重要线索:DMA2D 确实在干活,而且干得很快,只是"干的内容"不对。
七、根因深度剖析(寄存器级)
7.1 逐帧还原事故现场
出问题的 flush 代码(节选自 drv_lcd.c,USE_DMA2D_TRANSFER 分支):
/* 出问题的原始写法 */
HAL_DMA2D_Start_IT(&hdma2d, (uint32_t)rect->pixels, (uint32_t)dst,
rect->rect.width, rect->rect.height); /* ① 先启动! */
hdma2d.Init.OutputOffset = offset_pixels; /* ② 再改输出偏移 */
HAL_DMA2D_ConfigLayer(&hdma2d, 0); /* ③ 再配图层 */
LTDC_LAYER(&LtdcHandle, 0)->CFBAR = (uint32_t)(_lcd.front_buf); /* ④ 立刻切显存 */
...
HAL_LTDC_Reload(&LtdcHandle, LTDC_SRCR_VBR);
对照第二节的两张表,这里有四个叠加的错误:
错误一:OutputOffset 从未写进硬件(OOR 一直是 LVGL 的残留值)
HAL_DMA2D_Start_IT() 内部只写 NLR / OMAR / FGMAR(ST 官方 HAL 源码节选):
static void DMA2D_SetConfig(DMA2D_HandleTypeDef *hdma2d, ...)
{
/* Configure DMA2D data size */
MODIFY_REG(hdma2d->Instance->NLR, (DMA2D_NLR_NL | DMA2D_NLR_PL), ...);
/* Configure DMA2D destination address */
WRITE_REG(hdma2d->Instance->OMAR, DstAddress);
/* M2M / M2M_PFC / M2M_BLEND 模式:源地址写入 FGMAR */
WRITE_REG(hdma2d->Instance->FGMAR, pdata);
}
而 OOR 寄存器只有 HAL_DMA2D_Init() 会写:
/* HAL_DMA2D_Init() 内部(ST 官方 HAL 源码节选) */
MODIFY_REG(hdma2d->Instance->CR, DMA2D_CR_MODE, hdma2d->Init.Mode);
MODIFY_REG(hdma2d->Instance->OPFCCR, DMA2D_OPFCCR_CM, hdma2d->Init.ColorMode);
MODIFY_REG(hdma2d->Instance->OOR, DMA2D_OOR_LO, hdma2d->Init.OutputOffset);
原代码给 Init.OutputOffset 赋了值,但既没调 Init(),ConfigLayer() 写的也不是 OOR——这个正确的行偏移从头到尾没到过硬件。OOR 里躺着的,是 LVGL 渲染时最后一次 blend 留下的值(比如画 760 像素宽的 chart 时留下的 OOR = 40),而全屏 flush 需要 OOR = 0。每行多跳 40 像素、480 行累积越界——这就是画面上的对角斜带。
错误二:工作模式也是 LVGL 的残留值(纯拷贝变成了"混合")
HAL_DMA2D_Start_IT() 不会重写 CR.MODE。双开时,LVGL 刚用 DMA2D 做完带 Alpha 的混合,CR.MODE 停在 M2M_BLEND。于是本该"纯搬运"的 flush 变成了前景(draw_buf)与背景(BGMAR 指向的陈旧地址)的混合输出——输出颜色取决于一块来历不明的内存,且带着 Alpha 权重。这就是色块错位和半透明残影。
单开传输(序号 2)为什么没事?因为那时 LVGL 根本不碰 DMA2D,CR.MODE 从开机起就一直是 CubeMX 初始化的 M2M。
错误三:启动与配置的顺序颠倒 + 配错了图层
代码先 Start_IT() 再改配置——就算后面的配置有效,也是 DMA2D 已经跑起来之后的事,等于中途改挂挡。而且 HAL_DMA2D_ConfigLayer(&hdma2d, 0) 配的是背景层(BGPFCCR/BGOR),而 M2M 模式下真正作为源的前景层(FGMAR 对应 FGPFCCR/FGOR,索引为 1)从头到尾没被正确配置,FGOR 同样是 LVGL 的残留值。
错误四:Start_IT 是异步的,却立刻切换了 CFBAR
memcpy 路径是同步的,拷完再切显存天经地义;换成 Start_IT 后,DMA2D 还在后台搬运,主线程已经把 CFBAR 切到目标帧缓冲并请求 VBR 重载——LTDC 可能扫到一块搬运到一半的缓冲,这正是"忽明忽暗闪烁"的来源之一。
7.2 一张表看懂"为什么单开正常、双开必炸"
| 只开传输 | 只有驱动(HAL),开机后一次性配置 | M2M + OOR 保持初始值(全屏搬运时恰好正确) | 正常 |
| 只开渲染 | 只有 LVGL(裸寄存器),flush 走 memcpy 不碰 DMA2D | LVGL 自己配、自己用、用完清标志 | 正常 |
| 双开 | LVGL(渲染)与驱动(传输)先后动同一套寄存器 | 驱动按"寄存器仍是初始值"的假设启动传输,实际全是 LVGL 的残留 | 斜带 + 色块 + 闪烁 |
本质上,这是一次典型的共享外设状态被两个互不知情的模块污染的事故:HAL 的软件状态机(hdma2d.State)说"一切就绪",硬件寄存器却说着另一种语言。
八、修复方案与验证
8.1 修复思路
既然无法阻止 LVGL 直接改寄存器,那就每次 flush 都把 DMA2D 完整地"夺回来":等硬件空闲 → 清残留标志 → 恢复 HAL 状态机 → Init() 重写模式/输出格式/OOR → ConfigLayer(1) 配置前景层(源)格式与偏移 → 阻塞式启动并等待完成 → 最后才切换 CFBAR。
8.2 修复后的完整代码
#ifdef USE_DMA2D_TRANSFER
extern DMA2D_HandleTypeDef hdma2d;
static rt_err_t drv_lcd_control(struct rt_device *device, int cmd, void *args)
{
struct drv_lcd_device *lcd = LCD_DEVICE(device);
switch (cmd)
{
case RTGRAPHIC_CTRL_RECT_UPDATE:
{
struct lcd_rect_info *rect = (struct lcd_rect_info *)args;
RT_ASSERT(rect != RT_NULL);
uint32_t line_byte = lcd->lcd_info.width * lcd->lcd_info.bits_per_pixel / 8;
uint32_t row_size = rect->rect.width * (lcd->lcd_info.bits_per_pixel / 8);
/* 目的缓冲每行需要跳过的像素数 */
uint32_t offset_pixels = lcd->lcd_info.width – rect->rect.width;
/* ==========================================================
* 关键:LVGL 的 GPU 直接操作了 DMA2D 寄存器,
* 这里必须完整重新初始化,不能复用残留状态。
* ========================================================== */
/* 1. 等硬件真正空闲 */
uint32_t tick = rt_tick_get();
while ((DMA2D->CR & DMA2D_CR_START) && (rt_tick_get() – tick < 100));
/* 2. 清掉 LVGL 残留的中断标志 */
DMA2D->IFCR = 0x3FU;
/* 3. 恢复 HAL 状态机(LVGL 绕过 HAL,State 可能不同步) */
hdma2d.State = HAL_DMA2D_STATE_READY;
hdma2d.ErrorCode = HAL_DMA2D_ERROR_NONE;
/* 4. 配置 M2M 模式、输出格式、输出行偏移 OOR */
hdma2d.Init.Mode = DMA2D_M2M;
hdma2d.Init.ColorMode = DMA2D_OUTPUT_ARGB8888; /* 与 LTDC 层一致 */
hdma2d.Init.OutputOffset = offset_pixels;
HAL_DMA2D_Init(&hdma2d);
/* 5. 配置前景层(源):注意图层索引 1 才是前景层!
* M2M 的源地址由 Start() 写入 FGMAR(前景层),
* 源缓冲是紧凑矩形,输入偏移为 0 */
hdma2d.LayerCfg[1].InputOffset = 0;
hdma2d.LayerCfg[1].InputColorMode = DMA2D_INPUT_ARGB8888;
hdma2d.LayerCfg[1].AlphaMode = DMA2D_NO_MODIF_ALPHA;
hdma2d.LayerCfg[1].InputAlpha = 0;
hdma2d.LayerCfg[1].AlphaInverted = DMA2D_REGULAR_ALPHA;
hdma2d.LayerCfg[1].RedBlueSwap = DMA2D_RB_REGULAR;
HAL_DMA2D_ConfigLayer(&hdma2d, 1);
if (_lcd.cur_buf)
{
uint8_t *dst = lcd->front_buf
+ rect->rect.y * line_byte
+ rect->rect.x * (lcd->lcd_info.bits_per_pixel / 8);
HAL_DMA2D_Start(&hdma2d, (uint32_t)rect->pixels, (uint32_t)dst,
rect->rect.width, rect->rect.height);
/* 6. 阻塞等待完成,再切换缓冲,防止 LTDC 读到半成品 */
HAL_DMA2D_PollForTransfer(&hdma2d, 100);
LTDC_LAYER(&LtdcHandle, 0)->CFBAR = (uint32_t)(_lcd.front_buf);
_lcd.cur_buf = 0;
}
else
{
uint8_t *dst = lcd->back_buf
+ rect->rect.y * line_byte
+ rect->rect.x * (lcd->lcd_info.bits_per_pixel / 8);
HAL_DMA2D_Start(&hdma2d, (uint32_t)rect->pixels, (uint32_t)dst,
rect->rect.width, rect->rect.height);
HAL_DMA2D_PollForTransfer(&hdma2d, 100);
LTDC_LAYER(&LtdcHandle, 0)->CFBAR = (uint32_t)(_lcd.back_buf);
_lcd.cur_buf = 1;
}
rt_sem_take(&_lcd.lcd_lock, RT_TICK_PER_SECOND / 20);
HAL_LTDC_Reload(&LtdcHandle, LTDC_SRCR_VBR);
}
break;
case RTGRAPHIC_CTRL_GET_INFO:
{
/* …返回分辨率、帧缓冲地址等信息,保持不变… */
}
break;
default:
return –RT_EINVAL;
}
return RT_EOK;
}
#endif
8.3 关键改动逐条说明
| 1 | 每次 flush 前调用 HAL_DMA2D_Init() | 只有它会写 OOR(输出行偏移)和 CR.MODE(工作模式),把 LVGL 残留的 M2M_BLEND + 错误偏移彻底覆盖为 M2M + 正确偏移 |
| 2 | HAL_DMA2D_ConfigLayer(&hdma2d, 1) | 图层 1 = 前景层,对应 FGMAR/FGPFCCR/FGOR;InputOffset = 0 因为 draw_buf 里的脏矩形是紧凑排布的 |
| 3 | 等 START 位清零 + IFCR = 0x3F 清标志 + 手动恢复 hdma2d.State | LVGL 绕过 HAL 直接操作硬件,HAL 的软件状态机可能和硬件脱节,直接启动会被拒或行为异常 |
| 4 | HAL_DMA2D_Start()(阻塞)替代 Start_IT() | 保证 CFBAR 切换时搬运已完成,杜绝 LTDC 读到半成品缓冲;想异步化,应把切显存和 VBR 重载挪进 HAL_DMA2D_TransferCompletedCallback |
| 5 | Init.Mode、Init.ColorMode 每次显式赋值 | 不依赖 CubeMX 生成代码的默认值,防止工程迁移/重配时悄悄失效 |
| 6 | 切换 CFBAR 前先 PollForTransfer 完成 | 与第 4 点配合,保证"先搬完、再换屏"的顺序 |
D-Cache 补充说明(Cortex-M7 特有):若 MPU 将 SDRAM 帧缓冲区配置为 Cacheable,CPU 渲染写入的数据可能还停留在 Cache 中,DMA2D 从 SDRAM 读到的会是旧数据。此时必须在 DMA2D 读取源缓冲之前执行:
#if defined(__DCACHE_PRESENT) && (__DCACHE_PRESENT == 1U)
SCB_CleanDCache_by_Addr((uint32_t *)rect->pixels, row_size * rect->rect.height);
#endif
本工程实测通过的前提是帧缓冲区按 Non-cacheable 处理(或已含上述清理)。如果你复刻后仍有局部花屏,优先排查 Cache 与 MPU 配置。
8.4 修复验证
回归验证按"只开传输 → 只开渲染 → 双开"的顺序进行,三种组合画面均正常。双开的实拍效果:


串口数据:183~188 ms(均值约 187 ms),是四组实验中最快的——画面正常,性能收益也全额到手。
九、性能对比与总结
9.1 四组实测数据
测试条件:800×480 分辨率、ARGB8888(每帧 384,000 像素 / 1,536,000 字节)、每帧全屏无效区域、串口连续采样取平均。
| 1 | CPU | CPU | 326 ms | 3.1 FPS | — |
| 2 | CPU | DMA2D | 236 ms | 4.2 FPS | 耗时 −27.5%(快 90 ms) |
| 3 | DMA2D | CPU | 267 ms | 3.7 FPS | 耗时 −18.2%(快 59 ms) |
| 4 | DMA2D | DMA2D | 187 ms | 5.4 FPS | 耗时 −42.8%,帧率 +74.8% |

9.2 数据背后的三个结论
结论一:传输加速的收益(90 ms)大于渲染加速的收益(59 ms)。 传输阶段是纯粹的 1.5 MB 大块数据搬运,DMA2D 的总线搬运能力可以全额发挥;而渲染阶段中,本测试 UI 的折线绘制、文本光栅化、抗锯齿等像素级操作本质上仍由 CPU 完成,DMA2D 只能加速其中的纯色填充(背景、三色条)与混合搬运——它加速的是"搬运类"绘制,不是"计算类"绘制。UI 中矢量图形、文字占比越高,渲染加速的边际收益越低。
结论二:两个加速点的收益基本可加(90 + 59 ≈ 139 ms,实测 139 ms)。 渲染与传输使用 DMA2D 的时机在时间轴上天然错开(先画完才送显),阻塞式实现下二者几乎不争抢资源,收益近似线性叠加。
结论三:全屏刷新 5.4 FPS 是这套"每帧全屏重绘"方案的上限,不是 LVGL 的上限。 本实验为了让数据有区分度,用滚动波形制造了持续的全屏无效区域。实际应用中 LVGL 默认只重绘脏区域(dirty rect),点击一个按钮可能只需重绘几十 KB,帧率可以轻松上一个数量级。若仍需提升全屏重绘性能,方向按性价比排序:
9.3 一句话总结
DMA2D 是共享外设,"谁用谁配、用完不管"在单租户时是自由,在双租户时就是灾难。LVGL 的 GPU 绕过 HAL 直接写寄存器,意味着任何基于 HAL 的 DMA2D 操作都必须假设"寄存器状态不可信",每次使用前完整重建配置。修复后的双加速方案让全屏刷新耗时从 326 ms 降到 187 ms(帧率 +74.8%),且画面完全正常。
十、避坑清单
把这次排查沉淀成可复用的 CheckList,供同路人自查:
- HAL_DMA2D_Start() 不写 MODE / OPFCCR / OOR——只写 NLR/OMAR/FGMAR。跨模块共用 DMA2D 时,每次使用前必须 HAL_DMA2D_Init() 完整重配;
- Init.OutputOffset 赋值 ≠ 生效,只有 HAL_DMA2D_Init() 会把它写进 OOR 寄存器,ConfigLayer() 管的是 FGOR/BGOR;
- 图层 0 是背景层、图层 1 是前景层(DMA2D_BACKGROUND_LAYER=0,DMA2D_FOREGROUND_LAYER=1),M2M 的源是前景层,源侧配置要传 1;
- 先配置、后启动,顺序反了等于中途改挂挡;
- Start_IT 是异步的,切 CFBAR、发 VBR 重载之前必须确认搬运完成(阻塞等待或挪进完成中断);
- 绕过 HAL 的裸寄存器代码(LVGL GPU、TouchGFX、第三方库)会让 HAL 状态机与硬件脱节,共用时先等 START 清零、清 IFCR、恢复 hdma2d.State;
- Cortex-M7 上 DMA2D 读 SDRAM 前清理 D-Cache(SCB_CleanDCache_by_Addr),或经 MPU 将帧缓冲区设为 Non-cacheable,否则可能搬运到旧数据;
- 性能数据正常 ≠ 画面正确——本案例双开时 187 ms 全场最快,画面却完全崩坏。验收必须"看数据"和"看画面"两条腿走路。
环境说明:野火挑战者 STM32F767IGT6(Cortex-M7 @216 MHz,32 MB SDRAM)+ 5 寸 800×480 电容屏;RT-Thread 4.x + LVGL v8.3;串口实测数据为连续采样 30+ 帧的统计结果。

