恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
DMA步长实现图像旋转90度:嵌入式屏幕高效搬运方案
首页
资讯中心
/
DMA步长实现图像旋转90度:嵌入式屏幕高效搬运方案
DMA步长实现图像旋转90度:嵌入式屏幕高效搬运方案
发布时间:2026/8/29 10:39:22
做嵌入式屏幕相关的活经常碰到一个不算难但特别麻烦的需求传感器给了一幅横向的图像屏幕偏偏是竖着的得把内存里的像素矩阵旋转90度再送显。我之前在LAT1416上做一款电子相框时也被这个需求卡住过。一开始用CPU的像素双重循环去搬720x480的图像旋转一帧肉眼都能感觉到卡顿后来改用DMA做行列步长交换性能直接提升了一个数量级这篇就把整个方案和踩过的坑完整拆一遍。无论你是做摄像头显示、LCD竖屏横屏切换、还是图像预处理只要MCU的DMA控制器带地址步长或2D传输能力这个方法都能直接用。文章不绕弯子直接从原理讲到寄存器配置再给实测性能和翻车记录。1. 旋转90度看起来简单为什么跑起来这么费劲1.1 CPU旋转算法的时间开销去哪了图像旋转90度本质上就是一个坐标映射加数据搬移。以顺时针旋转90度为例源图像是W宽H高目标图像是H宽W高目标坐标和源坐标的对应关系是dst(x, y) src(x, y) x H - 1 - y y x所以最直白的实现就是双循环void rotate_cw(uint8_t *src, uint8_t *dst, int W, int H, int bpp) { for (int y 0; y H; y) { for (int x 0; x W; x) { int dst_x H - 1 - y; int dst_y x; memcpy(dst (dst_y * H dst_x) * bpp, src (y * W x) * bpp, bpp); } } }代码没问题但跑起来就知道疼。以常见的800x480 RGB565画面为例一共384000个像素每像素2字节。CPU循环里每个像素至少要做一次加法、一次乘法、一次memcpy再加上循环分支和地址计算一个像素吃掉几十个周期很正常。按240MHz主频估算384000 像素 * 40 周期/像素 15,360,000 周期 15.36M 周期 / 240MHz ≈ 64ms这还只是理想情况。实际更慢因为目标地址是跳跃式访问的每一行目标像素分布在源图像的不同行上Cache命中率极低CPU要频繁去等内存总线。换句话说瓶颈不在算法本身而在内存访问模式。1.2 旋转90度的本质就是“列变行”把上面的坐标映射换个角度看顺时针旋转90度后源图像的第一列变成了目标图像的第一行第二列变成第二行以此类推。这是整个方案最关键的认知——旋转90度不需要“逐像素计算坐标”只需要把源图像的每一列原封不动地搬到目标图像的每一行上。那问题就变成了怎么高效地把不连续内存里的“一列”读出来连续写到另一块内存里普通CPU循环是一格格挪但DMA本来就是干搬运的它能不能按“列”搬这就是本方案的核心切入点。只要DMA控制器允许配置地址增量步长它就能搬任意规律的内存序列。人话就是DMA每次搬完一个数据后下一个数据从哪里取可以由我们自己定义。默认情况下它每搬一个字节地址加1如果我们把“地址加1”改成“地址加一整行”DMA读出来的就是源图像的某一列。2. DMA的隐藏能力地址步长才是旋转的关键2.1 大多数人对DMA的理解只用了三分之一很多人对DMA的印象还停留在“连续内存搬运”。比如串口DMA接收、ADC多通道DMA采样、SPI DMA发送这些场景的数据在内存里都是线性连续的DMA配置好源地址、目的地址、传输数量启动就完事。但稍微翻一下DMA控制器的寄存器你会发现很多型号都有地址更新模式相关的配置。比如源地址增量、目的地址增量、传输宽度以及某些控制器上的固定步长模式或2D传输模式。这些能力平时用不上因为串口、SPI、ADC的数据天然就是连续的不需要跳着读。可一旦遇到图像操作这些寄存器就是宝藏。LAT1416的DMA控制器里就有源地址步进和目的地址步进的可配置项。它可以做到每传输完一个像素源地址不是加1字节而是加一个自定义步长目的地址也不是只能加1字节同样可以自定义。这就让DMA有能力“按任意规律从内存里抓数据”。2.2 一列DMA传输的映射推导假设源图像是W像素宽、H像素高RGB565格式那么每个像素占2字节一行的字节数是 W * 2源图像第x列第y行的像素地址是src y * W * 2 x * 2顺时针旋转90度后这列数据要变成目标图像的第x行而目标图像一行有H个像素所以目标行首地址是dst x * H * 2我们要做的是让DMA按下面的规律连续搬H个像素源地址序列: src x*2 0*W*2 src x*2 1*W*2 src x*2 2*W*2 ... src x*2 (H-1)*W*2相邻两个源地址的差值是 W*2 字节即源图像一整行的宽度。目的地址序列是连续的dst x*H*2 0*2 dst x*H*2 1*2 dst x*H*2 2*2 ... dst x*H*2 (H-1)*2相邻两个目的地址的差值是2字节。所以一次DMA传输的配置就是配置项值源起始地址src x * 2目的起始地址dst x * H * 2传输数量H像素数源地址步长W * 2每次读完跳到下一行同列目的地址步长2连续写传输宽度16bit执行完这次传输源图像第x列就变成了目标图像第x行。整个画面旋转90度只需要从x0执行到xW-1共W次DMA传输。2.3 硬件需求清单这个方案不是所有MCU都能直接抄的。在动手前先确认三件事DMA控制器是否支持源地址或目的地址的自定义步长。很多入门级MCU的DMA只有“固定递增”“固定不变”两种没有中间选项那样就无法用纯DMA做旋转。是否支持内存到内存传输。部分DMA通道只能在外设和内存之间搬运不能内存到内存这个也要看数据手册。中断响应是否够快。如果一次DMA只能搬一列那么W次DMA就需要W次中断重装地址。图像宽度越大中断频率越高中断服务代码必须精简。LAT1416这三条都能满足所以下面的实操代码以它为例写。你要是用别的芯片先去找DMA控制器的“步长”“stride”“increment”相关寄存器能找到这套思路就能落地。3. 在LAT1416上落地配置步骤与核心代码3.1 按列搬运的中断重装载方案先写最通用也最容易理解的方案每次DMA搬一列搬完中断在中断里把源地址和目的地址更新到下一列再次启动。#define IMG_W 720 #define IMG_H 480 #define PIXEL_BYTES 2 // RGB565 static uint8_t src_buf[IMG_H][IMG_W * PIXEL_BYTES]; static uint8_t dst_buf[IMG_W][IMG_H * PIXEL_BYTES]; static volatile uint16_t cur_col; void dma_set_column_transfer(uint16_t col) { dma_channel_config_t cfg; cfg.src_addr (uint32_t)src_buf[0][col * PIXEL_BYTES]; cfg.dst_addr (uint32_t)dst_buf[col][0]; cfg.transfer_num IMG_H; cfg.src_step IMG_W * PIXEL_BYTES; // 每搬一个像素源跳到下一行同列 cfg.dst_step PIXEL_BYTES; // 每搬一个像素目标连续前进一个像素 cfg.data_width DMA_WIDTH_16BIT; cfg.src_mode DMA_STEP_MODE; cfg.dst_mode DMA_STEP_MODE; dma_channel_setup(DMA_CH2, cfg); dma_channel_start(DMA_CH2); } void DMA_CH2_IRQHandler(void) { if (dma_channel_get_flag(DMA_CH2, DMA_FIFO_COMPLETE)) { dma_channel_clear_flag(DMA_CH2, DMA_FIFO_COMPLETE); cur_col; if (cur_col IMG_W) { dma_set_column_transfer(cur_col); } else { g_rotate_done 1; } } } void rotate_image_90_cw(void) { cur_col 0; g_rotate_done 0; dma_set_column_transfer(0); }这段逻辑很直白cur_col从0开始每完成一列搬运中断里把cur_col加1然后重新配置下一次传输。源起始地址每次向右移动一个像素目的起始地址每次向下一行移动。源地址步长保持一整行目的地址步长保持一个像素传输数量始终是H。一个容易忽视的细节是传输数量单位是“像素数”不是字节数。如果DMA控制器的传输数量寄存器是按数据宽度计的那么当数据宽度设为16bit时transfer_numIMG_H 就表示传输IMG_H个16bit数据。千万别写成IMG_H*PIXEL_BYTES不然会把下一列的数据也搬过来画面会错位。3.2 配置源步长和目的步长的坑位用这种“列搬运”思路时很多人第一次会配错源步长。常见错误是源地址步长写成了PIXEL_BYTES而传输数量写成IMG_H*PIXEL_BYTES。这样DMA实际是连续读取源图像某一行的一段而不是按列跳读。出来的画面就是一个斜条纹或者完全错乱的马赛克。正确的理解方式源地址步长 源图像的行字节数 目的地址步长 目标图像的行内像素字节数如果DMA的步长寄存器单位是字节那就直接填这两个值如果单位是数据宽度倍数那就分别填 IMG_W 和 1。不同芯片定义不一样务必查手册这是我第一次调这个方案被卡了半天的原因。另外DMA的突发长度也要注意。如果突发长度大于1DMA可能会在一个突发里连续读取多个像素导致步长配置失效。建议把突发长度设为1或者在验证通过后再尝试优化。反正图片数据量不大线性增加的收益有限稳定性优先。3.3 进阶2D DMA一次性搬完如果你的DMA控制器支持2D传输模式上面的W次中断可以合并成一次启动。LAT1416的DMA支持类似的行/块传输结构配置思路如下dma_2d_config_t cfg; cfg.src_base_addr (uint32_t)src_buf[0][0]; // 源图左上角 cfg.dst_base_addr (uint32_t)dst_buf[0][0]; // 目标图左上角 cfg.column_num IMG_H; // 每块搬运列数 cfg.row_num IMG_W; // 一共搬运W列 cfg.src_line_step IMG_W * PIXEL_BYTES; // 列内行间步长 cfg.dst_line_step PIXEL_BYTES; // 目标行内连续 cfg.src_block_step PIXEL_BYTES; // 读完一列后源跳到下一列 cfg.dst_block_step IMG_H * PIXEL_BYTES; // 写完一行后目标跳到下一行 cfg.data_width DMA_WIDTH_16BIT;这里最关键的是block_step源图像的“块”是垂直列所以读完一列后源地址只向右移动一个像素目标图像的“块”是水平行所以写完一行后目标地址下移一整行。这组参数与普通2D DMA“按行读、按行写”的直觉刚好相反很容易配反调试时用2x3的小图像验证最稳。4. 实测性能数据和五次翻车记录4.1 性能对比在我手里的LAT1416工程里主频240MHz源图放在SRAMDMA从SRAM搬到SRAM旋转后直接送LCD刷新。实测数据如下分辨率格式CPU双循环耗时DMA按列搬运耗时提升倍数480x272RGB56518ms2.1ms约8.5倍800x480RGB56561ms5.8ms约10.5倍1280x800RGB565205ms19.6ms约10.4倍数据会受总线竞争和Cache策略影响但趋势很明显分辨率越大DMA优势越大。原因也简单CPU算法每处理一个像素都要重算地址和边界条件而DMA一旦配置好硬件就自动把步长运算做掉了不占用CPU周期。CPU双循环的耗时估算也可以验算一下800x480共384000像素61ms对应约240MHz下14640000个周期平均每像素约38周期和前面估算吻合。4.2 翻车记录一Cache一致性导致旋转后花屏LAT1416如果开了D-CacheDMA往目标缓冲区写数据不会自动更新Cache。等CPU再去读目标缓冲区或者LCD控制器去取显存时看到的可能是旧数据。处理方案有两种优先推荐第一种把目标缓冲区放在non-cacheable区域。如果工程里没法改内存属性就在DMA完成中断里做一次Cache cleanSCB_InvalidateDCache_by_Addr((uint32_t *)dst_buf, sizeof(dst_buf));注意是Invalidate不是Clean。DMA写的是外部SRAMCPU没参与写入必须把Cache里的旧行作废下次读取才会从内存重取。反过来如果DMA是读取源缓冲区而源缓冲区是CPU刚写的CPU侧要先Clean把数据刷到内存再启动DMA。4.3 翻车记录二对齐问题导致DMA传输异常DMA控制器通常要求源地址、目的地址和传输宽度对齐。RGB565是16bit让缓冲区按2字节对齐就好还好。但如果你做的是RGB888图像每个像素3字节按32bit传输时会遇到对齐问题DMA可能会出现未定义行为或者直接hardfault。我的做法是要么把RGB888先转成RGB565再旋转要么用32bit宽度搬运把行宽补齐到4字节对齐后再配置步长。后一种做法需要小心补齐字节会污染目标缓冲区如果在DMA搬运之后还要用这个缓冲区做处理必须预留padding空间。4.4 翻车记录三错误启用循环模式有几次调试时开了DMA的循环模式结果目标图像一直重复第一列。原因很清楚循环模式会在传输完成后把地址寄存器重载成初始值然后继续下一轮传输。这个功能适合ADC连续采样、串口不定长接收这类需要持续搬运的场景但不适合一次性图像旋转。图像旋转的DMA配置一定要关掉循环模式用普通一次性传输。完成中断里手动修改地址和重新启动才能真正进入下一列。4.5 翻车记录四中断标志清除过早导致漏配中断服务里有一个顺序问题先清标志再配置下一次传输。如果反过来先配置了DMA寄存器再清除完成标志而清除标志的同时DMA已经开始搬运极有可能把刚写入的寄存器配置与当前传输混淆产生一次错误的传输。稳妥的顺序是1. 读取DMA完成标志 2. 清除完成标志 3. 更新cur_col 4. 判断是否还有下一列 5. 配置下一次传输寄存器和起始地址 6. 启动DMA另外几乎每个芯片的DMA标志位都有“传输完成”和“FIFO空/半满”等若干种中断服务里别取错。我用的LAT1416里DMA_FIFO_COMPLETE才是整列搬运完成不是DMA_CHANNEL_ACTIVE。4.6 翻车记录五中断里耗时太长影响性能一开始我在中断里做了不少打印和延时结果旋转一帧的时间从6ms飙到40ms。后来把调试代码全部移出中断只在g_rotate_done置位后由主循环打印耗时性能才正常。DMA中断服务代码要精简到极致尤其是按列搬运方案W很大时中断会非常频繁。800宽图像有800次中断每次中断哪怕多10us总耗时也会增加8ms。更好的做法是每搬运N列进一次中断但这就需要DMA连续执行多次传输配置复杂度更高。如果单列中断已经能满足帧率就不建议过度优化了。5. 旋转方向、镜像和更多扩展用法5.1 顺时针、逆时针和180度怎么配核心思路都是一样的区别只在源起始地址和步长的方向。操作源起始地址源地址步长目的起始地址目的地址步长说明顺时针90度src col * 2行字节数dst col * H * 22上面已实现逆时针90度src (W-1-col) * 2行字节数dst col * H * 22从最右列开始读顺时针90度水平镜像src col * 2行字节数dst (W-1-col) * H * 22目标行序反向镜像src 行首-2dst 行首2同一行内反向180度src 末尾-行字节数dst 开头2整体反向180度其实是最简单的不需要复杂步长把源地址改成缓冲区末尾源地址步长设为负的行字节数目的地址从开头连续写DMA搬完就是旋转180度。如果DMA不支持负步长那就把源地址从末尾开始、步长设为负行字节数等价于CPU倒序遍历。注意这里所有步长都是按字节数算的。如果你在代码里看到步长为负数先确认寄存器是有符号的还是无符号的。无符号寄存器无法表达负步长那就只能通过调整起始地址和正步长模拟。5.2 颜色格式转换与旋转的合并图像旋转经常伴随颜色格式转换比如摄像头输出的RGB565要转成RGB888再显示。DMA本身不具备像素格式转换能力但可以用源步长和目的步长做字节重排的部分工作。一种可行的思路是源步长设为3字节跨过RGB888的完整像素目的步长设为2字节写入RGB565的低16位同时把高8位丢弃。不过要注意这种方法只适合丢弃高位通道的情形不能实现真正的色彩空间变换。要想同时做RGB888转RGB565和旋转90度我建议分两步先DMA旋转再CPU批量转换。因为转换是线性遍历CPU访问很友好耗时远小于旋转算法本身。5.3 超大图像怎么处理分块打包如果你的图像大到一次DMA传输数量超过寄存器上限或者目标缓冲区不够大可以按“子块”处理。比如把1280x800的图拆成5个竖条每个竖条256列DMA分别旋转这5个区域再按位置拼到目标缓冲区。分块的关键是维持行字节数不变。每块的源起始地址就是原图左上角加上偏移源步长依旧是整幅图像的行字节数而不是子块的行字节数。这一点最容易出错一定要记住步长是实际源图像的行跨度不是块宽度。5.4 哪些场景不值得用DMA旋转DMA旋转不是万能的。如果是几个小图标旋转90度比如32x32或64x64DMA配置和中断的开销可能比直接CPU循环还大。我测试过16x16图标CPU双循环不到几微秒DMA方案反而因为中断调度和寄存器配置耗时数十微秒。另外如果芯片自带2D图形加速器、旋转引擎或LCD控制器的Rotation/RGB扫描方向寄存器那硬件方案优先级更高。比如有些LCD控制器支持通过寄存器改变扫描方向实现旋转那个是零拷贝零开销比DMA还省。DMA旋转方案更适合那些没有图形引擎、只有标准DMA控制器的MCU场景。5.5 再进一步的优化思路如果后续想在LAT1416上把帧率再往上推可以考虑这样几个方向使用双缓冲DMA在搬运当前帧时CPU同时处理下一帧的预处理或格式转换隐藏DMA耗时。提升DMA传输宽度到32bit一次搬两个RGB565像素传输次数减半中断频率也减半。但要注意源/目的步长都要相应改成2个像素的字节数且地址要4字节对齐。如果DMA支持链表模式可以把W列的寄存器配置全部提前算好DMA自动按链表依次执行彻底去掉每列中断全程零CPU干预。LAT1416的DMA控制器有类似的多缓冲描述符能力不过配置复杂度比单列模式高不少建议先把基础方案跑通再试。我在实际调试中还有一个习惯无论最终用什么方案都会先拿一张只有2x3像素的测试图跑一遍确认旋转方向、位置映射都对了再切到真实图像。不要一上来就盯着花屏分析那样很难分辨是DMA配置问题还是坐标映射问题。先用小图把规律摸清再放大到真实分辨率能省很多时间。