前言
在形形色色的嵌入式产品中,实现中文显示是一个广泛的需求。还记得我刚学习单片机时在一块LCD12864上显示出中文的激动,果然,岁月不居,时节如流哇!
那时我使用的是一个带字库的LCD12864显示模块,当然,那时我其实还没有所谓字库的概念。
那么何谓字库,如果是不带字库的显示屏如何更好的显示汉字呢?
来来来,一步一步聊~
一、字库、编码及显示
字库是一个包含各种字符的数据库,每个字符都对应着一个字模。字模是一个矩阵,每个点代表一个字的一个像素。字库的设计方法通常有两种:点阵字库和矢量字库。当然,在单片机领域一般都是点阵字库,至少在我接触的世界里如此~
我们从一张截图开始讲起吧
如上图,这是一个字库生成软件,或者叫取模软件,我们设置好要生成的字库的字体样式、大小,点阵大小,二值化阈值、字节排放方式等等等等,然后点击生成字库,就能一个bin文件。
这个bin文件里就是整个编码集里面的所有字符的点阵数据啦,这些点阵数据将按照指定的编码格式进行排放。
那么什么是字符编码呢?这个说来可就话长了,嗯,大概可以类比身份证号码和真人一一对应,字符编码也是这样,将一个个字符用数字按指定规则进行一一映射。
编码集根据制定标准的不同有很多很多,ASCII码、GB2312、GBK、UTF-8、Big5、Unicode等等等等,他们之间有的可以兼容,有的完全不兼容,对于我们单片机领域,ASCII码和GB2312就够用啦,最多再用GBK去替代GB2312一下,如果你要显示繁体字,再涉及一下Big5这样。
我们就GB2312讲解一下吧~
GB 2312 标准由中国国家标准总局 1980 年发布,GB 即国标,共收录 6763 个汉字,其中一级汉字 3755 个,二级汉字 3008 个。
GB 2312 的出现,基本满足了汉字的计算机处理需要,它所收录的汉字已经覆盖中国大陆 99.75% 的使用频率。
对于人名、古汉语等方面出现的罕用字,GB 2312 不能处理,这导致了后来 GBK 及 GB 18030 汉字字符集的出现。
GB 2312 收录了包括拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母在内的 682 个全角字符。
GB 2312 兼容 ASCII 码(0 - 127),之后对任意一个图形字符都采用两个字节表示,高位字节和低位字节都大于 127。
GB 2312 字符集分成 94 个区,每区有 94 个位,分别对应第一字节和第二字节,这种表示方式也称为区位码。
01-09 区为特殊符号
10-15 区为用户自定义符号区(未编码)
16-55 区为一级汉字,按拼音排序,共 3755 个
56-87 区为二级汉字,按部首/笔画排序,共 3008 个
88-94 区为用户自定义汉字区(未编码)
参考链接-GB2312 编码表
如上图,对于汉字“恒”,它的GB2312编码即为0xBAE3。值得一题的是,就keil来说,在Configuration–>Editor–>Encoding路径下可以旋转编码格式,我们一般是使用gb2312。
我们可以写个函数来直观的感受一下编码
void getFontImg(uint8_t *hz)
{
while(*hz){
usb_uart_printf("%2x ",*hz++);
}
usb_uart_printf("\r\n");
}
主函数中调用 getFontImg("赵子恒");
串口打印效果如下;
可以看到,汉字字符串“赵子恒”
它的存储其实是"0xd5 0xd4 0xd7 0xd3 0xba 0xe3 0x00",其中0xbae3正是“恒”
字的gb2312编码。
ok,我们再讲回字库,上图我通过那个生成软件获得了一个bin文件,我说过,这个bin文件就是所有gb2312字符集的点阵数据。那么,我们怎么将gb2312编码和这个字库联系起来呢?
字库软件在生成的时候描述了这个字库的一些信息,比如编码范围、点阵形状、排置方式等等,嗯,你看,这个字库软件生成的gb2312并不完整,完整的gb2312字符集是从A1A0开始的,不过前面省略的这些字符集一般用不上,所以为了节省空间,它就省略了。
继续,怎么将gb2312编码和这个字库联系起来呢?很简单,对齐、然后索引。
首先,我们需要把这个bin文件下载到flash的一个指定起始地址、然后连续存储起来。这里就有了一个起始地址,比如是0x80000
然后我们用j-flash工具烧录到mcu里面。嗯,这里我随便拿的个手边的nrf52840做验证,字库大小是475k,nrf52840的flash是1M。不搞协议栈,完全够了,下进去
然后怎么使用这个字库嘞~
嗯,口述有点说不清,我们随便写个代码来讲解吧
#define HZ_24_FONT_SIZE 24*24/8 //每个字符所占的字节数
#define FONT24_FLASH_ADDR 0x00080000 //flash起始地址
void getFontImg(uint8_t *hz)
{
uint32_t index = 0;
uint8_t *pdata = 0;
uint8_t gbkH=0,gbkL=0;
uint8_t i=0,t=0,x=0;
while(*hz){
if(*hz < 0xa1)
{
//调ASCLL字符集字库
return;
}else{
gbkH = *hz++;//获取编码高位
gbkL = *hz++;//获取编码低位
index = (gbkH-0xB0) * 94ul + (gbkL-0xA1);//获取该编码在字库编码集中的序号,即第几个字符
index = index * HZ_24_FONT_SIZE; //乘上每个字符的点阵所占字节数,即这个字符数据的起始地址在字库的第几个字节
index = index + FONT24_FLASH_ADDR;//加上flash存储的起始地址,即这个字符数据的起始地址在flash的地址
}
pdata = (uint8_t *)index;//获取flash中的点阵数据
//按照点阵数据的排置方式,进行打点
for(i=0;i<HZ_24_FONT_SIZE;i++)
{
uint8_t temp= pdata[i];
for(t=0;t<8;t++)
{
if(temp&0x80)
{
usb_uart_printf("@@");
}
else
{
usb_uart_printf(" ");
}
temp<<=1;
x++;
if(x == 24){//换行
x = 0;
usb_uart_printf("\r\n");
}
}
}
}
}
调用getFontImg("赵子恒");
效果如下:
二、抗锯齿
1、什么是锯齿?
由于LCD是由一个个独立的像素点组成,若不做任何处理,在图片或字体显示时,前景色和背景色交汇处会产生锯齿现象,如下图:
由像素点表示图像,锯齿是不可避免的,这是因为像素点与像素点之间并不是连续变化的。但如果我们从前景色平滑过渡到背景色,在视觉上就会减弱锯齿现象,如下图:
2、如何实现抗锯齿?
在一开始思考这个问题的时候,我尝试去用别人的开源GUI框架,不过和我当时的项目并不是很匹配,便没有往这个方向深究。
我当时想了一个取巧的方式,就是通过调整二值化阈值,生成3个不同二值化的字库,然后遍历3个字库,得到带灰度值的字库,至少从实际效果上而言,是很好的达到了需求。
后来我会了一些python,我想,我可以将3个字库合成一个字库,每个像素点用2个bit去表示,这样字库大2倍,但可以表示4级灰度值。后来我看了大佬的一篇博客,才知道这原来就是常规方法
参考链接----【玩转arm-2d】手把手教你实现抗锯齿的字体
除了这,还可以用数学的方法,就类似图像的平滑滤波,我们用个3*3的全1的卷积核,对取得的字库进行遍历卷积一遍,这样理论上应该也能起到抗锯齿的作用,不过我并没有验证,但我估计小字体可能会糊掉,改天有空再验证验证~~~