STM32 DMA2D技术

前言

最近在研究嵌入式GUI的东西,发现GUI的库有很多种,国内的AWTK,国外的Qt for MCUs、touchgfx、lvgl、emwin等等,而有一些GUI的库会特别说明支持特定的MCU。那么这些在这些MCU中有什么特别的东西能够更好的去支持嵌入式GUI呢?带着这个疑问,我去看了一下这类特别的MCU的文档,发现了一点是共性的,就是他们都有自己的硬件加速模块。对于嵌入式MCU来说,拥有一个“GPU”是一个很奢侈的事情,那么这类的“GPU”究竟有什么奥秘呢?本文将要从使用的角度来分析DMA2D模块,介绍DMA2D在嵌入式图形开发中的可以发挥的作用。

 准备工作

硬件准备

 可以使用任何带有DMA2D外设的STM32开发板来验证本文中的例子,如STM32F429,STM32F746,STM32H750等MCU的开发板。本文中使用的开发板是正点原子的STM32F429的开发板,显示屏可以是任意的彩色TFT显示屏,推荐使用16位或24位颜色的RGB接口显示屏。

 开发环境

本文中介绍的内容和出现的代码可以在任何你喜欢的开发环境中使用,如MDK,IAR等。

开始本文的实验前你需要一个以framebuffer技术驱动LCD显示屏的基本工程。运行本文中所有的代码前都需要预先使能DMA2D。

DMA2D的简介

我们先来看看ST是怎么描述DMA2D的:

从这里的简介可以看出来,DMA2D首先是一个DMA,DMA能做的内存操作DMA2D肯定是不成问题,其次DMA2D有自己独有的颜色填充(也叫寄存器到存储器)、颜色格式转换、透明度混合(层混合),所以总结一下DMA2D的功能点就如下了:

  1. 颜色填充(矩形区域)(寄存器到存储器)
  2. 图像(内存)复制(存储器到存储器)
  3. 颜色格式转换(如YCbCr转RGB或RGB888转RGB565)
  4. 透明度混合(Alpha Blend)

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

DMA2D系统框图

ST官方提供的DMA2D的系统框图如下图:

根据系统框图来看,STM32F4系列的DMA2D模块主要分为4个部分:

  1. FG FIFO、BG FIFO(前景层FIFO、背景层FIFO)
  2. FG PFC、BG PFC(前景层/背景层颜色转换器Pixel Format Convertor)
  3. 混合器(alpha blending)
  4. OUT PFC(输出颜色转换器)

简化来看就像ST官方PPT画出来的这样:

FG FIFO与BG FIFO 

 FG FIFO(Foreground FIFO)与BG FIFO(Backgroun FIFO)是两个64x32位大小的缓冲区,它们用于缓存从AHB总线获取的像素数据,分别专用于缓冲前景层和背景层的数据源。AHB总线的数据源一般是RAM,使用RAM的部分空间作为显存(framebuffer)。

FG PFC与BG PFC

FG PFC(FG Pixel Format Convertor)与BG PFC(BG Pixel Format Convertor)是两个像素格式转换器,分别用于前景层和背景层的像素格式转换,不管从 FIFO 的数据源格式如何,都把它转化成字的格式(即32位),ARGB8888。

图中的“ɑ”表示 Alpha,即透明度,经过 PFC,透明度会被扩展成 8 位的格式。

图中的“CLUT”表示颜色查找表(Color Lookup Table),颜色查找表是一种间接的颜色表示方式,它使用一个 256x32 位的空间缓存 256 种颜色,颜色的格式是 ARGB8888 或RGB888。见下图,利用颜色查找表,实际的图像只使用这 256 种颜色,而图像的每个像素使用 8位的数据来表示,该数据并不是直接的 RGB 颜色数据,而是指向颜色查找表的地址偏移,即表示这个像素点应该显示颜色查找表中的哪一种颜色。在图像大小不变的情况下,利用颜色查找表可以扩展颜色显示的能力,其特点是用 8位的数据表示了一个 24或32 位的颜色,但整个图像颜色的种类局限于颜色表中的 256 种。DMA2D 的颜色查找表可以由 CPU自动加载或编程手动加载。

 混合器

FIFO 中的数据源经过 PFC像素格式转换器后,前景层和背景层的图像都输入到混合器中运算,运算公式如下:

从公式可以了解到混合器的运算主要是使用前景和背景的透明度作为因子,对像素RGB颜色值进行加权运算。经过混合器后,两层数据合成为一层ARGB8888格式的图像。

OUTPFC 

OUT PFC是输出像素格式转换器,它把混合器转换得到的图像转换成目标格式,如ARGB8888、RGB888、RGB565、ARGB1555 或 ARGB4444,具体的格式可根据需要在输出 PFC控制寄存器 DMA2D_OPFCCR中选择。

小节

STM32F429 芯片使用 LTDC(或者FMC)、DMA2D 及RAM存储器,构成了一个完整的液晶控制器。LTDC负责不断刷新液晶屏,DMA2D用于图像数据搬运、混合及格式转换,RAM存储器作为显存。其中显存可以使用 STM32 芯片内部的 SRAM 或外扩 SDRAM/SRAM,只要容量足够大即可(framebuffer≥1)。

 DMA2D颜色填充

简介

颜色填充,也叫DMA2D的从寄存器到存储器的模式,作用是将一块矩形(目前F4系列的DMA2D看起来只有矩形)的区域按照指定的寄存器中的值更新成该值。简单点说,就是用特定的一种颜色填出目标图像的某一部分或者全部。例如想要生成一些简单的柱状图的时候就可以用这个模式。

 原理

DMA2D实现颜色填充的过程主要如下:

  1. 首先将想要填充的颜色的格式、颜色的数据、填充的起点在输出(送显)区域的起始位置、输出(送显)图像的分辨率(图像的长宽,最小单位为像素)、输出的列偏移、输出图像的格式等写入相应的寄存器中。
  2. 启动DMA2D的颜色填充模式,DMA2D会将颜色的数据按照设置好的区域写入需要送显的framebuffer中的指定区域
  3. 写完成后置起相应标志位,或者配置错误置起相应标志位。

在上述步骤1中,设置了很多寄存器,分别对应的是什么呢?

在分析DMA2D的寄存器之前,我们需要知道图像在计算机系统里面的储存方式。一般来说,一张储存在内存中的图片,他的地址是一段连续的、线性的地址,比如想要储存一张分辨率为6*10的图像,那么按照像素点作为一个最小的储存单元来看(不同的图像像素点占用的字节不一定是一样的),是一块连续的内存,并且可以看成是一个长和宽根据图像的分辨率确定的矩形。下图为6*10的图像的内存模型:

 那么如果想要把这张6*10的图片的某一块矩形区域填充成其他颜色时,此时可以看到下图蓝色区域为填充颜色的区域,可以看出矩形区域的内存地址是不连续的。

那么知道了矩形填充以后就不难理解DMA2D在颜色填充时需要配置的寄存器了,主要有以下几个:

  1. DMA2D_CR:将DMA2D设置为寄存器到存储器模式
  2. DMA2D_OCOLR:设置填充颜色的数据
  3. DMA2D_OPFCCR:设置输出区域颜色格式
  4. DMA2D_OMAR:设置输出区域的首地址
  5. DMA2D_NLR:设置需要填充区域的分辨率(硬件填充的最小单位为像素,类似于上图中每个序号(格子)里面可能是2个字节的RGB565,也有可能是4个字节的ARGB8888
  6. DMA2D_OOR:输出的行偏移量

上诉寄存器在实际的内存中的作用如下图:

 使用方式

按照DMA2D颜色填充的原理可以开始准备代码进行测试了,DMA2D填充一个区域的代码如下:

/* DMA 2D R2M */
static void DMA2D_Fill(void* pDst, uint32_t width, uint32_t height, uint32_t lineOff, uint32_t pixelFormat, uint32_t color) {
    /* DMA2D config */
    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;

    /* start transmit */
    DMA2D->CR |= DMA2D_CR_START;

    /* wait send complete */
    while (DMA2D->CR & DMA2D_CR_START) {
    }
}

Tips0:大多数情况下,使用HAL库可以简化代码编写,提高可移植性。但是在DMA2D的使用时则是个例外。因为HAL库存在的最大问题就是嵌套层数再加上各种安全检测过多效率不够高。在操作别的外设时,使用HAL库损失的效率并不会有多大的影响。但是对于DMA2D这种以计算和加速为目的的外设,考虑到相关的操作会在一个屏幕的绘制周期内被多次调用,此时再使用HAL库就会导致DAM2D的加速效率严重下降。

Tips1:如果你使用了OS,则可以使能DMA2D的传输完毕中断。然后我们可以创建一个信号量并且在开启传输后等待它,随后在DMA2D的传输完毕中断服务函数中释放该信号量。这样的话CPU就可以在DMA2D工作的时候去干点别的事儿而不是在此处傻等。

Tips2:当然,由于实际执行时,DMA2D进行内存填充的速度实在是太快了,以至于OS切换任务的开销都比这个时间要长,所以即便使用了OS,我们还是会选择死等 :)

 为了方便使用,这里再封装一层函数去计算填充矩形的起始地址以及行偏移量,如下代码所示:

#define PIXEL_WIDTH (320U)
#define PIXEL_HEIGHT (240U)
void FillRect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color) {
    void* pDist = &(((uint16_t*)framebuffer)[y * PIXEL_WIDTH + x]);
    DMA2D_Fill(pDist, w, h, PIXEL_WIDTH - w, 0x2, color);
}

 那么,简介中的矩形填充的图像按照如下代码就可以显示出来了:

        FillRect(0, 0, 320, 240, WHITE);

        FillRect(80, 80, 20, 120, 0x001f);
        FillRect(120, 100, 20, 100, 0x001f);
        FillRect(160, 40, 20, 160, 0x001f);
        FillRect(200, 60, 20, 140, 0x001f);

        FillRect(40, 200, 240, 1, 0x0000);
        LCD_update_framebuffer(0, 0, 320, 240, framebuffer);

 代码运行效果:

 DMA2D图像(内存)复制

简介

大部分人都知道,动画是由一帧一帧的图片按照一定刷新率显示在出来的,所以从图片素材里面按块复制素材,并且显示出来的这种方法就可以使用DMA2D的内存复制。比如一个动画素材如下图:

 Tips0:上面的图片素材的格式为jpeg格式,如果一个屏幕需要显示jpeg格式的数据的话必须把jpeg格式的图片进行解码,否则jpeg的原始数据是一个压缩过后的数据,无法送显到屏幕上;而STM32F4系列的MCU里面是不带硬件jpeg decode模块的(貌似F7系列支持),而用软件decode比较占代码量和效率,而本文并不是针对jpeg图片的编解码方法进行描述的,所以在将图片素材存到flash中时,请先将图片解码,我写了一个python脚本去将jpeg或者其他格式的图片解码成RGB565数据。

github地址:https://github.com/Ilikecoppy/jpeg2RGB565.git

 原理

DMA2D的内存复制的大致原理如下:

从图中来看这就是DMA2D作为DMA控制器的老本行了,将一块指定的内存搬到另一个地方,也就是说,将存在flash(或者其他的储存介质)的图像素材,根据实际需要显示的大小进行裁剪,确定需要送显的区域,然后DMA2D就开始根据规则执行内存搬运了。

下面是STM32F4中文参考手册里面对于内存复制的描述:

 也就是说内存复制需要确定两个窗口大小,第一个是对源数据“开窗”确认需要搬运的数据,第二个是对目标数据进行“开窗”确认需要显示的数据。而DMA2D利用前景层的寄存器对源数据进行处理,利用输出层的寄存器对目标数据进行处理。那么DMA2D里面与之有关的寄存器就很好理解了,主要有下列几个:

  1. DMA2D_CR:将DMA2D设置为存储器到存储器模式
  2. DMA2D_FGMAR:设置源数据的首地址
  3. DMA2D_OMAR:设置目标数据的首地址
  4. DMA2D_FGOR:设置源数据行偏移量
  5. DMA2D_OOR:设置输出的行偏移量
  6. DMA2D_FGPFCCR:设置源数据的颜色格式
  7. DMA2D_OPFCCR:设置输出数据的颜色格式
  8. DMA2D_NLR:设置需要填充区域的分辨率(硬件填充的最小单位为像素,类似于上图中每个序号(格子)里面可能是2个字节的RGB565,也有可能是4个字节的ARGB8888)

 使用方式

参考上一节的原理部分,那么DMA2D内存复制的代码参考如下:

static void DMA2D_MemCopy(uint32_t pixelFormat, void* pSrc, void* pDst, int xSize, int ySize, int OffLineSrc, int OffLineDst) {
    /* DMA2D config M2M */
    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->OPFCCR = pixelFormat;
    /* start transmit */
    DMA2D->CR |= DMA2D_CR_START;

    /* wait send complete */
    while (DMA2D->CR & DMA2D_CR_START) {
    }
}

那么为了方便显示简介中小人打架的动画,我们需要把图像数据中一些基本的位置信息做成一个表来实现查表,位置信息表实现如下:

/*
 *   {start_addr, offset_width, height}
 */
const uint32_t fight_image_index[][3] = {
    {0, 62, 61},
    {62, 76, 61},
    {138, 70, 61},
    {208, 62, 61},
    {270, 62, 61},
    {332, 68, 61},
    {400, 63, 61},
    {31000, 62, 66},
    {31062, 64, 66},
    {31126, 60, 66},
    {31186, 64, 66},
    {31250, 70, 66},
    {31320, 92, 66},
    {64000, 104, 80},
    {64104, 117, 80},
    {64221, 123, 80},
    {64344, 138, 80},
    {104000, 132, 60},
    {104132, 122, 60},
    {104260, 135, 60},
    {128000, 130, 65},
    {128130, 130, 65},
};

 然后我们再封装一个函数来调用内存复制的函数,查表找到每个图应该显示的位置即可,代码如下:

static void DMA2D_display_fighting(uint8_t index) {
    uint32_t offlineSrc;
    uint16_t* pStart = (uint16_t*)fight_image;
    void* pDist = (uint16_t*)framebuffer;

    pStart += fight_image_index[index][0];
    offlineSrc = TILE_WIDTH_PIXEL - fight_image_index[index][1];
    uint32_t offlineDist = 0;

    DMA2D_MemCopy(0x2, (void*)pStart, pDist, fight_image_index[index][1], fight_image_index[index][2], offlineSrc, offlineDist);
}

 然后就可以看到动画了:

/* todo:先留着,视频转码了以后再补充 */

DMA2D图像混合

 简介

假设我们要开发一个GUI界面,在两张图片进行切换时,直接进行切换会显得比较生硬,所以我们要加入切换时的动态效果,而渐变切换(淡入淡出)是一个很经常使用的,而且看起来还不错的效果。比如说我们拿如下两张照片图像混合。

 

 原理

这里我们需要先了解一下透明度混合(Alpha Blend)的基本概念。首先透明度混合需要有一个前景,一个背景。而混合的结果就相当于透过前景看背景时的效果。如果前景完全不透明,那么就完全看不到背景,反之如果前景完全透明,那么就只能看到背景。而如果前景是半透明的,则结果就是两者根据前景色的透明度按照一定的规则进行混合。

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

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

 因为颜色有RGB三个通道,所以我们需要对三通道都进行计算,计算完成后在进行组合:

R(C)=(1-alpha)*R(B) + alpha*R(A)

G(C)=(1-alpha)*G(B) + alpha*G(A)

B(C)=(1-alpha)*B(B) + alpha*B(A)

而在程序中为了效率起见(一般的MCU对于浮点的运算速度很慢),我们并不用0~1这个范围的值。通常情况下我们一般会使用一个8bit的数值来表示透明度,范围从0~255。需要注意的是,这个数值越大表示越透明,也就是说255是完全不透明,而0表示完全透明(所以也叫不透明度),然后我们可以得到最终的公式:

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

 DMA2D硬件混合原理

 在STM32的DMA2D里面,对于图像混合的描述如下:

可以看出,图像混合使用了两个图层,前景层和后景层。很好理解,两层blending用自然需要用两个图层,并且两个图层里面在做混合(alpha blending)前必须经过了格式转换,否则数据都对应不上也谈不上混合了,剩下的就是交给硬件混合模块去完成的事情了,在每个像素点完成混合之后,就送给输出模式转换器(OUTPFC)模块去处理送显的地址以及大小了。

那么DMA2D里面与之有关的寄存器就很好理解了,主要有下列几个:

  1. DMA2D_CR:将DMA2D设置为混合模式
  2. DMA2D_FGMAR:设置前景层数据的首地址
  3. DMA2D_BGMAR:设置后景层数据的首地址
  4. DMA2D_OMAR:设置目标数据的首地址
  5. DMA2D_FGOR:设置前景层数据行偏移量
  6. DMA2D_BGOR:设置后景层数据行偏移量
  7. DMA2D_OOR:设置输出的行偏移量
  8. DMA2D_FGPFCCR:设置源数据的混合颜色格式
  9. DMA2D_OPFCCR:设置输出数据的颜色格式
  10. DMA2D_NLR:设置需要填充区域的分辨率(硬件填充的最小单位为像素,类似于上图中每个序号(格子)里面可能是2个字节的RGB565,也有可能是4个字节的ARGB8888)

 使用方式

按照DMA2D图像混合的原理可以开始准备代码进行测试了,DMA2D图像混合的代码如下:

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) | ((uint32_t)opa << 24);    DMA2D->BGPFCCR = pixelFormat;  // 设置背景颜色格式
    DMA2D->OPFCCR = pixelFormat;

    /* start transmit */
    DMA2D->CR |= DMA2D_CR_START;

    /* wait send complete */
    while (DMA2D->CR & DMA2D_CR_START) {
    }
}

我们根据0~255个透明度等级设置依次调用设置透明度的函数:

        while (1) {
            for (uint8_t i = 0; i < 255; i++) {
                /* code */
                DMA2D_MixColors((void*)teacher_ma1, (void*)teacher_ma2, framebuffer, 0, 0, 0, 200, 160, 0x2, i);
                LCD_update_framebuffer(0, 0, 200, 160, framebuffer);
                delay_ms(10);
            }
        }

实际的效果:

/* todo:先留着,视频转码了以后再补充 */

总结

性能测试总结

由上面的测试结果可以看出,DAM2D至少有2个优势:

一是速度更快:在部分项目中,DMA2D实现的速度相比纯软件实现最高可以达到30倍的差距。

二是性能更加稳定:由测试结果可以看出,DMA2D实现的方式受编译器优化等级的影响非常小,几乎可以忽略不计,这意味着,无论你使用IAR,GCC或是MDK,使用DMA2D都可以达到相同的性能表现。不会出现同一段代码移植后性能相差很大的情况。

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

本文与DMA2D有关的代码已放在gitee上:

gitee地址:https://gitee.com/Ilikecoppy/STM32_DMA2D.git

  • 14
    点赞
  • 71
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
以下是STM32H7中DMA2D的配置代码示例: ```c /* 初始化DMA2D */ DMA2D_HandleTypeDef hdma2d; hdma2d.Instance = DMA2D; hdma2d.Init.Mode = DMA2D_M2M_BLEND; hdma2d.Init.ColorMode = DMA2D_ARGB8888; hdma2d.Init.OutputOffset = 0; hdma2d.LayerCfg[1].InputOffset = 0; hdma2d.LayerCfg[1].InputColorMode = DMA2D_INPUT_ARGB8888; hdma2d.LayerCfg[1].AlphaMode = DMA2D_COMBINE_ALPHA; hdma2d.LayerCfg[1].InputAlpha = 0xFF; hdma2d.LayerCfg[1].RedBlueSwap = DMA2D_RB_SWAP; HAL_DMA2D_Init(&hdma2d); /* 配置DMA2D的源和目标地址 */ DMA2D->FGMAR = (uint32_t)SrcAddress; // 源地址 DMA2D->OMAR = (uint32_t)DstAddress; // 目标地址 /* 配置DMA2D的源和目标像素格式 */ DMA2D->FGPFCCR = DMA2D_ARGB8888; // 源像素格式 DMA2D->OPFCCR = DMA2D_ARGB8888; // 目标像素格式 /* 配置DMA2D的源和目标像素宽度和高度 */ DMA2D->FGMAR = SrcWidth; // 源像素宽度 DMA2D->OMAR = DstWidth; // 目标像素宽度 DMA2D->FGOR = SrcHeight; // 源像素高度 DMA2D->OOR = DstHeight; // 目标像素高度 /* 启动DMA2D传输 */ HAL_DMA2D_Start(&hdma2d, SrcAddress, DstAddress, SrcWidth, SrcHeight); ``` 这段代码展示了DMA2D的初始化和配置过程。DMA2D的使用通常需要以下步骤: 1. 初始化DMA2D句柄,包括选择DMA2D模式、颜色模式、输出偏移等参数。 2. 配置DMA2D的源和目标地址,即需要进行像素传输的两个地址。 3. 配置DMA2D的源和目标像素格式,即源和目标像素的颜色模式。 4. 配置DMA2D的源和目标像素宽度和高度,即源和目标像素的尺寸。 5. 启动DMA2D传输。 需要注意的是,DMA2D的使用和配置方法可能因芯片型号和具体需求有所不同,以上代码仅供参考。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值