最后编辑于: 2023-03-26 16:11 | 分类: 嵌入式软件开发 | 标签: ESP32 中文字库 | 浏览数: 1955 | 评论数: 0
用ESP32作了个LVGL界面的项目, 自然少不了中文显示, 要显示中文自然需要中文字库.
这个中文字库一般会有3种存在方式:
由于我们选用的ESP32模块是16MB版本的, 模块内置flash的存储空间绰绰有余, 所以我们选用将中文字库烧写在模块内部flash的方式.
这样, 既保证了读取速度, 成本也增加不了多少(毕竟外置flash芯片也要钱, 还增加板上面积).
先来生成字库, 字库生成使用LvglFontTool软件, 官方下载地址, 绿色版软件, 直接解压即可使用. 感谢作者
软件运行如上图, 软件的使用方法很简单, 稍稍摸索一下就会了, 软件下载地址也有说明, 我这里就不赘述了.
我导入了6千多个常用汉字以及一些字母符号, 使用32像素高的字体, 字体名设为font_cn_32,
点击右下的“开始转换”按钮, 软件会生成2个文件:
font_cn_32.bin
: 字库bin文件, 需要烧写入flash中的文件, 大小约为3.45MBfont_cn_32.c
:供LVGL调用的字体接口API函数C文件, 本文后面还会需要对其进行小小的修改(注意: 软件中填写的字体名不同, 生成的2个文件名也随之改变)
好了, 字库文件已经有了, 终于到了本文的正题——字库烧写了.
先别急, 说到烧写字库, 是不是要想想, 烧写的地址是多少?烧进去的数据又如何读取出来呢?
烧进去读不出来不也没用, 所以说到如何烧写字库, 应该想的是 如何烧写和读取数据?
我们在前言里已经计划好了, 把字库烧写在ESP32模块内置flash中, 那么问题就变成了, 如何读取内置flash里的数据, 如何向内置flash里写入数据?
了解ESP32开发的朋友可能都知道, ESP32模块的内置flash, 乐鑫官方是以名为分区表的形式, 进行组织管理的(它还真的类似于Windows的硬盘分区).
乐鑫官网文档 API指南 >> 分区表 章节中有详细的介绍, 本文截取部分内容介绍一下.
每片 ESP32 的 flash 可以包含多个应用程序, 以及多种不同类型的数据(例如校准数据、文件系统数据、参数存储数据等). 因此, 我们在 flash 的 默认偏移地址 0x8000 处烧写一张分区表(注意:分区表是最终被烧写入flash里的).
分区表的长度为 0xC00 字节, 最多可以保存 95 条分区表条目. MD5 校验和附加在分区表之后, 用于在运行时验证分区表的完整性. 分区表占据了整个 flash 扇区, 大小为 0x1000 (4 KB). 因此, 它后面的任何分区至少需要位于 (默认偏移地址) + 0x1000 处.
要了解分区表, 最简单的方法就是打开项目配置菜单(idf.py menuconfig
), 在CONFIG_PARTITION_TABLE_TYPE
下选择一个预定义的分区表.
有2个预定义的内置分区表:
我们来看看这2个分区表的内容,
先看看 “Single factory app, no OTA” 这个分区表的内容, 如下:
# ESP-IDF Partition Table
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x6000,
phy_init, data, phy, 0xf000, 0x1000,
factory, app, factory, 0x10000, 1M,
一共3个条目.
Type
字段值为data
), 分别用于存储 NVS 库专用分区 和 PHY 初始化数据, 其具体意义超出本文主题太多, 请查阅官方文档. Type
字段值为app
), flash 的 0x10000 (64 KB) 偏移地址处存放一个name为 “factory” 的二进制应用程序, 启动加载器将默认加载这个应用程序. 再来看 “Factory app, two OTA definitions” 分区表的内容:
# ESP-IDF Partition Table
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x4000,
otadata, data, ota, 0xd000, 0x2000,
phy_init, data, phy, 0xf000, 0x1000,
factory, app, factory, 0x10000, 1M,
ota_0, app, ota_0, 0x110000, 1M,
ota_1, app, ota_1, 0x210000, 1M,
一共6个条目, 多了3个条目.
这里既然提到了 出厂应用程序 和 OTA应用程序, 就不得不说明一下:
ESP32启动, 会从 flash 的 0x1000 偏移地址处加载Bootloader, Bootloader会读取分区表, 并根据其中otadata(如果存在)的内容选择需要引导的应用程序 (app) 分区.
详细的请参见官方文档的 API 指南 >> 应用程序的启动流程 和 API 指南 >> 引导加载程序 (Bootloader), 以及API 参考 >> System API >> 空中升级 (OTA) 等章节.
通过前面2个预定义分区表, 我们对分区表有了一个直观粗浅的认识, 详细了解还请参看官方文档.
这里只列出几个需关注的点:
好了, 对分区表有一定的认识了. 为了把中文字库写入内置flash的分区内, 我们需要自定义分区表.
先给出我的自定义分区表:
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x4000,
otadata, data, ota, 0xd000, 0x2000,
phy_init, data, phy, 0xf000, 0x1000,
factory, app, factory, 0x10000, 2M,
ota_0, app, ota_0, 0x210000, 2M,
ota_1, app, ota_1, 0x410000, 2M,
font_cn_32, 0x50, 0x32, 0x610000, 4M,
下面说明一下,
自定义的分区表在电脑上是以.csv
文件的形式, 保存在工程根目录下, 比如我的自定义分区表文件为partitions.csv
.
我们在前面提到过分区表最终是被烧写到flash 的 默认偏移地址 0x8000 处, 因此csv
文件形式的分区表需要被二进制化, 才能被烧写.
我们在menuconfig
中选择“Custom partition table CSV”, 然后输入 分区表的csv文件名以及在工程中的路径, 即可.
实操一下, idf环境中, 输入idf.py menuconfig
命令:
在主界面下选择 Partition Table 分区表,
进入
再选择 Partition Table (Custom partition table CSV), 进入
选中 Custom partition table CSV (定制分区表CSV), 再回到上一层
在第二行选中, 可输入 定制分区表的 CSV文件名.
到此定制分区表的配置完毕.
另外提一下, 我们可在menuconfig中, 设置一下flash的size大小, 一定要和自己使用的模块一致, 如下图操作:
自定义分区表在menuconfig中配置好后, 后面编译工程执行idf.py build
时, 会自动将将csv分区表生成二进制bin文件.
字库数据写在flash什么地方已经安排好了, 现在要考虑怎么把flash里的字库数据读出来了.
回忆第1节中, 我们用字库生成软件 生成了 font_cn_32.c
文件, 其中有这么一段代码:
//static uint8_t __g_font_buf[714];//如bin文件存在SPI FLASH可使用此buff
static uint8_t *__user_font_getdata(int offset, int size){
//如字模保存在SPI FLASH, SPIFLASH_Read(__g_font_buf,offset,size);
//如字模已加载到SDRAM,直接返回偏移地址即可如:return (uint8_t*)(sdram_fontddr+offset);
return __g_font_buf;
}
需要在__user_font_getdata
函数体内, 写入实际的flash读数据的代码, 读出的数据放到buffer __g_font_buf
中(buffer数组的size是字库生成软件自动设定的, 和字体大小有关, 我们32的字体算较大的了, 所以buffer也不小.).
既然我们用了分区表, 乐鑫官方也提供了分区内数据读写的API函数, 参见官方文档API参考>>存储API>>分区API, 截取官方文档中的一段内容如下:
该组件在
esp_partition.h
中声明了一些 API 函数,用以枚举在分区表中找到的分区,并对这些分区执行操作:
- esp_partition_find():在分区表中查找特定类型的条目,返回一个不透明迭代器;
- esp_partition_get():返回一个结构体,描述给定迭代器的分区;
- esp_partition_next():将迭代器移至下一个找到的分区;
- esp_partition_iterator_release():释放 esp_partition_find() 中返回的迭代器;
- esp_partition_find_first():返回描述 esp_partition_find() 中找到的第一个分区的结构;
- esp_partition_read()、esp_partition_write() 和 esp_partition_erase_range() 等同于 esp_flash_read()、esp_flash_write() 和 esp_flash_erase_region(),但在分区边界内执行。
我们从flash分区中读数据, 最终只需要用到2个函数即可, esp_partition_find_first()
(用来找到我们的 字库 分区) 和 esp_partition_read()
(读出数据).
这两个函数的详细声明如下:
const esp_partition_t *
esp_partition_find_first (esp_partition_type_t type, esp_partition_subtype_t subtype, const char *label)
Find first partition based on one or more parameters.
参数:
返回: pointer to esp_partition_t
structure, or NULL if no partition is found. This pointer is valid for the lifetime of the application.
esp_err_t
esp_partition_read (const esp_partition_t *partition, size_t src_offset, void *dst, size_t size)
Read data from the partition.
Partitions marked with an encryption flag will automatically be be read and decrypted via a cache mapping.
参数:
返回: ESP_OK, if data was read successfully; ESP_ERR_INVALID_ARG, if src_offset exceeds partition size; ESP_ERR_INVALID_SIZE, if read would go out of bounds of the partition; or one of error codes from lower-level flash driver.
最终, 我们对font_cn_32.c
文件的修改如下:
#include "esp_partition.h"
...
...
...
static uint8_t __g_font_buf[714];//如bin文件存在SPI FLASH可使用此buff
static esp_partition_t* partition_font = NULL;
static uint8_t *__user_font_getdata(int offset, int size){
//如字模保存在SPI FLASH, SPIFLASH_Read(__g_font_buf,offset,size);
//如字模已加载到SDRAM,直接返回偏移地址即可如:return (uint8_t*)(sdram_fontddr+offset);
if( partition_font == NULL ) {
partition_font = esp_partition_find_first(0x50, 0x32, "font_cn_32");
assert(partition_font != NULL);
}
esp_err_t err = esp_partition_read(partition_font, offset, __g_font_buf, size);//读取数据
if(err != ESP_OK) {
printf("Failed to reading cn font date\n");
}
return __g_font_buf;
}
我们加了#include "esp_partition.h"
, 以便调用2个API函数.
通过分区的 Type值0x50, SunType值0x32 和 Name值"font_cn_32"
来找到我们的 字库分区,
代码很简单, 其他就没什么好说明的了.
记得把这个c文件加入到工程里, 至于如何添加, 就不是本文的范畴了.
现在, 我们分区表设定好了, 代码也改好了, 可以编译了.
idf环境里工程目录下, 执行idf.py build
编译成功, 最后会输出如下:
Project build complete. To flash, run this command:
C:\Users\admin\.espressif\python_env\idf4.3_py3.8_env\Scripts\python.exe
..\..\..\Users\admin\Desktop\esp-idf\components\esptool_py\esptool\esptool.py -p (PORT)
-b 460800 --before default_reset --after hard_reset --chip esp32s2
write_flash --flash_mode dio --flash_size detect --flash_freq 80m
0x1000 build\bootloader\bootloader.bin
0x8000 build\partition_table\partition-table.bin
0xd000 build\ota_data_initial.bin
0x10000 build\myapp.bin
or run 'idf.py -p (PORT) flash'
终于到了心心念念的烧写字库这个步骤了.
前面我们编译成功后, 最后的输出中, 可以看到:
要么用idf.py -p (PORT) flash
这个命令来烧写, 要么用那个"一长串的命令".
就是说这两者是等效的.
那个一长串的命令里, 不仅列出来很多烧写时的参数, 还列出了要烧写的各个bin文件及其开始地址, 如下:
0x1000 build\bootloader\bootloader.bin
0x8000 build\partition_table\partition-table.bin
0xd000 build\ota_data_initial.bin
0x10000 build\myapp.bin
看, 有bootloader, 分区表, ota_data初始值 (我打开看了全是0xFF) 和 我们的app 共4个bin文件,
它们前面的烧写地址也和本文前面所描述的预期地址一致.
但, 有个问题, 没有我们的字库bin文件.
没有我们就自己加上呗, 在最后加上0x610000 main\font_cn_32.bin
即可!
这样相当于修改了烧写命令, 就不能用idf.py -p (PORT) flash
这个命令来烧写了, 下面是我修改的烧写命令, 精简掉了python前的一大串路径(可惜esptool.py前的路径不能精简)
python ..\..\..\Users\admin\Desktop\esp-idf\components\esptool_py\esptool\esptool.py
-p COM3 -b 460800 --before default_reset --after hard_reset --chip esp32s2
write_flash --flash_mode dio --flash_size detect --flash_freq 80m
0x1000 build\bootloader\bootloader.bin
0x8000 build\partition_table\partition-table.bin
0xd000 build\ota_data_initial.bin
0x10000 build\myapp.bin
0x610000 main\font_cn_32.bin
注意几点:
用这个命令, 就可以在烧写程序的同时, 顺便把字库烧写进去了.
而且经过测试, 后续再用idf.py -p (PORT) flash
命令烧写更新程序, 也不会覆盖掉后面的字库分区, app烧写更新, 字库不受影响, 烧写一次会一直妥妥的在那里, nice.
如果仅烧写字库, 也可以使用下面的精简命令:
python ..\..\..\Users\admin\Desktop\esp-idf\components\esptool_py\esptool\esptool.py
-p COM3 write_flash 0x610000 main\font_cn_32.bin
至于如何在LVGL中显示汉字, 代码编写和显示英文差不多, 只是多个字体声明语句.
代码如下:
...
LV_FONT_DECLARE( font_cn_32 ); // 声明我们的中文字体, 如果代码里已声明过, 就不用再声明
...
lv_label_set_text(btnInfoLab, (LV_SYMBOL_HOME "你好 世界"));
lv_obj_set_style_local_text_font(btnInfoLab, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &font_cn_32);
...
OK, 这样就可以了.