ARTICLE DETAIL

资讯详情

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

LVGL中文显示实战:从字体生成到排查全攻略

LVGL中文显示实战:从字体生成到排查全攻略 做嵌入式GUI这几年LVGL是绕不开的东西。UI逻辑、控件布局、动画切换这些网上教程一抓一大把。但真正把很多人卡死的是个特别基础又特别琐碎的需求在屏幕上显示中文。我印象太深了第一次在STM32上跑LVGL时界面按钮英文都出来了中文全是方框口口口一堆人围着板子查了半天最后发现根子就在字体上。这个经历之后我把LVGL的字库生成、裁剪、动态加载这套东西完整摸了一遍包括后来项目中用到的图标字体方案。这篇就把实战中用到的知识和踩过的坑全部整理出来给正被中文显示折磨的人一条能直接落地的路。1. 先弄清LVGL一个“字”从字符串到像素的完整路线我建议任何做嵌入式UI的人动手转换字体前先搞明白LVGL渲染一个字符的完整链路否则后面排查问题全靠猜。LVGL显示文字的内部流程大致是这样用户程序里的字符串 - lv_label接收 - 逐字符按UTF-8解码成Unicode码点 - 在字体对象的cmap中查找该码点对应的字形索引 - 根据字形索引取到位图数据 - 结合当前样式的颜色、字号、字间距把位图绘制到屏幕上每一步都可能出问题而大部分中文显示故障都发生在解码、cmap查找这两步。新手最容易忽略的一点LVGL自带的默认字体Montserrat系列只覆盖拉丁字母、数字和常用符号它不包含任何CJK字符。你用默认字体去显示中文屏幕上出现的空矩形其实是LVGL在字形缺失时打的替代符不代表硬件坏了也不代表LVGL没移植好就是单纯字体里没这个字。为什么会这样因为英文字体开销实在太小了。26个字母大小写加上数字标点总共100个左右的字形就能覆盖所有英文文本每个字形轮廓又简单。而中文常用字少说3000个GB2312全量6763个汉字每个字都是独立图形没有任何字母拼装的取巧空间。一个全量中文字库做成LVGL可用的位图字体轻轻松松200KB起步要是保留矢量轮廓和渲染数据直接奔着几MB去了。对于Flash容量常常只有512KB甚至256KB的MCU来说这个体积根本装不下。这里要深入理解一下LVGL的字库数据结构。LVGL不认识TTF也不在运行时解析矢量轮廓至少默认方案如此它把字体预先转换成一种紧凑的位图表示存进C数组或二进制文件里。核心数据结构是lv_font_t它里面有三大块关键信息字符映射表cmap一组Unicode码点到字形索引的映射规则。可以理解为字典LVGL拿到码点去查这个字典查到才知道字形数据在哪。字形信息glyph_dsc记录每个字形在位图缓存里的偏移、宽高、advance前进宽度、bpp等等。渲染时需要这些信息来切割和摆放位图。回调和数据指针最终获取字形位图的函数指针以及字体数据所在位置。所以字体是否包含某个汉字在技术层面等价于该字体的cmap里是否映射了这个码点。如果没映射渲染时就只能画占位符。这个认知特别关键很多排查到最后问题就是生成字体时字符范围没覆盖到某个字而不是代码逻辑错了。还有一点容易被忽略LVGL渲染文字时会维护一个glyph cache把最近用到的字形位图缓存在RAM中避免每次绘制都从Flash读取。字体体积越大字符越多cache的命中率设计就越重要。如果你的界面文字频繁切换、滚动cache被反复换出换入就会出现滚动时文字短暂消失或变框的诡异现象。这个在后面排查章节我会再展开。2. 把TTF变成LVGL认识的C数组lv_font_conv参数逐个拆解既然LVGL不认TTF就得有转换这一步。官方维护的字体转换工具叫lv_font_conv基于Node.js命令行运行LVGL官网那个在线字体转换器fontconverter底层也是它。在线版适合快速验证想法命令行版适合集成到项目构建流程里。我工作中基本只用命令行因为产品文案一变重新生成字体是高频操作不能老是打开网页手动拖文件。先装好工具。电脑上需要Node.js环境然后在终端执行npm install -g lv_font_conv lv_font_conv --version如果你不想全局安装用npx跑也一样npx lv_font_conv --font ./demo.ttf --size 16 --bpp 4 --format lvgl --output demo_font.c --range 0x20-0x7E这是一条最基础的转换命令下面把每个主要参数的含义拆开讲--font指定原始字体文件路径。不止支持一个LVGL的字体文件其实可以合并把中文字体和图标字体合成一个大文件后面我会单独讲。--size字号的像素尺寸。简单理解就是屏幕上显示的高度单位是像素。如果你的屏幕是高分屏或者DPI比较特殊需要自己换算出逻辑像素和物理像素的关系但LVGL内部就是按这个像素值来布局和渲染的。--bpp每个像素用多少个bit描述灰度也就是抗锯齿等级。可选1、2、4、8数值越大显示越圆润占用的Flash和内存也越大。--format输出格式。lvgl是生成C源码数组适用于LVGL 8.x。LVGL 9.x的格式选项可能有差异建议先看一眼工具版本支持的格式列表再定。--output输出文件名常见.c或.bin。--range指定要转换的Unicode码点范围可以叠加多个。比如--range 0x20-0x7E --range 0x4E00-0x9FA5前者是ASCII后者是基本汉字区。转换完成后生成的C文件里是一堆静态数组位图数据和一个lv_font_t结构体。在工程里使用它分三步第一步把生成的.c文件加进编译。Keil里是添加源文件CMake里是加入源文件列表如果用的是其他IDE同理。很多人会忘记这一步编译时发现找不到符号。第二步在需要使用的代码里声明这个字体对象。比如生成的字体结构体名是demo_font那就写extern lv_font_t demo_font;第三步给控件样式设置这个字体。这里是最容易出问题的地方lv_style_set_text_font(my_style, demo_font); lv_obj_add_style(label, my_style, 0);如果不显式设置text_fontLVGL会继续用默认字体结果就是你已经生成了漂亮的中文字库屏幕上还是方框。这个问题我在好几个项目里见新人踩过甚至我自己一开始也犯过。还有两个参数在正式项目里值得关注。一个是--no-compress。LVGL9.x开始支持基于LZ4的字体位图压缩开启后字体文件体积能减少很多代价是渲染时CPU要多干活。对于Flash紧张、主频也一般的MCU要权衡一下。LVGL8.x时代默认不压缩生成的就是裸位图体积直观。另一个是--no-prefilter。这个参数用来关闭字体位图预处理。某些情况下预处理会优化渲染效果但它增加转换时间对大部分MCU用途来说开不开差别不大我习惯加一个让生成逻辑更可控。在线转换器与命令行的取舍我的经验是这样在线转换器最大的优势是零配置浏览器打开就能用适合快速验证一个字体的效果。但有两个痛点一是字符范围一次性别填太多处理大量汉字或大字号时网页很容易卡死二是它没法嵌入到自动化脚本里。命令行工具虽然要敲命令但它可以重复执行、参数化、纳入构建流程。我强烈建议只要你的产品不是一次性原型字体生成一定要走命令行脚本。3. 中文字库裁剪方案只把你用得到的汉字塞进固件这是中文显示真正核心的问题——资源。全量中文字体放不进去但实际产品界面上的汉字可能就一两百个完全没必要为全字库买单。所以首要方案是字符集裁剪。我见过很多人一看到界面需要中文就直接去找全字库然后发现放不进MCU干脆放弃用LVGL做中文界面或者强行上一个外部Flash。这些操作都不是最优解。正确的做法是先统计整个项目里到底会显示哪些汉字然后只生成这些字。实际操作中我建议把UI上所有用户可见字符串集中管理。可以做几件事约定所有界面文案都写在特定的源文件或资源头文件里不要散落各处。写一个脚本扫描这些文件提取所有中文字符和全角标点去重。把去重后的字符码点转换成lv_font_conv能用的--range参数。生成字体并纳入编译。下面是我在项目中用的Python提取脚本核心逻辑实测非常实用import re from pathlib import Path project_files [ ui/main_screen.c, ui/settings_screen.c, ui/about_screen.c, ] char_set set() for fp in project_files: text Path(fp).read_text(encodingutf-8) for ch in text: if \u4e00 ch \u9fff: char_set.add(ch) elif ch in 。‘’“”【】《》、、…—·: char_set.add(ch) codes sorted(ord(ch) for ch in char_set) ranges [] start prev codes[0] for c in codes[1:] [codes[-1] 2]: if c prev 1: prev c continue ranges.append(f0x{start:X}-0x{prev:X} if start ! prev else f0x{start:X}) start prev c print( .join(f--range {r} for r in ranges)) print(总字数:, len(char_set))脚本输出直接拼到命令行后面再补一个--range 0x20-0x7E把英文字母数字标点带上一条完整命令就出来了。以后新增界面文案时重新跑一遍脚本再重新生成字体就行。如果忘了跑新文案里的字就会在屏幕上显示成方框。这个bug特别隐蔽因为其他字都正常只有个别字是框排查时很容易误判成字库损坏或者Flash读取问题。第二种方案是把中文字库做成外部文件运行时加载不编译进固件。这个方案适合文案量大、需要频繁更新、或者要支持多语言的设备。流程大概是在PC上用lv_font_conv把完整的GB2312常用字集或GBK字集输出为二进制文件--format bin把生成的二进制文件放到外部Flash或TF卡上用LittleFS、FatFS、SPIFFS之类的文件系统管理程序初始化文件系统后调用lv_font_load(A:/res/main_font.bin)加载把返回的lv_font_t*设置到样式里。这个方案在LVGL 8.x和9.x中都需要打开相应的文件系统支持宏比如LV_USE_FS_POSIX、LV_USE_FS_FATFS等而且字体数据是按文件偏移随机读取的所以对文件系统的随机读取性能有要求。它绕开了Flash容量限制但会吃掉一些内存和加载时间适合有大容量外部存储的产品。两种方案怎么选我做了个表方案适用场景优点缺点字符集裁剪内置文案固定、字少、无存储芯片零依赖、启动快、不占额外RAM改文案需重新生成字体并刷固件外部字库动态加载文案多、更新频繁、多语言不用反复烧固件、支持大字符集依赖文件系统、占用RAM/CPU、更复杂大部分中小项目我建议直接用裁剪方案等到确实出现文案更新太频繁的痛点再切外部字库也来得及。另外提一个团队协作的细节字符集裁剪方案如果只是一个人单机玩倒还好。一旦多人协作就要规定好扫描文件清单或者把所有文案统一收拢到一个strings.h或JSON文件里让脚本只认这一个文件。不然有人把文案写在自己模块的文件里而扫描清单没包含生成的字库里就会缺字屏幕上的方框又得查半天。还有一点经验生成中文字体时原始字体文件的选择至关重要。不要拿系统默认的宋体、黑体去做嵌入式因为矢量轮廓里的一些hinting信息在低字号渲染时会有问题。我推荐用开源的中文字体比如思源黑体、鸿蒙字体这些它们在不同字号下渲染都比较好且授权对商业产品友好。具体的授权规则建议自己查阅不同版本有差异。4. 图标字体怎么做从iconfont到LVGL图标的完整流程图标字体的需求在嵌入式UI里越来越常见。很多新手一想要图标第一反应用lv_img加载PNG图片结果发现LVGL默认并不解码PNG还得额外引入解码器占内存、占CPU图片资源也多。实际上对于单色图标最省资源、最优雅的方案是把图标做成字体用一个label显示直接文本着色缩放不糊集成到代码里还特别简洁。先理解图标字体的底层原理。TTF字体文件里的glyph不仅可以表示文字也可以表示任意矢量轮廓。字体规范预留了一块Unicode私用区PUA范围大致是0xE000到0xF8FF这一段在Unicode标准里没有实际意义专门给字体作者放自定义符号。图标字体就是把每个图标当作一个glyph映射到这个私用区的某个码点上。所以在LVGL的label里写\uE001渲染出来的可能是一个WiFi图标、一个电池图标或者一个齿轮图标而不是一个奇怪的字符。码点是什么完全取决于你用的字体文件。具体操作流程分五步第一步准备图标字体文件。常见来源是iconfont.cn这类图标管理平台把需要的图标选好加入项目然后就能下载对应的TTF或WOFF文件。也可以用开源字体库比如FontAwesome、Material Icons。第二步确认码点范围。图标字体里每个图标对应的Unicode码点不是固定的下载时平台通常会给出码点对应表。如果没给可以用字体编辑软件打开TTF查看cmap表更直接的办法是写一点Python代码用fontTools库解析。iconfont.cn下载的字体我印象中码点通常是0xe001、0xe002这样的连续区段但不同版本可能不一样一定要实际确认别想当然。第三步用lv_font_conv转换。命令和转文字差不多只是range要指向私用区lv_font_conv --font ./iconfont.ttf --size 32 --bpp 4 --format lvgl --output iconfont.c --range 0xE000-0xF8FF注意--size的选择。图标的显示尺寸可以在LVGL里通过样式字号控制但生成时选一个合适的基准字号也很重要。比如你界面上的图标实际显示是24px那生成时--size 24比较合适。你要是生成一个32px的字体然后希望LVGL缩放到24px显示字体位图会被缩放边缘会劣化小图标尤其明显。第四步在代码里使用。图标字体和正文字体如果是两个字体那么图标label就需要单独设置图标字体。为了方便维护我习惯建一个头文件专门定义图标宏#define ICON_WIFI \uE001 #define ICON_BATTERY \uE002 #define ICON_SETTING \uE003使用时直接拼接字符串非常清爽lv_label_set_text(status_label, ICON_WIFI 信号强度);这里有个C语法细节相邻的字符串字面量会被自动拼接成一个字符串所以ICON_WIFI 信号强度等于\uE001 信号强度。这样图标和文字混排一行代码搞定。第五步检查实际效果。下载的图标字体要先在电脑上预览一下确认单色化后的形状是否清楚。有些图标在彩色状态下很精致但转成单色后线条粗细不均小尺寸下会糊成一团。所以图标字体的选择比文字字体更挑剔尽量挑轮廓简洁的图标。图标字体有两个常见坑必须提醒第一个图标字体是单色的。iconfont.cn页面上看到的彩色图标是网络平台用CSS/Canvas额外渲染出来的效果字体文件本身只存储单一轮廓。放进LVGL后图标颜色只能通过文本颜色设置不会出现五颜六色。如果你的产品界面必须用彩色图标那就别走字体路线直接上图片加解码器。第二个图标字体也可以和正文字体合并。lv_font_conv支持多个--font参数把图标字体和正文字体放一起转换lv_font_conv --font ./UI_Font.ttf --range 0x20-0x7E --range 0x4E00-0x9FA5 --font ./iconfont.ttf --range 0xE000-0xF8FF --size 16 --bpp 4 --format lvgl --output combined_font.c这样生成一个包含正文和图标的大字体文件界面上所有label都可以用同一个字体不再需要为图标单独设置样式。缺点是字体文件变大如果图标数量多会增加无谓的Flash占用。我的建议是图标少且界面统一合成一个字体方便图标多或变化频繁分开两个字体。5. 字号、bpp和渲染质量小字发虚的根源在转换参数很多人在LVGL里显示中文遇到字发虚、边缘毛刺、小字号糊成一坨的问题第一反应是换字体文件或者改屏幕分辨率但真正的源头常常是--bpp这个参数选得太低。bppbits per pixel每像素用几个bit来描述灰度。对字体渲染来说1bpp每个像素只有两种状态完全透明或者完全不透明。没有任何抗锯齿字是锯齿状的体积最小。2bpp每个像素有4级灰度轻度抗锯齿体积是1bpp的2倍。4bpp每个像素有16级灰度接近PC上常见的抗锯齿效果体积是1bpp的4倍。8bpp256级灰度适合高质量艺术字或复杂效果但嵌入式里几乎用不到体积偏大。把不同类型做横向对比就清楚了bpp抗锯齿效果单个16px汉字位图体积参考建议使用场景1无锯齿明显约32字节大号标题、图标、对边缘不敏感2轻度初步平滑约64字节中号图标、粗体标题4明显平滑接近PC约128字节正文、小字号汉字8高质量约256字节很少在MCU用这里有笔经济账同样16px的汉字1bpp只占32字节4bpp占128字节300个汉字就差28KB左右的Flash。而界面上真正需要的汉字也就一两百个这个差距对MCU来说很可观。对于中文小字号汉字的渲染我经验是图标和大号标题24px用2bpp够用。笔画粗、尺寸大边缘再锯齿人眼也不容易察觉。正文小字号14px~20px尽量用4bpp。尤其是横细竖粗的中文字体在小尺寸下如果用1bpp横线可能直接断掉撇和捺也变成一截一截的。这是最典型的字发虚根源。如果需要做10px甚至更小的中文那不是bpp能救的。这个尺寸下矢量轮廓本身在低分辨率上的hinting就不足应该换专门为小字号设计的点阵字体或者对原始TTF做hinting优化。还有一个和分辨率有关的点容易被忽略。LVGL的--size参数是逻辑像素如果你的屏幕物理分辨率比较高但LVGL界面是按逻辑分辨率设计的那么字体渲染和实际像素之间可能出现1.5倍、2倍的缩放关系。这个情况在PC模拟器上看不太出来但实机高分屏上字会显得小或者虚。建议在生成字体之前先明确目标屏幕上文字实际占据的物理像素高度直接用这个物理像素值作为--size去生成不要在LVGL里事后缩放字体。LVGL支持样式的缩放但那只适合布局整体缩放不适合单个字体位图缩放边缘质量会下降。关于灰度还有一个底层概念LVGL渲染字体位图时做的是alpha混色。bpp产生的灰色本质是半透明度不是真正的灰色颜料。这一点从渲染原理上想就明白了字体位图记录的是每个像素的覆盖率没有颜色信息。所以你设置文本颜色时抗锯齿的边缘是半透明度的文本颜色透出底下的背景色。这就意味着如果你给文本样式同时设置了颜色和背景色字体的抗锯齿边缘看起来会像是背景色透上来了这是正常现象不是字体转换出错了。6. 显示“口口口”、乱码和缺失字形时的排查顺序中文显示出问题时最容易让人慌神因为现象五花八门有的是全屏方框有的是个别字是方框有的是乱码有的是字重影或高低不齐。这里按我平时调试的实际顺序给出一条排查链路。第一步确认源文件字符串编码是UTF-8。LVGL内部把字符串当作UTF-8来解码。如果你在Windows上用Keil源文件很可能被编辑器存成了GB2312或GBK编码。LVGL读到的字节序列就完全错位了UTF-8解码出来的码点根本不是原来的汉字自然任何字库都找不到。解决办法是把所有包含中文的源文件统一转换为UTF-8编码编译器选项里也设置成UTF-8。在Keil中需要留意文件编码格式最好设置成UTF-8 without BOM或者统一风格。第二步确认字体文件cmap覆盖了你要显示的码点。这一步的操作是打开生成的字体C文件查找cmap映射表看里面有没有你要显示的字符码点。比如开字的Unicode是0x5F00就查0x5F00在不在映射表里。如果不在说明生成字体时--range没有覆盖到重新生成就行。这一步一定要亲自动手做别凭感觉猜因为很多字体工具的range参数写错或者字体本身缺少某些字形不查永远发现不了。第三步确认自定义字体真的被挂载到了正在渲染的控件上。这个是最常见的低级失误。LVGL样式系统允许不同来源的style叠加如果某个控件既设置了默认style又设置了状态style而后面的style没有text_font属性那字体就可能回退到默认字体。排查时可以打印当前控件最终生效的text_font指针确认它是不是你生成的那个字体对象。在LVGL里可以使用类似lv_obj_get_style_text_font(obj, LV_PART_MAIN)的接口来检查。第四步区分全部字符缺失和个别字符缺失。如果屏幕上的方框图里只有个别字是框其他字正常那几乎可以确定是range没覆盖到或者字符串里藏着不可见字符。如果整行整行的字都是框先怀疑编码和字体挂载。这两种现象的排查方向完全不同别一上来就重新生成整个字库浪费时间还容易引入新问题。第五步检查fallback字体机制。LVGL从8.x开始支持字体回退font fallback就是主字体里找不到的字可以自动去回退字体里找。这个机制非常适合作中文字体的补充主字体用小体积的自定义字库里面没有的冷僻字去全量回退字库找。但要注意主字体和回退字体的行高、基线如果不一致混排时会出现文字高低不齐、行高异常。一旦启用回退必须逐屏目测。关于乱码还有个小细节。如果你从网上下载的示例代码里直接复制字符串有些网页会把全角空格、零宽空格之类的不可见字符也复制进去。这些字符的码点在字体里几乎必然不存在表现出来就是某个字符是空框但你怎么查range都发现字是有的。遇到这个情况用十六进制编辑器查看源文件对应字节基本就能揪出邪门字符。最后替很多新手澄清一个误区LVGL自带的那几个lv_font_montserrat_xx字体全部不含中文字符。你用它们显示中文得到方框这不叫bug这是设计使然。所以网上很多示例工程里有一段代码能够显示中文不是因为用了LVGL默认字体而是那个工程自带了一个自定义中文字库。你复制代码时如果没把字体文件一起复制走那当然显示不正常。7. 最后再补充几个工作中沉淀下来的细节以上是核心的主体流程。还有一些细节是我在多个项目里踩过坑之后才沉淀下来的零散但都很实用一并写出来。第一字体转换这个动作一定要脚本化、自动化。我一开始也是手动敲命令后来产品文案改了几版每次重敲命令加上核对range烦不胜烦。现在的做法是把扫描字符串 - 生成range - 调用lv_font_conv - 检查输出文件写成一个脚本挂到项目构建流程里。字体文件本身不进版本库只存脚本和原始TTF这样团队里谁改文案重新build都会自动重新生成字体不会出现字体文件和代码不同步这种莫名其妙的问题。第二中英文混排时基线对齐是个视觉细节。中文字体和纯英文字体的基线定义可能不一致混排时英文字母和中文字会出现高低不齐。最好的办法不是写代码偏移而是选择一款同时覆盖拉丁和CJK的字体比如思源黑体、鸿蒙字体然后在一个转换阶段把ASCII范围和汉字范围都写进同一个字体让两种文字的基线统一。这个效果比在代码里手动调偏移自然得多。第三LV_FONT_CACHE_SIZE这个配置字库大了之后一定要关注。它对应LVGL的字体位图缓存条目大小。如果在界面上快速滚动或频繁切换页面时文字出现先变空再恢复的现象多半就是这个cache太小导致glyph频繁换入换出。解决方法是适当调大缓存大小或者减少同时使用的字体数量。后者的效果往往更立竿见影。第四字体文件放进Flash后如果在切换界面的时候偶尔出现字形残缺优先检查编译优化等级。某些编译器在高优化等级下会把静态数组里的数据错误优化导致字体位图访问越界或读到错误数据。遇到这种问题把字体C文件单独设置低优化等级或者将字体数组用const volatile修饰可以先排除编译器因素再做其他排查。第五维护好字体和码点的对应关系文档。图标字体项目里尤其重要因为一旦更换了图标字体文件码点可能整体变化而代码里那些\uE001宏不会变结果就是界面上所有图标全部乱套。我建议把图标字体文件本身、转换命令、码点对照表三样东西一起提交到项目文档谁改谁更新对照表这是避免灾难现场最稳的办法。如果你正在被LVGL的中文显示问题卡住我的建议很简单先别急着搞全量字库也别一上来就上外部Flash。花半小时把lv_font_conv跑通只生成十几个字的验证字体把字符串-字体-样式这条链路走通再扩展到全项目的字符集。这条路径看着绕其实是解决问题最快的一条路。字体机制想明白之后无论中文、英文还是图标字体都只是参数不同的一件小事。
返回列表