4a4ad6bc5f
现状口径:/keil-build 与 /serial-boot-test 是 DSH 会话的技能调用名, 不是 shell 命令;「名 + 命令行参数」的命令形态改为可执行体 run_boot_test.py 的真实调用。 - AGENTS.md:别名说明重写为「技能调用名不是 shell 命令」,两条目改 「编译(/keil-build)」「串口回归(/serial-boot-test)」 - keil-build / serial-boot-test 两 skill 调用名与示例同步;task-worktree 方案文档去掉 @build/@test 惯例从句;@task 保留(不在点名范围) - 123 文件现状陈述全改;台架捕获、变更记录/时间线、rename 映射输入 等 41 行历史证据保留旧名 - gen_memory_svg.py 6 处措辞随改,3 张 SVG 产物同批重跑 - 顺手补并行方案同行漏网计数 51→52 钩子 验证:提交期快钩子对 123 文件实跑全 Passed;check_doc_links --all rc=0(3038 处、无法判定 0);check_mermaid --all rc=0;YAML 解析 52 钩子不变;skill-size rc=0;py_compile 全过;字节卫生零漂移。
190 lines
14 KiB
Markdown
190 lines
14 KiB
Markdown
# UI 中文字体生成与配置
|
||
|
||
> **适用范围**:STM32F4-Base 远程单色屏(160×160,1bpp,`LV_COLOR_FORMAT_I1`)的 LVGL 中文显示。
|
||
> **目标**:把工程实际用到的汉字生成**子集化 1bpp 位图字体**,挂到 `LV_FONT_DEFAULT`,使菜单/标签正常显示中文并推流到客户端。
|
||
|
||
---
|
||
|
||
## 1. 背景
|
||
|
||
- LVGL 内置字体(Montserrat 等)**不含中文**;`LV_FONT_SIMSUN_16_CJK` 等全字库体积过大,不适合 160×160 小屏。
|
||
- 本工程显示是 **1bpp 单色**:像素 1=亮(白)/0=暗(黑),与客户端 `mono_to_png` 的 `1→白` 语义一致。
|
||
- 方案:用 `lv_font_conv` 把系统中文字体(**宋体 SimSun**)按"实际用到的汉字 + ASCII"子集,生成 1bpp 的 LVGL 字体 `.c`。
|
||
|
||
生成产物:`Drivers/BSP/UI/ui_font_cn_12.c`(变量名 `ui_font_cn_12`)。
|
||
|
||
---
|
||
|
||
## 2. 工具准备
|
||
|
||
### 2.1 工具与环境确认
|
||
|
||
| 工具 | 用途 | 安装 |
|
||
|------|------|------|
|
||
| Node.js (≥ v14) | 运行 `lv_font_conv` | 已具备 |
|
||
| `lv_font_conv` | TTF/OTF → LVGL 字体 `.c`(官方离线转换器) | `npm i -g lv_font_conv` |
|
||
| Python `fontTools` | `make_font.py` 读取 `.ttc` 集合时抽取单字重(仅在字库为 `.ttc` 时需要) | `pip install fonttools` |
|
||
|
||
安装完成后验证可用:`lv_font_conv -h` 能打印用法即成功。
|
||
|
||
> **Python 解释器(统一入口)**:用仓库内 `.venv`(已装 fontTools,见 AGENTS.md「Python 环境」):
|
||
>
|
||
> ```powershell
|
||
> & '.venv\Scripts\python.exe' tools/font_gen/make_font.py
|
||
> ```
|
||
>
|
||
> PATH 默认 `python` 是 msys64 的(无 fonttools),直接跑会失败。`.venv` 缺失/换机时重建:`& <本机任一 python ≥3.10> -m venv .venv; & '.venv\Scripts\python.exe' -m pip install -r requirements.txt`(`requirements.txt` 已含 `pre-commit`,重建后 主界面门禁直接可跑)。
|
||
> `extract_cn.py` 纯标准库,任意 python3 均可。
|
||
|
||
---
|
||
|
||
## 3. 提取字符集(只取会显示的汉字)
|
||
|
||
已落库脚本 `tools/font_gen/extract_cn.py`:扫描 `Drivers/BSP/UI/*.c/.h` 与 `App/task/ui_task.c/.h` 中**字符串字面量**里的非 ASCII 字符,跳过 `/* */`、`//` 注释与字符常量(避免把注释里的汉字也打进字体、徒增体积),输出 `<仓库根>/ui_cn_chars.txt`。脚本自动定位仓库根,可在任意目录运行。
|
||
|
||
**执行命令(在仓库根目录):**
|
||
|
||
```bash
|
||
python tools/font_gen/extract_cn.py
|
||
```
|
||
|
||
输出示例:
|
||
|
||
```
|
||
scanned 13 files; unique non-ASCII chars: 78
|
||
-> D:\Code\DTU Code\STM32F4-Base\ui_cn_chars.txt
|
||
```
|
||
|
||
产物 `ui_cn_chars.txt` 为本工程实际显示的汉字清单(当前 224 字,含 ▶/,/: 等符号;2026-10-29 录波 UI 道 +6 字(218→224);2026-10-28 UI 补齐批「记录 5 页 + 运行报告」+33 字,185→218——本行旧数 183 是未回填的历史值,改前实测 185;其中 60 字为 2026-10-25 按页面扩充规划显示串预置,同日道E 落 SOE 记录显示名映射时新用 采/样/恢/效 4 字;清单里源码暂未在用的字由 font-charset 门禁记 WARN 放行),可留作字符集记录。
|
||
|
||
---
|
||
|
||
## 4. 生成字体
|
||
|
||
已落库脚本 `tools/font_gen/make_font.py`:自动定位 `lv_font_conv`(全局安装或 PATH)、自动处理宋体 `.ttc` 的 face0 抽取,直接生成 `Drivers/BSP/UI/ui_font_cn_12.c`。
|
||
|
||
**前置依赖(仅需一次,统一装进仓库 `.venv`):**
|
||
|
||
```bash
|
||
npm i -g lv_font_conv # TTF/OTF -> LVGL 字体 离线转换器
|
||
.venv\Scripts\python.exe -m pip install fonttools # 仅当使用 .ttc 集合字体(如宋体)时需要;已含于 requirements.txt
|
||
```
|
||
|
||
> 若 `npm i -g lv_font_conv` 报代理错误,先清代理:`npm config delete proxy` / `npm config delete https-proxy`。
|
||
|
||
**执行命令(在仓库根目录,用 §2.1 的 `.venv` 解释器):**
|
||
|
||
```powershell
|
||
& '.venv\Scripts\python.exe' tools/font_gen/make_font.py # 默认:宋体, 12px, 1bpp
|
||
& '.venv\Scripts\python.exe' tools/font_gen/make_font.py --bpp 2 # 宋体细笔画发虚时改用 2bpp
|
||
& '.venv\Scripts\python.exe' tools/font_gen/make_font.py --font C:/path/to/font.ttf # 换其他字体
|
||
```
|
||
|
||
输出示例:
|
||
|
||
```
|
||
extracted SimSun regular (face0) -> C:\Users\...\Temp\simsun_regular.ttf
|
||
RUN: lv_font_conv.cmd --font ... --size 12 --bpp 1 -r 0x20-0x7E --symbols ... --no-compress --format lvgl --lv-font-name ui_font_cn_12 -o ...\ui_font_cn_12.c
|
||
已合并箭头 U+25B6:位图+10 字节,glyph_id=313
|
||
U+FF0C ',' 已可经既有 cmap 解析,跳过合并
|
||
OK 67325 bytes -> ...\Drivers\BSP\UI\ui_font_cn_12.c
|
||
```
|
||
|
||
> **▶ 箭头自动合并**:宋体(`simsun.ttc`)没有 U+25B6(▶)字形,`lv_font_conv` 会静默跳过 → 真机显示成方框。`make_font.py` 在生成后自动调用 `merge_arrow_glyph()`,把从 **Segoe UI Symbol**(`seguisym.ttf`,Windows 自带)预提取的箭头字形(位图 10 字节 + `glyph_dsc` + `cmap`)补回 `ui_font_cn_12.c`。该合并**幂等**(已存在则跳过),所以重生成字体后箭头不会丢。换用自带 ▶ 的字体(如黑体/Segoe)时可忽略此步。
|
||
|
||
> **低码位符号自动合并**:`lv_font_conv` 的稀疏 cmap 从符号集最小码位起算,**低于该起点的符号被静默丢弃**(实测 U+3001/U+FF0C 等:U+FF0C 全角逗号 < 0x4E2D 范围起点时丢失,页脚"第 X 页,共 Y 页"的逗号长期渲染为空白)。`make_font.py` 的 `merge_extra_symbols()` 对每个此类符号单独生成迷你字体(真实宋体位图/度量),再并入主字体(位图 + `glyph_dsc` + 单码位 `cmap`),同样**幂等**。新增此类符号时把它加入脚本的 `EXTRA_SYMBOLS` 列表即可。**两条 2026-10-25 规则**:合并前先用 `_cmap_lookup_gid()` 判可达(可达即跳过,不产生死条目);新 cmap 条目**插到第一个包含该码位的既有 range 之前**——LVGL `get_glyph_dsc_id()` 线性扫描、首个包含码位的 range 直接返回,追加在宽稀疏 range 之后的条目永远轮不到(见文末事故记录)。
|
||
|
||
> **生成后覆盖率断言**:`make_font.py` 在合并后调用 `verify_coverage()`——解析生成字体的 cmap,校验 `ui_cn_chars.txt` 的**全部码位**都进了字体,缺一个即以非零退出码报错(列出 `U+XXXX`)。把 lv_font_conv 的静默丢字(低码位、源字体缺字形)从"上线后才发现空白"变成"生成期硬错误"。判据自 2026-10-25 起按 **LVGL 首命中语义**解析(进了某个 range ≠ 能渲染:旧判据下 cmap 条目顺序倒置可全绿而上屏全方框,见文末事故记录)。
|
||
|
||
参数说明:
|
||
- `--bpp 1`:单色无抗锯齿;体积最小。**注意** lv_font_conv 在 1bpp 下斜线/细笔画偶发"丢像素",宋体尤甚。
|
||
- `--bpp 2/3`:字形带灰度,渲染到 1bpp 屏时按 `LV_DRAW_SW_I1_LUM_THRESHOLD` 阈值化,笔画更连贯;子集仅几十~上百字,体积仍很小。
|
||
- `--no-compress`:关闭内置压缩,规避 `LV_FONT_COMPRESSED` 未启用导致的兼容问题。
|
||
- `--lv-font-name ui_font_cn_12`:生成的 `lv_font_t` 变量名,须与下面 `LV_FONT_DECLARE` 一致。
|
||
|
||
---
|
||
|
||
## 5. 配置接入
|
||
|
||
### 5.1 声明与设为默认字体(`Lib/lv_conf.h`)
|
||
|
||
该文件当前为 **UTF-8(带 BOM)**。历史上它曾为 GBK,且工作区版本因某次整文件文本重写导致中文注释被 `U+FFFD` 不可逆损坏(`HEAD` 中的 GBK 版本仍干净)。
|
||
|
||
> **损坏恢复**:若二次损坏,不要用整文件文本重写。正确做法:
|
||
> `git show HEAD:Lib/lv_conf.h` 以 `gbk` 解码得到干净中文 → 叠加第 5.1 节的字体配置改动 → 重新写为 `utf-8-sig`(UTF-8 带 BOM)。这样中文可完整恢复,不会引入 `U+FFFD`。
|
||
|
||
```c
|
||
/* 自定义中文 1bpp 字体:由 lv_font_conv 生成(ASCII + 本工程所需汉字子集) */
|
||
#define LV_FONT_CUSTOM_DECLARE LV_FONT_DECLARE(ui_font_cn_12)
|
||
|
||
/* 默认字体:中文 1bpp 子集字体(已含 ASCII),菜单/标签均使用它 */
|
||
#define LV_FONT_DEFAULT &ui_font_cn_12
|
||
```
|
||
|
||
> 字体 `.c` 自身通过 `#ifndef UI_FONT_CN_12` 自包含,编译进工程即生效;无需额外 `#include`。
|
||
|
||
### 5.2 加入 Keil 工程
|
||
|
||
`MDK-ARM/STM32F407-Demo.uvprojx` 的 `Drivers/BSP/UI` 组追加(纯 ASCII 插入):
|
||
|
||
```xml
|
||
<File>
|
||
<FileName>ui_font_cn_12.c</FileName>
|
||
<FileType>1</FileType>
|
||
<FilePath>..\Drivers\BSP\UI\ui_font_cn_12.c</FilePath>
|
||
</File>
|
||
```
|
||
|
||
---
|
||
|
||
## 6. 编译验证
|
||
|
||
```bash
|
||
/keil-build # = keil-build skill(.dsh/skills/keil-build/scripts/keil_build.py),调用 UV4 全量重建
|
||
```
|
||
|
||
要求:`0 Error(s), 0 Warning(s)`。重新生成/修改字体或 `lv_conf.h` 后务必重建验证。当前已验证通过,`ui_font_cn_12.c` 正常编入。
|
||
|
||
---
|
||
|
||
## 7. 注意事项
|
||
|
||
1. **1bpp 细笔画发虚**:宋体在 1bpp 下可能丢像素。若上板/客户端中文偏淡,把第 4 节 `--bpp` 改为 `2` 重生成即可(子集小,体积可接受)。
|
||
2. **字符集变更**:以后新增中文菜单项,须重跑第 3、4 节(重新提取 `ui_cn_chars.txt` 并生成),否则新字显示为方框。
|
||
3. **编码**:UI 源文件须为 **UTF-8(带 BOM)**,LVGL 按 UTF-8 解码中文串;字体 `.c` 为纯 ASCII,无编码风险。当前 Keil 已加 Misc Controls 抑制项,无需额外 `--locale=english`。
|
||
4. **字体体积**:12px + 1bpp + 224 个汉字/符号(**不含 ASCII**——ASCII 0x20~0x7E 由 make_font 的 -r 参数一并打进子集,
|
||
故字形数 = 224 + 95 = **319**,`glyph_dsc` 数组实含 notdef 共 320 条;2026-10-29 录波 UI 道实测)源文件 ≈ 67.1KB(2026-10-29 实测 68,729 B;改前 185 字/280 字形/58.2KB,旧文本 183 字/278 字形系未回填);ROM 增量以 `.map` 为准(2026-10-25 扩 60 字实测 `RO-data` +1,528 B、RAM 0;同日 SOE 显示名再用 4 字实测 `RO-data` +108 B、RAM 0——1bpp 12px 一个字约 27 B,含字形描述符);改 bpp 2 约翻倍,仍在 F407 Flash 余量内。
|
||
5. **换字体**:黑体用 `simhei.ttf`(单文件免抽取);其余字体同理替换 `--font` 路径后重生成。
|
||
|
||
---
|
||
|
||
## 8. 文件清单
|
||
|
||
| 文件 | 说明 |
|
||
|------|------|
|
||
| `Drivers/BSP/UI/ui_font_cn_12.c` | 生成的中文 1bpp 字体(变量 `ui_font_cn_12`) |
|
||
| `Lib/lv_conf.h` | 字体声明与默认字体配置 |
|
||
| `MDK-ARM/STM32F407-Demo.uvprojx` | UI 组包含该字体 `.c` |
|
||
| `ui_cn_chars.txt`(工程根) | 实际显示汉字清单记录(可删) |
|
||
| `tools/font_gen/extract_cn.py` | 提取字符集脚本(见第 3 节) |
|
||
| `tools/font_gen/make_font.py` | 生成字体脚本,自动定位 lv_font_conv 并处理 `.ttc`(见第 4 节) |
|
||
|
||
|
||
---
|
||
|
||
## 重生成前的安全流程(2026-10-15 实测沉淀)
|
||
|
||
字体是**生成物**,但它进的是**像素基线 + 台架可见效果**。工具链一旦漂移,损失的是"保留字形静默变化"——而这种变化**编译和链接都不报错**。下面四步是必做项,不是可选:
|
||
|
||
1. **仓库外 dry-run**:先在临时目录搭一个最小生成器根(拷 `tools/font_gen` 两脚本 + 目标 `ui_cn_chars.txt`;`make_font` 的 ROOT 由自身位置推导,所以整目录拷走即可独立运行),在沙箱里跑完整生成链,确认 rc / coverage / 字节数 / BOM / LF / `EF BF BD` 全部正确,**才允许在仓库内跑正路**。
|
||
2. **确定性实证**:沙箱产物与仓库产物做 sha256 对比。一致即证明生成器确定性,之后任何 A/B 对比都可以用"快照 + 字节级还原",不必重新生成。
|
||
3. **逐码位结构对账(关键一步)**:解析新旧字体 `.c` 的 `glyph_bitmap`(按 `/* U+XXXX */` 注释切块)与 `glyph_dsc`,断言三件事——**删除集 == 预期字符集**、**其余码位位图字节零差异**、**度量多重集不变**。这一步把"golden 应该不变"**从期望升级为结构证明**,golden 跑出来只是复核。换工具版本(例如新装 `lv_font_conv`)时此项**必做**——它能照出"工具版本漂移导致保留字形悄悄变化",那才是真正会刷爆 golden 的事故源。
|
||
4. **A/B 必须同树**:并行开发时**千万不要拿别人给的"绿灯基线数"当 before**——那个数可能已经含了本次改动(实测照出过这种情况,见下)。正确做法:`git show HEAD:<字体路径>` 字节级换回旧产物 → 在**同一棵当前树**重跑 before → sha256 校验换回新产物跑 after。
|
||
|
||
> **口径:字体源文件字节数 ≠ ROM 降幅。** 源文件 ~45 KB 编到 `.constdata` 只有 ~4.5 KB,剔 3 个字形的真实降幅是**百字节量级**(实测 `ER_IROM1` −80 B、字体对象 RO −81 B,与"3 字 × ~25 B 位图 + 3 × 12 B 描述符、对齐后"预测吻合)。**报尺寸变化一律用 `.map` 的 `ER_IROM1` / 对象 RO**,不要把源文件字节数当成 Flash 增量。
|
||
|
||
> **保留字形(`KEEP_EXTRA`)的存在理由**:`ui_cn_chars.txt` 里除了"源码在用",还可能有**刻意保留**的字——当前为 异 / 常 / 片 / 芯,因为它们曾在 2026-09-14 `9eb1bd63` 被写进中文自检描述落盘 `/log/selftest.log`(定长 64 B 记录跨固件升级持久),删字形会使历史故障记录渲染空白且不可逆。`scripts/check_font_charset.py` 把这类字**按必需处理**:既照"缺字",也照"保留字被静默剔除"(严格模式下 FAIL 并逐字打印保留理由)。
|
||
> **所以"只跑 `extract_cn` 重新生成字符集"会把保留字洗掉**——这是本流程最容易犯的错,钩子就是为了挡住它。`--strict` 的判据因此是"**未知**冗余 = 0",而不是"冗余 = 0"。
|
||
|
||
> **2026-10-25 实测事故(扩字 Ⅰ/Ⅱ/Ⅲ,页面扩充预置字批)**:新字 Ⅰ(U+2160) 低于既有最小码位,稀疏 cmap range 起点由 0x4E09 下探到 0x2160,把宋体缺字补回的 U+25B6 单码位条目**吞进了宽 range**——旧 `verify_coverage()` 判"进了 range 即覆盖"全绿,模拟器/真机箭头却全变方框(菜单截图 658 px 差异,A/B 同树定位)。根因:LVGL `get_glyph_dsc_id()` 线性扫描 cmaps[],**首个包含码位的 range 直接返回**,其 sparse 列表缺该码位也不再向后找。修复:`_insert_cmap_entry()` 把补回条目插到包含它的 range 之前 + `_cmap_lookup_gid()` 可达性守卫(可达即跳过合并且不再追加死条目)+ `verify_coverage()` 改按首命中语义。**结论:低于最小码位的扩字 = cmap 结构变更,结构对账(第 3 步)照不出解析路径倒置,模拟器像素回归必跑。**
|