STM32 的图形加速器 DMA2D
1. 背景
在实际使用 LTDC 控制器控制液晶屏时,配置好的显存地址写入要显示的像素数据,LTDC 就会把这些数据从显存中搬运到液晶面板进行显示。实际上要显示的数据量非常的大,我们常常以纯软件的方式填充显存(指定那个位置要显示什么颜色),这样非常影响绘图速度,因此我们希望能用 DMA 来操作,针对这个需求,STM32 专门定制了 DMA2D 外设,它可以用于快速绘制矩形、执行、分层数据混合、数据复制以及进行图像数据格式转换,可以把它理解为图形专用的 DMA。
2. DMA2D 工作模式
- 寄存器到存储器(此模式通常同于清屏,将寄存器中保存的颜色值搬运到显存中的某个位置,或者整个显存)
- 存储器到存储器(将内存中的数据搬到显存中)
- 存储器到存储区并执行像素格式转换
- 存储器到存储器并执行像素格式转换和混合
3. DMA2D 控制
通过对 DMA2D 控制器寄存(DMA2D_CR)配置 DMA2D 控制器,用户可以执行下列操作:
- 选择工作模式
- 使能/禁止 DMA2D 中断
- 启动/挂起/中止进行中的数据传输
4. DMA2D 前景层 FIFO 和背景层 FIFO
DMA2D 前景层(FG) FIFO 和 背景(BG)FIFO 获取要处理的输入数据。这些 FIFO 根据相应像素格式转换器(PFC)中定义的颜色格式获取像素。通过如下一组寄存器对他们进行编程:
-
DMA2D 前景层存储地址寄存器(DMA2D_FGMAR 此寄存器存的是前景层数据地址)
-
DMA2D 前景层偏移寄存器(DMA2D_FGOR )
-
DMA2D 背景层存储地址寄存器(DMA2D_BGMAR )
-
DMA2D 背景层偏移寄存器
DMA2D 在寄存器到存储器模式下工作时,不激活任何 FIFO。
DMA2D 在存储器到存储器模式下工作时(没有像素格式转换和混合操作时),仅激活FG FIFO,并将其作用为缓冲区。
DMA2D 在存储器到存储器模式下工作时并支持像素格式转换时(无混合操作),不会激活
BG FIFO。
5. DMA2D 工作原理
5.1 register to memory
说了那么多理论,我们看一下实际上 DMA2D 的优势到底在那里。当我们想要在 LCD 显示屏的中间画上几个矩形时,或者清屏时,都是使用的纯软件循环填充framnbuffer 的空间比如:
for(y = y0; y <= y1; y++)
{
for(x = x0; x <= x1; x++)
framebuffer[y][x] = color;
}
这样的代码非常耗时,根据两个坐标(x0,y0),(x1,y1) 两个坐标,确定一个平面之后,两层循环填充 frambuffer。当我们清屏时,我们可能想到用 memset 函数去填充 frambuffer,当然这样是明智的选择,也是可行的。但是,如果不想清楚整块 frambuffer(操作 frambuffer 就相当于操作 LCD屏幕了)呢?memset 只适用于连续的地址空间。比如下面这种情况
我只想要蓝色这块区域填充成红色,就不能简单的使用 memset(frambuffer,0xf800, sizeof(frambuffer))
(像素格式RGB565)了吧。因为此块区域地址不连续,因此只能使用上述循环大法,确定左上角坐标,与右下角坐标,循环填充 frambuffer 中的这块地址。
让我们看看 DMA2D 是怎么干的。
首先我们给出这块地址的首地址,也就是红色方块的地址。由于地址不连续,只想画出蓝色区域,因此告诉 DMA2D 填充完成一行后,需要跨过多少个格子才开始画下一行(注意:一个格子对应一个像素点)。比如,蓝色的一行有6个格子,因此需要跨过18、19、20、21 这四个格子之后才开始继续填充。跨越的像素个数有一个简单的计算方式,即一行像素的总个数减去要这一行要显示的像素个数,对应上图为 10 — 6 = 4。
因此画出上面的区域我们可以这样配置 DMA2D:
由于仅仅只是矩形的填充,我们将 DMA2D 配置在 寄存器到存储区模式下
DMA2D->CR = (0x3UL << 16);
然后,告诉 DMA2D 要填充的frambuffer 地址和填充的矩形区域的宽高
DMA2D->OMAR = (uint32_t)(&frambuffer[x][y]);
DMA2D->NLR = (uint32_t)(width << 16) | (uint16_t)height;
紧接着告诉画一行要跳过多少像素,由输出偏移寄存器控制,用于确定下一行的起始地址
DMA2D->ORR = LCD_PIXEL_WIDTH - Width
然后告诉DMA2D 这块区域需要的颜色以及颜色格式
DMA2D->OCOLR = color
DMA2D->OPFCCR = 0x2;// RGB565
现在可以启动 DMA2D 传输了,只需要将 CR 寄存器的 bit0 置1即可,传输结束时,此位由硬件清0,因此,还可利用此位判断是否传输完成
DMA2D->CR |= 0x1;
while(DMA2D->CR & 0x1);//等待传输完成
将上面代码整理并封装成 api,,由于是最基本的填充函数,因此将会经常调用到,所以我们将设置为内联函数,减少函数调用的开销
static inline void dma2d_fill( void * fb, uint32_t width, uint32_t height, uint32_t lineoffect, uint32_t pixelformat, uint32_t color) {
/* DMA2D配置 */
DMA2D->CR = 0x00030000UL; // 配置为寄存器到储存器模式
DMA2D->OCOLR = color; // 设置填充使用的颜色
DMA2D->OMAR = (uint32_t)fb; // 填充区域的起始内存地址
DMA2D->OOR = lineoffect; // 行偏移,即跳过的像素(像素为单位)
DMA2D->OPFCCR = pixelformat; // 设置颜色格式
DMA2D->NLR = (uint32_t)(width << 16) | (uint16_t)height; // 设置填充区域的宽和高,单位是像素
/* 启动传输 */
DMA2D->CR |= DMA2D_CR_START;
/* 等待DMA2D传输完成 */
while (DMA2D->CR & DMA2D_CR_START) {}
}
/* 填充颜色,此处我的 fb 是uint8类型的,由于采用 RGB565格式存储像素点,因此,一个像素占用两个字节,因此下面地址偏移乘了2
* 也可强转位 uint16 然后就不用乘 2 了
* w :待填充区域的宽度
* h :待填充区域的高度
*/
void fill_rect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color){
void* distfb = &framebuffer[2*(y*800 + x)];
dma2d_fill(distfb, w, h, 800 - w, LTDC_PIXEL_FORMAT_RGB565, color);
}
int dma2d_file_rect_test(void)
{
fill_rect(0, 0, 800, 480, 0x0000);//清屏
fill_rect(300, 80, 20, 280, 0x001f);
fill_rect(340, 100, 20, 200, 0x001f);
fill_rect(380, 40, 20, 260, 0x001f);
fill_rect(420, 60, 20, 240, 0x001f);
fill_rect(260, 300, 240, 1, 0x0000);
return 0;
}
MSH_CMD_EXPORT(dma2d_file_rect_test,dma2d_file_rect_test);//导出到 shell 命令
运行结果如下:
5.2 memory to memory
DMA2D 还可以工作在 memory to memory 模式下,此模式可用于将图片数据搬运到 fb 中指定的地址中去,因此我们做如下配置
DMA2D->CR = (0 << 16);
然后我们得告诉我的图片放到了内存中的那里,以及要将图片搬到我的那里,因此做如下配置
DMA2D->BGMAR = (uint32_t)srcaddr;
DMA2D->OMAR = (uint32_t)dstaddr; // 目标地址
DMA2D->FGOR = offlinesrc; // 源数据偏移(像素)
DMA2D->OOR = offlinedst; // 目标地址偏移(像素)
DMA2D->FGPFCCR = pixelformat; //颜色格式
DMA2D->NLR = (uint32_t)(width << 16) | (uint16_t)height;// 图片的宽高
将其封装成 API
static void dma2d_memcopy(uint32_t pixelformat, void * psrc, void * pdst, int width, int height, int offLinesrc, int offLinedst)
{
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)(width << 16) | (uint16_t)height;
DMA2D->CR |= DMA2D_CR_START;
while (DMA2D->CR & DMA2D_CR_START) {}
}
/*
* sky_animation_mask1 为原图片地址
* _lcd.front_buf 为显存
* 原图片地址不偏移
* 目的地址偏移
*/
int dma2d_m2m(void)
{
dma2d_memcopy(LTDC_PIXEL_FORMAT_RGB565,sky_animation_mask1,_lcd.front_buf,LOGO1_W,LOGO1_H,0,800-LOGO1_W);
}
MSH_CMD_EXPORT(dma2d_m2m,dma2d_m2m);
执行效果如下:
如果想移动图片的位置,可以操作 fb 地址,这里给的目的地址就是 fb 的首地址,也就是左上角。
5.3 memory to mrmory 图片混合
通过将两张图片混合,并且设置图片透明度,可以达到图片渐变的效果,因此需要将 DMA2D 设置在待混合的模式下
DMA2D->CR = (0x2 << 16);
然后设置背景、背景偏移、前景、前景偏移、目的地址
DMA2D->BGMAR = (uint32_t)bgaddr;//背景 src addr
DMA2D->BGOR = offsetlineBg;// 背景偏移
DMA2D->FGMAR = (uint32_t)ggaddr;// 前景 src addr
DMA2D->FGOR = offsetlinefg;// 前景偏移
DMA2D->OMAR = dstaddr;// 目的地址
DMA2D->OOR = offlinedist; // 输出偏移,行便宜将添加到行末尾,用于确认下一行的起始地址
DMA2D->NLR = (uint32_t)(width << 16) | (uint16_t)height;
由于两幅图片需要混合,因此需要设置前景层图片透明度,这里我们需要设置FGPFCCR 寄存器
DMA2D->FGPFCCR = (opa << 24) | (1 << 16) | (pixelformat << 0)// 将第16位置1,强制图片透明度为opa
DMA2D->BGPFCCR = pixelformat; // 设置背景颜色格式
DMA2D->OPFCCR = pixelformat; // 设置输出颜色格式
将面的步骤整理成 api 如下:
void dma2d_mixcolorsbulk(void* pfg, void* pbg, void* pdst,
uint32_t offlinefg, uint32_t offlinebg, uint32_t offlinedist,
uint16_t width, uint16_t height,
uint32_t pixelformat, uint8_t opa) { // 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)(width << 16) | (uint16_t)height;
DMA2D->FGPFCCR = pixelformat | (1UL << 16) | ((uint32_t)opa << 24);
DMA2D->BGPFCCR = pixelformat;
DMA2D->OPFCCR = pixelformat;
DMA2D->CR |= DMA2D_CR_START;
while (DMA2D->CR & DMA2D_CR_START) {}
}
int dma2d_test(void)
{ dma2d_mixcolorsbulk(sky_animation_mask,sky_animation_mask1,&_lcd.front_buf[_lcd.lcd_info.width*200+400],0,0,800-LOGO_W,LOGO1_W,LOGO1_H,LTDC_PIXEL_FORMAT_RGB565,128);
}
MSH_CMD_EXPORT(dma2d_test,dma2d_test);
效果如下: