基于 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;
}
实验结果
实验结果如下所示。
-中文字库-实验结果-20230304-1024x768.jpg)
源码
本章节中的源码请参考《单片机 GUI 设计(零)- 大纲》