FreeType 字体显示与移植实验
实验目标
在泰山派 RK3566 上移植 FreeType,编写 C 程序加载 TTF/TTC 字体,将字符渲染为灰度位图,并通过 Framebuffer 显示到 ST7789 屏幕。
实验环境
| 项目 | 配置 |
|---|---|
| 开发板 | 泰山派 RK3566 V10 |
| SoC 架构 | ARM64 / AArch64 |
| Linux 内核 | 6.1.141 |
| 显示屏 | ST7789,240×240 |
| 显示接口 | SPI3 |
| 显示设备 | /dev/fb0 |
| 编译环境 | Ubuntu |
| 交叉编译工具链 | 泰山派提供的 aarch64-linux-gnu- 工具链 |
| FreeType | 2.15.0 |
实验原理
FreeType 负责解析字体文件并将字符轮廓转换为灰度位图,应用程序负责计算字符位置、转换像素格式,再将图像写入 Framebuffer。
TTF/TTC 字体文件
↓
FT_Init_FreeType()
↓
FT_New_Face()
↓
FT_Set_Pixel_Sizes()
↓
FT_Load_Char(..., FT_LOAD_RENDER)
↓
获取 face->glyph->bitmap
↓
灰度位图转换为 RGB565
↓
写入 /dev/fb0
↓
ST7789 显示字符FreeType 本身不负责操作 LCD,也不负责中文编码转换。它只提供字形加载和渲染能力,最终显示由应用程序完成。
移植步骤
1. 获取 FreeType 源码
从 FreeType 官方网站下载源码。本文使用 2.15.0 版本。
wget https://download.savannah.gnu.org/releases/freetype/freetype-2.15.0.tar.xz
tar -xf freetype-2.15.0.tar.xz
cd freetype-2.15.02. 配置交叉编译工具链
交叉编译工具链用于在 Ubuntu 主机上编译能够运行在目标开发板上的程序。
泰山派 RK3566 使用 ARM64(AArch64)架构,SDK 通常提供预置交叉编译工具链。由于不同 SDK 版本的 GCC 版本、工具链名称和目录结构可能不同,需要先找到实际使用的编译器。
优先使用此前已经成功编译并运行过目标板应用程序的工具链,确保 FreeType 库与开发板现有运行环境兼容。
2.1 SDK 工具链目录
泰山派 RK3566 Linux SDK 中,与交叉编译相关的主要目录如下:
tspi_sdk/
├── prebuilts/
│ └── gcc/
│ └── linux-x86/
│ └── aarch64/
│ └── <工具链版本目录>/
│ └── bin/
│ ├── <工具链前缀>-gcc
│ ├── <工具链前缀>-g++
│ └── ...
│
└── buildroot/
└── output/
└── <实际构建配置目录>/
└── ...主要目录说明:
prebuilts/gcc/:SDK 预置的交叉编译工具链。buildroot/output/:Buildroot 构建输出目录,可能包含生成的工具链、目标文件系统及其他构建文件。
如果此前已经使用 prebuilts/gcc/ 中的工具链成功编译过应用程序,优先继续使用该工具链。
如果没有现成的工具链,可以检查 Buildroot 输出目录中是否生成了对应的交叉编译器。
不同 SDK 版本的目录结构可能存在差异,以上仅展示常见位置。
2.2 查找交叉编译工具链
进入 SDK 根目录,即包含 prebuilts 和 buildroot 的目录。
首先搜索 SDK 预置工具链:
find prebuilts/gcc \
\( -type f -o -type l \) \
-name '*aarch64*gcc' -print 2>/dev/null搜索结果可能类似:
.../prebuilts/gcc/.../bin/aarch64-xxx-linux-gnu-gcc选择与当前 Buildroot 根文件系统匹配的工具链,不要仅凭编译器名称判断是否兼容。
2.3 配置交叉编译环境
找到交叉编译器后,将实际 GCC 路径赋值给 CC。
export CC=/实际工具链目录/bin/实际工具链前缀-gcc根据编译器路径自动提取工具链目录和前缀:
export TOOLCHAIN_BIN="$(dirname "$CC")"
export CROSS_COMPILE="${CC%gcc}"
export PATH="$TOOLCHAIN_BIN:$PATH"
export CXX="${CROSS_COMPILE}g++"
export AR="${CROSS_COMPILE}ar"
export AS="${CROSS_COMPILE}as"
export LD="${CROSS_COMPILE}ld"
export RANLIB="${CROSS_COMPILE}ranlib"
export STRIP="${CROSS_COMPILE}strip"变量说明:
| 变量 | 作用 |
|---|---|
CC | C 语言交叉编译器 |
CXX | C++ 交叉编译器 |
AR | 静态库归档工具 |
AS | 汇编器 |
LD | 链接器 |
RANLIB | 静态库索引工具 |
STRIP | 目标文件符号裁剪工具 |
CROSS_COMPILE | 交叉编译工具链前缀 |
其中:
TOOLCHAIN_BIN自动获取编译器所在目录。CROSS_COMPILE自动去掉 GCC 命令末尾的gcc,得到对应工具链前缀。PATH将工具链目录加入当前终端的环境变量。
这样不需要手动填写具体的工具链前缀,后续命令也不依赖某个固定 GCC 版本。
2.4 检查交叉编译工具链
配置完成后,检查编译器是否可用:
command -v "$CC"
"$CC" -v
"$CC" -dumpmachine
"$CC" -print-sysroot主要检查以下内容:
| 命令 | 作用 |
|---|---|
command -v | 确认编译器路径 |
-v | 查看 GCC 版本和编译配置 |
-dumpmachine | 查看目标平台三元组 |
-print-sysroot | 查看编译器使用的目标系统根目录 |
检查结果:
command -v应返回刚才配置的 SDK 编译器路径。-v应正常显示 GCC 版本。-dumpmachine应显示 AArch64 目标平台。-print-sysroot用于确认编译器使用的目标系统根目录。
如果 -print-sysroot 返回空值或 /,不要直接猜测 sysroot 路径,应进一步检查工具链配置及 Buildroot 输出目录。
2.5 设置目标编译平台
Autoconf 项目通常需要区分编译主机和程序运行平台:
--build:当前 Ubuntu 主机的编译平台。--host:编译完成后程序实际运行的平台。
通过编译器自动获取目标平台:
export BUILD="$(gcc -dumpmachine)"
export HOST="$("$CC" -dumpmachine)"后续配置 FreeType 时使用:
./configure \
--build="$BUILD" \
--host="$HOST"不要将 Ubuntu 主机的架构作为目标平台,也不要直接套用其他 SDK 的工具链前缀。
注意: FreeType 动态库和后续字体显示程序应使用同一套交叉编译工具链。这样可以避免不同工具链之间的 C 库、ABI 和链接环境不一致。
3. 配置 FreeType
本实验使用 TTF/TTC 字体并输出灰度位图,不需要 PNG 字体、BZip2 压缩字体和 HarfBuzz 字形整形功能,因此关闭这些可选依赖,避免额外移植依赖库。
执行配置:
./configure \
--build="$(gcc -dumpmachine)" \
--host=aarch64-linux-gnu \
--prefix=/usr \
--libdir=/usr/lib \
--without-bzip2 \
--without-png \
--without-zlib \
--without-harfbuzz参数说明:
| 参数 | 作用 |
|---|---|
--build | 当前 Ubuntu 主机的编译架构 |
--host | 编译后程序运行的目标架构,即泰山派 ARM64 |
--prefix=/usr | 指定目标系统中的安装目录 |
--libdir=/usr/lib | 指定目标系统中的动态库目录 |
--without-* | 关闭本实验不需要的可选依赖 |
交叉编译时,--host 使用泰山派工具链对应的目标前缀。这里使用 aarch64-linux-gnu,与泰山派官方 C 程序交叉编译示例一致。不要把主机 GCC 指定为目标编译器。
4. 编译并安装到临时目录
执行编译:
make -j$(nproc)编译完成后,将库安装到当前源码目录下的 _install,不会覆盖 Ubuntu 主机的系统库:
make DESTDIR="$PWD/_install" install安装完成后,主要文件位于:
_install/
└── usr/
├── include/
│ └── freetype2/
│ ├── ft2build.h
│ └── freetype/
└── lib/
├── libfreetype.so
└── libfreetype.so.6实际库文件可能带有更长的版本号,符号链接由安装过程生成,部署时应一并保留。
检查动态库架构:
aarch64-linux-gnu-readelf -h "$(readlink -f _install/usr/lib/libfreetype.so)" | grep Machine输出应包含:
Machine: AArch645. 编写并交叉编译测试程序
将下方完整 C 示例保存为 freetype_fb.c。
使用同一套泰山派工具链编译:
aarch64-linux-gnu-gcc freetype_fb.c \
-I_install/usr/include/freetype2 \
-L_install/usr/lib \
-Wl,-rpath-link,"$PWD/_install/usr/lib" \
-lfreetype \
-o freetype_fb检查可执行文件架构:
aarch64-linux-gnu-readelf -h freetype_fb | grep Machine输出应为 AArch64。
6. 准备字体文件
测试程序需要一个包含中文字符的 TTF 或 TTC 字体文件。可以使用已有字体,也可以在 Ubuntu 中安装文泉驿正黑字体:
sudo apt install fonts-wqy-zenhei字体路径:
/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc7. 部署到泰山派
在 Ubuntu 主机准备部署目录,将测试程序、FreeType 动态库和字体放在一起:
mkdir -p deploy/lib deploy/fonts
cp freetype_fb deploy/
cp -a _install/usr/lib/libfreetype.so* deploy/lib/
cp /usr/share/fonts/truetype/wqy/wqy-zenhei.ttc deploy/fonts/
tar -czf freetype-demo.tar.gz -C deploy .将压缩包复制到泰山派。以下使用 SSH,用户名和 IP 替换为开发板实际配置:
scp freetype-demo.tar.gz root@开发板IP:/tmp/在泰山派上解压并运行:
mkdir -p ~/freetype-demo
tar -xzf /tmp/freetype-demo.tar.gz -C ~/freetype-demo
cd ~/freetype-demo
LD_LIBRARY_PATH="$PWD/lib" \
./freetype_fb ./fonts/wqy-zenhei.ttc /dev/fb0程序通过 LD_LIBRARY_PATH 加载随程序部署的 libfreetype.so,不需要覆盖开发板原有系统库。
C 示例
下面的程序完成字体初始化、加载、字形渲染和 Framebuffer 显示。示例使用 RGB565 格式,适用于当前 ST7789 /dev/fb0 显示环境。
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <wchar.h>
#include <linux/fb.h>
#include <ft2build.h>
#include FT_FREETYPE_H
typedef struct {
int fd;
uint8_t *mem;
size_t size;
struct fb_var_screeninfo var;
struct fb_fix_screeninfo fix;
} Framebuffer;
static int fb_open(Framebuffer *fb, const char *device)
{
memset(fb, 0, sizeof(*fb));
fb->fd = open(device, O_RDWR);
if (fb->fd < 0) {
perror("open framebuffer");
return -1;
}
if (ioctl(fb->fd, FBIOGET_VSCREENINFO, &fb->var) < 0 ||
ioctl(fb->fd, FBIOGET_FSCREENINFO, &fb->fix) < 0) {
perror("get framebuffer info");
close(fb->fd);
return -1;
}
if (fb->var.bits_per_pixel != 16 ||
fb->var.red.offset != 11 ||
fb->var.green.offset != 5 ||
fb->var.blue.offset != 0) {
fprintf(stderr, "Framebuffer is not RGB565\n");
close(fb->fd);
return -1;
}
fb->size = fb->fix.smem_len;
fb->mem = mmap(NULL, fb->size, PROT_READ | PROT_WRITE,
MAP_SHARED, fb->fd, 0);
if (fb->mem == MAP_FAILED) {
perror("mmap framebuffer");
close(fb->fd);
return -1;
}
return 0;
}
static void fb_close(Framebuffer *fb)
{
if (fb->mem && fb->mem != MAP_FAILED)
munmap(fb->mem, fb->size);
if (fb->fd >= 0)
close(fb->fd);
}
static void put_pixel(Framebuffer *fb, int x, int y, uint8_t gray)
{
if (x < 0 || y < 0 ||
x >= (int)fb->var.xres || y >= (int)fb->var.yres)
return;
size_t offset =
((size_t)y + fb->var.yoffset) * fb->fix.line_length +
((size_t)x + fb->var.xoffset) * 2;
if (offset + 2 > fb->size)
return;
/* 将 8 位灰度值转换为 RGB565 白色像素。 */
uint16_t color =
((uint16_t)(gray >> 3) << 11) |
((uint16_t)(gray >> 2) << 5) |
(uint16_t)(gray >> 3);
memcpy(fb->mem + offset, &color, sizeof(color));
}
static void draw_bitmap(Framebuffer *fb, FT_Bitmap *bitmap,
int x, int y)
{
if (bitmap->pixel_mode != FT_PIXEL_MODE_GRAY)
return;
for (unsigned int row = 0; row < bitmap->rows; row++) {
const uint8_t *src;
if (bitmap->pitch >= 0) {
src = bitmap->buffer + row * bitmap->pitch;
} else {
src = bitmap->buffer +
(bitmap->rows - 1 - row) * (-bitmap->pitch);
}
for (unsigned int col = 0; col < bitmap->width; col++) {
uint8_t gray = src[col];
if (gray)
put_pixel(fb, x + (int)col, y + (int)row, gray);
}
}
}
int main(int argc, char *argv[])
{
if (argc < 2) {
printf("Usage: %s font.ttc [framebuffer]\n", argv[0]);
return 1;
}
const char *fb_device = argc >= 3 ? argv[2] : "/dev/fb0";
Framebuffer fb = {.fd = -1, .mem = NULL};
FT_Library library;
FT_Face face;
if (fb_open(&fb, fb_device) < 0)
return 1;
FT_Error error = FT_Init_FreeType(&library);
if (error) {
fprintf(stderr, "FT_Init_FreeType failed: %d\n", error);
fb_close(&fb);
return 1;
}
error = FT_New_Face(library, argv[1], 0, &face);
if (error) {
fprintf(stderr, "FT_New_Face failed: %d\n", error);
FT_Done_FreeType(library);
fb_close(&fb);
return 1;
}
error = FT_Select_Charmap(face, FT_ENCODING_UNICODE);
if (error) {
fprintf(stderr, "Unicode charmap not found\n");
FT_Done_Face(face);
FT_Done_FreeType(library);
fb_close(&fb);
return 1;
}
error = FT_Set_Pixel_Sizes(face, 0, 32);
if (error) {
fprintf(stderr, "FT_Set_Pixel_Sizes failed: %d\n", error);
FT_Done_Face(face);
FT_Done_FreeType(library);
fb_close(&fb);
return 1;
}
/* 清空当前屏幕可见区域。 */
for (unsigned int y = 0; y < fb.var.yres; y++) {
for (unsigned int x = 0; x < fb.var.xres; x++)
put_pixel(&fb, (int)x, (int)y, 0);
}
const wchar_t text[] = L"Hello 泰山派 RK3566";
int pen_x = 8;
int baseline_y = 55;
for (size_t i = 0; text[i] != L'\0'; i++) {
FT_ULong code = (FT_ULong)text[i];
if (FT_Get_Char_Index(face, code) == 0) {
fprintf(stderr, "Font missing character: U+%04lX\n",
(unsigned long)code);
continue;
}
error = FT_Load_Char(face, code, FT_LOAD_RENDER);
if (error) {
fprintf(stderr, "FT_Load_Char failed: %d\n", error);
continue;
}
FT_GlyphSlot slot = face->glyph;
draw_bitmap(&fb, &slot->bitmap,
pen_x + slot->bitmap_left,
baseline_y - slot->bitmap_top);
/* advance 使用 26.6 定点数,右移 6 位转换为像素。 */
pen_x += (int)(slot->advance.x >> 6);
}
FT_Done_Face(face);
FT_Done_FreeType(library);
fb_close(&fb);
puts("FreeType display finished");
return 0;
}实验结果
| 检查项目 | 预期结果 | 实际结果 |
|---|---|---|
| FreeType 交叉编译 | 生成 AArch64 动态库 | 待实测 |
| 测试程序编译 | 生成 AArch64 可执行文件 | 待实测 |
| 动态库加载 | 程序正常加载 libfreetype.so | 待实测 |
| 字体加载 | 成功加载 TTF/TTC 文件 | 待实测 |
| 字符渲染 | FreeType 生成灰度字形位图 | 待实测 |
| LCD 显示 | ST7789 显示中英文字符 | 待实测 |
注意事项
- 交叉编译库和测试程序必须使用同一套泰山派 AArch64 工具链。
--host指定目标运行平台,不能填写 Ubuntu 主机架构。--prefix=/usr指向目标板文件系统;配合DESTDIR暂存,不能直接安装到 Ubuntu 主机。- 部署时需要保留
libfreetype.so的符号链接和实际库文件。 - 字体文件需要单独部署,FreeType 不自带中文字体。
- 本示例只支持 RGB565 单平面 Framebuffer,不适用于 DRM/KMS 设备节点。
bitmap.pitch表示位图行跨度,不应直接用bitmap.width代替。
