# 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 的配置中,启用 **Dithering** 和 **Alpha 混合** 选项 ### 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 库,展示了核心初始化及中断处理逻辑。 ```c /* 全局变量 */ #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 进行像素格式转换,进一步优化性能。