Go back

Variable Fonts in LVGL, Part 3: Loading Fonts and Managing Memory on ESP32

Published:  at  08:40 PM
⏱️ 1869 words • 10 min read

阅读中文版

Loading variable fonts on ESP32 with FreeType and LVGL, configuring PlatformIO libraries, and managing memory with LV_FREETYPE_SBIT_CACHE.

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

NameQuantityRemarksLegend
ESP32 Development Board1\
1.54-inch LCD1Driver ST7789, resolution 240x240
Several Dupont linesN\
Micro SD Card Modules and Cards1\

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 nameMicro SD card module pin name
GNDGND
G26MISO
G13MOSI
G14SCK
G15CS
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

Weight=900

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.

Weight=900

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

dynamic

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

Repo


Share this post on:

Previous Post
ESP32 LCD Performance: Parallel Interface versus SPI
Next Post
Variable Fonts in LVGL, Part 2: Porting FreeType to ESP32