简介:面向 STM32F103ZET6 开发者的 STemWin 图形界面实验例程包,聚焦 TTF 格式字体显示功能,适合希望在资源有限的嵌入式平台实现高质量文字渲染的工程师或学习者。压缩包共 954 个文件,约 25.65MB,其中包含 409 个头文件、172 个 C 源码文件,以及大量编译链接产物(o、d、crf、axf、hex 等),还附带了工程配置与脚本文件,便于直接打开工程查看或重新构建。已有 64 人浏览学习。例程覆盖 STemWin 库初始化、字体资源加载、字符与字符串绘制等核心步骤,并包含 STM32F103ZET6 的时钟、GPIO、中断等底层硬件配置示例。通过阅读和运行源码,开发者可以快速掌握在 STM32 上引入 TTF 字体的完整流程,同时理解 GUI 绘制与字体渲染的平衡优化方法,为嵌入式产品界面开发提供可直接借鉴的参考模板。
1. 用 TTF 在 STM32F103ZET6 上显示矢量字体,先解决什么?
用 2.8 寸 320×240 LCD 做产品界面时,点阵字库放大到 24 像素以上全是锯齿,标题还没法看。想让界面达到接近手机那种平滑字体效果,STM32F103ZET6 + STemWin 的 TTF 字体显示是完整可用的一条路。TTF/OTF 字体用轮廓曲线描述字形,STemWin 在运行时按需把轮廓光栅化成位图,任意字号放大都不糊,中文、西文、符号统一走同一套机制。但很多例程下载下来编译通过、屏幕却只出方块,问题出在三个地方:TTF 资源以什么形式进工程、GUI 内存池够不够大、字符编码开没开。这篇文章按这个顺序把链路讲透,适合正在给 F103 加图形界面的工程师,也适合做毕设想真正跑通 STemWin 矢量字体的人。
2. STemWin 的 TTF 显示原理与内存/Flash 资源评估
2.1 STemWin 把 TTF 变平滑的关键:字形引擎与字形缓存
STemWin 是 Segger emWin 面向 ST 芯片的授权重编译版本,它对 TTF 的支持和 PC 上“装个字体就能用”完全不同。MCU 上不可能把整个 TTF 解成一整张大位图放进内存,所以 STemWin 内部是两层结构:第一层是字形引擎,解析 TTF 文件里的二次贝塞尔曲线轮廓,按当前字号和像素格式光栅化成小块位图;第二层是字形缓存(glyph cache),把这些小块位图临时保存下来,同一字符再次绘制时直接取缓存,不再跑一遍曲线解析。
这带来两个直接结论:第一次显示新字符时耗时明显,后续相同字符串重绘会快很多;缓存配小了就会反复“解析-丢弃”,界面表现为文字刷新慢、拖动窗口时发虚。理解了这两层,后面调内存池大小、判断“为什么 TTF 比点阵慢”就都有依据了。
2.2 为什么 ZET6 上外扩 SRAM 几乎是标配
STM32F103ZET6 有 512KB Flash、64KB SRAM,听起来不算小,但一接触图形就紧张了。一块 320×240、16bpp RGB565 的屏,单帧缓冲需要 320×240×2 = 150KB,已经超出内部 SRAM;如果还要给 STemWin 的 GUI 内存池和 TTF 字形缓存留空间,内部 RAM 完全不够用。
所以 STM32F103ZET6 的 STemWin TTF 例程,板级设计几乎都是 FSMC 外扩 SRAM,常见搭配是 IS62WV51216(512KB、16bit)映射在 Bank1 的 NE1 区。LCD 显存、STemWin 内存池、TTF 字形缓存都放在这 512KB 里,内部 64KB 留给堆栈和普通业务变量。不要指望把 GUI 内存池全放内部 SRAM 后跑 TTF,中文界面下基本跑不起来,光缓存几十个常用汉字的字形位图就要几十 KB。
// GUIConf.c 中的典型配置(STM32F103ZET6 + 外部 512KB SRAM) #define GUI_NUMBYTES (512 * 1024) // GUI 内存池大小,单位字节 #define GUI_SUPPORT_TTF 1 // 开启 TTF 矢量字体支持 #define GUI_SUPPORT_UNICODE 1 // 开启 Unicode/UTF-8 编码支持 #define GUI_DEFAULT_FONT &GUI_Font6x8 // 兜底字体,防止资源加载前无字体可用GUI_NUMBYTES是 STemWin 动态内存池的总量,所有窗口、控件、位图、字形缓存都从这里面分配。数值要根据外部 SRAM 容量和界面复杂度调整,512KB SRAM 里给它 300~400KB 比较合理,剩下留给 LCD 缓冲或直接由链接脚本划分。GUI_SUPPORT_TTF必须在编译 STemWin 库时就打开,否则GUI_TTF_*相关 API 不会被编进库,调用时直接链接报错。GUI_SUPPORT_UNICODE决定字符编码映射方式,处理中文 TTF 字体时这个宏必须为 1。
2.3 GUIConf 关键参数速查
| 宏名 | 推荐值 | 作用 | 设置错误的现象 |
|---|---|---|---|
GUI_SUPPORT_TTF | 1 | 启用 TTF 矢量字体引擎 | 链接期报GUI_TTF_*undefined |
GUI_SUPPORT_UNICODE | 1 | 支持 UTF-8/Unicode 字符映射 | 中文显示为空白或乱码 |
GUI_NUMBYTES | 300~400KB | GUI 动态内存池总大小 | 加载字体后黑屏、控件创建失败 |
GUI_DEFAULT_FONT | 系统内置字体 | GUI_Init 时的初始字体 | 初始字体资源未就绪时启动崩溃 |
提示:
GUI_NUMBYTES只是 STemWin 自己管理的池子,实际内存物理位置由链接脚本决定。用 FSMC 外扩 SRAM 时,要把这个池子放在外部存储区,方法是在分散加载文件中把外部 SRAM 区域划入堆区,否则编译器默认仍只在内部 RAM 里分配。
3. 把 TTF 字体资源挂进工程:编译期 C 数组与运行时加载
3.1 两条路线怎么选
TTF 资源进入 STemWin 工程有两条主流做法,官方例程两种都会用到。区别在于“字体文件放哪”和“字号固定不固定”:
| 对比项 | 编译期 C 数组 | 运行时文件加载 |
|---|---|---|
| 资源形式 | TTF 经转换工具生成.c文件 | TTF 原文件放在文件系统(如 SPI Flash 的 FatFS) |
| API | GUI_SetFont(&GUI_FontXXX) | GUI_TTF_CreateFont() |
| Flash 占用 | 只含所选字符与字号,几十 KB | 整个 TTF 文件,中文字体常达 3~10MB |
| 改字号 | 重新转换、重新编译 | 运行时传参即可 |
| 依赖 | 无文件系统 | 需要 FatFS 或 STemWin 存储设备抽象层 |
我一般这样选:产品界面固定、需要快速启动,选编译期 C 数组;需要多语言动态切换、字号热切换,选运行时加载。多数实验例程是前者,因为它不依赖文件系统,下载后直接跑。
3.2 用 Font Converter 把 TTF 转成 STemWin 的 C 字库
STemWin 安装目录的 Utility 里有 Segger 的字体转换工具,操作路径大致固定:打开工具,加载一个 TTF 文件(如simhei.ttf),选择目标字号和字符范围,导出为 C 文件。这个环节最影响 Flash 占用的是字符范围,如果全选,就连中文带符号全导进 Flash,一个 16 号字库能到几百 KB;只选 ASCII + 常用中文 3500 字,能压到 100KB 左右。
// 转换导出的 C 文件里,核心结构大致是这样 // GUI_FontSimHei_24 是工具生成的全局字体对象 const GUI_FONT GUI_FontSimHei_24 = { GUI_FONTTYPE_PROP_VAR4x4, 24, // 字体高度像素 24, // 基线位置 1, // 水平间距 &GUI_FontSimHei_24_Prop, // 字符属性表 GUI_GETSTATE_GETSTATE_PROP_VAR4x4 };生成后的.c文件加入工程,头文件里声明extern GUI_FONT GUI_FontSimHei_24;就能直接使用。工具导出的是已光栅化的位图字模,不再依赖 TTF 原始文件。注意生成时选择的像素格式要和 LCD 一致,例如 RGB565 的屏就选 16bpp,否则显示出来颜色值错乱。
3.3 最小代码:初始化字体并绘制一行 TTF 文本
编译期 C 数组路线的最小可运行代码:
#include "GUI.h" extern GUI_FONT GUI_FontSimHei_24; // 由转换工具生成的 C 文件提供 void TTFFont_Demo(void) { GUI_Init(); // 初始化 STemWin 内核与内存池 GUI_SetBkColor(GUI_WHITE); // 设置背景色并清屏 GUI_Clear(); GUI_SetColor(GUI_BLUE); // 设置前景色 GUI_SetFont(&GUI_FontSimHei_24); // 挂上由 TTF 转换来的 24px 字体 GUI_DispStringAt("TTF Smooth Font", 10, 20); // 从 (10,20) 开始输出 GUI_DispStringAt("STM32F103ZET6", 10, 50); while (1) { GUI_Delay(10); // 交给 STemWin 调度,看门狗也在这里喂 } }GUI_Init必须在任何绘图 API 之前调用,它负责初始化内存池和默认字体。GUI_SetFont传入的是转换工具生成的全局字体对象,不是 TTF 文件名,这一点和 PC 端思维不同——编译期路线在运行时已经不存在“解析 TTF 轮廓”这回事,GUI_DispStringAt只是在内存里查表复制字模。如果显示内容只有固定菜单和状态栏,这种方案足够。
运行时加载的路子是另一套 API,适合需要动态切字号或多语言包的场景:
#include "GUI.h" #include "GUI_TTF.h" GUI_FONT g_fontTTF24; void TTFFont_LoadFromFS(void) { GUI_Init(); // 从 FatFS 卷“0”里读取 simhei.ttf,目标高度 24 像素 GUI_TTF_CreateFont(&g_fontTTF24, "0:/font/simhei.ttf", 24, 0); GUI_SetFont(&g_fontTTF24); GUI_DispStringAt("Load TTF from FS", 10, 20); }GUI_TTF_CreateFont的第二个参数是文件系统路径,第三个参数是像素高度,第四个参数是绘制标志位(0 表示默认抗锯齿开启)。不同 STemWin 版本对第四个参数的宏定义名称略有差异,以你手上版本的GUI_TTF.h头文件为准。运行时加载对内存池压力更大,因为字形缓存和待解析字符都要同时驻留,如果GUI_NUMBYTES不够,GUI_TTF_CreateFont之后绘制没有任何输出,这是最常见的坑之一。
提示:运行时加载不需要在编译期生成字库,但 TTF 文件本身必须放在外部 Flash 等文件系统里。没有文件系统的板子,建议走编译期 C 数组路线,少一层驱动依赖。
4. 在 STM32F103ZET6 上跑通 STemWin 的 TTF 字体显示与排错
4.1 初始化顺序:从时钟、FSMC 到 GUI_Init
TTF 显示不只是“调一个字体函数”的事,底层初始化顺序错了,界面直接黑屏。常见例程的完整顺序是:
- 配置系统时钟,F103ZET6 典型是 72MHz 主频,APB1=36MHz、APB2=72MHz。
- 初始化 FSMC,同时挂 LCD(Bank1 NOR/SRAM 区)和外部 SRAM。LCD 用 NE1,SRAM 也用 NE1 时要错开片选,或 SRAM 用 NE3。
- 初始化 LCD 驱动,至少调用一次背光控制和读写测试,确认显存可访问。
- 调用
GUI_Init(),这会使用 FSMC 映射好的外部 SRAM 区初始化 GUI 内存池。 - 加载 TTF 字体(C 数组路线直接
GUI_SetFont;文件路线先GUI_TTF_CreateFont)。 - 创建窗口或调用
GUI_DispStringAt输出文本。
第 2、3 步是最容易出问题的地方。FSMC 时序配置错,表现为 LCD 白屏或花屏,和字体代码没关系。调试时先用GUI_DispStringAt配合内置字体如GUI_Font8x16输出一段文本,确认基础显示链路通,再切到 TTF 字体。
// 一个用于确认底层显示链路的自检函数,放在 TTF 调试前调用 void LCD_PreCheck(void) { GUI_Init(); GUI_SetBkColor(GUI_BLACK); GUI_Clear(); GUI_SetColor(GUI_GREEN); GUI_SetFont(&GUI_Font8x16); // 内置字体,不依赖 TTF GUI_DispStringAt("FSMC LCD OK", 10, 10); }这段自检代码用内置字体,排除 TTF 资源问题。屏幕上看到FSMC LCD OK后,再去换 TTF 字体。看不到,说明 FSMC 时序、引脚映射或 LCD 初始化驱动有问题,先解决这部分。
4.2 常见显示异常与排查对照
| 现象 | 大概率原因 | 先查什么 |
|---|---|---|
| 黑屏或白屏,无任何输出 | FSMC 时序、LCD 驱动、背光 | 用内置字体做自检 |
| 文字是方框或空格 | TTF 里没有对应字形,或编码没映射上 | 检查字符范围是否覆盖目标文字 |
| 中文正常、西文异常 | TTF 转换时只选了中文字符范围 | 转换工具里同时勾选 ASCII 子集 |
| 文字显示但刷新很慢 | 字形缓存太小,反复解析轮廓 | 加大GUI_NUMBYTES,或预生成 C 数组 |
| 边缘特别毛糙,不像矢量字体 | 抗锯齿关闭或像素格式不支持 | 检查 TTF 创建时 flags 参数 |
| 程序卡死在字体加载 | 文件系统路径错误或内存分配失败 | 确认路径卷号、文件是否存在,加大内存池 |
这里要特别强调“方框”和“慢”是两种最常见的误判。方框不一定是编码问题,很多时候是转换工具导出时只导了 ASCII 字符集,中文全部落在“未定义字形”区间;慢则多半不是 CPU 主频不够,而是缓存没设置好,同一个字符串在窗口重绘时每次都重新解析轮廓。
4.3 中文显示与字符集裁剪:别把整个 TTF 都放进 Flash
中文 TTF 字体文件动辄 3~10MB,STM32F103ZET6 内部 Flash 512KB 根本放不下,所以必须做字符集裁剪。编译期路线用转换工具的字符范围选择功能,只勾选需要用的汉字;运行时路线则要单独做一次子集化,常用的做法是:
// 常用字集 JSON 配置示例,供字符集裁剪脚本使用 { "ascii": true, "common_hans": true, "custom_chars": "电压电流温度设置确认返回", "min_font_size": 12, "max_font_size": 32 }按这份配置生成的裁剪后 TTF,通常能把体积从几 MB 压到 200~400KB,放进外部 SPI Flash 或直接转 C 数组都可行。这里有个容易忽略的细节:运行时要显示的内容集一定要比裁剪字符集“窄”,否则用户操作到某个未裁剪的汉字时,界面会出一个永远渲染不出来的方块,比直接乱码更难排查。我通常在编码阶段就把所有用户可见字符串抽到一个常量文件里,脚本自动提取字符集,避免手工维护漏字。
// 中文显示最小示例:裁剪后的字体 + UTF-8 字符串 extern GUI_FONT GUI_FontSimHei_24; void ChnText_Demo(void) { GUI_Init(); GUI_SetBkColor(GUI_WHITE); GUI_Clear(); GUI_SetColor(GUI_RED); GUI_SetFont(&GUI_FontSimHei_24); GUI_DispStringAt("设置完成", 10, 30); // 源码文件保存为 UTF-8 编码 }源文件编码必须和 STemWin 的 Unicode 配置一致。Keil 里默认可能把源文件保存为 GB2312,即使开了GUI_SUPPORT_UNICODE也可能映射错位。我的做法是统一要求源码用 UTF-8 编码,并在 Keil 的 Encoding 选项里设置,这样GUI_DispStringAt传入的字节串和转换工具导出的字符索引才能对齐。
5. TTF 字体显示的进阶优化:缓存、抗锯齿与渲染耗时验证
5.1 用内存设备缓存复杂界面的渲染结果
TTF 文本每次绘制都要在字形缓存里查一遍,窗口移动、刷新时如果整个界面都重绘,CPU 占用会明显上升。一个高效做法是用 STemWin 的内存设备(Memory Device)先渲染一次,后面只是整块拷贝。适合菜单标题、状态栏这种不变区域。
#include "GUI.h" #include "WM.h" GUI_MEMDEV_Handle hMemDev; void CreateCachedTTFRegion(void) { hMemDev = GUI_MEMDEV_Create(0, 0, 200, 40, GUI_MEMDEV_HASTRANS); GUI_MEMDEV_Select(hMemDev); // 在内存设备里绘制一次,字体轮廓只解析一次 GUI_SetBkColor(GUI_WHITE); GUI_Clear(); GUI_SetColor(GUI_BLUE); GUI_SetFont(&GUI_FontSimHei_24); GUI_DispStringAt("Cached Title", 5, 5); GUI_MEMDEV_Select(0); }窗口回调里处理WM_PAINT时直接调用GUI_MEMDEV_WriteAt(hMemDev, x, y),屏幕重绘只是位图复制,不再触发字形轮廓解析。这个技巧对 TTF 字体尤其有效,因为矢量字体的光栅化开销远大于位图字体的查表复制,缓存一次能省掉绝大部分 CPU 周期。
5.2 实测一次绘制的耗时,再决定开不开抗锯齿
抗锯齿让字体边缘平滑,但光栅化要处理更多灰度级,渲染耗时明显上升。做个量化测试再决定是否开启:
#include "core_cm3.h" void MeasureTTFRenderTime(void) { GUI_RECT rect = {0, 0, 300, 50}; uint32_t t0, t1; // 使能 DWT 时钟周期计数器 CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; DWT->CYCCNT = 0; t0 = DWT->CYCCNT; GUI_SetFont(&GUI_FontSimHei_24); GUI_DispStringInRect("STM32F103ZET6 TTF Render", &rect, GUI_TA_CENTER); t1 = DWT->CYCCNT; // 72MHz 主频下,周期数除以 72 就是微秒数 printf("TTF render: %lu cycles = %.2f ms\r\n", (unsigned long)(t1 - t0), (float)(t1 - t0) / 72000000.0f * 1000.0f); }DWT->CYCCNT是 Cortex-M3 内核自带的周期计数器,精度 1 个时钟周期,不占用定时器资源。注意要在主频配置完成后使用,否则换算关系不对。打印出来的耗时如果是毫秒级,说明字体光栅化在每次重绘都会产生明显延迟,建议把它放进内存设备缓存;如果只是个位毫秒级且界面不频繁重绘,抗锯齿可以放心开着。字号小于 18px 时抗锯齿收益不大,但开销并不小,这个场景可以关闭抗锯齿或直接改用点阵字库,也能省下一块字形缓存。
本文还有配套的精品资源,点击获取