STM32H7 无 CPU 干预的图层混合:MDMA 与 LTDC 深度联动实战

1. 为什么需要 MDMA 与 LTDC 联动?

在嵌入式 GUI 开发中,图层混合(如半透明菜单、弹窗阴影)是常见需求。传统做法是 CPU 读取两个图层像素,进行 Alpha 混合计算,再写回显存。对于 800x480 的 RGB565 屏幕,每帧混合运算量高达 76.8 万次,CPU 占用率极高。

STM32H7 的 LTDC 控制器支持最多 8 个图层,每个图层可独立配置像素格式和 Alpha 值,硬件自动完成混合。但问题在于:LTDC 的 Alpha 混合是固定透明度的,无法实现逐像素渐变效果。若需动态混合,仍需 CPU 介入。

MDMA(Master DMA)的引入改变了这一局面。MDMA 支持内存到内存的搬运,且具备 2D 寻址能力,可配合 LTDC 的帧同步信号(Line Interrupt)触发,实现图层数据的硬件级预处理。通过 MDMA 将背景图层数据搬运到前景图层缓冲区,并利用 LTDC 的 Alpha 混合功能,即可实现无 CPU 干预的叠加效果。

2. 硬件原理:MDMA 如何与 LTDC 协同

2.1 LTDC 图层混合机制

LTDC 每个图层都有独立的 PFCR(像素格式配置寄存器)和 CACR(恒定 Alpha 配置寄存器)。当两个图层重叠时,LTDC 根据前景图层的 Alpha 值(恒定值或像素值)与背景图层进行混合,公式为:

输出像素 = (前景像素 × Alpha + 背景像素 × (255 - Alpha)) / 255

2.2 MDMA 的 2D 搬运能力

MDMA 支持块传输(Block Transfer),可配置源/目的地址偏移、数据块大小和传输次数。利用此特性,MDMA 可以按行搬运图层数据,并在搬运过程中进行数据转换(如 RGB888 转 RGB565),甚至可对数据进行移位或掩码操作。

2.3 联动触发方式

LTDC 在每行扫描结束时会触发 Line Interrupt,该中断可配置为触发 MDMA 请求。MDMA 收到请求后,搬运下一行数据到目标图层缓冲区。这样,LTDC 扫描到某行时,MDMA 已提前准备好该行数据,形成流水线作业。

3. 实现方案设计

假设场景:背景图层(Layer0)为静态图片,前景图层(Layer1)为半透明菜单,菜单区域需要动态更新。我们使用 MDMA 将菜单数据从源缓冲区搬运到 LTDC 的 Layer1 帧缓冲区,并设置 Layer1 的恒定 Alpha 为 128(50% 透明度)。

3.1 硬件资源

  • STM32H743 开发板(或类似 H7 系列)
  • RGB 屏幕(800x480,RGB565)
  • 外部 SDRAM(用于帧缓冲区)

3.2 软件架构

  • 使用 STM32CubeMX 初始化 LTDC、MDMA 和 DMA2D(可选)
  • 配置 LTDC 双图层,Layer0 为背景,Layer1 为前景
  • 配置 MDMA 为内存到内存模式,触发源为 LTDC Line Interrupt
  • 在中断服务函数中启动 MDMA 传输(或使用硬件自动触发)

4. 配置步骤(基于 STM32CubeMX)

4.1 LTDC 配置

  1. 开启 LTDC,设置时序参数(Hsync、Vsync、HBP、VBP 等)
  2. 使能 Layer0:像素格式 RGB565,帧缓冲区地址指向背景图像
  3. 使能 Layer1:像素格式 RGB565,帧缓冲区地址指向前景缓冲区,设置恒定 Alpha 为 128
  4. 在 Layer1 的配置中,启用 DitheringAlpha 混合 选项

4.2 MDMA 配置

  1. 在 CubeMX 中启用 MDMA,选择 MDMA_Channel0
  2. 请求源选择 LTDC_LINE(需在 NVIC 中使能 LTDC 全局中断)
  3. 传输模式:内存到内存,数据宽度 32 位(RGB565 可打包为 32 位)
  4. 设置块大小(Block Size)为一行像素数(如 800),传输次数为 1(每次中断传输一行)
  5. 源地址和目的地址分别指向源缓冲区和 Layer1 帧缓冲区,地址偏移按行自动增加

4.3 中断配置

  • 使能 LTDC 全局中断,在中断回调中启动 MDMA 传输(或使用硬件请求自动触发,但需注意时序)
  • 推荐使用硬件自动触发:LTDC 的 Line Interrupt 直接连接 MDMA 请求,无需 CPU 介入

5. 完整代码示例

以下代码基于 HAL 库,展示了核心初始化及中断处理逻辑。

/* 全局变量 */
#define LCD_WIDTH  800
#define LCD_HEIGHT 480
#define LINE_BYTES (LCD_WIDTH * 2)  // RGB565 每像素 2 字节

uint16_t background_buffer[LCD_HEIGHT][LCD_WIDTH];  // 背景图像数据
uint16_t foreground_buffer[LCD_HEIGHT][LCD_WIDTH];  // 前景图像数据(菜单)
uint16_t source_buffer[LCD_HEIGHT][LCD_WIDTH];      // 源数据(需搬运到前景)

/* LTDC 句柄 */
LTDC_HandleTypeDef hltdc;

/* MDMA 句柄 */
MDMA_HandleTypeDef hmdma_mdma_channel0;

/* 初始化 LTDC 和 MDMA */
void MX_LTDC_Init(void)
{
    // ... 省略时序配置,使用 CubeMX 生成代码
    
    // 配置 Layer0(背景)
    LTDC_LayerCfgTypeDef layer0_cfg = {0};
    layer0_cfg.WindowX0 = 0;
    layer0_cfg.WindowY0 = 0;
    layer0_cfg.WindowX1 = LCD_WIDTH;
    layer0_cfg.WindowY1 = LCD_HEIGHT;
    layer0_cfg.PixelFormat = LTDC_PIXEL_FORMAT_RGB565;
    layer0_cfg.FBStartAdress = (uint32_t)background_buffer;
    layer0_cfg.Alpha = 255;  // 不透明
    HAL_LTDC_ConfigLayer(&hltdc, &layer0_cfg, 0);

    // 配置 Layer1(前景)
    LTDC_LayerCfgTypeDef layer1_cfg = {0};
    layer1_cfg.WindowX0 = 100;
    layer1_cfg.WindowY0 = 50;
    layer1_cfg.WindowX1 = 500;
    layer1_cfg.WindowY1 = 350;
    layer1_cfg.PixelFormat = LTDC_PIXEL_FORMAT_RGB565;
    layer1_cfg.FBStartAdress = (uint32_t)foreground_buffer;
    layer1_cfg.Alpha = 128;  // 50% 透明度
    HAL_LTDC_ConfigLayer(&hltdc, &layer1_cfg, 1);
}

void MX_MDMA_Init(void)
{
    hmdma_mdma_channel0.Instance = MDMA_Channel0;
    hmdma_mdma_channel0.Init.Request = MDMA_REQUEST_LTDC_LINE;
    hmdma_mdma_channel0.Init.TransferTriggerMode = MDMA_BLOCK_TRANSFER;
    hmdma_mdma_channel0.Init.Priority = MDMA_PRIORITY_HIGH;
    hmdma_mdma_channel0.Init.Endianness = MDMA_LITTLE_ENDIAN;
    hmdma_mdma_channel0.Init.SourceInc = MDMA_SRC_INC_WORD;
    hmdma_mdma_channel0.Init.DestinationInc = MDMA_DEST_INC_WORD;
    hmdma_mdma_channel0.Init.SourceDataSize = MDMA_SRC_DATASIZE_WORD;
    hmdma_mdma_channel0.Init.DestDataSize = MDMA_DEST_DATASIZE_WORD;
    hmdma_mdma_channel0.Init.DataAlignment = MDMA_DATAALIGN_PACK;
    hmdma_mdma_channel0.Init.BufferTransferLength = LCD_WIDTH / 2;  // 每行 800 像素,32 位传输,即 400 次
    hmdma_mdma_channel0.Init.SourceBurst = MDMA_SOURCE_BURST_4BEATS;
    hmdma_mdma_channel0.Init.DestBurst = MDMA_DEST_BURST_4BEATS;
    hmdma_mdma_channel0.Init.SourceBlockAddressOffset = 0;
    hmdma_mdma_channel0.Init.DestBlockAddressOffset = 0;
    hmdma_mdma_channel0.Init.SourceDataBlockCount = 1;
    hmdma_mdma_channel0.Init.DestDataBlockCount = 1;
    hmdma_mdma_channel0.Init.SourceDataBlockAddressOffset = 0;
    hmdma_mdma_channel0.Init.DestDataBlockAddressOffset = 0;
    HAL_MDMA_Init(&hmdma_mdma_channel0);

    // 配置 MDMA 请求映射到 LTDC 行中断
    HAL_MDMA_ConfigRequest(&hmdma_mdma_channel0, MDMA_REQUEST_LTDC_LINE, MDMA_REQ_MAPPING_0);
}

/* 启动 MDMA 传输(每次 LTDC 行中断时调用) */
void Start_MDMA_Transfer(uint32_t line_index)
{
    // 源地址:源缓冲区第 line_index 行
    uint32_t src_addr = (uint32_t)&source_buffer[line_index][0];
    // 目的地址:前景缓冲区第 line_index 行(注意窗口偏移)
    uint32_t dst_addr = (uint32_t)&foreground_buffer[line_index][0];
    
    HAL_MDMA_Start_IT(&hmdma_mdma_channel0, src_addr, dst_addr, LCD_WIDTH/2, 1);
}

/* LTDC 中断回调 */
void HAL_LTDC_LineEven_EventCallback(LTDC_HandleTypeDef *hltdc)
{
    static uint32_t line = 0;
    if (line < LCD_HEIGHT) {
        Start_MDMA_Transfer(line);
        line++;
    } else {
        line = 0;
    }
}

/* 主程序 */
int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_LTDC_Init();
    MX_MDMA_Init();

    // 初始化源缓冲区(例如生成渐变图案)
    for (int y = 0; y < LCD_HEIGHT; y++) {
        for (int x = 0; x < LCD_WIDTH; x++) {
            source_buffer[y][x] = (x * 32) | ((y * 32) << 5);  // 简单渐变
        }
    }

    // 启动 LTDC
    HAL_LTDC_Start(&hltdc);

    while (1)
    {
        // 主循环无需处理图层混合,可执行其他任务
    }
}

6. 注意事项与优化建议

6.1 时序同步

  • MDMA 必须在 LTDC 扫描到该行之前完成搬运,否则会出现撕裂。建议将 LTDC 的 Line Interrupt 提前几行触发(通过配置 LTDC_LIPCR 寄存器),或使用双缓冲机制。
  • 若使用硬件自动触发,需确保 MDMA 的传输时间小于一行扫描时间(例如 60Hz 刷新率下,一行约 20μs)。

6.2 内存带宽

  • MDMA 和 LTDC 同时访问 SDRAM 时,可能产生总线冲突。建议将 MDMA 的优先级设为高,并启用 AXI 总线 QoS 设置。
  • 使用 32 位数据宽度可提高带宽利用率,但需注意地址对齐。

6.3 像素格式

  • 若使用 RGB888 格式,则每像素 3 字节,MDMA 搬运需调整为 24 位对齐,可考虑使用 DMA2D 进行格式转换。
  • 本例使用 RGB565,每像素 2 字节,可打包为 32 位传输,效率更高。

6.4 中断开销

  • 虽然 MDMA 传输无需 CPU 干预,但 LTDC 行中断仍会触发 CPU 中断。若每行都触发,CPU 负载仍较高。可改为每 N 行触发一次,或使用 MDMA 的链表模式(Linked List)实现多行连续传输。

6.5 调试建议

  • 使用逻辑分析仪或示波器观察 LTDC 的 VSYNC 和 Line 信号,验证时序。
  • 在 MDMA 传输完成中断中添加标志位,检查是否出现传输溢出。

7. 总结

通过 MDMA 与 LTDC 的联动,STM32H7 可以在无 CPU 干预的情况下实现图层混合叠加,大幅降低 CPU 负载。本文提供的方案适用于动态菜单、弹窗等场景,且易于扩展。开发者可根据实际需求调整 MDMA 传输策略,例如使用链表实现多行预取,或结合 DMA2D 进行像素格式转换,进一步优化性能。