前言

STM32是这几年来相当相当流行的单片机系列,凭借着低功耗、高性能、高性价比等优点在爱好者中聚集了很多人气。博主是从2011年开始接触STM32的。因为博主自己是个液晶屏幕控,拿STM32点各种屏幕那是少不了的,SPI、i80、68K这些51都能点的自然不在话下,Raw-STN接口则是比较能体现STM32优势的地方,其实优势说白了也就是大RAM(数十KB)+高速度(IO翻转速度>10MHz)。这个blog里面也有一些关于STM32点Raw-STN接口的记录:深圳安鑫320160两片日常点屏:M320240-19 & LM081HB1T。然而如果要点Raw-DPI接口(大多数480*272 ~ 800*600屏幕所使用的接口)的屏幕,那STM32就无能为力了,于是以前在对付这种屏幕的时候,我请出了树莓派:树莓派驱动DPI液晶。不过其实早在2014年ST就推出了STM32F429/F439系列单片机,可以支持外扩SDRAM和Raw-DPI液晶。不过由于种种原因,我一直没有亲自去尝试。直到今年有幸得到了一块官方STM32F429I-EVAL,功能简直全面,于是就有机会玩一玩这个“新”芯片。

新芯片除了可以外扩SDRAM和DPI液晶之外,还很贴心地配套加入了一个2D加速功能,官方称为Chrom-Art Accelerator™。找了一圈国内论坛发现关于429的教程、日志一大堆,可是却没有人提及这个2D加速……今天花了半天时间把这个加速器玩了一圈,干脆写个博文记录一下,方便以后参考。

介绍

首先,Chrom-Art Accelerator™也称为DMA2D,说白了就是个加强版的DMA。DMA我们知道,其实就是一个内存复制器。而DMA2D,则是可以在复制内存的过程中对数据进行一些图像相关的处理,通过这种硬件处理来加快2D操作的速度。比如说更快地像素填充,更快地Alpha混合。DMA2D功能也不多,一共就以下四个:

  • 纯色填充
  • 图像复制
  • 像素格式转换
  • Alpha混合

前两种其实完全就是普通DMA就可以做到的功能,也就是寄存器到内存复制(R2M)和内存到内存(M2M)复制,而像素格式转换则是通过内部的LUT,在复制的过程中,以像素为单位进行格式转换。举个例子,在RGB565中,红色是0xF700,而在ARGB8888中,红色是0xFFFF0000,然后我使用下面的伪代码:

unsigned char Src[2] = {0x00, 0xF7};//Little-endian
unsigned char Dst[4] = {0x00, 0x00, 0x00, 0x00};
DMA2D.SrcColorMode = RGB565;
DMA2D.SrcAddress = Src;
DMA2D.DstColorMode = RGB888;
DMA2D.DstAddress = Dst;
DMA2D.Length = 1;
DMA2D.StartColorModeConvertion();

那么执行结束后Dst中应该为{0x00, 0x00, 0xFF, 0xFF}。Alpha混合则是类似于PS中那种图层混合的概念,这个下面细说。

实验

纯色填充

这个实验最为简单,也就放在最前面进行。进行前请确保SDRAM和LCD已经正确初始化且工作正常(比如先用传统的非加速手段进行下测试)。以下代码均基于STM32F4xx_HAL_Driver库编写(不是以前的那个STM32F4xx_StdPeriph_Driver了,虽然挺想念那个标准外设库),并且在STM32F429I-EVAL板上测试通过。

static void LL_FillBuffer(uint32_t LayerIndex, void *pDst, uint32_t xSize, uint32_t ySize, uint32_t OffLine, uint32_t ColorIndex) 
{
  //纯色填充模式,像素格式为ARGB8888 
  hdma2d.Init.Mode         = DMA2D_R2M;
  hdma2d.Init.ColorMode    = DMA2D_ARGB8888;
  hdma2d.Init.OutputOffset = OffLine;      

  hdma2d.Instance = DMA2D;

  /* DMA2D Initialization */
  if(HAL_DMA2D_Init(&hdma2d) == HAL_OK) 
  {
    if(HAL_DMA2D_ConfigLayer(&hdma2d, LayerIndex) == HAL_OK) 
    {
      if (HAL_DMA2D_Start(&hdma2d, ColorIndex, (uint32_t)pDst, xSize, ySize) == HAL_OK)
      {
        /* Polling For DMA transfer */  
        HAL_DMA2D_PollForTransfer(&hdma2d, 10);
      }
    }
  } 
}

代码应该是一目了然的,没有什么值得解释的……

BMP显示

纯色那当然太无聊了,赶紧弄点图片来看看。显示图片其实使用的就是DMA2D的第二种或者第三种模式。使用起来也不复杂,实际上应该说相当简单。首先来实现显示一行的函数:

static void LL_ConvertLineToARGB8888(void *pSrc, void *pDst, uint32_t xSize, uint32_t ColorMode)
{    
  /* Configure the DMA2D Mode, Color Mode and output offset */
  hdma2d.Init.Mode         = DMA2D_M2M_PFC;
  hdma2d.Init.ColorMode    = DMA2D_ARGB8888;
  hdma2d.Init.OutputOffset = 0;     

  /* Foreground Configuration */
  hdma2d.LayerCfg[1].AlphaMode = DMA2D_NO_MODIF_ALPHA;
  hdma2d.LayerCfg[1].InputAlpha = 0xFF;
  hdma2d.LayerCfg[1].InputColorMode = ColorMode;
  hdma2d.LayerCfg[1].InputOffset = 0;

  hdma2d.Instance = DMA2D; 

  /* DMA2D Initialization */
  if(HAL_DMA2D_Init(&hdma2d) == HAL_OK) 
  {
    if(HAL_DMA2D_ConfigLayer(&hdma2d, 1) == HAL_OK) 
    {
      if (HAL_DMA2D_Start(&hdma2d, (uint32_t)pSrc, (uint32_t)pDst, xSize, 1) == HAL_OK)
      {
        /* Polling For DMA transfer */  
        HAL_DMA2D_PollForTransfer(&hdma2d, 10);
      }
    }
  } 
}

感觉上代码还是相当简单明了。这里新多出来一个LayerCfg,顾名思义就是图层配置。其实看DMA2D的结构框图就会发现,DMA2D其实同时支持2个输入和1个输出:

前面因为是纯色填充,所以只使用到了输出和一个输入寄存器,这里需要输入图像,所以用到了一个输入模块。输入模块的配置就是上面的LayerCfg了。

有了显示一行的程序,写出显示BMP的程序也就不困难了:

void BSP_LCD_DrawBitmap(uint32_t Xpos, uint32_t Ypos, uint8_t *pbmp)
{
  uint32_t index = 0, width = 0, height = 0, bit_pixel = 0;
  uint32_t Address;
  uint32_t InputColorMode = 0;

  /* Get bitmap data address offset */
  index = *(__IO uint16_t *) (pbmp + 10);
  index |= (*(__IO uint16_t *) (pbmp + 12)) << 16;

  /* Read bitmap width */
  width = *(uint16_t *) (pbmp + 18);
  width |= (*(uint16_t *) (pbmp + 20)) << 16;

  /* Read bitmap height */
  height = *(uint16_t *) (pbmp + 22);
  height |= (*(uint16_t *) (pbmp + 24)) << 16; 

  /* Read bit/pixel */
  bit_pixel = *(uint16_t *) (pbmp + 28);   

  /* Set the address */
  Address = hltdc.LayerCfg[ActiveLayer].FBStartAdress + (((BSP_LCD_GetXSize()*Ypos) + Xpos)*(4));

  /* Get the layer pixel format */    
  if (bit_pixel == 32)
  {
    InputColorMode = CM_ARGB8888;
  }
  else 
  if (bit_pixel == 16)
  {
    InputColorMode = CM_RGB565;   
  }
  else 
  {
    InputColorMode = CM_RGB888;
  }

  /* Bypass the bitmap header */
  pbmp += (index + (width * (height - 1) * (bit_pixel/8)));  
 
  /* Convert picture to ARGB8888 pixel format */
  for(index=0; index < height; index++)
  {
    /* Pixel format conversion */
    LL_ConvertLineToARGB8888((uint32_t *)pbmp, (uint32_t *)Address, width, InputColorMode);
  
    /* Increment the source and destination buffers */
    Address+=  (BSP_LCD_GetXSize()*4);
    pbmp -= width*(bit_pixel/8);
  } 
}

实现效果如标题图。需要注意的是PS默认保存的BMP格式为XRGB1555,请自己在保存时在高级里设置为RGB565。

Alpha混合实验

Alpha混合并不是一个复杂的概念,其实就是图层叠加。STM32F429的LTDC支持2个硬件图层,但是通常前方的硬件图层用于实现精灵或者鼠标指针一类东西,其它UI界面需要进行的图层混合会直接混合进后方的硬件图层。Alpha混合也可以在前面一个实验的基础上简单完成。这个实验需要准备一些材料:背景图和要混合的前景图。

前景图我用PS随便画了一个,注意要是带透明的,不然这个实验也就没有意义了。画好后保存为PNG格式。然而我们还没实现PNG解码,PS又不支持带Alpha通道的BMP保存,于是请出PixelFormater软件,导入之前做好的PNG:

再导出为32bit ARGB8888的BMP:

和之前一样把这个图片转成数组存进源代码。接着代码在上一个显示BMP的基础上修改:

static void LL_BlendLine(void *pSrc, void *pDst, uint32_t xSize)
{    
  /* Configure the DMA2D Mode, Color Mode and output offset */
  hdma2d.Init.Mode         = DMA2D_M2M_BLEND;
  hdma2d.Init.ColorMode    = DMA2D_ARGB8888;
  hdma2d.Init.OutputOffset = 0;     

  /* Foreground Configuration */
  hdma2d.LayerCfg[1].AlphaMode = DMA2D_NO_MODIF_ALPHA;
  hdma2d.LayerCfg[1].InputAlpha = 0xD0;
  hdma2d.LayerCfg[1].InputColorMode = CM_ARGB8888;
  hdma2d.LayerCfg[1].InputOffset = 0;

  /* Background Configuration */
  hdma2d.LayerCfg[0].AlphaMode = DMA2D_REPLACE_ALPHA;
  hdma2d.LayerCfg[0].InputAlpha = 0xFF;
  hdma2d.LayerCfg[0].InputColorMode = CM_ARGB8888;
  hdma2d.LayerCfg[0].InputOffset = 0x0;

  hdma2d.Instance = DMA2D; 

  /* DMA2D Initialization */
  if(HAL_DMA2D_Init(&hdma2d) == HAL_OK) 
  {
    HAL_DMA2D_ConfigLayer(&hdma2d, 0);
    HAL_DMA2D_ConfigLayer(&hdma2d, 1);
    if(HAL_DMA2D_BlendingStart(&hdma2d, (uint32_t)pSrc, (uint32_t)pDst, (uint32_t)pDst, xSize, 1) == HAL_OK)
    {
      /* Polling For DMA transfer */  
      HAL_DMA2D_PollForTransfer(&hdma2d, 10);
    }
  } 
}

其实也就是在之前的基础上开启了第二个输入,输入源则是设定为和输出一样,也就是说先从目标buffer读取像素,和源buffer的像素进行混合,然后写回目标buffer。然而这次前面显示BMP的函数就遇到麻烦了。比如我这个BMP的头长度为54字节,也就是BMP存储是按字对齐的,但是像素数据却不对齐了,这样DMA2D就无法工作了。一个简单的解决办法就是把头去掉,然后hardcode图片的尺寸、bpp这种参数。当然这不是个好解决办法,不过实验嘛,就这样好了。如果要完善的话,可以先把像素复制到对齐的内存地址中再进行混合。

该实验效果如下:

总结

通篇写下来感觉自己好像真得什么都没写,因为DMA2D这个东西确实很简单,功能很简单,操作也很简单,或许这就是没有人写这个相关文章的原因吧……总之,就是这样,希望这篇记录对大家能有帮助。附一张照片结束本文。