资讯详情

资讯详情

LVGL多国语言字库制作:TTF转C数组实现中文与阿拉伯文显示

简介面向lvgl图形库开发者的多国语言字库制作工具专为需要为嵌入式界面添加中文、日文、韩文、阿拉伯文等多语言支持的场景而设计。工具提供字库编辑、字符集转换、字体优化与跨平台操作能力可将编辑好的字体直接生成lvgl可识别的格式并辅助集成到现有项目中适合有一定嵌入式基础的开发者快速完成国际化界面搭建。资源包共146个文件压缩后约32.44MB包含37个c源码、36个头文件、23个dll动态库、21个png图标、16个qm翻译文件以及工程配置文件、批处理脚本和可执行程序覆盖STM32工程模板与字体生成配套模块可直接参考或复用。当前已有427人下载学习。通过该工具与附带的示例工程开发者能掌握lvgl字库制作全流程理解字符集映射与字体文件生成原理并将多语言支持能力快速移植到自己的项目中减少重复开发成本。1. LVGL 多国语言字库制作工具让中文与阿拉伯文在同一块屏上正常显示的落地资源在 STM32F103 上调试 LVGL 界面时我第一次被“方框字”折腾到半夜——字符串里明明是中文屏幕上却是一排豆腐块。原因并不玄学LVGL 自带的字体只有 ASCII要显示中文、日文、阿拉伯文这些多语言字符必须先把 TTF 字库按 LVGL 认识的格式转换成一整份 C 数组。这套“lvgl多国语言字库制作工具”工程资源核心就是 mcu_font_30.c 与 mcu_font_40.c 两个已转好的字库文件配齐 lcd.c、stm32f10x_flash.c 等 STM32F10x 外设驱动适合正在做小屏多语言菜单、又不想从头摸字库格式的开发者。把它拆开看完你就能把同一套流程搬进自己的工程。2. LVGL 字库加载机制先看懂 glyph、cmap 和 bitmap再上手转换2.1 LVGL 显示一行文字时内部做了什么LVGL 的 label 组件显示字符串并不是直接拿字体文件渲染。它走的是一条“查表—取位图—绘制”的固定链路。字符串在内存里按 UTF-8 编码存放LVGL 每取到一个字符的 Unicode 码点就会调用 lv_font_t 结构体里的 get_glyph_dsc 回调去查这个字符在当前字库中是否存在返回对应的字形描述随后再调用 get_glyph_bitmap 从字库的位图区取出像素数据最后按照字形描述里的宽高、偏移量画到屏幕上。整个过程中有两张核心表cmap 负责把字符码点映射到字形编号glyph_dsc 负责描述每个字形的绘制参数。我一般在调试多语言显示问题时会先确认 cmap 是否覆盖了目标字符。常见做法是写一段极简的查询代码来验证/* 示意代码检查一个 Unicode 字符是否在字库 cmap 中 */ lv_font_t *font mcu_font_30; /* 已转换好的字库 */ uint32_t codepoint 0x4E2D; /* 字符“中”的 Unicode 码点 */ lv_font_glyph_dsc_t dsc; lv_result_t found lv_font_get_glyph_dsc(font, dsc, codepoint, 0); if (found LV_RES_OK) { /* 命中dsc.adv_w 是横向步进dsc.box_w 与 dsc.box_h 是字形框尺寸 */ } else { /* 未命中屏幕上会画方框或空白替代符 */ }这里的关键点是 lv_font_get_glyph_dsc 的第三个参数必须传 UTF-8 解码后的真正码点而不是字符串的原始字节。如果字符不在字库里回调直接返回失败label 只能画一个方框。换句话说转换字库时“字符范围”这个参数直接决定了哪些字符能从字符串变成屏幕上的像素。2.2 多语言字库的真正难点字符集覆盖和字形变体不少人以为多语言支持就是把字库文件换大一点。实际不是。ASCII 只有 95 个可打印字符按 1bpp 存储不到 2KB简体中文常用字有 6763 个GB2312就算 16px 高度、4bpp 抗锯齿体积也会到 100KB 上下日文要同时覆盖平假名、片假名和大量汉字阿拉伯文更特殊同一个字母在词首、词中、词尾会呈现三种不同形状这不是多放几个字符就能解决的而是需要能输出字形变体的转换流程。因此一个靠谱的多语言字库制作工具至少要让你控制下面几项参数。我在实际转换时最常调的也就是这张表参数作用我常用的取值字符范围决定字库包含哪些码点直接关系体积ASCII 所需语言区段字体尺寸字形的像素高度对应 line_height30、40 两套bpp每像素位数常用 1/2/4/84抗锯齿base_line基线位置影响西文与中文的中线对齐随字体尺寸自动计算亚像素渲染子像素级平滑依赖屏幕物理排列一般关闭阿拉伯文整形把独立字符变成连写变体按需开启把这些参数在转换前定好生成的 C 文件才有实用价值。很多项目翻车都是因为只调了字体大小字符范围还是默认的 ASCII结果多语言字符全部显示成方框。2.3 为什么不能直接把 TTF/OTF 扔给 LVGL嵌入式设备上MCU 的 Flash 和 RAM 都很紧张。TTF 需要字库解释器解析曲线并光栅化LVGL 不内置 FreeType也不会去读文件系统它只认按固定格式排布的 C 数组。这就是字库制作工具的定位把 TTF 里每个字形按固定 size 栅格化压缩成像素位图连同 cmap 映射表一起打包成 .c 文件编译时直接链接进程序。这样做的好处是运行时不需要字体引擎CPU 只做内存拷贝和绘图代价是每个字号都要单独生成一份字库。到这里你应该明白了转换工具产出的 mcu_font_30.c 本质上是“拍扁了”的像素字库改字号、改字符集都要重新走一遍转换。接下来我从这份资源里的实际文件出发拆解字库文件里的数据布局再给出重新生成的完整命令。3. 解剖 mcu_font_30.c字库 C 文件里的数据排布与体积账3.1 从工程文件反推这套资源的构成拿到这份资源时文件名其实已经把工程结构交代得比较清楚了。mcu_font_40.c 和 mcu_font_30.c 明显是两套不同字号的 LVGL 字库文件lcd.c 是屏幕驱动stm32f10x_tim.c、stm32f10x_flash.c、stm32f10x_rcc.c 这些是 STM32F10x 标准外设库里的定时器、Flash、时钟、ADC、I2C、CAN 驱动keilkilll.bat 是常用的 MDK 工程清理脚本。组合起来的判断是这是一套在 STM32F10x 上跑 LVGL 的完整工程多语言字库已经被提前转好并写进了工程。文件作用mcu_font_40.c / mcu_font_30.cLVGL 字库对应两种字号lcd.c屏幕驱动与画布初始化stm32f10x_flash.c内部 Flash 读写烧录和运行时加载字库stm32f10x_rcc.c / tim.c / i2c.c时钟、定时器、I2C 外设驱动keilkilll.bat清理 MDK 编译中间文件两套字库的存在很合理。界面上的标题栏、状态栏、正文用不同字号运行时不需要实时缩放位图省下不少 CPU 开销。我一般会把界面涉及的两三个字号全部生成按需引用同时把转换命令行配置存一份方便后面加字。3.2 lv_font_t 结构体与三块核心数据一个标准的 LVGL 字库 C 文件内部大体由三部分组成bitmap 位图数组、glyph_dsc 字形描述数组、cmaps 映射表数组最后统一挂进一个 lv_font_t 结构体。我用常见的生成格式做一个简化示意字段名称与 LVGL 转换器的输出保持一致/* 简化示意LVGL 字库文件内部的三段式结构 */ static const uint8_t mcu_font_30_bitmap[] { /* 所有字形像素连续排列按 glyph 顺序存放 */ 0x00, 0x7E, 0x81, 0x81, ... }; static const lv_font_fmt_txt_glyph_dsc_t mcu_font_30_glyph_dsc[] { /* 每个字形一段adv_w 步进、box_w/box_h 字形框、ofs 偏移、bpp、位图索引 */ { .adv_w 8, .box_w 8, .box_h 15, .ofs_x 0, .ofs_y 0, .bpp 4 }, ... }; static const uint8_t mcu_font_30_cmaps[] { /* cmap 表把 Unicode 码点映射到 glyph_dsc 数组下标 */ 0x04, 0x00, 0x1F, 0x01, ... }; static lv_font_t mcu_font_30 { .get_glyph_dsc lv_font_fmt_txt_get_glyph_dsc, .get_glyph_bitmap lv_font_fmt_txt_get_glyph_bitmap, .line_height 30, /* 决定 label 的行高 */ .base_line 7, /* 基线偏移混排时关键 */ .subpx LV_FONT_SUBPX_NONE, .dsc (lv_font_fmt_txt_dsc_t){ .cmaps (void *)mcu_font_30_cmaps, .glyph_dsc mcu_font_30_glyph_dsc, .bitmap mcu_font_30_bitmap, } };这里需要重点理解两个地方。第一bitmap 是按字形顺序连续排列的原始像素glyph_dsc 里不存实际像素只存“这段像素在数组里的第几个字节”第二cmap 表负责把字符码点翻译成 glyph_dsc 的下标。LVGL 渲染时先走 cmap 再走 glyph_dsc整个过程没有文件系统参与所以它轻。line_height 和 base_line 这两个字段直接决定多语言混排时行与行的对齐效果后面避坑章节会再展开。3.3 字库体积的估算账本中文字库的体积焦虑基本是每个做 MCU UI 的人都要面对的。算个账30px 高的字字形框按 30×30 估算4bpp 表示一个像素占半个字节单个字形最坏情况约 450 字节。如果转完整 GB2312 常用汉字 6763 个体积约 3MBSTM32F103 内部 Flash 一般不超过 512KB直接放不下。实际操作里大家普遍采用“按需子集化”只转界面中真实用到的字符而不是一上来就转全字库。这里有一个容易踩的盲区很多人把字符范围直接写成 0x4E00-0x9FA5以为这就是 GB2312 的 6763 个汉字。实际上那是 Unicode CJK 统一汉字基本区包含 20902 个码位中文字体大多能覆盖这些字形转换器会老老实实把两万多个字形全做出来。所以字符范围清单一定要按实际用字维护而不是甩一个超大区间。下一章的转换命令里我会把字符清单文件的写法一起给出来。4. 从 TTF 到 mcu_font_40.c转换命令、参数与集成实战4.1 转换前的准备工作先准备一份干净的 TTF/OTF 字体文件。注意版权找开源可商用的字体会省掉很多后续麻烦另外中文字体和西文字体经常是分开的界面里英文用西文字库、中文用中文字库也可以在转换时把多个字体文件合并进同一份 LVGL 字库。字符清单文件也要在转换前维护好下面是一份常见写法# char_range.txt注释行以 # 开头每行一个区间或单个码点 0x0020-0x007E # ASCII 0x00C0-0x00FF # 扩展拉丁 0x4E00-0x9FA5 # CJK 统一汉字注意这个区间有两万多个码位 0x30A0-0x30FF # 日文片假名 0x0600-0x06FF # 阿拉伯文这个文件会直接变成转换器的字符范围来源。我见过不少开发者忽略这个清单直接用全量 Unicode结果文件大到编译都过不去。建议先把界面涉及的语言区段和实际用字扫进清单宁可多转几十个不要一上来转几万个。如果只做中文环境更稳妥的办法是放一个“界面汉字收集表”把代码里出现的汉字去重后列进去。4.2 执行转换与参数说明常见做法是用 LVGL 维护的命令行转换器基于 Node.js命令直观。下面是我常用的转换命令npx lv_font_conv \ --font ./SourceHanSansSC-Regular.otf \ --size 40 \ --bpp 4 \ --range-file ./char_range.txt \ --format lvgl \ --output ./mcu_font_40.c \ --force-fast-kern-format \ --no-compress参数拆开讲--font 指定源字体文件路径--size 是字形像素高度我习惯对齐 UI 设计稿里的字号--bpp 4 是为了给文字边缘留抗锯齿过渡1bpp 的锯齿在 TFT 屏幕上非常明显--range-file 比一串 --range 更好维护--format lvgl 表示输出 LVGL 可直接加载的 C 字库--force-fast-kern-format 会把字距表做成快速索引格式界面有大量西文混排时建议开启--no-compress 关闭位图压缩MCU 上省去解压开销。如果你的转换器版本没有 --range-file退化成一条条 --range 参数效果一样。提示生成完成后检查输出文件mcu_font_40.c 顶部应有 LV_FONT_DECLARE 声明文件内部能看到 cmaps 与 glyph_dsc 段。如果只有一坨数组而没有这些结构大概率是 --format 参数没配对。4.3 把字库注册到 LVGL 界面里转换完成后把 .c 文件拖进 MDK 工程并加入编译同时在需要引用字库的源文件里加声明和样式设置/* 在 main.c 或 ui 初始化处引用字库 */ LV_FONT_DECLARE(mcu_font_40); LV_FONT_DECLARE(mcu_font_30); /* 给 label 设置字体样式里的 text_font 字段 */ lv_obj_t *label lv_label_create(screen); lv_obj_set_style_text_font(label, mcu_font_40, LV_STATE_DEFAULT); lv_label_set_text(label, 你好世界 Hello LVGL);这里有一个细节lv_obj_set_style_text_font 的第二个参数要传 lv_font_t 指针而不是数组名LV_FONT_DECLARE 展开后就是 extern 声明。设置完字体后最好调用一次 lv_obj_refresh_style强制 label 按新字体重新排版否则切换字体时行宽和行高可能还是旧值。4.4 字符串编码UTF-8 一条道走到底LVGL 内部默认按 UTF-8 解码字符串。代码里的“你好世界”如果是用 GBK 编码保存显示就会乱码。要注意三个地方源文件保存编码设为 UTF-8编译器不要对中文字符串做本地代码页转码串口打印调试时不要用系统本地编码去猜字符串内容。MDK 环境下我习惯把多语言文本集中到一个头文件里管理/* lang.h 示意集中管理多语言文本源文件务必存成 UTF-8 */ #define TXT_TITLE u8环境监控 #define TXT_TEMPER u8温度: %d.%d°Cu8 前缀在 C11 里表示 UTF-8 窄字符串字面量能帮助编译器严格按编码存储如果编译器不支持就保证源文件另存为 UTF-8。多语言切换时只需要切换这一组宏的取值label 内部走同一套解码逻辑不用改 UI 代码。这一步做扎实了后续中英文切换基本就是换字符串指针的事。5. 常见问题与避坑大数组、阿拉伯文、基线与方框5.1 编译报 Flash 超限或 RAM 溢出字库成了体积炸弹现象工程一编译MDK 报 Flash 空间不足或者 ZI/RW 段暴涨程序跑起来动不动 HardFault。原因字库 .c 文件里的数组如果没加 const 修饰会被放进可读写数据段而不是只读数据段几十上百 KB 直接扣进 RAM另一个常见原因是字符范围转太全全字库在 512KB Flash 的 STM32F103 上根本放不下。解决先确认生成的字库数组声明带 const并在 lv_conf.h 里合理设置 LV_MEM_SIZE避免 label 的缓存把内存池吃光再按第 3.3 节的账本裁剪字符范围把用不到的区段剔除。我一般保留 0x20-0x7E 加中文常用字、日文假名生僻字单独做成小字库按需加载。5.2 阿拉伯文显示成散装字符没有连写效果现象阿拉伯文、波斯文区域的语言字符串显示出来是零散的单字符完全不是从右往左的连笔排版。原因阿拉伯文每个字母在不同位置有不同字形变体生成字库时如果没把变体字形和整形支持开起来LVGL 拿到的只有独立字形自然连不起来。解决转换时确认字符范围覆盖 0x0600-0x06FF 及必要的扩展区并开启转换工具里的阿拉伯文整形选项LVGL 侧要做右到左排版label 的 base_dir 需要设置为 RTL。单靠字体文件不整形显示永远是散的。5.3 中文与西文混排时文字“飘起来”基线对不齐现象一行里中文显示正常但英文、数字明显偏高或偏低相邻行的高度也不一致。原因中文字体的 base_line 和西文字体不一致转换时用了不同源字体或不同尺寸LVGL 以字库里的 base_line 和 line_height 为准排版于是出现基线漂移。解决多语言混排建议用同一个源字体做转换并统一 --size 和基线设置如果必须混两个字库把两个字库的 line_height 和 base_line 调成一致数值。界面样式上统一设 text_line_space 也能缓解视觉差。5.4 字符显示成方框但字库里明明有该语言现象某些字符在 PC 上正常烧到设备上变方框比如生僻汉字或特殊符号。原因两种情况最常见。一种是字符范围没覆盖到该字符的具体码点比如常用汉字区段之外还有扩展区另一种是源字符串本身不是 UTF-8 编码LVGL 解码出错误码点查不到字库里任何一个字形。解决用串口把待显示字符串逐字节打印出来对照 UTF-8 表确认码点再去查字库 cmap 是否包含这个码点。我一般会在转换参数里把界面用到的字符单独列一个清单而不是依赖默认字符集。5.5 升级 LVGL 小版本后字库崩溃或花屏现象原本正常的字库换了一个 LVGL 小版本后文字花屏、程序重启、定位不到字形。原因LVGL 字库二进制格式在不同版本之间有调整旧字库按旧布局生成新版本读取时字段解释错位。解决升级 LVGL 时同步用新版本转换器重新生成字库文件别拿旧字库硬跑同时备份转换命令方便一键重出。这也是为什么我在第 4 章强调把字符清单和命令留在工程目录里——换版本、换字体时后悔药就是这几条命令。6. 进阶运行时内存摸底与动态换字库字库选型定下来之后还有两件事值得在工程里落地。第一是内存摸底。LVGL 渲染位图时会在内存池里开缓冲如果同时显示多个 label 且字体很大内存池可能被短暂撑满。我习惯在 main 循环里定时打印一次内存池状态/* 打印 LVGL 内存池状态用于验证字库与渲染开销 */ lv_mem_monitor_t mon; lv_mem_monitor(mon); printf(total%lu free%lu max_free%lu frag%u%%\n, (unsigned long)mon.total_size, (unsigned long)mon.free_size, (unsigned long)mon.free_biggest_size, mon.frag_pct);如果 free_biggest_size 小于 4KB说明连续可用内存吃紧优先减小 label 缓区或换小字号字库。第二是动态换字库。多语言切换的本质是运行时把 label 的 text_font 从一个字库指针换到另一个字库指针。工程里可以维护这样一张表/* 动态语言切换用的小结构 */ typedef struct { const lv_font_t *font; const char *lang_tag; } lang_font_t; static const lang_font_t lang_table[] { { mcu_font_30, zh-CN }, { mcu_font_40, ar }, };切换语言时只换指针重新 set_text代价极小。需要注意阿拉伯文切换后整个容器的 flex 流向要一起翻转否则仅换字体、文字方向还是错的。从那以后我每次做多语言界面都会在架构里预埋这套指针表而不是把字体写死在样式里字体改版、语言新增时改动面被压缩到一行表项。这份字库工程资源的用法其实就藏在两个 mcu_font_*.c 文件的结构里把转换命令、字符清单和字库文件一起留档后面维护会轻松很多希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →