《DNESP32P4开发指南_V1.0》第四十章 汉字显示实验

第四十章 汉字显示实验

在嵌入式系统中,显示汉字和字符是人机交互的重要组成部分。与普通的英文字母或数字不同,汉字具有更多的笔画和更复杂的结构,因此在显示和处理上需要更为精细的管理。为了能够在液晶显示器(LCD)等显示设备上准确、清晰地显示汉字,嵌入式系统通常依赖点阵字库和编码映射技术。这个章节将详细介绍汉字显示的基本原理,包括汉字编码、字库管理和点阵数据的处理方法,帮助开发者理解如何通过编码数据在LCD上准确地显示汉字。
本章分为如下几个小节:
40.1 汉字显示原理概述
40.2 硬件设计
40.3 程序设计
40.4 下载验证

40.1 汉字显示原理概述
汉字的显示原理与ASCII字符类似,都是通过点阵数据在显示屏上描点形成字符图像。具体而言,MCU(微控制器)根据汉字编码从预先存储的字库中找到对应的点阵数据,然后使用点阵数据对LCD进行描点绘制,以实现汉字的显示。字库包含了汉字的点阵数据,编码则用于定位特定汉字在字库中的位置。通过这种方式,MCU能够将汉字编码转换为LCD上的实际显示图形,从而实现汉字在LCD设备上显示。下面我们来看一下单个汉字显示原理,如下图所示。

image002

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

从上图中可以看出,汉字显示的过程是通过编码、字库查找和点阵绘制完成的。MCU首先根据要显示的汉字获取相应的编码和字库中的字节偏移量(图中①和②),并通过该编码在字库中查找对应的点阵数据保存到一个缓存区当中(图中③)。找到点阵数据后,MCU将数据按照字库的取模方式逐点描绘到LCD屏幕上(图中④),从而形成汉字的显示。这一过程实现了汉字的精确还原,使嵌入式设备能够以图形方式显示复杂的汉字内容。
单个汉字的显示原理同样适用于多个汉字的显示。不同之处在于,对于多个汉字,我们需要在字库中按字节偏移来定位每个汉字的点阵数据,逐一绘制到LCD上。接下来,笔者将逐步讲解汉字的编码方式、字库结构以及显示实现的相关知识,帮助读者深入理解汉字在嵌入式设备上的显示过程。

40.1.1 字符编码介绍
我们知道,无论那种电子产品的控制器都只能识别二进制的0和1,因此所有信息都以二进制形式存储。然而,MCU无法直接识别字符,因此我们需要对字符进行编码(即内码,每种特定编码对应特定字符),使MCU能够识别和显示字符。常见的字符集编码见下表所示。
字符集 编码长度 说明

QQ截图20260728095823

表40.1.1.1 常见字符集编码

上表中,ASCII编码最为简单,采用单字节编码。在之前LCD实验中我们已经接触过。ASCII是一套基于拉丁字母的编码系统,包含128个编码,其中95个是可显示字符,使用一个字节即可完成编码。常见的英文字母和数字使用ASCII编码表示,且ASCII字符的宽度仅为汉字宽度的一半,即“ASCII字符宽度 = 高度的一半”。
GB2312、GBK和BIG5编码则用于汉字显示。GBK是GB2312的扩展,通常是中国大陆系统默认的汉字编码,而BIG5主要用于中国香港和中国台湾的系统。一般来说,汉字显示所占宽度等于其高度,即宽高相等。
UNICODE是一种国际标准编码,支持多国语言,通常为2字节编码(也有3字节的情况)。这里不深入讨论,读者可自行查阅资料。
本章节的实验采用的是GBK字库,所以我们重点来看一下GBK编码相关知识。
GBK 采用双字节表示,总体编码范围为 8140-FEFE,首字节在 81-FE 之间,尾字节在 40-FE 之间,剔除 xx7F 一条线。总计 23940 个码位(见下图所示),共收入 21886 个汉字和图形符号,其中汉字(包括部首和构件)21003 个,图形符号 883 个。

image003

图40.1.1.1 各种中文编码方式的前两位

从图中可以看到,不同颜色表示了不同字符集的来源和作用:
1)ASCII字符(橙色区域):代表标准的ASCII字符,位于0x00到0x7F。
2)GB2312字符(浅蓝色区域):这是GBK的一个子集,包括6763个汉字和一些符号。
3)GBK扩展字符(绿色区域):GBK编码在GB2312基础上进行了扩展,涵盖了更多汉字和符号,使得首尾字节范围更大。
4)GB18030-2000扩展字符(红色区域)和GB18030-2005扩展字符(紫色区域):GB18030标准进一步扩展了字符集,主要用于兼容繁体字、少数民族文字和其他非汉字符号。
从上图可知,GBK编码中,高字节(即首字节)的范围为0x81到0xFE,因此总共有0xFE - 0x81 + 1 = 126个区。每个区内包含190个汉字,这个数量是通过计算尾字节(即第二个字节)的范围得到的。尾字节的范围是0x40到0x7E和0x80到0xFE,分别包含0x7E - 0x40 + 1 = 63个字符和0xFE - 0x80 + 1 = 127个字符。合起来,每个区内共有63 + 127 = 190个汉字。因此,GBK编码中,总共有126 × 190 = 23940个汉字。下图为全国信息技术标准化技术会员会汉字内码扩展规范(GBK)。

image005

图40.1.1.2 GBK的81区内的汉字截图

从上图可以看到,我们的第一个汉字GBK编码为0x8140,即“丂”字开始。在GBK编码中,计算每个区内汉字的数量可以通过分析行和列的范围来进行。具体来说,首字节的范围从0x81到0xFE,这对应了16行,而尾字节的范围从0x40到0x7E和0x80到0xFE,这对应了12列。因此,按行列数相乘,每个区理论上包含192个汉字(16 行 × 12 列)。不过,需要注意的是,0x817F和0x81FF这两个位置没有对应的汉字,因此在最终计算时需要剔除这两个位置。经过调整后,得出每个区内实际的汉字数量为190个,这与之前的计算结果一致。依次对所有汉字进行编码,详见:www.qqxiuzi.cn/zh/hanzi-gbk-bianma.php。

40.1.2 汉字字模原理介绍
在上小节中,我们已经了解了汉字编码的相关知识,但这些知识并不足以让微控制器(MCU)在LCD上显示汉字,因为LCD显示的汉字不仅需要对应的编码,还需要与该编码相对应的点阵数据。通过这些点阵数据,单片机可以逐个像素点地绘制汉字,从而将其显示在屏幕上。所有汉字点阵数据的集合被称为汉字字库。由于不同大小的汉字需要不同的点阵表示,因此我们会有多种不同大小的汉字字库,如:12×12字库、16×16字库、24×24字库等。
每个汉字的点阵数据,通常被称为字模。汉字在液晶显示屏上的展示本质上是通过显示和不显示一些点来完成的。这种方式可以类比为“画笔”,通过“画笔”经过的地方来显示点,而未经过的地方则不显示。为了实现字模的提取和点阵显示,我们通常规定一个统一的取模方向。取模和描点操作均遵循此方向,能够确保点阵数据的提取与显示能够精确进行。
接下来,笔者以12×12大小的“好”字为例,演示如何使用正点原子的字模生成软件ATK-XFONT来生成字模数据。在生成字模时,我们首先需要设置一些关键参数:
1)点阵格式:选择阴码格式。阴码是指通过1表示需要显示的点,通过0表示不显示的点,通常用于LCD等显示设备。
2)取模走向:选择顺向(即高位在前)。这意味着每个字节的高位(bit7)表示该列的第一个点。
3)取模方式:选择逐列方式。这意味着我们按照从左到右、从上到下的顺序来取模,每列的数据由两个字节表示,逐列提取字模。
生成字模时,软件会将汉字“好”字的点阵数据按照这些设置生成(见下图所示),并展示在软件界面上。接着,生成的字模数据可以通过字节数组形式保存,以便用于LCD显示。

image007

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

最后,在软件下生成了点阵数据组成12*12大小的“好”字的字模,如下图所示。

image009

图40.1.2.3 “好”字字模

在图中,第一个字节为0x11,表示图41.1.2.2中第一个列的第一个字节的点阵数据,1表示该位置要画点,0表示不画点。第二个字节0x10表示第一个列的第二个字节的点阵数据。第三个字节0x1E则代表第二列的第一个字节的点阵数据。如此类推,每列数据由两个字节表示,共有12列,每列两个字节,总共24个字节。这24个字节完整描述了一个12×12汉字的点阵数据,最终可以通过这些数据在LCD屏上准确显示出对应的汉字图案。
了解了汉字字莫的原理后,我们可以推导出如何使用整个汉字库来显示任意汉字。为了显示一个汉字,首先需要知道该汉字对应的点阵数据。整个GBK字库包含了超过2万个汉字的点阵数据,这些数据通常由专门的软件生成。接下来的小节,笔者着重讲解汉字字库的制作过程。
40.1.3 汉字字库的制作
字库的制作,我们用到一款由正点原子团队开发的软件:ATK_XFONT。该软件可以在WINDOWS系统下生成任意点阵大小的ASCII,GB2312(中文)、GBK(中文)、BIG5(繁体中文)和Unicode等共十几种编码的字库,不但支持生成二进制文件格式的文件,还支持生成图片功能,并支持横向,纵向等多种扫描方式。该软件的默认界面如下图所示:

image005

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

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

image007

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

在电脑端,字体大小与点阵大小之间的关系为:

image015

其中,fsize 是电脑端字体的大小,dsize 是点阵大小(如12、16、24等)。例如,16×16点阵大小对应的是12号字体。
生成字库后,我们需要手动修改文件名和后缀。将生成的文件名改为 GBK16.FON,这里的后缀需要手动修改为.FON。同样的方法,也可以生成12×12和24×24点阵字库,分别命名为GBK12.FON和GBK24.FON,总共制作三个字库。
40.1.4 汉字显示原理介绍
经过以上三个小节的学习,我们可以总结出汉字显示的过程:
MCU汉字编码汉字字库汉字点阵数据描点LCD
编码和字库的制作已经讲解过,那么剩下的问题就是:如何通过汉字编码在字库中查找对应汉字的点阵数据?

1,汉字点阵数据定位方法
根据GBK编码规则,我们的汉字点阵字库是按照该编码规则从0x8140开始逐一建立的。每个区的点阵数据大小为每个汉字所用字节数乘以190。因此,我们可以根据这个编码规则定位字库中某个汉字的点阵数据。
具体的计算方法如下:
1)当GBKL(低字节) < 0x7F时:

image017

2)当GBKL(低字节) > 0x7F时:

image019

在汉字字库中,我们使用GBKH表示汉字的高字节,GBKL表示汉字的低字节,csize表示单个汉字点阵数据的大小(即字节数),Hp表示该汉字点阵数据在字库中的起始地址(如果字库的存储位置不是从0开始,则需要加上对应的偏移量foffset)。汉字点阵的大小(csize)取决于点阵的尺寸,即每个汉字的长宽,如:12x12、16x16、24x24 等。公式如下:,

image021

经过上述的公式,我们可以计算每一个字体汉字的点阵大小,下面我们来看一下12、16、24、和32号字体的单个汉字点阵大小,如下表所示。

QQ截图20260728095843

表40.1.4.1 常用的字体单个汉字点阵大小

大家不妨使用正点原子的字模生成软件ATK-XFONT来生成不同字体大小的字模数据,就可以知道每一个汉字所占用的点阵大小了。
很多读者可能会对Hp计算公式的来源感到疑惑。为了更好地理解,我们通过两个具体的汉字来讲解。我们选择的汉字分别是“丂”和“亐”,它们的GBK编码分别为0x8140和0x8180。根据GBK编码规则,这两个汉字都位于字库的第一区,其中0x8140是第一区的第一个汉字,而0x8180则紧跟在0x817F之后。接下来,我们将通过Hp计算公式,计算这两个汉字在字库中的偏移量,假设使用12号字体。
对于“丂”字,CBKH为0x81,CBKL为0x40,csize为24。由于CBKL小于0x7F,偏移量的计算公式为:Hp = ((GBKH - 0x81) * 190 + GBKL - 0x40) * csize。代入数据后,我们得到该字的偏移量为0,因此该汉字的点阵数据位于字库的首地址。
而“亐”字的CBKH为0x81,CBKL为0x80,csize依然为24。由于CBKL大于0x7F,偏移量的计算公式变为:Hp = ((GBKH - 0x81) * 190 + GBKL - 0x41) * csize。计算得出该字的偏移量为1512,这意味着它的点阵数据位于字库地址偏移1512的位置。为了验证计算的正确性,我们可以逆向计算。一个12号字体的汉字点阵大小为24,因此亐字前面的63个汉字(见上图41.1.1.2)占据了63*24=1512个字节,这与偏移量相符。
以下是正点原子为获取多个字号汉字点阵的实现代码。

/**
 * @brief     	获取单个汉字点阵数据
 * @param   	code  : 当前汉字编码(GBK码)
 * @param      	mat   : 当前汉字点阵数据存放地址
 * @param    	size  : 字体大小
 * @retval    	无
 */
static void text_get_hz_mat(unsigned char *code, unsigned char *mat, 
uint8_t size)
{
    unsigned char qh, ql;
    unsigned char i;
    unsigned long foffset;
    uint8_t csize;
    /* 计算字体一个字符对应点阵集所占的字节数 */
    csize = (size / 8 + ((size % 8) ? 1 : 0)) * (size);	
    qh = *code;                        	/* 获取GBK汉字的高位字节 */
ql = *(++code);                       	/* 获取GBK汉字的低位字节 */
/* 非常用汉字 */
    if ((qh < 0x81) || (ql < 0x40) || (ql == 0xFF) || (qh == 0xFF)) 
    {
        for (i = 0; i < csize; i++)
        {
            *mat++ = 0x00;           		/* 填充满格 */
        }
        return;
    }
    
    if (ql < 0x7F) 		/* 判断当前汉字GBK编码低位字节是否在0x7F之前 */
    {
        ql -= 0x40;
    }
    else   				/* 判断当前汉字GBK编码低位字节是否在0x7F之后 */
    {
        ql -= 0x41;
    }
    qh -= 0x81;       	/* 高位字节减去第一个汉字首地址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;
        }
    }
}

正如上面所描述的,这个函数的计算方法正是基于我们之前讲解的公式来计算字库偏移量,并根据此偏移量读取单个汉字的点阵数据。该函数的实现核心思想是根据传入的 汉字编码和字体大小来计算该汉字在字库中的偏移量,然后根据偏移量从字库中读取对应的汉字点阵数据。

2,汉字点阵描点至LCD上
之前,我们已经知道了如何获取对应的汉字点阵数据。接下来,我们怎么把汉字点阵数据绘制在LCD显示屏上呢?如下代码所示:

/**
 * @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, uint32_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_point(x, y, color);            /* 画需要显示的点 */
            }
            else if (mode == 0) 	/* 如果非叠加模式,不需要显示的点用背景色填充 */
            {
                lcd_draw_point(x, y, 0xFFFF);           /* 填充背景色 */
            }
            
            temp <<= 1;
            y++;
            if ((y - y0) == font_size)
            {
                y = y0;
                x++;
                break;
            }
        }
    }
    
    free(dzk);
}

/**
 * @brief     	在指定位置开始显示一个字符串
 * @note      	该函数支持自动换行
 * @param      	x,y   : 起始坐标
 * @param    	width : 显示区域宽度
 * @param    	height: 显示区域高度
 * @param      	str   : 字符串
 * @param     	size  : 字体大小
 * @param     	mode  : 显示模式
 * @note                0, 正常显示(不需要显示的点,用LCD背景色填充,即g_back_color)
 * @note                1, 叠加显示(仅显示需要显示的点, 不需要显示的点, 不做处理)
 * @param     	color : 字体颜色
 * @retval    	无
 */
void text_show_string(uint16_t x, uint16_t y, uint16_t width, uint16_t height, 
char *str, uint8_t size, uint8_t mode, uint32_t color)
{
    uint16_t x0 = x;
    uint16_t y0 = y;
    uint8_t bHz = 0;                       		/* 字符或者中文 */
    uint8_t *pstr = (uint8_t *)str;        		/* 指向char*型字符串首地址 */

    while (*pstr != 0)                      		/* 数据未结束 */
    {
        if (!bHz)
        {
            if (*pstr > 0x80)              		/* 中文 */
            {
                bHz = 1;                     		/* 标记是中文 */
            }
            else                             		/* 字符 */
            {
                if (x > (x0 + width - size / 2)) 	/* 换行 */
                {
                    y += size;
                    x = x0;
                }
                
                if (y > (y0 + height - size))   	/* 越界 */
                {
                    break;
                }
                
                if (*pstr == 13)                	/* 换行符号 */
                {
                    y += size;
                    x = x0;
                    pstr++;
                }
                else
                {
                    /* 有效部分写入 */
lcd_show_char(x, y, *pstr, size, mode, color);  
                }
                
                pstr++;
                x += size / 2;         	/* 英文字符宽度,为中文汉字宽度的一半 */
            }
        }
        else                            	/* 中文 */
        {
            bHz = 0;                     	/* 有汉字库 */
            
            if (x > (x0 + width - size)) 	/* 换行 */
            {
                y += size;
                x = x0;
            }
            
            if (y > (y0 + height - size)) 	/* 越界 */
            {
                break;
            }
            /* 显示这个汉字,空心显示 */
            text_show_font(x, y, pstr, size, mode, color);	
            pstr += 2;
            x += size;              		/* 下一个汉字偏移 */
        }
    }
}

text_show_font函数用于在LCD屏幕上显示指定大小的汉字,支持正常显示和叠加显示两种模式。通过传入汉字的GBK编码、字体大小、显示模式以及颜色等参数,可以在屏幕的指定位置绘制汉字。而text_show_string函数则用于在指定区域内显示一段字符串,支持汉字与英文字符的混合显示。该函数通过地址偏移依次获取字符串中的字符,并判断字符类型:如果是汉字,则调用text_show_font函数绘制该汉字的点阵数据;如果是英文字符,则通过lcd_show_char函数绘制。

40.2 硬件设计
40.2.1 程序功能
程序启动时,会从TF卡读取字库并将其存储到storage分区。接着,通过分区表API获取对应的汉字点阵数据,并将其显示在LCD屏幕上。如果按下板载的BOOT按键,程序会重新从TF卡更新字库到storage分区,并重新加载显示字库中的汉字点阵数据。

40.2.2 硬件资源
1)LED灯
LED 0 - IO51
2)KEY按键
BOOT - IO35
3)RGBLCD/MIPILCD(引脚太多,不罗列出来)
4)SPIFFS
5)SD卡
CMD - IO44
CLK - IO43
D0 - IO39
D1 - IO40
D2 - IO41
D3 - IO42

40.2.3 原理图
SD原理图已在38.2.3小节中详细阐述,为避免重复,此处不再赘述。

40.3 程序设计
40.3.1 分区表的IDF驱动
分区表外设驱动位于ESP-IDF的components\esp_partition目录。该目录中的include文件夹存放分区表相关的头文件,声明了分区表函数和结构体等;而partition_target.c文件是实现分区表的读写等操作函数。要使用分区表功能,必须先导入以下头文件。

#include "esp_partition.h"

对于分区表的操作函数,笔者已在第七章分区表管理和使用章节中介绍过,此处不再赘述。

40.3.2 程序流程图

image024

图40.3.2.1 汉字显示实验程序流程图

40.3.3 程序解析
在31_chinese_display例程中,作者在31_chinese_display\components\Middlewares路径下新建TEXT文件夹,并在TEXT文件夹下新建了CMakeLists.txt文件。其中,TEXT文件夹用于存放TEXT组件驱动,而CMakeLists.txt文件则用于将TEXT组件添加至构建系统,以便项目工程能够使用TEXT组件功能。
1,TEXT驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。TEXT驱动源码包括四个文件:text.c、text.h、fonts.c和fonts.h。
汉字显示实验代码主要分为两部分:一部分是字库的更新,另一部分是汉字的显示。字库的更新代码位于font.c和font.h文件中,汉字的显示代码位于text.c和text.h中。
接下来,我们将介绍关于字库操作的代码,首先我们来看看fonts.h文件中定义的字库信息结构体,其代码如下:

/* 字库信息结构体定义
 * 用来保存字库基本信息,地址,大小等
 */
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字节,用于记录字库的地址和大小等信息。其中,首字节指示字库状态,其余字节记录地址和文件大小。用户字库等文件存储在分区表的storage分区。前 33字 节 保 留 给_font_info, 以 保 存 其 结 构 体 数 据 ; 随 后 是UNIGBK.BIN、GBK12.FON、GBK16.FON和GBK24.FON文件。
下面介绍font.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(fonts_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(pdMS_TO_TICKS(10));
    }

    if (ftinfo.fontok != 0XAA)
    {
        return 1;
    }
    
    return 0;
}

这里就是把storage分区首地址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, uint32_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);
            /* copy src内容到pname */
strcpy((char *)pname, (char *)src); 
/* 追加具体文件路径 */                                     
            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, uint32_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 *)malloc(sizeof(FIL));  /* 分配内存 */

    if (fftemp == NULL)rval = 1;

    tempbuf = malloc(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.BIN */
/* UNIGBK之后,紧跟GBK12字库 */
                ftinfo.f12addr = ftinfo.ugbkaddr + ftinfo.ugbksize; 
                ftinfo.gbk12size = fftemp->obj.objsize; 	/* GBK12字库大小 */
                flashaddr = ftinfo.f12addr; 				/* GBK12的起始地址 */
                break;

            case 2:                                     	/* 更新 GBK16.BIN */
/* GBK12之后,紧跟GBK16字库 */
                ftinfo.f16addr = ftinfo.f12addr + ftinfo.gbk12size; 
                ftinfo.gbk16size = fftemp->obj.objsize; 	/* GBK16字库大小 */
                flashaddr = ftinfo.f16addr;           		/* GBK16的起始地址 */
                break;

            case 3:                        	/* 更新 GBK24.BIN */
/* 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已经帮我们实现了。这里用一个例子说明:

image025

在这里可以看出VSCode识别汉字的方式是GBK码,换句话来说就是VSCode自动会把汉字看成是两个字节表示的东西。知道了要表示的汉字和其GBK码,那么就可以去找对应的点阵数据。在text.c文件中,我们就定义了一个获取汉字点阵数据的text_get_hz_mat函数和显示一个汉字text_show_font函数。这两个函数笔者已经在40.1.4小节中讲解了。

2,CMakLists.txt文件
本例程的功能实现主要依靠TEXT组件。要在main函数中,成功调用TEXT文件中的内容,就得需要修改Middlewares/TEXT文件夹下的CMakeLists.txt文件,修改如下:

list(APPEND srcs    fonts.c
                    text.c)

idf_component_register(SRCS "text.c" "${srcs}"
                    INCLUDE_DIRS "."
                    REQUIRES esp_partition
                             spi_flash
                             fatfs
                             BSP)

3,main.c驱动代码
在main.c里面编写如下代码。

void app_main(void)
{
    esp_err_t ret;
    uint8_t t = 0;
    uint8_t key = 0;
    uint32_t fontcnt = 0;
    uint8_t i = 0;
    uint8_t j = 0;
    uint8_t fontx[2] = {0};
    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());
        ESP_ERROR_CHECK(nvs_flash_init());
    }

    led_init();             	/* LED初始化 */
    key_init();             	/* KEY初始化 */
    lcd_init();             	/* LCD屏初始化 */

    while (sdmmc_init())    	/* 检测不到SD卡 */
    {
        lcd_show_string(30, 110, 200, 16, 16, "SD Card Error!", RED);
        vTaskDelay(pdMS_TO_TICKS(200));
        lcd_fill(30, 110, 239, 126, WHITE);
        vTaskDelay(pdMS_TO_TICKS(200));
    }

    while (fonts_init())    	/* 检查字库 */
    {
UPD:
        lcd_clear(WHITE);

        lcd_show_string(30, 30, 200, 16, 16, "ESP32-P4", 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(pdMS_TO_TICKS(200));
            lcd_fill(20, 50, 200 + 20, 90 + 16, WHITE);
            vTaskDelay(pdMS_TO_TICKS(200));
        }

        lcd_show_string(30, 50, 200, 16, 16, "Font Update Success!   ", RED);
        vTaskDelay(pdMS_TO_TICKS(1000));
        lcd_clear(WHITE);
    }
   
    text_show_string(30, 30,  200, 16, "正点原子ESP32-P4开发板", 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, "BOOT: 更新字库", 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*16)为:", 16, 0, BLUE);
    text_show_string(30, 220, 200, 12, "对应汉字(12*12)为:", 12, 0, BLUE);

    while (1)
    {
        fontcnt = 0;
        /* GBK内码高字节范围为0x81~0xFE */
        for (i = 0x81; i < 0xFF; i++)               
        {
            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(pdMS_TO_TICKS(1));
                    
                    key = key_scan(0);
                    
                    if (key == BOOT)
                    {
                        goto UPD;       /* 跳转到UPD位置(强制更新字库) */
                    }
                }
                
                LED0_TOGGLE();
            }
        }
    }
}

app-main函数实现了我们在硬件设计例程功能所表述的一致,至此整个软件设计就完成了。

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

image027

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

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

image029

图40.4.2 汉字字库更新界面

posted @ 2026-07-31 17:50  正点原子  阅读(6)  评论(0)    收藏  举报