跳转到正文
实验记录
泰山派 RK3566 V10待验证

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- 工具链
FreeType2.15.0

实验原理 ​

FreeType 负责解析字体文件并将字符轮廓转换为灰度位图,应用程序负责计算字符位置、转换像素格式,再将图像写入 Framebuffer。

text
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 版本。

bash
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.0

2. 配置交叉编译工具链 ​

交叉编译工具链用于在 Ubuntu 主机上编译能够运行在目标开发板上的程序。

泰山派 RK3566 使用 ARM64(AArch64)架构,SDK 通常提供预置交叉编译工具链。由于不同 SDK 版本的 GCC 版本、工具链名称和目录结构可能不同,需要先找到实际使用的编译器。

优先使用此前已经成功编译并运行过目标板应用程序的工具链,确保 FreeType 库与开发板现有运行环境兼容。

2.1 SDK 工具链目录 ​

泰山派 RK3566 Linux SDK 中,与交叉编译相关的主要目录如下:

text
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 预置工具链:

bash
find prebuilts/gcc \
  \( -type f -o -type l \) \
  -name '*aarch64*gcc' -print 2>/dev/null

搜索结果可能类似:

text
.../prebuilts/gcc/.../bin/aarch64-xxx-linux-gnu-gcc

选择与当前 Buildroot 根文件系统匹配的工具链,不要仅凭编译器名称判断是否兼容。

2.3 配置交叉编译环境 ​

找到交叉编译器后,将实际 GCC 路径赋值给 CC。

bash
export CC=/实际工具链目录/bin/实际工具链前缀-gcc

根据编译器路径自动提取工具链目录和前缀:

bash
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"

变量说明:

变量作用
CCC 语言交叉编译器
CXXC++ 交叉编译器
AR静态库归档工具
AS汇编器
LD链接器
RANLIB静态库索引工具
STRIP目标文件符号裁剪工具
CROSS_COMPILE交叉编译工具链前缀

其中:

  • TOOLCHAIN_BIN 自动获取编译器所在目录。
  • CROSS_COMPILE 自动去掉 GCC 命令末尾的 gcc,得到对应工具链前缀。
  • PATH 将工具链目录加入当前终端的环境变量。

这样不需要手动填写具体的工具链前缀,后续命令也不依赖某个固定 GCC 版本。

2.4 检查交叉编译工具链 ​

配置完成后,检查编译器是否可用:

bash
command -v "$CC"
"$CC" -v
"$CC" -dumpmachine
"$CC" -print-sysroot

主要检查以下内容:

命令作用
command -v确认编译器路径
-v查看 GCC 版本和编译配置
-dumpmachine查看目标平台三元组
-print-sysroot查看编译器使用的目标系统根目录

检查结果:

  1. command -v 应返回刚才配置的 SDK 编译器路径。
  2. -v 应正常显示 GCC 版本。
  3. -dumpmachine 应显示 AArch64 目标平台。
  4. -print-sysroot 用于确认编译器使用的目标系统根目录。

如果 -print-sysroot 返回空值或 /,不要直接猜测 sysroot 路径,应进一步检查工具链配置及 Buildroot 输出目录。

2.5 设置目标编译平台 ​

Autoconf 项目通常需要区分编译主机和程序运行平台:

  • --build:当前 Ubuntu 主机的编译平台。
  • --host:编译完成后程序实际运行的平台。

通过编译器自动获取目标平台:

bash
export BUILD="$(gcc -dumpmachine)"
export HOST="$("$CC" -dumpmachine)"

后续配置 FreeType 时使用:

bash
./configure \
  --build="$BUILD" \
  --host="$HOST"

不要将 Ubuntu 主机的架构作为目标平台,也不要直接套用其他 SDK 的工具链前缀。

注意: FreeType 动态库和后续字体显示程序应使用同一套交叉编译工具链。这样可以避免不同工具链之间的 C 库、ABI 和链接环境不一致。

3. 配置 FreeType ​

本实验使用 TTF/TTC 字体并输出灰度位图,不需要 PNG 字体、BZip2 压缩字体和 HarfBuzz 字形整形功能,因此关闭这些可选依赖,避免额外移植依赖库。

执行配置:

bash
./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. 编译并安装到临时目录 ​

执行编译:

bash
make -j$(nproc)

编译完成后,将库安装到当前源码目录下的 _install,不会覆盖 Ubuntu 主机的系统库:

bash
make DESTDIR="$PWD/_install" install

安装完成后,主要文件位于:

text
_install/
└── usr/
    ├── include/
    │   └── freetype2/
    │       ├── ft2build.h
    │       └── freetype/
    └── lib/
        ├── libfreetype.so
        └── libfreetype.so.6

实际库文件可能带有更长的版本号,符号链接由安装过程生成,部署时应一并保留。

检查动态库架构:

bash
aarch64-linux-gnu-readelf -h "$(readlink -f _install/usr/lib/libfreetype.so)" | grep Machine

输出应包含:

text
Machine: AArch64

5. 编写并交叉编译测试程序 ​

将下方完整 C 示例保存为 freetype_fb.c。

使用同一套泰山派工具链编译:

bash
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

检查可执行文件架构:

bash
aarch64-linux-gnu-readelf -h freetype_fb | grep Machine

输出应为 AArch64。

6. 准备字体文件 ​

测试程序需要一个包含中文字符的 TTF 或 TTC 字体文件。可以使用已有字体,也可以在 Ubuntu 中安装文泉驿正黑字体:

bash
sudo apt install fonts-wqy-zenhei

字体路径:

text
/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc

7. 部署到泰山派 ​

在 Ubuntu 主机准备部署目录,将测试程序、FreeType 动态库和字体放在一起:

bash
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 替换为开发板实际配置:

bash
scp freetype-demo.tar.gz root@开发板IP:/tmp/

在泰山派上解压并运行:

bash
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 显示环境。

c
#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 显示中英文字符待实测

注意事项 ​

  1. 交叉编译库和测试程序必须使用同一套泰山派 AArch64 工具链。
  2. --host 指定目标运行平台,不能填写 Ubuntu 主机架构。
  3. --prefix=/usr 指向目标板文件系统;配合 DESTDIR 暂存,不能直接安装到 Ubuntu 主机。
  4. 部署时需要保留 libfreetype.so 的符号链接和实际库文件。
  5. 字体文件需要单独部署,FreeType 不自带中文字体。
  6. 本示例只支持 RGB565 单平面 Framebuffer,不适用于 DRM/KMS 设备节点。
  7. bitmap.pitch 表示位图行跨度,不应直接用 bitmap.width 代替。

参考资料 ​