将LVGL的中文字体编译为文件写入Flash中并读取
LVGL的字体系统提供了包括抗锯齿在内的许多高级功能,但字体的尺寸也变得非常大,会为下载带来不小的困难。本文介绍一种方法,将LVGL官方的字体转换工具生成的.c
字体文件,转换为可以烧录到Flash中的.bin
文件,并在EPS32平台上读取的方法。
LVGL对字体的支持情况
LVGL官方目前支持两种字体,一种是.c
源文件形式的字体,另一种则是.bin
格式的字体,这两种字体都可以通过官方提供的lv_font_conv工具生成。关于工具的使用,下文也会有详细介绍。
.c
格式字体
.c
格式的字体可以在编译后直接跟随用户程序一同烧入MCU之中,并使用lvgl
的原生接口读取并显示。.c
格式的字体中,所有数据都是使用lvgl
标准的数据结构定义的,因此程序可以直接通过寻址的方式读取这些字体,不需要进行任何其他的操作,开销较低,适合MCU平台使用。
.bin
格式字体
.bin
格式的字体则使用了与lvgl
标准的结构体定义不同的数据结构进行存储。这样虽然可以缩小文件体积,但lvgl
内部是无法直接读取.bin
格式的字体的。lvgl
的做法是通过载入函数,将.bin
格式的文件,在内存中转换为与.c
相同格式的数据结构,并使用.c
格式的接口进行读取与显示。这种载入方式则更适合Linux
等内存较大的平台。
字体格式选择
基于上述两种字体的特性,为了读取的便利性与运行性能,使用.c
格式的字体是比较好的选择。但是.c
格式的字体会为每次下载增加数M的数据大小,大大降低了调试时的体验。
本文将介绍如何生成.c
格式的字体,并将字体数据放入Flash中,避免反复下载。
生成字体
生成字体需要使用lvgl
官方推出的命令行工具lv_font_conv,该工具使用javascript
编写。
安装Node.js
为了使用这个工具,首先要在本地安装Node.js
解释器。您可以前往官方网站进行下载,选择最新的LTS版即可。
安装lv_font_conv
安装完毕后,打开Powershell
/Git Bash
等命令行工具,输入
npm install lv_font_conv -g
安装lv_font_conv
工具。安装完毕后,在命令行直接输入lv_font_conv
命令,若可以显示命令行帮助,则代表安装成功。
使用lv_font_conv
生成字体
官方提供的lv_font_conv
工具支持所有lvgl
字体系统的高级特性,例如多像素抗锯齿、RGB次像素渲染、字体压缩等。
打开你喜欢的终端并进入存放字体的目录中,本文均以HarmonyOS Sans字体为例。假定字体文件的文件名为HarmonyOS_Sans_SC_Regular.ttf
,输入下面的命令可以生成一个包含所有ASCII、汉字以及符号的.c
格式字体文件。
lv_font_conv --bpp 4 --size 20 -o font_harmony_sans_20.c --format lvgl --no-kerning --no-prefilter --font HarmonyOS_Sans_SC_Regular.ttf --range 0x20-0xBF --range 0x3000-0x3011 --range 0x4E00-0x9FAF --range 0xFF00-0xFF64
其中,--bpp
参数指定生成的字体中,1个显示像素使用多少字体像素。当此参数大于1时,代表启用灰度抗锯齿,字体的边缘将更加清晰,但是会占用更大的空间。为了显示效果,建议使用4像素抗锯齿。
--size
参数指定生成字体的高度,可根据需要自行选择。
--format
参数指定生成字体的格式,共有lvgl
与bin
两种格式可选,分别对应生成.c
格式与.bin
格式的字体。
--no-kerning
参数用于关闭字体可变间距的功能。此功能在汉字显示过程中效果不明显,且会带来额外的查表时间并占用更多的存储空间,建议关闭。
--font
参数用于指定使用的字体文件。其后所接的所有--range
参数与--symbols
参数均只对此字体起效,直到出现下一个--font
参数之前。
--range
参数用于指定要添加进入字体的Unicode字符范围,可以重复使用以指定多个范围。在本例中,0x20-0xBF
包含所有ASCII字符与一些增补字符(例如©
),0x3000-0x3011
包含一些CJK括号(例如《
【
等),0x4E00-0x9FAF
包含所有简体繁体与日文汉字,0xFF00-0xFF64
包含全角英文数字与标点(例如、
0
)等。在此基础上,还可以根据需要自行添加或删除字体。下表包含了一些常用的Unicode字符范围。
编码范围 | 字符数量 | 编码内容 |
---|---|---|
0x0020-0x007F | 96 | ASCII码 |
0x0080-0x00BF | 64 | ASCII码增补(™ © ) |
0x2600-0x26FF | 256 | 各种符号(☆ ♂ ♫ ♡ ) |
0x3000-0x301F | 32 | CJK符号(《 【 ) |
0x3040-0x309F | 94 | 日文平假名 |
0x30A0-0x30FF | 94 | 日文片假名 |
0x4E00-0x9FAF | 20902 | 所有简体、繁体、日文汉字 |
0xAC00-0xD7A3 | 11172 | 韩文音节 |
0xFF00-0xFF64 | 100 | 全角英文数字与标点 |
此外,还可以使用--symbols
参数来直接指定要添加的字,如下则演示生成一个只包含“一二三四五”五个汉字的字体:
lv_font_conv --bpp 4 --size 20 -o font_harmony_sans_20.c --format lvgl --no-kerning --no-prefilter --font HarmonyOS_Sans_SC_Regular.ttf --symbols 一二三四五
可以参考此文件以获得标准一级与二级汉字的列表。将列表中的汉字直接添加到--symbols
参数后即可制作只包含常用汉字的精简版字体。下文中将使用添加了标准一级二级汉字的字体作为演示。
lv_font_conv
还默认启用字体压缩功能,此功能用约30%的性能开销,换取了30%的空间节省。如无特殊需要,建议保持默认开启状态。使用ESP32显示字体
上面得到的.c
格式字体,可以直接加入工程中,编译并跟随可执行文件一同烧入。同时,.c
格式的字体,可以视为一个没有入口函数的源文件。因此,只需要用合适的方式编译它,即可生成二进制格式的文件以供烧入Flash。
下文将基于ESP32与ESP-IDF介绍如何将.c
格式的字体编译为二进制文件,通过工具直接烧录进ESP32的Flash中。
对源文件的处理
将字体文件复制到配置好lvgl
的工程根目录下(后续还需要用到原文件),此处以font_harmony_sans_20.c
文件作为演示。下文所提到的工程均基于向ESP32-S3移植LVGL一文中提到的工程。
由于编译需要使用到lvgl
的头文件,因此请确保工程目录下的components\lvgl\src
目录中包含lvgl.h
头文件。若您的工程路径与示例不同,请自行修改下述指令中的路径。
使用编辑器打开font_harmony_sans_20.c
文件,可以看到文件由数个部分组成,它们分别是:
- 文件开始处的宏与头文件引用
- BITMAPS (
glyph_bitmap
,保存字符的点阵信息) - GLYPH DESCRIPTION (
glyph_dsc
,用于保存字符的大小宽度等信息与在glyph_bitmap
中的偏移) - CHARACTER MAPPING (
cmaps
,用于查找字符在glyph
中的索引) - ALL CUSTOM DATA (
font_dsc
,针对.c
格式字体的存储自定义字体信息的数据结构) - PUBLIC FONT (
font_harmony_sans_20
,用户最终引用的字体结构)
从上述描述中可以看出,各个数据结构的引用关系是从下而上的。其中,占据最大空间的是glyph_bitmap
与glyph_dsc
。若字体中的字符不连续,则cmaps
还将包含对额外数组的引用以存储字符的偏移。与此同时,font_dsc
与font_harmony_sans_20
结构中也包含对其余结构的引用。
由于ESP32的MMU映射无法指定地址,因此凡是需要在链接中动态指定地址(即包含对其他数据结构的引用)的,都必须放入源文件中而不能静态编译为二进制文件烧录进Flash。所以,最优的解决方案是将glyph_bitmap
与glyph
两个没有引用其它数据结构的数据结构编译为二进制文件并烧入Flash,而保持其他数据结构存放在源文件中,在链接时动态指定地址。
对font_harmony_sans_20.c
,删除glyph_bitmap
定义前的所有代码,只保留#include "lvgl.h"
这一句代码。然后,删除除了glyph_bitmap
与glyph_dsc
之外的所有代码。修改后的源文件看起来像这样:
#include "lvgl.h"
static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
......
};
static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = {
......
};
编译生成二进制文件
首先,需要配置ESP-IDF的编译环境以使用编译器。如何安装并配置ESP-IDF,请参考基于VSCode搭建ESP-IDF开发环境一文,此处将按照文中的配置方法演示。
idf32
编译
然后,对刚刚修改的源文件进行编译。此处使用ESP32-S3的编译器,若使用其他型号的芯片,请自行更换对应的编译器,以保证二进制格式兼容。
xtensa-esp32s3-elf-gcc -c -D LV_CONF_SKIP -D LV_FONT_FMT_TXT_LARGE -I components\lvgl\src\ font_harmony_sans_20.c
上述指令中,-c
参数表示跳过连接部分,只进行编译操作。若不使用此参数,gcc会默认对可执行文件进行链接,但源文件中没有入口函数,因此会导致失败。
-D
参数用于添加全局宏定义。LV_CONF_SKIP
表示跳过lvgl的配置文件,LV_FONT_FMT_TXT_LARGE
表示这个字体中会含有很多字符,lvgl存储字符的数据结构会因此发生改变。
-I
参数表示添加头文件查找目录。该目录应该包含lvgl.h
头文件。
链接
上述操作会生成font_harmony_sans_20.o
文件,该文件需要进行链接才可以生成最终的二进制格式文件。
xtensa-esp32s3-elf-ld -T rodata_gen_bin.ld .\font_harmony_sans_20.o -o font_harmony_sans_20.out
-T
参数用于指定一个链接器脚本。rodata_gen_bin.ld
文件的内容如下,请复制并保存为对应的文件。该文件的作用主要是将.rodata
段放入地址开头0x0
的存储区域中。
SECTIONS {
. = 0x0;
.text : {
*(.rodata)
} = 0
}
链接过程中会产生警告,这是因为可执行文件没有入口函数导致的,无视即可。
生成bin文件
上述操作会生成font_harmony_sans_20.out
可执行文件,该文件内除了所需要的所有数据还包含了一些符号表等不需要的数据,接下来需要将它转换成不含任何符号信息的.bin
格式。
xtensa-esp32s3-elf-objcopy --output-target elf-xtensa-le -O binary -S font_harmony_sans_20.out font_harmony_sans_20.bin
其中,--output-target
参数用于指定目标架构,elf-xtensa-le
代表xtensa
架构的小端目标。
-O
参数用于指定输出格式,binary
即.bin
格式。
-S
参数用于指定输出文件(.bin
文件)不含任何符号信息。
执行完上述指令后,便获得了font_harmony_sans_20.bin
文件。
查看文件内偏移
最后,还需要获取该文件内两个数据结构对应的偏移,方便在用户程序中调用。
xtensa-esp32s3-elf-objdump -t font_harmony_sans_20.out
执行上述指令后,会得到一个类似于这样的打印信息:
font_harmony_sans_20.out: file format elf32-xtensa-le
SYMBOL TABLE:
00000000 l d .text 00000000 .text
00000000 l d .comment 00000000 .comment
00000000 l d .xtensa.info 00000000 .xtensa.info
00000000 l d .xt.prop 00000000 .xt.prop
00000000 l df *ABS* 00000000 font_harmony_sans_20.c
00000000 l O .text 000e443c glyph_bitmap
000e443c l O .text 0001a280 glyph_dsc
可以看到,glyph_bitmap
数据结构的偏移是0x00000000
,glyph_dsc
数据结构的偏移是0x000e443c
。
将字体放入ESP32的Flash中
烧入Flash
得到.bin
文件后,需要将其烧录入Flash中。在烧录之前,需要先更改ESP-IDF的分区表,将字体文件所在的偏移设置为一个分区。
在将要使用字体的工程目录下进入idf.py menuconfig
,进入Partition Table
→ Partition Table
,选择Custom partition table CSV
,保存退出。
然后,在工程根目录下新建partitions.csv
文件,填入下列内容:
# Name Type SubType Offset Size Flags
# bootloader 0x0 0x8000
# partition_table 0x8000 0x1000
nvs, data, nvs, 0x9000, 0x6000,
phy_init, data, phy, 0xf000, 0x1000,
factory, app, factory, 0x10000, 0x1F0000,
font_hs20, data, , 0x200000, 1042108,
相比默认的分区表,此分区表新增了一个font_hs20
分区,该分区位于Flash偏移2M处,大小为1042108
字节(可以通过计算glyph_bitmap
与glyph_dsc
结构大小的和得到)。
然后,便可以使用乐鑫官方提供的Flash Download Tool将.bin
文件烧入Flash的对应位置了。
显示字体
前文中将font_harmony_sans_20.c
文件中的glyph_bitmap
与glyph_dsc
两个数据结构编译成了二进制文件,现在介绍如何在用户程序中加载它们并显示。
修改字体源码
打开未经修改的font_harmony_sans_20.c
文件,同样删除glyph_bitmap
定义前的所有代码,只保留#include "lvgl.h"
这一句代码,同时删除glyph_bitmap
与glyph_dsc
两个结构体。这时的源文件中应该包含cmaps
font_dsc
font_harmony_sans_20
三个数据结构。
由于前面将glyph_bitmap
与glyph_dsc
两个数据结构置入了Flash中,这两个数据结构的地址将在Flash映射时动态设置。为了动态设置它们的地址,需要将代码中的static const lv_font_fmt_txt_dsc_t font_dsc
声明中的const
删除,并将glyph_bitmap
与glyph_dsc
的地址设置为NULL
。
修改完的字体文件由于内容较多,请自行前往文末下载工程查看。
映射Flash并加载字体
font_harmony_sans_20.c
文件中的glyph_bitmap
与glyph_dsc
的地址被设置为NULL
,需要在使用的过程中动态为其指定地址。在font_harmony_sans_20.c
文件末尾,添加一个函数,用于加载它们的地址:
void lv_port_font_harmony_sans_20_load(const char *partition_label)
{
/* 查找对应名称的分区 */
const esp_partition_t *partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_ANY, partition_label);
if(partition == NULL) {
ESP_LOGE(TAG, "Can't find %s partition!", partition_label);
abort();
}
spi_flash_mmap_handle_t map_handle;
const void *flash_offset;
/* 将分区通过MMU映射到内存中 */
ESP_ERROR_CHECK(
esp_partition_mmap(partition, 0, partition->size, SPI_FLASH_MMAP_DATA, &flash_offset, &map_handle)
);
ESP_LOGI(TAG, "mapped %s@%1p", partition->label, flash_offset);
font_dsc.glyph_bitmap = flash_offset;
font_dsc.glyph_dsc = flash_offset+0xe443c;
}
这个函数内部使用了ESP32的内存映射函数,将指定名称的Flash分区通过MMU进行映射,并返回MMU映射的地址。该地址可以被程序直接寻址,将地址加上查看文件内偏移中得到的偏移值赋给font_dsc
中的glyph_bitmap
与glyph_dsc
即可。
由于前文中启用了LV_FONT_FMT_TXT_LARGE
宏,因此在调用此字体的用户工程中,需要启用Enable it if you have fonts with a lot of characters.
设置项,来保证二进制格式兼容。与此同时,还需要在工程配置中打开Sets support for compressed fonts.
设置项来支持压缩过的字体。
上述的两个设置项均在Component config
→ LVGL configuration
→ Font usage
中。
最后,在显示字体之前,调用lv_port_font_harmony_sans_20_load()
函数加载字体即可。
lv_port_font_harmony_sans_20_load("font_hs20");
显示效果
使用如下代码,显示一段较长的文字,用来测试显示效果:
void lv_example_label(void)
{
lv_obj_t *label = lv_label_create(lv_scr_act());
lv_label_set_long_mode(label, LV_LABEL_LONG_WRAP);
lv_obj_set_width(label, 320);
lv_obj_set_style_text_font(label, &font_harmony_sans_20, 0);
lv_label_set_text(label, "ESP32-S3 是一款集成 2.4 GHz Wi-Fi 和 Bluetooth 5 (LE) 的 MCU 芯片,支持远距离模式 (Long Range)。 ESP32-S3 搭载 Xtensa® 32 位 LX7 双核处理器,主频高达 240 MHz,内置 512 KB SRAM (TCM),具有 45 个可编程 GPIO 管脚和丰富的通信接口。ESP32-S3 支持更大容量的高速 Octal SPI flash 和片外 RAM,支持用户配置数据缓存与指令缓存。ESP32-S3 集成 2.4 GHz Wi-Fi (802.11 b/g/n),支持 40 MHz 带宽;其低功耗蓝牙子系统支持 Bluetooth 5 (LE) 和 Bluetooth Mesh,可通过 Coded PHY 与广播扩展实现远距离通信。它还支持 2 Mbps PHY,用于提高传输速度和数据吞吐量。ESP32-S3 的 Wi-Fi 和 Bluetooth LE 射频性能优越,在高温下也能稳定工作。");
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
}