1. 为什么Emwin中文显示是个“技术活”?
很多刚接触Emwin的朋友,尤其是从STM32这类32位单片机入手的,都会遇到一个共同的“拦路虎”:为什么在模拟器上跑得好好的界面,一放到单片机上,中文就变成了一堆“乱码”或者干脆不显示?这背后的原因,其实和Emwin这个GUI库的“出身”以及嵌入式系统的资源限制紧密相关。
Emwin,或者说STemWin,是SEGGER公司为嵌入式系统开发的一套图形库。它默认是为西方语言设计的,核心支持的是ASCII字符集。ASCII字符集很小,一个字符用一个字节(8位)就能表示,处理起来非常高效。但中文属于双字节字符,一个汉字在Unicode编码下通常需要两个甚至更多字节来表示。Emwin本身并不“认识”这些多字节编码,它需要一个“翻译官”——也就是我们常说的字库——来告诉它某个编码对应的汉字长什么样。
在PC模拟器上开发时,我们通常使用的是Windows系统自带的TrueType字体(比如宋体、黑体)。这些字体文件包含了海量的字符,显示中文自然不在话下。但问题来了,一个完整的16x16点阵中文字库,可能就需要几百KB的存储空间;如果是24x24甚至更大的字体,轻松上MB。这对于Flash可能只有几百KB的STM32F103这类单片机来说,是根本无法承受的。
所以,Emwin中文显示的核心矛盾就出现了:如何在资源极其有限的单片机里,既能让界面显示漂亮的中文,又不至于把宝贵的存储空间撑爆?我刚开始做项目时,也在这里卡了很久,试过各种方法,最终发现,一套从模拟器到单片机的无缝字库移植方案,才是最高效、最实用的路径。下面我就把自己踩过坑、验证过的方法,一步步分享给你。
2. 模拟器环境下的中文显示实战
在真刀真枪地给单片机移植之前,我们最好先在PC的模拟器上把中文显示调通。这里有个巨大的好处:调试速度快,所见即所得。我用的是VS2013配合Emwin的模拟器,你也可以用更高版本的VS,原理都一样。
2.1 工具准备:你的“瑞士军刀”
工欲善其事,必先利其器。在开始前,请确保你手头有这几样工具,它们都是SEGGER官方提供或常用的辅助工具:
- Visual Studio:用于运行Emwin模拟器工程。我用的2013,你用2017、2019甚至VS Code搭配相应插件都行。
- GUI Builder:SEGGER提供的可视化界面设计工具。用它拖拽控件生成代码,能省去大量手写UI布局的时间,非常直观。
- emWin Font Converter:字库转换的核心工具。它能把电脑上的TrueType字体,转换成Emwin能识别的C语言字体文件(.c格式)。
- U2C(UTF-8 to C)工具:这是一个小巧但关键的工具,负责将包含中文的UTF-8编码文本文件,转换成C语言中的字符串数组。网上有很多开源版本,SEGGER有时也会提供。
提示:这些工具在SEGGER官网都能找到,可能需要注册账号。如果找不到U2C,也可以自己写个小程序,原理就是读取UTF-8文件,将每个字节转换成“\xXX”的格式输出到C数组。
2.2 第一步:用GUI Builder搭建你的界面原型
别急着写代码,先用GUI Builder把界面画出来。新建一个对话框,拖几个TEXT控件、BUTTON控件、EDIT控件上去,把它们的ID和默认文本(先用英文)设置好。比如,你可以放一个标签显示“温度:”,一个编辑框用来输入,一个按钮写着“提交”。
画好后,点击保存,GUI Builder会生成一个.c文件和一个.h文件。把它们添加到你的VS模拟器工程中。这时候编译运行,你应该能看到一个全英文的界面。这一步的目的是确保你的工程基础配置和控件创建逻辑是正确的,为后续替换中文字符串扫清障碍。
2.3 第二步:生成你的专属中文字库
这是最关键的一步,目的是生成一个只包含你需要的汉字的“迷你字库”。
准备字库原料:新建两个文本文件。
font_unicode.txt:用Windows记事本保存,编码选择“Unicode”(在Win10/11里通常是“UTF-16 LE”)。在这个文件里,把你界面上所有要用到的中文汉字、标点都写进去。比如:“温度设置提交取消”。font_utf8.txt:同样用记事本保存,内容完全一样,但编码选择“UTF-8 with BOM”(带BOM的UTF-8)。这两个文件内容一致,但编码不同,各有用途。
使用emWin Font Converter生成字体C文件:
- 打开emWin Font Converter。
- 字体类型选择“Standard”就行,这是最通用的点阵字体格式。
- 在
Font下拉框里选择你想要的字体,比如“SimSun”(宋体)。Size选择你需要的像素大小,比如18。Font style可以选择常规或粗体。 - 关键一步:在
Characters区域,先点击“Disable all”禁用所有字符。因为我们不需要完整的ASCII表,那样文件会很大。 - 然后,点击“Enable range”,输入
0x00到0x7F。这个范围包含了数字、字母和常用英文标点,是必须的。 - 接下来,点击“Read characters from file”,选择你刚才准备好的
font_unicode.txt文件。这时软件会读取文件中的汉字,并将它们加入到转换列表中。 - 最后,点击“Save As...”,保存类型选择“C File”,命名为比如
GUI_FontSong18.c。这个文件里就包含了汉字和基本ASCII字符的点阵数据。
使用U2C工具生成字符串C文件:
- 打开U2C工具。
- 加载你的
font_utf8.txt文件。 - 设置输出为C文件,比如命名为
ChineseString.c。 - 点击转换,你会得到一个C文件,里面是类似这样的数组:
const char TEMPERATURE[] = "\xe6\xb8\xa9\xe5\xba\xa6"; // “温度”的UTF-8编码 const char SETTING[] = "\xe8\xae\xbe\xe7\xbd\xae"; // “设置”
这个文件的作用是,在代码里我们可以用
TEMPERATURE这个易读的变量名,来代替一长串晦涩的十六进制编码。
2.4 第三步:在模拟器工程中集成与调用
现在,把生成的两个C文件(GUI_FontSong18.c和ChineseString.c)都添加到你的VS工程里。
- 声明字体:在界面代码文件(GUI Builder生成的那个.c文件)的开头,添加外部字体声明:
/* 外部声明我们自定义的字体 */ extern GUI_CONST_STORAGE GUI_FONT GUI_FontSong18; - 引入字符串变量:把
ChineseString.c文件里的那些const char数组定义,复制或者通过extern声明到你的界面代码文件中。extern const char TEMPERATURE[]; extern const char SETTING[]; // ... 其他字符串 - 替换控件文本和字体:在你界面的初始化函数里(通常是
WM_INIT_DIALOG消息处理部分),找到各个控件,为它们设置中文字体和文本。/* 初始化‘温度’文本标签 */ hItem = WM_GetDialogItem(pMsg->hWin, ID_TEXT_0); // 假设ID_TEXT_0是温度标签 TEXT_SetFont(hItem, &GUI_FontSong18); // 设置字体 TEXT_SetText(hItem, TEMPERATURE); // 设置中文文本 /* 初始化按钮 */ hItem = WM_GetDialogItem(pMsg->hWin, ID_BUTTON_0); // 提交按钮 BUTTON_SetFont(hItem, &GUI_FontSong18); BUTTON_SetText(hItem, SUBMIT); // SUBMIT是对应“提交”的字符串变量
编译并运行模拟器,如果你的步骤都正确,现在应该能看到一个完美显示中文的界面了!这一步的成功,意味着你的字库生成和编码转换流程是通的,为单片机移植打下了最坚实的基础。
3. 从模拟器到32单片机的无缝移植
在模拟器上成功显示中文,只算成功了一半。更重要的挑战是如何让这套界面和字库,在资源紧张的STM32等单片机上跑起来。好消息是,如果我们前期工作做得扎实,移植会非常平滑。
3.1 工程结构与文件迁移
首先,在你的单片机工程(比如Keil MDK或IAR工程)中,建立清晰的字体和GUI文件目录。
复制核心文件:将模拟器工程中的以下文件复制到单片机项目的相应目录:
GUI_FontSong18.c(你生成的字库C文件)ChineseString.c(你的中文字符串文件)YourDialog.c和YourDialog.h(GUI Builder生成的界面文件)- 你从SEGGER获取的Emwin库文件(
GUI_X.c,GUI_X_Touch.c等)和移植层文件。
配置工程路径:在IDE中,将这些新目录添加到头文件包含路径(Include Paths)中,确保编译器能找到它们。
3.2 存储空间的考量与优化
单片机Flash和RAM都很宝贵,所以我们需要做些优化:
- 字库存放位置:
GUI_FontSong18.c文件里是一个巨大的const数组,它会被编译器放到Flash中(通常是.text或.constdata段)。确保你的单片机Flash足够存放它。你可以通过编译后的map文件查看它具体占用了多少空间。 - 使用
const关键字:确保所有字库数据和字符串数据都被声明为const,这样它们就会被存储到Flash,而不是占用宝贵的RAM。 - 字体选择策略:如果空间非常紧张,可以考虑:
- 使用更小的字体尺寸(如16x16代替24x24)。
- 进一步精简字符集,只保留界面绝对用到的汉字,甚至拆分出多个小字库,按需加载。
- 考虑使用外置SPI Flash或SD卡来存储字库,运行时动态加载到RAM或内存映射区域。但这会复杂很多,需要实现文件系统和动态加载逻辑。
3.3 关键移植步骤与代码适配
移植的核心是确保Emwin库本身能在你的单片机上正确运行。这里假设你已经完成了Emwin的基础移植(如LCD驱动、触摸驱动等)。我们专注于中文部分:
包含头文件:在
GUI.h包含之后,确保包含你的字体和字符串头文件。#include "GUI.h" #include "YourDialog.h" #include "ChineseString.h" // 声明了中文字符串变量 /* 字体声明通常在字库.c文件里已定义,无需重复包含,但需确保链接 */初始化与调用:在你的主任务或GUI初始化函数中,创建对话框。字体和字符串的调用代码,和模拟器里一模一样,无需修改!这就是“无缝移植”的魅力所在。
void MainTask(void) { GUI_Init(); // 初始化Emwin // 创建对话框,这个函数由GUI Builder生成 CreateYourDialog(); while(1) { GUI_Delay(100); } }对话框创建函数
CreateYourDialog()内部,已经通过WM_INIT_DIALOG消息回调,完成了对所有控件字体和文本的设置。处理编码问题:确保你的工程编译选项和源文件编码正确。我强烈建议将所有源文件(
.c和.h)的编码统一设置为UTF-8 with BOM。在Keil中,可以在“Edit -> Configuration -> Editor”中设置编码。这可以避免源码中的中文注释或字符串常量变成乱码。
3.4 常见问题与调试技巧
第一次移植很可能不会一帆风顺,以下是几个我踩过的坑和解决方法:
屏幕一片空白或乱码:
- 首先检查基础显示:注释掉所有中文设置代码,先让界面用默认英文字体显示英文。如果英文都不显示,问题出在Emwin基础移植或LCD驱动上。
- 检查字库数据:在调试器中,查看
GUI_FontSong18这个字体变量在内存中的地址,确认它是否被正确链接和加载。可以尝试在初始化后直接调用GUI_SetFont(&GUI_FontSong18);和GUI_DispString("Test");,看能否显示一个英文“Test”。如果不能,说明字体注册或设置有问题。 - 检查字符串编码:在内存中查看
TEMPERATURE这样的字符串变量,它的内容应该是类似\xE6\xB8\xA9\xE5\xBA\xA6这样的UTF-8序列。如果变成了其他乱码,说明源文件编码或U2C转换过程有问题。
中文显示为方框或错误字符:
- 字体高度/宽度不匹配:在
emWin Font Converter中生成字体时,选择的尺寸(如18)必须和代码中设置的控件字体尺寸匹配。同时,要确保控件本身有足够的宽度和高度来显示整个汉字。 - 字符集不匹配:最可能的原因是你的
font_unicode.txt文件里漏掉了某个汉字。检查界面所有中文,确保它们都出现在了那个“原料”文件里,并且被成功转换到了字库C文件中。
- 字体高度/宽度不匹配:在
运行速度慢:
- 绘制中文,特别是大字体,是比较耗时的操作。如果感觉界面刷新慢,可以:
- 启用存储设备(Memory Device),
WM_SetCreateFlags(WM_CF_MEMDEV);,这能有效减少闪烁并优化复杂界面的绘制速度。 - 优化重绘区域,只刷新需要更新的部分。
- 检查单片机的主频和图形接口(如FSMC)的时钟配置是否达到最优。
- 启用存储设备(Memory Device),
- 绘制中文,特别是大字体,是比较耗时的操作。如果感觉界面刷新慢,可以:
4. 进阶技巧与优化空间
当你成功实现了基本的中文显示后,可以看看下面这些进阶玩法,能让你的界面更专业、更灵活。
4.1 动态切换字体与多字库管理
一个优秀的UI可能需要多种字体(比如标题用大号粗体,正文用小号常规体)。你可以用同样的方法生成多个字库文件(如GUI_FontSong24B,GUI_FontHei16)。在代码中,根据需要为不同的控件设置不同的字体。
更高级的用法是运行时动态切换字库。你可以将不同的字库文件放在外部存储器中,当用户切换语言或进入不同模块时,从外部加载新的字库数据到RAM中的一个缓冲区,然后使用GUI_AddFont()或相关API注册这个新字体。这需要你深入理解Emwin的字体管理机制,并实现一个字体加载器。
4.2 抗锯齿与矢量字体(对于高性能MCU)
如果你的单片机性能足够强大(比如带MMU的Cortex-M7,或者有足够的Flash),可以尝试使用抗锯齿字体(AA Font)甚至矢量字体。
在emWin Font Converter中,除了Standard,你还会看到Antialiased和Extended等选项。抗锯齿字体边缘更平滑,美观度大幅提升,但每个字符需要的存储空间也成倍增加。矢量字体则更加灵活,可以无损缩放,但渲染计算量很大,对MCU的算力要求高。对于STM32F4/F7/H7系列,如果项目对UI美观度要求高,可以尝试小范围使用抗锯齿字体。
4.3 使用XBF格式字体节省内存
前面我们生成的.c字体文件,是直接编译进程序Flash的。Emwin还支持一种叫XBF(eXtended Bitmap Font)的字体格式。这种字体可以存放在外部SPI Flash或SD卡中,Emwin在需要显示某个字符时,才去读取它的点阵数据。
使用XBF格式的优点是极大节省了内部Flash。缺点是显示速度会慢一些,因为多了外部存储器的读取操作。如果你的项目Flash非常紧张,但外部存储器充足,XBF是一个非常好的选择。生成XBF字体同样使用emWin Font Converter,输出格式选择XBF即可,然后在代码中使用GUI_XBF_CreateFont()等API来创建和加载字体。
4.4 与RTOS(如FreeRTOS)的协同工作
在复杂的嵌入式产品中,GUI往往运行在一个独立的任务里。你需要处理好:
- 任务堆栈大小:确保GUI任务的堆栈足够大,因为Emwin内部和你的字库会消耗一定的栈空间。
- 中断与资源共享:如果从触摸中断或定时器中断中调用Emwin的API(如发送消息),要使用Emwin提供的线程安全API(通常是带
_ex后缀的,如GUI_Exec()),或者通过消息队列将事件传递到GUI任务中处理。 - 内存管理:Emwin需要一块动态内存(通常通过
GUI_ALLOC_AssignMemory()分配)。在RTOS环境下,这块内存最好从专用的堆中分配,避免碎片化。
5. 方案优缺点分析与选择建议
回顾我们这套“模拟器生成+单片机移植”的方案,我们来客观分析一下它的优缺点,并给出一些选择建议。
优点:
- 开发效率高:在PC上利用强大的模拟器进行UI设计和调试,所见即所得,极大缩短了开发周期。
- 无缝移植:一旦在模拟器上调通,移植到单片机上的代码改动极小,真正实现了“一次编写,到处显示”。
- 资源可控:生成的字库只包含需要的字符,体积最小化,非常适合资源受限的单片机。
- 灵活性好:可以方便地生成不同字体、不同大小的字库,满足多样化的UI设计需求。
缺点与挑战:
- 字库更新繁琐:这是本方案最大的痛点。如果UI设计变更,需要增加新的汉字,你必须:
- 更新
font_unicode.txt和font_utf8.txt。 - 重新运行
emWin Font Converter和U2C工具生成新的C文件。 - 重新编译整个工程。 对于频繁修改的UI,这个过程会有些麻烦。
- 更新
- 工具链依赖:依赖于SEGGER的特定工具(Font Converter),虽然这些工具是免费的。
- 无法动态增删字符:字库在编译时就已经固定,运行时无法动态添加新的汉字(除非预埋了大量备用字)。
给不同场景的开发者的建议:
- 对于产品UI相对固定、单片机资源紧张的项目(如智能家电面板、工业HMI):强烈推荐本方案。它是在资源、效率和效果之间取得的最佳平衡。
- 对于需要支持多国语言、或UI文本经常变动的项目:可以考虑结合XBF字体,将不同语言的字库存放在外部Flash中,通过配置文件来动态切换。这样更新语言包时,无需重新编译固件,只需替换外部存储中的字体文件即可。
- 对于使用高性能MCU(如STM32H7)、且追求极致UI效果的项目:可以探索抗锯齿字体,甚至使用TrueType字体渲染引擎(如FreeType的轻量级移植),但这会带来更高的复杂度和资源消耗。
在我自己的多个量产项目中,只要产品定义阶段把UI文本确定下来,我都会采用这套方案。它的稳定性和可控性让我非常放心。前期多花一点时间把工具链和流程跑通,后期开发和维护会节省大量的时间和精力。最后,再分享一个小经验:一定要为你的字库生成和字符串管理写一个简单的脚本(比如Python或批处理),自动化这个流程,这样在需要更新时,一键就能完成,能把“缺点”的影响降到最低。