linux字符点阵的显示-韦东山第三期笔记

时间:2021-12-29 16:15:40

    最近在学习韦东山系列课程的第三期,本篇文章做一些记录。

    本篇,我的目标是学习字符点阵的显示。首先要明确这个工作的整体流程和原理。显示的字符分为字母和汉字,无论是显示那种,原理都是点亮表示该字符的像素点阵。如果是显示字母,那么专门来表示字母的点阵数组,如果是显示汉字,那么需要用表示汉字的点阵库,比如HZK16。

    首先要明白的是,像素点阵的和字符的关系。想象一下现在有一个大方块,然后把它切割乘8*16的小方块,这里面每一个小方块都是一个像素。然后对一些像素的点亮和熄灭就可以显示不同的文字。

    在显示字母的过程中,这个表示字母的大方块就是8*16的在程序中,很多个字母放在一个数组里面,这个可以表示很多字母的数组如下:

#define FONTDATAMAX 4096

static const unsigned char fontdata_8x16[FONTDATAMAX] = {

    /* 65 0x41 'A' */
    0x00, /* 00000000 */
    0x00, /* 00000000 */
    0x10, /* 00010000 */
    0x38, /* 00111000 */
    0x6c, /* 01101100 */
    0xc6, /* 11000110 */
    0xc6, /* 11000110 */
    0xfe, /* 11111110 */
    0xc6, /* 11000110 */
    0xc6, /* 11000110 */
    0xc6, /* 11000110 */
    0xc6, /* 11000110 */
    0x00, /* 00000000 */
    0x00, /* 00000000 */
    0x00, /* 00000000 */
    0x00, /* 00000000 */
}

从这个数组可以看到,可以用16个八位二进制数表示一个字母,这八位二进制数就代表了像素,如果是1就点亮,0就熄灭,然后点亮的轮廓就是字符A。这是显示字母的库。

    显示汉字我们用HZK16这个汉字库,HZK16字库是符合GB2312标准的16×16点阵字库,使用这个汉字库之前要研究一下他如何使用。举个例子,我们要显示汉字‘中’字,这个中字的国标编码是D6D0。在HZK16里面,存储了很多组类似D6D0的数据,每一组都代表一个字符,然后这些数据联系到了一个点阵,然后就去可以扫描这个点阵了。介绍一下这个汉字库,这个汉字库有很多个区块,每个区块有96个字符,但是每个区域第一个和最后一个都是空的,没有具体的字符。一个区域的编码如下:

code  +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F
A1A0       、 。 · ˉ ˇ ¨ 〃 々 — ~ ‖ … ‘ ’
A1B0  “ ” 〔 〕 〈 〉 《 》 「 」 『 』 〖 〗 【 】
A1C0  ± × ÷ ∶ ∧ ∨ ∑ ∏ ∪ ∩ ∈ ∷ √ ⊥ ∥ ∠
A1D0  ⌒ ⊙ ∫ ∮ ≡ ≌ ≈ ∽ ∝ ≠ ≮ ≯ ≤ ≥ ∞ ∵
A1E0  ∴ ♂ ♀ ° ′ ″ ℃ $ ¤ ¢ £ ‰ § № ☆ ★
A1F0  ○ ● ◎ ◇ ◆ □ ■ △ ▲ ※ → ← ↑ ↓ 〓   
D6A0     帧 症 郑 证 芝 枝 支 吱 蜘 知 肢 脂 汁 之 织
D6B0  职 直 植 殖 执 值 侄 址 指 止 趾 只 旨 纸 志 挚
D6C0  掷 至 致 置 帜 峙 制 智 秩 稚 质 炙 痔 滞 治 窒
D6D0  中 盅 忠 钟 衷 终 种 肿 重 仲 众 舟 周 州 洲 诌
D6E0  粥 轴 肘 帚 咒 皱 宙 昼 骤 珠 株 蛛 朱 猪 诸 诛
D6F0  逐 竹 烛 煮 拄 瞩 嘱 主 著 柱 助 蛀 贮 铸 筑   

从这个例子就可以看出来,中是D6D0,这个D6就是区域号,D0表示中在这个区域的哪个位置。相当于横纵坐标。只要横纵两个方向位置确定了,就可以找到他了。可以在网上找他这个汉字库的完成的表格,可以看到,第一个区域是从A1开始的,每个区域的第一个字符是从A1开始的。那么就是说只要我们得到想要显示的汉字的编码就可以在汉字库里面找到的位置了,然后我们拿到这个点阵,再去扫描它,点亮该点亮的像素,字体就出来了。

    字符点阵的显示原理大体就是这样,接下来说整体的流程。首先我们要打开设备文件,然后得到设备的可变信息和固定信息。

int fd_fb;
struct fb_var_screeninfo var;	/* Current var */
struct fb_fix_screeninfo fix;	/* Current fix */

fd_fb = open("/dev/fb0", O_RDWR);
ioctl(fd_fb, FBIOGET_VSCREENINFO, &var);
ioctl(fd_fb, FBIOGET_FSCREENINFO, &fix);

这个open打开的/dev/fb0是和Framebuffter有关的,不是很明白,等等再去了解。用ioctl函数获取可变参数和固定参数var和fix,他们是两个结构体,里面有一些数据。上面可以用ctrl+鼠标左键去看一下这个结构体里面都有什么。例举一些:

struct fb_var_screeninfo {
	__u32 xres;			/* visible resolution		*/
	__u32 yres;
	__u32 xres_virtual;		/* virtual resolution		*/
	__u32 yres_virtual;
	__u32 xoffset;			/* offset from virtual to visible */
	__u32 bits_per_pixel;		/* guess what			*/
}

这个可变参数里面有xy方向的分辨率,bpp每个像素的位数等等。然后就可以利用这些参数得到每行的大小,整个屏幕的大小,这里所说的大小都是指的字节。然后映射,函数如下:

fbmem = (unsigned char *)mmap(NULL , screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd_fb, 0);

这里有一些需要了解的东西,

1. 一般调用函数时候,如果没有的特殊的返回值的话,调用函数执行正确返回0,出错返回-1。

2. mmap函数,这个函数的功能是做一个映射,把一个可以操作的空间映射到内核。函数原型如下:

void *mmap(void *start,size_t length,int prot,int flags,int fd,off_t offsize);

从参数的名字就可以看出来各个参数的意义。第一个参数 *start是映射的其实地址,如果是NULL的话,就由系统自己指定。第二个参数length是长度,就是想要映射的内存大小。第三个参数是prot,不知道什么意思,百度一下,知道他是映射区域的保护方式,举例PROT_READ是映射区域可被读取,还有其他的不一一列举,百度学习即可。第四个参数flags,从名字上看应该是标志的意思,影响映射区域的各种特性, MAP_SHARED 对映射区域的写入数据会复制回文件内,而且允许其他映射该文件的进程共享。这个不是很了解,以后再做了解。第五个参数fd就是之前打开的那个文件要映射的,要映射到内存中的文件描述符。最后一个是offsize文件映射的偏移量,通常设置为0。最后,若映射成功则返回映射区的内存起始地址,否则返回MAP_FAILED(-1)。那么假设现在调用这个函数映射成功了,返回了这段空间的起始地址,是地址,为了能够使用,强制转换为指针。好了,现在就可以通过这个指针操作这个空间了。

    接下来,实现打开汉字库,再汉字库的映射,之后就可以用指针去操作这个汉字库了,代码如下:

int fd_hzk16;
struct stat hzk_stat;
unsigned char *hzkmem;
fd_hzk16 = open("HZK16", O_RDONLY);
fstat(fd_hzk16, &hzk_stat)
hzkmem = (unsigned char *)mmap(NULL , hzk_stat.st_size, PROT_READ, MAP_SHARED, fd_hzk16, 0);

首先,open函数打开这个汉字库,方式是只读。然后做映射,映射之前,需要得到这个汉字库的大小,用统计函数fstat,函数的用法可以通过百度查找,或者在linux下,man fstat,得到更加的详细的fstat函数的使用信息。这个函数第一个参数是打开的那个文件,第二个参数是一个结构体,里面有这个文件大小参数st_size。然后是mmap函数吗,这个函数的用法和各个参数上面已经大致的介绍了,有一点需要注意,prot参数指定这个问价保护方式应该是和打开这个文件是一样的。

    到这里,做了一些映射,需要额外查的知识有:mmap函数的使用,他的各个参数的多种选择的意义、fstat函数和stat函数和lstat函数的区别。接下来要去显示一个字母字符A和一个汉字字符中,用的分别是之前映射好的汉字库和字符数组。显示之前,要做清除屏幕,把屏幕上所有的像素全部关闭,函数及其圆形如下:

memset(fbmem, 0, screen_size);
void *memset(void *s, int ch, size_t n);

功能是:将s所指向的某一块内存中的后n个 字节的内容全部设置为ch指定的ASCII值, 第一个值为指定的内存地址,块的大小由第三个参数指定,这个函数通常为新申请的内存做初始化工作, 其返回值为s。就是清除1,熄灭所有像素。

unsigned char str[] = "中";

lcd_put_ascii(var.xres/2, var.yres/2, 'A');
lcd_put_chinese(var.xres/2 + 8,  var.yres/2, str);

写如上两个显示字符和汉字的函数,在去实现他们,先实现显示A的代码如下:

void lcd_put_ascii(int x, int y, unsigned char c)
{
    unsigned char *dots = (unsigned char *)&fontdata_8x16[c*16];
    int i, b;
    unsigned char byte;
        
    for (i = 0; i < 16; i++)
    {
	byte = dots[i];
	for (b = 7; b >= 0; b--)
	{
	    if (byte & (1<<b))
	    {
	        lcd_put_pixel(x+7-b, y+i, 0xffffff); /* 白 */
            }
	    else
	    {
	        lcd_put_pixel(x+7-b, y+i, 0); /* 黑 */
	    }
	}
    }
}
原理和功能:这个函数是用来扫描这个字符点阵的像素的,需要点亮就点亮,需要熄灭就熄灭。然后形成一个字形的轮廓。首先我们要得到这个字符点阵的数组或者指针。字母字符是存储在fontdata_8x16的数组里面,这是一个用8x16的像素点阵,也就是每个字符用128个像素表示,即128个二进制的0或者1,也就是每个字符用了16个字节来表示,这个数组的使用的ascii码表示的,上面的A其实是65,在数组中也就是第65项,每一项目(一个字符)又是16个字节,那么想要得到这个字符点阵的第一个字节或者指向这个字符点阵第一个字节的unsigned char 指针,首先要得到这个字符点阵的第一个字节,然后把他转为地址,再次转为指针。那么直接赋值给指针可以么?(这个下一步在做验证)然后通过这个指针(其实也是数组),就可以取出每一行的8位数,即一个字节,然后去每一个位判断是0还是1,是1就点亮,是0就熄灭。然后把像素的坐标传给操作像素的函数,去熄灭还是点亮。

    接下来是显示汉字的程序:

void lcd_put_chinese(int x, int y, unsigned char *str)
{
	unsigned int area  = str[0] - 0xA1;
	unsigned int where = str[1] - 0xA1;
	unsigned char *dots = hzkmem + (area * 94 + where)*32;
	unsigned char byte;

	int i, j, b;
	for (i = 0; i < 16; i++)
		for (j = 0; j < 2; j++)
		{
			byte = dots[i*2 + j];
			for (b = 7; b >=0; b--)
			{
				if (byte & (1<<b))
				{
					/* show */
					lcd_put_pixel(x+j*8+7-b, y+i, 0xffffff); /* 白 */
				}
				else
				{
					/* hide */
					lcd_put_pixel(x+j*8+7-b, y+i, 0); /* 黑 */
				}
				
			}
		}
	
}

之前说过,使用HZK16时候,要知道这个汉字在那个区域,区域中的哪个位置,区域和位置都是从A1开始的,想要得到区域和位置就要-A1,汉字“中”的国标码是D6D0,D6是区域码,D0是位置码,分别减去他们起始的就是得到这个汉字“中”点阵了。但是要注意的是,我们先要得到的是这个中在汉字库的位置,得到的区域码要x94,因为每个区域有94个字符,再加上位置码,就得到这个中在这个汉字库中的位置,再加上汉字库的位置,就可以得到这个中的字符点阵的指针了。然后原理和字母点阵的扫描原理是一样的,位移的区别是,这个hzk16是16x16的,也就是16行16列,每一行是两个字节,不是一个了。

    然后我们实现点亮像素函数:

/* color : 0x00RRGGBB */
void lcd_put_pixel(int x, int y, unsigned int color)
{
	unsigned char *pen_8 = fbmem+y*line_width+x*pixel_width;
	unsigned short *pen_16;	
	unsigned int *pen_32;	

	unsigned int red, green, blue;	

	pen_16 = (unsigned short *)pen_8;
	pen_32 = (unsigned int *)pen_8;

	switch (var.bits_per_pixel)
	{
		case 8:
		{
			*pen_8 = color;
			break;
		}
		case 16:
		{
			/* 565 */
			red   = (color >> 16) & 0xff;
			green = (color >> 8) & 0xff;
			blue  = (color >> 0) & 0xff;
			color = ((red >> 3) << 11) | ((green >> 2) << 5) | (blue >> 3);
			*pen_16 = color;
			break;
		}
		case 32:
		{
			*pen_32 = color;
			break;
		}
		default:
		{
			printf("can't surport %dbpp\n", var.bits_per_pixel);
			break;
		}
	}
}

这个函数显得有些复杂,从参数看他的原理,接受一个坐标,和颜色。把这个坐标的像素给予一定的颜色,就是点亮了。我们的颜色是用24位数表示的0xRRGGBB,但是屏幕的像素不知道是用多少位表示的。可能是8、16、32。var.bits_per_pixel(bpp)是个像素的位数,这个意思是:如果bpp是8位的,那么他只能接受8位的颜色数据。同理如果是32位的,只能接受32位的颜色数据。好的,于是就有,如果是32位的,那么我们前置的颜色用0xffffff24位置的就可以直接给他,还有8位没有怎么办?没有就是0。如果是8位的呢?也直接给他,我们的0xffffff,给8位的,就是保存最后八位,舍弃前面的。就是0xff了。那如果是16位的话,就是RGB(红绿蓝)565的位数,是这样排放的,从高到底是红色R有5位、绿色G有6位、蓝色B有5位置。我们的程序传进去的是0xffffff,都是8位的。对于R取高5位,对于G取高6位,对于B取高5位即可,在组成一个16位的的数,通过位操作,即可实现。

    到这里,显示一个字符的原理,整个流程,就比较清楚了。最后记录一下我的体会。一开始我只是抄写代码,然后编译改错,很多函数的功能和原理都不清楚,这样导致了,有时候我都不知道我在做什么,为什么这么做,后来我自己去百度相关的函数,比如,memst、mmap函数,对这些函数有了多一些的认识和了解,虽然还没达到随心所欲的写出自己的代码,但是对于那些函数,我都有所了解,不是一点不懂,我相信随着我的努力,了解学习更多的知识,会有更高的收获,而且我能写出自己设计的功能代码。

    下一篇预计是记录关于学习Freetype的笔记,这个里面涉及了很多抽象的信息,我在想能不能用稍微宏观一点的东西来解释他们。