简介LVGL多国语言字库制作工具是一份面向嵌入式开发者的实用资源专门针对LVGL图形库定制字库与多语言支持。该工具可处理中、日、韩、阿拉伯文等字符集将其转换为图形库可识别的字库格式并支持调整字体大小、样式与颜色生成适合项目直接集成的二进制文件。资源包共146个文件约32.44兆字节。其中除可执行程序与配套动态库外还有37份C语言源码和36份头文件便于查看字库生成与加载逻辑21个图片文件用于界面预览16个语言资源文件提供界面翻译另含工程配置文件可直接在集成开发环境中打开。整体结构清晰适合具备基础嵌入式经验、正在为界面寻求多语言方案的开发者。目前已有427人学习下载。借助完整工具和源码读者可结合其中STM32平台示例理解从字库生成到屏幕显示的完整链路从而减少多语言适配工作量显著缩短产品迭代周期。1. 多国语言字库制作工具LVGL 界面全是方块问题不在屏幕而在字库链我接手第一个海外项目时设备在实验室一切正常送到客户那边开机后中文、俄文、泰文全部变成空方块。排查了好几天主控、屏幕、驱动最后发现根本不是屏幕的问题LVGL 并不像 PC 那样直接读取 TTF 字体它只认编译成 C 数组的位图字库而当时工程里只烧了一个 ASCII 字库。所谓 lvgl 多国语言字库制作工具就是把 TTF/OTF 字体按指定字符范围、字号、灰度打平成 LVGL 能加载的.c字库的工具链。它解决的问题很直接让同一套 UI 在不同语言环境下能显示完整字符同时不把整个字体文件塞进 Flash。这篇不写玄学只讲能直接落地的流程和参数新手能照做熟手能避开那些容易翻车的细节。2. lv_font_t 到底装了什么从 C 数组到多语种渲染的原理与选型2.1 LVGL 的字库本质上是一张位图缓存表LVGL 渲染文字时并不会在运行期动态解析 TTF 的曲线数据。它通过一个lv_font_t结构体指针工作这个结构体里保存着字符映射表、字形描述符和位图数据。每次要绘制一个码点就按码点查表定位到字形描述符再把这个字形对应的位图按 1/2/4/8 bpp 的位数放到屏幕上。也就是说字库制作工具的本质是“离线把字体文件里的轮廓转成像素并打包成可索引的 C 数组”而不是做字体文件的格式转换。这个设计直接决定了多语种字库的制作方式。因为 C 数组必须提前包含具体码点所以字库文件和字符范围强相关。范围越大固件越大。常见做法是把界面里用到的字符做成子集而不是把 TTF 全量导入。LVGL 8 之后还支持字体回退即同一个文本里主字库没有的码点会自动去备用字库找这对多语种混排很有用但前提是每个语言的字库都得先做成单独的 C 文件。另一个容易忽略的细节是LVGL 自带的默认字体文件里只包含 ASCII 和很少的扩展字符做英文原型够用一旦语言包切到俄文、泰文必须使用自己生成的外部字库。那些默认字体并不是“不好”而是覆盖范围固定不适合作为多语种方案的底子。2.2 多语种比 ASCII 多出的三件事Unicode 范围、字体覆盖与文字整形做 ASCII 字库只需要一段连续范围0x20-0x7F从空格到波浪号简单直接。但多语种要面对三件 ASCII 没有的麻烦。第一Unicode 范围是多个离散块。西欧拉丁扩展在0x00C0-0x024F西里尔在0x0400-0x04FF泰文在0x0E00-0x0E7F阿拉伯在0x0600-0x06FFCJK 统一汉字在0x4E00-0x9FFF。这些范围不能只靠记忆最好从 Unicode 标准表里查或者直接用脚本从文本内容反推码点。第二字体覆盖不足。即使你把码点范围写进了命令源字体本身没有这个字形工具也只能跳过而且很多命令行工具不会报错只会安静地什么都不输出。比如用一款只带拉丁字符的字体生成俄文字库出来的必然是大量空白。选择字体时要用字体查看工具逐个语言页确认别等烧到设备上才发现缺字形。第三文字整形。这是最容易被忽略、也最容易翻车的一点。阿拉伯文、泰文这类复杂文字同一个字母在词首、词中、词尾的形状不一样甚至出现强制连写泰文还有叠加在字母上下的符号例如上元音、下元音。LVGL 的字库制作工具只做静态码点到位图的映射不做上下文整形。也就是说你往lv_label里塞一个阿拉伯字符串它并不会自动被拆成正确的连字字形。要么保证字符串里存的已经是连字位置的字形码要么用外部 shaping 引擎预处理生成字形序列再交给 LVGL。阿拉伯字库“能显示”不等于“能正常读”这一点我在避坑章节还会展开。拿泰文举例一个泰文字符串经常由基字、上元音、下元音组成LVGL 静态映射时会把每个组合字符画在基字中心附近16px 小字号下就容易叠成一团黑。有些团队选择使用预组合字形把常见音节整体映射成自定义码位业务层再做映射这样能绕开 shaping 引擎但需要维护一张不小的映射表。是否要上 shaping应该在最开始选型时决定而不是等 UI 联调时再说。2.3 命令行转换器与图形化配置工具怎么分工主流做法是使用命令行工具lv_font_conv。它由社区维护支持从 TTF/OTF 生成 LVGL 格式的 C 文件参数清晰适合批量、脚本化和版本管理。图形化的字体配置工具通常集成在 UI 设计软件里适合鼠标选中字符表、拖一拖范围就预览效果但它生成的项目文件比较重不适合反复重新构建也不会自动处理源文本扫描。我自己的分工是原型阶段用图形化工具看效果正式固件全部走命令行工具把生成命令写进构建脚本。这样换字体、改字号、改语言配置时只需要改一个配置文件不需要重新在图形界面里点一遍。做多国语言字库前建议先列一张清单避免做着做着忘了哪个环节。下面的表可以直接复制到项目文档里项目取值示例说明目标语言简中、繁中、俄、泰、阿拉伯决定 Unicode 范围和是否需要 shaping源字体文件思源黑体、Noto 系列等必须包含目标语言全部字形字号16px / 20px与 UI 实际显示字号一致放大会有锯齿bpp1 / 2 / 4 / 8抗锯齿位数越大越清晰也越占内存压缩开 / 关位图做 RLE 压缩明显减小 Flash 占用字符范围由源文本提取避免全量 CJK 导致固件爆炸这张表就是后面所有命令的输入。应该在提需求时就把这些定下来而不是生成字库之后再反复试。很多项目延期不是因为工具难用而是因为“先做一个看看”导致字库反复重新生成。2.4 第一版字库怎么做先小后大先静态后动态真正动手时别一上来就想做一个覆盖所有语言的大字库。我一般先做三件事确认目标语言里最简单的一段文本能显示确认标点符号和数字没有丢确认字体覆盖和 shaping 需要分开对待。把最小字符集做出来烧到设备上看效果再逐步扩展到完整语言包。动态内容是另一个变量。设备上如果有用户输入、蓝牙设备名、文件列表等动态文本字库覆盖策略必须预留常见字符。静态文案可以从语言包提取动态内容只能靠覆盖率统计。你不能把整个 Unicode 都放进去所以需要在产品定义阶段决定“哪些字符必须显示哪些字符缺失时提示不支持”。这个边界越早划清楚后期越少返工。3. 用 lv_font_conv 跑出第一个多语种字库最小命令与三个必调参数3.1 最小生成命令指定字体、范围、输出先把环境准备说明白lv_font_conv是 Node.js 写的命令行工具装好后在终端直接调用。下面这条命令可以生成一个覆盖拉丁扩展、西里尔和常用汉字的字库。lv_font_conv \ --font NotoSansSC-Regular.otf \ --size 16 \ --bpp 4 \ --range 0x20-0x7F,0x00C0-0x00FF,0x0400-0x04FF,0x4E00-0x9FA5 \ --format lvgl \ --no-compress \ --output app_font.c这里--font指定源字体文件--size 16决定字形高度单位是像素--bpp 4表示每个像素用 4 bit即 16 级抗锯齿--range用逗号分隔多个 Unicode 区间这条命令里依次是 ASCII、拉丁扩展、西里尔和常用汉字--format lvgl表示输出给 LVGL 使用的 C 数组--output是输出文件名。运行结束后会得到一个 C 文件里面包含lv_font_t app_font的结构体定义和字形相关数组。注意这里加了--no-compress是为了方便你拿原始位图对照调试正式发布时建议去掉这个开关让工具默认做压缩体积会小不少。工具遇到源字体里不存在的字形会跳过命令本身不会因为缺字形而报错所以后面避坑部分会专门说怎么发现缺字。3.2 三个必调参数size、bpp、range--size最好与 UI 里的实际字号一致。LVGL 的坐标、行高都以像素为基准你做一个 16px 的字库把 label 的字号设成 16行内垂直正好字号设成 17顶部会被截掉一点底部留白变大。不要靠“字库做大一号再缩小”来解决问题LVGL 不做缩放绘制位图多大就画多大。--bpp的取舍比较依赖场景。bpp1 只有黑白两色小字号下笔画发虚但一个 16px 的字形位图只需要几十字节bpp2 有 4 级灰度能看清抗锯齿轮廓bpp4 在普通屏上已经够用bpp8 适合大字号或对观感要求高的场景内存翻倍但提升有限。做小屏设备我一般用 bpp2 或 4做大字号标题才上 4。--range是字库体积的大头。一个覆盖全部常用汉字的字库仅 CJK 一段就包含两万多个码点16px、bpp4 全量生成后可能超过 1 MB这在 MCU 场景直接不可用。所以正确做法是缩小范围到实际用到的字符。下一小节就是干这个的。3.3 从项目源文本抽取码点生成 range 参数很多新手直接抄一个很大的--range结果固件体积失控。稳妥套路是把所有要显示的语言包字符串拼成一个文本文件然后用脚本筛出出现过的码点。下面是一段可复现的 Python 脚本import unicodedata def load_texts(path): with open(path, encodingutf-8) as f: return f.read() def build_ranges(codepoints): if not codepoints: return codepoints sorted(set(codepoints)) ranges [] start prev codepoints[0] for cp in codepoints[1:]: if cp prev 1: prev cp continue ranges.append((start, prev)) start prev cp ranges.append((start, prev)) return ,.join(f0x{s:04X}-0x{e:04X} if s ! e else f0x{s:04X} for s, e in ranges) text load_texts(all_strings.txt) print(build_ranges(ord(c) for c in text if not unicodedata.category(c).startswith(Z)))这段脚本读取all_strings.txt把所有码点排序后合并成连续区间输出成--range需要的格式。过滤Z开头的 Unicode 分类会把空白去掉避免把换行、空格算进字库空格可以由 ASCII 段覆盖不需要重复烧录。把输出粘贴回命令里的--range再执行一次生成字库体积通常能缩小到全量 CJK 的十分之一以下。注意从文本抽码点只能覆盖静态文案如果设备上有用户输入、实时读取的设备名那还是要预留一块常用字库范围否则用户一输入冷僻字就变成方块。3.4 把生成的 C 字库接进 LVGL 工程生成.c文件后要让它参与编译。直接把app_font.c加进工程的源文件列表然后在需要使用字库的地方声明extern lv_font_t app_font;再把它挂到控件的样式上。extern lv_font_t app_font; lv_obj_t *label lv_label_create(screen); lv_obj_set_style_text_font(label, app_font, 0); lv_label_set_text(label, 你好世界);这里lv_obj_set_style_text_font(label, app_font, 0)的最后一个0是样式选择器表示默认状态。如果是在lv_style_set_text_font里传参就把app_font传给该函数。编译时还会用到--lv-include参数让生成的 C 文件自带头文件引用lv_font_conv --font NotoSansSC-Regular.otf --size 16 --bpp 4 \ --range 0x20-0x7F,0x4E00-0x9FA5 --format lvgl \ --lv-include lvgl/lv_font.h --output app_font.c加上--lv-include后生成的 C 文件顶部会带一行#include避免你手工写的头文件路径和实际工程不一致。如果你发现编译提示字体未定义第一反应别改代码先检查这个.c文件有没有真被编译链接很多构建系统里新增文件不会自动加入。把构建日志翻出来确认app_font.c出现在编译过程里再谈 extern 和头文件的问题。4. 从文本抽字符集并批量生成多语种字库脚本化工作流4.1 把字库配置写成一个 JSON 清单多语言项目通常不止一个字库。我一般会在工程根目录放一个fonts.json按语言和维护状态记录字号、字体文件、字符范围、输出文件名。例如[ { name: app_font_latin, font: Roboto-Regular.ttf, size: 16, bpp: 4, ranges: 0x20-0x7F,0x00C0-0x00FF, output: gen/app_font_latin.c }, { name: app_font_cyrillic, font: NotoSans-Regular.ttf, size: 16, bpp: 4, ranges: 0x20-0x7F,0x0400-0x04FF, output: gen/app_font_cyrillic.c } ]这份清单的价值在于可追溯字体来源、字号、字符覆盖、输出位置都记录在案后续维护不用翻聊天记录和旧命令。生成脚本逐条读取并调用转换工具如果某一个源字体缺字形工具会输出一条警告但执行并不停止我会把警告统一收集进日志等全部生成完再去处理避免一次失败打断整个批量流程。4.2 写批量生成脚本循环调用转换命令下面是一个常见的 Bash 脚本骨架#!/usr/bin/env bash set -euo pipefail cd $(dirname $0) mkdir -p gen jq -c .[] fonts.json | while read -r item; do name$(echo $item | jq -r .name) font$(echo $item | jq -r .font) size$(echo $item | jq -r .size) bpp$(echo $item | jq -r .bpp) ranges$(echo $item | jq -r .ranges) output$(echo $item | jq -r .output) lv_font_conv \ --font $font \ --size $size \ --bpp $bpp \ --range $ranges \ --format lvgl \ --output $output done echo all fonts generated.set -euo pipefail保证中间任何一步失败就停止避免把半成品字库烧进固件。jq -c把 JSON 数组逐条交给循环每次执行一次转换命令。这里没有加--no-compress正式产出默认带压缩。脚本还能再加一层判断当输出文件的时间戳比源字体和配置都新时跳过转换节省 CI 时间。如果你不用 jq也可以用 Python 写同样逻辑。脚本的目的不是炫技而是让“换一个字号、换一个范围”变成只改 JSON不碰命令。这样团队成员都能维护不会因为某个人离开导致字库流程断掉。4.3 用脚本比对源文本和字库避免漏字符生成完成后应该再跑一个校验脚本把源语言文件里的每一个字符和字库的码点集合做比对。这里不用重新生成直接解析刚刚生成的 C 文件就行。下面脚本以unicode 0xXXXX字段为准import re font_c open(gen/app_font_latin.c, encodingutf-8).read() covered set(int(u, 16) for u in re.findall(runicode 0x([0-9A-Fa-f]), font_c)) source open(all_strings.txt, encodingutf-8).read() missing sorted({ord(c) for c in source if ord(c) not in covered}) if missing: print(MISSING:, , .join(fU{m:04X} for m in missing)) else: print(OK)这段代码用正则从 C 文件里取出所有已生成的码点再和源文本做差集。不同版本的工具输出的结构字段名可能有一点差异但unicode这个字段是字形描述表的基础字段基本都会保留。如果你的版本输出的是U注释格式把正则改成匹配UXXXX也可以。到这里生产和校验就是一条完整的自动化链文本变更后重新抽码点更新 JSON批量生成字库自动比对最后交给构建系统。整个过程没有人工双击图形界面也就没有“忘了更新字库”这种事故。4.4 动态输入内容怎么覆盖静态文案可以扫描语言包动态内容不行。比如设备支持用户搜索地铁站名那必须提前准备一张“可显示字符表”。这张表的来源可以是业务数据里的高频字符统计也可以是产品定义的输入法字符集。设备端检测到未覆盖字符时与其让它变成方块不如在输入层就拦截。常见做法是把动态字符做成一个独立的小字库比如 16px、bpp2只覆盖常用几千字和主 UI 字体分开。静态界面用大一点的 bpp4 保证观感用户输入区用这个小字库这样既不拖慢主界面也不会因为用户输入一个生僻字导致功能崩溃。多语言输入法和输入法字库要单独评估不要和界面字库混在一起做。5. 多语种字库避坑指南方块、连字错乱、编译失败怎么排查5.1 只有个别字符显示成方块现象界面上 90% 的文字正常偶尔几个字是空心方块通常出现在不常更新的弹窗或提示语里。原因--range没有覆盖到那个码点或者源字体缺少该字形而工具静默跳过。解决把那个字符的码点找出来加进--range。调试时写一个函数打印字符串的 UTF-8 字节转成 Unicode 码点再对照已生成字库的 cmap。最直接的方法是重新生成一个只包含该码点的临时字库烧进去看是否显示如果仍显示方块就把源字体拿到字体查看器里搜这个码点确认字体文件本身有没有字形。很多时候不是工具坏了是字体没有这个字。5.2 英文和数字正常中文全变方块现象纯英文界面无问题切到中文后所有汉字都消失。原因你用的源字体是拉丁字体没有 CJK 字形中文码点全被跳过。解决换一个支持 CJK 的字体文件重新生成。注意不要只看字体名字带“CN”或“SC”就认为支持最好在字体工具里直接搜“中”和“文”两个字确认。生成命令里明明写了0x4E00-0x9FA5但源字体缺字形时工具不会停下来这也是很多人踩坑之后才发现的原因。选字体时我会直接看字体文件里汉字部首的覆盖统计而不是看宣传文字。5.3 阿拉伯文/泰文顺序错乱、连字丢失现象阿拉伯文字母像被打散字符位置完全不对甚至从右往左的排版变成左往右。原因LVGL 默认不做双向排版和复杂文本整形字库工具只能静态映射码点无法判断字母在词中的位置。同一个阿拉伯字母词首、词中、词尾和独立形式的字形码是不同的。解决思路分两层。第一层在字库工具里把阿拉伯字母的四种形式所对应的字形码都包含进来第二层在业务层用外部 shaping 引擎把字符串转换成带位置信息的字形序列再交给 LVGL。泰文也类似需要处理上方元音、下方辅音等叠置符号。这不是换一个字库就能解决的必须在软件设计阶段就给语言模块加 shaping 步骤否则测试时看着每个字母能显示用户一读就发现是乱的。5.4 编译报错 undefined reference toapp_font现象代码里写了extern lv_font_t app_font;链接时仍然报未定义。原因多数情况是.c文件没有加进构建系统或者输出文件名与 extern 不一致。解决先打开构建脚本确认gen/app_font.c在源文件列表里再搜索生成文件里的字体变量名不同参数下变量名可能与文件名不一致以结构体名字为准。还有同学把lv_font_t app_font和lv_font_t *app_font混在一起extern 声明成指针导致链接器找的是另一个符号。记住生成的变量是结构体不是指针使用时要传app_font。如果构建系统做了文件分类把.c文件放进了“工具链源码”目录但没被识别也会出现这个问题。5.5 字库文件太大Flash 塞不下现象固件编译完超出片内 Flash 容量裁掉日志功能才勉强放下。原因字符范围太大、bpp 太高或者没有开压缩。解决开压缩改小 bpp严格限制字符范围。做 CJK 时不要整个0x4E00-0x9FA5全量包含先按文案提取。还可以把字库放进外部 Flash运行时加载但加载路径、内存池和缓存管理都要额外设计适合产品功能稳定之后作为优化项。一个项目里多个字号千万不要各做一份全量字库应该让 UI 统一字号或者把常用小字号做成共享字库大字号只覆盖标题文本。6. 进阶验证把字库覆盖检查做进 CI再验证阿拉伯语连字当字库由脚本批量生成后最怕的不是命令写错而是有人改了一行文案新增了一个生僻字CI 却不知道。我会把第 4 章的校验脚本做成一个独立命令在每次构建前先执行扫描所有源语言文件比对已提交的字库 C 文件一旦发现缺失码点直接让构建失败而不是等测试人员肉眼去找。这一步成本很低收益很大值得立刻做。另一个进阶技巧是针对阿拉伯语等复杂文字。在接入 shaping 引擎后不能只看最终字符串还要在模拟器里传入三种上下文独立形式、词首、词中、词尾。同一个字母至少对应 4 个不同字形码字库范围必须把这些字形码全部覆盖。验证方法是在 LVGL 模拟器里放三行内容一行是独立单词一行是双连写一行是带前后缀的完整词截屏后逐项对照标准形状。测试用例直接编进语言模块的自动化回归里比每次手工输入可靠得多。如果产品有动态字符串建议把用户可能输入的常用字也做成一个输入字库。比如设备名、蓝牙名、地名搜索这部分可以提前收集典型数据做词频统计再做低频覆盖冷僻字缺失时提示“暂不支持”而不是显示方块。到这里多语言字库的整个链路就算闭环了。我自己的教训是早期做某项目 X 时阿拉伯文测试只看“能显示”没验证连字结果海外样机拿过去没法读。后来在流程里加了 shaping 用例这个坑再也没踩过。希望帮到你。本文还有配套的精品资源点击获取