前几天写的都是故障复盘,这篇换个口味:收几个这周(其实跨了两三周)落地的小设计。每一个都是被具体问题逼出来的,但抽象一下都觉得"以后别的项目还能再用"。

1. 两级状态记忆:芯片权威 + 影子双写

问题:OTG 音源芯片的状态只能问它自己,一次查询是 7 条 UART 读;控制面板重启后要立刻恢复完整视图;更要命的是——高频访问会把芯片问卡死。

设计:

  • 芯片是唯一权威(source of truth);
  • 主机维护一份影子副本:所有写操作双写(发给芯片 + 更新影子);关键是读响应也顺带更新影子——每次查询本身就是一次同步机会;
  • 面板只读影子,永不直接打芯片;
  • 主机对芯片的主动查询全局节流(3 秒一次封顶)。

一句话:读路径也是同步路径,查询即备份。面板重启从影子秒恢复,芯片被问到卡死的风险也被节流封了顶。

2. 音量"拧定"协议:settle-final-value

问题:下位机命令处理串行且慢、接收 FIFO 小,旋钮飞速拧动时会打出命令风暴把它淹死;重启后音量归零还会触发"自愈"回写,来回打架。

设计三条:

  • 转动中 150ms 节流;
  • 拧停 400ms 后只发终值——中间值全部丢弃,不追过程只追结果;
  • 与影子偏差 ≤10% 触发自愈,10% 以内不值得一次写操作(另开 8 秒调音窗口,防止刚拧完被自愈顶回去)。

“发终值"这个思路适用于一切高抖动输入 + 慢执行器的组合:旋钮、滑条、拖拽进度条都算。

3. 幽灵按键抑制:时间戳要选对事件沿

问题:旋编码器往左/右拧,机械抖动会蹭出"幽灵 Enter”——两个键几乎同时按下,Enter 还先派发。抓日志发现幽灵对的特征是 duration 逐毫秒一致,明显是同一次物理晃动。

设计:两个按键按下沿时间差 ≤120ms 判为串扰,丢弃后到者。

踩坑:第一版用的是"释放-按下时差",上线从未命中——因为两键同时按下,释放沿差很大;真正稳定接近的是按下沿对按下沿。教训:抑制窗口的时间基准,必须选在两个事件都"新鲜"的那个沿上,不然窗口永远打不中。

4. 媒体字库与 UI 字库分离

问题:歌名显示方块(tofu)。根因是媒体文本误用了内嵌 UI 文案字库——那是个封闭集,只覆盖界面词条,歌名里的生僻字当然没有。

设计:两套字体两条加载路径——UI 文案字库(各界面独立维护,封闭集,小)与媒体字库(按销售区域全字符集生成,挂在 mediaMetadata 资源下)。规矩:新增任何媒体标签必须挂 mediaMetadata,否则中文回到方块。

原则:封闭集字库的小,正是靠"不含生人"换来的——封闭集只许服务封闭集文本。

5. 无网络时的串口部署通道

问题:台架阶段的产品没有网络,唯一物理入口是调试串口。要把新二进制部署上去。

设计:把可靠部署做成纯 shell 能完成的通道:

  • 二进制 → hex → busybox printf 八进制转义写文件(\x 在 busybox 里不可靠,八进制稳);
  • 单行行长上限 1200——printk 风暴会挤爆串口 TX 丢字符,行长纪律是命;
  • 写完 cp + mv 到目标路径替换——直接写运行中的二进制会 ETXTBSY;
  • 每段末尾用算术展开回显一个标记,当简易校验。

串口不是最快的通道,但它是永远在场的通道——网络、USB、SD 卡升级都可能不可用,调试串口不会。

尾声

这几个设计有个共同点:都不复杂,甚至可以说都"显而易见"——但每一个背后都先躺着一个真实故障。设计的原材料不是聪明,是伤疤。