基于IMX6ULL开发板LCD点阵显示字符学习
目录
- 1. 点阵显示字符介绍
- 1.1 点阵显示字符的概念
- 1.2 点阵显示字符的原理
- 1.3 点阵显示的一般应用
- 2. LCD屏幕使用点阵显示英文字符
- 2.1 字库文件
- 2.2 源码
- 2.3 源码分析
- 3. LCD屏幕使用点阵显示中文字符
- 3.1 编码格式测试
- 3.2 汉字区位码
- 3.3 源码
- 3.4 源码分析
基于韦东山IMX6ULL开发板学习
参考教程:韦东山老师教程
ANSI字符编码文章参考:ANSI是什么编码?
1. 点阵显示字符介绍
1.1 点阵显示字符的概念
点阵显示字符,也称为点阵字体或位图字体,是一种将字符以点阵形式进行表示的方法。在这种表示方法中,每个字符都被划分为一个固定大小的点阵,点阵中的每个点(像素)可以独立控制其是否显示,从而构成字符的轮廓和形状。常见的点阵字体大小有8x8、8x16、16x16、24x24等,这些数字分别表示字符在水平和垂直方向上所占的像素点数。
1.2 点阵显示字符的原理
点阵显示字符的原理可以概括为以下几个步骤:
字符编码与字模:
- 每个字符都有一个唯一的编码,如ASCII码或Unicode码。
- 为了在LCD上显示字符,需要将这些编码转换为对应的字模(也称为点阵数据)。字模是一组二进制数据,用于描述字符在点阵中的显示状态(即哪些像素点需要显示)。
像素控制:
- LCD屏幕由大量的像素点组成,每个像素点都可以独立控制其是否发光或显示颜色。
- 通过控制LCD屏幕上特定区域的像素点,可以形成所需的字符形状。
字符显示:
- 当需要在LCD上显示某个字符时,首先根据字符的编码找到对应的字模。
- 然后,按照字模中的二进制数据,逐位控制LCD屏幕上对应区域的像素点是否显示。
- 如果字模中的某位为1,则对应的像素点显示;如果为0,则不显示。
字符定位:
- 为了在LCD屏幕上正确显示字符,还需要确定字符的显示位置(即字符的起始坐标)。
- 通过设置字符的起始坐标,可以控制字符在LCD屏幕上的具体位置。
颜色与亮度:
- 在彩色LCD屏幕上,除了控制像素点的显示与否外,还可以控制像素点的颜色和亮度。
- 这使得字符的显示更加丰富多彩,可以根据需要设置字符的前景色、背景色以及亮度等属性。
1.3 点阵显示的一般应用
- 电子显示屏:如广告牌、电子看板等,通过点阵显示字符来展示文字、数字等信息。
- 嵌入式系统:如智能手表、智能家居设备等,这些设备通常具有较小的LCD屏幕,通过点阵显示字符来显示时间、日期、状态等信息。
- 计算机终端:早期的计算机终端和某些特定的计算机设备也采用点阵显示字符的方式来显示文本信息。
2. LCD屏幕使用点阵显示英文字符
基于IMX6ULL的LCD屏幕显示字符‘A’。
2.1 字库文件
要在LCD中显示一个ASCII字符,即英文字母这些字符,首先是要找到字符对应的点阵。在Linux内核源码中有这个文件:lib\fonts\font_8x16.c
,里面以数组形式保存各个字符的点阵,比如:
数组里的数字是如何表示点阵的?以字符 A 为例:
上图左侧有16行数值,每行1个字节。每一个节对应右侧一行中8个像素:像素从右边数起,bit0对应第0个像素,bit1对应第1个像素,……,bit7对应第7个像素。某位的值为1时,表示对应的像素要被点亮;值为0时表示对应的像素要熄灭。
所以要显示某个字符时,根据它的ASCII码在fontdata_8x16数组中找到它的点阵,然后取出这16个字节去描画16行像素。
比如字符A的ASCII值是0x41,那么从fontdata_8x16[0x41*16]开始取其点阵数据。
2.2 源码
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <linux/fb.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>#define FONTDATAMAX 4096static const unsigned char fontdata_8x16[FONTDATAMAX] = {…省略,就是font_8x16.c文件中的fontdata_8x16…};int fd_fb;
struct fb_var_screeninfo var; /* Current var */
int screen_size;
unsigned char *fbmem;
unsigned int line_width;
unsigned int pixel_width;
/*********************************************************************** 函数名称: lcd_put_pixel* 功能描述: 在LCD指定位置上输出指定颜色(描点)* 输入参数: x坐标,y坐标,颜色* 输出参数: 无* 返 回 值: 会* 修改日期 版本号 修改人 修改内容* -----------------------------------------------* 2020/05/12 V1.0 zh(angenao) 创建***********************************************************************/
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;}}
}
/*********************************************************************** 函数名称: lcd_put_ascii* 功能描述: 在LCD指定位置上显示一个8*16的字符* 输入参数: x坐标,y坐标,ascii码* 输出参数: 无* 返 回 值: 无* 修改日期 版本号 修改人 修改内容* -----------------------------------------------* 2020/05/12 V1.0 zh(angenao) 创建***********************************************************************/
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)){/* show */lcd_put_pixel(x+7-b, y+i, 0xffffff); /* 白 */}else{/* hide */lcd_put_pixel(x+7-b, y+i, 0); /* 黑 */}}}
}int main(int argc, char **argv)
{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;}line_width = var.xres * var.bits_per_pixel / 8;pixel_width = var.bits_per_pixel / 8;screen_size = var.xres * var.yres * var.bits_per_pixel / 8;fbmem = (unsigned char *)mmap(NULL , screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd_fb, 0);if (fbmem == (unsigned char *)-1){printf("can't mmap\n");return -1;}/* 清屏: 全部设为黑色 */memset(fbmem, 0, screen_size);lcd_put_ascii(var.xres/2, var.yres/2, 'A'); /*在屏幕中间显示8*16的字母A*/munmap(fbmem , screen_size);close(fd_fb);return 0;
}
2.3 源码分析
核心函数是void lcd_put_ascii(int x,int y, unsignedchar c),它在LCD的(x,y)位置处显示字符c。
获取点阵:
对于字符c,char c,它的点阵获取方法如下:
4693 unsigned char *dots = (unsigned char *)&fontdata_8x16[c*16];
描点:
分析如何利用点阵在LCD上显示一个英文字母。
因为有十六行,所以首先要有一个循环16次的大循环,然后每一行里有8位,那么在每一个大循环里也需要一个循环8次的小循环。小循环里的判断单行的描点情况,如果是1,就填充白色,如果是0就填充黑色,如此一来,就可以显示出黑色底,白色轮廓的英文字母。
4697 for (i = 0; i < 16; i++)
4698 {
4699 byte = dots[i];
4700 for (b = 7; b >= 0; b--)
4701 {
4702 if (byte & (1<<b))
4703 {
4704 /* show */
4705 lcd_put_pixel(x+7-b, y+i, 0xffffff); /* 白 */
4706 }
4707 else
4708 {
4709 /* hide */
4710 lcd_put_pixel(x+7-b, y+i, 0); /* 黑 */
4711 }
4712 }
4713 }
main函数:
main函数中首先要打开LCD设备,获取Framebuffer参数,实现lcd_put_pixel函数;然后调用lcd_put_ascii即可绘制字符。
4716 int main(int argc, char **argv)
4717 {
4718 fd_fb = open("/dev/fb0", O_RDWR);
4719 if (fd_fb < 0)
4720 {
4721 printf("can't open /dev/fb0\n");
4722 return -1;
4723 }
4724 if (ioctl(fd_fb, FBIOGET_VSCREENINFO, &var))
4725 {
4726 printf("can't get var\n");
4727 return -1;
4728 }
4729
4730 line_width = var.xres * var.bits_per_pixel / 8;
4731 pixel_width = var.bits_per_pixel / 8;
4732 screen_size = var.xres * var.yres * var.bits_per_pixel / 8;
4733 fbmem = (unsigned char *)mmap(NULL , screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd_fb, 0);
4734 if (fbmem == (unsigned char *)-1)
4735 {
4736 printf("can't mmap\n");
4737 return -1;
4738 }
4739
4740 /* 清屏: 全部设为黑色 */
4741 memset(fbmem, 0, screen_size);
4742
4743 lcd_put_ascii(var.xres/2, var.yres/2, 'A'); /*在屏幕中间显示 8*16 的字母 A*/
4744
4745 munmap(fbmem , screen_size);
4746 close(fd_fb);
4747
4748 return 0;
4749 }
4750
使用工具链交叉编译后生成可执行程序拷贝到IMX6ULL开发板后运行效果:
3. LCD屏幕使用点阵显示中文字符
使用点阵字库时,中文字符的显示原理跟英文字符(ASCII字符)是一样的。要注意的地方在于中文的编码:在C源文件中它的编码方式是GB2312还是UTF-8?编译出的可执行程序,其中的汉字编码方式是GB2312还是UTF-8?
**注意:**一般不会使用UTF-16的编码方式,在这种方式下ASCII字符也是用2字节来表示,而其中一个字节是0,但是在C语言中0表示字符串的结束符,会引起误会。
可以使用以下的选项告诉编译器编译代码时,输入的代码是ANSI编码还是UTF-8编码:
-finput-charset=GB2312
-finput-charset=UTF-8
如果不指定“-finput-charset”,GCC就会默认C程序的编码方式为UTF-8,即使你是以ANSI格式保存,也会被当作UTF-8来对待。
可以使用以下的选项告诉编译器编译代码时,输出的程序是ANSI编码还是UTF-8编码:
-fexec-charset=GB2312
-fexec-charset=UTF-8
如果不指定“-fexec-charset”,GCC就会默认编译出的可执行程序中字符的编码方式为UTF-8。
如果“-finput-charset”与“-fexec-charset”不一样,编译器会进行格式转换。
3.1 编码格式测试
比如以下源码分别以ANSI和UTF-8格式保存为test_charset_ansi.c、test_charset_utf8.c:
01 #include <stdio.h>
02 #include <string.h>
03
04 int main(int argc, char **argv)
05 {
06 char *str = "A 中";
07 int i;
08
09 printf("str's len = %d\n", (int)strlen(str));
10 printf("Hex code: ");
11 for (i = 0; i < strlen(str); i++)
12 {
13 printf("%02x ", (unsigned char)str[i]);
14 }
15 printf("\n");
16 return 0;
17 }
上传到Ubuntu编译并运行:
-
默认编码时:
-
不指定“-finput-charset”与“-fexec-charset”时,input-charset和exec-charset默认都是UTF-8,不会进行编码转换。即使C文件是ANSI,也会被认为是UTF-8,所以不会导致编码转换。
-
GB2312转为UTF-8时:
-
从上面的输出信息可以看出来,GB2312的“0xd6 0xd0”可以转换为UTF-8的“0xe4 0xb8 0xad”。而如果把原本就是UTF-8格式的test_charset_utf8.c当作GB2312格式,会引起错误。
-
UTF-8转为GB2312时:
-
从上面的输出信息可以看出来,如果把原本就是GB2312格式的test_charset_ansi.c当作UTF-8格式,会引起错误。而UTF-8格式的“中”编码值为“0xe4 0xb8 0xad”,可以转换为GB2312的“0xd6 0xd0”。
因此,在代码中使用汉字这类非ASCII码时,要特别留意编码格式。
3.2 汉字区位码
从网上搜到HZK16这个文件,它是常用汉字的16*16点阵字库。HZK16里每个汉字使用32字节来描述,如图所示:
跟ASCII字库一样,每个字节中每一位用来表示一个像素,位值等于1时表示对应像素被点亮,位值等于0时表示对应像素被熄灭。
HZK16中是以GB2312编码值来查找点阵的,以“中”字为例,它的编码值是“0xd6 0xd0”,其中的0xd6表示“区码”,表示在哪一个区:第“0xd6-0xa1”区;其中的0xd0表示“位码”,表示它是这个区里的哪一个字符:第“0xd0-0xa1”个。每一个区有94个汉字。区位码从0xa1而不是从0开始,是为了兼容ASCII码。
所以,我们要显示的“中”字,它的GB2312编码是d6 d0,它是HZK16里第“(0xd6-0xa1)*94+(0xd0-0xa1)”个字符。
以汉字「我」为例,在HZK16文件中找到它对应的32个字节的字模数据:
3.3 源码
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <linux/fb.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>#define FONTDATAMAX 4096static const unsigned char fontdata_8x16[FONTDATAMAX] = {…省略,就是font_8x16.c文件中的fontdata_8x16…};int fd_fb;
struct fb_var_screeninfo var; /* Current var */
int screen_size;
unsigned char *fbmem;
unsigned int line_width;
unsigned int pixel_width;int fd_hzk16;
struct stat hzk_stat;
unsigned char *hzkmem;/*********************************************************************** 函数名称: lcd_put_pixel* 功能描述: 在LCD指定位置上输出指定颜色(描点)* 输入参数: x坐标,y坐标,颜色* 输出参数: 无* 返 回 值: 会* 修改日期 版本号 修改人 修改内容* -----------------------------------------------* 2020/05/12 V1.0 zh(angenao) 创建***********************************************************************/
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;}}
}
/*********************************************************************** 函数名称: lcd_put_ascii* 功能描述: 在LCD指定位置上显示一个8*16的字符* 输入参数: x坐标,y坐标,ascii码* 输出参数: 无* 返 回 值: 无* 修改日期 版本号 修改人 修改内容* -----------------------------------------------* 2020/05/12 V1.0 zh(angenao) 创建***********************************************************************/
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)){/* show */lcd_put_pixel(x+7-b, y+i, 0xffffff); /* 白 */}else{/* hide */lcd_put_pixel(x+7-b, y+i, 0); /* 黑 */}}}
}
/*********************************************************************** 函数名称: lcd_put_chinese* 功能描述: 在LCD指定位置上显示一个16*16的汉字* 输入参数: x坐标,y坐标,ascii码* 输出参数: 无* 返 回 值: 无* 修改日期 版本号 修改人 修改内容* -----------------------------------------------* 2020/05/12 V1.0 zh(angenao) 创建***********************************************************************/
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); /* 黑 */} }}
}int main(int argc, char **argv)
{unsigned char str[] = "中";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;}line_width = var.xres * var.bits_per_pixel / 8;pixel_width = var.bits_per_pixel / 8;screen_size = var.xres * var.yres * var.bits_per_pixel / 8;fbmem = (unsigned char *)mmap(NULL , screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd_fb, 0);if (fbmem == (unsigned char *)-1){printf("can't mmap\n");return -1;}fd_hzk16 = open("HZK16", O_RDONLY);if (fd_hzk16 < 0){printf("can't open HZK16\n");return -1;}if(fstat(fd_hzk16, &hzk_stat)){printf("can't get fstat\n");return -1;}hzkmem = (unsigned char *)mmap(NULL , hzk_stat.st_size, PROT_READ, MAP_SHARED, fd_hzk16, 0);if (hzkmem == (unsigned char *)-1){printf("can't mmap for hzk16\n");return -1;}/* 清屏: 全部设为黑色 */memset(fbmem, 0, screen_size);lcd_put_ascii(var.xres/2, var.yres/2, 'A'); /*在屏幕中间显示8*16的字母A*/printf("chinese code: %02x %02x\n", str[0], str[1]);lcd_put_chinese(var.xres/2 + 8, var.yres/2, str);munmap(fbmem , screen_size);close(fd_fb);return 0;
}
3.4 源码分析
打开汉字库文件:
4787 fd_hzk16 = open("HZK16", O_RDONLY);
4788 if (fd_hzk16 < 0)
4789 {
4790 printf("can't open HZK16\n");
4791 return -1;
4792 }
4793 if(fstat(fd_hzk16, &hzk_stat))
4794 {
4795 printf("can't get fstat\n");
4796 return -1;
4797 }
4798 hzkmem = (unsigned char *)mmap(NULL , hzk_stat.st_size, PROT_READ, MAP_SHARED, fd_hzk16, 0);
4799 if (hzkmem == (unsigned char *)-1)
4800 {
4801 printf("can't mmap for hzk16\n");
4802 return -1;
4803 }
第4787行打开当前目录的字库文件:HZK16。
第4793行获得文件的状态信息,里面含有文件长度,这在后面的mmap中用到。
第4798行使用mmap映射文件,以后就可以像访问内存一样读取文件内容;mmap的返回结果保存在hzkmem中,它将作为字库的基地址。
编写显示汉字的函数:
核心函数是void lcd_put_chinese(int x, int y, unsigned char *str),它在 LCD 的(x,y)位置处显示汉字字符str,str[0]中保存区码、str[1]中保存位码。
4732 void lcd_put_chinese(int x, int y, unsigned char *str)
4733 {
4734 unsigned int area = str[0] - 0xA1;
4735 unsigned int where = str[1] - 0xA1;
4736 unsigned char *dots = hzkmem + (area * 94 + where)*32;
4737 unsigned char byte;
4738
4739 int i, j, b;
4740 for (i = 0; i < 16; i++)
4741 for (j = 0; j < 2; j++)
4742 {
4743 byte = dots[i*2 + j];
4744 for (b = 7; b >=0; b--)
4745 {
4746 if (byte & (1<<b))
4747 {
4748 /* show */
4749 lcd_put_pixel(x+j*8+7-b, y+i, 0xffffff); /* 白 */
4750 }
4751 else
4752 {
4753 /* hide */
4754 lcd_put_pixel(x+j*8+7-b, y+i, 0); /* 黑 */
4755 }
4756 }
4757 }
4758 }
第4734行确定该汉字属于哪个区;第4735行确实它是该区中哪一个汉字。
第4736行确实它的字库地址:每个区中有94个汉字,每个汉字在字库中占据32字节。
第4740行开始循环,总共有十六行,因此需要一个循环16次的大循环(第4740行)。
考虑到一行有两个字节,在大循环中加入一个2次的循环用于区分是哪个字节(第4741行)。
最后使用第3个循环来处理一个字节中的8位(第4744行)。对于每一位,它等于1时对应的像素被设置为白色,它等于0时对应的像素被设置为黑色。需要注意的是根据x、y、i、j、b来计算像素坐标。
使用 lcd_put_chinese 函数:
4762 unsigned char str[] = "中";
……
4810 printf("chinese code: %02x %02x\n", str[0], str[1]);
4811 lcd_put_chinese(var.xres/2 + 8, var.yres/2, str);
使用工具链交叉编译后生成可执行程序拷贝到IMX6ULL开发板后运行效果:
代码格式必须是 ANSI(GB2312),否则编译时需要指定“ -fexec-charset=GB2312”。
arm-buildroot-linux-gnueabihf-gcc -fexec-charset=GB2312 -o show_chinese show_chinese.c