单片机 GUI 设计(十一)- 中文字库
单片机 GUI 设计(十一)- 中文字库

单片机 GUI 设计(十一)- 中文字库

基于 GD32F303ZET6 苹果派开发板

简介

没什么好介绍的,就是要显示中文,总不能总是显示英文吧。

GBK/GBK2312 字库

中文编码方式有很多,除了 GBK/GBK2312,还有 Unicode、utf-8 等等。在 Kail 集成开发环境中,我们使用的更多的是 GBK/GBK2312,如下所示。在 Keil 配置中选择编码方式为 GBK2312 之后,所有的 .c 和 .h 文件中的中文注释、中文字符串等,编码方式都是 GBK2312。复制别人代码的时候,特别是使用了中文注释的,有可能会出现中文乱码,这是因为别人的编辑器使用的编码方式与 Keil 的编码方式不一样。

GBK 包含 GBK2312。GBK2313 可以表达 6763 个汉字,几乎囊括了所有常见的汉字。GBK 可以表达的汉字更多,多达两万个,可以表达绝大多数汉字。为了能显示更多的汉字,本系列提供的中文字库是 GBK 的编码方式。本系列提供了 12×12、16×16、24×24、32×32、36×36 和 64×64 的中文字库,足以应对绝大多数应用场景。当然,读者也可以在网上搜索相关资料,制作属于自己的中文字库。

GBK 编码

GBK 使用两个字节来表达一个汉字,编码范围是 0x8140 到 0xFEFE,先存储高字节,再存储低字节。高字节从 0x81 到 0xFE,低字节 从 0x40 到 0xFE,剔除 xx7F 码位。所有 GBK 最多可以表达 23940 个汉字。目前 GBK 编码共收录汉字和图形符号 21886 个,其中汉字(包括部首和构件)21003 个,图形符号 883 个。

汉字字宽

与英文不同,汉字的长和宽是相同的。

点阵数据的扫描方式

本系列提供的中文字库的扫描方式是按列存储,从上往下、从左往右,高字节在前,低字节在后。如果一列不满足 8 位对齐,那么就需要在后边补零。以 12×12 汉字为例,因为行数不是 8 字节对齐,所以实际的点阵数据大小为 16×12/8=24B,即 24 个字节。如果汉字是 16×16 的,因为行数 8 字节对齐,所以点阵数据的大小为 16×16/8=32B,即 32 个字节。

点阵数据的寻址

由点阵数据组成的文件我们称之为字库文件。在本系列提供的中文字库里,汉字的点阵数据按照从 0x8140 到 0xFEFE 的顺序,依次存储在文件中。编码 0x8140 对应的汉字是“丂”,所以“丂”的点阵数据会被存储在文件首地址。GBK 点阵数据的寻址如下所示。以 24×24 字体的汉字为例,24×24 汉字的点阵数据大小为 72 字节。因为 GBK 编码剔除了 xx7F 码位,所以要对编码低地址 0x40~0x7E 和 0x80~0xFE 这两个范围分开处理。当编码低地址位于 0x40~0x7E 范围时,点阵数据的地址为 ((高字节 – 0x81) * 190 + (低字节 – 0x40)) * 72。当编码低地址位于 0x80~0xFE 范围时,点阵数据的地址为 ((高字节 – 0x81) * 190 + (低字节 – 0x41)) * 72。

void GetCNFont24x24(u32 code, u8* buf)
{
  u8  gbkH, gbkL; //GBK码高位、低位
  u32 addr;       //点阵数据在SPI Flash中的地址
  u32 i;          //循环变量

  //拆分GBK码高位、低位
  gbkH = code >> 8;
  gbkL = code & 0xFF;

  //校验高位
  if((gbkH < 0x81) || (gbkH > 0xFE))
  {
    for(i = 0; i < 72; i++)
    {
      buf[i] = 0;
    }
    return;
  }

  //低位处在0x40~0x7E范围
  if((gbkL >= 0x40) && (gbkL <= 0x7E))
  {
    addr = ((gbkH - 0x81) * 190 + (gbkL - 0x40)) * 72;
    ForceReadByNameWithoutCheck(addr, buf, 72, CN_FONT_24x24_DIR);
  }

  //低位处在0x80~0xFE范围
  else if((gbkL >= 0x80) && (gbkL <= 0xFE))
  {
    addr = ((gbkH - 0x81) * 190 + (gbkL - 0x41)) * 72;
    ForceReadByNameWithoutCheck(addr, buf, 72, CN_FONT_24x24_DIR);
  }

  //出错
  else
  {
    for(i = 0; i < 72; i++)
    {
      buf[i] = 0;
    }
  }
}

汉字的绘制

获取到汉字的点阵数据后,我们就可以根据点阵数据在屏幕上绘制汉字了。汉字的绘制如下所示。LCDShowChar 函数中,首先根据输入的字符编码判断是英文还是汉字,如果是汉字的话,就调用 GetCharLatticeData 函数获取汉字的点阵数据,然后按照从上往下,从左往右的扫描顺序,显示汉字。

void LCDShowChar(u16 x, u16 y, u32 code, u8 size, u8 mode)
{
  u8* gbk;        //汉字点阵数据
  u32 byte, i, j; //临时变量和循环变量
  u32 y0;         //用于保存起始纵坐标
  u32 len;        //单个点阵数据字节总数
  
  //保存纵坐标
  y0 = y;
  
  //ASCII 码
  if(code < 0x80)
  {
    ...
  }
  
  //中文
  else
  {
    //获取点阵数据大小
    if (24 == size)
    {
      len = 72;
    }
    else if (16 == size)
    {
      len = 32;
    }
    else if (12 == size)
    {
      len = 24;
    }
    else if (32 == size)
    {
      len = 128;
    }
    else if (36 == size)
    {
      len = 180;
    }
    else if (64 == size)
    {
      len = 512;
    }

    //获取汉字点阵数据
    gbk = GetCharLatticeData(code, size);

    //显示汉字
    for (i = 0; i < len; i++)
    {
      //获取一字节点阵数据
      byte = gbk[i];

      //显示这一字节内容
      for (j = 0; j < 8; j++)
      {
        if (byte & 0x80)
        {
          LCDFastDrawPoint(x, y, g_iLCDPointColor);
        }
        else if (0 == mode)
        {
          LCDFastDrawPoint(x, y, g_iLCDPointColor);
        }

        //左移一位
        byte = byte << 1;

        //更新坐标
        y++;
        if ((y - y0) >= size)
        {
          y = y0;
          x++;
          break;
        }
      }
    }
  }
}

显示字符串

引入汉字后,一条字符串中将同时存在两种编码,分别是 ASCII 码和 GBK 编码,此时字符串显示函数的实现如下所示。字符串显示函数的重点是将英文 ASCII 码和中文编码分开,简单的方法就是遇到一个字符编码时,如果该编码小于 0x80,那么该编码一定为 ASCII 码;如果大于等于 0x80,那么该编码就为 GBK 编码的高位,此时要继续读取下一个字节,组成 2 字节的 GBK 编码。

void LCDShowString(u16 x, u16 y, u16 width, u16 height, u8 size, char* p)
{
  u32 x0, y0, x1, y1, i, code, codeWidth;
  
  //计算显示区域大小
  x0 = x;
  y0 = y;
  x1 = x0 + width - 1;
  y1 = y0 + height - 1;
  
  //循环显示整个字符串
  i = 0; x = x0; y = y0;
  while(0 != p[i])
  {
    //获取字符编码
    code = p[i];
    
    //英文
    if(code < 0x80)
    {

      if((code < ' ') || (code > '~')){return;}
      codeWidth = size / 2;
      i++;
    }
    
    //中文
    else
    {
      code = (code << 8) | p[i + 1];
      codeWidth = size;
      i = i + 2;
    }
    
    //未超出显示区域
    if((x + codeWidth - 1) <= x1)
    {
      LCDShowChar(x, y, code, size, 1);
      x = x + codeWidth;
    }
    
    //已超出显示区域
    else 
    {
      //更新纵坐标
      y = y + size;
      
      //横坐标返回最左侧
      x = x0;
      
      //将要显示的字符未超出显示区域
      if((y + size - 1) <= y1)
      {
        LCDShowChar(x, y, code, size, 1);
        x = x + codeWidth;
      }
      
      //已经超出了显示区域
      else
      {
        return;
      }
    }
  }
}

文字缓冲池

绘制字符时,从文件系统中获取点阵数据往往最耗时间,此时我们可以在内存中建立一个文字缓冲池。文字缓冲池维护了一张列表,里边记录了所有已经显示过的字符的编码和点阵数据。要获取一个字符的点阵数据时,如果该字符在列表中有记录,即被显示过,那么直接返回表中的点阵数据首地址;如果该字符没有记录,即未被显示过,那么就需要从文件系统中读取点阵数据,并在列表中添加记录。

引入文字缓冲池后,那些频繁出现的字符的刷新速度明显提高,因为点阵数据是储存在 SRAM 中的。除了第一次绘制此字符时会比较慢,因为第一次绘制时需要从文件系统中读取数据,后续都是利用 SRAM 中的点阵数据直接绘制文字。

一般项目中需要显示的汉字最多也就几百个,按照两百个计算,如果需要显示的是 36×36 的汉字,那么所需的内存是 32.4KB。用这 32.4KB 内存提升整个项目的运行速度是值得的。

文字缓冲池的简单实现如下所示。

#define CHAR_BUF_SIZE      (1 * 1024)  //文字缓冲池每次申请的动态内存大小
#define CHAR_TEXT_BUF_SIZE (10 * 1024) //文字缓冲池点阵数据每次申请的动态内存大小

static StructCharBuf* s_pCharBufHead       = NULL; //文字缓冲池链表首地址
static u8*            s_pCharBufMalloc     = NULL; //文字缓冲池申请的动态内存首地址
static i32            s_iCharBufUsed       = 0;    //文字缓冲池已使用的动态内存大小
static i32            s_iCharBufRemain     = 0;    //文字缓冲池剩余动态内存大小
static u8*            s_pCharTextBufMalloc = NULL; //文字缓冲池点阵数据申请的动态内存首地址
static i32            s_iCharTextBufUsed   = 0;    //文字缓冲池点阵数据已使用的动态内存大小
static i32            s_iCharTextBufRemain = 0;    //文字缓冲池点阵数据剩余动态内存大小

u8* GetCharLatticeData(u32 code, u32 font)
{
  StructCharBuf* charBuf;
  StructCharBuf* newCharBuf;
  u32 charBufSize;

  //查验文字缓冲池中是否有记录,有则返回该记录
  charBuf = s_pCharBufHead;
  while (NULL != charBuf)
  {
    //查找到了记录
    if ((code == charBuf->code) && (font == charBuf->font))
    {
      return charBuf->buf;
    }

    //比对失败,继续下一项
    charBuf = charBuf->next;
  }

  //动态内存剩余量不足一个文字缓冲池项目,需要申请新的动态内存
  if (s_iCharBufRemain < sizeof(StructCharBuf))
  {
    //申请动态内存
    s_pCharBufMalloc = MyMalloc(SRAMIN, CHAR_BUF_SIZE);
    if (NULL == s_pCharBufMalloc)
    {
      s_pCharBufMalloc = MyMalloc(SRAMEX, CHAR_BUF_SIZE);
      if (NULL == s_pCharBufMalloc)
      {
        printf("GetCharLatticeData: Fail to malloc1\r\n");
        while (1) {}
      }
    }

    //重置计数
    s_iCharBufUsed = 0;
    s_iCharBufRemain = CHAR_BUF_SIZE;
  }

  //为文字缓冲池项目分配内存
  newCharBuf = (StructCharBuf*)((u8*)s_pCharBufMalloc + s_iCharBufUsed);
  s_iCharBufUsed = s_iCharBufUsed + sizeof(StructCharBuf);
  s_iCharBufRemain = s_iCharBufRemain - sizeof(StructCharBuf);
  
  //保存文字编码和字体,并设置指向的下一项为 NULL
  newCharBuf->code = code;
  newCharBuf->font = font;
  newCharBuf->next = NULL;

  //获取点阵数据大小
  switch (font)
  {
  case 12 : charBufSize = 24; break;
  case 16 : charBufSize = 32; break;
  case 24 : charBufSize = 72; break;
  case 32 : charBufSize = 128; break;
  case 36 : charBufSize = 180; break;
  case 64 : charBufSize = 512; break;

  default:
    break;
  }

  //剩余的动态内存不够储存点阵数据,需要申请新的一批动态内存
  if (s_iCharTextBufRemain < charBufSize)
  {
    //申请动态内存
    s_pCharTextBufMalloc = MyMalloc(SRAMEX, CHAR_TEXT_BUF_SIZE);
    if (NULL == s_pCharTextBufMalloc)
    {
      printf("GetCharLatticeData: Fail to malloc2\r\n");
      while (1) {}
    }

    //重置计数
    s_iCharTextBufUsed = 0;
    s_iCharTextBufRemain = CHAR_TEXT_BUF_SIZE;
  }

  //为点阵数据分配内存
  //为文字缓冲池项目分配内存
  newCharBuf->buf = (u8*)((u8*)s_pCharTextBufMalloc + s_iCharTextBufUsed);
  s_iCharTextBufUsed = s_iCharTextBufUsed + charBufSize;
  s_iCharTextBufRemain = s_iCharTextBufRemain - charBufSize;
  
  //读取点阵数据
  switch (font)
  {
  case 12 : GetCNFont12x12(code, newCharBuf->buf); break;
  case 16 : GetCNFont16x16(code, newCharBuf->buf); break;
  case 24 : GetCNFont24x24(code, newCharBuf->buf); break;
  case 32 : GetCNFont32x32(code, newCharBuf->buf); break;
  case 36 : GetCNFont36x36(code, newCharBuf->buf); break;
  case 64 : GetCNFont64x64(code, newCharBuf->buf); break;
  default:
    break;
  }

  //添加到文字缓冲池,添加到链表表尾
  if (NULL == s_pCharBufHead)
  {
    s_pCharBufHead = newCharBuf;
  }
  else
  {
    charBuf = s_pCharBufHead;
    while (NULL != charBuf->next)
    {
      charBuf = charBuf->next;
    }
    charBuf->next = newCharBuf;
  }

  //返回点阵缓冲区首地址
  return newCharBuf->buf;
}

实验结果

实验结果如下所示。

源码

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