乐趣区

关于程序员:STM32的GPUDMA2D实例详解

本文首发于 RT-Thread 社区,未获得受权不得转载。
前言

GPU 即图形处理器,是古代显卡的外围。在没有 GPU 的时代,所有图形的绘制都是由 CPU 来实现的,CPU 须要计算图形的边界、色彩等数据,并且负责将数据写入显存。简略的图形还没有什么问题,但随着计算机的倒退(尤其是游戏的倒退),须要显示的图形图像越来越简单,CPU 也就越来越力不从心。所以起初 GPU 应运而生,将 CPU 从沉重的图形计算工作中援救了进去,大大减速了图形的显示速度。

而单片机这边也有相似的倒退历程。在晚期的单片机应用场景中,极少有图形显示的需要。即便有,也只是简略的 12864 之类的显示设施,运算量不大,单片机的 CPU 能够很好的解决。然而随着嵌入式图形的倒退,单片机须要承当的图形计算和显示工作越来越多,嵌入式零碎的显示分辨率和色调也一路飙升。缓缓地,单片机的 CPU 对这些计算就开始力不从心了。所以,自 STM32F429 开始,一个相似 GPU 的外设开始退出到 STM32 的单片机中,ST 称之为 Chrom-ART Accelerator,也叫 DMA2D(本文将应用此名称)。DMA2D 能够在很多 2D 绘图的场合提供减速,完满嵌合了古代显卡中“GPU”的性能。

尽管这个“GPU”只能提供 2D 减速,而且性能非常简单,与 PC 中的 GPU 不可同日而语。然而它曾经能够满足大多数嵌入式开发中的图形显示减速需要,只有用好了 DMA2D,咱们在单片机上也能够做出晦涩、富丽的 UI 成果。

本文将从实例登程,介绍 DMA2D 在嵌入式图形开发中的能够施展的作用。目标是使读者能简略、疾速地对 DAM2D 的建设最根本的概念并且学会最根本的用法。为了避免内容过于艰涩和难懂,本文不会对 DMA2D 的高级性能和个性进行深刻地刨析(如具体介绍 DMA2D 的架构、全副的寄存器等等)。如果须要更加具体、业余地学习 DAM2D,能够在浏览完本文后参考《STM32H743 中文编程手册》。

浏览本文之前须要对 STM32 中的 TFT 液晶控制器(LTDC)和根本的图形常识(如帧缓冲 framebuffer、像素、色彩格局等概念)有肯定的理解。

另,除了 ST 之外,其余不少厂商生产的 MCU 中也存在相似性能的外设(如 NXP 在 RT 系列中设计的的 PxP),不过这些不在本文的探讨范畴内,有趣味的敌人能够自行理解。
筹备工作
硬件筹备

能够应用任何的,带有 DMA2D 外设的 STM32 开发板来验证本文中的例子,如 STM32F429,STM32F746,STM32H750 等 MCU 的开发板。本文中应用的开发板是 ART-Pi。ART-Pi 是由 RT-Thread 官网出品的开发板,采纳了主频高达 480MHz 的 STM32H750XB+32MB SDRAM 的强悍配置。而且板载了调试器(ST-Link V2.1),应用起来十分不便,特地适宜各种技术计划的验证,用来作为本文的硬件演示平台再适合不过了。

显示屏能够是任意的黑白 TFT 显示屏,举荐应用 16 位或 24 位色彩的 RGB 接口显示屏。本文中应用的是一块 3.5‘’的 TFT 液晶显示屏,接口为 RGB666,分辨率为 320×240(QVGA)。在 LTDC 中,配置应用的色彩格局为 RGB565

image.png
开发环境筹备

本文中介绍的内容和呈现的代码能够在任何你喜爱的开发环境中应用,如 RT-Thread Studio,MDK,IAR 等。

开始本文的试验前你须要一个以 framebuffer 技术驱动 LCD 显示屏的根本工程。运行本文中所有的代码前都须要事后使能 DMA2D。

使能 DMA2D 能够通过这个宏来实现(硬件初始化时使能一次即可):

// 应用 DMA2D 之前肯定要先使能 DMA2D 外设
__HAL_RCC_DMA2D_CLK_ENABLE();

DMA2D 的简介

咱们先来看看 ST 是怎么形容 DMA2D 的

image.png

乍一看有点艰涩,但其实说白了就以下几个性能:

色彩填充(矩形区域)图像(内存)复制
色彩格局转换(如 YCbCr 转 RGB 或 RGB888 转 RGB565)透明度混合(Alpha Blend)

前两种都是针对内存的操作,后两个则是运算减速操作。其中,透明度混合、色彩格局转换能够和图像复制一起进行,这样就带来了较大的灵活性。

能够看到,ST 对 DMA2D 的定位就像它的名字一样,是一个针对图像处理性能强化过的 DMA。而在理论开发的过程中,咱们会发现 DMA2D 的应用形式也十分相似传统的 DMA 控制器。在某些非图形处理场合,DMA2D 甚至也能够代替传统的 DMA 来发挥作用。

须要留神的是,ST 的不同产品线的 DMA2D 加速器是有渺小区别的,比方 STM32F4 系列 MCU 的 DMA2D 就没有 ARGB 和 AGBR 色彩格局互转的性能,所以具体须要用到某个性能的时候,最好先查看编程手册看所需的性能是否被反对。

本文只介绍所有平台的 DMA2D 共有的性能。
DMA2D 的工作模式

就像传统 DMA 有外设到外设,外设到存储器,存储器到外设三种工作模式一样,DMA2D 作为一个 DMA,也分为以下四种工作模式:

寄存器到存储器
存储器到存储器
存储器到存储器并执行像素色彩格局转换
存储器到存储器且反对像素色彩格局转换和透明度混合

能够看出,前两种模式起始就是简略的内存操作,而前面两种模式,则是在进行内存复制时,依据须要同时进行色彩格局转换或 / 和透明度混合。
DMA2D 和 HAL 库

大多数状况下,应用 HAL 库能够简化代码编写,进步可移植性。然而在 DMA2D 的应用时则是个例外。因为 HAL 库存在的最大问题就是嵌套层数再加上各种平安检测过多效率不够高。在操作别的外设时,应用 HAL 库损失的效率并不会有多大的影响。然而对于 DMA2D 这种以计算和减速为目标的外设,思考到相干的操作会在一个屏幕的绘制周期内被屡次调用,此时再应用 HAL 库就会导致 DAM2D 的减速效率重大降落。

所以,咱们大多时候都不会用 HAL 库中的相干函数来对 DMA2D 进行操作。为了效率,咱们会间接操作寄存器,这样能力起到最大化的减速成果。

因为咱们应用 DMA2D 的大多数场合都会频繁变更工作模式,所以 CubeMX 中对 DMA2D 的图形化配置也失去了意义。
DMA2D 场景实例

  1. 色彩填充

下图是一个简略的柱状图:

image.png

咱们来思考一下如何把它绘制进去。

首先,咱们须要应用红色来填充屏幕,作为图案的背景。这个过程是不能疏忽的,否则屏幕上原来显示的图案会对咱们的主体产生烦扰。而后,柱状图其实是由 4 个蓝色的矩形方块和一条线段形成的,而线段也能够视作一个非凡的,高度为 1 的矩形。所以,这个图形的绘制能够合成为一系列“矩形填充”操作:

应用红色填充一个大小等于屏幕大小的的矩形
应用蓝色填充四个数据条
应用彩色填充一根高度为 1 的线段

在画布中实现任意地位绘制任意大小的矩形的实质就是将内存区域中对应像素地位的数据设定为指定的色彩。然而因为 framebuffer 在内存中的存储是线性的,所以除非矩形的宽度正好和显示区域的宽度重合,否看似间断的矩形的区域在内存中的地址是不间断的。

下图展现了典型的内存散布状况,其中的数字示意了 frame buffer 中每个像素的内存地址(绝对首地址的偏移,这里疏忽掉了一个像素占多个字节的状况),蓝色区域是咱们要填充的矩形。能够看出矩形区域的内存地址是不间断的。

image.png

framebuffer 的这种个性使得咱们不能简略应用 memset 这类高效的操作来实现矩形区域的填充。通常状况下,咱们会应用以下形式的双重循环来填充任意矩形,其中 xs 和 ys 是矩形左上角在屏幕上的坐标,width 和 height 示意矩形的宽和高,color 示意须要填充的色彩:

for(int y = ys; y < ys + height; y++){

for(int x = xs; x < xs + width; x++){framebuffer[y][x] = color;        
}

}

代码尽管简略,但理论执行时,大量的 CPU 周期节约在了判断、寻址、自增等的操作,理论写内存的工夫占比很少。这样一来,效率就会降落。

这时候 DMA2D 的寄存器到存储器工作模式就能够施展用场了,DAM2D 能够以极高的速度填充矩形的内存区域,即便这些区域在内存中理论是不间断的。

仍然以这张图中演示的状况为例,咱们来看它是如何实现的:

image.png

首先,因为咱们只是进行内存填充,而不须要进行内存拷贝,所以咱们要让 DAM2D 工作在寄存器到存储器模式。这通过设置 DMA2D 的 CR 寄存器的 [17:16] 位为 11 来实现,代码如下:

DMA2D->CR = 0x00030000UL;

而后,咱们要通知 DAM2D 要填充的矩形的属性,比方区域的起始地址在哪里,矩形的宽度有多少像素,矩形的高度有多少。

区域起始地址是矩形区域左上角第一个像素的内存地址(图中红色像素的地址),这个地址由 DAM2D 的 OMAR 寄存器治理。而矩形的宽度和高度都是以像素为单位的,别离由 NLR 寄存器的高 16 位(宽度)和低 16 位(高度)来进行治理,具体的代码如下:

DMA2D->OMAR = (uint32_t)(&framebuffery); // 设置填充区域的起始像素内存地址
DMA2D->NLR = (uint32_t)(width << 16) | (uint16_t)height; // 设置矩形区域的宽高

接着,因为矩形在内存中的地址不间断,所以咱们要通知 DMA2D 在填充完一行的数据后,须要跳过多少个像素(即图中黄色区域的长度)。这个值由 OOR 寄存器治理。计算跳过的像素数量有一个简略的办法,即显示区域的宽度减去矩形的宽度即可。具体实现代码如下:

DMA2D->OOR = screenWidthPx – width; // 设置行偏移,即跳过的像素

最初,咱们须要告知 DAM2D,你将应用什么色彩来进行填充,色彩的格局是什么。这别离由 OCOLR 和 OPFCCR 寄存器来治理,其中色彩格局由 LTDC_PIXEL_FORMAT_XXX 宏来定义,具体代码如下:

DMA2D->OCOLR = color; // 设置填充应用的色彩
DMA2D->OPFCCR = pixelFormat; // 设置色彩格局,比方想设置成 RGB565,就能够应用宏 LTDC_PIXEL_FORMAT_RGB565

所有都设置结束,DMA2D 曾经获取到了填充这个矩形所须要的全副信息,接下来,咱们要开启 DMA2D 的传输,这通过将 DMA2D 的 CR 寄存器的第 0 位设置为 1 来实现:

DMA2D->CR |= DMA2D_CR_START; // 开启 DMA2D 的数据传输,DMA2D_CR_START 是一个宏,其值为 0x01

等 DMA2D 传输开始后,咱们只须要期待它传输结束即可。DAM2D 传输实现后,会主动把 CR 寄存器的第 0 位设置为 0,所以咱们能够通过以下代码来期待 DAM2D 传输实现:

while (DMA2D->CR & DMA2D_CR_START) {} // 期待 DMA2D 传输实现

tips0:如果你应用了 OS,则能够使能 DMA2D 的传输结束中断。而后咱们能够创立一个信号量并且在开启传输后期待它,随后在 DMA2D 的传输结束中断服务函数中开释该信号量。这样的话 CPU 就能够在 DMA2D 工作的时候去干点别的事儿而不是在此处傻等。

tips1:当然,因为理论执行时,DMA2D 进行内存填充的速度切实是太快了,以至于 OS 切换工作的开销都比这个工夫要长,所以即使应用了 OS,咱们还是会抉择死等 :)。

为了函数的通用性思考,起始传输地址和行偏移都在函数外计算结束后传入,咱们抽出的残缺的函数代码如下:

static inline void DMA2D_Fill(void * pDst, uint32_t width, uint32_t height, uint32_t lineOff, uint32_t pixelFormat, uint32_t color) {

/* DMA2D 配置 */  
DMA2D->CR      = 0x00030000UL;                                  // 配置为寄存器到储存器模式
DMA2D->OCOLR   = color;                                         // 设置填充应用的色彩,格局应该与设置的色彩格局雷同
DMA2D->OMAR    = (uint32_t)pDst;                                // 填充区域的起始内存地址
DMA2D->OOR     = lineOff;                                       // 行偏移,即跳过的像素,留神是以像素为单位
DMA2D->OPFCCR  = pixelFormat;                                   // 设置色彩格局
DMA2D->NLR     = (uint32_t)(width << 16) | (uint16_t)height;    // 设置填充区域的宽和高,单位是像素

/* 启动传输 */
DMA2D->CR   |= DMA2D_CR_START;   

/* 期待 DMA2D 传输实现 */
while (DMA2D->CR & DMA2D_CR_START) {} 

}

为了不便编写代码,咱们再包装一个针对所应用屏幕坐标系的矩形填充函数:

void FillRect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color){

void* pDist = &(((uint16_t*)framebuffer)[y*320 + x]);
DMA2D_Fill(pDist, w, h, 320 - w, LTDC_PIXEL_FORMAT_RGB565, color);

}

最初咱们尝试用代码把本大节刚开始的示例图表画进去:

// 填充背景色
FillRect(0, 0, 320, 240, 0xFFFF);
// 绘制数据条
FillRect(80, 80, 20, 120, 0x001f);
FillRect(120, 100, 20, 100, 0x001f);
FillRect(160, 40, 20, 160, 0x001f);
FillRect(200, 60, 20, 140, 0x001f);
// 绘制 X 轴
FillRect(40, 200, 240, 1, 0x0000);

代码运行成果:

image.png
2. 图片显示(内存复制)

假如咱们当初要开发一个游戏,而后想在屏幕上显示一团跳动的火焰。个别是由美工先把火焰的每一帧都画进去,而后放到同一张图片素材外面,如下图所示:

fire

而后咱们以肯定的距离轮流显示每一帧图像,就能够在屏幕上实现“跳动的火焰”这个成果了。

咱们当初略过素材文件加载到内存的过程,假如这张素材图片曾经在内存中了。而后咱们来思考如何将其中的一帧图片显示到屏幕上。通常状况下,咱们会这样实现:先计算得出每一帧的数据在内存中的地址,而后将这一帧图片的数据复制到 framebuffer 中相应的地位即可。代码相似于这样:

/**

  • 将素材中的一帧画面复制到 framebuffer 中的对应地位
  • index 为画面在帧序列中的索引
    */

static void General_DisplayFrameAt(uint16_t index) {

// 宏阐明
// #define FRAME_COUNTS     25  // 帧数量
// #define TILE_WIDTH_PIXEL 96  // 每一帧画面的宽度(等于高度)// #define TILE_COUNT_ROW   5   // 素材中每一行有多少帧

// 计算帧起始地址
uint16_t *pStart = (uint16_t *) img_fireSequenceFrame;
pStart += (index / TILE_COUNT_ROW) * (TILE_WIDTH_PIXEL * TILE_WIDTH_PIXEL * TILE_COUNT_ROW);
pStart += (index % TILE_COUNT_ROW) * TILE_WIDTH_PIXEL;

// 计算素材地址偏移
uint32_t offlineSrc = (TILE_COUNT_ROW - 1) * TILE_WIDTH_PIXEL;
// 计算 framebuffer 地址偏移(320 是屏幕宽度)uint32_t offlineDist = 320 - TILE_WIDTH_PIXEL;

// 将数据复制到 framebuffer
uint16_t* pFb = (uint16_t*) framebuffer;
for (int y = 0; y < TILE_WIDTH_PIXEL; y++) {memcpy(pFb, pStart, TILE_WIDTH_PIXEL * sizeof(uint16_t));
    pStart += offlineSrc + TILE_WIDTH_PIXEL;
    pFb += offlineDist + TILE_WIDTH_PIXEL;
}

}

可见要实现这个成果须要大量的内存复制操作。在嵌入式零碎中,须要大量数据复制的时候,硬件 DMA 的效率是最高的。然而硬件 DMA 只能搬运地址间断的数据,而这里,须要复制的数据在源图片和 frambuffer 中的地址都是不间断的,这引来了额定的开销(与第一大节中呈现的问题雷同),也导致咱们无奈应用硬件 DMA 来进行高效的数据复制。

所以,尽管咱们实现了指标,然而效率不高(或者说没有达到最高)。

为了以最快的速度把素材图片中的某一块数据搬运到帧缓冲中,咱们来看如何应用 DMA2D 来实现。

首先,因为这次是要在存储器中进行数据复制,所以咱们要把 DMA2D 的工作模式设定为“存储器到存储器模式”,这通过设置 DMA2D 的 CR 寄存器的 [17:16] 位为 00 来实现,代码如下:

DMA2D->CR = 0x00000000UL;

而后咱们要别离设置源和指标的内存地址,与第一节中不同,因为数据源也存在内存偏移,所以咱们要同时设定源和指标地位的数据偏移

DMA2D->FGMAR = (uint32_t)pSrc; // 源地址
DMA2D->OMAR = (uint32_t)pDst; // 指标地址
DMA2D->FGOR = OffLineSrc; // 源数据偏移(像素)
DMA2D->OOR = OffLineDst; // 指标地址偏移(像素)

而后仍然是设置要复制的图像的宽和高,以及色彩格局,这点与第一大节中的雷同

DMA2D->FGPFCCR = pixelFormat;
DMA2D->NLR = (uint32_t)(xSize << 16) | (uint16_t)ySize;

同样的形式,咱们开启 DMA2D 的传输,并期待传输实现:

/ 启动传输 /
DMA2D->CR |= DMA2D_CR_START;

/ 期待 DMA2D 传输实现 /
while (DMA2D->CR & DMA2D_CR_START) {}

最终咱们抽出的函数如下:

static void DMA2D_MemCopy(uint32_t pixelFormat, void pSrc, void pDst, int xSize, int ySize, int OffLineSrc, int OffLineDst)
{

/* DMA2D 配置 */
DMA2D->CR      = 0x00000000UL;
DMA2D->FGMAR   = (uint32_t)pSrc;
DMA2D->OMAR    = (uint32_t)pDst;
DMA2D->FGOR    = OffLineSrc;
DMA2D->OOR     = OffLineDst;
DMA2D->FGPFCCR = pixelFormat;
DMA2D->NLR     = (uint32_t)(xSize << 16) | (uint16_t)ySize;

/* 启动传输 */
DMA2D->CR   |= DMA2D_CR_START;

/* 期待 DMA2D 传输实现 */
while (DMA2D->CR & DMA2D_CR_START) {}

}

为了不便,咱们包装一个调用它的函数:

static void DMA2D_DisplayFrameAt(uint16_t index){

uint16_t *pStart = (uint16_t *)img_fireSequenceFrame;
pStart += (index / TILE_COUNT_ROW) * (TILE_WIDTH_PIXEL * TILE_WIDTH_PIXEL * TILE_COUNT_ROW);
pStart += (index % TILE_COUNT_ROW) * TILE_WIDTH_PIXEL;
uint32_t offlineSrc = (TILE_COUNT_ROW - 1) * TILE_WIDTH_PIXEL;


DMA2D_MemCopy(LTDC_PIXEL_FORMAT_RGB565, (void*) pStart, pDist, TILE_WIDTH_PIXEL, TILE_WIDTH_PIXEL, offlineSrc, offlineDist);

}

而后轮流播放每一帧图片,这里设置的帧距离是 50 毫秒,并且将指标地址定义到了 frambuffer 的地方:

while(1){

for(int i = 0; i < FRAME_COUNTS; i++){DMA2D_DisplayFrameAt(i);
    HAL_Delay(FRAME_TIME_INTERVAL);
}

}

最终运行的成果:

fire.gif
3. 图片突变切换

假如咱们要开发一个看图利用,在两张图片进行切换时,间接进行切换会显得比拟僵硬,所以咱们要退出切换时的动态效果,而突变切换(淡入淡出)是一个很常常应用的,而且看起来还不错的成果。

就用这两张图片好了:

image.png

这里咱们须要先理解一下透明度混合(Alpha Blend)的基本概念。首先透明度混合须要有一个前景,一个背景。而混合的后果就相当于透过前景看背景时的成果。如果前景齐全不通明,那么就齐全看不到背景,反之如果前景齐全通明,那么就只能看到背景。而如果前景是半透明的,则后果就是两者依据前景色的透明度依照肯定的规定进行混合。

如果 1 示意齐全通明,0 示意不通明,则透明度的混合公式如下,其中 A 是背景色,B 是前景色:

X(C)=(1-alpha)X(B) + alphaX(A)

因为色彩有 RGB 三个通道,所以咱们须要对三通道都进行计算,计算实现后在进行组合:

R(C)=(1-alpha)R(B) + alphaR(A)
G(C)=(1-alpha)G(B) + alphaG(A)
B(C)=(1-alpha)B(B) + alphaB(A)

而在程序中为了效率起见(CPU 对于浮点的运算速度很慢),咱们并不必 0~1 这个范畴的值。通常状况下咱们个别会应用一个 8bit 的数值来示意透明度,范畴从 0~255。须要留神的是,这个数值越大示意越不通明,也就是说 255 是齐全不通明,而 0 示意齐全通明(所以也叫不透明度),而后咱们能够失去最终的公式:

outColor = ((int) (fgColor alpha) + (int) (bgColor) (256 – alpha)) >> 8;

实现 RGB565 色彩格局像素的透明度混合代码:

typedef struct{

uint16_t r:5;
uint16_t g:6;
uint16_t b:5;

}RGB565Struct;

static inline uint16_t AlphaBlend_RGB565_8BPP(uint16_t fg, uint16_t bg, uint8_t alpha) {

RGB565Struct *fgColor = (RGB565Struct*) (&fg);
RGB565Struct *bgColor = (RGB565Struct*) (&bg);
RGB565Struct outColor;

outColor.r = ((int) (fgColor->r * alpha) + (int) (bgColor->r) * (256 - alpha)) >> 8;
outColor.g = ((int) (fgColor->g * alpha) + (int) (bgColor->g) * (256 - alpha)) >> 8;
outColor.b = ((int) (fgColor->b * alpha) + (int) (bgColor->b) * (256 - alpha)) >> 8;


return *((uint16_t*)&outColor); 

}

理解了透明度混合的概念,也实现了单个像素的透明度混合后,咱们来看如何实现图片的突变切换。

假如整个突变在 30 帧内实现,咱们须要在内存中开拓一块儿大小等于图片的缓冲区。而后咱们以第一张图片(以后显示的图片)为背景,第二张图片(接下来显示的图片)为前景,而后为前景设置一个透明度,对每个像素进行透明度混合,并且将混合后果暂存至缓冲区中。待混合完结后,将缓冲区中的数据复制到 framebuffer 中即实现了一帧的显示。接下来持续进行第二帧、第三帧……逐步增大前景的不透明度,直到前景色的变为不通明,即实现了图片的突变切换。

因为每一帧都须要对两张图片中的每一个像素都进行混合运算,这带了来微小的运算量。交给 CPU 实现是很不明智的行为,所以咱们还是把这些工作交给 DMA2D 来实现吧。

这次用到了 DMA2D 的混合性能,所以咱们要使能 DAM2D 的带颜色混合的存储器到存储器模式,对应 CR 寄存器 [17:16] 位的值为 10,即:

DMA2D->CR = 0x00020000UL; // 设置工作模式为存储器到存储器并带颜色混合

而后别离设置前景、背景和输入数据的内存地址和数据传输偏移、传输图像的宽和高:

DMA2D->FGMAR = (uint32_t)pFg; // 设置前景数据内存地址
DMA2D->BGMAR = (uint32_t)pBg; // 设置背景数据内存地址
DMA2D->OMAR = (uint32_t)pDst; // 设置数据输入内存地址

DMA2D->FGOR = offlineFg; // 设置前景数据传输偏移
DMA2D->BGOR = offlineBg; // 设置背景数据传输偏移
DMA2D->OOR = offlineDist; // 设置数据输入传输偏移

DMA2D->NLR = (uint32_t)(xSize << 16) | (uint16_t)ySize; // 设置图像数据宽高(像素)

设置色彩格局。这里设置前景色的色彩格局时须要留神,因为如果应用的是 ARGB 这样的色彩格局,那么咱们进行透明度混合时,色彩数据中自身的 alpha 通道就会对混合后果产生影响,所以咱们这里要设定在进行混合操作时,疏忽前景色本身的 alpha 通道。并强制设定混合时的透明度。

输入色彩格局和背景色彩格局

DMA2D->FGPFCCR = pixelFormat // 设置前景色色彩格局

    | (1UL << 16)                       // 疏忽前景色彩数据中的 Alpha 通道
    | ((uint32_t)opa << 24);            // 设置前景色不透明度

DMA2D->BGPFCCR = pixelFormat; // 设置背景色彩格局
DMA2D->OPFCCR = pixelFormat; // 设置输入色彩格局

tips0:有时咱们会遇到一张带有通明通道的图片与背景叠加显示的状况,此时就不应该禁用色彩自身的 alpha 通道

tips1:这个模式下,咱们不仅能够进行颜色混合,还能够同时转换色彩格局,能够依据须要设置前景和背景以及输入的色彩格局

最初,启动传输即可:

/ 启动传输 /
DMA2D->CR |= DMA2D_CR_START;

/ 期待 DMA2D 传输实现 /
while (DMA2D->CR & DMA2D_CR_START) {}

残缺代码如下:

void _DMA2D_MixColors(void pFg, void pBg, void* pDst,

    uint32_t offlineFg, uint32_t offlineBg, uint32_t offlineDist,
    uint16_t xSize, uint16_t ySize,
    uint32_t pixelFormat, uint8_t opa) {

DMA2D->CR    = 0x00020000UL;                // 设置工作模式为存储器到存储器并带颜色混合

DMA2D->FGMAR = (uint32_t)pFg;               // 设置前景数据内存地址
DMA2D->BGMAR = (uint32_t)pBg;               // 设置背景数据内存地址
DMA2D->OMAR  = (uint32_t)pDst;              // 设置数据输入内存地址

DMA2D->FGOR  = offlineFg;                   // 设置前景数据传输偏移
DMA2D->BGOR  = offlineBg;                   // 设置背景数据传输偏移
DMA2D->OOR   = offlineDist;                 // 设置数据输入传输偏移

DMA2D->NLR = (uint32_t)(xSize << 16) | (uint16_t)ySize; // 设置图像数据宽高(像素)DMA2D->FGPFCCR = pixelFormat                // 设置前景色色彩格局
        | (1UL << 16)                       // 疏忽前景色彩数据中的 Alpha 通道
        | ((uint32_t)opa << 24);            // 设置前景色不透明度

DMA2D->BGPFCCR = pixelFormat;               // 设置背景色彩格局
DMA2D->OPFCCR  = pixelFormat;                // 设置输入色彩格局

/* 启动传输 */
DMA2D->CR   |= DMA2D_CR_START;

/* 期待 DMA2D 传输实现 */
while (DMA2D->CR & DMA2D_CR_START) {}

}

编写测试代码,这次不须要二次包装函数了:

void DMA2D_AlphaBlendDemo(){

const uint16_t lcdXSize = 320, lcdYSize = 240;
const uint8_t cnvFrames = 60; // 60 帧实现切换
const uint32_t interval = 33; // 每秒 30 帧
uint32_t time = 0;

// 计算输入地位的内存地址
uint16_t distX = (lcdXSize - DEMO_IMG_WIDTH) / 2;
uint16_t distY = (lcdYSize - DEMO_IMG_HEIGHT) / 2;
uint16_t* pFb = (uint16_t*) framebuffer;
uint16_t* pDist = pFb + distX + distY * lcdYSize;
uint16_t offlineDist = lcdXSize - DEMO_IMG_WIDTH;

uint8_t nextImg = 1;
uint16_t opa = 0;
void* pFg = 0;
void* pBg = 0;
while(1){
    // 切换前景 / 背景图片
    if(nextImg){pFg = (void*)img_cat;
        pBg = (void*)img_fox;
    }
    else{pFg = (void*)img_fox;
        pBg = (void*)img_cat;
    }

    // 实现切换
    for(int i = 0; i < cnvFrames; i++){time = HAL_GetTick();
        opa = 255 * i / (cnvFrames-1);
        _DMA2D_MixColors(pFg, pBg, pDist,
                0,0,offlineDist,
                DEMO_IMG_WIDTH, DEMO_IMG_HEIGHT,
                LTDC_PIXEL_FORMAT_RGB565, opa);
        time = HAL_GetTick() - time;
        if(time < interval){HAL_Delay(interval - time);
        }
    }
    nextImg = !nextImg;
    HAL_Delay(5000);
}

}

最终成果:

GIF.gif
性能比照

后面介绍了三种嵌入式图形开发种的实例,并对别离介绍了通过传统和 DMA2D 实现的办法。这时候必定有敌人会问,DMA2D 实现,比起传统办法实现,到底能快多少呢?咱们来理论测试一下。

独特的测试条件如下:

framebuffer 搁置在 SDRAM 中,320x240,RGB565
SDRAM 工作频率 100MHz,CL2,16 位带宽。MCU 为 STM32H750XB,主频 400MHz,开启 I -Cache 和 D -Cache
代码和资源在外部 Flash 上,64 位 AXI 总线,速度为 200MHz。GCC 编译器(版本:arm-atollic-eabi-gcc-6.3.1)

矩形填充

测试方法:

绘制上一章第 1 节中的图表,绘制 10000 次,统计后果

测试后果:
绘制形式 耗费工夫 (-O0) 耗费工夫(-O3)
软件实现 39641 ms 9930 ms
DMA2D 9827 ms 9817 ms
内存复制

测试方法:

绘制上一章第 2 节中的序列帧 10000 帧,统计后果

测试后果:
绘制形式 耗费工夫 (-O0) 耗费工夫(-O3)
软件实现 68787 ms 48654 ms
DMA2D 26201 ms 26160 ms
透明度混合

测试方法:

突变切换上一章第 3 大节中的两张图片 100 次,每次 30 帧实现,共计 3000 帧
混合后果间接输入到 framebuffer,不再通过缓冲区缓冲

测试后果:
绘制形式 耗费工夫 (-O0) 耗费工夫(-O3)
软件实现 20824 ms 2617 ms
DMA2D 681 ms 681 ms
性能测试总结

由下面的测试后果能够看出,DAM2D 至多有 2 个劣势:

一是速度更快:在局部我的项目中,DMA2D 实现的速度相比纯软件实现最高能够达到 30 倍的差距!这还是在主频高达 400MHz 还带 L1-Cache 的 STM32H750 平台上测试的后果,如果是在无 cache 且主频较低的 STM32F4 平台上进行测试,差距会进一步拉大。

二是性能更加稳固:由测试后果能够看出,DMA2D 实现的形式受编译器优化等级的影响十分小,简直能够忽略不计,这意味着,无论你应用 IAR,GCC 或是 MDK,应用 DMA2D 都能够达到雷同的性能体现。不会呈现同一段代码移植后性能相差很大的状况。

除这两个直观的后果外,其实还有第三点劣势,那就是代码编写更加简略。DMA2D 的寄存器不多,而且比拟直观。在某些场合,应用起来要比软件实现不便的多。
结语

本文中的三个实例,都是我自己在嵌入式图形开发中常常遇到的状况。实际上,DMA2D 的用法还有很多,有趣味的话能够参考《STM32H743 中文编程手册》中的相干内容,置信有了本文的根底,在浏览外面的内容时肯定会事倍功半。

受限于作者的技术,文章中的内容无奈做到 100% 的正确,如果存在谬误,请大家指出,谢谢。

原文链接:https://club.rt-thread.org/as…

退出移动版