一、汉字显示原理简介 常用的汉字内码系统有 GB2312,GB13000,GBK,BIG5(繁体)等几种,其中 GB2312 支持的汉字仅有几千个,很多时候不够用,而 GBK 内码不仅完全兼容 GB2312,还支持了繁体 字,总汉字数有 2 万多个,完全能满足我们一般应用的要求。 本实例我们将制作三个 GBK 字库,制作好的字库放在 SD 卡里面,然后通过 SD 卡,将字 库文件复制到外部 FLASH 芯片 W25Q64 里,这样,W25Q64 就相当于一个汉字字库芯片了。 汉字在液晶上的显示原理与前面显示字符的是一样的。汉字在液晶上的显示其实就是一些 点的显示与不显示,这就相当于我们的笔一样,有笔经过的地方就画出来,没经过的地方就不 画。所以要显示汉字,我们首先要知道汉字的点阵数据,这些数据可以由专门的软件来生成。 只要知道了一个汉字点阵的生成方法,那么我们在程序里面就可以把这个点阵数据解析成一个 汉字。 知道显示了一个汉字,就可以推及整个汉字库了。汉字在各种文件里面的存储不是以点阵 数据的形式存储的(否则那占用的空间就太大了),而是以内码的形式存储的,就是 GB2312/GBK/BIG5 等这几种的一种,每个汉字对应着一个内码,在知道了内码之后再去字库 里面查找这个汉字的点阵数据,然后在液晶上显示出来。这个过程我们是看不到,但是计算机 是要去执行的。 单片机要显示汉字也与此类似:汉字内码(GBK/GB2312)→查找点阵库→解析→显示。 所以只要我们有了整个汉字库的点阵,就可以把电脑上的文本信息在单片机上显示出来了。 这里我们要解决的最大问题就是制作一个与汉字内码对得上号的汉字点阵库。而且要方便单片 机的查找。每个 GBK 码由 2 个字节组成,第一个字节为 0X81~0XFE,第二个字节分为两部分, 一是 0X40~0X7E,二是 0X80~0XFE。其中与 GB2312 相同的区域,字完全相同。 我们把第一个字节代表的意义称为区,那么 GBK 里面总共有 126 个区(0XFE-0X81+1), 每个区内有 190 个汉字(0XFE-0X80+0X7E-0X40+2),总共就有 126*190=23940 个汉字。我 们的点阵库只要按照这个编码规则从 0X8140 开始,逐一建立,每个区的点阵大小为每个汉字 所用的字节数*190。这样,我们就可以得到在这个字库里面定位汉字的方法: 当 GBKL0X80 时:Hp=((GBKH-0x81)*190+GBKL-0X41)*csize;其中 GBKH、GBKL 分别代表 GBK 的第一个字节和第二个字节(也就是高位和低位),Hp为对应汉字点阵数据在字库里面的起始地址(假设是从 0 开始存放),csize 代表一个汉字点阵所占的字节数。假定采用与 15.3 节 ASCII 字库一样的提取方法(从上到下,从左到右),可以得出字体大小与点阵所占字节数的对应关系为:csize=(size/8+((size%8)?1:0))*size;size 为字体大小,比如 12(12*12)、16(16*16)、24(24*24)等。这样我们只要得到了汉字的 GBK 码,就可以得到该汉字点阵在点阵库里面的位置,从而获取其点阵数据,显示这个汉字了。上一章,我们提到要用 cc936.c,以支持长文件名,但是 cc936.c 文件里面的两个数组太大了(172KB),直接刷在单片机里面,太占用 flash 了,所以我们必须把这两个数组存放在外部flash。cc936 里面包含的两个数组 oem2uni 和 uni2oem 存放 unicode 和 gbk 的互相转换对照表,这两个数组很大,这里我们利用 ALIENTEK 提供的一个 C 语言数组转 BIN(二进制)的软件:C2B 转换助手 V1.1.exe,将这两个数组转为 BIN 文件,我们将这两个数组拷贝出来存放为一个新的文本文件,假设为 UNIGBK.TXT,然后用 C2B 转换助手打开这个文本文件,然后点击转换,就可以在当前目录下(文本文件所在目录下)得到一个 UNIGBK.bin 的文 件。这样就完成将 C 语言数组转换为.bin 文件,然后只需要将 UNIGBK.bin 保存到外部 FLASH 就实现了该数组的转移。

2、字库

字库的生成,我们要用到一款软件,由易木雨软件工作室设计的点阵字库生成器 V3.8。该 软件可以在 WINDOWS 系统下生成任意点阵大小的 ASCII,GB2312(简体中文)、GBK(简体中 文)、BIG5(繁体中文)、HANGUL(韩文)、SJIS(日文)、Unicode 以及泰文,越南文、俄文、乌克 兰文,拉丁文,8859 系列等共二十几种编码的字库,不但支持生成二进制文件格式的文件,也 可以生成 BDF 文件,还支持生成图片功能,并支持横向,纵向等多种扫描方式,且扫描方式 可以根据用户的需求进行增加。

要生成 16*16 的 GBK 字库,则选择:936 中文 PRC GBK,字宽和高均选择 16,字体大小 选择 12,然后模式选择纵向取模方式二(字节高位在前,低位在后),最后点击创建,就可以 开始生成我们需要的字库了(.DZK 文件)

注意:电脑端的字体大小与我们生成点阵大小的关系为: fsize=dsize*6/8 。其中,fsize 是电脑端字体大小,dsize 是点阵大小(12、16、24 等)。所以 16*16 点阵大小对应的是 12 字体。 生成完以后,我们把文件名和后缀改成:GBK16.FON。同样的方法,生成 12*12 的点阵库 (GBK12.FON)和 24*24 的点阵库(GBK24.FON),总共制作 3 个字库。 另外,该软件还可以生成其他很多字库,字体也可选,大家可以根据自己的需要按照上面 的方法生成即可。

3、软件设计

在 HARDWARE 文件夹所在的文件夹下新建一 个 TEXT 的文件夹。在 TEXT 文件夹下新建 fontupd.c、fontupd.h、text.c、text.h 这 4 个文件。 并将该文件夹加入头文件包含路径。

fontupd.c :

#include "fontupd.h"
#include "ff.h" 
#include "flash.h" 
#include "lcd.h" 
#include "malloc.h"
//字库存放起始地址
#define FONTINFOADDR (4916+100)*1024 //MiniSTM32 是从 4.8M+100K 地址开始的
//字库信息结构体. 
//用来保存字库基本信息,地址,大小等
_font_info ftinfo;
//字库存放在 sd 卡中的路径
const u8 *GBK24_PATH="0:/SYSTEM/FONT/GBK24.FON"; //GBK24 的存放位置
const u8 *GBK16_PATH="0:/SYSTEM/FONT/GBK16.FON"; //GBK16 的存放位置
const u8 *GBK12_PATH="0:/SYSTEM/FONT/GBK12.FON"; //GBK12 的存放位置
const u8 *UNIGBK_PATH="0:/SYSTEM/FONT/UNIGBK.BIN";//UNIGBK.BIN 的存放位置
//显示当前字体更新进度
//x,y:坐标
//size:字体大小
//fsize:整个文件大小
//pos:当前文件指针位置
u32 fupd_prog(u16 x,u16 y,u8 size,u32 fsize,u32 pos)
{
float prog; u8 t=0XFF;
prog=(float)pos/fsize;
prog*=100;
if(t!=prog)
{
LCD_ShowString(x+3*size/2,y,240,320,size,"%");
t=prog;
if(t>100)t=100;
LCD_ShowNum(x,y,t,3,size);//显示数值
}
return 0; 
} 
//更新某一个
//x,y:坐标
//size:字体大小
//fxpath:路径
//fx:更新的内容 0,ungbk;1,gbk12;2,gbk16;3,gbk24;
//返回值:0,成功;其他,失败.
u8 updata_fontx(u16 x,u16 y,u8 size,u8 *fxpath,u8 fx)
{
u32 flashaddr=0; u16 bread; u32 offx=0; 
FIL * fftemp;
u8 *tempbuf; u8 res; u8 rval=0; 
fftemp=(FIL*)mymalloc(sizeof(FIL)); //分配内存
if(fftemp==NULL)rval=1;
tempbuf=mymalloc(4096); //分配 4096 个字节空间
if(tempbuf==NULL)rval=1;
res=f_open(fftemp,(const TCHAR*)fxpath,FA_READ); 
if(res)rval=2;//打开文件失败 
if(rval==0)
{
switch(fx)
{
case 0: //更新 UNIGBK.BIN
ftinfo.ugbkaddr=FONTINFOADDR+sizeof(ftinfo);// UNIGBK 转换码表
ftinfo.ugbksize=fftemp->fsize; //UNIGBK 大
flashaddr=ftinfo.ugbkaddr;
break;
case 1:
ftinfo.f12addr=ftinfo.ugbkaddr+ftinfo.ugbksize; //GBK12 字库地址
ftinfo.gbk12size=fftemp->fsize; //GBK12 字库大小
flashaddr=ftinfo.f12addr; //GBK12 的起始地址
break;
case 2:
ftinfo.f16addr=ftinfo.f12addr+ftinfo.gbk12size; // GBK16 字库地址
ftinfo.gbk16size=fftemp->fsize; //GBK16 字库大小
flashaddr=ftinfo.f16addr; //GBK16 的起始地址
break;
case 3:
ftinfo.f24addr=ftinfo.f16addr+ftinfo.gbk16size; // GBK24 字库地址
ftinfo.gkb24size=fftemp->fsize; //GBK24 字库大小
flashaddr=ftinfo.f24addr; //GBK24 的起始地址
break;
} 
while(res==FR_OK)//死循环执行
{
res=f_read(fftemp,tempbuf,4096,(UINT *)&bread); //读取数据
if(res!=FR_OK)break; //执行错误
SPI_Flash_Write(tempbuf,offx+flashaddr,4096); //从 0 开始写入 4096 个数据 
 offx+=bread; 
fupd_prog(x,y,size,fftemp->fsize,offx); //进度显示
if(bread!=4096)break; //读完了.
} 
f_close(fftemp);
}
myfree(fftemp); //释放内存
myfree(tempbuf); //释放内存
return res;
}
//更新字体文件,UNIGBK,GBK12,GBK16,GBK24 一起更新
//x,y:提示信息的显示地址
//size:字体大小
//提示信息字体大小 
//返回值:0,更新成功;
// 其他,错误代码. 
u8 update_font(u16 x,u16 y,u8 size)
{
u8 *gbk24_path=(u8*)GBK24_PATH;
u8 *gbk16_path=(u8*)GBK16_PATH
u8 *gbk12_path=(u8*)GBK12_PATH;
u8 *unigbk_path=(u8*)UNIGBK_PATH;
u8 res; 
res=0XFF;
ftinfo.fontok=0XFF;
 SPI_Flash_Write((u8*)&ftinfo,FONTINFOADDR,sizeof(ftinfo));
//清除之前字库成功的标志.防止更新到一半重启,导致的字库部分数据丢失.
SPI_Flash_Read((u8*)&ftinfo,FONTINFOADDR,sizeof(ftinfo));
//重新读出 ftinfo 结构体数据
LCD_ShowString(x,y,240,320,size,"Updating UNIGBK.BIN");
res=updata_fontx(x+20*size/2,y,size,unigbk_path,0); //更新 UNIGBK.BIN
if(res)return 1;
LCD_ShowString(x,y,240,320,size,"Updating GBK12.BIN ");
res=updata_fontx(x+20*size/2,y,size,gbk12_path,1); //更新 GBK12.FON
if(res)return 2;
LCD_ShowString(x,y,240,320,size,"Updating GBK16.BIN ");
res=updata_fontx(x+20*size/2,y,size,gbk16_path,2); //更新 GBK16.FON
if(res)return 3;
LCD_ShowString(x,y,240,320,size,"Updating GBK24.BIN ");
res=updata_fontx(x+20*size/2,y,size,gbk24_path,3); //更新 GBK24.FON
if(res)return 4; 
ftinfo.fontok=0XAA; //全部更新好了
 SPI_Flash_Write((u8*)&ftinfo,FONTINFOADDR,sizeof(ftinfo)); //保存字库信息
return 0;//无错误.
} 
//初始化字体
//返回值:0,字库完好.
// 其他,字库丢失
u8 font_init(void)
{ 
SPI_Flash_Init(); 
SPI_Flash_Read((u8*)&ftinfo,FONTINFOADDR,sizeof(ftinfo));//读出 ftinfo 结构体数据
if(ftinfo.fontok!=0XAA)return 1; //字库错误. 
return 0;
}

fontupd.h

#ifndef __FONTUPD_H__
#define __FONTUPD_H__
#include <stm32f10x.h>
//前面 4.8M 被 fatfs 占用了.
//4.8M 以后紧跟的 100K 字节,用户可以随便用.
//4.8M+100K 字节以后的字节,被字库占用了,不能动!
//字体信息保存地址,占 33 个字节,第 1 个字节用于标记字库是否存在.后续每 8 个字节一组
//分别保存起始地址和文件大小
extern u32 FONTINFOADDR;
//字库信息结构体定义
//用来保存字库基本信息,地址,大小等
__packed typedef struct 
{
u8 fontok; //字库存在标志,0XAA,字库正常;其他,字库不存在
u32 ugbkaddr; //unigbk 的地址
u32 ugbksize; //unigbk 的大小
u32 f12addr; //gbk12 地址
u32 gbk12size; //gbk12 的大小
u32 f16addr; //gbk16 地址
u32 gbk16size; //gbk16 的大小
u32 f24addr; //gbk24 地址
u32 gkb24size; //gbk24 的大小
}_font_info; 
extern _font_info ftinfo; //字库信息结构体
u32 fupd_prog(u16 x,u16 y,u8 size,u32 fsize,u32 pos); //显示更新进度
u8 updata_fontx(u16 x,u16 y,u8 size,u8 *fxpath,u8 fx); //更新指定字库
u8 update_font(u16 x,u16 y,u8 size); //更新全部字库
u8 font_init(void);
#endif

text.c

#include "sys.h" 
#include "fontupd.h"
#include "flash.h"
#include "lcd.h"
#include "text.h"
#include "string.h" 
//code 字符指针开始
//从字库中查找出字模
//code 字符串的开始地址,GBK 码
//mat 数据存放地址 (size/8+((size%8)?1:0))*(size) bytes 大小
//size:字体大小
void Get_HzMat(unsigned char *code,unsigned char *mat,u8 size)
{unsigned char qh,ql;
unsigned char i; 
unsigned long foffset; 
u8 csize=(size/8+((size%8)?1:0))*(size);//得到该字体一个汉字对应点阵集所占字节数
qh=*code;
ql=*(++code);
if(qh<0x81||ql<0x40||ql==0xff||qh==0xff)//非 常用汉字
{ 
 for(i=0;i<csize;i++)*mat++=0x00;//填充满格
 return; //结束访问
} 
if(ql<0x7f)ql-=0x40;//注意!
else ql-=0x41;
qh-=0x81; 
foffset=((unsigned long)190*qh+ql)*csize; //得到字库中的字节偏移量 
switch(size)
{
case 12:SPI_Flash_Read(mat,foffset+ftinfo.f12addr,24);break;
case 16:SPI_Flash_Read(mat,foffset+ftinfo.f16addr,32);break;
case 24:SPI_Flash_Read(mat,foffset+ftinfo.f24addr,72);break;
} 
} 
//显示一个指定大小的汉字
//x,y :汉字的坐标
//font:汉字 GBK 码
//size:字体大小
//mode:0,正常显示,1,叠加显示 
void Show_Font(u16 x,u16 y,u8 *font,u8 size,u8 mode)
{
u8 temp,t,t1;
u16 y0=y;
u8 dzk[72]; 
u8 csize=(size/8+((size%8)?1:0))*(size);//得到字体一个字符对应点阵集所占的字节数
if(size!=12&&size!=16&&size!=24)return; //不支持的 size
Get_HzMat(font,dzk,size); //得到相应大小的点阵数据
for(t=0;t<csize;t++)
{ 
temp=dzk[t]; //得到点阵数据 
for(t1=0;t1<8;t1++)
{
if(temp&0x80)LCD_Fast_DrawPoint(x,y,POINT_COLOR);
else if(mode==0)LCD_Fast_DrawPoint(x,y,BACK_COLOR); 
temp<<=1
y++;
if((y-y0)==size) { y=y0; x++; break; }
} 
} 
}
//在指定位置开始显示一个字符串 
//支持自动换行
//(x,y):起始坐标
//width,height:区域
//str :字符串
//size :字体大小
//mode:0,非叠加方式;1,叠加方式 
void Show_Str(u16 x,u16 y,u16 width,u16 height,u8*str,u8 size,u8 mode)
{
……此处代码省略
} 
//在指定宽度的中间显示字符串
//如果字符长度超过了 len,则用 Show_Str 显示
//len:指定要显示的宽度
void Show_Str_Mid(u16 x,u16 y,u8*str,u8 size,u8 len)
{
……//此处代码省略
} 

此部分代码总共有 4 个函数,我们省略了两个函数(Show_Str_Mid 和 Show_Str)的代码, 另外两个函数,Get_HzMat 函数用于获取 GBK 码对应的汉字字库,通过在外部 flash 查找字库,然后返回对应的字库点阵。Show_Font 函数用于在指定地址显示一 个指定大小的汉字,采用的方法和 LCD_ShowChar 所采用的方法一样,都是画点显示,这里就 不细说了。保存此部分代码,并把 text.c 文件加入 TEXT 组下。text.h 里面都是一些函数申明,,,

最后,随便看看主函数内容示例,代码如下:

int main(void)
{
u32 fontcnt; 
u8 i,j;
u8 fontx[2];//gbk 码
u8 key,t; 
delay_init(); //延时函数初始化 
uart_init(9600); //串口初始化为 9600
LCD_Init(); //初始化液晶
LED_Init(); //LED 初始化
KEY_Init(); //按键初始化 
usmart_dev.init(72); //usmart 初始化
mem_init(); //初始化内存池 
exfuns_init(); //为 fatfs 相关变量申请内存 
exfuns_init(); //为 fatfs 相关变量申请内存 
 f_mount(fs[0],"0:",1); //挂载 SD 卡
f_mount(fs[1],"1:",1); //挂载 FLASH.
while(font_init()) //检查字库
{
UPD: 
LCD_Clear(WHITE); //清屏
POINT_COLOR=RED; //设置字体为红色 
LCD_ShowString(60,50,200,16,16,"Mini STM32");
while(SD_Initialize()) //检测 SD 卡
{
LCD_ShowString(60,70,200,16,16,"SD Card Failed!");
delay_ms(200);
LCD_Fill(60,70,200+60,70+16,WHITE);
delay_ms(200); 
} 
LCD_ShowString(60,70,200,16,16,"SD Card OK");
LCD_ShowString(60,90,200,16,16,"Font Updating...");
key=update_font(20,110,16);//更新字库
while(key)//更新失败
{ 
LCD_ShowString(60,110,200,16,16,"Font Update Failed!"); delay_ms(200);
LCD_Fill(20,110,200+20,110+16,WHITE); delay_ms(200);
} 
LCD_ShowString(60,110,200,16,16,"Font Update Success!");
delay_ms(1500);
LCD_Clear(WHITE);//清屏 
} 
POINT_COLOR=RED; 
Show_Str(60,110,200,16,"2023年 3 月 14 日",16,0);
Show_Str(60,130,200,16,"按 KEY0,更新字库",16,0);
POINT_COLOR=BLUE; 
Show_Str(60,150,200,16,"内码高字节:",16,0); 
Show_Str(60,170,200,16,"内码低字节:",16,0); 
Show_Str(60,190,200,16,"汉字计数器:",16,0);
Show_Str(60,220,200,24,"对应汉字为:",24,0); 
Show_Str(60,244,200,16,"对应汉字(16*16)为:",16,0);
Show_Str(60,260,200,12,"对应汉字(12*12)为:",12,0);
while(1)
{
fontcnt=0;
for(i=0x81;i<0xff;i++)
{
fontx[0]=i;
LCD_ShowNum(148,150,i,3,16); //显示内码高字节 
for(j=0x40;j<0xfe;j++)
{
if(j==0x7f)continue;
fontcnt++;
LCD_ShowNum(148,170,j,3,16); //显示内码低字节
LCD_ShowNum(148,190,fontcnt,5,16);//汉字计数显示
fontx[1]=j;
Show_Font(60+132,220,fontx,24,0); 
Show_Font(60+144,244,fontx,16,0); 
Show_Font(60+108,260,fontx,12,0); 
t=200;
while(t--)//延时,同时扫描按键
{
delay_ms(1);
key=KEY_Scan(0);
if(key==KEY0_PRES)goto UPD;
}
LED0=!LED0;
} 
}
} 
}

我们通过下 载代码到 STM32 开发板上,可以看到 LCD 开始显示三种大小的汉字及汉字内 码

物联沃分享整理
物联沃-IOTWORD物联网 » STM32汉字显示方法详解

发表评论