嵌入式linux/鸿蒙开发板(IMX6ULL)开发(十四)文字显示(上)

简介: 嵌入式linux/鸿蒙开发板(IMX6ULL)开发(十四)文字显示

一.文字显示


1.1 字符的编码方式


1.1.1 编码和字体


在计算机上,我们看到的字符“A”可能长这样:

1670898004445.jpg

也可能长这样:

1670898013218.jpg

对于同一个TXT文件中的内容,你在Notepad上选择不同字体时,字符显示的形状不一样。所以TXT文件中保存的是字符的核心:它的编码值。而Notepad上显示时,这些字符对应什么样的形状态,这是由字符文件决定的。编码值,字体是两个不一样的东西,比如A的编码值是0x41,但是在屏幕上显示出来时可以使用不同的形状。


什么叫编码?就是一个字符用什么数字来表示。在计算机里一切都是用数字来表示,比如字符A,用0x01还是0x02来表示它?我们使用0x41来表示它。当你去打开一个TXT文件时,发现里面含有数值0x41,你就知道了:哦,这里有一个字符A。


一个字符用哪个数字来表示?有很多标准,举例讲解。


a. ASCII


是“American Standard Code for Information Interchange”的缩写,美国信息交换标准代码。

电脑毕竟是西方人发明的,他们常用字母就26个,区分大小写、加上标点符号也没超过127个,每个字符用一个字节来表示就足够了。一个字节的7位就可以表示128个数值,在ASCII码中最高位永远是0。

字符和数值的对应关系可以参考:ASCII

下面摘录部分给大家一个印象:

1670898040416.jpg

但128个字符表示汉字就明显不够用了。


b. ANSI


强烈建议阅读:ANSI

使用记事本保存文件时,可以选择“ANSI”编码,却没有“ASCII”,各下图所示。怎么回事?

1670898060287.jpg

ASNI是ASCII的扩展,向下包含ASCII。对于ASCII字符仍以一个字节来表示,对于非ASCII字符则使用2字节来表示。并没有固定的ASNI编码,它跟“本地化”(locale)密切相关。比如在中国大陆地区,ANSI的默认编码是GB2312;在港澳台地区默认编码是BIG5。以数值“0xd0d6”为例,对于GB2312编码它表示“中”;对于BIG5编码它表示“笢”。所以对于ANSI编码的TXT文件,如果你打开它发现乱码,那么还得再次细分它的具体编码。

比如对于一个TXT文件,里面的数值如下:

1670898070375.jpg

使用Notepad打开后,选择不同的编码(或称为字符集),有不一样的显示,如下:

1670898084646.jpg

这仅仅是在中国地区就出现这些不兼容的问题。对于不同国家,它们默认的ANSI编码各不相同,所以同一个TXT文件在不同国家就很有可能出现乱码。

根本的原理在于没有“统一的编码”,那解决方法自然就是使用“统一的编码”:UNICODE


c. UNICODE


在ANSI标准中,很多种文字都有自己的编码标准,汉字简体字有GB2312、繁体字有BIG5,这难免同一个数值对应不同字符。比如数值“0xd0d6”,对于GB2312编码它表示“中”;对于BIG5编码它表示“笢”。这造成了使用ANSI编码保存的文件,不适合跨地区交流。

UNICODE编码就是解决这类问题:对于地球上任意一个字符,都给它一个唯一的数值。

UNICODE仍然向下兼容ASCII,但是对于其他字符会有对应的数值,比如对于“中”、“笢”,它们的数值分别是:0x4e2d、0x7b22

UNICODE中的数值范围是0x0000至0x10FFFF,有1,114,111即100多万个数值,可以表示100多万个字符,足够地球人使用了。


1.1.2 UNICODE编码实现


所谓编码实现,就是对于一个数值,怎么表示它。这很奇怪,数值还能怎么表示?比如“中”的UNICODE值是0x4e2d,在TXT文件中怎么表示0x4e2d?直接写入0x4e2d?不行!

比如在TXT文件中写入2字节数据“0x2d 0x4e”,它可以用来表示“中”字吗?不能!它们对应ASCII字符“-N”。

问题的关键在于:怎么断字。在TXT文件中,2字节数据“0x2d 0x4e”是作为一个整体看待,还是拆成2部分看待?


所以,需要用一定的技巧来表示数值,这就对应不同的编码实现。


现在我们知道:


ASCII编码中使用一个字节来表示一个字符,只用到其中的7位,最高位恒为0;

ANSI编码中,对于ASCII字符仍使用一个字节来表示(BIT7是0),对于非ASCII字符一般使用2个字节来表示,非ASCII字符的数值BIT7都是1。

UNICODE:这就有点复杂了,下面一一讲解。

先用记事本新建3个文件:utf-16_le.txt、utf-16_be.txt、utf-8.txt、bom_utf-8.txt,里面的内容都是“ab中”,保存时编码分别选择“UTF-16 LE”、“UTF-16 BE”、“UTF-8”、“带有BOM的UTF-8”,下图是其中一个例子:

1670898128996.jpg

怎么表示一个UNICODE数值?


使用3个字节表示一个UNICODE

不,太浪费。

UNICODE的最大值是0x10FFFF,那使用3个字节来表示一个UNICODE数值?这当然是很省事的方法,但是会造成浪费,比如字符A的UNICOCDE值是0x41,难道也用“0x41 0x00 0x00”这3个字节来表示?

UCS-2 Little endian/UTF-16 LE

每个UNICODE值用3字节来表示有点浪费,那只用2字节呢?它可以表示2^16=65536个字符,全世界常用的字符都可以表示了。

Little endian表示小字节序,数值中权重低的字节放在前面,比如字符“A中”在TXT文件中的数值如下,其中的“A”使用“0x41 0x00”两字节表示;“中”使用“0x2d 0x4e”两字节表示。文件开头的“0xff 0xfe”表示“UTF-16 LE”。

1670898144766.jpg

UCS-2 Big endian/UTF-16 BE

Big endian表示大字节序,数值中权重低的字节放在后面,比如字符“ab中”在TXT文件中的数值如下,其中的“A”使用“0x00 0x41”两字节表示;“中”使用“0x4e 0x2d”两字节表示。文件开头的“0xfe 0xff”表示“UTF-16 BE”。

1670898157398.jpg

UTF8

UTF-8、UTF-16、UTF-32 都是 Unicode 的一种实现。

在上面2种方法中,每一个UNICODE使用2字节来表示,这有3个缺点:表示的字符数量有限、对于ASCII字符有空间浪费、如果文件中有某个字节丢失,这会使得后面所有字符都因为错位而无法显示。

使用UTF8可以解决上述所有问题。UTF8是变长的编码方法,有2种UTF8格式的文件:带有头部、不带头部。先举例,看下图:

1670898166501.jpg

对于其中的ASCII字符,在UTF8文件中直接用其ASCII码来表示,比如上图中的0x61表示字符a、0x62表示字符b。上图中的3个字节“0xe4 0xb8 0xad”表示的数值是0x4e2d,对应“中”的UNICODE码。

对于非ASCII字符,使用变长的编码:每一个字节的高位都自带长度信息。请看下图:

1670898184238.jpg

上图中,0xe4的二进制是“11100100”,高位有3个1,表示从当前字节起有3字节参与表示UNICODE;

0xb8的二进制是“10111000”,高位有1个1,表示从当前字节起有1字节参与表示UNICODE;

0xad的二进制是“10101101”,高位有1个1,表示从当前字节起有1字节参与表示UNICODE;

除去高位的“1110”、“10”、“10”后,剩下的二进制数组合起来得到“01001110001101”,它就是0x4e2d,即“中”的UNICODE值。

使用UTF8编码时,即使TXT文件中丢失了某些数据,也只会影响到当前字符的显示,后面的字符不受影响。


1.2 ASCII字符点阵显示


要在LCD中显示一个ASCII字符,即英文字母这些字符,首先是要找到字符对应的点阵。在Linux内核源码中有这个文件:lib\fonts\font_8x16.c,里面以数组形式保存各个字符的点阵,比如:

1670898250065.jpg

数组里的数字是如何表示点阵的?以字符A为例,如下图所示:

1670898310370.jpg

上图左侧有16行数值,每行1个字节。每一个节对应右侧一行中8个像素:像素从右边数起,bit0对应第0个像素,bit1对应第1个像素,……,bit7对应第7个像素。某位的值为1时,表示对应的像素要被点亮;值为0时表示对应的像素要熄灭。

以要显示某个字符时,根据它的ASCII码在fontdata_8x16数组中找到它的点阵,然后取出这16个字节去描画16行像素。

比如字符A的ASCII值是0x41,那么从fontdata_8x16[0x41*16]开始取其点阵数据。


使用GIT下载所有源码后,本节源码位于如下目录:

01_all_series_quickstart\
04_嵌入式Linux应用开发基础知识\source\08_show_ascii\  c

核心函数是void lcd_put_ascii(int x, int y, unsigned char c),它在LCD的(x,y)位置处显示字符c,代码如下图所示:

1670898324170.jpg


1.2.1 获取点阵


对于字符c,char c,它的点阵获取方法如下:

4693  unsigned char *dots = (unsigned char *)&fontdata_8x16[c*16];

1.2.2 描点


根据上图,我们分析下如何利用点阵在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 }


1.2.3 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


1.2.4 编译c文件show_ascii.c


编译命令:

arm-linux-gnueabihf-gcc -o show_ascii show_ascii.c

注意:不同的板子,编译工具的前缀可能不一样。


1.2.5 上机实验


把show_ascii程序放到板子上,执行命令:

./show_ascii

如果实验成功,我们将看到屏幕中间会显示出一个白色的字母‘A’。


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; 
}


显示结果如下:

1670898478110.jpg


1.3 中文字符的点阵显示


使用GIT下载所有源码后,本节源码位于如下目录:

01_all_series_quickstart\
04_嵌入式Linux应用开发基础知识\source\09_show_chinese\
test_charset_ansi.c
test_charset_utf8.c
show_chinese.c


1.3.1 指定编码格式


使用点阵字库时,中文字符的显示原理跟ASCII字符是一样的。要注意的地方在于中文的编码:在C源文件中它的编码方式是GB2312还是UTF-8?编译出的可执行程序,其中的汉字编码方式是GB2312还是UTF-8?

注意:一般不会使用UTF-16的编码方式,在这种方式下ASCII字符也是用2字节来表示,而其中一个字节是0,但是在C语言中0表示字符串的结束符,会引起误会。

我们编写C程序时,可以使用ANSI编码,或是UTF-8编码;在编译程序时,可以使用以下的选项告诉编译器:

-finput-charset=GB2312
-finput-charset=UTF-8


如果不指定“-finput-charset”,GCC就会默认C程序的编码方式为UTF-8,即使你是以ANSI格式保存,也会被当作UTF-8来对待。

对于编译出来的可执行程序,可以指定它里面的字符是以什么方式编码,可以使用以下的选项编译器:

-fexec-charset=GB2312
-fexec-charset=UTF-8


如果不指定“-fexec-charset”,GCC就会默认编译出的可执行程序中字符的编码方式为UTF-8。

如果“-finput-charset”与“-fexec-charset”不一样,编译器会进行格式转换。

1670898523014.jpg


1.3.2 编码格式实验


下面做实验。

test_charset_ansi.c、 test_charset_utf8.c的编码格式分别为ANSI、UTF-8,它们的程序代码是一样的,如下:

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 }


默认编码

实验如下:

book@100ask:~/09_show_chinese$ gcc -o test_charset_ansi test_charset_ansi.c
book@100ask:~/09_show_chinese$ ./test_charset_ansi
str's len = 3
Hex code: 41 d6 d0
book@100ask:~/09_show_chinese$
book@100ask:~/09_show_chinese$ gcc -o test_charset_utf8 test_charset_utf8.c
book@100ask:~/09_show_chinese$ ./test_charset_utf8
str's len = 4
Hex code: 41 e4 b8 ad


不指定“-finput-charset”与“-fexec-charset”时,input-charset和exec-charset默认都是UTF-8,不会进行编码转换。即使C文件是ANSI,也会被认为是UTF-8,所以不会导致编码转换。


GB2312转为UTF-8

实验如下:

book@100ask:~/09_show_chinese$ gcc -finput-charset=GB2312 -fexec-charset=UTF-8 -o test_charset_ansi test_charset_ansi.c
book@100ask:~/09_show_chinese$ ./test_charset_ansi
str's len = 4
Hex code: 41 e4 b8 ad
book@100ask:~/09_show_chinese$
book@100ask:~/09_show_chinese$ gcc -finput-charset=GB2312 -fexec-charset=UTF-8 -o test_charset_utf8 test_charset_utf8.c
cc1: error: failure to convert GB2312 to UTF-8


从上面的输出信息可以看出来,GB2312的“0xd6 0xd0”可以转换为UTF-8的“0xe4 0xb8 0xad”。

而如果把原本就是UTF-8格式的test_charset_utf8.c当作GB2312格式,会引起错误。

3.UTF-8转为GB2312

实验如下:

book@100ask:~/09_show_chinese$ gcc -finput-charset=UTF-8 -fexec-charset=GB2312 -o test_charset_ansi test_charset_ansi.c
test_charset_ansi.c: In function ‘main’:
test_charset_ansi.c:6:14: error: converting to execution character set: Invalid or incomplete multibyte or wide character
  char *str = "A▒▒";
              ^~~~~
book@100ask:~/09_show_chinese$
book@100ask:~/09_show_chinese$ gcc -finput-charset=UTF-8 -fexec-charset=GB2312 -o test_charset_utf8 test_charset_utf8.c
book@100ask:~/09_show_chinese$ ./test_charset_utf8
str's len = 3
Hex code: 41 d6 d0


从上面的输出信息可以看出来,如果把原本就是GB2312格式的test_charset_ansi.c当作UTF-8格式,会引起错误。

而UTF-8格式的“中”编码值为“0xe4 0xb8 0xad”,可以转换为GB2312的“0xd6 0xd0”。


在代码中使用汉字这类非ASCII码时,要特别留意编码格式。

相关文章
|
8天前
|
存储 JavaScript 关系型数据库
鸿蒙开发:实现全局异常捕获和异常查看
如何灵活的拿到错误信息后,执行我们想要的逻辑,也是自研的一个诉求,比如全局监听到异常后,重启应用,或者上传到自己的服务器,或者可以在应用内查看等等,实现一个全局异常捕获,确实有很多的有用之处。
鸿蒙开发:实现全局异常捕获和异常查看
|
8天前
|
前端开发 API
鸿蒙开发:走进stateStyles多态样式
stateStyles为多态样式,可以依据组件的内部状态的不同,快速设置不同样式,比如背景颜色,颜色、大小等等常见的通用属性,此种行为,很类似于css中的伪类,但语法稍有不同
鸿蒙开发:走进stateStyles多态样式
|
4天前
|
安全 测试技术 数据安全/隐私保护
猫头虎分享:鸿蒙生态带给开发者的全新机遇!轻松实现按需加载与多端适配,开发效率翻倍
猫头虎分享:鸿蒙生态带来的全新机遇!华为在原生鸿蒙之夜发布会上,推出了全新的鸿蒙系统和焕新升级的应用市场。此次升级在用户体验和隐私保护方面实现了重大突破,提供了自动化检测前移、按需加载和多端适配等服务,帮助开发者提高开发效率和应用质量。
45 6
|
7天前
|
开发框架 JavaScript 前端开发
HarmonyOS UI开发:掌握ArkUI(包括Java UI和JS UI)进行界面开发
【10月更文挑战第22天】随着科技发展,操作系统呈现多元化趋势。华为推出的HarmonyOS以其全场景、多设备特性备受关注。本文介绍HarmonyOS的UI开发框架ArkUI,探讨Java UI和JS UI两种开发方式。Java UI适合复杂界面开发,性能较高;JS UI适合快速开发简单界面,跨平台性好。掌握ArkUI可高效打造符合用户需求的界面。
44 8
|
7天前
|
安全 测试技术 数据安全/隐私保护
|
2天前
|
传感器 开发框架 物联网
鸿蒙next选择 Flutter 开发跨平台应用的原因
鸿蒙(HarmonyOS)是华为推出的一款旨在实现多设备无缝连接的操作系统。为了实现这一目标,鸿蒙选择了 Flutter 作为主要的跨平台应用开发框架。Flutter 的跨平台能力、高性能、丰富的生态支持和与鸿蒙系统的良好兼容性,使其成为理想的选择。通过 Flutter,开发者可以高效地构建和部署多平台应用,推动鸿蒙生态的快速发展。
70 0
|
4天前
|
Dart 安全 UED
Flutter&鸿蒙next中的表单封装:提升开发效率与用户体验
在移动应用开发中,表单是用户与应用交互的重要界面。本文介绍了如何在Flutter中封装表单,以提升开发效率和用户体验。通过代码复用、集中管理和一致性的优势,封装表单组件可以简化开发流程。文章详细讲解了Flutter表单的基础、封装方法和表单验证技巧,帮助开发者构建健壮且用户友好的应用。
55 0
|
23天前
|
开发框架 JavaScript 前端开发
鸿蒙NEXT开发声明式UI是咋回事?
【10月更文挑战第15天】鸿蒙NEXT的声明式UI基于ArkTS,提供高效简洁的开发体验。ArkTS扩展了TypeScript,支持声明式UI描述、自定义组件及状态管理。ArkUI框架则提供了丰富的组件、布局计算和动画能力。开发者仅需关注数据变化,UI将自动更新,简化了开发流程。此外,其前后端分层设计与编译时优化确保了高性能运行,利于生态发展。通过组件创建、状态管理和渲染控制等方式,开发者能快速构建高质量的鸿蒙应用。
|
28天前
|
Android开发 iOS开发 容器
鸿蒙harmonyos next flutter混合开发之开发FFI plugin
鸿蒙harmonyos next flutter混合开发之开发FFI plugin
|
10天前
|
JavaScript API 开发者
掌握ArkTS,打造HarmonyOS应用新视界:从“Hello World”到状态管理,揭秘鸿蒙UI开发的高效秘诀
【10月更文挑战第19天】ArkTS(ArkUI TypeScript)是华为鸿蒙系统中用于开发用户界面的声明式编程语言,结合了TypeScript和HarmonyOS的UI框架。本文介绍ArkTS的基本语法,包括组件结构、模板和脚本部分,并通过“Hello World”和计数器示例展示其使用方法。
24 1