✅作者简介:嵌入式入坑者,与大家一起加油,希望文章能够帮助各位!!!!
个人主页:@rivencode的个人主页
系列专栏:玩转STM32
推荐一款模拟面试、刷题神器,从基础到大厂面试题点击跳转刷题网站进行注册学习
字符(Character)
是各种文字和符号的总称,包括各国家文字、标点符号、图形符号、数字等。
字符集(Character set)
是多个字符的集合,字符集种类较多,每个字符集包含的字符个数不同,常见字符集名称:ASCII字符集、GB2312字符集、BIG5字符集、 GB18030字符集、Unicode字符集等。
字符编码
计算机要准确的处理各种字符集文字,就需要进行字符编码,以便计算机能够识别和存储各种文字。而存储在计算机中只能是以二进制的方式进行存储(01),所以我们可以用特定的二进制序列来表示字符(英文、中文、…)
产生原因:在计算机中,所有的数据在存储和运算时都要使用二进制数表示(因为计算机用高电平和低电平分别表示1和0),例如,像a、b、c、d这样的52个字母(包括大写)以及0、1等数字还有一些常用的符号(例如#、@等)在计算机中存储时也要使用二进制数来表示,而具体用哪些二进制数字表示哪个符号,当然每个人都可以约定自己的一套(这就叫编码),而大家如果要想互相通信而不造成混乱,那么大家就必须使用相同的编码规则,于是美国有关的标准化组织就出台了ASCII编码,统一规定了上述常用符号用哪些二进制数来表示。
特点:
0~31及127(共33个)是控制字符或通信专用字符(其余为可显示字符)
如控制符:LF(换行)、CR(回车)、FF(换页)、DEL(删除)、BS(退格)、BEL(响铃)等
32~126(共95个)是字符(32是空格),其中48~57为0到9十个阿拉伯数字。
65~90为26个大写英文字母,97~122号为26个小写英文字母,其余为一些标点符号、运算符号等。
以字符A举例它的ASCII值表示为65(十进制),0x41(十六进制),至于为什么要用65来表示,只是特定的数字而已,所以如果要在电脑上存储一个字符A,就应该存储A的ASCII码值65所代表的二进制:01000001
而当我们要从电脑内存中取出A显示在屏幕中时,当电脑碰到65表示的二进制序列后电脑会自动将它识别成A,然后通过某种映射关系将A显示在屏幕上。
扩展字符集
7位编码的字符集只能支持128个字符,为了表示更多的欧洲常用字符对ASCII进行了扩展,ASCII扩展字符集使用8位(bits)表示一个字符,共256字符,扩充后的符号增加了表格符号、计算符号、希腊字母和特殊的拉丁符号。
亚洲国家的文字,使用的符号就更多了,汉字就多达10万左右。一个字节只能表示256种符号,肯定是不够的,就必须使用多个字节表达一个符号。比如,简体中文常见的编码方式是 GB2312,使用两个字节表示一个汉字,所以理论上最多可以表示 2的16次方= 65536 个符号 。
GB2312编码是第一个汉字编码国家标准,由中国国家标准总局1980年发布,1981年5月1日开始使用。GB2312编码共收录汉字6763个,其中一级汉字3755个,二级汉字3008个。同时,GB2312编码收录了包括拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母在内的682个全角字符。
分区表示
GB2312编码对所收录字符进行了“分区”处理,共94个区,每区含有94个位,共8836个码位。这种表示方式也称为区位码(这个不是真正存储在计算机中的编码)。
**举例来说,“啊”字是GB2312编码中的第一个汉字,它位于16区的01位,所以它的区位码就是1601,其中区位码为 0101 的码位表示的是“空格”符。
**
详情请看:GB2312编码
双字节编码
GB2312规定对收录的每个字符采用两个字节表示,第一个字节为“高字节”,对应94个区;第二个字节为“低字节”,对应94个位。所以它的区位码范围是:0101-9494(十进制)。区号和位号(先分别转化为十六进制)然后分别加上0xA0就是GB2312编码
GB2312编码才是真正存储在计算机中表示字符的,而区位码相当于一个内部编码不要搞混了,可以通过字符的区位码得出GB2312编码。
例如第一个码位是0101,区号和位号分别转换成十六进制是0101,0x01+0xA0=0xA1,所以该码位的GB2312编码是A1A1。
最后一个码位是9494,区号和位号分别转换成十六进制是5E5E,0x5E+0xA0=0xFE,所以该码位的GB2312编码是FEFE。
所以我们得出:GB2312编码范围:A1A1-FEFE
其中汉字编码范围:B0A1-F7FE,其中啊字就是第一个汉字。
接下来就来验证一下:在GB2312编码环境下,中文在计算机内存中存储。
不清楚大小端的请看:C语言深度解剖之数据到底在内存中如何存储
GB2312如何兼容ASCII码
它把 ASCII 码表 127 号之后的扩展字符集直接取消掉,并规定小于 127 的编码按原来 ASCII 标准解释字符。当 2 个大于 127 的字符连在一起时,就表示 1 个汉字。
第 1 个字节使用 (0xA1-0xFE) 编码,第 2 个字节使用(0xA1-0xFE)编码
为兼容 ASCII 码,区号和位号分别加上 0xA0 偏移就得到
GB2312 编码。在区位码上加上 0xA0 偏移,可求得 GB2312 编码范围:0xA1A1-0xFEF
ASCII 里原本就有的数字、标点以及字母也重新编了 2 个字节长的编码,这就是平时在输入法里可切换的“全角”字符,而标准的 ASCII 码表中 127 号以下的就被称为“半角”字符。
当我们设定系统使用 GB2312 标准的时候,它遇到一个字符串时,会按字节检测字符值的大小,若遇到连续两个字节的数值都大于 127 时就把这两个连续的字节合在一起,用 GB2312 解码,若遇到的数值小于 127,就直接用 ASCII 把它解码。
Big5 编码
在台湾、香港等地区,使用较多的是 Big5 编码,它的主要特点是收录了繁体字。而从GBK 编码开始,已经把 Big5 中的所有汉字收录进编码了。即对于汉字部分,GBK 是 Big5的超集,Big5 能表示的汉字,在 GBK 都能找到那些字相应的编码,但他们的编码是不一样的,两个标准不兼容,如 GBK 中的“啊”字编码是“0xB0A1”,而 Big5 标准中的编码为“0xB0DA”
GBK编码,是对GB2312编码的扩展,因此完全兼容GB2312-80标准,GBK编码依然采用双字节编码方案,其编码范围:8140-FEFE,剔除xx7F码位,共23940个码位。共收录汉字和图形符号21886个,其中汉字(包括部首和构件)21003个,图形符号883个。GBK编码支持国际标准ISO/IEC10646-1和国家标准GB13000-1中的全部中日韩汉字,并包含了BIG5编码中的所有汉字(但是与BIG5的编码格式不同)。GBK编码方案于1995年12月15日正式发布,这一版的GBK规范为1.0版。
既然都是采用双字节编码方案,而双字节已经被GB2312编的差不多了,那GBK编码是如何兼容GB2312编码和ASCII编码,然后在此基础上扩展的呢。
前面GB2312为了兼容ASCII编码两个字节的编码都大于127,而GBK规定只要第 1 个字节大于 127 就表示这是一个汉字的开始,这样就做到了兼容 ASCII 和GB2312 标准。
当我们设定系统使用 GBK标准的时候,它按顺序遍历字符串,按字节检测字符值的大小,若遇到一个字符的值大于127 时,就再读取它后面的一个字符,把这两个字符值合在一起,用 GBK 解码,解码完后,再读取第 3 个字符,重新开始以上过程,若该字符值小于 127,则直接用 ASCII 解码。
其实你仔细对比GB2312与GBK的实际编码区域,GBK是利用了GB2312未使用的区域进行双字节编码,加上只有一个字节大于127就能表示一个字符极大的增加了编码字符。
了解即可,知道GBK是怎么兼容GB2312和ASCII编码的就行。
随着计算机技术的普及,我们后来又在 GBK 的标准上不断扩展字符,这些标准被称为 GB18030,如 GB18030-2000、GB18030-2005 等(“-”号后面的数字是制定标准时的年号),GB18030 的编码使用 4 个字节,它利用前面标准中的第 2 个字节未使用的“0x30-0x39”编码表示扩充四字节的后缀,兼容 GBK、GB2312 及 ASCII 标准
。
GB18030-2000 主要在 GBK 基础上增加了“CJK(中日韩)统一汉字扩充 A”的汉字。加上前面 GBK 的内容,GB18030-2000 一共规定了 27533 个汉字(包括部首、部件等)的编码,还有一些常用非汉字符号。
GB18030-2005 的主要特点是在 GB18030-2000 基础上增加了“CJK(中日韩)统一汉字扩充 B”的汉字。增加了 42711 个汉字和多种我国少数民族文字的编码(如藏、蒙古、傣、彝、朝鲜、维吾尔文等)。加上前面 GB18030-2000 的内容,一共收录了 70244 个汉字。
GB18030码位分配
GB18030编码采用单字节、双字节和四字节三种方式对字符编码。
单字节部分采用GB/T 11383的编码结构与规则,使用0x00至0x7F码位(对应ASCII码位)。
双字节部分,首字节码位从0x81至0xFE,尾字节码位分别是0x40至0x7E和0x80至0xFE。
四字节部分采用GB/T 11383未采用的0x30到0x39作为对双字节编码扩充的后缀,这样扩充的四字节编码,其范围为0x81308130到0xFE39FE39。其中第一、三个字节编码码位均为0x81至0xFE,第二、四个字节编码码位均为0x30至0x39。
以上基本是查阅资料再加上我自己的理解,主要理解GB2321标准,区位码与GB2312编码的转化关系。
为什么要有字模
但是如果仅有字符编码,计算机还不知道该如何表达该字符,因为字符实际上是一个个独特的图形,计算机必须把字符编码转化成对应的字符图形人类才能正常识别,因此我们要给计算机提供字符的图形数据,这些数据就是字模,多个字模数据组成的文件也被称为字库,计算机显示字符时,根据字符编码与字模数据的映射关系找到它相应的字模数据,液晶屏根据字模数据显示该字符。
通俗的说,字符有了编码就可以存储在计算机中,而当计算机遇到特定编码时就知道它是一个什么字符,比如说A字符它的ASCII编码为65,而存储在计算机中的是65二进制序列,虽然计算机知道这一串二进制序列表示A字符,但是我们一般取这个A字符时一般是让它显示在屏幕上,而不是给我们A字符的编码,计算机需要字模数据将字符显示出来。
已知字模是图形数据,而图形在计算机中是由一个个像素点组成的,所以字模实质是一个个像素点数据。为方便处理,我们把字模定义成方块形的像素点阵,且每个像素点只有 0 和 1 这两种状态(可以理解为单色图像数据)。
如何制作字模数据
当然不是让我们一个一个字符去画然后用数据表示出来,尤其批量做字模GB2312包含7000多个汉字,那我们就得做7000个字模,这时候就要用到做字模的软件了。
参考一下野火的:
如果使用 LCD 的画点函数,按位来扫描这些字模数据,把为 1 的位以黑色来显示(也可以使用其它颜色),为 0 的数据位以白色来显示,即可把整个点阵还原出来,显示在液晶屏上。
这里用VS2013打印来模拟液晶屏,其实原理是一样的,就是通过扫描字模数据,为1的显示 * ,为0的显示空格。
unsigned char Display_char[] = { 0x02, 0x00, 0x01, 0x00, 0x7F, 0xFE, 0x40, 0x02, 0x80, 0x04, 0x1F, 0xE0, 0x00, 0x40, 0x00, 0x80, 0x01, 0x00, 0xFF, 0xFE, 0x01, 0x00, 0x01, 0x00 ,
0x01, 0x00, 0x01, 0x00, 0x05, 0x00, 0x02, 0x00 };/*"字",0*/
void Display_char_test(void)
{
int cow_count,Byte_count,bit_count;
//一共有16行
for(cow_count=0;cow_count<16;cow_count++)
{
//每行2个字节
for(Byte_count=0;Byte_count<2;Byte_count++)
{
//一个字节8位
for(bit_count=0;bit_count<8;bit_count++)
{
//从左到右,从上到下扫描(扫描到1就打印 *, 0就打印空格)
if(Display_char[cow_count*2+Byte_count] & (0x80>>bit_count))
{
printf("*");
}
else
{
printf(" ");
}
}
}
//每行打印一个回车
printf("\n");
}
}
int main()
{
Display_char_test();
return 0;
}
实验效果:
仔细看一下代码很容易懂
如果对C语言的操作符(移位、&、|、~ 等等)还不熟的请看->C语言操作符详解
使用字模软件制作的字模数据一般会按照编码格式排列。如我们利用以上软件生成的字模文件《GB2312_H2424.FON》中的数据,是根据 GB2312 的区位码表的顺序存储的,它存储了区位码为 0101-9494 的字符,每个字模的大小为 16x16/8=32 字节。其中第一个字符“空格”的区位码为 0101,它是首个字符,所以文件的前 32 字节存储的是它的字模数据
所以我们可以导出任意字符的寻址公式:
Addr = (((CodeH-0xA1)*94) +(CodeL-0xA1)) * 16 * 16/8
A1A1是空格字符的GB2312编码。
其中 CodeH和 CodeL分别是 GB2312 编码的第一字节和第二字节;94 是指一个区中有94 个位(即 94 个字符)。公式的实质是根据字符的 GB2312 编码,求出区位码,然后区位码乘以每个字符占据的字节数,求出字符相对于空格字符的地址偏移。
1.ASCII 字模数据
要显示字符首先要有字库数据,在工程的“fonts.c”文件中我们定义了一系列大小为 24x32、16x24、16x8 的 ASCII 码表的字模数据
*这里以16(高度)8(宽度)为例
这里生成的字模数据去除了ASCII表前面32个不显示的字符
数组前面加了const关键字,则说明数组的内容那些字模数据是默认存储在STM32内部flash中的。
如何计算任意ASCII字符的在数组中的位置。
2.管理英文字模的结构体
只要你学好的指针和结构体,整个液晶显示的代码没什么难的
《指针从入门到熟练掌握》
《结构体详解》
关于结构体与指针的知识请看上面两篇,就不再详细说
3.切换字体
4.ASCII字符显示单个字符函数(重点)
void ILI9341_DispChar_EN ( uint16_t usX, uint16_t usY, const char cChar )
{
uint8_t byteCount, bitCount,fontLength;
uint16_t ucRelativePositon;
uint8_t *Pfont;
//对ascii码表偏移(字模表不包含ASCII表的前32个非图形符号)
ucRelativePositon = cChar - ' ';
//每个字模的字节数
fontLength = (LCD_Currentfonts->Width*LCD_Currentfonts->Height)/8;
//字模首地址
/*ascii码表偏移值乘以每个字模的字节数,求出字模的偏移位置*/
Pfont = (uint8_t *)&(LCD_Currentfonts->table[ucRelativePositon * fontLength]);
//设置显示窗口
ILI9341_OpenWindow ( usX, usY, LCD_Currentfonts->Width, LCD_Currentfonts->Height);
ILI9341_Write_Cmd ( CMD_SetPixel );
//按字节读取字模数据
//由于前面直接设置了显示窗口,显示数据会自动换行
for ( byteCount = 0; byteCount < fontLength; byteCount++ )
{
//一位一位处理要显示的颜色
for ( bitCount = 0; bitCount < 8; bitCount++ )
{
if ( Pfont[byteCount] & (0x80>>bitCount) )
ILI9341_Write_Data ( CurrentTextColor );
else
ILI9341_Write_Data ( CurrentBackColor );
}
}
}
/**
* @brief 在 ILI9341 显示器上显示英文字符串
* @param usX :在特定扫描方向下字符的起始X坐标
* @param usY :在特定扫描方向下字符的起始Y坐标
* @param pStr :要显示的英文字符串的首地址
* @note 可使用LCD_SetBackColor、LCD_SetTextColor、LCD_SetColors函数设置颜色
* @retval 无
*/
void ILI9341_DispString_EN ( uint16_t usX ,uint16_t usY, char * pStr )
{
while ( * pStr != '\0' )
{
if ( ( usX + LCD_Currentfonts->Width ) > LCD_X_LENGTH )
{
usX = ILI9341_DispWindow_X_Star;
usY += LCD_Currentfonts->Height;
}
if ( ( usY+ LCD_Currentfonts->Height ) > LCD_Y_LENGTH )
{
usX = ILI9341_DispWindow_X_Star;
usY = ILI9341_DispWindow_Y_Star;
}
ILI9341_DispChar_EN ( usX, usY, * pStr);
pStr ++;
usX += LCD_Currentfonts->Width;
}
}
显示 ASCII 编码比较简单,由于字库文件小,可以直接存储在STM32芯片内部的flash中,而GB2312字库包含7000多个汉字,内部的flash肯定存储不下,需要事先将GB2312的字库存储在外部的flash中(从flash的某个地址开始存储),所以我们显示汉字时必须得去外部的flash中获取相应的字模数据(STM32与外部flash通信(SPI通信))。
这里如何将字库存储在外部flash就不讲了,一般板子买来字库数据已经存储在外部flash中。
1.显示一个中文字符
/**
* @brief 在 ILI9341 显示器上显示一个中文字符
* @param usX :在特定扫描方向下字符的起始X坐标
* @param usY :在特定扫描方向下字符的起始Y坐标
* @param usChar :要显示的中文字符(国标码)
* @note 可使用LCD_SetBackColor、LCD_SetTextColor、LCD_SetColors函数设置颜色
* @retval 无
*/
void ILI9341_DispChar_CH ( uint16_t usX, uint16_t usY, uint16_t usChar )
{
uint8_t rowCount, bitCount;
uint8_t ucBuffer [ WIDTH_CH_CHAR*HEIGHT_CH_CHAR/8 ];
uint16_t usTemp;
//设置显示窗口
ILI9341_OpenWindow ( usX, usY, WIDTH_CH_CHAR, HEIGHT_CH_CHAR );
ILI9341_Write_Cmd ( CMD_SetPixel );
//取字模数据
GetGBKCode ( ucBuffer, usChar );
//按字节读取字模数据
//由于前面直接设置了显示窗口,显示数据会自动换行
for ( rowCount = 0; rowCount < 32; rowCount++ )
{
//一位一位处理要显示的颜色
for ( bitCount = 0; bitCount < 8; bitCount++ )
{
if ( ucBuffer[rowCount] & (0x80>>bitCount) )
ILI9341_Write_Data ( CurrentTextColor );
else
ILI9341_Write_Data ( CurrentBackColor );
}
}
}
如何取字模数据
原理详情请看字模寻址公式,其实与ascll字符一样取一个字模数据只需要中断该字模相对于第一个字模数据的偏移量即可。
int GetGBKCode_from_EXFlash( uint8_t * pBuffer, uint16_t c)
{
unsigned char High8bit,Low8bit;
unsigned int pos;
static uint8_t everRead=0;
/*第一次使用,初始化FLASH*/
if(everRead == 0)
{
SPI_FLASH_Init();
everRead = 1;
}
High8bit= c >> 8; /* 取高8位数据 */
Low8bit= c & 0x00FF; /* 取低8位数据 */
/*GB2312 公式*/
pos = ((High8bit-0xa1)*94+Low8bit-0xa1)*WIDTH_CH_CHAR*HEIGHT_CH_CHAR/8;
SPI_FLASH_BufferRead(pBuffer,GBKCODE_START_ADDRESS+pos,WIDTH_CH_CHAR*HEIGHT_CH_CHAR/8); //读取字库数据
// printf ( "%02x %02x %02x %02x\n", pBuffer[0],pBuffer[1],pBuffer[2],pBuffer[3]);
return 0;
}
void ILI9341_DispString_CH ( uint16_t usX , uint16_t usY, char * pStr )
{
uint16_t usCh;
while( * pStr != '\0' )
{
if ( ( usX - ILI9341_DispWindow_X_Star + WIDTH_CH_CHAR ) > LCD_X_LENGTH )
{
usX = ILI9341_DispWindow_X_Star;
usY += HEIGHT_CH_CHAR;
}
if ( ( usY - ILI9341_DispWindow_Y_Star + HEIGHT_CH_CHAR ) > LCD_Y_LENGTH )
{
usX = ILI9341_DispWindow_X_Star;
usY = ILI9341_DispWindow_Y_Star;
}
usCh = * ( uint16_t * ) pStr;
usCh = ( usCh << 8 ) + ( usCh >> 8 );
ILI9341_DispChar_CH ( usX, usY, usCh );
usX += WIDTH_CH_CHAR;
pStr += 2; //一个汉字两个字节
}
}
重点来了,到底一串中文字符串是如何转化成GB2312编码的呢
液晶显示总的来说不算很难,但是它结合了很多知识点,C语言的功底要扎实指针与结构体要学好,其次理解液晶显示原理、字符编码、以及字符在内存中如何存储,最后就是重点理解字模寻址公式。
结束语:
最近发现一款刷题神器,如果大家想提升编程水平,玩转C语言指针,还有常见的数据结构(最重要的是链表和队列)后面嵌入式学习操作系统的时如freerots、RT-Thread等操作系统,链表与队列知识大量使用。
大家可以点击下面连接进入牛客网刷题
点击跳转进入网站(C语言方向)
点击跳转进入网站(数据结构算法方向)