《DNESP32S3使用指南-IDF版_V1.6》第三十九章 汉字显示实验

第三十九章 汉字显示实验


       本章,我们将介绍如何使用ESP32控制SPILCD显示汉字。在本章中,我们将使用通过SD卡更新字库。ESP32读取存在SD卡里面的字库,然后将汉字显示在LCD上面。

       本章分为如下几个小节:

       39.1 汉字显示简介

       39.2 硬件设计

       39.3 程序设计

       39.4 下载验证


        39.1 汉字显示原理简介

       汉字的显示和ASCII显示其实是一样的原理,如图39.1.1所示:


图39.1.1 单个汉字显示原理框图


       上图显示了单个汉字显示的原理框图,单片机(MCU)先根据汉字编码(①,②)从字库里面找到该汉字的点阵数据(③),然后通过描点函数,按字库取模方式,将点阵数据在LCD上画出来(④),就可以实现一个汉字的显示。

       接下来,重点介绍一下汉字的:编码、字库及显示等相关知识。


       39.1.1 字符编码介绍

       单片机只能识别0和1,所有信息都是以0和1的形式存储的,单片机本身并不能识别字符,所以我们需要对字符进行编码(也叫内码,特定的编码对应特定的字符),单片机通过编码来识别具体的汉字。常见的字符集编码如表:39.1.1.1所示:


表39.1.1.1 常见字符集编码


       其中ASCII编码最简单,采用单字节编码,在前面的OLED和LCD实验,我们已经有所接触。ASCII是基于拉丁字母的一套电脑编码系统,仅包括128个编码,其中95个显示字符,使用一个字节即可编码完所有字符,我们常见的英文字母和数字,就是使用ASCII字符编码,另外ASCII字符显示所占宽度为汉字宽度的一半!也可以理解成:ASCII字符的宽度 = 高度的一半。

       GB2312、GBK和BIG5都是汉字编码,GBK码是GB2312的扩充,是国内计算机系统默认的汉字编码,而BIG5则是繁体汉字字符集编码,在香港和台湾的计算机系统汉字编码一般默认使用BIG5编码。一般来说,汉字显示所占的宽度等于高度,即宽度和高度相等。

       UNICODE是国际标准编码,支持各国文字,一般是2字节编码(也可以是3字节),这里不做讨论。想详细了解的可以执行百度学习。

       接下来,我们重点介绍一下GBK编码。

       GBK是一套汉字编码规则,采用双字节编码,共23940个码位,收录汉字和图形符号21886个,其中汉字(含繁体字和构件)21003个,图形符号883个。

       每个GBK码由2个字节组成,第一个字节范围:0X81~0XFE,第二个字节分为两部分,一是:0X40~0X7E,二是:0X80~0XFE。其中与GB2312相同的区域,字完全相同。GBK编码规则如表39.1.1.2所示:


表39.1.1.2 GBK编码规则


       我们把第一个字节(高字节)代表的意义称为区,那么GBK里面总共有126个区(0XFE - 0X81 + 1),每个区内有190个汉字(0XFE - 0X80 + 0X7E - 0X40 + 2),总共就有126*190=23940个汉字。

       第一个编码:0X8140,对应汉字:丂;

       第二个编码:0X8141,对应汉字:丄;

       第三个编码:0X8142,对应汉字:丅;

       第四个编码:0X8143,对应汉字:丆;

       依次对所有汉字进行编码,详见: www.qqxiuzi.cn/zh/hanzi-gbk-bianma.php


       39.1.2 汉字字库简介

       光有汉字编码,单片机还是无法在LCD上显示这个汉字的,必须有对应汉字编码的点阵数据,才可以通过描点的方式,将汉字显示在LCD上。所有汉字点阵数据的集合,就叫做汉字字库。而不同大小的汉字,其字库大小也不一样,因此又有不同大小汉字的字库(如:12*12汉字字库、16*16汉字字库、24*24汉字字库等)。

       单个汉字的点阵数据,也称之为字模。汉字在液晶上的显示其实就是一些点的显示与不显示,这就相当于我们的笔一样,有笔经过的地方就画出来,没经过的地方就不画。为了方便取模和描点,我们一般规定一个取模方向,当取模和描点都按取模方向来操作,就可以实现一个汉字的点阵数据提取和显示。

       以12*12大小的“好”字为例,假设我们规定取模方向为:从上到下,从左到右,且高位在前,则其取模原理如图39.1.2.1所示:


图39.1.2.1 从上到下,从左到右取模原理


       图中,我们取模的时候,从最左上方的点开始取(从上到下,从左到右),且高位在前(bit7在表示第一个位),那么:

       第一个字节是:0X11(1,表示浅蓝色的点,即要画出来的点,0则表示不要画出来);

       第二个字节是:0X10;

       第三个字节是:0X1E(到第二列了,每列2个字节);

       第四个字节是:0XA0;

       以此类推,共12列,每列2个字节,总共24字节,12*12“好”字完整的字模如下:

uint8_t hzm_1212[24]={
0x11,0x10,0x1E,0xA0,0xF0,0x40,0x11,0xA0,0x1E,0x10,0x42,0x00,
0x42,0x10,0x4F,0xF0,0x52,0x00,0x62,0x00,0x02,0x00,0x00,0x00};  /* 好字字模 */

        在显示的时候,我们只需要读取这个汉字的点阵数据(12*12字体,一个汉字的点阵数据为24个字节),然后将这些数据,按取模方式,反向解析出来(坐标要处理好),每个字节,是1的位,就画出来,不是1的位,就忽略,这样,就可以显示出这个汉字了。

       知道显示一个汉字的原理,就可以推及整个汉字库了,要显示任意汉字,我们首先要知道该汉字的点阵数据,整个GBK字库是比较大的(2W多个汉字),这些数据可以由专门的软件来生成。

       字库的制作

       字库的制作,我们要用到一款软件,由正点原子设计的字模生成软件。该软件可以在Windows 2000/XP/7/8/10等操作系统下生成任意点阵大小的ASCII,GB2312(简体中文)、GBK(简繁体中文)、BIG5(繁体中文)、HANGUL(韩文)、SJIS(日文)、Unicode 以及泰文,越南文、俄文、乌克兰文,拉丁文,8859 系列等共二十几种编码的字库,不但支持生成二进制文件格式的文件,也可以生成BDF 文件,还支持生成图片功能,并支持横向,纵向等多种扫描方式,且扫描方式可以根据用户的需求进行增加。软件主界面如图39.1.2.2所示:


图39.1.2.2 点阵字库生成器默认界面


       要生成 16*16 的 GBK 字库,第一步进入 XFONT 软件的字库模式;第二步设置编码和字体大小,这里需要选择 GBK 编码以及设置字体大小为 16*16,另外还需要设置输出路径,这个由用户自己去设置,后面生成的字库文件会出现在该路径下;第三步设置取模方式,这里需要设置为从上到下,从左到右,高位在前;第四步,按下生成字库按钮,等待字库生成。这一系列的具体操作如图39.1.2.3所示:


图39.1.2.3 生成GBK16*16字库的设置方法


       注意:电脑端的字体大小与我们生成点阵大小的关系为:

fsize = dsize * 6 / 8

       其中,fsize是电脑端字体的大小,dsize是点阵大小(12、16、24等)。所以16*16点阵大小对应的是12号字体。

       生成完以后,我们把文件名和后缀改成:GBK16.FON(这里是手动修改后缀!!)。用类似的方法,生成12*12的点阵库(GBK12.FON)和24*24的点阵库(GBK24.FON),总共制作3个字库。

       另外,该软件还可以生成其他很多字库,字体也可选,大家可以根据自己的需要按照上面的方法生成即可。该软件的详细介绍请看《ATK-XFONT软件用户手册》。

       最后,我们生成的字库,要先放入TF卡,然后TF卡中的字库文件通过分区表进行加载,我们在分区表(partitions-16MiB)中定义了一个分区专用于SPIFFS以及存储。使用的时候,分区表读取传入的字库存放的起始地址。 


       39.1.3 汉字显示原理

       经过以上两个小节的学习,我们可以归纳出汉字显示的过程:

MCU→汉字编码→汉字字库→汉字点阵数据→描点

       编码和字库的制作我们已经学会了,所以只剩下一个问题:如何通过汉字编码在汉字字库里面查找对应汉字的点阵数据?

       根据GBK编码规则,我们的汉字点阵字库只要按照这个编码规则从0X8140开始,逐一建立,每个区的点阵大小为每个汉字所用的字节数*190。这样,我们就可以得到在这个字库里面定位汉字的方法:

       当GBKL < 0X7F时:Hp = ((GBKH - 0x81) * 190 + GBKL - 0X40) * csize;

       当GBKL > 0X80时:Hp = ((GBKH - 0x81) * 190 + GBKL - 0X41) * csize;

       其中GBKH、GBKL分别代表GBK的第一个字节和第二个字节(也就是高字节和低字节),csize代表单个汉字点阵数据的大小(字节数),Hp则为对应汉字点阵数据在字库里面的起始地址(假设是从0开始存放,如果是非0开始,则加上对应偏移量即可)。

       单个汉字点阵数据大小(csize)计算公式如下:

csize = (size / 8 + ((size % 8) ? 1 : 0)) * (size);

       其中size为汉字点阵长宽尺寸,如:12(对应12*12字体)、16(对应16*16字体)、24(对应24*24字体)。对于12*12字体,csize大小为24字节,对于16*16字体,csize大小为32字节。

       通过以上方法,从字库里面获取到某个汉字点阵数据后,按取模方式(我们使用:从上到下、从左到右,高位在前)进行描点还原即可将汉字显示在LCD上面。这就是汉字显示的原理。


        39.2 硬件设计


       39.2.1 例程功能

       本实验开机的时候程序通过预设值的标记位检测分区表中是否已经存在字库,如果存在,则按次序显示汉字(三种字体都显示)。如果没有,则检测SD卡和文件系统,并查找SYSTEM文件夹下的FONT文件夹,在该文件夹内查找UNIGBK.BIN、GBK12.FON、GBK16.FON和GBK24.FON这几个文件的由来,我们在前面已经介绍过了。在检测到这些文件之后,就开始更新字库,更新完毕才开始显示汉字。通过按按键KEY0,可以强制更新字库。

       LED闪烁,提示程序运行。


       39.2.2 硬件资源


       1. LED灯

              LED -IO0


       2. XL9555

              IIC_SDA-IO41

              IIC_SCL-IO42


       3. SPILCD

              CS-IO21

              SCK-IO12

              SDA-IO11

              DC-IO40(在P5端口,使用跳线帽将IO_SET和LCD_DC相连)

              PWR- IO1_3(XL9555)

              RST- IO1_2(XL9555)


       4. SD

              CS-IO2

              SCK-IO12

              MOSI-IO11

              MISO-IO13


       39.2.3 原理图

       本章实验使用的字库管理库为软件库,因此没有对应的连接原理图。


        39. 3   程序设计


       39.3.1 程序流程图

       程序流程图能帮助我们更好的理解一个工程的功能和实现的过程,对学习和设计工程有很好的主导作用。下面看看本实验的程序流程图:


图39.3.1.1 汉字实验程序流程图


       39.3.2 汉字显示函数解析

       正点原子提供的字库管理库包含了六个文件,分别为:convert.c、convert.h、fonts.c、fonts.h、text.c和text.h,本章实验配套实验例程中已经提供了这六个文件,并且已经针对正点原子ESP32-S3软硬件进行了移植适配,用户在使用时,仅需将这六个文件添加到自己的工程中即可,如下图所示:


图39.3.2.1 正点原子字库文件


       字库管理库中fonts.c和fonts.h两个文件提供了字库更新和初始化的函数,test.c和test.h文件中提供了在LCD上显示中文字符的函数,convert.c和convert.h提供了UTF8与GBK互转代码。

       字库管理库在显示中文字符至LCD上时会使用SD卡或者分区表中的中文字库,因此需要确保这两者中的中文字库无误,若分区表中没有中文字库的数据,那么在进行字库初始化时就会提示失败,这时就需要使用字库管理库中提供的字库更新函数更新分区表中的中文字库数据,更新字库是读取SD卡中的字库文件将其写入分区表,因此需确保SD卡中有对应的中文字库文件。本章实验所需的中文字库文件可在A盘→5,SD卡根目录文件→SYSTEM→FONT中找到,建议将A盘à5,SD卡根目录文件中的所有文件按照该目录的目录结构复制到SD卡中,方便后续实验的使用。


       39.3.3 汉字显示驱动解析

       在IDF版的28_chinese_display例程中,作者在28_chinese_display\components\BSP路径下并未添加新的内容,而是在28_chinese_display\main\APP路径下面,新增了一个APP文件,我们将详细解析这四个文件的实现内容。


       1,fonts.h文件

/* 字库信息结构体定义
 * 用来保存字库基本信息,地址,大小等
 */
__packed typedef struct
{
    uint8_t  fontok;             /* 字库存在标志,0XAA,字库正常;其他,字库不存在 */
    uint32_t ugbkaddr;           /* unigbk的地址 */
    uint32_t ugbksize;           /* unigbk的大小 */
    uint32_t f12addr;            /* gbk12地址 */
    uint32_t gbk12size;          /* gbk12的大小 */
    uint32_t f16addr;            /* gbk16地址 */
    uint32_t gbk16size;          /* gbk16的大小 */
    uint32_t f24addr;            /* gbk24地址 */
    uint32_t gbk24size;          /* gbk24的大小 */
} _font_info;

        这个结构体占用33字节,用于记录字库的地址和大小等信息。其中,首字节指示字库状态,其余字节记录地址和文件大小。用户字库等文件存储在分区表后的12M存储storage分区。前33字节保留给_font_info,以保存其结构体数据;随后是UNIGBK.BIN、GBK12.FON、GBK16.FON和GBK24.FON文件。


       2,fonts.c文件

       字库初始化函数也是利用其存储顺序,进行检查字库,其定义如下:

/**
 * @brief       初始化字体
 * @param       无
 * @retval      0, 字库完好; 其他, 字库丢失;
 */
uint8_t fonts_init(void)
{
    uint8_t t = 0;
 
    storage_partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA,
                                                     ESP_PARTITION_SUBTYPE_ANY,
                                                     "storage");
    
    if (storage_partition == NULL)
    {
        ESP_LOGE(TAG, "Flash partition not found.");
        return 1;
    }
    
    while (t < 10)  /* 连续读取10次,都是错误,说明确实是有问题,得更新字库了 */
    {
        t++;
        
        /* 读出ftinfo结构体数据 */
        fonts_partition_read((uint8_t *)&ftinfo,
                                FONTINFOADDR,
                                sizeof(ftinfo));
 
        if (ftinfo.fontok == 0XAA)
        {
            break;
        }
        
        vTaskDelay(20);
    }
 
    if (ftinfo.fontok != 0XAA)
    {
        return 1;
    }
    return 0;
}

        这里就是把分区表的12M地址的33个字节数据读取出来,进而判断字库结构体ftinfo的字库标记fontok是否为AA,确定字库是否完好。

       有人会有疑问:ftinfo.fontok是在哪里赋值AA呢?肯定是字库更新完毕后,给该标记赋值的,那下面就来看一下是不是这样子,字库更新函数定义如下:

/**
 * @brief       更新字体文件
 * @note        所有字库一起更新(UNIGBK,GBK12,GBK16,GBK24)
 * @param       x, y    : 提示信息的显示地址
 * @param       size    : 提示信息字体大小
 * @param       src     : 字库来源磁盘
 * @arg                   "0:", SD卡;
 * @Arg                   "1:", FLASH盘
 * @param       color   : 字体颜色
 * @retval      0, 成功; 其他, 错误代码;
 */
uint8_t fonts_update_font(uint16_t x, 
   uint16_t y,
   uint8_t size,
   uint8_t *src,
   uint16_t color)
{
    uint8_t *pname;
    uint32_t *buf;
    uint8_t res = 0;
    uint16_t i, j;
    FIL *fftemp;
    uint8_t rval = 0;
    res = 0XFF;
    ftinfo.fontok = 0XFF;
    pname = malloc(100);                    /* 申请100字节内存 */
    buf = malloc(4096);                     /* 申请4K字节内存 */
    fftemp = (FIL *)malloc(sizeof(FIL));    /* 分配内存 */
 
    if (buf == NULL || pname == NULL || fftemp == NULL)
    {
        free(fftemp);
        free(pname);
        free(buf);
        return 5;          /* 内存申请失败 */
    }
 
    for (i = 0; i < 4; i++) /* 先查找文件UNIGBK,GBK12,GBK16,GBK24 是否正常 */
    {
        strcpy((char *)pname, (char *)src); /* copy src内容到pname */
        strcat((char *)pname, (char *)FONT_GBK_PATH[i]);  /* 追加具体文件路径 */
        res = f_open(fftemp, (const TCHAR *)pname, FA_READ);/* 尝试打开 */
 
        if (res)
        {
            rval |= 1 << 7;      /* 标记打开文件失败 */
            break;               /* 出错了,直接退出 */
        }
    }
 
    free(fftemp);          /* 释放内存 */
 
    if (rval == 0)                /* 字库文件都存在. */
    {
        /* 提示正在擦除扇区 */
        lcd_show_string(x, y, 240, 320, size, "Erasing sectors... ", color); 
 
        /* 先擦除字库区域,提高写入速度 */
        for (i = 0; i < FONTSECSIZE; i++)
        {
            /* 进度显示 */
            fonts_progress_show(x+20*size/2,y,size,FONTSECSIZE,i,color);
            
            /* 读出整个扇区的内容 */
            fonts_partition_read((uint8_t *)buf,
   ((FONTINFOADDR/4096)+i)*4096,
4096);
 
            for (j = 0; j < 1024; j++)           /* 校验数据 */
            {
                if (buf[j] != 0XFFFFFFFF)break;  /* 需要擦除 */
            }
 
            if (j != 1024)
            {
                /* 需要擦除的扇区 */
                fonts_partition_erase_sector(((FONTINFOADDR / 4096) + i)*4096);
            }
        }
 
        for (i = 0; i < 4; i++) /* 依次更新UNIGBK,GBK12,GBK16,GBK24 */
        {
            lcd_show_string(x,y,240,320,size,FONT_UPDATE_REMIND_TBL[i],color);
            strcpy((char *)pname, (char *)src); /* copy src内容到pname */
            strcat((char *)pname, (char *)FONT_GBK_PATH[i]);/* 追加具体文件路径 */
            
            /* 更新字库 */
            res = fonts_update_fontx(x + 20 * size / 2,y,size,pname,i,color);
 
            if (res)
            {
                free(buf);
                free(pname);
                return 1 + i;
            }
        }
 
        /* 全部更新好了 */
        ftinfo.fontok = 0XAA;
        
        /* 保存字库信息 */
        fonts_partition_write((uint8_t *)&ftinfo, FONTINFOADDR, sizeof(ftinfo));
    }
 
    free(pname);            /* 释放内存 */
    free(buf);
    return rval;            /* 无错误 */
}

        函数的实现:动态申请内存→尝试打开文件(UNIGBK、GBK12、GBK16和GBK24),确定文件是否存在→擦除字库→依次更新UNIGBK、GBK12、GBK16和GBK24→写入ftinfo结构体信息。

       在字库更新函数中能直接看到的是ftinfo.fontok成员被赋值,而其他成员在单个字库更新函数中被赋值,接下来分析一下更新某个字库函数,其代码如下:

/**
 * @brief       更新某一个字库
 * @param       x, y    : 提示信息的显示地址
 * @param       size     : 提示信息字体大小
 * @param       fpath    : 字体路径
 * @param       fx       : 更新的内容
 * @arg                  0, ungbk;
 * @Arg                  1, gbk12;
 * @arg                  2, gbk16;
 * @arg                  3, gbk24;
 * @param       color    : 字体颜色
 * @retval      0, 成功; 其他, 错误代码;
 */
static uint8_t fonts_update_fontx(uint16_t x, uint16_t y, uint8_t size, uint8_t *fpath, uint8_t fx, uint16_t color)
{
    uint32_t flashaddr = 0;
    FIL *fftemp;
    uint8_t *tempbuf;
    uint8_t res;
    uint16_t bread;
    uint32_t offx = 0;
    uint8_t rval = 0;
    fftemp = (FIL *)mymalloc(SRAMIN, sizeof(FIL)); /* 分配内存 */
    if (fftemp == NULL)rval = 1;
 
tempbuf = mymalloc(SRAMIN, 4096);               /* 分配4096个字节空间 */
 
    if (tempbuf == NULL)rval = 1;
 
    res = f_open(fftemp, (const TCHAR *)fpath, FA_READ);
 
if (res)rval = 2;   /* 打开文件失败 */
 
    if (rval == 0)
    {
        switch (fx)
        {
            case 0: /* 更新 UNIGBK.BIN */  
/*信息头之后,紧跟UNIGBK转换码表 */
                ftinfo.ugbkaddr = FONTINFOADDR + sizeof(ftinfo); 
                ftinfo.ugbksize = fftemp->obj.objsize;      /* UNIGBK大小 */
                flashaddr = ftinfo.ugbkaddr;
                break;
 
            case 1: /* 更新 GBK12.FONT */  
/* UNIGBK之后,紧跟GBK12字库 */
                ftinfo.f12addr = ftinfo.ugbkaddr + ftinfo.ugbksize; 
                ftinfo.gbk12size = fftemp->obj.objsize;   /* GBK12字库大小 */
                flashaddr = ftinfo.f12addr;               /* GBK12的起始地址 */
                break;
 
            case 2: /* 更新 GBK16.FONT */  
/* GBK12之后,紧跟GBK16字库 */
                ftinfo.f16addr = ftinfo.f12addr + ftinfo.gbk12size; 
                ftinfo.gbk16size = fftemp->obj.objsize;     /* GBK16字库大小 */
                flashaddr = ftinfo.f16addr;                 /* GBK16的起始地址 */
                break;
 
            case 3: /* 更新 GBK24.FONT */
                /* GBK16之后,紧跟GBK24字库 */
                ftinfo.f24addr = ftinfo.f16addr + ftinfo.gbk16size; 
                ftinfo.gbk24size = fftemp->obj.objsize;     /* GBK24字库大小 */
                flashaddr = ftinfo.f24addr;                 /* GBK24的起始地址 */
                break;
        }
 
        while (res == FR_OK)   /* 死循环执行 */
        {
res = f_read(fftemp, tempbuf, 4096, (UINT *)&bread); /* 读取数据 */
          if (res != FR_OK)break;     /* 执行错误 */
 
 /* 从0开始写入bread个数据 */
 fonts_partition_write(tempbuf, offx + flashaddr, bread);
 
          offx += bread;
         fonts_progress_show(x,y,size,fftemp->obj.objsize,offx,color);/*进度显示*/
          if (bread != 4096)break;    /* 读完了. */
        }
 
        f_close(fftemp);
}
 
    free(fftemp);     /* 释放内存 */
    free(tempbuf);    /* 释放内存 */
    return res;
}

        单个字库更新函数,主要是对把字库从SD卡中读取出数据,写入分区表。同时把字库大小和起始地址保存在ftinfo结构体里,在前面的整个字库更新函数中使用函数:

/*保存字库信息*/
fonts_partition_write((uint8_t *)&ftinfo,FONTINFOADDR,sizeof(ftinfo));

        结构体的所有成员一并写入到那33字节。有了这个字库信息结构体,就能很容易进行定位。结合前面的说到的根据地址偏移寻找汉字的点阵数据,我们就可以开始真正把汉字搬上屏幕中去了。

       首先我们肯定需要获得汉字的GBK码,这里VSCode已经帮我们实现了。这里用一个例子说明:


图39.3.1.1 终端显示一个汉字的高位码与低位码


       在这里可以看出VSCode识别汉字的方式是GBK码,换句话来说就是VSCode自动会把汉字看成是两个字节表示的东西。知道了要表示的汉字和其GBK码,那么就可以去找对应的点阵数据。在这里我们就定义了一个获取汉字点阵数据的函数,其定义如下:

/**
 * @brief       获取汉字点阵数据
 * @param       code  : 当前汉字编码(GBK码)
 * @param       mat   : 当前汉字点阵数据存放地址
 * @param       size  : 字体大小
 * @note        size大小的字体,其点阵数据大小为: 
   (size / 8 + ((size % 8) ? 1 : 0)) * (size)  字节
 * @retval      无
 */
static void text_get_hz_mat(unsigned char *code,unsigned char *mat,uint8_tsize)
{
    unsigned char qh, ql;
    unsigned char i;
    unsigned long foffset;
    uint8_t csize;
    
    /* 计算字体一个字符对应点阵集所占的字节数 */
    csize = (size / 8 + ((size % 8) ? 1 : 0)) * (size);
    qh = *code;
ql = *(++code);
 
/* 非常用汉字 */
    if ((qh < 0x81) || (ql < 0x40) || (ql == 0xFF) || (qh == 0xFF)) 
    {
        for (i = 0; i < csize; i++)
        {
            *mat++ = 0x00;                                          /* 填充满格 */
        }
        return;
    }
    
    if (ql < 0x7F)
    {
        ql -= 0x40;
    }
    else
    {
        ql -= 0x41;
    }
    
    qh -= 0x81;
    foffset = ((unsigned long)190 * qh + ql) * csize;/* 得到字库中的字节偏移量 */
    
    switch (size)
    {
        case 12:
        {
            fonts_partition_read(mat, foffset + ftinfo.f12addr, csize);
            break;
        }
        case 16:
        {
            fonts_partition_read(mat, foffset + ftinfo.f16addr, csize);
            break;
        }
        case 24:
        {
            fonts_partition_read(mat, foffset + ftinfo.f24addr, csize);
            break;
        }
    }
}

        函数实现的依据就是前面39.1.3小节讲到的两条公式:

       当GBKL < 0X7F时:Hp = ((GBKH - 0x81) * 190 + GBKL - 0X40) * csize;

       当GBKL > 0X80时:Hp = ((GBKH - 0x81) * 190 + GBKL - 0X41) * csize;

       目标汉字的GBK码满足上面两条公式其一,就会得出与一个GBK对应的汉字点阵数据的偏移。在这个基础上,通过汉字点阵的大小,就可以从对应的字库提取目标汉字点阵数据。

       在获取到点阵数据后,接下来就可以进行汉字显示,下面看一下汉字显示函数,其定义如下:

/**
 * @brief       显示一个指定大小的汉字
 * @param       x,y   : 汉字的坐标
 * @param       font  : 汉字GBK码
 * @param       size  : 字体大小
 * @param       mode  : 显示模式
 * @note                0, 正常显示(不需要显示的点,用LCD背景色填充,即g_back_color)
 * @note                1, 叠加显示(仅显示需要显示的点, 不需要显示的点, 不做处理)
 * @param       color : 字体颜色
 * @retval      无
 */
void text_show_font(uint16_t x,
                     uint16_t y,
                     uint8_t *font, 
                     uint8_t size, 
                     uint8_t mode, 
                     uint16_t color)
{
    uint8_t temp, t, t1;
    uint16_t y0 = y;
    uint8_t *dzk;
    uint8_t csize;
    uint8_t font_size = size;
    
    /* 计算字体一个字符对应点阵集所占的字节数 */
    csize = (font_size / 8 + ((font_size % 8) ? 1 : 0)) * (font_size);         
    
    if ((font_size != 12) && (font_size != 16) && (font_size != 24))
    {
        return;
    }
    
    dzk = (uint8_t *)malloc(font_size * 5);      /* 申请内存 */
    
    if (dzk == NULL)
    {
        return;
    }
    
    text_get_hz_mat(font, dzk, font_size);       /* 得到相应大小的点阵数据 */
    
    for (t = 0; t < csize; t++)
    {
        temp = dzk[t];                           /* 得到点阵数据 */
        
        for (t1 = 0; t1 < 8; t1++)
        {
            if (temp & 0x80)
            {
                lcd_draw_pixel(x, y, color);     /* 画需要显示的点 */
            }
            
            /* 如果非叠加模式,不需要显示的点用背景色填充 */
            else if (mode == 0)                                 
            {
                lcd_draw_pixel(x, y, 0xffff);    /* 填充背景色 */
            }
            
            temp <<= 1;
            y++;
            if ((y - y0) == font_size)
            {
                y = y0;
                x++;
                break;
            }
        }
    }
    
    free(dzk);                                   /* 释放内存 */
}

        汉字显示函数通过调用获取汉字点阵数据函数text_get_hz_mat就获取到点阵数据,使用lcd画点函数把点阵数据中“1”的点都画出来,最终会在LCD显示你所要表示的汉字。

       其他函数就不多讲解,大家可以自行消化。


       39.3.4 CMakeLists.txt文件

       打开本实验BSP下的CMakeLists.txt文件,其内容如下所示:

set(src_dirs
            IIC
            LCD
            LED
            SPI
            XL9555)
 
set(include_dirs
            IIC
            LCD
            LED
            SPI
            XL9555)
 
set(requires
            driver
            fatfs)
 
idf_component_register(SRC_DIRS ${src_dirs} 
INCLUDE_DIRS ${include_dirs} REQUIRES ${requires})
 
component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)

        上述的红色fatfs依赖库需要由开发者自行添加,以确保汉字显示驱动能够顺利集成到构建系统中。这一步骤是必不可少的,它确保了汉字显示驱动的正确性和可用性,为后续的开发工作提供了坚实的基础。

       打开本实验main文件下的CMakeLists.txt文件,其内容如下所示:

idf_component_register(
    SRC_DIRS 
        "."
        "APP"
    INCLUDE_DIRS 
        "."
        "APP")

        上述的红色APP驱动需要由开发者自行添加,在此便不做赘述了。 


       39.3.5 实验应用代码

       打开main/main.c文件,该文件定义了工程入口函数,名为app_main。该函数代码如下。

i2c_obj_t i2c0_master;
 
 
/**
 * @brief       程序入口
 * @param       无
 * @retval      无
 */
void app_main(void)
{
    esp_err_t ret;
    uint8_t t;
    uint8_t key;
    uint8_t res;
    uint32_t fontcnt;
    uint8_t i;
    uint8_t j;
    uint8_t fontx[2];
 
    ret = nvs_flash_init();                             /* 初始化NVS */
    
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || 
ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
    {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }
 
    ESP_ERROR_CHECK(ret);
 
    led_init();                                         /* 初始化LED */
    i2c0_master = iic_init(I2C_NUM_0);                  /* 初始化IIC0 */
    spi2_init();                                        /* 初始化SPI */
    xl9555_init(i2c0_master);                           /* 初始化IO扩展芯片 */  
    lcd_init();                                         /* 初始化LCD */
 
    while (sd_spi_init())                               /* 检测不到SD卡 */
    {
        lcd_show_string(30, 50, 200, 16, 16, "SD Card Failed!", RED);
        vTaskDelay(200);
        lcd_fill(30, 50, 200 + 30, 50 + 16, WHITE);
        vTaskDelay(200);
    }
 
    while (fonts_init())                                /* 检查字库 */
    {
UPD:
        lcd_clear(WHITE);                               /* 清屏 */
        lcd_show_string(30, 30, 200, 16, 16, "ESP32-S3", RED);
        
        key = fonts_update_font(30, 50, 16, (uint8_t *)"0:", RED);  /* 更新字库 */
 
        while (key)                                     /* 更新失败 */
        {
            lcd_show_string(30, 50, 200, 16, 16, "Font Update Failed!", RED);
            vTaskDelay(200);
            lcd_fill(20, 50, 200 + 20, 90 + 16, WHITE);
            vTaskDelay(200);
        }
 
        lcd_show_string(30, 50, 200, 16, 16, "Font Update Success!   ", RED);
        vTaskDelay(1500);
        lcd_clear(WHITE);                               /* 清屏 */
    }
   
    text_show_string(30, 30, 200, 16, "正点原子ESP32-S3开发板", 16, 0, RED);
    text_show_string(30, 50, 200, 16, "GBK字库测试程序", 16, 0, RED);
    text_show_string(30, 70, 200, 16, "ATOM@ALIENTEK", 16, 0, RED);
    text_show_string(30, 90, 200, 16, "KEY0: 更新字库", 16, 0, RED);
    
    text_show_string(30, 110, 200, 16, "内码高字节:", 16, 0, BLUE);
    text_show_string(30, 130, 200, 16, "内码低字节:", 16, 0, BLUE);
    text_show_string(30, 150, 200, 16, "汉字计数器:", 16, 0, BLUE);
    
    text_show_string(30, 180, 200, 24, "对应汉字为:", 24, 0, BLUE);
    text_show_string(30, 204, 200, 16, "对应汉字为:", 16, 0, BLUE);
    text_show_string(30, 220, 200, 12, "对应汉字为:", 12, 0, BLUE);
 
    while (1)
    {
        fontcnt = 0;
        
        for (i = 0x81; i < 0xFF; i++)   /* GBK内码高字节范围为0x81~0xFE */                                       
        {
            fontx[0] = i;
            lcd_show_num(118, 110, i, 3, 16, BLUE);  /* 显示内码高字节 */                           
            
/* GBK内码低字节范围为0x40~0x7E、0x80~0xFE) */
            for (j = 0x40; j < 0xFE; j ++)                                      
            {
                if (j == 0x7F)
                {
                    continue;
                }
                
                fontcnt++;
                lcd_show_num(118, 130, j, 3, 16, BLUE);    /* 显示内码低字节 */                       
                lcd_show_num(118, 150, fontcnt, 5, 16, BLUE); /* 汉字计数显示 */                  
                fontx[1] = j;
                text_show_font(30 + 132, 180, fontx, 24, 0, BLUE);
                text_show_font(30 + 144, 204, fontx, 16, 0, BLUE);
                text_show_font(30 + 108, 220, fontx, 12, 0, BLUE);
                
                t = 200;
                
                while ((t --) != 0)  /* 延时,同时扫描按键 */                                            
                {
                    vTaskDelay(1);
                    
                    key = xl9555_key_scan(0);
                    
                    if (key == KEY0_PRES)
                    {
                        goto UPD;    /* 跳转到UPD位置(强制更新字库) */                                             
                    }
                }
                
                LED_TOGGLE();
            }
        }
    }
}

        通过上述描述得知,程序首先执行部分外设初始化,并通过while循环实现SD卡以及字库的检测,当以上两者检测无误后便可以在LCD相应区域内显示字库内的信息。


        39.4 下载验证

       本例程支持12*12、16*16和24*24等三种字体的显示,将程序下载到开发板后,可以看到LED0不停的闪烁,提示程序已经在运行了。LCD开始显示三种大小的汉字及内码如图39.4.1所示:


图39.4.1 汉字显示实验显示效果


       一开始就显示汉字,是因为板子在出厂的时候都是测试过的,里面刷了综合测试程序,已经把字库写入到分区表里面,所以并不会提示更新字库。如果你想要更新字库,就需要先找一张SD卡,把A盘资料\5,SD卡根目录文件 文件夹下面的SYSTEM文件夹拷贝到SD卡根目录下,插入开发板,并按复位,之后,在显示汉字的时候,按下KEY0,就可以开始更新字库。字库更新界面如图39.4.2所示:


图39.4.2擦除界面


图39.4.3汉字字库更新界面


请使用浏览器的分享功能分享到微信等