Py_FontMaker 在线生成 研究笔记
← 返回生成页

嵌入式点阵字库:从零到跑通

Py_FontMaker 完全文档——上篇讲怎么用(照着做即可),下篇讲原理与方案(格式/引擎/对比)。
免费使用Web 免安装LVGL v8/v9 + 裸机18 种变形语言
赶时间?直接跳 第 2 章,五分钟生成你的第一个字库; 遇到问题查 第 9 章排错手册。本文不需要任何前置知识。

1三分钟看懂点阵字库

电脑和手机上的字体文件(TTF/OTF)存的是矢量轮廓——每个字是一组贝塞尔曲线, 显示时由 FreeType 这样的引擎实时"画"出来。这需要相当的 CPU 和内存,小型嵌入式设备(手表、仪表、 家电面板)跑不动,也没必要跑。

嵌入式的做法是预先把每个字"画"成小图片——点阵位图,存进 Flash; 显示时按编号查出来贴到屏幕上即可,查表 + 拷贝,任何单片机都能轻松胜任。这个"把字体变成点阵图集合"的 过程就是字库生成,Py_FontMaker 干的就是这件事:

总体方案架构
整体流程:PC 端把 TTF 栅格化打包,设备端查表渲染

两个马上会遇到的概念

概念一句话解释怎么选
bpp(灰度位深) 每个像素用几个 bit 表示。1bpp 非黑即白有锯齿;2/4bpp 有灰度过渡,边缘平滑(抗锯齿);8bpp 最细腻 彩屏选 2bpp(大小与效果的最佳平衡);墨水屏/单色屏选 1bpp;追求极致选 4bpp
字号(像素) 字的高度,直接按屏幕像素理解,如 24 就是 24 像素高的字 看你的屏幕分辨率与 UI 设计稿;常用 16/24/32
字形渲染流程
一个字从 TTF 到屏幕像素的旅程

这套方案带来的效果

指标效果
内部 Flash 占用仅引擎(~2 KB) + 稀疏 cmap 表 + 兜底字库,字库本体 0 占用(放外挂 Flash)
RAM 占用1~2 个字形缓冲(几百字节) + 100 条描述缓存,与字库大小无关
字库升级bin 与固件用随机版本号配对,字库可独立 OTA;版本不匹配自动落兜底字库不花屏
接口LVGL v8 / v9 官方版自动适配,也提供裸机回调;一个宏切换

2五分钟跑通第一个字库

不需要安装任何软件,打开 pyfontmaker.com 即可。

正式生成

  1. 选生成模式:保持默认的「外挂字库」(pyf_lib.bin + 全套 C 代码)。 每个模式下方有"做什么/什么场景选它"的说明,拿不准就用默认。
  2. 选字体:两种方式任选——上传你自己的 TTF,或直接勾选服务器内置的免费字体 (25 个 SIL OFL 授权字体,可商用可分发,一个文件都不用传)。 例如做中文产品:勾「Noto Sans CJK SC(简体中文全量)」。
  3. 勾字符集:按语系勾选即可——「基本拉丁(ASCII)」默认已勾,做中文产品再勾 「中文(简/繁通用)」;也可以把界面会显示的文字直接粘贴进"需要生成的文本"框—— 只生成用到的字,库最小。语系之外的特殊区间才需要用"自定义码点范围"手填。
  4. 字号列表填 16,24(要几个字号写几个),其余参数保持默认 (每个参数右侧都有说明)。点开始生成。
  5. 看结果:完成后页面直接显示两张预览图——「字形预览」(字库数据直绘)和 「LVGL 实机渲染」;可改预览文本实时刷新,也可用"查字符"确认某个字在不在库里。 浏览器同时自动下载 result.zip。
全程服务器完成,任务串行排队;含全量中文的生成单字号约 1~3 分钟。 生成结果保留 2 小时,请及时下载。

模式选型与功能一览

模式产物什么场景选它
外挂字库pyf_lib.bin + 全套引擎 C 代码 字符多(中文/多语言)、固件放不下字库——绝大多数产品的主力方案,烧外挂 SPI Flash
内部字库pyf_inner_*.c(编译进固件) 字符集很小(几十~几百字)、不想接外挂 Flash 的小项目
XBF 字库.xbf + LVGL 官方 pyf_lvgl_*.c / pyf_lvgl_*.bin emWin 工程;纯 LVGL 想要零集成官方格式(LV_FONT_DECLARE / lv_binfont_create);自行二次加工
图标字库pyf_icon_*.c + 图标宏定义 iconfont.cn 图标当"字"渲染,与文本共用引擎,不用切图

拿不准就选「外挂字库」。另有两个命令行专属功能: emoji 图片字库(--EMOJI,Noto Emoji PNG 转灰度字库,绕开点阵码点只到 BMP 的限制)与 多语言 Excel 转 .c(--LANG,翻译表一键生成 pyf_language.c/h,依赖本机 Excel)。

预览与查字符(所见即设备所得)

3生成了什么:产物逐个认识

解开 result.zip,每个文件的职责:

文件它是什么 / 拿来干嘛
pyf_lib.bin字库数据本体,烧写到外挂 SPI Flash(或任何能提供"按偏移读"的存储)
pyf_lib.h字库配置总头文件:LVGL 开关、hash/RLE/缓存等编译期开关都在这
pyf_engine.c/h字库引擎(自动生成):查表、读 Flash、RLE 解压、缓存、版本校验、缺字兜底
pyf_app.c/h应用层模板:你只需要在这里实现一个"读 Flash"函数(第 4 章)
pyf_cmap_*.c各字库的码点索引表(XIP 常量,放固件里,查字 O(1))
pyf_fake_font_*.c兜底字库(编译进固件):字库损坏/缺字时显示占位框,不至于黑屏
pyf_shaper.c/h变形文字整形器(勾了语言包才有,第 6 章)
pyf_lvgl_*.c / pyf_lvgl_*.binLVGL 官方格式字库(XBF 模式产物,第 8 章):纯 LVGL 用户零集成
preview_*.png生成时的渲染预览图,拿给 UI 设计师确认效果
xbf/*.xbf中间格式点阵文件(兼容 emWin 的 FontCvtST),可独立使用或二次加工

4集成到你的工程(四步)

  1. 烧字库:把 pyf_lib.bin 烧进外挂 Flash(任意起始地址,记住它)。
  2. 加代码:把 zip 里所有 .c/.h 加进工程编译。
  3. 实现读接口:在你自己的代码里写一个 Flash 读函数(生成的 .c/.h 一行都不用改):
    static uint32_t my_flash_read(uint32_t offset, uint8_t *pbuf, uint32_t size)
    {
        /* 从 pyf_lib.bin 烧写起始地址 + offset 处读 size 字节到 pbuf,
           返回实际读到的字节数(成功即 size,失败返回 0) */
        bsp_flash_read(pbuf, size, PYF_LIB_START_ADDR + offset);
        return size;
    }
    引擎与具体 Flash 型号完全解耦——SPI NOR、SD 卡、甚至文件系统都行。
  4. 用起来:启动时把读函数注册给字库引擎:pyf_font_init(my_flash_read, printf)(第二个参数是可选的日志输出回调,printf 风格,传 NULL 则引擎静默),之后——
    LVGL(v8/v9 自动适配):字库就是标准 lv_font_t:
    lv_obj_set_style_text_font(label, &SourceHanCJK_24_2Bpp, 0);
    lv_label_set_text(label, "你好,世界");
    裸机:引擎暴露"取字形描述 + 取位图"两个回调,按位图逐像素画即可 (pyf_lib.h 里 LVGL_FONT_ENABLE 设 0)。
不想上真机先验证?本站生成结果里的「LVGL 实机渲染」预览用的正是配套的 LVGL PC 模拟器工程(字库 demo + 无头渲染器 lv_render_cli,LVGL 9.3 官方版)——网页上看到的就是设备端引擎画出来的效果, 不必自己搭模拟器。完整参考工程(RT-Thread + LVGL + Nordic,含烧写脚本)见 ZJ_RT_Thread_LVGL_Font_Nordic。

5参数详解:每一项的含义与效果

页面上每个参数旁都有简要说明;这里给出完整含义、效果与选择建议。数据格式层面的图解见下篇。

灰度 bpp(1 / 2 / 4 / 8)

bpp灰度级相对体积适用
12(黑白)1×单色屏 / 极致省空间;配合 mono hinting 边缘锐利
242×彩屏正文的性价比之选,抗锯齿已明显
4164×大字号标题、对边缘平滑要求高
82568×图标/emoji 字库,保留完整灰度

字号列表 / 字高(像素)

外挂字库一次可打包多个字号(如 16,24,32),其余模式填单一字高。 渲染器直接按像素设定字号(FreeType FT_Set_Pixel_Sizes),所得行高、基线为字体真实度量。

字符范围 + 文本(取并集)

范围段每行一对十六进制码点(如中文常用区 0x4e00, 0x9fa5);文本框里直接粘贴界面会出现的文字,两者取并集。 效果:只打包界面用到的几百个字,能把字库从几 MB 降到几十 KB,生成时间同步降一个量级。 码点连续时自动用"连续 cmap"(设备端 O(1) 直算),不连续时自动用"稀疏 cmap"(hash / 二分查表)。

LVGL 接口(True / False)

True:生成的 public_font_t 就是 lv_font_t,可直接赋给 style; 编译期通过 LVGL_VERSION_MAJOR 自动适配 v8 与 v9 官方版 (v9 按 v9.3 契约把字形展开为 A8 写入 draw_buf)。False:走裸机回调 get_glyph_dsc() + get_glyph_bitmap()。

hash 查表(True / False)

稀疏字库的 cmap 查找方式。True:开放寻址 hash 表,平均接近 O(1);False:排序数组二分,O(log n)。 两套表都生成在 C 文件里(XIP,不占 RAM),宏一键切换。

RLE 压缩(True / False)

效果:逐字形独立压缩,随机访问不受影响;1 bpp 字库(大片空白)收益最大,bpp 越高收益递减。 代价是解压需要双份字形缓冲(几百字节)。编码规则图解见 §11.5。此开关随 bin 写死到 pyf_lib.h,两者必须一致,不可手改。

字形缓存(True / False)

设备端缓存最近 100 条字形描述(可在 pyf_lib.h 改条数)。界面上反复出现的字符命中后 0 次 Flash 读,滚动列表明显更顺。

位流存储(True / False,默认 True)

True:打包时去掉每行末尾的补位,字形内按位连续,box_w 上报真实宽度——与 LVGL 官方打包方式一致。 效果:位图体积再省 5%~25%(1 bpp 收益最大)。False:退回行字节对齐布局(裸机代码不想处理位偏移时用)。两种布局图解见 §11.4。

基线坐标系(top / bottom)

选项存储约定适用
top(默认)FontCvtST 顶部基准,引擎运行时换算给 LVGL要兼容 emWin/FontCvtST 生态、沿用既有工程
bottomLVGL 原生(ofs_y=yMin、基线从底起算),LVGL 路径零换算纯 LVGL 新项目

约定标记写在 XBF 头部,打包器自动生成 BASELINE_TOP_REFERENCED 宏; 同一个 bin 内混用两种约定会在打包时直接报错。坐标系图解见 §11.6。

hinting(auto / normal / light / mono / none / autohint)

选项效果
auto(默认)1 bpp 自动用 mono,其余用 normal
normalFreeType 默认 hinting,笔画对齐像素网格,小字号更清晰
light轻 hinting,字形更忠实原设计,边缘更柔和
mono纯黑白渲染(仅 1 bpp),无灰度过渡,单色屏最锐利
none关闭 hinting,完全按矢量轮廓采样
autohint强制 FreeType 自动 hinter,字体自带 hint 质量差时可尝试

变形字体(Variable Font)轴参数

上传含 fvar 表的 VF 字体(如内置思源黑体,wght 250~900)后页面自动出现轴滑条, 服务器用 fontTools instancer 固化成静态实例再进管线。效果:一个 VF 文件产出任意字重的点阵字库。

kerning(自动,无需开关)

渲染时自动从 TTF 的 kern/GPOS 表提取字偶距(如 AV、To 的间距修正), 按目标字号缩放为 1/16 px 精度,随字库生成 XIP 查找表;设备端按"当前字符+下一字符"二分查找修正 advance。 效果:西文排版质量对齐 lv_font_conv;中文方块字无字偶距、零开销;字体没有 kerning 数据时表为空。 命令行打包可用 --kern False 关闭 XIP kern 表——全字库场景该表可达数百 KB 内部 Flash,空间紧张时建议关闭。

6变形文字:阿拉伯语、印地语、泰语……

有一大类语言,两个字符组合后字形会变——简单查表渲染必然出错:

为什么逐码点渲染会错
左:阿拉伯文连写选形;右:天城文辅音合体——都不是"一个码点一张图"能表达的

6.1 背景:哪些语言需要变形,为什么

需要"组合变形"(专业上叫 text shaping / 复杂文本布局)的语言覆盖 40+ 个国家,集中在四大类文字系统:

特殊情况:韩文谚文的音节已被 Unicode 全部预组合成独立码点,按普通字符处理即可; 希伯来文只是从右向左,字母不连写变形,不算变形文字;拉丁、西里尔、希腊、CJK、假名等都不需要变形。

6.2 三组语言包:机制与动态文本支持

组语言机制动态文本
A 组希伯来语、泰语、老挝语字形不变形,直接支持(RTL/组合符号零 advance + 位偏移叠画)✅ 完整支持
B 组阿拉伯语/波斯语/乌尔都语/维吾尔语等 Unicode 预变形区(U+FB50–FDFF / FE70–FEFF)+ 设备端小查表选形(pyf_shaper.c,约 3KB); 字体缺预变形字形时 PC 端用 HarfBuzz 合成入库✅ 完整支持
C 组天城文等印度系 9 文字、僧伽罗、藏、缅甸、高棉、蒙古 合体组合爆炸无取巧办法——把 HarfBuzz 放在 PC 端:按公开语料的高频字位簇整簇预渲染, 映射到 PUA 私用区码点,设备端最长匹配查表(引擎零改动) ✅ 字典命中即正确(96~99%+);未命中生僻簇降级逐码点显示,不乱码

字体 vs 语言包的分工:字体决定"字形从哪来"(原材料),语言包决定"如何整形"(加工工艺)。 只勾字体不勾语言 → 合体/连写散开;只勾语言没有对应字形 → 方框。勾选语言时 Web 会自动勾上对应内置字体。

多个 C 组语言合打一个字库时,簇字典共享 6400 个 PUA 码位(物理上限), 工具会按覆盖率自动削减并打印明细;单语言生成可达 99.9% 覆盖。 完整的方案论证(语言分级/体积估算/风险对策)见变形文字方案技术评审。

6.3 在本工具里怎么用

支持它们只需要两个勾选:

  1. 在「变形语言支持」勾选目标语言(如 天城文/阿拉伯字母系)—— 对应的内置字体会自动勾上;
  2. 正常生成。完成后在预览框粘一段该语言文字,亲眼确认连写/合体正确。
语言分级
三组语言的支持机制:A 直通 / B 运行时选形 / C 语料字典预整形

设备端唯一的额外动作:送显前把字符串过一次整形函数(生成包里的 pyf_shaper.c,约 3KB 查表代码):

char shaped[256];
pyf_font_shape_utf8("مرحبا بالعالم", shaped, sizeof(shaped), FONT_SHAPE_FLAG_BIDI_VISUAL);
lv_label_set_text(label, shaped);   /* LVGL 开了 LV_USE_BIDI 时 flags 传 0 */
动态文本(如服务器推送的新闻)也支持:C 组语言内置了从公开语料提炼的 高频字位簇字典(命中率 96~99%+),未命中的生僻组合自动降级为逐码点显示,不乱码。 把产品固定文案粘进"补充文案"框可保证 100% 命中。
簇预渲染管线
原理:PC 端用 HarfBuzz 把"字位簇"整簇渲染并映射到私用区码点,设备端仍是简单查表

7多字体组合(fallback 链)

没有任何一个 TTF 能覆盖所有文字(Noto 全家桶都是按文字系统分成上百个文件的)。 要做多语言产品,把多个字体组成一条链:

主字体放谁:覆盖拉丁 + 你主要 UI 语言的字体(如 Noto Sans CJK SC),因为它定行高和基线; 变形语言的字体放在链的任何位置都行。浏览器多选文件的顺序按文件名排,想指定主字体就给它起个排前面的名字(如 0_main.ttf)。
看到"标记定位异常, 已跳过":说明那份 TTF 是改制/重打包版本(下标轮廓被挪动而 GPOS 锚点没跟着改, 任何整形引擎都会画错,不是本工具的问题),已自动改用链中下一个健康的字体;若提示"链中无其它可用字体", 勾一个对应文字的内置 Noto Sans 字体或改用官方原版即可。
空格:XBF 模式只生成勾选范围与预览文本里的字符,空格总会自动带上(LVGL 缺空格字形会画占位方框)。

内置免费字体清单(全部 SIL OFL,可商用/嵌入/分发)

Web 上传框下方可直接勾选这些服务器内置字体;勾选"变形语言"时自动勾上对应文字的字体。 OFL 授权唯一要求:随产品保留字体版权声明,不得单独出售字体文件本身。

字体覆盖文字官方页面
思源黑体 KR(SourceHanSansKR-VF)拉丁/韩文/假名/繁体汉字(变形字体,可调字重) adobe-fonts/source-han-sans
Noto Sans CJK SC简体中文全量(含繁体/日韩汉字) notofonts/noto-cjk
Noto Sans Devanagari / Bengali / Gurmukhi / Gujarati / Oriya / Tamil / Telugu / Kannada / Malayalam / Sinhala 印度系 9 文字 + 僧伽罗文 Google Fonts · Noto
Noto Serif Tibetan / Sans Myanmar / Sans Khmer / Sans Lao / Sans Thai藏文 / 缅甸文 / 高棉文 / 老挝文 / 泰文 Google Fonts · Noto
Noto Naskh Arabic阿拉伯文(含波斯/乌尔都字母) Google Fonts
Noto Sans Hebrew / Mongolian希伯来文 / 传统蒙古文·满文·锡伯文 Google Fonts · Noto
Noto Sans Syriac / NKo / Javanese / Balinese / Cham叙利亚文 / NKo / 爪哇文 / 巴厘文 / 占文 notofonts.github.io

授权提醒:操作系统自带字体(macOS 冬青黑/GeezaPro/各 MN 系列、 Windows 微软雅黑等)多为商业授权,转成点阵字库分发给第三方通常超出授权范围——分发场景请使用本清单的 OFL 字体。

8进阶能力

8.1 LVGL 官方格式(零集成)

只想要标准 LVGL 字库、不想集成引擎?用「XBF 字库」模式,产物自动附带: pyf_lvgl_*.c(LV_FONT_DECLARE 即用)与 pyf_lvgl_*.bin (lv_binfont_create() 运行时从文件系统动态加载,可 OTA 换字库)。 官方格式同样携带变形文字簇——纯 LVGL 也能正确显示印地语。 注意位图超 1MB 的大字库需在 lv_conf.h 开 LV_FONT_FMT_TXT_LARGE。

8.2 BDF 导出:预览与逐字修饰

参数区勾「BDF 导出」,可用 BdfEditor/FontForge 打开预览、逐像素修饰个别字符 (小字号笔画粘连的手工优化),再用命令行回填:

python pyf_xbf_to_bdf.py --xbf xxx.xbf --patch edited.bdf --out-xbf new.xbf   # 1bpp 无损

8.3 命令行与批量化

Web 的每个能力都有对应的命令行版本(CI/批量生成场景)。源码暂未公开, 如需命令行/批量集成请通过页尾的公众号或 QQ 群联系作者;参数形式如下,供了解能力边界:

# 环境(Python ≥ 3.9)
python3 -m venv .venv && source .venv/bin/activate && pip install -r requirements.txt

# 多字体链 + 变形语言, 生成 XBF(自动附带 LVGL 官方格式)
python Py_FontMaker.py --XBF True --min 0x20 --max 0xfeff \
  --ttf main.ttf,NotoSansDevanagari.ttf,NotoNaskhArabic.ttf \
  --langs devanagari,arabic --bpp 2 --height 24

# 打包外挂字库(先按字号生成 XBF, 再打包)
python Py_FontMaker.py --LIB True --lib_file ./font_type/my_lib.txt --ttf main.ttf --bpp 2 --height 24

全部参数详解见第 5 章;需要"全语言通用字库"(25 字体链、覆盖全部内置语言) 可直接联系作者获取成品 bin,不必自己生成。

与 LVGL 官方字库如何选型,见下篇第 14 章逐维度对比。

9排错手册与常见问题

现象原因与解法
某些字显示空心方框 该字不在字库里:①字符范围/文本没包含它——补上重新生成; ②所选字体本身没有这个字形(如韩版思源缺大量简体字)——fallback 链里加一个覆盖它的字体。 用结果区的「查字符」可立即定位属于哪种。
印地语/高棉语个别组合散开显示 生僻字位簇不在语料字典(降级显示,不乱码)。把含它的文案粘进"补充文案"框重新生成即 100% 命中。
阿拉伯语显示成孤立字母或方向反了 送显前没过整形:调 pyf_font_shape_utf8();多行文本建议 LVGL 开 LV_USE_BIDI(此时 flags 传 0,别做二次 bidi)。
设备上全是兜底方框,PC 预览正常 固件与 bin 版本不配套(引擎有 magic 校验)——两者都用同一次生成的产物重新烧写。
换了 RLE/位流开关后乱码 这两个开关随 bin 写死,不能只改 pyf_lib.h——重新生成一套即可。
生成的字库太大 ①只放用到的文本,别全量 0x4e00-0x9fa5;②开 RLE + 位流;③降 bpp(4→2); ④多语言时按目标市场拆分生成。
全量 CJK 生成很慢? 0x4e00~0x9fa5 逐字栅格化需要一些时间属正常;只打包界面用到的文本能把时间和体积都降一个量级。
生成的 .c 里字体名不是合法 C 标识符? TTF 文件名只用字母/数字/下划线;Web 端会自动清洗。
emoji 怎么显示?码点超过 0xFFFF? 点阵字库码点上限为 BMP(u16);增补平面的 emoji 用图片字库方案(命令行 --EMOJI)。
裸机下开了位流存储,位图怎么解析? 字形内按位连续:第 y 行起始 bit = y×box_w×bpp,逐行取 box_w×bpp 位。不想处理位偏移就把「位流存储」设为 False。
top 和 bottom 基线能混用吗? 同一个 bin 内不能(打包会报错);不同工程/不同 bin 各选各的没问题。
LVGL v9 需要额外适配吗? 不需要。生成代码按 LVGL_VERSION_MAJOR 自动切换 v8/v9 回调契约(按 v9.3 官方发布版验证)。
多选字体时怎么确定谁是主字体? 上传框下方会列出链顺序,第一个即主字体(行高/基线/kerning/字库名);浏览器多选一般按文件名排序,必要时给主字体加个排前面的文件名前缀。 变形语言的字形来源另按"标记定位健康检查"自动选,结果区会显示各语言实际用的字体。
结果区提示某字体"标记定位异常, 已跳过"是什么意思? 那份 TTF 的 GPOS 锚点与字形轮廓不一致(常见于网上重打包的字体),下标/元音会画到基字外或互相压住,任何整形引擎都一样。 工具已自动改用链中下一个健康的字体;建议改用内置 Noto Sans 系列或官方原版。
结果 zip 能保留多久? 服务器保留 2 小时,请及时下载;任务串行排队执行。
下篇 · 原理与方案详解 数据格式规格 · 引擎设计 · 优缺点 · LVGL 官方对比——不影响使用,但理解之后所有参数你都能自己做决定
📝 说明:文中提到的 *.py / tests/* 等源码文件为工具内部实现的落点记录,源码暂未公开;不影响阅读方案本身。

下篇面向使用/评估本方案的嵌入式开发者,回答三个问题:

  1. 当前点阵字库方案是什么(数据格式、生成管线、设备端引擎);
  2. 它的优点和缺点分别是什么(逐条给依据,含代码位置);
  3. 它和 LVGL 官方点阵字库(lv_font_conv 产物)的差异在哪里(逐维度对比 + 选型建议)。

文中所有结论均以本仓库代码为准(pyf_ttf_to_fontcvtst_xbf.py、pyf_xbf_to_fontlib_bin.py、pyf_engine_code.py 等);LVGL 侧以 v8.3 的 lv_font_fmt_txt 与 lv_font_conv 官方文档为参照,v9 的接口差异在 §14.5 单独说明。


10方案总览

一句话概括:PC 端把 TTF 逐字栅格化成灰度点阵,打包成一个 pyf_lib.bin 烧进外挂 SPI Flash;设备端用自动生成的 C 引擎按需逐字形读取、解压、渲染,向上以 lv_font_t(LVGL)或裸机回调两种接口暴露。

(总体架构图见第 1 章。)

整条链路分两段:

PC 端工具链(Python,跨平台,另有 Web 在线生成与本地 GUI):

阶段 脚本 输入 → 输出
① 栅格化 pyf_ttf_freetype_to_xbf.py(默认,FreeType 直连;--renderer pygame 退回 pyf_ttf_to_fontcvtst_xbf.py) TTF → .xbf + .kern.json(每字号/每 bpp 一组)
② 打包 pyf_xbf_to_fontlib_bin.py 多个 .xbf + 码点配置 → pyf_lib.bin + 全套 C 代码
配套 pyf_ttf_to_fake_font_c.py / pyf_ttf_icon_to_c.py / pyf_emoji_to_c.py / pyf_ttf_to_inner_font_c.py / pyf_excel_multi_language_to_c.py fake 兜底字库 / 图标字库 / emoji 图片字库 / 内部字库 / 多语言表

变形字体(Variable Font)先用 fontTools instancer 按轴(如 wght 字重)固化成静态实例再进管线,一个 VF 文件可产出任意字重的点阵字库。

设备端(自动生成的 C 代码,见 pyf_engine_code.py 模板):各文件职责见第 3 章产物表,引擎内部机制见第 12 章。


11数据格式详解

11.1 字符集与 Unicode:字是怎么编号的

计算机里每个字符都有一个全球统一的编号——Unicode 码点,写作 U+XXXX。 比如 A 是 U+0041、中 是 U+4E2D。常用汉字集中在 U+4E00~U+9FA5(约 2 万个)。文件与网络传输时,码点按 UTF-8 规则变成 1~4 个字节存储; 你在网页上填的"字符范围"就是码点区间,粘贴的文本会被自动转回码点。 点阵字库本质上就是一张「码点 → 位图」的大表。

11.2 XBF 中间格式

XBF 是①阶段的产物,格式兼容 FontCvtST(ST 提供的 emWin 字体转换工具),因此也可以脱离本方案单独给 emWin/裸机工程使用。本工具直接生成 XBF,不依赖任何第三方 exe。

XBF 文件布局

要点(对应 pyf_ttf_to_fontcvtst_xbf.py:34-157):

11.3 pyf_lib.bin 外挂字库格式

②阶段把若干 XBF 按各自的码点配置文件(font_type/*.txt,"范围段 + 文本段取并集")裁剪后打进一个 bin。

pyf_lib.bin 布局

要点(对应 pyf_xbf_to_fontlib_bin.py:85-111, 732-741, 1164-1361):

区域 起始地址 内容
字库头 0x0000 魔数 "John_LIB"(8B) + 随机版本 magic(u32) + 字体数(u32) + 首字体偏移 u16(=0x1000)
字体信息表 0x0100 每字体 64 B:44 B 名称 + bpp/cmap_continued/y_size/y_dist/baseline/l_hight/c_hight/first_code/last_code/cmap_offset
字体数据区 0x1000 依次为每个字体的 cmap 区 + 字形记录区

cmap 有两种形态,打包时自动判定(码点集合连续 → 连续表,否则稀疏表,pyf_xbf_to_fontlib_bin.py:722-727):

字形记录 = 6 字节属性头(u8 adv_w/box_w/box_h、s8 ofs_x/ofs_y、u8 bytes_per_line,比 XBF 的 12 B 减半)+ 位图(可 RLE)。

版本闭环是这个格式的特色:每次打包生成随机 magic,同时写进 bin 头与 pyf_lib.h 的 PYF_LIB_MAGIC 宏;设备启动时引擎校验二者一致,不一致(固件与字库脱节)则全部退回 fake 字库并可由 pyf_font_version_ok() 触发字库 OTA,保证字库可以独立于固件发布/升级而不会花屏。

11.4 位图打包方式

2026-09 更新:--bitstream(默认开)已支持字形内连续位流存储——打包进 bin 时去掉行尾补位,box_w 上报真实宽度,与 LVGL 原生打包方式一致;--bitstream False 退回下图左侧的行字节对齐布局。下图描述的是两种可选布局的差异。

XBF 中间格式每行按字节对齐(bytes_per_line = ceil(box_w × bpp / 8),保持 FontCvtST/emWin 兼容);打包为 pyf_lib.bin 时默认重打包为连续位流,LVGL 官方也是连续位流:

位图打包对比

历史行为说明:旧版(--bitstream False)设备端上报的 box_w 按 bytes_per_line × 8 / bpp 反推,即对齐后的宽度——真实 13 列、2 bpp 会上报 16 列,右侧 3 列为补 0 空白列。位流模式下该问题不复存在(pyf_lib.h 的 BITMAP_BIT_CONTINUOUS_ENABLE 宏由工具随 bin 写死,两者必须一致)。

11.5 RLE 压缩

--rle True(默认开)时对每个字形的位图单独做字节级 RLE(pyf_run_length_encoding.py),cmap 中的 size 记录压缩后长度,解码器约 30 行 C(pyf_rle_glyph_bitmap_decode):

RLE 编码

11.6 基线与坐标系

2026-09 更新:基线坐标系已做成可选(--baseline top|bottom)。top 为 FontCvtST 顶部基准(默认,兼容 emWin 生态);bottom 直接按 LVGL 底部基准存储(ofs_y = yMin、baseline 从行框底起算),LVGL 路径零换算。约定标记写在 XBF 头 0x0C 保留字段,打包器自动检测并生成 BASELINE_TOP_REFERENCED 宏,引擎按宏对齐换算;同一个 bin 内混用两种约定会在打包时报错。

top 模式下,存储侧的垂直量以行框顶部为原点向下计(baseline = ascent、ofs_y = ascent − yMax + 1);而 LVGL 以基线为原点向上计,引擎在 get_glyph_dsc 里做一次整数换算:

基线坐标系


12设备端引擎

取一个字形的完整路径如下(pyf_engine_code.py 生成的 pyf_engine.c):

设备端取字流程

几个关键设计:

  1. 两级缓存

    • 字形描述缓存:全局 font_cache[100](PYF_FONT_DSC_CACHE_CNT,可改),按 (unicode, font_type) 线性扫描命中则 0 次 Flash 读——界面上反复出现的字符(数字、标点、固定文案)基本全命中;
    • 单条 cmap 缓存:每个字体记住上一次查过的 {code, offset, size},get_glyph_dsc 和紧随其后的 get_glyph_bitmap 查同一个字时第二次 cmap 查找免掉。
    • 位图本体没有缓存:每次绘制都要从 Flash 读一次位图(详见 §13.2)。
  2. 三种 cmap 查找

    • 连续(全字库/连续区间):直算偏移 + 读 Flash 6 B;
    • 稀疏 + hash(默认):XIP C 表开放寻址,平均接近 O(1);
    • 稀疏 + 二分:XIP 排序数组,O(log n)。
  3. fake 字库兜底:码点越界、cmap 查不到、size == 0(生成时就缺字)、bin 版本 magic 不匹配——统统落到编译进固件的 fake 字库显示占位字形,任何情况下不花屏。

  4. 与存储解耦:引擎只通过 pfunc_get_data(offset/buf/size)拉数据,SPI NOR、QSPI、eMMC、甚至文件系统都能接。

  5. LVGL / 裸机双接口:LVGL_FONT_ENABLE=1 时 public_font_t 就是 lv_font_t,pyf_font_create() 填好 get_glyph_dsc/get_glyph_bitmap/line_height/base_line 后可直接赋给 style;=0 时是等价的自定义结构体,裸机直接调两个回调。

  6. LVGL v8 / v9 双版本支持(2026-09 新增):生成代码通过 LVGL_VERSION_MAJOR 自动识别大版本。v8 下 get_glyph_bitmap 直接返回位流位图(v8 渲染器原生按 width_bit = box_w × bpp 位寻址);v9(按 v9.3 官方发布版契约)下走包装回调——从 g_dsc->gid.index 取码点、读 Flash 解码后,由共享的 pyf_font_bitmap_to_a8_draw_buf() 展开为 A8(每像素 1 字节,按 draw_buf->header.stride 对齐)写入 LVGL 分配的 draw_buf 并返回,与官方 lv_font_fmt_txt 同一行为;glyph_dsc 按版本填 bpp(v8)或 format + gid(v9)。fake/内部/图标/emoji 四类 C 字库同样双版本适配。


13方案优点与缺点

13.1 方案优点

按重要性排序:

  1. 内部 Flash / RAM 占用极小,CJK 全量字库可用。字库数据全部在外挂 SPI Flash;固件里只有引擎(~2 KB 代码)、稀疏 cmap C 表(仅裁剪字库需要)和 fake 字库。RAM 只需 1~2 个字形位图缓冲(MAX_STATIC_BITMAP_BUF_SIZE = 最大字号的 y_dist²·bpp/8,如 24px/2bpp 约 256 B;56px/2bpp 约 900 B)+ 100 条 dsc 缓存。这是 LVGL 官方两条路径(C 数组占内部 Flash、binfont 占 RAM)都做不到的。

  2. 字库独立升级的版本闭环。bin 与 C 头文件用随机 magic 配对,版本错落 fake 字库 + pyf_font_version_ok() 上报,OTA 时字库和固件可以分开发布、分开烧写,工程上非常实用。

  3. 多字体多字号一包。一个 pyf_lib.bin 打包 N 个字体×M 个字号(含图标字库、emoji 字库),信息表统一管理,应用层用枚举取字体;固件与字库的对应关系只有一处。

  4. 全套 C 代码自动生成,拿来即用。用户唯一要写的是一个 Flash 读函数;LVGL/裸机、hash/二分、RLE 开关、缓存条数都是 pyf_lib.h 里的宏。

  5. 缺字兜底。fake 字库保证任何异常路径都有占位显示,不会出现乱码/花屏——对量产设备是刚需。

  6. 按需裁剪,体积可控。码点配置支持"范围 + 界面文案文本"取并集,"只打包界面用到的几百个字"能把字库从几 MB 降到几十 KB,生成时间也降一个量级。

  7. 查表结构为外挂场景优化。连续区间直算 O(1);稀疏区间 hash 平均 O(1) 且表在 XIP 不占 RAM;dsc 缓存吸收热点字符的重复查询。

  8. RLE 逐字形压缩,随机访问不受影响,1 bpp 字库收益显著;解码器极简(无状态、无查找表)。

  9. 工具链友好:纯 Python 跨平台(服务器无头环境可跑)、Web 在线生成(上传 TTF 选参数下载 zip)、变形字体一个文件出全字重、XBF 兼容 emWin 生态、附带 iconfont/emoji/多语言 Excel 一条龙。

  10. emoji 图片字库:把 Noto Emoji PNG 转成同一 bin 里的一个"字体",用同一引擎渲染——LVGL 官方点阵字库路线没有对应能力(需另接 lv_imgfont)。


13.2 方案缺点与限制

同样按影响面排序,均给出代码依据:

  1. 渲染吞吐受 SPI 总线限制,位图无缓存。每个字形每次绘制都要 1~2 次 Flash 读(dsc 未命中缓存时 6 B 属性 + 位图本体);滚动长文本、跑马灯时同一个字反复读取。dsc 缓存只缓存 6 B 描述,不缓存位图(__user_font_get_bitmap 每次都走 Flash)。对比 LVGL C 数组的零 IO,这是外挂方案的本质代价,但位图 LRU 缓存是明确的补强空间(见 §15)。

  2. 无 kerning(字距调整)(2026-09 已解决)。freetype 渲染器自动从 TTF 的 kern/GPOS 表提取字偶对(含 PairPos format1/2 与 Extension),按目标字号缩放为 1/16 px 定点写入 XIP kern 表;引擎在 get_glyph_dsc 按 (left<<16)|right 二分查找修正 adv_w(与 lv_font_fmt_txt 同款舍入),dsc 缓存存基础值不受污染。

  3. advance 存储为整数像素(bin 中 u8,2026-09 部分改进)。freetype 渲染器按 26.6 定点四舍五入,逐字形精确;kern 修正按 1/16 px 定点计算后舍入——这已达到 LVGL 自定义字体回调接口(dsc->adv_w 为整数)能表达的精度上限。与官方剩余差距仅在无 kern 长串的亚像素累积(官方 fmt_txt 同样逐字形舍入输出,实际无差);u8 存储仍限制单字形 advance ≤255 px(72 px 字号内安全)。

  4. 码点仅支持 BMP。XBF 头与 cmap 都是 u16(pyf_ttf_to_fontcvtst_xbf.py:43-44),增补平面(含大多数 emoji)必须走图片字库产线;bin 的稀疏 cmap 条目也是 u16 码点(emoji 字体单独用 u32 变体)。

  5. 部分字体度量硬编码(2026-09 已解决)。y_size/y_dist/baseline 全部渲染路径(freetype / pygame / FontCvtST 导入)均直接取 XBF 头的实测值;按字号查的经验值表已删除,"字号不在表内沿用上一个字体度量"的隐患随之消除。 注意:legacy(pygame) 路径旧表输出的行高偏大(如 16px 为 18/18/16,实测为 16/16/13),依赖旧值排版的存量工程升级后需过一遍视觉确认。

  6. 强依赖文件命名约定。bpp 从文件名 ..._2Bpp 解析、字号取 name[-7:-5] 两位数字(pyf_xbf_to_fontlib_bin.py:1112-1113, 1295);文件名不合规会产出错误的元数据,且字号只支持两位数。

  7. hash 表装载因子为 1.0。生成时 hash 表大小 = 字符数(pyf_xbf_to_fontlib_bin.py:1436),满载的开放寻址线性探测在聚簇处会退化,未命中查找最坏遍历整表(先被 first/last 区间过滤挡掉大部分)。README 所说 O(1) 是平均意义上的。

  8. 位图行对齐带来的宽度虚报(2026-09 已解决)。--bitstream(默认开)改为字形内连续位流存储,box_w 上报真实宽度;仅 --bitstream False 的兼容模式仍是旧行为。实测 ASCII 测试字库(16px/2bpp + 24px/1bpp)位流化单独可省约 25% 位图体积(1bpp 收益最大;RLE 开启时行尾 0 本就可压缩,叠加后净收益缩小)。

  9. 静态缓冲非线程安全。pyf_glyph_bitmap_buf 是全局静态,get_glyph_bitmap 返回其指针,多线程同时取字会互相踩;LVGL 单线程模型下没问题,裸机双核/RTOS 多任务需自行加锁。RLE 静态模式还要双倍缓冲。

  10. 生成侧渲染质量依赖 pygame(2026-09 已解决)。XBF 默认由 pyf_ttf_freetype_to_xbf.py 直连 FreeType:FT_Set_Pixel_Sizes 精确像素字号(不再 pt 扫描)、原生覆盖度灰度(不再亮度反推)、hinting 五档可选(1bpp 默认 mono 渲染更锐利)、生成速度快一个量级。fake/内部/图标字库生成器仍走 pygame(占位/图标字形对渲染精度不敏感),--renderer pygame 可整体退回旧链路。

  11. 生成的 LVGL 接口是 v8 签名(2026-09 已解决)。生成代码按 LVGL_VERSION_MAJOR 自动适配 v8 与 v9(v9.3 官方发布版契约),见 §14.5。

  12. 工程细节类:字体信息区 0x100~0x1000 上限约 60 个字体;pyf_cmap_*.c 稀疏表同时以二分数组 + hash 表两份形态生成(XIP 体积 ×2,好在仅裁剪字库有此表);bin 内稀疏 cmap 为预留未被引擎使用。


14与 LVGL 官方字库对比与选型

14.1 LVGL 官方点阵字库简介

LVGL 官方的静态点阵字库由 lv_font_conv(Node.js CLI,亦有官方在线转换器)生成,运行时格式是 lv_font_fmt_txt:

lv_font_t
 └─ lv_font_fmt_txt_dsc_t
     ├─ glyph_bitmap[]           位流紧凑打包的位图池
     ├─ glyph_dsc[]              每字形 {bitmap_index:20, adv_w:12(8.4 定点), box_w/h, ofs_x/y}
     ├─ cmaps[]                  多段 cmap(FORMAT0_FULL/TINY、SPARSE_FULL/TINY 四种)
     ├─ kern_dsc                 kerning:pair(字偶对)或 class(字距类矩阵),1/16 px 精度
     ├─ bpp: 1/2/3/4             抗锯齿位深(--lcd/--lcd-v 另有 ×3 子像素)
     └─ bitmap_format            RAW 或 COMPRESSED(XOR 行预滤波 + 位级 RLE)

两条使用路径:

另有运行时矢量方案(Tiny TTF、FreeType 接口),不属于点阵字库,此处不展开。


存储与访问路径对比

14.2 存储与资源占用

维度 Py_FontMaker 外挂字库 LVGL 官方 lv_font_conv
字库数据位置 外挂 SPI Flash(pyf_lib.bin) 内部 Flash C 数组(XIP)或 binfont 载入 RAM
内部 Flash 占用 引擎 + 稀疏 cmap 表 + fake 字库(KB 级) 整个字库(CJK 24px/2bpp 全量 ≈ 3 MB+)
RAM 占用 位图缓冲(几百 B~1 KB)+ dsc 缓存(100×~12 B) C 数组 0;binfont ≈ 字库体积
渲染时 IO 每字形 1~2 次 SPI 读(dsc 缓存命中 0 次) 0
字库独立升级 ✅ bin 单独烧写/OTA,magic 版本闭环 C 数组随固件;binfont 可换文件但无版本机制
多字体打包 ✅ 一个 bin 装 N 字体 M 字号 + icon + emoji 每字体独立 .c/.bin

14.3 格式能力

维度 Py_FontMaker lv_font_conv
码点范围 BMP(u16);增补平面走 emoji 图片字库 全 Unicode(u32),支持多 range 与码点重映射
cmap 单区间;连续直算 / 稀疏 hash(平均 O(1))或二分 多段 cmap,段内二分;无 hash
bpp 1/2/4/8 1/2/3/4(另有 --lcd/--lcd-v 子像素 ×3)
advance 精度 整数像素(bin 内 u8) 12 bit 8.4 定点(1/16 px)
kerning ✅ kern/GPOS 提取,XIP 表二分查找,1/16 px ✅ pair / class 两种,1/16 px
位图打包 默认字形内连续位流(--bitstream False 可退回行字节对齐) 默认位流紧凑(可 --byte-align 切回)
压缩 逐字形字节级 RLE(随机访问友好) XOR 行预滤波 + 位级 RLE(--no-compress 可关)
缺字处理 fake 字库兜底 + 版本校验,永不花屏 lv_font_t.fallback 字体链(需自行配置)
彩色 emoji ✅ PNG → 图片字库,同一引擎渲染 ❌ 点阵路线不支持(需 lv_imgfont 另做)

14.4 工具链与生态

维度 Py_FontMaker lv_font_conv
实现语言 Python(pygame + fontTools) Node.js
使用形态 CLI / 本地 GUI / Web 在线生成(自部署) CLI / LVGL 官网在线转换器
变形字体 ✅ 轴固化(Web 端拖滑条) 需自行先固化
中间格式互通 XBF 兼容 FontCvtST / emWin 生态 无
字符集配置 范围段 + 界面文案文本取并集 -r 范围 / --symbols 字符列表
附加产线 iconfont、emoji、多语言 Excel、内部字库、fake 字库 仅字体转换(icon 可把 FontAwesome 当字体转)
渲染引擎 FreeType 直连(hinting 五档可选;--renderer pygame 退回旧链路) FreeType 系(--autohint-off/--autohint-strong)

14.5 LVGL 版本兼容性

2026-09 更新:生成代码已同时支持 LVGL v8 与 v9 官方发布版,编译期通过 LVGL_VERSION_MAJOR 自动切换,无需手工适配。

两个大版本的回调契约不同:

/* v8:get_glyph_bitmap 返回位流位图指针, 渲染器按 width_bit = box_w*bpp 位寻址 */
const uint8_t * (*get_glyph_bitmap)(const lv_font_t *, uint32_t);

/* v9(按 v9.3 契约):由字体把字形展开为 A8(每像素 1 字节)写入 LVGL 分配的
 * draw_buf(行按 header.stride 对齐), 返回 draw_buf; 码点从 g_dsc->gid.index 取 */
const void * (*get_glyph_bitmap)(lv_font_glyph_dsc_t *, lv_draw_buf_t *);

引擎的实现方式:内部保留 v8 风格的裸回调(pyf_font_get_bitmap_raw / pyf_fake_font_get_bitmap_raw,跨字库兜底委托统一走裸回调,任何版本下都可直接调用);v9 下再套一层包装回调,用共享的 pyf_font_bitmap_to_a8_draw_buf() 完成 A8 展开——与官方 lv_font_fmt_txt 的行为一致。glyph_dsc 填充由 FONT_GLYPH_DSC_SET_FORMAT 宏按版本落到 bpp(v8/裸机)或 format + gid.index(v9)。fake/内部/图标/emoji 字库同样适配。v9 的 release_glyph 置 NULL(引擎从不设置 g_dsc->entry,LVGL 不会调用它)。

参考工程当前基于 RT-Thread + LVGL v8 验证(https://gitee.com/woowill/ZJ_RT_Thread_LVGL_Font_Nordic.git);v9 路径已在宿主机用 v9.3 结构桩做过运行时逐位验证(tests/test_host_c.py),上板验证欢迎反馈。

14.6 一图流总结

字符集小(拉丁/数百字) 字符集大(CJK 全量/多字号)
内部 Flash 充足 LVGL C 数组(官方,最优) LVGL C 数组(能塞下也可)
内部 Flash 紧张 / 字库需独立升级 两者皆可,看是否要 OTA Py_FontMaker 外挂字库(主场)

14.7 选型建议

选 LVGL 官方 lv_font_conv,当:

选本方案(Py_FontMaker 外挂字库),当:

两者混用也常见:界面骨架用 LVGL 内置 Montserrat(C 数组),中文正文/大字号用本方案外挂字库——lv_font_t 层面完全等价,可在同一个 label 样式体系里共存。


15可优化方向

结合 §13.2 的缺点,按投入产出排序。每一项都不是"免费的午餐",收益与代价并列如下:

优化项优化后的收益代价与风险建议
1. 字形位图 LRU 缓存
dsc 缓存旁加 N 条位图缓存
滚动列表/跑马灯里反复出现的字形 0 次 SPI 读(当前每次绘制都读位图本体),IO 降一个数量级,帧率更稳——收益最大的单点优化 RAM 显著增加:每条缓存存整张位图(24px/2bpp 约 150 B,56px/2bpp 约 800 B),起效需几十条即 KB~十 KB 级;LRU 管理增加引擎复杂度;字库 OTA 后需清缓存;"一次性滚过大量不同字"(电子书翻页)命中率低、收益有限 做成宏开关 + 可配条数,默认关,RAM 富余的项目自行开启
2. hash 表预留空槽
表大小 = 字符数 × 1.25~1.5
查找长度稳定接近 O(1),消除满载线性探测在聚簇处的退化;未命中遇空槽提前终止(现在最坏遍历整表)——缺字查询多的多语言混排提速明显 XIP 的 cmap C 表体积 ×1.25~1.5(全量 CJK 约 200 KB → 250~300 KB 内部 Flash,与"省内部 Flash"的卖点相抵);生成器与引擎 hash 计算需同步修改,新旧产物不互通(magic 版本闭环可挡住错配) 只有裁剪型稀疏字库受益(连续 cmap 不走 hash);多语言产品值得做
3. adv_w 升级 8.4 定点 u16
属性头 6 B → 7~8 B
主要是解除单字形 advance ≤ 255 px 的上限(72 px 以上超大字号安全);理论精度对齐 lv_font_conv 的 12 bit 定点 属性区体积 +17~33%(位图占大头,整库约涨几个百分点);bin 格式 break,工具 + 引擎 + 测试全链路改 实际排版收益很小:kern 已按 1/16 px 修正,且 LVGL 自定义字体回调的 adv_w 本身是整数输出,当前已达接口精度上限——等超大字号需求出现再做
4. 码点扩展到 u32
打通增补平面
CJK 扩展 B~G 生僻字(人名/古籍)、少数民族文字、历史文字可直接进普通字库,对齐 lv_font_conv 的全 Unicode 能力 四项里最重:XBF 头的 First/LastCode 是 u16,扩位即破坏与 FontCvtST/emWin 的格式兼容(XBF 的立身之本),需引入新格式版本或双模式;稀疏 cmap 条目 8 B → 10 B;hash/二分/缓存/测试全链路改 u32 BMP 已覆盖绝大多数产品需求,emoji 另有图片字库产线兜底——需求驱动再做

一句话排序:1 体验收益最大(做成可选项)→ 2 看产品形态(多语言裁剪字库才值得)→ 3、4 都会 break 格式,等真实需求出现再动,别白付兼容成本。

16深入阅读与交流

微信公众号
微信公众号
QQ群 BLE5.4 学习讨论 177341833
QQ群 177341833
QQ群 BLE 开发学习 498676838
QQ群 498676838

本文由 2022 年的《嵌入式字库研究及 Py_FontMaker 工具编写》重写而来,内容与当前代码保持一致。 交流:公众号 Bluetooth-BLE · QQ 群 177341833 · 返回生成页