欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

[数码相框]项目储备一: 字符编码与LCD显示单个字母/汉字

程序员文章站 2022-07-14 10:26:50
...

1 字符编码

对于字符编码,网上一堆好文章,写得更详细。这里只是提及一些关键点。
字符编码的发展历程概括为:
	ASCII码--> 各国/地区独立编码(如GB2312/GBK、BIG5等)--->统一编码:unicode码
	
在面对同一字符可有不同编码,或者同一编码值可解码为不同字符,为统一编码,
出现了unicode码,值得注意的是,unicode码只是给出了每个字符对应的编码值,并没有明确
每个字符的存储字长、存储方式(大端/小端)。因此出现了utf-8、utf-16等“表示”unicode码,换句话说
utf-8/16/32等这些指定了存储unicode码的字长、存储方式。
普遍使用utf-8, 它是变长存储的(1-4字节),如何实现变长存储参考下文的一个bug的解释。

而GB2312/GBK是根据区码+位码索引某个字的,下面的“编程思路”有讲。

对于字符编码,了解这些就够了,起码对于下文的理解是没问题的。
值得注意的是在window下用记事本/notepad++打开/编辑源码文件,尤其注意文件编码。
出现乱码时。可查看编码值再看看字符编码是什么,就能锁定问题了。

另外,gcc/arm-linux-gcc 有两个实用选项:
-finput_charset : 指定输入源文件的字符编码,默认UTF-8
-fexec_charset: 指定输出elf文件的字符编码, 默认UTF-8
使用这两个选项可把不同编码的源文件编译成指定编码的可执行程序

下面看看如何实现在LCD显示单个字母和汉字

2 LCD显示单个字母/汉字

硬件:s3c2440A开发板,4.3寸LCD,型号:AT043TN24
软件:使用交叉编译工具链:arm-linux-gcc (3.4.5版本),linux2.6.22.6内核版本
目标:在LCD*显示“A中”
2.1 编程思路
1)对于显示单个字母,从2.6.22.6内核版本中获得 font_8x16.c文件,当然也可使用其它大小的点阵(如8x8 16x16,内核都有)
	怎么显示单个字母呢?
	很简单,font_8x16用数组保存字母的编码值,根据索引获得编码值后,再逐个像素显示即可。具体来讲,
	需先获得LCD的硬件参数(如bpp、分辨率、framebuffer基址),然后根据这些参数计算出某点像素的坐标,再填充颜色
	
2)对于显示单个汉字,需从HZK16这个字体文件获取,HZK16文件已上传到我的代码集合,有需要下载即可。
	HZK16字库是符合GB2312标准的16×16点阵字库,HZK16的GB2312-80支持的汉字有6763个,符号682个
	问题一:如何获取某个字(字模)呢?
	某个字的索引 = (区码, 位码)。什么是区码、位码呢?具体如何索引呢?举个例子
	“中国”的“中”字,在GBK编码中,编码值为 D6 D0,区码就是D6, 位码就是D0 
	"中"个字模的索引/偏移量按如下计算(以16x16为例)
	offset =【(区码-0xa1)×0x5e + (位码-0xa1)】×0x20 (0x20=32,即取2个字节, 16x16)
	即 offset = ( (0xD6 - 0xA1) x 0x5e + (0xD0- 0xA1)) x 0x20 = =0x274A0 = 160928
	
	问题二:为啥区码和位码要减去0xA1呢?
	因为汉字编码是从0xa0区开始的,所以文件最前面就是从0xa0区开始,要算出相对区码

	问题三:有了字模的偏移量,如何显示呢?
	答:与显示单个字母一样,逐个像素显示即可,只是字模是16x16而已。
	
	GBK2312简体中文编码:https://wenku.baidu.com/view/0ef57bfb04a1b0717fd5dd1a.html
	更详细关于HZK16的使用可参考:https://wenku.baidu.com/view/0774d20c52ea551810a68768.html
2.2 代码实现

font_8x16.c

#define FONTDATAMAX 4096

const unsigned char fontdata_8x16[FONTDATAMAX] = {
 /* 0 0x00 '^@' */
 /* 1 0x01 '^A' */
 0x00, /* 00000000 */
 0x00, /* 00000000 */
 0x7e, /* 01111110 */
 0x81, /* 10000001 */
 0xa5, /* 10100101 */
 0x81, /* 10000001 */
 0x81, /* 10000001 */
 0xbd, /* 10111101 */
 0x99, /* 10011001 */
 0x81, /* 10000001 */
 0x81, /* 10000001 */
 0x7e, /* 01111110 */
 0x00, /* 00000000 */
 0x00, /* 00000000 */
 0x00, /* 00000000 */
 0x00, /* 00000000 */
 /* ... 其它省略 */
}

备注:font_8x16.c文件太大,限于篇幅,只贴出要显示的’A’,完整文件已上传到我的代码集合

show_font.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <linux/fb.h>
#include <sys/mman.h>

extern const unsigned char fontdata_8x16[];
/* 变量定义 */
int fd_fb, fd_hzk;
static line_width, pixel_width, screen_size;
static unsigned char* fbmem, *hzk16mem;
static struct fb_var_screeninfo var;
static struct fb_fix_screeninfo fix;
static struct stat phzk_stat;

/* 函数声明(最好放在头文件) */
void lcd_put_pixel(unsigned int x, unsigned y, unsigned int color);
void lcd_put_ascii(unsigned int x, unsigned int y, unsigned char c);
void lcd_put_chinese(unsigned int x, unsigned int y, unsigned char*str);

int main(int argc, char** argv)
{
	char str[] = "жа";
	if(argc != 2)
	{
		printf("Usage: \n");
		printf("%s <字库文件>\n", argv[0]);
		return 0;
	}
 	fd_fb = open("/dev/fb0", O_RDWR); 
 	if(fd_fb < 0)
 	{
   		printf("can't open /dev/fb0\n");
   		return -1;
 	}
 	if(ioctl(fd_fb, FBIOGET_VSCREENINFO, &var))
 	{
  		printf("can't get var\n");
  		return -1;
 	}
 	if(ioctl(fd_fb, FBIOGET_FSCREENINFO, &fix))
 	{
  		printf("can't get fix\n");
  		return -1;
 	}
 	line_width = var.xres * var.bits_per_pixel /8;
 	pixel_width = var.bits_per_pixel / 8;
 	screen_size = var.xres * var.yres * pixel_width;
 	
 	fbmem=(unsigned char*)mmap(NULL, screen_size,PROT_READ | PROT_WRITE, MAP_SHARED,fd_fb, 0);
 	// NULL表示内核会自动确定分配内存的位置,返回映射内存的起始地址
 	if(fbmem == (unsigned char*)-1)
 	{
  		printf("can't map fbmem\n");
  		return -1;
 	}
 	fd_hzk = open("HZK16", O_RDONLY);
 	if(fd_hzk < 0)
 	{
  		printf("can't open HZK16\n");
  		return -1;
 	}
 	if(fstat(fd_hzk, &phzk_stat))
 	{
  		printf("can't get hzk_stat");
 	}
 	
 	hzk16mem = (unsigned char*)mmap(NULL, phzk_stat.st_size, PROT_READ, MAP_SHARED, fd_hzk, 0);
 	if(hzk16mem == (unsigned char*)-1)
 	{
  		printf("can't map hzk16mem\n");
  		return -1;
 	}
 	
 	memset(fbmem, 0, screen_size); // clear screen 
 	lcd_put_ascii(var.xres/2, var.yres/2, 'A');
 	printf("chinese code: %02x %02x\n", str[0], str[1]);
 	lcd_put_chinese(var.xres/2 + 8, var.yres/2, str);
 	
 	// remember to release
 	munmap(hzk16mem, phzk_stat.st_size);
	munmap(fbmem, screen_size);
 	return 0; 
}

void lcd_put_pixel(unsigned int x, unsigned y, unsigned int color)
{
	unsigned char* pen_8 = fbmem + y*line_width + x*pixel_width;
 	unsigned short* pen_16 = NULL;
	unsigned int* pen_32 = NULL;
 	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:
  		{
   			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);
   			return ;
  		}
 	}
}
void lcd_put_ascii(unsigned int x, unsigned int y, unsigned char c)
{
 	const unsigned char *dots = &fontdata_8x16[c*16];
 	unsigned char byte;
 	int i, j;
 	for(i = 0; i < 16; ++i)
 	{
  		byte = dots[i];
  		for(j = 7; j >=0; --j)
  		{
   			if(byte & (1<<j))
   			{
    				lcd_put_pixel(x + 7 - j, y + i, 0xffffff);
   			}
  		}
 	}
}
void lcd_put_chinese(unsigned int x, unsigned int y, unsigned char*str)
{
	unsigned int area = str[0] - 0xA1;
 	unsigned int  where = str[1] - 0xA1;
 	unsigned char *offset = hzk16mem + (area*94 + where)*32;
 	unsigned char byte;
 	int i, j, b;
 	for(i = 0; i < 16; ++i)
 	{
  		for(j = 0; j < 2; ++j)
  		{
  			byte = offset[i*2 + j];
   			for(b = 7; b >= 0; --b)
   			{
    				if(byte & (1<<b))
    				{
     					lcd_put_pixel(x+j*8+7- b, y + i, 0xffffff);
    				}
   			}
  		}
 	}
}

上面文件解释一下:
1.随便在内存某个位置映射一块screen_size大小的内存当作framebuffer显存使用
2.映射HZK16文件到内存某一位置,是方便随机读取某一个字,提高读取效率。
3.注意,映射的内存最后记得释放呀

2.3 编译、测试与bug

编译:arm-linux-gcc -o show_font font_8x16.c show_font.c
注意:LCD驱动文件替换为我的part19文章写的LCD驱动文件,修改drivers/video/Makefile,如下
[数码相框]项目储备一: 字符编码与LCD显示单个字母/汉字
注意:先把 lcd_drv.c复制到drivers/vides/目录下
然后再重新编译make uImage ,并把uImage烧写到0x3000,0000,重启后加载驱动(参考part19)

一个bug:在LCD屏幕中间显示"A涓"
为啥“中”字变为“涓”,看下图
[数码相框]项目储备一: 字符编码与LCD显示单个字母/汉字
发现字符编码都不对,肯定是源文件编码有问题。
因为我在notpad++编写,发现源文件编码是UTF-8,而“中字”的编码是“E4 B8 AD”,我们使用HZK16
是16位的,也就是两个字节。而UTF-8是变长编码(1-4bytes),表示为三个字节,肯定不行。
顺便讲下UTF-8的编码,解释如下
E4 B8 AD展开来1110 0100 1011 1000 1010 1101, 如下图
[数码相框]项目储备一: 字符编码与LCD显示单个字母/汉字

解决办法:
在notepad++ 修改字符编码为"ANSI",这样"中"就用两个字节存储。
(ANSI:如果在简体中文windows下,表示“GBK”编码,因此“中”就会用两字节存储)

修改后,正常显示,如下图
[数码相框]项目储备一: 字符编码与LCD显示单个字母/汉字
其它字也可以,只要不超过HZK16范围
[数码相框]项目储备一: 字符编码与LCD显示单个字母/汉字

3 总结与下文预告

LCD显示单个字母/汉字的编程分两步
	1)获得字母(字体文件)/汉字(HZK16文件)
	2)在LCD逐个像素显示
	难点在于确定开始描绘的起点,且字母和汉字点阵大小不同。逐个像素显示时需计算像素的位置。

这里只是显示单个字母/汉字,下文讲讲解如何显示多行文字和图片
相关标签: ARM裸机基础编程