侧边栏切换

在ESP32模块内置Flash中烧写和使用中文字库的方法

最后编辑于: 2023-03-26 16:11  |  分类: 嵌入式软件开发  |  标签: ESP32 中文字库   |  浏览数: 1956  |  评论数: 0


0. 前言

用ESP32作了个LVGL界面的项目, 自然少不了中文显示, 要显示中文自然需要中文字库.

这个中文字库一般会有3种存在方式:

  1. 大数组的形式直接写在代码里. 这种适用于需要的汉字个数较少的情况;如果把几千个常用汉字都搞成大数组写在code里的话, 一方面code编译生成的bin会超大, 另一方面 你不觉得这种方式太野蛮太不优雅了吗?(不过这种方式, 程序读取字体数据的速度倒是挺快)
  1. 直接烧写在flash里. 也分两种情况, 一种是烧在ESP32模块内部flash里, 一种是在外部flash芯片里. 这种方式, 读取字体数据的速度也很快.
  1. 以文件的形式存在文件系统里. 当然, 这个文件系统也是存在于flash上, 相当于把第2种方式套了一层文件系统的壳. 据官方文档描述, 由于每次读取字体数据时都需要通过文件系统API, 速度较慢, 会引起LVGL界面显示卡顿. 所以此方式, 我们不考虑.

由于我们选用的ESP32模块是16MB版本的, 模块内置flash的存储空间绰绰有余, 所以我们选用将中文字库烧写在模块内部flash的方式.

这样, 既保证了读取速度, 成本也增加不了多少(毕竟外置flash芯片也要钱, 还增加板上面积).

1. 字库生成

先来生成字库, 字库生成使用LvglFontTool软件, 官方下载地址, 绿色版软件, 直接解压即可使用. 感谢作者

软件运行如上图, 软件的使用方法很简单, 稍稍摸索一下就会了, 软件下载地址也有说明, 我这里就不赘述了.

我导入了6千多个常用汉字以及一些字母符号, 使用32像素高的字体, 字体名设为font_cn_32,

点击右下的“开始转换”按钮, 软件会生成2个文件:

  1. font_cn_32.bin: 字库bin文件, 需要烧写入flash中的文件, 大小约为3.45MB
  1. font_cn_32.c:供LVGL调用的字体接口API函数C文件, 本文后面还会需要对其进行小小的修改

(注意: 软件中填写的字体名不同, 生成的2个文件名也随之改变)

2. 字库的烧写

好了, 字库文件已经有了, 终于到了本文的正题——字库烧写了.

先别急, 说到烧写字库, 是不是要想想, 烧写的地址是多少?烧进去的数据又如何读取出来呢?

烧进去读不出来不也没用, 所以说到如何烧写字库, 应该想的是 如何烧写和读取数据?

我们在前言里已经计划好了, 把字库烧写在ESP32模块内置flash中, 那么问题就变成了, 如何读取内置flash里的数据, 如何向内置flash里写入数据?

2.1 分区表

了解ESP32开发的朋友可能都知道, ESP32模块的内置flash, 乐鑫官方是以名为分区表的形式, 进行组织管理的(它还真的类似于Windows的硬盘分区).

乐鑫官网文档 API指南 >> 分区表 章节中有详细的介绍, 本文截取部分内容介绍一下.

2.1.1 概述

每片 ESP32 的 flash 可以包含多个应用程序, 以及多种不同类型的数据(例如校准数据、文件系统数据、参数存储数据等). 因此, 我们在 flash 的 默认偏移地址 0x8000 处烧写一张分区表(注意:分区表是最终被烧写入flash里的).

分区表的长度为 0xC00 字节, 最多可以保存 95 条分区表条目. MD5 校验和附加在分区表之后, 用于在运行时验证分区表的完整性. 分区表占据了整个 flash 扇区, 大小为 0x1000 (4 KB). 因此, 它后面的任何分区至少需要位于 (默认偏移地址) + 0x1000 处.

2.1.2 预定义的内置分区表

要了解分区表, 最简单的方法就是打开项目配置菜单(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个条目.

再来看 “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.1.3 关于分区表需要注意的点

通过前面2个预定义分区表, 我们对分区表有了一个直观粗浅的认识, 详细了解还请参看官方文档.

这里只列出几个需关注的点:

2.1.4 自定义分区表

好了, 对分区表有一定的认识了. 为了把中文字库写入内置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,

下面说明一下,

2.2 配置menuconfig

自定义的分区表在电脑上是以.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文件.

2.3 修改C文件

字库数据写在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文件加入到工程里, 至于如何添加, 就不是本文的范畴了.

2.4 烧写字库

2.4.1 工程编译

现在, 我们分区表设定好了, 代码也改好了, 可以编译了.

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'

2.4.2 烧写

终于到了心心念念的烧写字库这个步骤了.

前面我们编译成功后, 最后的输出中, 可以看到:

要么用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

3. 字库的使用

至于如何在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, 这样就可以了.


上一篇: linux内核container_of()宏

下一篇: makefile中关于all和.phony