单片机 GUI 设计(四)- 显示图片
单片机 GUI 设计(四)- 显示图片

单片机 GUI 设计(四)- 显示图片

基于 GD32F303ZET6 苹果派开发板

简介

在 GUI 设计中,图片很重要,毫不夸张的说,GUI 设计就是一个贴图的过程。想要制作出一个好看、使用的界面,贴图必不可少。我们可以用图片来装饰各种控件,让它们看起来不是那么死板。如此一来,做界面设计的时候,我们可以先在 PPT 上描绘出界面,做出最终的效果,然后再将 PPT 中一些关键部分,例如背景、小部件等等,导出图片,最终在单片机上显示出来。图片的引入极大方便了我们的 GUI 设计,本章中将会介绍 GUI 设计常用的图片格式,以及图片如何解码。

位图简介

位图,即 BMP 图片,文件后缀为 “.bmp”,是一种无压缩文件,在单片机中应用广泛。位图文件里保存了未经压缩的像素点数据,单片机可以直接显示。一些高端的微处理器,例如 GD32F450/470 、GD32F429 系列内置了硬件刷图模块,可以一键解码显示位图,速度非常快。

常用的位图有 32 位和 24 位的。32 位位图像素点格式为 ARGB8888,包含了透明度信息,适合部署在形状不规则小部件上。引入透明度后,可以利用透明度叠加算法将小部件与背景完美融合到一起。24 位位图像素点格式为 RGB888,不含透明度信息,适合部署在矩形方块上,又或者是背景图片。

JPEG 简介

JPEG 图片是一种有损压缩文件,后缀为 “.jpg” 或 “.jpeg”,常用于显示背景图片和装饰一些矩形方块。假定屏幕的尺寸为 800×480,如果使用 24 位的位图做为背景图片的话,因为位图是无压缩文件所消耗的内存控件至少为 800x480x2 = 768000 字节,即 750k 字节。根据数据手册,GD32F303ZET6 的 Flash 才 512k 字节,连张背景图片都存不下,此时必须要添加 SPI Flash、SD 卡或 Nand Flash 等存储设备保存图片,这无疑增加了成本。JPEG 图片是经过压缩的,如果背景图片比较单调,经过压缩后,800×480 的图片可能会被压缩到 20k 不到,单片机的 Flash 完全存得下。JPEG 的特点是图片越单调,压缩后占据的内存越小。

JPEG 图片不含透明度信息,所以只适合做为背景图片或一些矩形窗口显示。在单片机中可以使用 TJpgDec 轻松解码,解码后得到的就是 RGB565 格式的像素点数据,无需转换即可填充到屏幕上显示。

因为 JPEG 采用的是有损压缩,所以最终显示出来的图案要模糊一些。

PNG 图片

PNG 图片的压缩率比 JPEG 图片高,即同一张图片,经过 PNG 压缩后占据的内存空间比 JPEG 小,而且 PNG 采用的是无损压缩方案,解码后不会造成图案模糊。PNG 图片还支持透明度,可以实现 32 位位图一样的效果。 单片机中可以使用 lodepng 对 PNG 图片解码,但速度上要慢一些。一些带有 GPU 的微处理器可以一键解码 PNG 和 JPEG 图片,只可惜我们的 GD32F303ZET6 并不支持。

GIF 图片

GIF 图片,即动图,可以显示动画效果。GIF 同样也支持透明度,在 GD32F450/470 、GD32F429 等处理器上有良好的显示效果,但 GD32F303ZET6 没有针对图形显示的硬件加速,所以显示效果不尽人意。

图片的存储

单片机系统中,图片可以保存到 SPI Flash、SD 卡和 Nand Flash 等存储介质中,对于占据内存小的图片,也可以选择将图片保存到单片机内部 Flash。如果选择将图片保存到 SPI Flash、SD 卡和 Nand Flash 等存储介质中,很大程度上需要在代码工程里加入一个文件系统。文件系统有个很头疼的诟病,读写速度慢,而且还容易读取出错,读取出错后又要重新挂载文件系统,再次尝试读取文件,就挺烦。将图片保存到单片机内部 Flash 后,就无需在系统里布置一个文件系统,读取速度也比使用外部存储介质快得多,适合应用于简单的 GUI 设计工程。使用博主提供的 AnythingToC 小工具可以很方便的将任意文件转成 C 语言数组,从而将文件以数组的形式保存到单片机中。

当然,如果工程里使用到的图片太多,Flash 存不下,外拓存储设备是躲不过的。

透明度叠加

当我们使用 32 位位图或 PNG 图片显示时,往往需要使用到透明度叠加。使用透明度叠加显示的效果会更好,当然也更耗费 CPU 资源。透明度,有时又称为不透明度,用于描述像素点的虚化程度。透明度通常取值范围为 0x00 到 0xFF,即 0 到 255,0x00 表示完全透明,0xFF 表示完全不透明。

透明度叠加算法如下所示。对于 RGB565,首先需要将背景像素点还原成 RGB888,然后才能进行透明度叠加计算。显示带有透明度的图片时,每个像素点都要做透明度叠加,解码速度自然慢得多,但显示效果会非常好。

注意:backColor 为背景颜色,格式为 RGB565;r、g、b 为前景颜色的 R、G、B 分量,格式为 RGB888;alpha 表示透明度,范围为 0x00~0xFF。

u16 CalcAlphaRGB565(u16 backColor, u8 r, u8 g, u8 b, u8 alpha)
{
  u16 backR, backG, backB, foreR, foreG, foreB, colorAlpha;
  u16 resultR, resultG, resultB;
  u16 result;

  //透明度太小,直接返回背景色
  if(alpha < 5)
  {
    return backColor;
  }

  //提前背景RGB通道数据
  backR = ((backColor >> 11) & 0x1F);
  backG = ((backColor >> 5 ) & 0x3F);
  backB = ((backColor >> 0 ) & 0x1F);

  //提取前景RGB通道数据
  foreR = r >> 3;
  foreG = g >> 2;
  foreB = b >> 3;

  //获取透明度
  colorAlpha = alpha;

  //RGB通道透明度叠加
  resultR = (foreR * colorAlpha + backR * (0xFF - colorAlpha)) >> 8;
  resultG = (foreG * colorAlpha + backG * (0xFF - colorAlpha)) >> 8;
  resultB = (foreB * colorAlpha + backB * (0xFF - colorAlpha)) >> 8;

  //组合成RGB565格式
  result = (((u16)resultR) << 11) | (((u16)resultG) << 5) | (((u16)resultB) << 0);

  return result;
}

位图解码

位图支持很多种像素点格式,不同像素点格式的位图文件信息头略有不同,在此我们只讨论 24 位、32 位位图的解码。对于 24 位和 32 位位图,完整的图片文件由文件头、信息头和数据体三大部分组成。

文件头位于位图文件的起始位置,其中包含了文件标志、文件大小以及位图数据的起始位置相对于文件头的偏移量,如下所示。如果事先知道了图片的宽度和高度,那么就可以直接从文件头跳转到数据体,即像素点数据所在位置,开始解码显示。位图文件头固定为 14 个字节。

//BMP 文件头
typedef __packed struct
{
  u16 type;      //文件标志,只为'BM',必须是BM,十六进制中则是 0x424D;
  u32 size;      //文件大小,单位为字节
  u16 reserved1; //保留,必须为 0
  u16 reserved2; //保留,必须为 0
  u32 offBits;   //位图数据的起始位置相对于文件头的偏移量,单位为字节。
}StructBmpFileHeader;

文件头紧挨着的就是信息头,信息头包含了位图的具体信息,例如长度、宽度、像素点格式等等,如下所示。信息头固定 40 个字节。

文件头和信息头长度总和为 54 个字节,再往后便是像素点数据,占据了位图文件绝大多数内存空间。对于 24 位位图图片,像素点数据按照 BGR,注意不是 RGB,顺序排列,一个像素点占据 3 字节内存空间;对于 32 位位图图片,像素点数据按照 BGRA 排列,一个像素点占据 4 个字节内存空间。

//BMP信息头
typedef __packed struct
{
  u32 infoSize;       //该结构体所需的字节数
  u32 width;          //位图的宽度(像素单位)
  u32 height;         //位图的高度(像素单位)
  u16 planes;         //目标设备的级别,必须为1
  u16 colorSize;      //每个像素所需的位数
  u32 compression;    //说明图象数据压缩的类型,位图压缩类型,必须是 0(不压缩)
  u32 imageSize;      //位图的大小(其中包含了为了补齐行数是4的倍数而添加的空字节),以字节为单位
  u32 xPelsPerMeter;  //位图水平分辨率,每米像素数
  u32 yPelsPerMeter;  //位图垂直分辨率,每米像素数
  u32 colorUsed;      //位图实际使用的颜色表中的颜色数
  u32 colorImportant; //位图显示过程中重要的颜色数,如果是 0,表示都重要
}StructBmpInfoHeader;

位图文件默认按从左往右,从下往上,按行储存数据,如果 1 行像素点数据所占据的内存空间不满足 4 字节对齐,那么就需要补零。

简单的位图显示驱动如下所示,假定该图片储存在 Flash 中。如果图片是储存在文件系统中的,那么可以选择将整个文件均读入内存中,可以是外拓 SRAM/SDRAM,再显示。

#include "LCD.h"

void DrawBMP(unsigned char* img, int x, int y)
{
  StructBmpFileHeader* fileHeader; //文件头
  StructBmpInfoHeader* infoHeader; //信息头
  int x0, y0, x1, y1, width, height, readCnt;
  unsigned char* data;
  unsigned int a, r, g, b;
  unsigned short color, backgroung;

  //获取图片宽度和高度
  infoHeader = (StructBmpInfoHeader*)(img + 14);
  width = infoHeader->width;
  height = infoHeader->height;

  //计算起点和终点
  x0 = x;
  y0 = y;
  x1 = x0 + width - 1;
  y1 = y0 + height - 1;

  //获取像素点数据起始位置
  fileHeader = (StructBmpFileHeader*)img;
  data = img + fileHeader->offBits;
  readCnt = 0;
  
  //24 位位图解码
  if(24 == infoHeader->colorSize)
  {
    //按行显示
    for(y = y1; y >= y0; y--)
    {
      //一个一个像素点显示
      for(x = x0; x <= x1; x++)
      {
        //获取像素点数据,右移是为了方便后续转成 RGB565 格式
        b = data[readCnt++] >> 3; //蓝色
        g = data[readCnt++] >> 2; //绿色
        r = data[readCnt++] >> 3; //红色

        //RGB888 转 RGB565
        color = ((r << 11) | (g << 5) | (b << 0));

        //没有透明度信息,直接覆盖显示
        LCDFastDrawPoint(x, y, color);
      }

      //显示完一行后要做 4 字节对齐
      while(0 != (readCnt % 4))
      {
        readCnt++;
      }
    }
  }

  //32 位位图解码
  else if(32 == infoHeader->colorSize)
  {
    //按行显示
    for(y = y1; y >= y0; y--)
    {
      //一个一个像素点显示
      for(x = x0; x <= x1; x++)
      {
        //获取像素点数据
        b = data[readCnt++]; //蓝色
        g = data[readCnt++]; //绿色
        r = data[readCnt++]; //红色
        a = data[readCnt++]; //透明度

        //读取背景颜色
        backgroung = LCDReadPoint(x, y);

        //透明度叠加
        color = CalcAlphaRGB565(backgroung, r, g, b, a);

        //显示
        LCDFastDrawPoint(x, y, color);
      }
    }
  }
}

JPEG 图片解码

单片机中可以用 TJpgDec 轻松解码 JPEG 图片,TJpgDec 将 JPEG 解码后生成 RGB888/RGB565 格式的像素点数据,可以不用做任何转换直接导入屏幕显示。

TJpgDec 文件组成

TJpgDec 总共有三个文件,分别是 integer.h、tjpgd.h 和 tjpgd.c。integer.h 用于变量类型声明,方便不同平台间移植,tjpgd.h 和 tjpgd.c 则负责解码 JPEG 图片。这三个文件可以在源码工程里找到。

TJpgDec 配置

TJpgDec 可以通过修改 tjpgd.h 中的宏定义来配置,例如将 JD_FORMAT 定义为 1 可使能解码输出 RGB565,定义为 0 可使能解码输出 RGB888,如下所示。其它宏定义暂时不清楚什么作用,保留原来的配置就好。

#define	JD_SZBUF      1024  /* Size of stream input buffer */
#define JD_FORMAT     1     /* Output pixel format 0:RGB888 (3 BYTE/pix), 1:RGB565 (1 WORD/pix) */
#define	JD_USE_SCALE  1     /* Use descaling feature for output */
#define JD_TBLCLIP    1     /* Use table for saturation (might be a bit faster but increases 1K bytes of code size) */

TJpgDec 的使用

TJpgDec 的工作需要一个最少 3092 字节的工作区,凑个整,在这里我们取 4k 字节;同时,TJpgDec 还需要两个回调函数,分别用于数据输出和像素点输出,具体使用如下所示。

#include "tjpgd.h"

//缓冲区大小定义
#define JPEG_BUF_SIZE 4096

//定义jpeg解码工作区大小(最少需要3092字节),作为解压缓冲区,必须4字节对齐
__align(4) unsigned char s_arrJpegWorkBuf[JPEG_BUF_SIZE];

//其它静态变量
static u32 s_iX0, s_iY0, s_iReadCnt;
static unsigned char* s_iImage;

//jpeg 数据输入回调函数
//jd : 储存待解码的对象信息的结构体
//buf: 输入数据缓冲区 (NULL:执行地址偏移)
//num: 需要从输入数据流读出的数据量/地址偏移量
static u32 JpegInfunc(JDEC* jd, u8* buf, u32 num) 
{
  unsigned int i;   //循环变量
  unsigned int rb;  //返回值

  //buf非空表示TJpgDec模块要读取图片数据
  if(NULL != buf)
  {
    rb = 0;
    for(i = 0; i < num; i++)
    {
      buf[i] = s_pImage[s_iReadCnt++];
      rb++;
    }
  }

  //buf为空表示TJpgDec模块要调整读取位置
  else
  {
    s_iReadCnt = s_iReadCnt + num;
    rb = num;
  }

  return rb;
}

//jpeg    数据输出回调函数
//jd     :储存待解码的对象信息的结构体
//rgbbuf :指向等待输出的RGB位图数据的指针
//rect   :等待输出的矩形图像的参数
//返 回 值:0,输出成功;1,输出失败/结束输出
static u32 JpegOutfunc(JDEC* jd, void* rgbbuf, JRECT* rect) 
{ 
  u16 x0, y0, x1, y1; //起点、终点坐标

  //计算起点、终点
  x0 = rect->left  + s_iX0;
  y0 = rect->top   + s_iY0;
  x1 = rect->right + s_iX0;
  y1 = rect->bottom + s_iY0;

  //填充LCD
  LCDColorFill(x0, y0, x1, y1, rgbbuf);

  //返回0使得模块继续工作
  return 0;
}

//解码并显示 JPEG 图片
//image:图片首地址
//x、y:显示位置
//size:图片大小,以字节为单位
void DrawJPEG(unsigned char* image, unsigned int x, unsigned int y, unsigned int size)
{
  static JDEC s_structJpegDev;  //JPEG解码设备结构体

  //保存图片首地址和大小
  s_pImage = image;
  
  //保存原点信息
  s_iX0 = x;
  s_iY0 = y;
  
  //清空读取记录
  s_iReadCnt = 0;

  //解码并显示
  if(JDR_OK == jd_prepare(&s_structJpegDev, JpegInfunc, s_arrJpegWorkBuf, JPEG_BUF_SIZE, NULL))
  {
    jd_decomp(&s_structJpegDev, JpegOutfunc, 0);
  }
}

PNG 解码

单片机中可以用 lodepng 轻松对 PNG 解码,解码后得到的像素点格式为 ARGB888,可显示出透明效果。lodepng 支持直接解码文件系统中的图片,也可以解码已读取到内存中的图片数据。不仅如此,lodepng 还支持 PNG 编码,也就是说可以利用 lodepng 生成一张 PNG 图片。使用 lodepng 解码内存或 Flash 中的图片如下所示。通过 lodepng_decode32 函数解码出来的像素点格式为 RGBA8888,与 32 位位图的 BGRA8888 略有不同。如果不想要透明度信息,也可使用 lodepng_decode24 函数,具体请看 lodepng 官网详细实例。

注意:lodepng 会自动为输出缓冲区申请动态内存,因此用户有义务为其释放内存。

#include "PNG.h"
#include "PNGImage.h"
#include "lodepng.h"
#include "LCD.h"
#include "stdio.h"

//image:PNG 图片首地址
// x、y:起始坐标
//size:PNG 文件大小(字节)
void DrawPng(unsigned char* image, unsigned int x, unsigned int y, unsigned int size)
{
  unsigned char* output;
  unsigned int width, height, error;
  unsigned char r, g, b, a;
  unsigned int x0, y0, x1, y1, readCnt;
  unsigned short color;
  
  //解码 PNG 图片
  error = lodepng_decode32(&output, &width, &height, image, size);
  if(error)
  {
    printf("Fail to show PNG\r\n");
    return;
  }

  //显示,像素点格式为 RGBA
  x0 = x;
  y0 = y;
  x1 = x0 + width - 1;
  y1 = y0 + height - 1;
  readCnt = 0;
  for(y = y0; y <= y1; y++)
  {
    for(x = x0; x <= x1; x++)
    {
      //获取像素点值
      r = output[readCnt++];
      g = output[readCnt++];
      b = output[readCnt++];
      a = output[readCnt++];
      
      //读取背景像素点值
      color = LCDReadPoint(x, y);
      
      //透明度叠加
      color = CalcAlphaRGB565(color, r, g, b, a);
      
      //显示
      LCDFastDrawPoint(x, y, color);
    }
  }

  //释放动态内存
  free(output);
}

GIF 解码

GIF 解码暂时不介绍,后边有机会再说。

实验结果

BMP 和 PNG 图片显示如下所示。

JPEG 图片显示如下所示。

源码

本章节中的源码请参考《单片机 GUI 设计(零)- 大纲