ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

FontCvtST生成STM32 emWin中文字库:从工具到控件显示全流程

FontCvtST生成STM32 emWin中文字库:从工具到控件显示全流程 做嵌入式界面开发特别是手上只有一块野火MINI想用emWin把界面做出“中文版”你大概率躲不开一个叫FontCvtST.exe的小工具。这工具看着不起眼却是把Windows字体转成STM32能用的C语言字库文件的关键一环。我第一次在野火MINI上做中英文切换界面时就卡在“如何在按钮上显示‘确定’两个字”“如何在文本框里回显中文数据”这两个看似基础、实际全是坑的问题上折腾了整整一个周末。这篇就把整个流程讲透从FontCvtST.exe生成.c文件到工程挂载再到按钮和文本框控件上能正常显示汉字一步都不跳把我踩过的坑也一并交代清楚。1. 野火MINI的显示机制和那个“必须生成.c文件”的汉字问题1.1 野火MINI的GUI显示方案野火MINI开发板上最常见的屏幕是2.8寸TFTLCD320x240分辨率驱动芯片多为ILI9341或ILI9325官方资料里一般都会附带一套完整的emWin早先叫uCGUI移植工程。这套GUI库很强大但它的字体体系和PC端完全不同它不直接认电脑里的TTF字体也不像串口屏那样自带字库芯片emWin的字体系统是由一张张“字符点阵数据表”组成的每个要显示的字符都得有对应的点阵数据挂在GUI_FONT结构体上。换句话讲你想在屏幕上显示“确定”“取消”“温度正常”就得先想办法把这些字的点阵数据按emWin要求的格式组织好链接进工程。纯英文环境下emWin自带ASCII字体开箱即用一旦涉及汉字因为汉字数量多、码位复杂官方库不会预置任何中文字体你必须自己生成。1.2 汉字的编码与点阵到底是怎么回事这里先把基础概念理清楚后面排查问题才不慌。汉字在计算机里有两个层面的存在形式编码层也就是汉字在内存/源码里怎么表示。常见的有UTF-8一个汉字3字节、GB2312/GBK一个汉字2字节、UTF-16/Unicode码点一个汉字1个或2个word。你在Keil里写的字符串常量本质就是按源文件编码格式存下来的一串字节。点阵层也就是每个字形实际长什么样。一个16x16的汉字点阵在单色1bpp模式下每行16个像素占2字节一共16行刚好32字节。这32字节里每一位是0还是1决定了LCD上这个汉字的笔画形状。FontCvtST.exe 干的事情就是把PC端系统字体宋体、黑体、微软雅黑等里你需要的那些字符按你指定的字号逐字“描”成点阵再把这些点阵数据连同字符编码信息一起打包成一个标准的GUI_FONT结构体以.c文件的形式输出。你把这个.c文件编译进固件emWin在显示字符串时就会按字符编码去查这个字体表找到对应的点阵数据画到屏幕上。所以“生成.c文件”不是它的独特癖好而是emWin这种嵌入式GUI的固有要求字库以常量数组的形式存在于Flash中运行时不需要文件系统也不需要外部存储器上电即可直接访问快且稳。代价就是你生成多少个字符就得占多少Flash而且改一个字都要重新生成一遍。2. FontCvtST.exe 实操从打开工具到导出.c文件2.1 界面认知与新建字体FontCvtST.exe 是野火资料包里自带的Windows小工具来源于Segger的FontCvt技术野火做了界面和功能上的定制。我建议你右键“以管理员身份运行”否则偶尔会遇到字体列表加载不全的问题尤其是在Win10/Win11上。工具界面非常克制就几个菜单。第一步点击File - New弹出标准的Windows字体选择框字体建议选宋体或微软雅黑。宋体的点阵风格更接近传统嵌入式屏笔画细、清晰微软雅黑偏粗适合做标题字但在小字号下容易糊。字形常规、粗体、斜体。一般界面用常规需要强调的标题用粗体。字号这里要特别强调一点字号单位是“像素”。野火MINI的屏幕是320x240如果做主界面16像素的汉字在屏幕上大约占整个屏幕宽度的1/20属于比较常规的选择如果做标题可以用24像素小字号不推荐低于12低于12的汉字笔画会挤成一团显示效果很差。选择好后点确定字体列表区域会出现一条新记录。到这里只是建了字体项目还没选要生成哪些字符这是下一步的核心工作。2.2 字符集的选择决定你的Flash还够不够用这一步是整个工具使用中最重要、也最容易被忽略的地方。很多人一上来就把整个GB2312字库全选生成一个几百KB的.c文件结果链接时发现STM32F103的Flash根本塞不下或者即便塞下了程序也所剩无几。关键在于你真正需要的汉字可能只有几十个。一个16x16单色汉字占32字节我列一下不同选择下的体积字符集方案字符数量字库体积16x16单色仅界面用词确定、取消、设置等30~50个约1~1.6KBGB2312一级汉字常用3755个3755ASCII约120KB完整GB23126763个6763ASCII约216KB野火MINI上常见的STM32F103RCT6Flash 256KB你还有程序代码、图标图片、协议栈真正能分给字库的空间非常有限。所以我的原则很清楚先列需求清单把界面上所有可能出现的汉字全部列进一个txt文档比如“确定、取消、返回、设置、开始、停止、温度、湿度、报警、正常……”。如果产品后续版本文字可能变动可以适当多选20%作为缓冲但绝不盲目全选。ASCII字符0x20~0x7E一定要一起带上界面上的数字、字母、英文标点都要用到否则中文能显示、英文和数字变空白你会非常痛苦。在FontCvtST工具的字符集设置界面里一般提供“Unicode范围选择”和“手动输入字符”两种方式。手动输入是最直接可靠的直接把你需求清单里的汉字复制粘贴进去工具会自动去重并按统一编码处理。2.3 格式参数单色还是抗锯齿FontCvtST里还有一项关键参数位深bit per pixel就是你一个字模用几位来表示一个像素。1bpp单色每个像素0或1只有前景色和背景色。体积最小显示锐利是嵌入式界面的主流选择推荐。4bpp / 8bpp抗锯齿每个像素可以有多级灰度显示效果更平滑适合在PC模拟器上做高保真UI但对STM32来说体积和渲染开销都偏高。16x16抗锯齿字模一个汉字约128字节量一大Flash立刻爆炸而且emWin在低主频MCU上渲染抗锯齿字体的速度明显变慢。如果你不是做需要视觉冲击力的产品界面强烈建议用1bpp。抗锯齿字库在2.8寸屏上的视觉提升并不值那个Flash和CPU代价。2.4 导出.c文件并读懂生成的结构配置完成后File - Save As文件类型选C File.c给它一个可读性强的名字比如HZ_Font16.c保存。然后用文本编辑器打开它你会看到一个类似这样的结构/* 字符信息表每个字符一行包含宽度/高度/位深和点阵数据指针 */ static const GUI_CHARINFO GUI_FontHZ16_CharInfo[128] { { 16, 16, 1, (void *)GUI_FontHZ16_CharData[0] }, /* ... */ }; /* 码位范围描述从那个字符到哪个字符对应上面的CharInfo */ static const GUI_FONT_PROP GUI_FontHZ16_Propa { 0x20, /* 起始码位 */ 0x7E, /* 结束码位 */ GUI_FontHZ16_CharInfo[0], GUI_FontHZ16_Propa_next, /* 链表下一段 */ }; /* 字体总入口代码里引用的就是它 */ const GUI_FONT GUI_FontHZ16 { GUI_FONTTYPE_PROP_16, /* 字体类型 */ 16, /* 字体高度 */ 16, /* 字体宽度 */ 1, /* 比例/间距相关 */ 1, (GUI_FONT_PROP *)GUI_FontHZ16_Propa };这里不同emWin版本的GUI_FONT结构体字段略有差异你不用纠结和我贴的完全一致关键是理解三个东西GUI_FontHZ16这是对外导出的字体句柄代码里extern它然后传给GUI_SetFont、BUTTON_SetFont、EDIT_SetFont即可。GUI_CHARINFO数组每个字符对应的点阵数据入口这个数组是有序的。GUI_FONT_PROP描述了一段连续的码位范围e.g. 0x20~0x7E的ASCII以及后续可能还有一段0x4E00~0x9FA5的中文区。emWin显示字符串时会解出每个字符的Unicode码点再按这个链表逐段查找。注意生成的文件里如果你的字符数量大通常会拆成多段GUI_FONT_PROP段与段之间用next指针串起来每段对应一个码位区间。千万不要自己手改这些数据结构容易改崩。3. 把生成的.c文件挂进工程让裸文本先跑通3.1 Keil工程里正确添加文件生成好的HZ_Font16.c在Keil里右键对应的分组选择Add Existing Files to Group把它加进去。然后检查两件事.c文件里是否#include GUI.h如果有确认工程里的Include Paths包含emWin源码的头文件目录。确认这个文件在Target里被勾选了编译别加进去之后发现构建设置里被Exclude掉了这种低级错误会浪费很多时间。编译一下如果报undefined symbol或者cannot open file GUI.h优先检查文件是否真的被加入编译以及头文件路径是否配全。3.2 源文件编码、工具编码、emWin解码三方必须一致这是汉字显示能否成功的命门九成以上的乱码都出在这一关上。我单独拎出来说清楚。你在Keil里写字符串常量例如GUI_DispStringAt(野火MINI, 0, 0);编译后这个字符串在Flash里是什么字节序列取决于你保存这个.c文件时的编码格式。而FontCvtST.exe生成的字体表每个汉字是按某个Unicode码点存的。emWin显示字符串时会按它当前配置的多字节解码方式把字符串字节序列解成Unicode码点再去字体表里匹配。三方要对的编码方案我推荐一套最不容易出错的组合也是野火官方工程里常见的做法Keil源码文件统一为UTF-8编码。具体操作在Keil里打开源文件菜单Edit - Configuration - Editor - Encoding选择UTF-8或者干脆用记事本/VS Code把源文件另存为UTF-8。注意改了编码之后源文件里以前写的汉字要检查一下有没有变成乱码不要越搞越乱。emWin启用UTF-8解码。在初始化代码里GUI_Init()之后显式调用一次GUI_UC_SetEncodeUTF8()老版本可能需要你在GUI_X.c或GUIConf.c里置GUI_SUPPORT_GB2312相关宏具体以你的emWin版本和移植文件为准。FontCvtST生成字库时的字符输入本身也要以同一套Unicode语义输入。因为工具是在PC上运行输入框里你输入的就是Unicode语义的字符生成时它直接按字符码位存表所以问题不大真正容易出错的是前两步。如果这三处不一致比如Keil用GB2312保存源文件而emWin按UTF-8解码那“确定”两个字的字节序列会被拆成完全错误的码点字体表里根本查不到屏幕上就是乱码或者空洞。3.3 先做最小验证裸文本显示不要一上来就折腾按钮和文本框控件。先在窗口绘制回调里用最原始的API验证整条链路void _cbMainWin(WM_MESSAGE * pMsg) { switch (pMsg-MsgId) { case WM_PAINT: GUI_SetFont(GUI_FontHZ16); GUI_SetColor(GUI_BLACK); GUI_DispStringAt(温度正常, 10, 10); break; default: WM_DefaultProc(pMsg); break; } }如果这里正常显示“温度正常”说明字库文件、编码配置、emWin解码链路全部打通再去碰控件。如果这里就乱请先回头检查上一节的三方编码不要带着编码问题继续往下走否则控件上出的问题会干扰你判断。4. 把汉字搬到按钮和文本框控件上4.1 按钮控件BUTTON_SetFont BUTTON_SetText缺一不可很多人第一反应是“按钮创建后指定字体就好了”但实际往往看到按钮上的文字还是默认字体样式或者干脆不变。这是因为emWin按钮控件的字体和文本内容是两个独立的状态字体控制文字怎么画通过BUTTON_SetFont设置。文本内容控制画什么字创建时传入的capText只是初始值切换字体不会自动重新解析并重绘字符串。标准做法是两行都要写hBtn BUTTON_CreateEx(60, 80, 80, 36, hMainWnd, WM_CF_SHOW, 0, ID_BTN_OK); BUTTON_SetFont(hBtn, GUI_FontHZ16); BUTTON_SetText(hBtn, 确定);BUTTON_SetText内部会处理字符串重绘不需要额外WM_InvalidateWindow但如果遇到改完文字不刷新手动调用一次WM_InvalidateWindow(hBtn)基本能解决。如果按钮要显示两行字比如“开 始”文字里带中文空格\x20\x20或直接\n前提是字体高度除以行数后每行还有足够可读的高度。16号字写两行会有点挤建议两行按钮用24号字或者改用两张图。4.2 文本框控件EDIT_SetFont 与缓冲区大小的隐藏坑标题里说的“文本框”在emWin里对应的是EDIT控件多行编辑用MULTIEDIT。它的设置思路和按钮类似hEdit EDIT_CreateEx(100, 50, 180, 30, hMainWnd, WM_CF_SHOW, 0, ID_EDIT_TEMP, 64); EDIT_SetFont(hEdit, GUI_FontHZ16); EDIT_SetText(hEdit, 温度正常);看到那个64了吗这是EDIT控件的内部文本缓冲区大小很容易被忽略。它的计算逻辑要特别留意UTF-8编码下一个汉字占3字节一个英文字母或数字占1字节。如果文本框要显示“温度正常”4个汉字UTF-8下就是12字节加结束符\0至少13字节你BufferSize要是只给4那“温”字的3字节加上结束符就满了后面三个字直接丢失甚至可能越界踩坏内部内存。稳妥的公式BufferSize 期望最大汉字个数 * 3 1。如果你是在PC模拟器上调试因为PC端内存大缓冲溢出可能不明显一旦到了STM32上旁边控件的内存被踩了可能出现各种诡异现象——按钮文字莫名其妙变了、触摸点击区域偏移、甚至HardFault。所以在初始化时就把BufferSize算够是最省心的。4.3 静态文本框TEXT控件显示汉字顺手一并处理很多人会把TEXT控件当成“文本框”来用就是那种只显示、不输入的标签。它的设置和EDIT类似但少了缓冲区的问题hText TEXT_CreateEx(10, 10, 120, 25, hMainWnd, WM_CF_SHOW, 0, ID_TEXT_1, 湿度45%); TEXT_SetFont(hText, GUI_FontHZ16);值得注意的是TEXT控件默认不会自动换行字符串超出控件宽度会被截断所以创建时给的宽度要预估好或者用TEXT_SetWrap(hText, 1)开自动换行部分版本支持。因为TEXT控件没有内部缓冲区它只是保存字符串指针所以你要保证传入的字符串生命周期有效别在函数里定义一个局部数组传进去就返回了。4.4 动态回显串口收到汉字数据如何显示到文本框里这个场景在对接温湿度传感器、报警器、上位机时非常常见。串口发来一句带汉字的字符串你要在EDIT控件里回显出来。我踩过的坑也在这里。流程其实简单// 假设串口接收完成中断置了一个标志rxBuf里是一帧完整数据 if (revFlag) { EDIT_SetText(hEdit, (const char *)rxBuf); revFlag 0; }但有个前提rxBuf里的字节序列必须是一帧完整的汉字编码。如果上位机一次发4个汉字分成了两包第一包发了前7个字节第二包发了后5个字节一个汉字被拆到两包那你设置文本时拿到的是残缺的字节序列UTF-8解码会失败显示非乱码不可。解决办法有两种从上位机侧保证汉字字符串按帧发送帧结束符\n或\r\n确认整帧收完后再提交显示。这是推荐做法简单可靠。MCU侧做缓冲重组维护一个环形缓冲区收到字节先存入等检测到帧结束符或超时后再把完整的一帧交给EDIT_SetText。另外如果你的汉字字符串来自第三方设备比如某些模块回传的GB2312编码文本而你的emWin配置的是UTF-8解码那就要先做编码转换把GB2312字节流转成UTF-8再给控件。这个转换可以做成一张表查表转换也可以用现成的库如果你Flash充裕的话。转换逻辑不复杂但却是很多人在项目联调时才发现的问题PC端调试一切正常一到真实设备回传数据就乱码。5. 乱码、黑块、死机常见问题与完整排查链路5.1 现象一显示成空心方块或黑块屏幕上一堆方块或黑块通常是这个字符在字体表里查不到。有可能是FontCvt生成时漏了这个字或者字符集覆盖范围不够同一个字你生成的字体表里是简体显示字符串里输入的是繁体/特殊符号。排查链路先用一个确定在字符集里的字做裸文本测试如果能显示再逐步逼近出问题的那个字如果确定是漏字回到FontCvtST把缺的字加进去重新生成.c重新编译即可。5.2 现象二乱码偏旁部首错乱或者字符替换成问号这是最典型的编码不一致问题。按下表快速定位表现原因验证方法全部乱码包括英文字母后的汉字Keil源文件编码与emWin解码不匹配把源文件另存为UTF-8初始化时调用GUI_UC_SetEncodeUTF8()部分汉字错但英文正常字库字符集本身不完整核对FontCvt生成时的字符清单显示问号或空白emWin根本解不出这个码点的UTF-8序列检查字符串字节断点看内存里的字节序列是否完整我举个真实例子同事用VS Code写代码默认UTF-8在Keil里直接打开编辑工程里其他文件是GB2312两者混在一起。结果按钮上的“确定”显示成“纭畾”这种莫名其妙的东西。最后统一把工程所有.c/.h源文件转为UTF-8编码问题瞬间消失。5.3 现象三编译提示Flash空间不足或者链接报未定义符号Flash空间不足最简单直接的就是字库太大了。这时候先看你的字符集是不是全选了GB2312如果是赶紧换成只包含需求清单里的字。如果文字确实多可以考虑缩小字号从24降到16体积能减少一半以上用单色确认位深是1bpp而不是4bpp/8bpp拆分字体表把界面上最常用的词生成一个小字体表常驻Flash把不常用的词放外部Flash或字库芯片按需加载。链接报undefined symbol GUI_FontHZ16则查三件事.c文件有没有真的加入编译文件名拼写和extern声明是否一致是否有条件编译宏把整个函数体包裹了导致实际没生成目标代码。5.4 一个容易忽略的配置Keil编译优化和字节对齐最后说一个比较隐蔽的坑。如果你的字库非常大或者编译器开了高优化等级然后运行时发现极少数汉字在屏幕上会错位、花点、偶发花屏不一定是字库生成错了可能是结构体对齐问题。GUI_CHARINFO和GUI_FONT_PROP里的数据指针、码位字段某些编译器下会要求2字节或4字节对齐。如果GUI_CharData数组本身没对齐可能导致指针取数据时错位。大部分时候野火官方库的移植已经处理好了对齐问题但如果你自己魔改了启动文件或者链接脚本建议在字库文件顶部用编译指令强制对齐__align(4) static const unsigned char GUI_FontHZ16_CharData[] { ... };不同编译器写法不一样MDKARMCC/AC6下__align(4)或__attribute__((aligned(4)))都可以试。这个问题不是每个人都会遇到但一旦遇到会非常折腾排查优先级可以放在编码问题之后。5.5 小技巧用字符清单文件管理字库版本最后分享一个我做产品时养成的小习惯能帮你少走很多弯路。在工程目录下放一个chars.txt专门记录当前版本界面用到的所有汉字一行一句带注释确定 取消 设置 返回 温度正常 报警每次要改文字先更新这个文件再打开FontCvtST全选粘贴重新生成字库。这样做的好处是你以后再也不用来回翻UI代码统计用了哪些字也让字库的“最小化”有了依据——只生成当前需要的字Flash占用压到最低。我接手过好几版工程就是因为早期没有这个清单每次加一个新界面词都要整套重新生成还经常漏字有了清单之后这件事彻底变成例行公事。另外如果项目里同时需要16号、24号两套中文字体我建议你分别命名HZ_Font16.c/HZ_Font24.c代码里对应GUI_FontHZ16/GUI_FontHZ24避免混淆。别问我为什么强调这个等你某天调了半小时发现按钮用的是16号字体、给的是24号字体句柄还死活找不出问题就知道这个命名规则有多重要了。
返回列表