数据面(App/algo,纯逻辑,双端编译) - wave_ring:8 路 × 320 点列优先环(1 kHz,预触发 160 + 后触发 160),adcTask 唯一写者;触发/冻结/封存/冷却四态,冻结持续到落盘回执才解冻,滞留 2 s 自弃 兜底——下一次真故障无波可录比丢一段更贵 - wave_store:段头编解码 + 拉模式打包游标 + 自带无表位运算 CRC-16/MODBUS(带 锚点自检)+ 段名与整段长度等式;格式不依赖平台类型宽度 - 段头只存原始码值与封存瞬间生效系数,工程量只当单位标签、不进任何算式 - 三套主机单测(环 113 / 编解码 170 / 触发状态机 110),全部无桩、生产源原样 纳入;新文件补 stddef 是 ARMCC 与主机差异,主机侧照不出这条分叉 - test/host/wave_read.py 是按 §6.2 的独立第二实现(dump/fft/shape/plot),92 条 自洽断言;三份合成段互读双校验全过、哈希钉死,格式面双向闭合 - 三套新单测不加专属钩子,由覆盖率总闸全量真跑(沿用既有裁定) 接线(任务层) - adc_cal:新增生效系数的 32 位定点原值入口(与既有浮点入口共用生效视图), 段头按最低有效位精确记账,不经浮点回算 - adcTask:每拍压 8 路语义原始码值并推进状态机,封存那一拍同拍交棒封存瞬间事实; 台架定周期自抓走双层编译门,产品镜像里零存在;串口新增一条 wave: 周期行 - defaultTask:卷空闲后一次心跳整段搬完(try 锁 + 256 字节分片 + 一次完整开档到 关档),成败都回执;失败先删半成品再计错,段号只在成功时推进;持锁超 50 ms 补一条告警并记历史峰值(喂狗余量可核) - Lead 复核补第四道并发陷阱:采集任务优先级高于落盘道,搬段中途它可能正好把滞留 超时的本段自弃、甚至封出下一段——数据区校验和是对「实际读到的字节」算的, 半新半旧的段照样自洽,故关档后复检环侧身份判废:删文件、不补账、段号不推进 - Lead 复核抓到触发挂号不是原子操作(旧实现分三个字节写,消费者可在窗口里读到 上一段的触发源与元件号):已把载荷压进一个字节,越界当场拒且不记任何计数; 段文件字节布局零改动,三份合成段哈希逐字节不变 - protectTask:保护启动上升沿同点请求录波,一拍一次、元件号取最低位;判据本体 零改动 人机(160×160 单色屏) - 菜单树节点 26「手动录波」绑页面 + 记录查询组末新增「录波回放」叶,重跑生成器 得 43 节点 / 34 叶 / 30 已绑,页面映射 28 → 30 - 回放页一屏一路、触发点前后各一周波共 40 点、每点 3 像素阶梯线(只用水平与 垂直两种线段——单色屏斜线会被阈值裁掉);零点虚线、触发光标、窗口峰值、 单位徽标与形状受限徽标;空态与坏段各一句显式文案,绝不用半截波形冒充数据 - ui_data 新增录波注入通道(六件操作表、三态返回);执行侧胶水带段头缓存、 手动请求后连缓存一起作废、逐点走全仓唯一的码值换算入口 - 模拟器接真实环与编解码合成两段(含坏段与空态旗标),6 张新黄金参考,既有 60 张逐字节零漂移未用更新模式;字库 218 → 224 字 / 319 字形 - 两份图形配置里图表组件仍为关;零堆分配;页面池水位不变 - 回放读路径的 uiTask 阻塞上界按实码写清:一次渲染 = 段表刷新 + 未缓存段头最坏 22 次小读;已验段只剩一次开窗读 + 40 点解码;无实测数不编,实测归台架、 仪器用已在跑的单轮工作耗时探针,不新加埋点 台账与文档 - 内存台账:录波环与落盘/回放静态逐条入册,三档对照 81,536 → 88,296 → 89,936 字节(62.2% → 68.6%),累计 +8,400;预估 5.9 KB 与实测 +6,760 的差额点名; 散列文件头注释同步;任务栈表核对后无需抬栈;体积趋势行与同码凭证由权威构建 重打(0 错误 0 警告,四项体积与上一轮相同——本轮改动只有注释与声明位置) - 设计说明:§5 补第四条并发陷阱;§6.3 定字段口径(两份独立实现的共同依据); §7 回填实测;§9 周期行写实;§10 台架判据改两段式且不比绝对相位;§12 补读路径 风险行;抬头状态改成「代码面已交付、未做完的只有板级那一轮」 - 卡片:软件闭环一栏列证据;台架三条按用户裁定保持未完成,不做虚假销项 - 口径同步:数据可信、UI 补齐、页面补齐规划、产品需求、采集短板评测、任务规划 索引与两份 README;陈旧现状句清扫 16 处(带日期戳的历史取数行一律不动) - 门禁说明:nested-comment 记仓库根探测必须兼容 linked worktree 的 gitdir 文件 形态(修前该钩子在任务树里恒报环境错、静默不可用);coding-standard 记 「函数头注释超过 24 行就把函数功能挤出回看窗口」这条机制与本次现场 - 覆盖率基线登记两个新覆盖模块(套件 41 → 44 套);环单测重打后该文件有效行 227 → 226、覆盖行 226 → 225,比率仍 99.6%,是分母变小不是覆盖率回退 为什么是单一提交:文档链接、双端注册、生成器幂等(菜单与字库)、内存数字与 体积事实源、编号门禁这五道整仓门禁要求代码、页面、菜单树、字库、文档与台账 同处一个提交才自洽,按数据面/接线/人机/台账拆成四笔必然红其中至少四道 编号门禁新增行 14 处命中已按语义改写:魔数常量按字符命名、段名样例改由格式串 生成、频谱表头改 f / 2x / 5x 并加一行图例;改写后自洽断言 92 条仍全过
14 KiB
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 环境」):& '.venv\Scripts\python.exe' tools/font_gen/make_font.pyPATH 默认
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。脚本自动定位仓库根,可在任意目录运行。
执行命令(在仓库根目录):
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):
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 解释器):
& '.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 之前——LVGLget_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。
/* 自定义中文 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 插入):
<File>
<FileName>ui_font_cn_12.c</FileName>
<FileType>1</FileType>
<FilePath>..\Drivers\BSP\UI\ui_font_cn_12.c</FilePath>
</File>
6. 编译验证
@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. 注意事项
- 1bpp 细笔画发虚:宋体在 1bpp 下可能丢像素。若上板/客户端中文偏淡,把第 4 节
--bpp改为2重生成即可(子集小,体积可接受)。 - 字符集变更:以后新增中文菜单项,须重跑第 3、4 节(重新提取
ui_cn_chars.txt并生成),否则新字显示为方框。 - 编码:UI 源文件须为 UTF-8(带 BOM),LVGL 按 UTF-8 解码中文串;字体
.c为纯 ASCII,无编码风险。当前 Keil 已加 Misc Controls 抑制项,无需额外--locale=english。 - 字体体积: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 余量内。 - 换字体:黑体用
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 实测沉淀)
字体是生成物,但它进的是像素基线 + 台架可见效果。工具链一旦漂移,损失的是"保留字形静默变化"——而这种变化编译和链接都不报错。下面四步是必做项,不是可选:
- 仓库外 dry-run:先在临时目录搭一个最小生成器根(拷
tools/font_gen两脚本 + 目标ui_cn_chars.txt;make_font的 ROOT 由自身位置推导,所以整目录拷走即可独立运行),在沙箱里跑完整生成链,确认 rc / coverage / 字节数 / BOM / LF /EF BF BD全部正确,才允许在仓库内跑正路。 - 确定性实证:沙箱产物与仓库产物做 sha256 对比。一致即证明生成器确定性,之后任何 A/B 对比都可以用"快照 + 字节级还原",不必重新生成。
- 逐码位结构对账(关键一步):解析新旧字体
.c的glyph_bitmap(按/* U+XXXX */注释切块)与glyph_dsc,断言三件事——删除集 == 预期字符集、其余码位位图字节零差异、度量多重集不变。这一步把"golden 应该不变"从期望升级为结构证明,golden 跑出来只是复核。换工具版本(例如新装lv_font_conv)时此项必做——它能照出"工具版本漂移导致保留字形悄悄变化",那才是真正会刷爆 golden 的事故源。 - 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-149eb1bd63被写进中文自检描述落盘/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 同树定位)。根因:LVGLget_glyph_dsc_id()线性扫描 cmaps[],首个包含码位的 range 直接返回,其 sparse 列表缺该码位也不再向后找。修复:_insert_cmap_entry()把补回条目插到包含它的 range 之前 +_cmap_lookup_gid()可达性守卫(可达即跳过合并且不再追加死条目)+verify_coverage()改按首命中语义。结论:低于最小码位的扩字 = cmap 结构变更,结构对账(第 3 步)照不出解析路径倒置,模拟器像素回归必跑。