Files
STM32F4-Base/docs/UI设计/UI_中文字体生成与配置.md
wandering1 4a4ad6bc5f chore(全仓): @build/@test 旧文档别名清退为技能调用名
现状口径:/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 全过;字节卫生零漂移。
2026-10-30 13:03:12 +08:00

190 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 步)照不出解析路径倒置,模拟器像素回归必跑。**