关于程序员: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…

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理