- Implementing variable fonts (Variable Font) in LVGL-Chapter 1
- Implementing variable fonts (Variable Font) in LVGL-Chapter 2
- Implementing variable fonts (Variable Font) in LVGL - Chapter 3
Preface
(November 2025 remastered version description): I published the first version of this article on a third-party platform (Jianshu + Bilibili) and received a total of 50,000+ views and a large number of developer feedback. In order to provide a better reading experience, I have optimized the layout and part of the content of the article and published it exclusively on this personal blog
Chapter 2 introduced how to run LVGL on ESP32, and now we can finally start working towards the effect at the end of the Chapter 1 article.
Preparation
Software preparation
Visithttps://download.savannah.gnu.org/releases/freetype/Download the source code, unzip it, rename it to freetype and copy it to the project lib folder I downloaded the latest version freetype-2.11.1.tar.gz currently (2021.12.11) (older versions do not support variable fonts operations) Then go to thisRepo to obtain the SD and SPI libraries and copy them to the project lib folder
.
├── 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
Go to the freetype folder and create FreeType’s library.json and library.properties files library.propertiesThe content is as follows
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.jsonThe content is as follows
{
"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"
}
}
Hardware preparation
Compared with the previous article, there is an additional SD card module.
At first, I also tried to store the font files directly in SPIFFS to reduce the hardware involved, but the speed was shockingly slow, so I came back here and rewrote the SD card version.
2022.02.08: It was discovered that since LVGL 8.1.1-dev (the official version is 8.2.0), the built-in lv_freetype module has supported the FT_New_Memory_Face method to create fonts. If there is no memory card, please name the font file Lite.ttf and put it in the source code src folder. Add in platformio.ini
board_build.embed_txtfiles =
src/Lite.ttf
and add in main.cpp
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;
When creating a font, add the following additional content:
static lv_ft_info_t info;
/**/
info.mem = font_start;
info.mem_size = size;
/**/
With SD card
| Name | Quantity | Remarks | Legend |
|---|---|---|---|
| ESP32 Development Board | 1 | \ | ![]() |
| 1.54-inch LCD | 1 | Driver ST7789, resolution 240x240 | ![]() |
| Several Dupont lines | N | \ | ![]() |
| Micro SD Card Modules and Cards | 1 | \ | ![]() |
Please refer to LVGL cooperates with FreeType to set font weight for variable fonts-ESP32 (Part 1) for wiring and lighting the screen.
The wiring of the Micro SD card module is as follows
| ESP32 pin name | Micro SD card module pin name |
|---|---|
| GND | GND |
| G26 | MISO |
| G13 | MOSI |
| G14 | SCK |
| G15 | CS |
| 5V(Vin) | VCC |
Ready, let’s get started
FreeType with LVGL enabled
Define the macro content in lv_conf.h and enable FTC_SBitCache_Lookup
/*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 is set to 1
Configuring the FreeType file system
Create a file named ft_fs_port.cpp with the content
##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();
}
}
Open devel\ft2build.h located in the freetype folder and add
##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_ */
Create the ftstdlib.h (full version in Repo at the end of the article) file in the same directory, and copy the content from freetype\include\freetype\config\ftstdlib.h
Modify the content of the file handling section to
/**************************************************************************
*
* 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
Create ftmodule.h in the same directory and enable the required modules The content is
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 )
Createftoption.hin the same directory. The content is copied from freetype\include\freetype\config\ftoptions.h. The detailed configuration is too long, so it is omitted here. See in the Repo at the end of the article.
At this point, the basic function configuration of FreeType is completed.
The main ones involve 4 files under freetype\devel
│ └── freetype
│ └── devel
│ └── ft2build.h
│ └── ftmodule.h
│ └── ftoption.h
│ └── ftstdlib.h
Test FreeType porting results
Open main.cpp, write some content, burn it to ESP32 and run it to see the effect.
##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() {}
The operation effect is as follows

Extend LVGL’s FreeType
Just like in the previous article, you need to manually add variable fonts support for the current version of LVGL.
lv_freetype.hThe lv_ft_info_t is modified to
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;
Then go to lv_freetype.c and add the variable fonts operation code
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);
Don’t forget that we have modified the meaning of weight, and the font size is now determined by height
Back to main.cpp, when initially creating the font, set the weight to 100
static lv_ft_info_t info;
info.name = "/archivo.ttf";
info.height = 18;
info.weight = 100;
info.style = FT_FONT_STYLE_NORMAL;
Then burn it to ESP32, you can see that the font weight has been reduced.

Move it, font weight
Use LVGL’s lv_timer_t or lv_anim_t to dynamically modify the font weight
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);
}
lv_timer_t *weightTimer = lv_timer_create(onTimer, 50, &label);
// 一切就绪, 启动LVGL任务
xTaskNotifyGive(handleTaskLvgl);
renderings

Environment:
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
References
- https://github.com/lvgl/lvgl
- https://github.com/peng-zhihui/Peak
- https://www.youtube.com/watch?v=LY3ypzPcDCE



