欢迎光临
我们一直在努力

STM32F767 + RT-Thread + LVGL:DMA2D「渲染 + 传输」双加速踩坑实录(根因剖析 + 性能实测)

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 对照实验定量测出四种组合的性能差异。


目录

  • 显示链路与实验背景
  • 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 与本文强相关的寄存器

    寄存器作用由哪个 HAL 函数写入
    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 对照矩阵

    序号渲染(LV_USE_GPU_STM32_DMA2D)传输(USE_DMA2D_TRANSFER)含义
    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 一张表看懂"为什么单开正常、双开必炸"

    场景谁在动 DMA2D 寄存器寄存器状态结果
    只开传输 只有驱动(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,帧率可以轻松上一个数量级。若仍需提升全屏重绘性能,方向按性价比排序:

  • 降低色深:ARGB8888 → RGB565,数据量直接减半,LTDC/DMA2D/SDRAM 三处同时受益;
  • 脏区域刷新:让 RTGRAPHIC_CTRL_RECT_UPDATE 真正按 rect 局部生效(本工程已支持,只是测试 UI 每帧都全屏无效);
  • 传输异步化:把 CFBAR 切换和 VBR 重载移入 DMA2D 传输完成中断,CPU 在搬运期间可继续渲染下一帧;
  • SDRAM 带宽:帧缓冲放 SDRAM(FMC 16 位总线)本身就有带宽约束,LTDC 扫描、DMA2D 搬运、CPU 渲染三方共享,必要时用 LTDC 行中断分时搬运。
  • 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+ 帧的统计结果。

    赞(0)
    未经允许不得转载:171主机测评 » STM32F767 + RT-Thread + LVGL:DMA2D「渲染 + 传输」双加速踩坑实录(根因剖析 + 性能实测)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址