在嵌入式 UI 开发中,常遇到横屏转竖屏的场景,需将 framebuffer 中的图像逆时针旋转 90 度后显示。传统软件旋转方案通过双重循环实现,但效率低下,占用大量 CPU 算力。本文基于意法半导体 LAT1416 技术文档,详解 STM32H7(MDMA)与 STM32U5(GPDMA)系列芯片借助 DMA 实现图像旋转的核心原理、实操代码及方案对比,助力开发人员释放 CPU 资源,提升系统响应速度。
1. 核心背景与技术痛点
1.1 需求场景
- 应用场景:UI 界面从横屏切换为竖屏,需对 RGB565 格式图像(16 位 / 像素,2 字节存储)进行逆时针 90 度旋转;
- 核心诉求:替代软件旋转方案,通过 DMA 硬件搬运实现旋转,解放 CPU 处理其他事务。
1.2 技术难点
原始图像数据在内存中连续存储(如 W×H 分辨率图像按 “行优先” 排列),但旋转后像素在目标内存中呈 “列优先” 分布,数据地址完全不连续。若直接使用 DMA 传输,无法充分利用 burst 模式提升效率,但仍能通过 DMA 的灵活寻址能力实现旋转,核心优势是脱离 CPU 干预。
2. 两种 DMA 实现方案(分芯片系列)
2.1 STM32H7 系列:MDMA + LinkedList 模式
STM32H7 的 MDMA(Multi-channel DMA)无原生 2D 寻址功能,需通过LinkedList(链表)模式,将旋转操作拆解为多个节点传输任务,每个节点负责将源图像的一行数据转存为目标图像的一列数据。
(1)核心原理
- 节点数量:需创建与图像高度(HH)相等的链表节点,每个节点处理 1 行→1 列的转换;
- 寻址逻辑:
- 源地址:从每行末尾开始(如第 i 行起点为
data_src + (WW-1) + i×WW),按半字(2 字节)递减,读取整行像素;
- 目标地址:从目标列起点开始(如第 i 列起点为
data_dst + i),传输后目标地址按HH×2字节偏移(跳至下一列同位置);
- 数据配置:RGB565 格式为半字(16 位),块传输长度设为 2 字节,每个节点传输
WW个块(覆盖整行像素)。
(2)完整实现代码
// 图像参数:WW=图像宽度,HH=图像高度(可根据实际需求修改)
#define WW 5
#define HH 4
// 源/目标图像缓冲区(32字节对齐,满足DMA传输要求)
ALIGN_32BYTES(uint16_t data_src[WW*HH]); // 源图像:W×H
ALIGN_32BYTES(uint16_t data_dst[HH*WW]); // 目标图像:H×W(旋转后分辨率反转)
ALIGN_32BYTES(MDMA_LinkNodeTypeDef Xfer_Node[HH]); // 链表节点(数量=HH)
MDMA_HandleTypeDef MDMA_Handle;
MDMA_LinkNodeConfigTypeDef mdmaLinkNodeConfig;
uint32_t i = 0;
// 初始化每个链表节点
for(i=0; i<HH; i++) {
// 1. 配置MDMA节点传输参数
mdmaLinkNodeConfig.Init.Request = MDMA_REQUEST_SW; // 软件触发传输
mdmaLinkNodeConfig.Init.TransferTriggerMode = MDMA_FULL_TRANSFER; // 完整传输触发
mdmaLinkNodeConfig.Init.Priority = MDMA_PRIORITY_HIGH; // 高优先级
mdmaLinkNodeConfig.Init.Endianness = MDMA_LITTLE_ENDIANNESS_PRESERVE; // 保留小端序
mdmaLinkNodeConfig.Init.SourceInc = MDMA_SRC_DEC_HALFWORD; // 源地址按半字递减
mdmaLinkNodeConfig.Init.DestinationInc = MDMA_DEST_INC_DISABLE; // 目标地址固定(块传输后偏移)
mdmaLinkNodeConfig.Init.SourceDataSize = MDMA_SRC_DATASIZE_HALFWORD; // 源数据:16位(半字)
mdmaLinkNodeConfig.Init.DestDataSize = MDMA_DEST_DATASIZE_HALFWORD; // 目标数据:16位(半字)
mdmaLinkNodeConfig.Init.DataAlignment = MDMA_DATAALIGN_PACKENABLE; // 使能数据打包
mdmaLinkNodeConfig.Init.SourceBurst = MDMA_SOURCE_BURST_SINGLE; // 源突发传输:单次
mdmaLinkNodeConfig.Init.DestBurst = MDMA_DEST_BURST_SINGLE; // 目标突发传输:单次
mdmaLinkNodeConfig.Init.BufferTransferLength = 2; // 每个块传输2字节(1个RGB565像素)
mdmaLinkNodeConfig.Init.SourceBlockAddressOffset = 0; // 源块地址无偏移
mdmaLinkNodeConfig.Init.DestBlockAddressOffset = HH*2; // 目标块偏移:HH×2字节(跳至下一列)
// 2. 配置源/目标地址
mdmaLinkNodeConfig.SrcAddress = (uint32_t)(data_src + (WW-1) + i*WW); // 源地址:当前行末尾
mdmaLinkNodeConfig.DstAddress = (uint32_t)(data_dst + i); // 目标地址:当前列起点
mdmaLinkNodeConfig.BlockCount = WW; // 块数量=图像宽度(传输整行)
// 3. 创建并添加链表节点
HAL_MDMA_LinkedList_CreateNode(&(Xfer_Node[i]), &mdmaLinkNodeConfig);
HAL_MDMA_LinkedList_AddNode(&MDMA_Handle, &(Xfer_Node[i]), 0);
}
// 启动MDMA链表传输(软件触发)
HAL_MDMA_Start(&MDMA_Handle, mdmaLinkNodeConfig.SrcAddress, mdmaLinkNodeConfig.DstAddress, WW*HH*2);
(3)关键注意事项
- 缓冲区对齐:源 / 目标缓冲区及链表节点需 32 字节对齐(
ALIGN_32BYTES宏),否则 DMA 传输会出错;
- 内存占用:链表节点数量随图像高度增加而增多(如 1080P 图像需 1080 个节点),会占用额外内存资源。
2.2 STM32U5 系列:GPDMA + 2D 寻址模式
STM32U5 的 GPDMA(General Purpose DMA)支持原生 2D 寻址功能,无需链表模式,直接通过配置地址偏移和重复传输次数,即可实现一行→一列的旋转转换,配置更简洁,且不占用额外内存。
(1)核心原理
- 2D 寻址优势:通过
RepeatCount(重复块传输次数)和地址偏移配置,自动完成 HH 次块传输(覆盖所有行→列转换);
- 寻址逻辑:
- 源地址:按半字递增(行优先读取原始图像);
- 目标地址:从目标图像最后一行的第一列开始,传输后按
-(HH×2 + 2)字节偏移(上移一行),块传输完成后按HH×2 + (HH×(WW-1)+1)×2字节偏移(跳至下一列);
- 触发方式:软件触发,单次配置即可完成整幅图像旋转。
(2)完整实现代码(基于 STM32U585)
// 图像参数:WW=80(宽度),HH=100(高度),RGB565格式
#define WW 80
#define HH 100
// 源图像数据(假设已加载RGB565格式图像)
uint16_t *data_src = (uint16_t *)image_icon_80x100;
// 目标图像缓冲区(32字节对齐)
ALIGN_32BYTES(uint16_t data_dst[HH*WW]);
// GPDMA通道12句柄
DMA_HandleTypeDef handle_GPDMA1_Channel12;
DMA_RepeatBlockConfigTypeDef RepeatBlockConfig;
// 1. 初始化GPDMA通道
handle_GPDMA1_Channel12.Instance = GPDMA1_Channel12;
handle_GPDMA1_Channel12.Init.BlkHWRequest = DMA_BREQ_SINGLE_BURST; // 单次突发硬件请求
handle_GPDMA1_Channel12.Init.Request = DMA_REQUEST_SW; // 软件触发
handle_GPDMA1_Channel12.Init.Direction = DMA_MEMORY_TO_MEMORY; // 内存到内存传输
handle_GPDMA1_Channel12.Init.SrcInc = DMA_SINC_INCREMENTED; // 源地址递增
handle_GPDMA1_Channel12.Init.DestInc = DMA_DINC_INCREMENTED; // 目标地址递增
handle_GPDMA1_Channel12.Init.SrcDataWidth = DMA_SRC_DATAWIDTH_HALFWORD; // 源数据:16位
handle_GPDMA1_Channel12.Init.DestDataWidth = DMA_DEST_DATAWIDTH_HALFWORD; // 目标数据:16位
handle_GPDMA1_Channel12.Init.Priority = DMA_LOW_PRIORITY_LOW_WEIGHT; // 优先级配置
handle_GPDMA1_Channel12.Init.SrcBurstLength = 1; // 源突发长度:1
handle_GPDMA1_Channel12.Init.DestBurstLength = 1; // 目标突发长度:1
handle_GPDMA1_Channel12.Init.TransferAllocatedPort = DMA_SRC_ALLOCATED_PORT0 | DMA_DEST_ALLOCATED_PORT1; // 分配传输端口
handle_GPDMA1_Channel12.Init.TransferEventMode = DMA_TCEM_REPEATED_BLOCK_TRANSFER; // 重复块传输模式
handle_GPDMA1_Channel12.Init.Mode = DMA_NORMAL; // 正常模式
// 初始化GPDMA
if (HAL_DMA_Init(&handle_GPDMA1_Channel12) != HAL_OK) {
Error_Handler();
}
// 2. 配置2D寻址重复块参数
RepeatBlockConfig.RepeatCount = HH; // 重复块传输次数=图像高度(HH次)
RepeatBlockConfig.SrcAddrOffset = 0; // 源地址无偏移
RepeatBlockConfig.DestAddrOffset = -(HH*2 + 2); // 目标地址偏移:上移一行
RepeatBlockConfig.BlkSrcAddrOffset = 0; // 块内源地址无偏移
RepeatBlockConfig.BlkDestAddrOffset = HH*2 + (HH*(WW-1) + 1)*2; // 块传输后跳至下一列
// 配置重复块传输
if (HAL_DMAEx_ConfigRepeatBlock(&handle_GPDMA1_Channel12, &RepeatBlockConfig) != HAL_OK) {
Error_Handler();
}
// 3. 配置通道属性(非特权通道)
if (HAL_DMA_ConfigChannelAttributes(&handle_GPDMA1_Channel12, DMA_CHANNEL_NPRIV) != HAL_OK) {
Error_Handler();
}
// 4. 启动GPDMA传输(源地址、目标地址、传输长度=WW×2字节)
HAL_DMA_Start_IT(
&handle_GPDMA1_Channel12,
(uint32_t)data_src,
(uint32_t)(data_dst + (HH*(WW-1))), // 目标起始地址:最后一行第一列
WW*2 // 传输长度:一行像素(WW个×2字节)
);
(3)关键注意事项
- 目标地址计算:起始地址需设为
data_dst + HH*(WW-1),对应旋转后第一列的最后一行,确保像素顺序正确;
- 偏移量配置:
DestAddrOffset和BlkDestAddrOffset需根据图像分辨率动态调整,核心是保证列优先存储。
3. 方案对比与扩展建议
3.1 两种 DMA 方案核心差异
| 对比维度 |
STM32H7(MDMA + LinkedList) |
STM32U5(GPDMA + 2D 寻址) |
| 配置复杂度 |
较高(需创建多个链表节点) |
较低(原生 2D 寻址,单次配置) |
| 内存占用 |
额外占用链表节点内存(随 HH 增加) |
无额外内存占用 |
| 适用场景 |
STM32H7 系列无 2D 寻址功能的芯片 |
STM32U5 系列支持 GPDMA 的芯片 |
| 传输效率 |
中等(链表调度有轻微开销) |
较高(原生 2D 寻址,无调度开销) |