🐱作者:一只大喵咪1201
🐱专栏:《智能家居项目》
🔥格言:你只管努力,剩下的交给时间!
今天实现上图整个项目系统中的字体子系统和显示子系统。
目录
🀄设计思路
在显示设备上显示字体其实也是比较复杂的,显示的字体有点阵字体,矢量字体等方式。
- 使用点阵绘制文字时:每个文字的大小一样,前后文字互不影响:
如上图所示,点阵字体中的每个字体的点阵大小都是固定的,也就是需要的像素点个数是固定的,例如8*16
就是宽用8个像素点,长用16个像素点,无论是汉字,字母,数字甚至是一个标点符合,都用8*16
个像素点。
- 点阵方式的字体并不连续,字体与字体之间分隔较远,看上去并不是那么美观。
- 使用Freetype(矢量字体)绘制文字时:大小可能不同,前面文字会影响后面文字:
如上图所示,矢量字体中的每个字体的大小是不一样的,根据字体的类型,是汉字还是字母甚至是标点符合,以及前一个字体的位置,会对显示的字体做适当的调整。
- 矢量字体的排列更加紧凑合理,看起来也更美观,符合我们的生活经验。
描述点阵字体:
对于普通的点阵字体,描述该字体需要:
- X、Y方向大小及原点坐标
- 每个像素点的值
所以可以用如下结构体来表示一个点阵字体:
cpp
struct dot_font
{
int iX;
int iY; //字体坐标
int iWidth;//宽度
int iHeight;//高度
unsigned char* dots;//用于显示该字体的16字节数组(字模)
}
通过坐标以及长度和宽度可以确定字体的轮廓,将dots
数组中的数据发送给显存,一个完整的字体就显示在屏幕上了。
描述矢量字体:
如上图所示,对于矢量字体,每个字体的大小可能不一样,前一个字体会影响下一个字体,其中有两黑点非常重要:
- 左边的黑点:当前字符的原点。
- 右边的黑点:下一个字符的原点。
下一个字符的位置和上一个字符息息相关,还有其他要素,如宽度,高度,绘制的起始点等等组合在一起才可以确定一个字符。
- 绘制起点和原点不是一个,原点是相当于是整个字符的标点,字符的位置由原点决定。
- 绘制起点也就是显示开始的位置,一般是字符的左上角。
所以可以用如下结构体来表示一个矢量字体:
cpp
typedef struct FontBitMap
{
int iLeftUpX; /* 位图左上角X坐标 */
int iLeftUpY; /* 位图左上角Y坐标 */
int iWidth; /* 字体宽度 */
int iHeight; /* 字体高度 */
int iCurOriginX; /* 原点X坐标 */
int iCurOriginY; /* 原点Y坐标 */
int iNextOriginX; /* 下一个字符X坐标 */
int iNextOriginY; /* 下一个字符Y坐标 */
unsigned char* Buffer;/* 字符点阵 */
}FontBitMap, *pFontBitMap;
包含矢量字体的绘制左边(左上角坐标),字体的宽度和高度,当前字体的原点,下一个字体的原点,以及一个字模数组。
现在我们要做的就是抽象出一个结构体,既能描述点阵字体,也能描述矢量字体。
- 能用来描述矢量字体的结构体必然也能够描述点阵字体。
绘制起始坐标以及宽度和高度和点阵字体中的坐标位置以及x和y方向的长度一样,当前字符原点和下一个字符原点,点阵字体也可以通过计算得到,所以无论是点阵字体还是矢量字体,都可以共用这一个结构体。
如上图,整个字体系统并不涉及到内核或者芯片,它是属于软件层面的,所以并不需要分那么多层,都放在一层中即可。
无论是点阵字体还是矢量字体,它们都必须有字库,将字符在字库中对应的数据发送给显示设备才能显示出相应字符,常见的字库有ASCII
码字库,GBK
字符,以及FreeType
字库。
- 字库也要被描述,也要被管理起来。
🀄字体子系统
🃏管理层
先来实现对字符位图以及字库的管理。
如上图头文件中代码所示,结构体FontBitLib
是用来描述一个要显示字符的位图的,每一个字符都会创建一个结构体对象,而成员中的Buffer
中存放的就是该字符的字母数据,将这些数据发送给显示设备就能显示出对应字符。
结构体FontLib
是用来描述字库的,本喵这里只会实现ASCII码字库,其中的成员函数有很多在这里是用不到的,但是为了符合所有字库,这里本喵仍然写了,方便以后的扩展和维护。
还有一个注册字库的函数声明和一个获取字库的函数声明,获取字库的函数有__
表明该函数在另一层被调用,这样也是为了避免重复包含的问题。
如上图所示源文件代码,创建了一个用来管理字库的全局链表,以及实现了注册字库和获取字库的函数。
🃏子系统层
字库的管理已经实现了,下面该实现一下子系统层调用这些管理函数的字体系统了:
如上图所示头文件代码,提供了对字库进行一系列操作的函数声明。
如上图源文件代码所示,由于在同一时刻只能使用一个字库,所以创建了一个全局的默认字库变量,还提供了操作字库所用方法的具体实现,在初始化默认字库的时候,需要判断该字库的初始化方法是否为空,为空说明不用初始化。
🃏字库层
此时对字库的管理以及各种操作都已经实现了,但是字库还没有,所以接下来就需要实现ASCII码字库:
如上图所示是ASCII字库的头文件,只有一个增加ASCII码字库的函数声明。
如上图所示源文件中代码,包含一个ASCII码字库,这是一个全局的二维数组,字库中的数据是通过字模制作软件生成的,在本喵的文章I2C通信协议 | OLED屏中详细讲解过,有兴趣的小伙伴可以移步。
创建了全局的ASCII码字库结构体变量并进行了初始化,本喵这里的ASCII码大小是固定的,就是8*16
的,所以就在函数中就直接给了定值,对于获取字符的位图函数本喵单独讲解一下:
仍然是这个图,在显示该字符的时候,是通过原点坐标来确定位置的,也就是图中坐标的黑点,这个黑点的坐标是由用户指定的,所以在函数中该坐标是已知的。
左上角的绘画起始坐标和原点在x方向上相同,在y方向上相差字体的高度,ASCII码字符中就是16,所以可以通过当前原点坐标计算出左上角坐标。
下一个字符的原点坐标,在x方向上和当前字符原点坐标相差字体的宽度,ASCII码字符中就是8,所以也可以通过当前原点坐标计算出下一个字符的原点坐标。
字符的高度和宽度是固定的,也就是8*16
的,最重要的字模数组Buffer
中的内容就来自前面的字库ascii_font
二维数组,如果用户没有向Buffer
中存放数据,那么就直接返回字库中对应的字模数据,如果用户存放了,那么就将字库中的字模数据复制到Buffer
中。
如上图代码就是用来从字库中获取指定字符的位图数据的。
虽然将字体系统分为了三层,但是它们仍然属于系统层,只是在系统层中又细分出来的三层。
如上图所示便是这三小层各种的功能和互相之间的调用关系,子系统层只负责使用字库,并不关心字库的维护和管理,管理层则要做到细节处的管理,管理多个字库,但是并不用知道每个字库中的内容,字库层则需要详细实现自己所代表的字库,包括所有字模数据,以及字库结构体中的那些成员方法。
🀄显示子系统
显示子系统和设备子系统中的显示设备并不是一回事。
如上图,文字子系统会将从字体子系统中取出的点阵发送给设备子系统中的显示设备绘制点阵从而显示出来。
编码集:
如上图所示,字符串"ABC中国"
使用不同的编码集在内存中的数据不一样,使用Unicode
编码集中的UTF-8(左边内存窗口),每一个汉字占用3个字节,使用GB2312编码集(右边内存窗口),每一个汉字占用2个字节。
- 无论什么编码集,对ASCII码都是用一个字节表示。
编码格式:
拿常用的Unicode
编码集举例,它包含三种编码格式,其中最常用的就是UTF-8
编码格式:
Unicode数值范围(16进制) | UTF-8编码方式(二级制) |
---|---|
0000 0000-0000 007F | 0xxxxxxx |
0000 0080-0000 07FF | 110xxxxx 10xxxxxx |
0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx |
0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
第一列表示Unicode
编码集支持的编码范围,第二列表示UTF-8
编码格式,该编码格式中每个字节中包含有其他信息:
- 每个字节中,从高位到地位,遇到第一个0之前,有几个1就表示这个字符有几个字节共同表示,如果是
10xxxxxx
,10
表示当前字节和前一个字节共同表示一个字符。- 去掉表示字节个数的位以后,将剩余的位放在一起,组成的数值就是
UTF-8
编码值。
如1110xxxx 10yyyyyy 10zzzzzz
这三个字节表示的字符,这串二进制序列表示该字符用3个字节表示,组合成UTF-8
编码值为xxxxyyyy yyzzzzzz
两个字节大小。
除了UTF-8编码格式外,还有UTF-16LE,UTF-16BE等编码格式,不同的编码格式,表示同一个字符的数据也会不一样。
点阵:
从默认字库中取出字符的点阵数据,然后再选择显示设备,将字符在LCD或者OLED上显示。
如上图所示便是整个显示的过程,先确定编码集为Unicode
,再确定编码格式为UTF-8
,然后算出编码值,取出对应的点阵数据,最后在显示设备上显示。
🃏编程
显示子系统是在使用字体子系统和设备子系统中的显示设备,所以它并不用分很多层,只工作在显示子系统层(应用层)。
如上图,要想在显示设备上显示字符,有三要素,分别是具体的显示设备,字符的坐标,已经要显示的字符。
如上图代码便是整个显示系统中的核心代码,里面有几个被调用的函数本喵会单独拿出来讲解。
调用该函数显示字符串的时候,首先做的第一步就是获得字符的编码值,这里调用了GetCodeForStr
函数来获得编码值:
如上图代码所示,专门创建一个文件encoding
来处理编码值,在该函数中,可以获取指定字符不同编码集下的编码值,这里本喵就只用ASCII编码集。
使用该函数获取的是一个字符的编码值,所以对于ASCII编码集来说,直接返回该字符的ASCII码值作为该字符的编码值即可,因为ASCII编码集默认就是支持的。
得到字符的编码值以后,就要获取该编码值对应的点阵数据。
如上图,根据编码值获取点阵数据是从字体子系统中获取,该函数在ascii_font.c
中已经实现了,调用默认字库中的GetFontBitMap
即可从默认字库(ASCII码字库)中获得点阵数据。
点阵数据有了以后,就需要将点阵数据写入到显示设备在RAM中的显存中去:
如上图代码所示,该函数的就是将点阵数据写入到RAM中的显存中的,点阵数据是在FontBitMap
对象中的,将该字符的所有像素点数据都获取到并写入到显存中。
本喵这里使用的OLED显示,所以按照OLED显示方式来分析如何获取像素点的位置:
如上图所示,OLED显示的字符是8*16
的,前一页显示一个字符点阵的前八8字节,后一也显示后8个字节,每个字节中的一个比特位就是一个像素点,所以要获取的是每一个比特位中的值。
如上图代码所示,获取(iX,iY)坐标像素点的像素值时,按照OLED显示方式图,通过横坐标x确定像素点所在字节在Buffer
中的地址,然后根据像素点的y坐标确定是该字节的哪个比特位,然后通过按位与和左移操作返回该像素点的值。
如上图所示,整个显示子系统的调用关系,首先调用显示子系统中的ShowTextInDisplayDevice
函数去显示字符str
,应用层只用关心这一个函数怎么调用,不比管它的实现,站在应用层的角度,此时就可以显示出指定字符了。
显示函数又调用显示子系统中编码集层中的GetCodeStr
得到该字符的编码值,再用编码值去字体子系统中的默认字库中得到点阵数据,数据放在FontBitMap
结构体对象中。
显示子系统再调用DrawBitMapOnFrameBuffer
将点阵数据写到设备子系统中显示设备的RAM显存中,在这个函数内部,显示子系统会先调用GetPixelColorFromBitMap
函数获取像素点的颜色数据,然后再调用显示设备自带的SetPixel
方法将颜色数据写到RAM显存中。
最后会调用Flush
函数将RAM显存中的数据发送到显示设备自带的显存中,至此整个显示流程就结束了。
- 在获取编码值的时候,根据不同的编码集和编码方式获得编码值,这里可以进行扩展维护。
- 在获取像素点颜色数据的时候,显示设备的显示规则不同,获取编码值的方式也就不同,这里也可以扩展维护。
🀄测试
最后就是测试显示子系统,设备子系统和字体子系统三个系统能否成功配合在OLED上显示字符了:
如上图代码所示,在测试函数中,先将字体系统中的字库初始化完毕,包括添加字库设置默认字库等等,然后再初始化显示设备,包括指定显示设备,初始化等等步骤。
- 显示字符只用调用一个函数
ShowTextInDidsplayDevice(ptDev,16,16,str)
即可在作为为(16,16)处显示指定字符str
。
可以看到,成功显示字符,源代码中的字符串太长,涉及到了换行,所以本喵在仅显示了A Big MiaoMi
字符串,没有实现换行。