LGVL配合FreeType为可变字体设置字重-ESP32篇(下)

前言

上篇中介绍了如何在ESP32上运行LVGL,现在终于可以开始奔着第一篇文章末尾的效果去了.

准备工作

软件准备

访问https://download.savannah.gnu.org/releases/freetype/下载源码,解压后重命名为freetype并复制到项目lib文件夹下
我下载的是目前(2021.12.11)最新版本freetype-2.11.1.tar.gz(较老的版本不支持可变字体操作)
然后去这个Repo获取SD和SPI库,复制到项目lib文件夹下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.
├── include
│ └── README
├── lib
│ └── README
│ └── lvgl_freetype
│ └── lvgl
│ └── freetype
│ └── SD
│ └── SPI
│ └── TFT_eSPI
├── platformio.ini
├── src
│ └── main.cpp
│ └── Port
│ └── lv_port_disp.cpp
│ └── lv_port_disp.h
└── test
└── README

到freetype文件夹下,创建FreeType的library.jsonlibrary.properties文件
library.properties内容如下

1
2
3
4
5
6
7
8
9
10
11
name=FreeType
version=2.11.1
author=David Turner, Robert Wilhelm, Werner Lemberg
maintainer=Werner Lemberg
sentence=A freely available software library to render fonts.
paragraph=It is written in C, designed to be small, efficient, highly customizable, and portable while capable of producing high-quality output (glyph images) of most vector and bitmap font formats.documentation.
category=Font
url=https://freetype.org/
architectures=*
repository=https://gitlab.freedesktop.org/freetype/freetype
license=GNU

library.json内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{
"name":"freetype",
"version": "2.11.1",
"description":"Software library to render fonts",
"keywords":"freetype",
"license": "FreeType License",
"repository": {
"type": "git",
"url": "https://gitlab.freedesktop.org/freetype"
},
"frameworks": "*",
"platforms": "*",
"build": {
"srcFilter": [
"+<base/ftsystem.c>",
"+<base/ftmm.c>",
"+<base/ftinit.c>",
"+<base/ftdebug.c>",
"+<base/ftbase.c>",
"+<base/ftbbox.c>",
"+<base/ftglyph.c>",
"+<base/ftbdf.c>",
"+<bdf/bdf.c>",
"+<cff/cff.c>",
"+<truetype/truetype.c>",
"+<sfnt/sfnt.c>",
"+<smooth/smooth.c>",
"+<cache/ftcache.c>",
"+<gzip/ftgzip.c>",
"+<base/ftbitmap.c>"
],
"flags": [ "-DFT2_BUILD_LIBRARY", "-I include" ],
"includeDir": "devel"
}
}

硬件准备

和上篇相比,多了一个SD卡模块.

起初也尝试了直接把字体文件存在SPIFFS里面,减少涉及到的硬件,奈何速度慢的感人,于是回到这里重新写了SD卡版本.

2022.02.08: 发现LVGL自8.1.1-dev(正式版为8.2.0)开始,内置的lv_freetype模块已经支持FT_New_Memory_Face方式创建字体了.
如果没有内存卡的话,请将字体文件命名为Lite.ttf放到源码src文件夹内,
在platformio.ini内添加

1
2
board_build.embed_txtfiles = 
src/Lite.ttf

并在main.cpp中添加

1
2
3
extern const uint8_t font_start[] asm("_binary_src_Lite_ttf_start");
extern const uint8_t font_end[] asm("_binary_src_Lite_ttf_end");
extern const size_t size = font_end - font_end - 1;

创建字体时候额外添加如下内容

1
2
3
4
5
static lv_ft_info_t info;
/**/
info.mem = font_start;
info.mem_size = size;
/**/

有SD卡的情况下

名称 数量 备注 图例
ESP32 开发板 1 \
1.54寸LCD 1 驱动ST7789,分辨率240x240
杜邦线若干 N \
Micro SD卡模块和卡 1 \

接线和点亮屏幕请去参考LGVL配合FreeType为可变字体设置字重-ESP32篇(上)

Micro SD卡模块的接线如下

ESP32引脚名称 Micro SD卡模块引脚名称
GND GND
G26 MISO
G13 MOSI
G14 SCK
G15 CS
5V(Vin) VCC

准备完毕,开干

启用LVGL的FreeType

将lv_conf.h内的宏定义内容, 并启用FTC_SBitCache_Lookup

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*FreeType library*/
#define LV_USE_FREETYPE 0
#define LV_USE_FREETYPE 1
#if LV_USE_FREETYPE
/*Memory used by FreeType to cache characters [bytes] (-1: no caching)*/
#define LV_FREETYPE_CACHE_SIZE (16 * 1024)
#if LV_FREETYPE_CACHE_SIZE >= 0
/* 1: bitmap cache use the sbit cache, 0:bitmap cache use the image cache. */
/* sbit cache:it is much more memory efficient for small bitmaps(font size < 256) */
/* if font size >= 256, must be configured as image cache */
#define LV_FREETYPE_SBIT_CACHE 1
/* Maximum number of opened FT_Face/FT_Size objects managed by this cache instance. */
/* (0:use system defaults) */
#define LV_FREETYPE_CACHE_FT_FACES 0
#define LV_FREETYPE_CACHE_FT_SIZES 0
#endif
#endif

0设置为1

配置FreeType的文件系统

创建一个命为ft_fs_port.cpp的文件,内容为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include "FS.h"
#include "SD.h"
#include "SPIFFS.h"

extern "C" {
typedef void lvbe_FILE;
lvbe_FILE *lvbe_fopen(const char *filename, const char *mode) {
File f = SD.open(filename, mode);
if (f) {
File *f_ptr = new File(f); // copy to dynamic object
*f_ptr = f; // TODO is this necessary?
return f_ptr;
}
return nullptr;
}
int lvbe_fclose(lvbe_FILE *stream) {
File *f_ptr = (File *)stream;
f_ptr->close();
delete f_ptr;
return 0;
}
size_t lvbe_fread(void *ptr, size_t size, size_t count, lvbe_FILE *stream) {
File *f_ptr = (File *)stream;
int32_t ret = f_ptr->read((uint8_t *)ptr, size * count);
if (ret < 0) { // error
ret = 0;
}
return ret;
}
int lvbe_fseek(lvbe_FILE *stream, long int offset, int origin) {
File *f_ptr = (File *)stream;
fs::SeekMode mode = fs::SeekMode::SeekSet;
if (SEEK_CUR == origin) {
mode = fs::SeekMode::SeekCur;
} else if (SEEK_END == origin) {
mode = fs::SeekMode::SeekEnd;
}
bool ok = f_ptr->seek(offset, mode);
return ok ? 0 : -1;
}
int lvbe_ftell(lvbe_FILE *stream) {
File *f_ptr = (File *)stream;
return f_ptr->position();
}
}

打开位于freetype文件夹下的devel\ft2build.h
并添加

1
2
3
4
5
6
7
8
9
10
#ifndef FT2BUILD_H_
#define FT2BUILD_H_

#define FT_CONFIG_MODULES_H <ftmodule.h>
#define FT_CONFIG_OPTIONS_H <ftoption.h>
#define FT_CONFIG_STANDARD_LIBRARY_H <ftstdlib.h>

#include <freetype/config/ftheader.h>

#endif /* FT2BUILD_H_ */

在同级目录下创建ftstdlib.h(文末Repo内有完整版)文件,内容从freetype\include\freetype\config\ftstdlib.h复制

修改file handling部分的内容为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  /**************************************************************************
*
* file handling
*
*/
#include <stdio.h>
typedef void lvbe_FILE;
extern lvbe_FILE * lvbe_fopen(const char * filename, const char * mode );
extern int lvbe_fclose(lvbe_FILE * stream);
extern size_t lvbe_fread(void * ptr, size_t size, size_t count, lvbe_FILE * stream);
extern int lvbe_fseek(lvbe_FILE * stream, long int offset, int origin );
extern int lvbe_ftell(lvbe_FILE * stream);

#define FT_FILE lvbe_FILE
#define ft_fclose lvbe_fclose
#define ft_fopen lvbe_fopen
#define ft_fread lvbe_fread
#define ft_fseek lvbe_fseek
#define ft_ftell lvbe_ftell
#define ft_sprintf sprintf

在同级目录下创建ftmodule.h,启用所需模块
内容为

1
2
3
FT_USE_MODULE( FT_Driver_ClassRec, tt_driver_class )
FT_USE_MODULE( FT_Module_Class, sfnt_module_class )
FT_USE_MODULE( FT_Renderer_Class, ft_smooth_renderer_class )

在同级目录下创建ftoption.h**, 内容从freetype\include\freetype\config\ftoptions.h复制,详细的配置过长,故此处省略,见文末Repo内**

此时FreeType的基本功能配置完成

主要的涉及到freetype\devel下的4个文件

1
2
3
4
5
6
│       └── freetype
│ └── devel
│ └── ft2build.h
│ └── ftmodule.h
│ └── ftoption.h
│ └── ftstdlib.h

测试FreeType移植结果

打开main.cpp 随便写点内容,烧录到ESP32上运行查看效果.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#include <Arduino.h>

#include "./Port/lv_port_disp.h"
#include "SD.h"
#include "SPI.h"

// 初始化SD卡
bool SD_Init() {
pinMode(22, INPUT);

SPIClass *sd_spi = new SPIClass(HSPI); // another SPI
if (!SD.begin(15, *sd_spi, 12000000, "")) // SD-Card SS pin is 15
{
Serial.println("Card Mount Failed");
return false;
}
uint8_t cardType = SD.cardType();

if (cardType == CARD_NONE) {
Serial.println("No SD card attached");
return false;
}

Serial.print("SD Card Type: ");
if (cardType == CARD_MMC) {
Serial.println("MMC");
} else if (cardType == CARD_SD) {
Serial.println("SDSC");
} else if (cardType == CARD_SDHC) {
Serial.println("SDHC");
} else {
Serial.println("UNKNOWN");
}

uint64_t cardSize = SD.cardSize() / (1024 * 1024);
Serial.printf("SD Card Size: %lluMB, used size:%llu\n", cardSize,
SD.usedBytes());

return true;
}

void setup() {
Serial.begin(115200); // Set to a high rate for fast image transfer to a PC

Port_Init();
if (!SD_Init()) {
Serial.println("SPIFFS Mount Failed");
};
/*创建字体*/
static lv_ft_info_t info;
/*在SD卡根目录下放置前一节中的可变字体文件,重命名为archivo.ttf*/
info.name = "/archivo.ttf";
info.weight = 18;
info.style = FT_FONT_STYLE_NORMAL;
if (!lv_ft_font_init(&info)) {
LV_LOG_ERROR("create failed.");
} else {
LV_LOG_ERROR("create done.");
}

/*为新字体创建Style*/
static lv_style_t style;
lv_style_init(&style);
lv_style_set_text_font(&style, info.font);
lv_style_set_text_color(&style, lv_color_black());

/*应用Style到Label*/
lv_obj_t *altria = lv_label_create(lv_scr_act());
lv_obj_t *shirou = lv_label_create(lv_scr_act());
// lv_obj_add_style(altria, &style, 0);

lv_label_set_text(altria, "Toou.\nAnata wa watashi no masuta ka?");
lv_label_set_text(shirou, "Master?");
lv_obj_set_style_text_font(shirou, info.font, 0);
lv_obj_center(shirou);

// 一切就绪, 启动LVGL任务
xTaskNotifyGive(handleTaskLvgl);
}

void loop() {}

运行效果如下

Weight=900

拓展LVGL的FreeType

就像前篇内容中的一样,需要手动地为当前版本LVGL添加可变字体支持.

lv_freetype.h内的lv_ft_info_t修改为

1
2
3
4
5
6
7
8
9
typedef struct {
const char * name; /* The name of the font file */
const void * mem; /* The pointer of the font file */
size_t mem_size; /* The size of the memory */
lv_font_t * font; /* point to lvgl font */
uint16_t height; /* font size */
uint16_t weight; /* font weight */
uint16_t style; /* font style */
} lv_ft_info_t;

然后去lv_freetype.c里添加可变字体操作代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
FT_MM_Var *amaster = NULL;
FT_Error err = FT_Get_MM_Var(face, &amaster);
if (err) {
LV_LOG_ERROR("FT_Get_MM_Var error:%d\n", err);
return err;
}
FT_Fixed w = dsc->weight << 16;
if (w > amaster->axis->maximum) {
w = amaster->axis->maximum;
}
err = FT_Set_Var_Design_Coordinates(face, 1, &w);
if (err) {
LV_LOG_ERROR("FT_Set_Var_Design_Coordinates error:%d\n", err);
return err;
}
FT_Done_MM_Var(library, amaster);

别忘记我们已经修改了weight的含义,此时字体大小已经由height决定了

回到main.cpp,在创建字体初时候,设置weight为100

1
2
3
4
5
static lv_ft_info_t info;
info.name = "/archivo.ttf";
info.height = 18;
info.weight = 100;
info.style = FT_FONT_STYLE_NORMAL;

然后烧录到ESP32,可以看到字重已经降低了.

Weight=900

动起来吧,字重

利用LVGL的lv_timer_t或者lv_anim_t动态的修改字重

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
typedef struct _VF_Label {
lv_obj_t *label;
lv_ft_info_t *font;
} VF_Label;

static void onTimer(lv_timer_t *timer) {
static uint16_t weight = 100;
static uint16_t size = 18;
VF_Label *vfl = (VF_Label *)(timer->user_data);
if (vfl == nullptr) {
printf("vfl == nullptr\n");
}

if (vfl == nullptr) {
printf("vfl->font->font == nullptr\n");
}

lv_ft_font_destroy(vfl->font->font);
printf("lv_ft_font_destroy\n");
vfl->font->name = "/archivo.ttf";
vfl->font->height = size;
vfl->font->weight = weight;
vfl->font->style = FT_FONT_STYLE_NORMAL;
lv_ft_font_init(vfl->font);
printf("lv_ft_font_init\n");
size = 48;
weight += 100;
if (weight > 600) {
weight = 100;
}
lv_obj_set_style_text_font(vfl->label, vfl->font->font, 0);
}
1
2
3
lv_timer_t *weightTimer = lv_timer_create(onTimer, 50, &label);
// 一切就绪, 启动LVGL任务
xTaskNotifyGive(handleTaskLvgl);

效果图

dynamic

环境:

1
2
3
4
5
6
7
Espressif 32 (3.4.0) > ESP32 Pico Kit
framework-arduinoespressif32 3.10006.210326 (1.0.6)
tool-esptoolpy 1.30100.210531 (3.1.0)
toolchain-xtensa32 2.50200.97 (5.2.0)
<lvgl> 8.1.1-dev
<TFT_eSPI> 2.3.89
esptool.py v3.1

参考资料

Repo

LGVL配合FreeType为可变字体设置字重-ESP32篇(上)

前言

我们在上篇中已经实现了模拟器环境下可变字体字重的设置.

是时候掏出你吃灰已久的ESP32了.

本文会使用PlatformIO创建一个全新的项目,直到显示出现上篇文章末尾的动图为止.
如遇到问题,可参考常见问题内解答.

准备工作

软件准备

为了后续内容顺利进行下去,这里需要你安装好VSCode,并在VSCode上安装PlatformIO插件.

硬件准备

名称 | 数量 | 备注 | 图例 |
:-: | :-: | :-:
ESP32 开发板 | 1 | \ |
1.54寸LCD | 1 | 驱动ST7789,分辨率240x240 |
杜邦线若干 | N | \ |

创建项目

使用PlatformIO创建一个名为lvgl_with_freetype的项目
create project
创建完毕后目录结构如下:

1
2
3
4
5
6
7
8
9
10
.
├── include
│ └── README
├── lib
│ └── README
├── platformio.ini
├── src
│ └── main.cpp
└── test
└── README

点亮屏幕

由于已经写过一篇点亮屏幕的文章,故本文不做过多赘述,只说明一下区别.

之前的屏幕分辨率是135x240,这次的屏幕分辨率是240x240.

所以需要使用TFT_eSPI里面的Setup24_ST7789.h

同时接线变更为

ESP32引脚名称 液晶屏引脚名称
VCC VCC
GND GND
G23 SDA
G18 SCL
G2 DC
G4 RES
GND CS
VCC BLK

对应Setup24_ST7789.h里面内容

Setup24_ST7789.h
1
2
3
4
5
6
#define TFT_MISO    19
#define TFT_MOSI 23
#define TFT_SCLK 18
#define TFT_CS -1
#define TFT_DC 2
#define TFT_RST 4

随便写点内容.测试下屏幕的点亮.

main.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <Arduino.h>
#include <TFT_eSPI.h> // Hardware-specific library
#include <SPI.h>

TFT_eSPI tft = TFT_eSPI(); // Invoke custom library

void setup() {
Serial.begin(115200); // Set to a high rate for fast image transfer to a PC

tft.init();
tft.setRotation(0);
tft.fillScreen(TFT_BLACK);
}

void loop() {
tft.print("Ready Perfectly");
}

lcd_ready_perfect

移植LVGL

屏幕点亮以后,就可以开始移植LVGL了.

当前时间为2021.12.19,GitHub上LVGL最新版本是8.1.1-dev

使用命令

1
git clone https://github.com/lvgl/lvgl.git

获取LVGL后将其复制到lib文件夹下.此时文件目录为

1
2
3
4
5
6
7
8
9
10
11
12
.
├── include
│ └── README
├── lib
│ └── README
│ └── lvgl
│ └── TFT_eSPI
├── platformio.ini
├── src
│ └── main.cpp
└── test
└── README

platformio.ini文件内容

1
2
3
4
5
6
7
8
[env:pico32]
platform = espressif32
board = pico32
framework = arduino
monitor_speed = 115200
lib_extra_dirs =
lib/TFT_eSPI
lib/lvgl

修改LVGL配置文件

创建LVGL的配置文件,找到lvgl文件夹内的lv_conf_templat.h,复制一份lv_conf_templat.h并重命名为lv_conf.h,然后打开lv_conf.h

为了使配置文件内容生效,找到第15行(其他版本的lvgl行数可能不在这里,需要自行寻找)

1
#if 0 /*Set it to "1" to enable content*/

改为

1
#if 1 /*Set it to "1" to enable content*/

找到第30行,修改颜色顺序

1
#define LV_COLOR_16_SWAP 0

改为

1
#define LV_COLOR_16_SWAP 1

找到第49行,启用自定义内存管理

1
#define LV_MEM_CUSTOM 0

修改为

1
#define LV_MEM_CUSTOM 1

找到第88行,设置自定义周期函数

1
#define LV_TICK_CUSTOM 0

修改为

1
#define LV_TICK_CUSTOM 1

找到第174行,启用LVGL日志功能

1
#define LV_USE_LOG 1

修改为

1
#define LV_USE_LOG 1

对接LVGL和TFT_eSPI

按照目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.
├── include
│ └── README
├── lib
│ └── README
│ └── lvgl
│ └── TFT_eSPI
├── platformio.ini
├── src
│ └── main.cpp
│ └── Port
│ └── lv_port_disp.cpp
│ └── lv_port_disp.h
└── test
└── README

创建lv_port_disp.cpplv_port_disp.h

lv_port_disp.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#ifndef LV_PORT_DISP_H_
#define LV_PORT_DISP_H_

#include "TFT_eSPI.h"
#include "lvgl.h"

#define DISP_HOR_RES 240
#define DISP_VER_RES 240
#define DISP_BUF_SIZE (DISP_HOR_RES*DISP_VER_RES/4)

extern TaskHandle_t handleTaskLvgl;
void Port_Init();
void lv_port_disp_init(TFT_eSPI* scr);

#endif
lv_port_disp.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
#include "lv_port_disp.h"

// 用于初始化完毕后启用LVGL显示的TaskHandle_t
TaskHandle_t handleTaskLvgl;
// lvgl显示驱动
static lv_disp_drv_t disp_drv;

// lvgl更新任务
void TaskLvglUpdate(void* parameter) {
// 阻塞在此处,直到xTaskNotifyGive
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
for (;;) {
lv_task_handler();

delay(5);
}
}

/**
* @brief 显示初始化
* @param 无
* @retval 无
*/
void Port_Init() {
static TFT_eSPI screen;

/* 屏幕初始化 */
screen.begin();
screen.initDMA(true);
screen.setRotation(0);
screen.fillScreen(TFT_BLACK);

/* lvgl初始化 */
lv_init();
lv_port_disp_init(&screen);
printf("lvInitDone\n");
// 在核心2上执行LVGL
xTaskCreatePinnedToCore(TaskLvglUpdate, "LvglThread", 20480, nullptr,
configMAX_PRIORITIES, &handleTaskLvgl, 1);
}

/**
* @brief 自定义打印函数
* @param 无
* @retval 无
*/
void my_print(lv_log_level_t level, const char *file, uint32_t line,
const char *fun, const char *dsc) {
Serial.printf("%s@%d %s->%s\r\n", file, line, fun, dsc);
Serial.flush();
}

/**
* @brief 屏幕刷新回调函数
* @param disp:屏幕驱动地址
* @param area:刷新区域
* @param color_p:刷新缓冲区地址
* @retval 无
*/
static void disp_flush_cb(lv_disp_drv_t *disp, const lv_area_t *area,
lv_color_t *color_p) {
TFT_eSPI *screen = (TFT_eSPI *)disp->user_data;

int32_t w = (area->x2 - area->x1 + 1);
int32_t h = (area->y2 - area->y1 + 1);

screen->startWrite();
screen->setAddrWindow(area->x1, area->y1, w, h);
screen->pushPixelsDMA((uint16_t *)(&color_p->full), w * h);
screen->endWrite();

lv_disp_flush_ready(disp);
}


/**
* @brief 屏幕初始化
* @param 无
* @retval 无
*/
void lv_port_disp_init(TFT_eSPI* scr) {
lv_log_register_print_cb(reinterpret_cast<lv_log_print_g_cb_t>(
my_print)); /* register print function for debugging */
DMA_ATTR static lv_color_t *lv_disp_buf =
static_cast<lv_color_t *>(heap_caps_malloc(
DISP_BUF_SIZE * sizeof(lv_color_t), MALLOC_CAP_DMA));
static lv_disp_draw_buf_t disp_buf;
lv_disp_draw_buf_init(&disp_buf, lv_disp_buf, nullptr,
DISP_BUF_SIZE);

/*Initialize the display*/
lv_disp_drv_init(&disp_drv);
disp_drv.hor_res = DISP_HOR_RES;
disp_drv.ver_res = DISP_VER_RES;
disp_drv.flush_cb = disp_flush_cb;
disp_drv.draw_buf = &disp_buf;
disp_drv.user_data = scr;
lv_disp_drv_register(&disp_drv);
}

再写个简单例子测试下LVGL能不能运行

main.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <Arduino.h>
#include "./Port/lv_port_disp.h"

void setup() {
Serial.begin(115200); // Set to a high rate for fast image transfer to a PC
Port_Init();


lv_obj_t *label = lv_label_create(lv_scr_act());
lv_label_set_text(label, "Toou.\nAnata wa watashi no masuta ka?");

// 一切就绪, 启动LVGL任务
xTaskNotifyGive(handleTaskLvgl);
}

void loop() {

}

Toou.Anata wa watashi no masuta ka?

lvgl_ready_perfect

施工中,待更新

常见问题

Q:点亮屏幕时候,编译器报找不到TFT_eSPI

A:检查TFT_eSPI是否集成
将TFT_eSPI放置在lib文件夹内,并向platformio.ini文件末尾添加

1
lib_extra_dirs =  lib/TFT_eSPI

Q:在移植LVGL时候,屏幕颜色异常

A:可能与lv_conf.h文件内#define LV_COLOR_16_SWAP 0有关
可以尝试将此处的0改成1,或1改回0

环境:

1
2
3
4
5
6
7
Espressif 32 (3.4.0) > ESP32 Pico Kit
framework-arduinoespressif32 3.10006.210326 (1.0.6)
tool-esptoolpy 1.30100.210531 (3.1.0)
toolchain-xtensa32 2.50200.97 (5.2.0)
<lvgl> 8.1.1-dev
<TFT_eSPI> 2.3.89
esptool.py v3.1

参考资料

LGVL配合FreeType为可变字体设置字重-模拟器篇

前言

不知道多久前看到了MIUI更新了”动态字体系统“功能,不过当时没太在意(毕竟我用的也不是MIUI,哈哈哈,不过确实挺方便的),演示视频里面展示了随意调节字体粗细的功能,后来知道这个参数叫做字重(zhong第四声).

然后又有一次去Material.io时候,看见了首页的Material You概念视频
手机解锁以后,系统时间的字体由细变粗.和上面MIUI动态字体系统调节字重的时候效果十分相似.

老早就听说了LVGL的大名,但是一直没有行动起来.

后来看见稚晖君PeakFASTSHIFTX-Track, 羡慕极了.
于是决定这次一定要试试LVGL,看看用起来到底是啥感觉.

之前自己搁那瞎捣鼓过一阵子的GUI,结果嘛,结果就是就弃坑了.

这次打算直接上LVGL这种成熟的GUI方案了,而恰好LVGL是支持FreeType的,借助FreeType就可以相对轻松的实现上面的字重动画.

什么是FreeType

FreeType库是一个完全免费(开源)的、高质量的且可移植的字体引擎

目前主流的屏幕均都是由像素点构成,不能直接显示矢量图,所以就需要字体引擎将字体的矢量数据转换为位图数据,然后在屏幕上显示 “点灯” 出来.

Variable Font又是什么

https://www.ifanr.com/1265373
储存轮廓变化数据的可变字体,在初始字形轮廓的基础上自动生成丰富的变化造型,使用户可以自由调整文字的外观。
枯燥的描述不如直接上手体验一下,V-Fonts是一个在线体验可变字体的网站,拖动滑块就可以修改字体在对应轴上的值,即上述的自由调整文字的外观.

准备工作

准备工作的准备工作之先把Visual Studio装了再说

下载LVGL模拟器

本打算再开一篇文章说模拟器安装的,重装的时候才发现原来一键就能安装.

(可能需要科学上网)
直接复制git命令就完事了,下完点击LVGL.Simulator.sln直接启动.

1
git clone --recurse-submodules https://github.com/lvgl/lv_sim_visual_studio.git

LVGL.Simulator.sln
启动以后看到lvgl自带的**lv_demo_widgets()**运行效果.
simluator_glance

为LVGL配置FreeType

得益于lv_sim_visual_studio的完整性,刚才git clone –recurse-submodules时候freetype被一并下载了.
所以现在暂时不需要额外配置什么内容,但是在其他情况下还是需要手动的配置一下LVGL的FreeType支持.(比如在板子上跑freetype的时候)

LVGL内置的FreeType Demo

将LVGL.Simulator.cpp内

1
2
3
4
5
6
7
8
9
10
11
12
13
// ----------------------------------
// Demos from lv_examples
// ----------------------------------
lv_demo_widgets(); // ok
// lv_demo_benchmark();
// lv_demo_keypad_encoder(); // ok
// lv_demo_music(); // removed from repository
// lv_demo_printer(); // removed from repository
// lv_demo_stress(); // ok

// ----------------------------------
// LVGL examples
// ----------------------------------

修改为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ----------------------------------
// Demos from lv_examples
// ----------------------------------

// lv_demo_widgets(); // ok
// lv_demo_benchmark();
// lv_demo_keypad_encoder(); // ok
// lv_demo_music(); // removed from repository
// lv_demo_printer(); // removed from repository
// lv_demo_stress(); // ok
lv_example_freetype_1();
// ----------------------------------
// LVGL examples
// ----------------------------------

如果一切顺利,点击运行.会看见下图
simluator_glance

下面则是lv_example_freetype_1的内容,我已经为他添加了详luo细suo的中文注释

lv_example_freetype_1.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* 使用FreeType加载字体
*/
void lv_example_freetype_1(void)
{
/* 创建字体结构体 info */
static lv_ft_info_t info;
/* FreeType 使用 C standard 文件系统, 所以不需要盘符 */
/* 目前程序在Windows上运行, 根目录为LVGL.Simulator.c所在文件夹 */
// 希望使用的字体的所在位置,即它的路径
info.name = "./lvgl/examples/libs/freetype/arial.ttf";
// 希望生成字体的高度, 这里叫作weight感觉挺奇怪的,weight应该是字重的英文
// [update-2021.12.17] 作者这里是一个失误,但是为了兼容性就没更正
// [Issues](https://github.com/lvgl/lv_lib_freetype/issues/17)
info.weight = 24;
// 字体的风格
info.style = FT_FONT_STYLE_NORMAL;
// 字体文件指针
info.mem = NULL;
// 初始化字体
if(!lv_ft_font_init(&info)) {
LV_LOG_ERROR("create failed.");
}
// 为上面的新字体创建一个style
static lv_style_t style;
// 初始化style
lv_style_init(&style);
// 应用刚才创建的字体到style上
lv_style_set_text_font(&style, info.font);
// 设置style的align为居中
lv_style_set_text_align(&style, LV_TEXT_ALIGN_CENTER);

// 为上面的style创建一个label以展示
lv_obj_t * label = lv_label_create(lv_scr_act());
// 为label添加刚才创建的style
lv_obj_add_style(label, &style, 0);
// 设置label的内容为Hello world\nI'm a font created with FreeType
lv_label_set_text(label, "Hello world\nI'm a font created with FreeType");
// 居中label
lv_obj_center(label);
}

迫于arial.ttf不是可变字体,所以我们趁此机会修改一下lv_example_freetype_1中用到的字体,熟悉下使用其他字体的方式.

**Archivo-VF.ttf**是一款Open Font License的字体
点击这里可以下载Archivo-VF.ttf

将其复制到arial.ttf同级目录,然后修改

1
info.name = "./lvgl/examples/libs/freetype/arial.ttf";

1
info.name = "./lvgl/examples/libs/freetype/Archivo-VF.ttf";

运行结果如下
simluator_glance

字体在线展示

点击这里在线体验ArchivoVF字体的可变属性

ArchivoVF的可变属性如下

  • 最小字重100
  • 最大字重900
  • 最小宽度62
  • 最大宽度125

https://fonts.google.com/specimen/Archivo

拓展LVGL的FreeType支持

由于LVGL目前版本(8.10-dev)还没有内置可变字体参数控制,所以需要我们手动的为其添加这部分内容.

添加支持前,速览下修改可变参数的方式及其啰嗦的注释
(需要使用可变字体进行操作,否则无法正常运行(包括不限于崩溃以及崩溃),测试用的字体文件为Archivo-VF,可在前面一节末尾获取到)

foo.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// face的类型为FT_Face
FT_Error error;
FT_MM_Var *amaster = nullptr;
// 获取可变参数
error = FT_Get_MM_Var(face, &amaster);
printf("error %d\n", error);
// 可变参数数量(每一个可变参数被称为一个axis(轴)))
printf("amaster->axis=%d\n", (amaster)->num_axis);
// 注意:上面提供的Archivo-VF文件只有2个可变轴Weight和Width
// 而amaster->axis是一个指向可变轴数组首位数据的指针
// 可变参数的名称id,在font文件内表'name'的id
printf("amaster->axis->strid=%u\n", amaster->axis->strid);
// 可变参数的内置标签
printf("amaster->axis->tag=%lu\n", amaster->axis->tag);
// 可变参数的名称
printf("amaster->axis->name=%s\n", amaster->axis->name);
// 可变参数的默认值
printf("amaster->axis->def=%ld\n", amaster->axis->def);
// 可变参数的最大值 16.16的定点数 amaster->axis->maximum / 65536; // 除以65536转化
printf("amaster->axis->maximum=%ld\n", amaster->axis->maximum);
// 可变参数的最小值 16.16的定点数 amaster->axis->minimum / 65536; // 除以65536转化
printf("amaster->axis->minimum=%ld\n", amaster->axis->minimum);
// 以前为了获得不同粗细的字体,就需要准备字重Thin,Bold,Normal这样不同字重的文件
// 而引入可变字体后,一个字体文件就内置了这些Thin Bold Normal的对应字重.
// 此处储存了当前轴内置默认值的数量
printf("amaster->num_namedstyles=%d\n", amaster->num_namedstyles); // 此处打印2
// 可变参数的内置num_namedstyle值的数组,以字重为例,里面可能存有(100,200,300,400,500,600,700,800,900)
printf("amaster->namedstyle->coords=%ld\n", (signed long) amaster->namedstyle->coords);
FT_Fixed coords[2] = { (amaster->axis)->maximum ,(amaster->axis + 1)->maximum};
printf("amaster->(axis)->maximum=%ld\n", (amaster->axis)->maximum /65536); // 此处打印900,符合前一节的图中字重最大值
printf("amaster->(axis+1)->maximum=%ld\n", (amaster->axis+1)->maximum /65536); // 此处打印125,符合前一节的图中字宽最大值
// 调整可变参数的值,此处将字重和字宽都设置为最大值
error = FT_Set_Var_Design_Coordinates(face,
2, coords);
printf("FT_Set_Var_Design_Coordinates error %d\n", error);
printf("face num_faces: %ld\n", face->num_faces);
// 使用可变参数的内置num_namedstyle
// styleIndex需要小于上述num_namedstyles
// error = FT_Set_Named_Instance(m_face, styleIndex);
// printf("FT_Set_Named_Instance error %d\n", error);
FT_Done_MM_Var(m_library, amaster);

由于目前版本的lvgl(8.1.0-dev)还没有提供相应的API,所以需要手动修改一些地方.
打开lv_freetype.h,修改lv_ft_info_t内容,如下所示

lv_freetype.h
1
2
3
4
5
6
7
typedef struct {
const char * name; /* The name of the font file */
lv_font_t * font; /* point to lvgl font */
uint16_t weight; /* font weight */
uint16_t height; /* font size */
uint16_t style; /* font style */
} lv_ft_info_t;

请注意:

lv_ft_info_t本身就有一个名为weight的uint16_t属性,但是后续被用到了字体的宽高尺寸上,所以现在我们需要添加height取代原来的weight,让weight成为名副其实的weight

推荐先把weight重命名为height,再把后续用到weight的地方改成height,当lv_example_freetype_1又能够成功运行的时候再添加”uint16_t weight;”

lv_font_fmt_ft_dsc_t也要添加字重(weight)
打开lv_freetype.c,修改lv_font_fmt_ft_dsc_t内容,如下所示

lv_freetype.c
1
2
3
4
5
6
7
8
9
10
11
typedef struct {
#if LV_FREETYPE_CACHE_SIZE >= 0
void *face_id;
#else
FT_Size size;
#endif
lv_font_t *font;
uint16_t style;
uint16_t height;
uint16_t weight;
} lv_font_fmt_ft_dsc_t;

这里直接添加”uint16_t weight;”即可

将修改字重的代码添加到lv_freetype.c内get_glyph_dsc_cb_cache中

lv_freetype.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
static bool get_glyph_dsc_cb_cache(const lv_font_t * font,
lv_font_glyph_dsc_t * dsc_out, uint32_t unicode_letter, uint32_t unicode_letter_next)
{
LV_UNUSED(unicode_letter_next);
if(unicode_letter < 0x20) {
dsc_out->adv_w = 0;
dsc_out->box_h = 0;
dsc_out->box_w = 0;
dsc_out->ofs_x = 0;
dsc_out->ofs_y = 0;
dsc_out->bpp = 0;
return true;
}

lv_font_fmt_ft_dsc_t * dsc = (lv_font_fmt_ft_dsc_t *)(font->dsc);

FTC_FaceID face_id = (FTC_FaceID)dsc->face_id;
FT_Size face_size;
struct FTC_ScalerRec_ scaler;
scaler.face_id = face_id;
scaler.width = dsc->height;
scaler.height = dsc->height;
scaler.pixel = 1;
if(FTC_Manager_LookupSize(cache_manager, &scaler, &face_size) != 0) {
return false;
}




FT_Face face = face_size->face;

FT_MM_Var* amaster = NULL;
FT_Error err = FT_Get_MM_Var(face, &amaster);
if (err) {
LV_LOG_ERROR("FT_Get_MM_Var error:%d\n", err);
return err;
}
// 别忘记左移16位,还有一件事,我只修改了字重,没修改字宽,所以数组大小是1
FT_Fixed coords[1] = { dsc->weight<<16 };
err = FT_Set_Var_Design_Coordinates(face, 1, coords);
if (err) {
LV_LOG_ERROR("FT_Set_Var_Design_Coordinates error:%d\n", err);
return err;
}
FT_Done_MM_Var(library, amaster);

FT_UInt charmap_index = FT_Get_Charmap_Index(face->charmap);
FT_UInt glyph_index = FTC_CMapCache_Lookup(cmap_cache, face_id, charmap_index, unicode_letter);
dsc_out->is_placeholder = glyph_index == 0;

if(dsc->style & FT_FONT_STYLE_ITALIC) {
FT_Matrix italic_matrix;
italic_matrix.xx = 1 << 16;
italic_matrix.xy = 0x5800;
italic_matrix.yx = 0;
italic_matrix.yy = 1 << 16;
FT_Set_Transform(face, &italic_matrix, NULL);
}

if(dsc->style & FT_FONT_STYLE_BOLD) {
current_face = face;
if(!get_bold_glyph(font, face, glyph_index, dsc_out)) {
current_face = NULL;
return false;
}
goto end;
}

FTC_ImageTypeRec desc_type;
desc_type.face_id = face_id;
desc_type.flags = FT_LOAD_RENDER | FT_LOAD_TARGET_NORMAL;
desc_type.height = dsc->height;
desc_type.width = dsc->height;

#if LV_FREETYPE_SBIT_CACHE
FT_Error error = FTC_SBitCache_Lookup(sbit_cache, &desc_type, glyph_index, &sbit, NULL);
if(error) {
LV_LOG_ERROR("SBitCache_Lookup error");
return false;
}

dsc_out->adv_w = sbit->xadvance;
dsc_out->box_h = sbit->height; /*Height of the bitmap in [px]*/
dsc_out->box_w = sbit->width; /*Width of the bitmap in [px]*/
dsc_out->ofs_x = sbit->left; /*X offset of the bitmap in [pf]*/
dsc_out->ofs_y = sbit->top - sbit->height; /*Y offset of the bitmap measured from the as line*/
dsc_out->bpp = 8; /*Bit per pixel: 1/2/4/8*/
#else
FT_Error error = FTC_ImageCache_Lookup(image_cache, &desc_type, glyph_index, &image_glyph, NULL);
if(error) {
LV_LOG_ERROR("ImageCache_Lookup error");
return false;
}
if(image_glyph->format != FT_GLYPH_FORMAT_BITMAP) {
LV_LOG_ERROR("Glyph_To_Bitmap error");
return false;
}

FT_BitmapGlyph glyph_bitmap = (FT_BitmapGlyph)image_glyph;
dsc_out->adv_w = (glyph_bitmap->root.advance.x >> 16);
dsc_out->box_h = glyph_bitmap->bitmap.rows; /*Height of the bitmap in [px]*/
dsc_out->box_w = glyph_bitmap->bitmap.width; /*Width of the bitmap in [px]*/
dsc_out->ofs_x = glyph_bitmap->left; /*X offset of the bitmap in [pf]*/
dsc_out->ofs_y = glyph_bitmap->top -
glyph_bitmap->bitmap.rows; /*Y offset of the bitmap measured from the as line*/
dsc_out->bpp = 8; /*Bit per pixel: 1/2/4/8*/
#endif

end:
if((dsc->style & FT_FONT_STYLE_ITALIC) && (unicode_letter_next == '\0')) {
dsc_out->adv_w = dsc_out->box_w + dsc_out->ofs_x;
}

return true;
}

初始化的时候,别忘了lv_ft_font_init_cache时候传递字重信息

lv_freetype.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
static bool lv_ft_font_init_cache(lv_ft_info_t * info)
{
lv_font_fmt_ft_dsc_t * dsc = lv_mem_alloc(sizeof(lv_font_fmt_ft_dsc_t));
if(dsc == NULL) return false;

dsc->font = lv_mem_alloc(sizeof(lv_font_t));
if(dsc->font == NULL) {
lv_mem_free(dsc);
return false;
}
lv_memset_00(dsc->font, sizeof(lv_font_t));
lv_face_info_t * face_info = NULL;
face_info = lv_mem_alloc(sizeof(lv_face_info_t) + strlen(info->name) + 1);
if(face_info == NULL) {
goto Fail;
}
face_info->mem = info->mem;
face_info->size = info->mem_size;
face_info->name = ((char *)face_info) + sizeof(lv_face_info_t);
strcpy(face_info->name, info->name);

dsc->face_id = face_info;
dsc->height = info->height;
dsc->weight = info->weight;
dsc->style = info->style;

/* use to get font info */
FT_Size face_size;
struct FTC_ScalerRec_ scaler;
scaler.face_id = (FTC_FaceID)dsc->face_id;
scaler.width = info->height;
scaler.height = info->height;
scaler.pixel = 1;
FT_Error error = FTC_Manager_LookupSize(cache_manager, &scaler, &face_size);
if(error) {
lv_mem_free(face_info);
LV_LOG_ERROR("Failed to LookupSize");
goto Fail;
}

lv_font_t * font = dsc->font;
font->dsc = dsc;
font->get_glyph_dsc = get_glyph_dsc_cb_cache;
font->get_glyph_bitmap = get_glyph_bitmap_cb_cache;
font->subpx = LV_FONT_SUBPX_NONE;
font->line_height = (face_size->face->size->metrics.height >> 6);
font->base_line = -(face_size->face->size->metrics.descender >> 6);

FT_Fixed scale = face_size->face->size->metrics.y_scale;
int8_t thickness = FT_MulFix(scale, face_size->face->underline_thickness) >> 6;
font->underline_position = FT_MulFix(scale, face_size->face->underline_position) >> 6;
font->underline_thickness = thickness < 1 ? 1 : thickness;

/* return to user */
info->font = font;

return true;

Fail:
lv_mem_free(dsc->font);
lv_mem_free(dsc);
return false;
}

效果预览

不出意外的话,需要修改的地方已经修改完了,现在回到lv_example_freetype_1上来.初始化info的时候把weight也给初始化了.

1
2
// 由于已经知道了要使用的Archivo-VF最大字重900;
info.weight = 900 ;

然后运行就可以看到字重900的Archivo-VF

下图是在LVGL模拟器上的运行效果。
Weight=900

下集预告

Weight Animation

On ESP32

环境

1
2
3
4
Windows 10 Pro 18363.1556
Microsoft Visual Studio Community 2019 16.6.2
LVGL 8.1.1-dev
FreeType 2.11.0

参考资料

附件

© 2025 Do U Find IT? All Rights Reserved.
Theme by hiero