文件
phonerail/需求文档.md
zhou yang和Claude Opus 5.5 ece65f7c19 固件 0.1.19~0.1.21-c5:配网热点(SoftAP + 网页)、风扇 40 ℃ 以下断电
- 0.1.19 配网热点:没配 WiFi / 连续 60 s 连不上时自己开 SoftAP,192.168.4.1 网页里加删 WiFi、改 MQTT;
  配置拼成 wifi / mqtt 命令走唯一的命令处理器(新链路号 3 CMD_LINK_AP),MQTT 仍不收配置命令
- 0.1.20 热点口令固定 88888888
- 0.1.21 风扇:板温 >=40 ℃ 上电、<38 ℃ 断电,40~65 ℃ 线性调速;fan 命令加可选 sec(下限到点自动交回)
- mqtt 命令加 keep_pass;net 回应加 ap;status 的 fan 加 left
- 新增 test_prov(13 例),test_t_thermal +3;29 ctest 全绿

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-06 14:35:44 +08:00

86 KiB

五路手机直供电源板 —— 项目需求文档

项 值
文档版本 v1.2(2026-09-06)—— 上位机链路定稿为方案 C(WiFi 常态 + USB CDC 兜底),回填 §10.8,新增 D5a / SW-12 / IF-19 与两条验收
v1.1 —— 新增 §10 软件架构设计,原 §10~§14 顺延为 §11~§15
项目代号 PhoneRail
对应设计文档 五路直供电源板设计.md v1.6 及其后续增补(§22 起的两板架构)
状态 需求已冻结;硬件处于 PCB 布线阶段(见 PROGRESS.md)

本文档与设计文档的分工 本文只回答 「要做成什么样、凭什么算做到了」; 「怎么做到」 一律在 五路直供电源板设计.md 里,本文只给条目号交叉引用。 两者冲突时:电气数值以设计文档为准(它是逐项算过并对过 datasheet 的), 需求的取舍与验收标准以本文为准。 设计文档 §0.1 的 13 条决策已定稿,本文把它们表述成需求,不重新讨论。 唯一的例外是 §10 软件架构设计 —— 那是「怎么做」,但固件在设计文档里没有落脚点, 暂放本文,理由写在该节开头。


1. 背景与目标

1.1 要解决的问题

手机农场里成排运行的小米8(dipper)主板,用原装电池供电有三个绕不开的问题:

  1. 电池会老化、会鼓包,几十台规模下变成持续的维护负担和安全隐患;
  2. 手机卡死时没法远程恢复 —— 人必须跑到机架前拔电池 / 长按电源键;
  3. 充电管理系统一直在工作,长期 100% 电量反而触发系统的异常逻辑。

1.2 项目目标

做一块电池位直供的电源板:拆掉电芯与保护板,从主板电池座直接灌一个受控的、 稳定在正常锂电范围内的电压;同时把「断电重启、按键模拟、进 Fastboot / 9008、 USB 通断」这些原本要人手做的动作全部电气化,交给板上 MCU 远程执行。

一句话验收目标:五台小米8 主板挂在这套板子上连续跑 24 小时无异常重启, 任何一台卡死时都能在不到机架前的情况下恢复到正常运行状态。

1.3 目标用户与使用场景

用户 场景
农场运维 日常挂机;某台无响应时下发一次重启;查某台的电流曲线定位接触不良
刷机 / 开发 远程进 Fastboot 或 9008(EDL) 刷包;有线 adb 抓日志
本项目自身 首板调试、长稳测试、失效注入验证

2. 范围

2.1 交付范围(In Scope)

# 交付物
D1 下板(电源板):12V 输入 → 五路 4 A 直供 + eFuse + 遥测 + ESP32-S3 控制
D2 上板(载板):五块主板的机械承载 + 板载 USB Hub + VBUS 开关 + 侧键顶针 + EDL 口
D3 两板对插的板对板接口(两条 2×20 排针,共 80 针)
D4 手机侧线束与离板电容组(电池排线改造、尾插 USB 线、EDL 引线)
D5 ESP32-S3 固件(遥测、两级重启、按键 / EDL 模拟、VBUS 通断、风扇闭环、告警)
D5a 双链路命令接口(2026-09-06 用户拍板):WiFi 为常态、USB CDC 为兜底,两条链路共用同一套命令语义。本项目交付板侧协议与实现,见 §10.8;上位机见 D7
D7 机架管理前后端(2026-09-27 用户拍板,取代原 N7):架构 A 纯中心化 —— home 服务器(10.5.5.13)上跑 mosquitto + Python(FastAPI)后端 + 时序库 + Vue3 前端,走统一门户;板子经 WiFi MQTT 直连。首版功能:实时总览、手动操作、自动崩溃恢复(服务器侧 adb over WiFi 探活 + 电流曲线确认,先软后硬)、历史曲线与事件。不做机架旁 agent,USB CDC 只用于上架配网与调试(代价:WiFi 故障时没有远程救援通道,见 §10.8)。开发顺序:先补固件 A12/A13/A14,再写前后端。2026-09-27 已上线:代码 rack/(说明见 rack/README.md),服务 phonerail-rack(127.0.0.1:7764),门户 key rack
D6 完整原理图(11 页)+ 两块 PCB + BOM + 首板调试与验收记录

2.2 明确不做(Out of Scope)

# 不做 理由
N1 电池充放电管理、模拟真实电池阻抗 直供不需要,电芯已移除
N2 支持 1S 以外的机型 / 非小米8 主板 全部机械与引脚定义都是 dipper 专用(§13)
N3 自制 USB-C 尾插转接板 决策 10:装原厂尾插小板,口温 NTC / CC / ESD / 天线馈点全在上面
N4 USB PD / BC1.2 协商,接快充充电器 决策 11:上游只接电脑 / Hub,VBUS 恒 5 V
N5 把过流保护做进 MCU 实时环路 那是 eFuse 的职责(SR-3)
N6 五路 Buck 错相同步 §8.4 已否决:器件离散度远大于想制造的相位差
N7 上位机管理软件 / Web 界面 2026-09-27 撤销,改为交付范围 D7

3. 系统组成(需求视角)

12V/10A 适配器
      │
 ┌────┴─────────────── 下板(电源 + 大脑)────────────────────┐
 │ 输入保护 → 五路独立 Buck → eFuse → 分流检测 → 输出          │
 │ ESP32-S3 + DAC ×2 + INA226 ×6 + 74HC595 ×3 + PCA9555        │
 │ 侧键驱动管 ×15 · 风扇驱动 · 辅助 3V3 / 5V                   │
 └────┬──────────── 2×20 排针 ×2(无高速信号)─────────────────┘
 ┌────┴─────────────── 上板(载板)────────────────────────────┐
 │ 5 × 小米8 主板(NPTH 定位孔 + 尼龙立柱)                     │
 │ USB2517I 7 口 Hub + 上游 USB-C + VBUS 负载开关 ×5            │
 │ 侧键顶针 ×15 · EDL 模拟开关 ×5 · P+/P− 焊盘 + 电容组 ×5      │
 └──────────────────────────────────────────────────────────────┘

两板之间保留 13 mm 缝,既是装配空间也是散热风道。


4. 功能需求(FR)

ID 需求 优先级 设计出处
FR-1 同时为 5 台小米8 主板供电,各路电气独立(独立回流地,不在手机端共地) 必须 §1 / §6.4
FR-2 每路输出电压可独立微调,调节在正常锂电范围内进行,步进优于 1 mV 必须 §4
FR-3 每路可独立硬断电并主动放电,实现真正意义上的「拔电池」 必须 §7
FR-4 每台手机可模拟 3 个侧键(PWR / VOL+ / VOL−),支持组合键 必须 §13.3
FR-5 每台手机可远程进入 EDL(9008) 模式 必须 §22.4
FR-6 每台手机的 USB VBUS 可独立通断,默认断开 必须 §15.1
FR-7 对外只用一根 USB 线,五路下行由板载 Hub 分 必须 §15.1
FR-8 每路可实时读取输出电压与电流;整板可读取输入电压与电流 必须 §8.2
FR-9 上报每路 eFuse 故障、电源好(PG)等状态 必须 §8.2
FR-10 主动风冷,风扇转速可调、可检测「有风扇停转」。固件 0.1.21(2026-10-06 用户定):板温(两路 NTC 取高)低于 40 ℃ 风扇断电(≥40 ℃ 开、<38 ℃ 关),40→65 ℃ 按 30 %→100 % 线性调速;温度读不到或上位机给了下限时一律开着。fan 命令的下限只抬不压,可带 sec 到点自动交回(rack 网页与 AppTask「机架」页都能发) 必须 §22.6
FR-11 记录过流 / 故障事件(哪一路、何时、多大电流) 高 §8.3
FR-12 模拟电量曲线,避免系统因「电量永远 100%」触发异常逻辑 中(可选) §8.3
FR-13 板级温度监控,超阈值先降流再关路 高 §8.3
FR-14 整板功率预算,超过适配器能力时按优先级降载 中 §8.3
FR-15 调试口不依赖外部转串口芯片,插上 USB-C 即可下载与看日志 必须 §8.1

4.1 崩溃恢复流程(FR-3 / FR-4 的组合,本项目的核心价值)

固件必须实现两级重启,且先软后硬:

级 动作 触发条件 要求
一级(软) 长按电源键 ≈10 s adb 探活连续失败 N 次,或电流曲线卡死 优先使用,对闪存和文件系统友好
二级(硬) EFUSE_EN ↓ → 放电 → 等 3~5 s → EFUSE_EN ↑ 一级无效 输出必须真的掉到 <0.5 V,不能靠自然放电

为什么二级必须有主动放电:手机侧 2200 µF 配 ≈50 µA 关机待机电流,自然放电到安全 电压要 154 秒,「断电 10 秒」根本不触发重启。见 §7.1。


5. 电气与性能需求(PR)

ID 参数 要求 出处
PR-1 输入电压 12 V ±10%,成品适配器 §1
PR-2 输入电流 ≤ 8 A(含风扇 ≈8.3 A),建议配 12 V / 10 A(120 W) §1 / §22.6
PR-3 输出路数 5,彼此独立不共地 §1
PR-4 输出电压范围 3.52 ~ 4.01 V 连续可调 §4.3
PR-5 硬件电压上限 标称 4.01 V,最坏容差 4.12 V,软件无法越过 §4.4
PR-6 电压设定分辨率 12 位 DAC 微调,步进 ≈0.12 mV §4.5
PR-7 输出电流 4 A / 路(峰值);手机常态 0.3 ~ 1 A §1
PR-8 输出纹波 < 20 mVpp(板端测试点) §1
PR-9 开关频率 480 kHz,五路统一,不错相 §1 / §8.4
PR-10 整板功率 峰值 80 W(不会发生);常态 10 ~ 25 W §1
PR-11 电流测量分辨率 ≥ 0.5 mA(5 mΩ 分流 + Kelvin 四线) §8.2
PR-12 负载阶跃响应 0→4 A 阶跃(带 20 cm 线 + 电容组)压降 < 300 mV,无 6 kHz 振铃 §6.2
PR-13 满载温升 4 A 连续 30 min,电感 / IC 温升 < 40 K §11
PR-14 断电放电时间 EFUSE_EN ↓ 后 200 ms 内输出 < 0.5 V §7.2
PR-15 上电斜坡 缓启动 0.36 V/ms,涌流 ≤ 0.8 A;EN↑ 到稳定 ≈17 ms §5

5.1 输出上限为什么定在 4.01 V(这条需求的来源)

去掉原厂保护板后,输出上限由 1S 锂电端子的绝对上限 4.45 V 反推,而不是由保护板决定。 4.01 V 这个数字不是「够用就行」,是为了给 eFuse 的过压跳闸窗口让开 86 mV —— 若按早期的 4.22 V,第一层的最坏输出(4.34 V)会和 eFuse 的最坏跳闸下沿(4.206 V)重叠, 坏角落下 DAC 失效会误跳保护。详见 §5 的容差窗口分析。

因此 PR-4 / PR-5 是安全需求的一部分,不得为了「多给一点电压」而放宽。


6. 安全需求(SR)—— 本项目最高优先级

必须记录的失效模式:直供电最致命的失效不是固件写错电压值,而是 反馈分压的下电阻开路 → FB = 0 V → Buck 满占空比 → 输出直接跳到 12 V。 软件对此完全无能为力。 全部安全需求都要围绕这一条来读。

ID 需求 出处
SR-1 三层保护彼此独立:任何一层单独失效都不得导致手机损坏 §5
SR-2 第一层 · 拓扑限幅:分压网络与 DAC 量程在物理上把输出限死在 3.42 ~ 4.12 V(含最坏容差),全部落在正常锂电电压内。防固件 bug / I²C 误码 / MCU 跑飞 §4
SR-3 第二层 · eFuse 独立过压断开(每路一颗,最后一道硬件防线):OVP 4.36 V 标称、断开而非钳位、响应 2 µs;限流 4.45 A 恒流、不依赖 MCU §5
SR-4 eFuse 的过压响应必须来得及:按最坏值(Buck 高侧限流 14 A、负载 2200 µF)核算,余量 ≥ 10 倍 §5
SR-5 第三层 · 软件监控:电压高于设定 >100 mV 或低于设定 >250 mV(欠压一侧 2026-09-28 放宽:小米8 带屏开机峰值 2.1 A 时实测 −115 mV),且持续 20 ms → 拉低该路 EN 并上报。仅承担记录与告警,不承担实时保护 §5
SR-6 复位安全:ESP32 非计划复位(上电、崩溃、看门狗、按 EN、esptool)期间,五路输出全关、五路 VBUS 全断、15 路按键全不按、5 路 EDL 全不触发、风扇不转。靠上下拉电阻保证,不靠固件。
例外(2026-09-27 用户定):固件自己发起的计划内重启(OTA 换固件、自证超时回滚)锁住 EFUSE_EN、新固件接管,手机不断电;按键 / EDL 仍全松,VBUS 保持。见 §10.9「重启不断电」
§8.1 / §22.4
SR-7 放电 MOS 与 eFuse EN 硬件反相联锁,物理上不可能同时导通 §7.3
SR-8 输入保护:保险丝 + TVS + P-MOS 反接保护 §2
SR-9 12 V 母线泄放:母线 >13.2 V 时导通泄放,防 USB 反灌或突卸能量抬高母线 §15.2
SR-10 USB 反灌三条对策全做,一条都不能省:① 软件关充电 ② VBUS 负载开关 ③ 母线泄放。唯一的有意例外:刷机模式(固件 0.1.5,2026-09-28 用户定)——按路、带时限(默认 60 分钟,最长 120)、最多同时 2 路,窗口内允许手机充电、「顶高断 VBUS」与过压改按绝对值 4.45 V 判,反灌与欠压判据不变;依据是每路 eFuse(TPS25940A)自带反向阻断,充电器顶高节点时能量进不了 Buck / 12V 母线(板 B 五次顶高实测电流 0 ~ −0.5 mA) 决策 12 / §13.5 / firmware chgmode.h
SR-11 上游 USB 不做 PD 协商(CC 双 5.1 kΩ 下拉,纯 UFP),杜绝 VBUS 被抬到 9 / 12 V §15.1
SR-12 五台手机的地不得在载板上并成一根:定位孔做 NPTH,立柱用尼龙 §25.2
SR-13 固件不得提供任何能让输出超过 4.2 V 的接口 §8.3
SR-14 任一路输出不得给 ESP32 供电(用独立的 12 V→3.3 V 小 Buck) §8.3
SR-15 电源必须接原厂排线的 P+/P−(保护板输出侧),不是 B+/B− 决策 6 / §13.5

⚠️ SR-15 是最容易在装配环节做错、且常温下完全测不出来的一条。 焊在 B+/B− 时一切表现正常,只有过压时才暴露 —— 那时保护板仍串在通路里。 判别与验证步骤见 §13.5 / §13.7,已列为打样前阻塞项(V-1)。

⚠️ J600 的 GND3/GND4 固定脚实际接的是 VBATT,J1401 的 GND2 是 USB_VBUS。 按名字当地用会直接短路电池。


7. 接口需求(IF)

编号只增不改:新增接口一律取下一个空号,位置按逻辑归类 —— 所以表内序号不一定连续(如 IF-19 排在 IF-14 后面)。ID 是稳定的引用标识, 不是目录序号;改动已有编号会让所有交叉引用与验收记录失效。

7.1 手机侧接口(每台一套)

ID 接口 要求 出处
IF-1 供电 原装 BM3E 电池排线的 P+/P− 焊点;保留 BTB 公座 / NTC / ID,旁路保护板;电芯移除 §13.2 / §13.5
IF-2 侧键 J601(VOL+) / J602(VOL−) / J603(PWR) / J604(公共地),对地短接方式,绝不从外部灌电压 §13.3
IF-3 EDL 短接主板上的 TP1204 ↔ TP1205(1.8 V / 1 mA),用模拟开关,不用普通 N-MOS §22.4
IF-4 USB 装原厂尾插小板,本板只接 VBUS / D± / GND 四根 决策 10 / §15.1
IF-5 离板电容组 每路 2200 µF 普通铝电解(靠 ESR 阻尼)+ 100 µF ×2 + 22 µF ×2 + 100 nF,必须装在排线端,不在电源板上 §6.3
IF-6 线缆 ≤ 20 cm、18 AWG 或更粗、正负双绞或紧贴平行;板端用 Micro-Fit 3.0 / XT30,不用杜邦线 §6.4

⚠️ IF-5 的 2200 µF 千万不能买成聚合物电容。 整个阻尼设计靠的就是普通铝电解那 几十 mΩ 的 ESR;换成 12 mΩ 的聚合物,Q 从 0.19 跳回 4.5,线缆 LC 会在 6.2 kHz 上振铃。

⚠️ IF-5 的大电容必须在排线端。 放在电源板上等于隔着 300 nH 的线缆电感,基本无效。

7.2 板对板接口

ID 需求 出处
IF-7 两条 2×20 排针(共 80 针),分列板子两端 §22.2 / §25.10
IF-8 一根高速差分都不穿连接器 —— Hub、上游 USB-C、VBUS 开关全部上移到载板 §22.1
IF-9 排针只走电源与低速控制;每路 P+/P− 独立分配针位,保证独立回流(FR-1) §22.2
IF-10 两板间净空 13 mm;载板底面元件与下板高元件的高度预算必须逐点核过 §26.2

7.3 对外接口

ID 接口 要求
IF-11 电源输入 DC 12 V 插座(XT30 级),配 10 A 适配器
IF-12 上游 USB 载板 USB-C(UFP),一根线接电脑
IF-13 调试 USB 下板 USB-C,ESP32-S3 原生 USB-Serial-JTAG;VBUS 故意不接(物理上杜绝反灌 3V3)。同一个口兼作 SW-12 的兜底命令链路(原生 USB-Serial-JTAG 本身就是 CDC-ACM,不影响 esptool 下载),分帧要求见 §10.8
IF-14 风扇 5 × KF2510-3A(+12 V / 回流 / TACH)
IF-19 控制网络 WiFi(ESP32-S3 板载,无需增加器件),SW-12 的常态命令链路。凭据与服务端地址存 NVS,不编译进固件 —— 新板上架时用配网热点(固件 0.1.19,2026-10-06:没配网 / 连续 60 s 连不上 WiFi 时板子自己开 SoftAP PhoneRail-<id 末 6 位>,手机连上在 192.168.4.1 网页里填)或 IF-13 的 USB 链路配网

⚠️ IF-12 与 IF-13 是两个长得一样的 USB-C,插错的现象是「adb 整排不见」, 很难联想到是插错口。丝印必须写清楚,建议用不同颜色座子或在外壳上做标记。

7.4 内部数字接口

ID 需求
IF-15 I²C 单条总线挂 10 个器件:INA226 ×6(0x40/41/44/45/42/43)、DAC63204 ×2(0x4A / 0x4B)、PCA9555(0x20)、LM75BD(0x48),地址互不冲突
IF-16 ⚠️ INA226 的通道号 ≠ 地址低位(为布线让路重排过),固件必须查表,不能算
IF-17 74HC595 ×3 级联成 24 位移位链:15 路侧键 + HUB_RST + 5 路 EDL_EN;先移入的字节落在 U33 上
IF-18 EFUSE_EN ×5 与 VBUS_EN ×5 必须 GPIO 直连,不得经移位寄存器 —— 安全关键,故障瞬间要能立刻关断

8. 机械与热需求(MR / TR)

ID 需求 出处
MR-1 载板 220 × 135 mm,四角 R5;五块主板 3+2 双排面对面,第二排居中 §25.1
MR-2 下板 160 × 125 mm,装配时相对载板原点偏移 (30, 5) §25.2
MR-3 每台手机 7 个定位孔,全部 NPTH Φ2.5 + 尼龙立柱(见 SR-12) §25.2
MR-4 侧键用顶针接触,顶针焊盘必须避免把相邻触点短路 §25.3
MR-5 排线穿槽:电池排线 8×38 → 槽 10×3.0;尾插排线 18×55 → 槽 20×3.0(在 J1401 正下方) §25.16
TR-1 总热负荷 ≈25 W(5×SDM845 满载 10~15 W + 下板功率级 ≈9 W) §22.6
TR-2 强制风冷 4 进 1 出 = 机箱正压(灰尘往外走);两板间 13 mm 缝作天然风道 §22.6
TR-3 所需风量仅 ≈2.9 CFM,风量不是瓶颈,风道与正压才是设计要点 §22.6
TR-4 风扇 5 × 12 V ≈0.70 A;低边 N-MOS + PWM,漏极必须并续流二极管 §22.6
TR-5 风扇闭环的测温点放载板(测的是手机),不能用下板功率级的两路 NTC 做反馈 §22.6
TR-6 TACH 必须分压后再进 GPIO;⚠️ 低边调速下 PWM 关断期间读不到转速,固件需在导通窗口采样 §22.6

9. 固件需求(SW)

本节只列「固件要做到什么」。怎么组织成一份固件见 §10 软件架构设计。

9.1 必做

ID 需求
SW-1 崩溃检测 + 两级自动重启(§4.1)—— 整块板子最核心的价值
SW-2 20 ms 周期巡检六路 INA226,电压偏离 >+100 / −250 mV 持续 20 ms 即关路告警(SR-5)
SW-3 过流 / 故障事件记录:路号、时刻、电流值 —— 排查排线接触不良的唯一线索
SW-4 单路电压微调接口(跑不稳的手机提到 4.0 V,或反向降压降温),上限受 SR-13 约束
SW-5 侧键序列:短按 100 ms 唤醒 / 长按 5 s 开机(小米8 实测 2 s 不够,2026-09-28;3 s 也不够,2026-09-30)/ 长按 10 s 强制重启 / VOL−+PWR 进 Fastboot / VOL++PWR 进 Recovery。硬重启末尾的开机键按完看电流:松键后 3~5 s 内 < 100 mA 就重按,最多 3 次,仍不开记事件 19「开机重按」并保持供电(固件 0.1.2,起因是电源键弹片接触不良)
SW-6 EDL 时序固定为 EDL_EN=1 → VBUS_EN=1 → 等枚举 → EDL_EN=0(先短接再插 USB)
SW-7 开机脚本执行「关充电」(写 input_suspend 类节点),路径需在首台机上确认后写死
SW-8 USB 反灌保护:检测到该路电流转负或母线上升,200 ms 内断开该路 VBUS。刷机模式窗口内「顶高」按绝对值 4.45 V 判(SR-10 的例外),电流转负照断
SW-9 风扇 PWM 闭环(以载板 LM75BD 为反馈)+ TACH 停转告警
SW-10 整板功率预算:读输入侧 INA226,超适配器能力时按优先级降载
SW-11 上电默认全关:五路输出关、VBUS 断、按键不按、EDL 不触发
SW-12 双链路命令接口:WiFi 为常态、USB CDC 为兜底,两条同时在线、共用同一套命令语义与同一个命令处理器;命令带全局序号做幂等判重。链路失联不触发任何动作。架构见 §10.8

9.2 明确禁止

ID 禁止事项 理由
SW-N1 让 ESP32 参与过流保护的实时环路 那是 eFuse 的活,MCU 不够快也不够可靠
SW-N2 提供任何能让输出超过 4.2 V 的接口 SR-13
SW-N3 烧写 DIS_USB_JTAG eFuse 烧了之后引脚 JTAG 和 USB 两边都进不去,板上没有 JTAG 排针
SW-N4 把 INA226 通道号当地址算 IF-16

10. 软件架构设计

这一节为什么在需求文档里:本文原则上只讲「做成什么样」,但固件在设计文档里没有 落脚点 —— 设计文档 §8.3 只有一张按价值排序的功能清单,没有结构。在补出一份独立的 软件设计文档之前,架构先放这里,与 §9 的固件需求贴在一起看。 硬件的「怎么做」仍然一律在设计文档。

10.1 分层与职责边界

┌─ 农场管理系统(既有,本项目不交付 —— N7)────────────────────────┐
│   adb 探活 / 业务调度 / 崩溃判定                                  │
└───────┬────────────────────────────────┬──────────────────────────┘
        │ adb over WiFi(业务面)         │ 控制面(10.8,⚠️ 链路待定)
        ▼                                ▼
   5 台小米8 主板                  ┌─────────────────────┐
        ▲                          │  PhoneRail 控制器    │
        │ 供电 / 侧键 / EDL / VBUS  │  ESP32-S3 固件       │
        └──────────────────────────┤  遥测 · 执行 · 记录  │
                                   └──────────┬──────────┘
                                              │ 只能建议,不能否决
                              ┌───────────────▼───────────────┐
                              │ 硬实时层:eFuse OVP / 限流      │
                              │ 放电 MOS 反相联锁 / 上下拉      │
                              │ —— 不含任何软件(SR-3、SR-7)   │
                              └───────────────────────────────┘

一条边界决定了下面所有取舍:固件是遥测 + 执行器 + 记录仪,不是保护装置 (SR-5、SW-N1)。凡是「必须及时」的事都已经在硬件里了;固件里没有任何一条路径 是「不跑到就会烧手机」的。因此:

  • 任务优先级不需要为保护让路,只需要保证遥测不被执行器序列饿死;
  • 看门狗复位是可接受的失效(复位即全关,SR-6),不需要为了「保持输出」而弱化看门狗; (计划内重启不断电是另一回事:那是固件自己选的时机、自己留的标记,§10.9「重启不断电」。)
  • 固件可以随时被重启、被降级运行,硬件安全性不变。

10.2 架构的第一个判断:失联 ≠ 断电

状态 输出该怎么办 为什么
ESP32 复位 / 上电中 全关(硬件下拉保证,SR-6) 状态未知,且此刻手机本来就没在跑
ESP32 计划内重启(OTA / 回滚) 保持(EN 锁住,新固件接管) 重启前的状态是已知的;为换一次固件让五台手机全掉电不值得
与上位机失联 保持供电不动 五台手机正在跑业务;因为控制链路断了就把它们全断电,是把一次网络故障放大成一次全场停机
本地判据触发(SR-5 / SW-8 / 过温) 关对应那一路 已知的危险,且范围可控

⚠️ 这三行看起来像常识,但它们互相矛盾过一次:「安全默认值 = 关」只对未知状态成立。 失联是已知状态,此时最安全的动作是「什么都不做」。固件里不要写「心跳超时就关输出」。

推论:失联期间固件必须能独立运行全部本地职责 —— 巡检、风扇闭环、事件记录、 按队列执行未完成的序列。上位机回来之后靠事件日志补齐时间线(10.7)。

A10 已落地(2026-09-07,firmware/main/t_safety.{h,c},判据全在 t_safety_step() 里、放锁之后、直接写引脚)。本表第三行「本地判据触发 → 关对应 那一路」的两条判据是 SR-5 与 SW-8,实现时定下来的七条:

  1. 「持续 20 ms」数的是圈,不是墙钟毫秒。 巡检的采样时刻带亚毫秒残差 (一圈自己占 1.56 ms 总线,而「睡到下一拍」按整毫秒算),实测相邻两次判据的 时刻是 21 / 19 ms 交替。按 >= 20 ms 判,19 ms 那半数情况要拖到第三圈 ——40 ms,超了 §12.1 的验收,而且是间歇性的:同一个故障注两次,一次过 一次不过。改成「连着两圈都越界」,在 20 ms 的周期下就是 20 ms。 (SR-5 说「持续 20 ms 才算」、§12.1 说「20 ms 内关路」,字面互相顶着, 只有「第一次看见的下一圈就断」同时满足两条。这 20 ms 是从第一次看见算的, 对外部世界的最坏延迟是 2 个周期 —— 不要紧,SR-5 原文就写着它 「仅承担记录与告警,不承担实时保护」。)
  2. 反灌不设确认窗口,一圈看见就断(设计文档 §15.2 对策 2 原话「立即」)。 与过压那 20 ms 的不对称是有意的:误断 VBUS 只丢一次 USB 数据,手机还由直供 电着;漏判则把能量泵回 12V 母线连累另外四台。最坏 2 圈 = 40 ms, SW-8 给的预算是 200 ms。
  3. 判据只在「这一路该有电」且「这一圈的值是新鲜的」时武装,另加一个 EFUSE_EN↑ 之后 60 ms 的缓启动窗口(tON 5.1 + 斜坡 11.5 ≈ 17 ms 的 3 倍)。 少了第一条,每圈都会「发现」关着的五路各偏低 3900 mV;少了第三条, 每次上电都会自己把自己关掉;第二条就是 §10.10 那行「盲跑 → 保持供电, 禁用该路的 SR-5 / SW-8 判据」。
  4. 比的是该路 DAC 的设定值(rail_setpoint_mv() 的影子,不碰总线), 不是 3900 这个常数 —— SW-4 允许运维单路微调,拿常数比会把一台调到 4.0 V 的 机器当成过压关掉。
  5. SR-5 关路 = 先断 VBUS 再关 eFuse(rail_all_off() 的同一条理由:中间不留 「手机只靠 USB 供着电」那个既不是开也不是关的窗口);SW-8 只断 VBUS, 不碰 eFuse —— 反灌是 USB 那一侧的事,把直供也关掉等于「因为充电器有问题 就把手机关机」。过压且 VBUS 通着时两条都成立,于是先撤能量来源, 20 ms 后再决定要不要关这一路。
  6. 断电之前先 t_actuator_abort()。 不 abort 的现象最阴:硬重启的 HR_OFF 会在 4 s 后自己把 EFUSE_EN 抬回去,「关掉的那一路」几秒后又带电了, 而日志里只有一条「已关路」。断 VBUS 时只打断 EDL(三个序列里只有它写 VBUS_EN)—— 无条件打断会把一次正在进行的救砖重启也顺手停掉。
  7. 动作在前,记录在后,事件出口是一个回调(t_safety_set_event_sink()), A11 在那里接环形缓冲。记录用的 safety_event_t 已经按 §10.7 的「单条」 形状定好(时间戳 + 路号 + 类型 + 当时的 V/I,16 B)。

29 条 ctest 用例,25 条「一定会被写出来的错」逐个注进去跑过,24 条变红; 剩下那一条(关路之后把 dev_n 清零)在 sim 里造不出「写引脚失败」,抓不到 —— 所以那一行干脆没写,理由记在代码里。

10.3 任务划分(FreeRTOS / ESP-IDF)

任务 周期 优先级 职责 失败后果
t_safety 20 ms 最高 读 6 片 INA226(V+I)与 PCA9555 输入;执行 SW-2 / SW-8 判据;越界直接操作 EFUSE_EN / VBUS_EN(GPIO 直连,不经任何队列或抽象层) 告警延迟;保护本身不受影响(eFuse 是硬件)
t_thermal 1 s 中 两路 NTC(下板功率级)+ 载板 LM75BD(手机侧)→ 风扇 PWM 闭环;过温先降流后关路(FR-13、TR-5) 风扇维持上一个占空比
t_actuator 事件驱动 中 每路一个状态机:断电重启 / 按键序列 / EDL 序列(10.6) 序列停在中间 —— 必须有超时兜底
t_link 事件驱动 低 上位机命令解析与遥测上报(10.8) 见 10.2:不影响本地职责
t_log 空闲 最低 环形缓冲落盘(10.7) 丢日志,不丢功能

共享资源只有两个,各配一把互斥锁:

  1. I²C 总线 —— 10 个器件在同一条上(IF-15),其中 LM75BD 还在排针另一侧的载板上。
  2. 74HC595 的 24 位链 —— 全板共享,见 10.6 的影子寄存器。

I²C 带宽核算(400 kHz,t_safety 是唯一的高频用户):

一次 INA226 寄存器读 ≈ 46 个位周期 ≈ 115 µs
6 片 × 2 个寄存器(总线电压 + 分流电压) = 12 次  ≈ 1.4 ms
PCA9555 读两个输入端口                          ≈ 0.12 ms
                                        合计 ≈ 1.5 ms / 20 ms = 约 8 % 占用

余量充足,t_thermal 和偶发的 DAC 写完全挤得下。首板上电时用逻辑分析仪实测复核一次 ——这个数是按理想时序算的,没算时钟延展和重试。

A8 已落地(2026-09-07,firmware/main/{tasks,sched,t_safety}.c + components/phonerail_hal/{include/hal_lock.h,hal_i2c_acct.c})。五条实现约定:

  1. 五个任务是一张 const 表(tasks.c),周期与优先级写在一起,开机 task_table_ok() 自检,不过就 abort。查的是相对顺序:safety 唯一最高、log 唯一最低、 thermal 与 actuator 同级、link 夹在中间;外加「巡检周期必须长于 INA226 的 一帧转换 8.8 ms」,否则同一帧被读两次,现象是「电流特别稳」。
  2. 周期是间隔,不是延时。 sched_periodic_*() 记的是下一次该醒的绝对时刻 (回绕安全,一律用 int32_t 差判先后)。写成「干完睡 20 ms」的话, 实际周期是 21.6 ms 且随总线忙闲抖动。过载时跳过错过的整周期、保住相位, 不补跑 —— 补跑的现象是「卡一下之后连着打满总线」。
  3. 两把锁不许嵌套持有。 595 是纯 GPIO、I²C 是纯总线,两者没有交集, 所以「拿着 A 去拿 B」永远是写错了,hal_lock_take() 直接判 HAL_ERR_STATE ——与其定一套加锁顺序再指望人人记得,不如让它不可能发生。ESP 侧用的是 mutex 不是 binary semaphore(要优先级继承:t_link 拿着总线时 t_safety 不能被中优先级任务无限期挡住)。
  4. 巡检不死等锁:等 5 ms(周期的 1/4)拿不到就跳过这一圈并计数。 死等的现象是最高优先级的任务整个停摆,而且没有任何人会发现。 同样地,一个器件读失败不中断这一圈,失败那一路保留上一圈的值并把 ok 标 false ——清零的话 A10 的过压判据会看到一个「正常」的 0 V。
  5. 那 8 % 是算出来的,不是抄的。 hal_i2c_acct 在 HAL 里按线上真实字节数记账 (两个目标同一份代码),t_safety_budget_us() 按事务形状静态预算,两者必须相等。 实测:一次寄存器读 48 个位周期(本节手算的 46 没数 STOP 和一个重复 START), 一圈 13 次事务 = 1560 µs / 20 ms = 7.8 %,与本节的「约 1.5 ms / 约 8 %」相符。 多读一次寄存器就会让静态预算与实测对不上,测试当场变红。 首板仍要按本节末尾那句用逻辑分析仪复核一次(这个数不含时钟延展与重试)。

10.4 五路一张表:禁止用通道号算任何东西

这是本项目最容易写出「看起来对、实际错一路」的地方。三组编号都不是逻辑顺序:

  • INA226 地址按布线难度重排过(IF-16,设计文档 §8.2);
  • 74HC595 的位序按 PCB 几何排(设计文档 §17.2 / §22.4);
  • GPIO 编号按出线方向倒推(设计文档 §26.18)。

所以固件里只允许有一张静态配置表,全部五路循环都索引它:

typedef struct {
    uint8_t  ina_addr;      /* 0x40 0x41 0x44 0x45 0x42 —— 查表,不能算 */
    uint8_t  dac_chip;      /* 0x4A: CH1-3   0x4B: CH4-5 */
    uint8_t  dac_ch;        /* U6.OUT0/1/2   U7.OUT0/1  */
    gpio_num_t efuse_en;    /* GPIO 直连 */
    gpio_num_t vbus_en;     /* GPIO 直连 */
    uint8_t  key_bit[3];    /* PWR / VUP / VDN 在 24 位链里的位号 */
    uint8_t  edl_bit;       /* U33 上的位号 */
    uint8_t  flt_bit;       /* PCA9555 IO0_0..4 */
    uint8_t  pg_bit;        /* PCA9555 IO1_0..4 */
} rail_cfg_t;

⚠️ ina_addr = 0x40 + ch 这行代码一定会被写出来,而且只有 CH3/CH4/CH5 是错的。 实际地址是 0x40 / 0x41 / 0x44 / 0x45 / 0x42。查表,不要算。

A3 已落地:表在 firmware/components/drv/include/ina226.h,取值只走 ina226_rail_addr(rail)(rail 是 1~5,越界 abort), test_channel_index_is_not_the_address_low_bits 专门拦「改回算式」这一手。 同处还记了两条 INA226 的实现约定:寄存器指针不自增(每次读前重写指针, sim 会自增、真件不会,靠自增在宿主机上测不出来);Calibration 丢了不报错、 只让 Current/Power 恒读 0,所以巡检要定期 ina226_check()。

同理,PCA9555 的方向寄存器只能配成全输入:备用脚 Port0[7] / Port1[7:5] 在硬件上 直接接地,配成输出并写 1 就是对地短路(设计文档 §17.2 已在图纸上注明)。

A6 已落地:驱动在 firmware/components/drv/include/pca9555.h, 根本不提供「把某个脚配成输出」的接口(和 sr595.h 不提供「抬 OE#」同一条规矩)。 方向字一律过 pca9555_dir_safe(),不安全直接 abort。 同处还记了四条这一片的实现约定: ① ⚠️ Output Port 寄存器的复位值是 0xFF —— 所以把接地脚配成输出的那一刻它就 输出高电平,中间没有「先写个 0 再说」的余地,这比「配错方向」本身更急; ② 极性反转寄存器必须无条件清零,不能赌复位值:看门狗只复位 ESP32, PCA9555 不掉电,上一条命的残留值原样留着,而写错的现象是「五路同时报故障」 或者更糟的「五路故障全都看不见」,I²C 一切正常; ③ 三种极性(FLT# 低有效 / PGOOD 高有效 / INA_ALERT 低有效)各自只在 pca9555_rail_fault() 等四个函数里出现一次,上层不许自己去 & 位; ④ INT# 是加速路径不是主路径:它要读走输入端口才释放,漏读一次就再也等不到 下一个沿,所以 t_safety 必须无条件轮询(§10.3 的 20 ms 就是按这个排的)。

⚠️ FLT 与 PG 两张位表这次确实是顺排的(都是 bit = rail - 1), 和 INA226 地址、595 位号那两张表不一样。所以这里最容易写出来的错不是「算式」, 而是把 Port0 和 Port1 搞反 —— 两个端口的 bit0~4 长得一模一样,搞反之后 「有故障」会被读成「电源良好」,方向正好相反,而且静息态两个端口读回来都是 全 1,单看一帧完全看不出来。test_one_rail_at_a_time 专门拦这一手。

A7 已落地(2026-09-07):那张 rail_cfg_t 表在 firmware/main/rail_cfg.h / rail_cfg.c,取值只走 rail_cfg(rail)(1~5,越界 abort)。六列的样子:

CH INA226 DAC 按键位 PWR/VUP/VDN EDL 位 EFUSE_EN VBUS_EN
1 0x40 U6.OUT2 1 / 2 / 3 16 IO39 IO40
2 0x41 U6.OUT0 19 / 17 / 18 7 IO48 IO10
3 0x44 U7.OUT0 22 / 21 / 20 6 IO6 IO11
4 0x45 U6.OUT1 9 / 10 / 11 0 IO41 IO42
5 0x42 U7.OUT1 15 / 14 / 13 5 IO8 IO17

落地时定下来的五条: ① 本表与四个驱动那四张表是「两处写同一个数」,所以要对账而不是现算。 开机自检 rail_selftest() 逐格拿本表和 ina226_rail_addr() 等取值函数比一遍, 对不上就报 RAIL_SELFTEST_TABLE。做成「开机从驱动现算」的话按定义就查不出分家, 而且表不再是 const 的、用之前得先记着初始化。 ② 判据本身也要被测:真表是 const 的,测试改不动它,所以判据单拎成 rail_selftest_table(t),测试喂几张故意写坏的表进去看它认不认 (分家 / 引脚重复 / 撞 SDA / strapping / 不存在的号 / 越界,六种都验过)。 ③ 两列 GPIO 是全固件唯一一处出处,没有第二张表能对账 —— 所以 「EFUSE_EN1 和 VBUS_EN1 写反」这种错自检查不出来(两个都是合法空闲脚、 不重复、不越界)。唯一的拦网是 test_en_pins_match_the_pcb_netlist 里钉死的 十个值(出处:.claude/pcb1-parts.json 的 U20 焊盘网络)和 B1 上板实测。 ④ 「不是顺排」要看整列,不能逐格撞算式:CH4 的按键位正好是 9/10/11、 CH1 的 EDL 位正好是 16、CH3 的两根 EN 正好等于「IO4 起 / IO9 起」那个算式 —— 单看任何一路都「像顺排」。测试改成断言整列不是等差数列。 ⑤ ⚠️ HAL 两侧的 GPIO 上限原来是 pin < 48,把 IO48 关在了门外, 而 EFUSE_EN2 就在 IO48 上 —— 现象是 CH2 的 eFuse 既开不了也关不掉。 已改成 048(含)。ESP32-S3 的编号是连续的但芯片不是:IO22IO25 不存在、 IO26~IO32 是模组内部 flash/PSRAM,写进配置表 gpio_config() 不报错也不干活, 所以自检把这些号也当成越界。

同处还落地了全板引脚表 firmware/main/board_pins.h(§8.1 那张表里 「全板独一份」的那些脚)与两根 EN 线的表驱动遍历 (rail_en_init() / rail_efuse_enable() / rail_vbus_enable() / rail_all_off())。 rail_all_off() 先断五路 VBUS 再关五路 eFuse,中间不留「手机只靠 USB 供着电」的窗口;十个脚全部试一遍,不因为某一个失败就提前返回。 高有效这件事只在 en_write() 里出现一次。

10.5 电压通道:DAC 只做微调,而且是反向的

事实 含义
Vd = 0 V → 输出 4.010 V(上限) DAC 写得越大,输出越低
Vd = 0.548 V → 输出 3.90 V(NVM 出厂默认) 上电即安全值,不依赖 ESP32 初始化成功
Vd = 2.424 V(满量程)→ 输出 3.524 V(下限) 满量程是 2.424 V,不是 2.5 V(内部基准 1.212 V ×2)

⚠️ 反向关系是本节最容易写反的一条。 写反的现象不是报错,而是 「调高电压的命令把手机调到了 3.52 V」——手机照样能开机,只是更容易在负载尖峰下掉电, 而且要跑一阵子才看得出来。固件里把这一步封成 rail_set_mv() 单一入口, 内部做 mV → Vd → code 的换算,不要在别处直接写 DAC 码值。

三条固件规矩:

  1. 越界即拒绝:rail_set_mv() 收到 >4010 mV 的请求直接返回错误并记事件(SR-13)。 注意这是第四道限制,前三道在硬件里 —— 它的作用只是让操作者早点知道自己写错了。
  2. 不用 DAC 做保护:任何时候都不要靠「把电压调低」来处理异常,那是 EFUSE_EN 的活。
  3. 出厂标定:分压网络与 DAC 基准都有 ±1 % 级的容差,用该路 INA226 读回实际输出, 做一次两点线性标定存进 NVS。标定值缺失时退回理论公式(误差约 ±40 mV,可用)。

A4 已落地:换算与设定在 firmware/main/rail.c,唯一入口 rail_set_mv(rail, mv) (rail 是 1~5)。三个验收点各有一条测试:4020 mV 被拒、码大电压低、3900 mV ↔ Vd 0.548 V。 标定(本节规矩 3)还没做,现在走的是理论公式;RAIL_VOUT0_UV / RAIL_DAC_SPAN_UV 两个常量就是将来要被标定值顶替的地方。

同处记下三条 DAC63204 的实现约定(出处 SLASEX3A):

  • 12 位码在 DAC-X-DATA(19h~1Ch) 里是左对齐的(bit15-4)。忘了 << 4 不报错, 只让输出变成 1/16 —— 而在本板上那表现为「五路都是 4.010 V 满电」,看不出异常。
  • EN-INT-REF 要先于增益设定写,且上电安全码要写在通道上电之前: 复位码是 0,而 Vd = 0 就是输出上限 4.010 V。dac63204_init() 因此把 COMMON-CONFIG 写两次(先按住输出,写完码再上电)。
  • 通道分配不是 rail−1(设计文档 §26.18 重排过,五路里四路对不上): CH1→U6.OUT2 / CH2→U6.OUT0 / CH3→U7.OUT0 / CH4→U6.OUT1 / CH5→U7.OUT1。 四通道电气对等 ⇒ 接错了照样工作,只是调错路,功能测试测不出来。

10.6 执行器:三个状态机 + 一个影子寄存器

每路三个序列,彼此互斥、不可重入(同一路上同时只能跑一个):

序列 时序 关键约束
硬重启 EN↓ → 等 3~5 s → EN↑ → 等 17 ms(tON 5.1 ms + 缓启动斜坡 11.5 ms)→ 等 PG 置位 放电由硬件联锁自动完成(SR-7),固件不要去控制放电 MOS
按键 置位 → 保持 T(100 ms / 2 s / 10 s)→ 清零 组合键(VOL−+PWR)必须同一帧写进去,不能先后两次
EDL EDL_EN=1 → VBUS_EN=1 → 等枚举 → EDL_EN=0 顺序固定(SW-6):先短接再插 USB

影子寄存器是这一节的核心:15 路按键 + HUB_RST + 5 路 EDL_EN 全在同一条 24 位 移位链上,写任何一位都要把整条链重发一遍。

ESP32.KEY_SER → U21 → U22 → U33        (先移入的字节落在链尾 U33)
写入顺序:U33 字节 → U22 字节 → U21 字节,最后一个 RCLK 上升沿统一生效

规矩:内存里保存一份 24 位影子值,全固件只有一个函数能写 595,任何任务改按键 或 EDL 都是「改影子 → 请求刷新」。多处直接操作移位链,症状是另一台手机的按键被顺手 清掉,而且复现要靠两个动作正好撞在一起 —— 属于会拖很久的那类 bug。

RCLK 打点之前输出不变,所以整帧刷新是原子的;OE# 保持有效,不要用拉高 OE# 的方式「临时全松开」——那会让 15 个栅极同时进高阻,靠的是每个栅极自己的 100 kΩ 下拉, 是复位路径,不是正常控制路径。

2026-09-06 A5 落地:components/drv/sr595.{h,c}。影子是一个 24 位整数 (bit0 = U21.QA 链首 … bit23 = U33.QH 链尾),唯一写入点是 sr595_flush(); 别处只准 sr595_modify() 改影子,组合键走 sr595_write(mask, value) 一帧发完。 位表见设计文档 §17.2(§26.18 重排后的那张,不是 09-04 那张)。

同处记下三条这一层的实现约定:

  • 上电顺序是「先锁存安全值,再使能 OE#」,不能换。 三片的 MR# 接死在 +3V3, 没有复位线 ⇒ 上电时移位寄存器内容不确定。先拉低 OE# 就是把那坨随机值 放到 15 个栅极上 —— 五台手机的电源键可能一起被按下去。所以 sr595_init() 里 KEY_OE# 在安全值锁存完之前根本不配成输出,靠板上 R609 上拉着。 (ESP-IDF 的 gpio_config() 一配成输出就开始驱动,输出寄存器复位值是 0。)
  • HUB_RST 低有效,所以上电安全影子是 1 << 23 而不是 0。写成 0 的现象是 「Hub 插上枚举不出来」,而五路电压电流全都正常,看不出跟固件有关。
  • 发送失败要留着脏(sr595_flush() 失败不清 dirty,下次重发整帧)。 丢了脏标志 = 链上和影子永久对不上,而后面每一次 flush 都会「成功」。

2026-09-07 A9 落地:firmware/main/t_actuator.{h,c}(+ test/test_t_actuator.c 23 例)。 三个序列成了一张每路一份的状态机:HR_OFF → HR_RAMP → HR_PG / KEY_HOLD / EDL_SHORT → EDL_ENUM,全部以 CLEANUP 收尾。时间常量都带出处 (断电 4000 ms 落在 §7.4 的 3~5 s 窗口里、斜坡 17 ms、等 PG 上限 500 ms、 按键 100/2000/10000 ms、EDL 短接 10 ms + 等枚举 3000 ms)。 这一层的六条实现约定:

  1. 一个函数一步,不许 while(1)。 t_actuator_step() 只看时钟、推状态、 返回「下次最晚多久再来」;任务体在 app_main.c 里还是三行。写进死循环的 时序在宿主机上一行也跑不到 —— 而这一包的全部内容就是那些时间常量与它们的先后。
  2. 按下去的东西一定要松开,CLEANUP 不放弃。 正常结束、超时、出错、被 t_actuator_abort() 打断走的是同一条收尾路径;写不进 595 就一直重试, 这一路一直算 busy。放弃的现象是电源键留在按下状态、手机再也起不来, 而遥测上五路电压电流全都正常。连「按下那一步就失败」也要走收尾 —— 否则影子里留着一个脏的「按下」,谁下一次 flush 谁就替你把键按下去。
  3. 等不到 PG 不许再断电。 ACT_RES_NO_PG 的语义是「电已经开着,只是没能确认」。 PCA9555 失联时把「看不见」当成「坏了」去断电,等于一片 I/O 扩展器失联 就让五台手机一起停机 —— §10.10 明写着这片不应答不关输出。
  4. PG 是喂进来的,执行器不碰 I²C。 PGOOD 已经被 t_safety 每 20 ms 读一遍, 再读一次等于凭空多一份总线占用和一份两边不一致的可能。回调(act_pg_fn) 同时也是「两把锁永远不会嵌套」的物理保证:本文件只碰 GPIO 与 595。
  5. 不可重入 = 直接拒(HAL_ERR_STATE),要抢占得先显式 t_actuator_abort()。 被拒的请求一个字节都不发,也不改正在跑的那个序列的时长。
  6. 组合键一次 chain_write() 发完(同一个 RCLK 沿)。分两次写的实现在 sim_sr595_latches() 上会 +2,而且中间那一帧只按下了一个键。

验收之外还钉了两条容易漏的:硬重启开始前先松掉本路三键(否则手机开进 Recovery/Fastboot),以及 EDL 结束后 VBUS 留着不动(手机正挂在 9008 上)。 21 条注错逐个跑过,全部变红。

10.7 事件记录与存储

事件是 SW-3 的交付物,也是排查排线接触不良的唯一线索,所以记录路径不能依赖上位机在线。

项 设计
缓冲 RAM 环形缓冲,≥256 条;t_safety 只入队,不落盘
落盘 t_log 空闲时批量写 NVS / SPIFFS 分区
单条 时间戳 + 路号 + 事件类型 + 当时的 V/I(16 B 量级)
容量 4096 条 ≈ 64 KB,按每天几十条算够存半年以上
时间 上电从 0 计;上位机连上后下发一次墙钟做偏移校正(不加 RTC 芯片)

必记事件:eFuse FAULT / PG 变化、SR-5 触发关路、SW-8 反灌断 VBUS、过温降载、 每次重启序列(软 / 硬、谁下的命令)、越界命令被拒、I²C 器件失联、风扇停转。

A11 已落地(2026-09-07,firmware/main/evlog.{h,c} + 新增的 hal_store(掉电不丢区)+ partitions.csv 的 evlog 分区)。落地时定下来的八条:

  1. 落盘不用 NVS,也不用 SPIFFS,是一块裸的 data 分区。 NVS 是键值表 (4096 条记录 = 4096 个键,页式 GC 会把「写一条事件」变成一次不可预测的搬移), SPIFFS 的掉电一致性自己就是一整章。evlog 本身已经是环形缓冲,它要的只是 「擦一个扇区 / 写一段字节」—— HAL 因此多出第六样 hal_store。
  2. 容量是 17 个扇区 = 4335 条,不是 16 个。 每个扇区要拿一个 16 B 的槽 当扇区头(epoch),于是 4 KB 只装得下 255 条,16 扇区 = 4080, 差 16 条不到本表要求的 4096。8 MB flash 上多这 4 KB 没有代价。
  3. 序号 = flash 里的位置(epoch × 255 + 槽号),不是一个自增计数器。 重启之后接着数不用把计数器存起来,掉电也不会倒退。代价是没落盘的事件 还没有序号 ⇒ events 命令(10.8)要先 evlog_flush() 再读。 反过来做(入队时发号)会在缓冲溢出丢事件时留下一段永远补不上的空号, 上位机那边的现象是「拉日志卡在某个序号上不动」。
  4. 入队在临界区里做完,不是加第三把锁(§10.3 的共享资源只有两个)。 入队方是 t_safety(最高优先级、断电之后调),停在一把 t_log 持着的锁上 就等于让最低优先级的任务决定巡检的节拍。关中断的临界区里只改内存: 时间戳与校验都在外面算完。
  5. 丢事件可以,悄悄丢不行。 RAM 缓冲(256 条)满了盖最旧的 ——「最新的一条 永远进得去」——但被盖掉的会在下一次落盘时留下一条 LOG_EV_LOST, 值是丢了多少条。一份会安静少掉几行的日志比没有日志更糟:排查的人会拿它 当完整的时间线看。flash 写失败丢掉的那一批同样补这条。
  6. 每条记录带一个 CRC-8,而且只能是 CRC-8。 NOR 的写会被掉电打断在任意 字节上,半条记录在字节层面跟真记录长得一模一样。第一版用异或和, 漏掉的正是最常见的那条:bus_mv 与 current_ua 都是 0 的记录被切在第 8 个 字节上时,后 8 个字节留在 0xFF,而 8 个 0xFF 的异或恰好是 0 —— 半条的校验和与整条一模一样。
  7. 写失败之后放弃这个扇区的剩余部分。 接着写的话,重启扫描会停在那半条上, 把后面写进去的全部当成不存在。代价是最多浪费 254 个槽,换来的是 「扫描停在哪 = 日志到哪为止」这条不变量永远成立。被放弃的那段在序号空间里 是个洞,evlog_read() 给它一条空记录占住位置 —— 跳过它的话上位机的 游标会卡在洞上。
  8. 每次上电记一条 LOG_EV_BOOT。 本表写着「上电从 0 计」,所以时间戳会 跳回去;没有这条记录的话,两次上电的事件在时间轴上互相穿插, 而日志里看不出中间重启过。

事件类型的数字是 flash 格式的一部分(改一个值就等于把跑了半年的历史记录 重新解释了一遍,而且读出来仍然是合法记录、没有一处报错),所以 log_ev_t 只许在末尾追加;safety_ev_t → log_ev_t 是一个显式的 switch,不是加偏移。 上表里还没人产生的那几类(eFuse FAULT / PG 变化 / 过温降载 / 重启序列 / 命令被拒 / I²C 失联 / 风扇停转)号已经占住,各归各的包去填。

27 条 ctest 用例;23 条「一定会被写出来的错」逐个注进去跑过,21 条变红。 剩下两条抓不到,理由见 firmware/HANDOFF.md。

10.8 上位机接口 ✅ 已定稿(2026-09-06 用户拍板:方案 C)

这一节回答的问题:SW-1 的触发条件写的是「adb 探活连续失败 N 次」, 而 ESP32 跑不了 adb —— 所以崩溃判定必然发生在上位机,板子只负责执行动作。 这意味着必须有一条控制链路,而在此之前全部文档里都没有定过它。

结论:WiFi 为常态链路,USB CDC 为兜底链路,两条同时在线。

选它不是因为「都要」,而是因为 SW-1 是这块板子的核心价值, 而 WiFi 与手机 adb 走的是同一张网 —— 只做 WiFi 等于让「救援通道」和「故障通道」 共命运,恰好在最需要它的时候一起失效。USB 的增量成本很低:线本来就插着。

留档备查的两个单选方案:

方案 优点 为什么不单独用
A. 只做 WiFi 无线、可多台组网;设计文档 §9.5 的 3V3 预算里已经给 ESP32-S3 的 WiFi 突发留了 ≈350 mA,说明当初就是按带 WiFi 设计的 网炸了救砖通道跟着炸,见上
B. 只做 USB CDC 最可靠,不依赖网络 每块板占上位机一个 USB 口;机架规模上去之后不现实

两条链路怎么共存

        WiFi (MQTT/HTTP)          USB CDC (J22)
              │                        │
      ┌───────▼────────┐      ┌────────▼───────┐
      │ 传输适配器 A    │      │ 传输适配器 B   │   ← 只管收发字节与分帧
      └───────┬────────┘      └────────┬───────┘
              └──────────┬─────────────┘
                  ┌──────▼───────┐
                  │ 单一命令处理器 │   ← 全部鉴权、限幅、幂等判重都在这里
                  └──────┬───────┘
                         ▼
                    执行器 / 遥测

四条规矩:

  1. 命令处理器只有一个。 两条链路是传输,不是两套功能。任何安全约束 (SR-13 的限幅、越界拒绝、序号判重)都写在处理器里,绝不写进适配器 —— 否则以后加第三条链路时必然漏掉一处,而功能测试完全测不出来。
  2. 不做主备切换,两条都常开。 谁先送到算谁的,靠 3 的序号去重。 做「主备」就要判定"主链路死了没有",而那个判定本身就是新的故障点。
  3. 序号是全局的,不分链路。 上位机给每条命令一个单调递增的序号, 板子记住最近 N 个已执行的序号。WiFi 重传 + USB 又发了一遍同一条命令时, 第二次必须被丢弃 —— 否则「按 10 秒电源键」会执行两遍,第二遍正好按在刚开机的手机上。
  4. USB 链路与日志共用一条流。 ESP32-S3 的原生 USB-Serial-JTAG 本身就是 CDC-ACM,不需要切到 TinyUSB,也不影响 esptool 下载(FR-15 不变)。 代价是命令响应和运行日志在同一条串流上,必须分帧(建议命令用带前缀的单行 JSON,日志走另一个前缀),否则日志一刷就把响应冲散。

⚠️ 别把 WiFi 凭据和 MQTT 地址编译进固件。 放 NVS,用 USB 链路配置 —— 这正好是兜底链路的第二个用途:新板子上架时先插 USB 配网。 (0.1.19 起多一条不用插线的路:配网热点,见下面「配网热点」那一条。)

命令语义与链路无关,两条链路完全一致:

命令 参数 说明
status — 五路 V/I/PG/FLT、输入 V/I、两路 NTC + 载板温度、风扇转速、各路状态机状态
set_voltage 路号, mV 走 rail_set_mv(),越界拒绝(10.5)
power_cycle 路号, 断电秒数 二级硬重启(4.1)
key 路号, 键位掩码, 保持 ms 单键与组合键同一个接口
edl 路号, on/off 内部按 SW-6 的固定时序展开
vbus 路号, on/off 默认断开
fan 占空比 / auto auto 交回 t_thermal 闭环
events 起始序号 增量拉取事件日志

三条接口硬约束:

  • 没有任何命令能让输出超过 4.2 V(SR-13)—— 包括「原始 DAC 写入」这类调试命令, 它压根不该存在于发布固件里。
  • 命令必须幂等或带序号:链路重传时不能让「按 10 秒电源键」执行两遍(上面第 3 条)。
  • 链路失联不触发任何动作(10.2):没有心跳超时关输出这回事。

A12 定稿的线上格式(2026-09-27,firmware/main/cmd.h)

一行一条、单层扁平 JSON(嵌套 / 数组 / 转义 / 浮点 / null / 重复键一律 parse)。

→ {"seq":17,"cmd":"key","rail":3,"keys":"pwr","ms":10000}
← {"seq":17,"ok":true}
← {"seq":17,"ok":false,"err":"range"}          重传命中:{"seq":17,"ok":true,"dup":true}
命令 参数 与上表的差别
status — 只读,seq 可省,不判重。读不到的量回 null(不是 0)
set_voltage rail, mv 限 3524~4010(DAC 物理范围,比 SR-13 的 4.2 V 更紧)
power_cycle rail 不收断电秒数:固定 4 s(§7.4 的 3~5 s 窗口)
key rail, keys("pwr" / "vdn+pwr"…), ms(1~15000) 单按电源键 ≥10 s 记一条软重启事件
edl rail 不收 on/off:跑一遍 SW-6 固定序列;退出 EDL 用 power_cycle
vbus rail, on eFuse 关着时 on:true 拒(state);序列进行中拒(busy)
power ⭐新增 rail, on 管理界面要能停一台。关 = 先断 VBUS 再关 eFuse;VBUS 断不掉也照样关 eFuse,回 io
fan duty(0~1000,0 = 自动) 只是下限:实际 = max(温度曲线, duty),远程不能把风扇调小
events from(可省) 一次最多 8 条 [序号,上电ms,路,类型,arg,mV,µA],类型是 log_ev_t 的冻结数字
  • 会动东西的 7 条必须带 seq(1~2³²−1);判重窗口 32 条、全局、按值相等判。 上位机同一个 seq 只用于同一条命令的重传,「再试一次」要换新 seq。
  • 错误码:parse / unknown_cmd / no_seq / arg / range / empty / busy / state / io / overflow。 range / empty / state 记 LOG_EV_CMD_REJECT(arg = 命令号,bus_mv = 越界值,current_ua = 原因号),其余不记。
  • 空槽(PRSNT# 高)的按路命令一律 empty。
  • 板子重启会清空判重窗口;上位机看 status.up 变小识别重启。

A13 定稿的 USB 分帧(2026-09-27,firmware/main/ulink.h)

上位机 → 板   @C <json>[*XXXX]
     CRC 可省(给人手敲);带了就必须对
板 → 上位机   @R <json>*XXXX
       CRC 必带;XXXX = CRC-16/CCITT-FALSE(json),4 位大写十六进制
其余任何一行 = 日志
  • 前缀只在行首算数;日志行若以 @ 开头,固件会垫一个空格 ⇒ 日志永远不会被当成帧。
  • 链路层自己的错误(不进命令处理器、不记事件):too_long / bad_crc / bad_frame(NUL 或控制字符)/ queue_full,能解析出 seq 就回显。
  • USB 副控制台已关(CONFIG_ESP_CONSOLE_SECONDARY_NONE):USB 上只有「日志钩子的副本 + 回应」两路,同一把锁;printf / panic 只去 UART0(J33)。
  • 上位机打开串口时 DTR / RTS 保持低,否则可能把板子复位。参考实现 firmware/tools/prlink.py。
  • 实测(板 B,2026-09-27):200 条 status 连发 0 坏帧,混进的 t_safety 日志全部正确识别;往返 status ≈3 ms、小命令 <1 ms。

A14 定稿的 MQTT 链路(2026-09-27,firmware/main/mlink.h / netcfg.h)

base = phonerail/<id>,id = 板子 STA MAC 的 12 位小写十六进制(板 B = b81f3fc9c8bc)。

topic 方向 QoS retain 内容
<base>/cmd 服务器 → 板 1 否 一条命令 JSON(与 USB @C 负载相同,无前缀无 CRC)
<base>/rsp 板 → 服务器 1 否 一条回应 JSON
<base>/tele 板 → 服务器 0 否 周期遥测 = 一条 status 回应(无 seq),默认 1 s(tele_ms 200~60000)
<base>/state 板 → 服务器 1 是 {"online":true,"fw":..,"ip":..};掉线由遗嘱改成 {"online":false}(keepalive 15 s)
  • 配网(C1):wifi(ssid / pass)与 mqtt(uri / user / pass / tele_ms)两条命令,存 NVS,只收 USB 与配网热点(经 MQTT 来回 link 并记事件)。 net 查网络状态,永不回显密码(只报 pass_set / mpass_set);含 wifi_reason(最近一次断开原因码)。 mqtt 可带 "keep_pass":true = 口令沿用已存的(与 pass 同时给回 arg)。
  • 配网热点(0.1.19,2026-10-06 用户定「SoftAP + 网页,连 WiFi 失败后默认回到配网模式」),firmware/main/prov.h:
    • 开:一组 WiFi 都没配 ⇒ 立刻;配了但连续 60 s 没拿到地址 ⇒ 开。开着期间 STA 照常在后台重连,热点不设超时。
    • 关:STA 连上 ⇒ 热点上没人立刻关,有人再留 60 s(让网页看到结果)。
    • 热点 PhoneRail-<id 末 6 位>,WPA2,口令所有板子固定 88888888(0.1.20,2026-10-06 用户定;0.1.19 曾是每板随机), net 回应的 "ap":{"on","why","sta","ssid","pass"} 里也报(两条链路都报)。
    • 网页 http://192.168.4.1/(DNS 劫持 ⇒ 手机连上自动弹出):看状态、扫描、加 / 删 WiFi、改 MQTT。 提交的内容拼成 wifi / mqtt 命令走同一个命令处理器(链路号 3 CMD_LINK_AP),这条链路拼不出别的命令; 网页同样不回显任何已存口令。只应答热点那一侧来的连接。
    • 有人在网页上操作时 STA 重连间隔不短于 20 s(每次重连要离开热点信道扫一圈);只放慢、不停。
    • 热点不碰任何输出(同「失联不动作」)。代价:路由器短暂重启也会让热点冒出来约一分钟; 口令是公开的固定值 ⇒ 在无线范围内的任何人,都能在板子没网时把它指向别的 broker(等同拿到这块板)。
  • uri 收 mqtts://(TLS,CA 编在固件:firmware/main/certs/evse_ca.pem)或 mqtt://(只该用在局域网)。
  • broker = home 服务器现有 mosquitto 的 8883(TLS),与 evse 共用:板子用户名 = id,ACL 用 pattern … phonerail/%u/… 只许碰自己的四个 topic;后端用户 phonerail-backend 可读写 phonerail/#。口令在服务器 /root/phonerail/mqtt.env(仅 root)。
  • 失联不动作:WiFi 断按 1/2/4…60 s 退避重连,MQTT 由 esp-mqtt 每 10 s 重连;没有任何分支会碰输出。
  • 实测(板 B,2026-09-27):经 WiFi CANBOX-5G → 公网 → 8883 TLS 连上;判重 / link / empty / parse 回应与遥测(1 条/s)全部符合。

C2 定稿的 OTA(2026-09-27,firmware/main/ota.h / ota_esp.c)

用户定的三条:明文 HTTP 下载 + 命令带 sha256(哈希走 MQTT/TLS,下载被改了只会哈希不对)、不签名(不烧 eFuse)、 新固件 120 s 内连上 broker 才算健康,否则回滚。

  • 命令(两条链路都收,会动东西 ⇒ 必须带 seq、进判重窗口): {"seq":N,"cmd":"ota","url":"http://home.example:7765/fw/<sha256 前 16 位>.bin","sha256":"<64 位小写>","size":N,"reboot":false} 回 {"seq":N,"ok":true,"act":"start"|"reboot"|"ready"};下载中再来 busy;新固件还没自证时 state;size 超一个槽(3 MB)range。 url 只收 http://、≤64 字节(命令字符串上限)。
  • ⚠️ reboot 默认 false:只下载 + 校验 + 切启动分区、下次复位生效;同一个镜像已就绪时再发带 "reboot":true 的,不重下、直接重启。 重启 / 超时回滚:板上固件 ≥0.1.0-c3 且目标镜像认接管 ⇒ 手机不断电(§10.9「重启不断电」);否则 ESP 一复位 EN 就被下拉,五台手机硬断电一次。
  • status 多一块 "ota":{"st":"idle|dl|ready","got":N,"size":N,"err":null|"http|size|hash|flash|image|boot|noslot","pend":bool,"bad":null|"版本号"}; pend = 正在跑的固件还没自证,bad = 被判无效回滚掉的那份的版本号(镜像头版本 = PHONERAIL_FW_VERSION)。
  • 新事件 LOG_EV_OTA(18),arg:1 开始 / 2 就绪 / 3 失败(bus_mv = 原因号)/ 4 自证健康 / 5 回滚 / 6 为换固件重启。新命令号 ota = 13。
  • 健康判据:启动走完 + 配了 broker 就要 MQTT 连上(只配 WiFi 则 WiFi 通、都没配只看启动);在 t_link 里判,另有 esp_timer 在 150 s 兜底 (app_main 卡死也能回滚);自证前崩溃 / 看门狗由 bootloader 回滚(CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE)。
  • 分区(16 MB):nvs 0x9000(没动)/ phy_init / ota_0 0x10000、ota_1 0x310000 各 3 MB / otadata 0x610000 / evlog 0x612000。
  • 服务器:nginx 7765(rack/deploy/nginx-phonerail-fw.conf)只放行 /fw/<16 位十六进制>.bin,不过门户(板子过不了门禁; 完整性靠哈希、镜像里无机密);路由器 7765 转发 2026-09-27 已加。推送工具 firmware/tools/ota_push.py <id> [--reboot]。
  • 实测(板 B,2026-09-27):1 MB 下载 ≈8 s,推送到自证健康 19 s;错哈希 ⇒ err:hash、不切分区;删掉板子 broker 账号后推送 ⇒ 120.1 s 回滚到旧版、bad 报出被回滚的版本;下载期间巡检 skipped 0。

10.9 启动顺序

1  上电:GPIO 全部显式置安全态(硬件下拉已保证,固件再确认一次)
2  I²C 扫描 10 个器件 ——  缺任何一个都进「降级模式」(10.10),不要照常启动
3  读回 DAC 通道值(NVM 默认 3.90 V),与期望比对
4  载入 NVS 里的标定值与上次的每路使能状态
5  逐路上电,**路与路之间错开 100 ms**
6  等各路 PG,超时则记事件并保持该路关闭
7  起风扇闭环,起链路任务

⚠️ 第 5 步的错开不是讲究,是必须:单路缓启动涌流 0.8 A,五路同时开就是 4 A 叠在 12 V 输入上,而输入预算本来就是 8.3 A(PR-2)。错开 100 ms 之后 各路的斜坡(11.5 ms)完全不重叠。

A15 定稿的落地(2026-09-27,firmware/main/bringup.{h,c},宿主机 24 例):

  • 分两段:bringup_devices() = 第 14 步;t_safety 在两段之间起来,再 bringup_power_restore() = 第 56 步 —— 上电那一刻巡检已经在看(原文第 7 步没说巡检何时起,等五路开完才起就有近 1 s 的盲区)。
  • 第 2 步 R2 实际是 15 片:主干 mux / 两片 DAC / 输入侧 INA226 / CH224A,五片刀片各 INA226 + PCA9555。 六片 INA226 都在这一步写 Config / Calibration(A14 之前没做,电流恒读 0);输入侧 0x43 在这一步登记对调接。
  • 第 3 步 DAC 读回比对只在上电复位时做:热重启时 DAC 没断过电,里面是上次命令设的值。
  • 第 4 步「上次的每路使能状态」= 意图:只有 power 命令改它(NVS 键 phonerail/rail_want,1 字节); 判据跳闸、硬重启、mux 全断都不改。新板没有这个键 = 一路都不开。 ⚠️ 设定电压不持久化:重启后五路回到 3.90 V,上位机要的话自己重下。
  • 第 5 步错开按「上一路 EN 抬高的时刻」算(不等上一路 PG),ESP 上实际 100~110 ms。
  • 第 6 步每路各等 500 ms(= 硬重启的 PG 上限);等不到就关回去并记 LOG_EV_PG_TIMEOUT(17,新增)。 PCA9555 读不到时改按 INA226 电压(≥ 设定 − 100 mV)判;两样都读不到 = 开着、报「确认不了」,不当成坏了。
  • HUB_RST# 只在上电复位时脉冲,其余复位原因连配置都不做(配成输出那一刻就是一次复位)。

重启不断电(2026-09-27,firmware/main/warm_esp.c + bringup.h 的 adopt)——用户定:只管计划内重启,VBUS 也保持。

  • 重启前(warm_restart()):记下开着的路(RTC_NOINIT:魔数 + 掩码 + 反码)→ gpio_hold 锁住五根 EFUSE_EN → 重启。 目标固件不认接管就不锁(镜像第 288 字节的能力标记 PRCAPS1,.rodata_custom_desc):推老版本(≤0.1.0-c2)时 它起来写不动被锁住的 EN,五路会永远关不掉 —— 所以对它照老样子全断重启。
  • 起来后(warm_take(),bringup 之前):复位原因 = ESP_RST_SW 且标记对得上才接管;标记一次性。 再读 EN 脚真电平核对(输出脚配成 INPUT_OUTPUT,锁住时输入缓冲还开着),线上是低的那路不接管。
  • 第 1 步 rail_en_init_keep(adopt):接管的路写高,五根一律放开 hold —— 崩溃 / 看门狗时上一条命的锁可能还在,不放开就关不掉。 刀片不在 / mux 失联的接管路立刻关(adopt_dropped,空槽记 SLOT_EMPTY arg 1)。
  • PCA9555 用 pca9555_init_keep(VBUS 位):方向寄存器已是本固件写的才留 VBUS(掉过电的片子锁存器是 0xFF,照留 = 凭空开 VBUS),键 / EDL 清零。
  • 第 5~6 步:已开着的路不重开、不等 PG。DAC 照常 init(中间约 1 ms Hi-Z ⇒ 输出退化到 3.890 V,设计文档「DAC 未供电 / 输出 Hi-Z」那一行),自定义电压回到 3.90 V(本来就不持久化)。
  • 事件:LOG_EV_OTA arg 7,bus_mv = 接管掩码,µA 列 = 关掉的掩码。
  • 代价:esp_restart → 新固件 t_safety 起来约 1~2 s,SR-5 / SW-8 不判(eFuse 硬件保护在)。兜底定时器回滚、新固件崩溃都不走接管。
  • 实测(板 B,空槽强制 CH1 在位的台架镜像):CH1 开着 → OTA 带重启 → 新固件读到 IO9 仍为高、记 18/7 mv=1 → CH1 一直开着。

10.10 降级矩阵

⚠️ 2026-09-16 按 R2 硬件更新(底板 + 5 片刀片,见 刀片式方案规划-R2.md)。 新增了三行(mux / PRSNT# / PD),改了两行(PCA9555、LM75),理由写在表后。

失效 检测方式 行为
某片 INA226 无应答 I²C NAK 该路失去遥测 → 保持供电,标记为「盲跑」,禁用该路的 SR-5 / SW-8 判据并持续告警
I²C 总线挂死(SDA 被拉低) 连续超时 发 9 个时钟脉冲做总线恢复;仍不通则复位 I²C 外设;再不通进降级模式
DAC 无应答 I²C NAK 不关输出 —— DAC 不响应时输出停在 NVM 默认的 3.90 V,是安全值
某片刀片的 PCA9555 无应答(R2 改) I²C NAK 失去该路的 FAULT / PG 可见性 → 不关输出,改用 INA226 的电压判据兜底并持续告警。⚠️ R2 新增的代价:这一路的 VBUS_EN 也控制不了了 —— 它在这颗芯片的 P0.4 上。见表后第 1 条
TCA9548A(mux)连续选不中(R2 新增) 连续 N 次选通失败(N 待实测,暂定 3) 先脉冲一次 MUX_RST#;复位后仍失败 ⇒ 五路全部断电并记事件。理由见表后第 2 条
PRSNT# 为高(刀片不在位)(R2 新增) GPIO 直读,不占总线 该路不许上电,已上电的立刻关断并记事件。空槽上电 = 金手指裸露带 12 V
整片刀片不应答:本路 INA226 与 PCA9555 同时 NAK,mux 正常(0.1.4 新增) 连续 10 圈(200 ms),只在 eFuse 开着时数 关这一路(只关 eFuse,不再去写那片 PCA9555)并记事件 20 BLADE_MUTE。理由见表后第 3 条
CH224A 读不到 / PD 档位低于预期(R2 新增) I²C NAK 或 0x50 读数偏小 不关输出,按读到的档位(读不到就按最保守的 60 W)设整板功率上限,逼近上限时按优先级降载(§8.3)
载板 LM75BD 无应答(R2 作废) — R2 没有载板,这片不存在。温度只剩两路板载 NTC;两路都读不到时仍按老规矩处理:风扇转到 100 % 并告警,不要按「读不到温度 = 不热」处理
上位机失联 心跳超时 见 10.2:什么都不做
看门狗复位 — 复位即全关(SR-6),重启后按 10.9 恢复,并记一条复位事件

A15 落地时补的三条(2026-09-27,逐行用例在 firmware/test/test_bringup.c):

  • mux 失联时主干也够不着:i2cbus_xact(主干) 要先给 mux 写「全关」,这一步失败回调就不跑。 所以启动时 mux 复位后仍不通 ⇒ 不去碰 DAC / 输入侧 / CH224A(不刷三条误导性的 I2C_LOST), 设定值影子回 NVM 默认、功率按 60 W,只记一条 MUX_LOST。
  • 看门狗复位的「复位事件」就是 LOG_EV_BOOT,arg = esp_reset_reason()(1 上电 / 4 panic / 6 任务看门狗 / 11 USB …)。
  • I²C 总线挂死那一行(9 个时钟恢复 + 复位外设)还没做,在 HAL 层,待办。
  • CH224A:设计上挂 mux 通道 5,板 B 实测在主干应答(0x22/0x23);启动时两处都找。

表后的两条,都是 R2 的架构后果,不是待办:

  1. VBUS_EN 从 GPIO 变成了 I²C 后面的一个位。 R1 里 t_safety 断 VBUS 是一次 gpio_set_level(),几百 ns、永远成功;R2 里它是「选 mux 通道 + 写刀片 PCA9555」, 约 0.16 ms,而且I²C 失联时断不掉。兜底是关 EFUSE_EN(GPIO 直连,永远关得掉), 但要说清楚:关 eFuse 并不能阻止手机经 USB 往回灌,它只是让这一路不再直供电。 真正的反灌保护是硬件那一层(设计文档 §15.2 的母线泄放 + 电容),固件这一层在 I²C 失联时确实降级了。这是 R2 用一根跨板针换来的代价,验收在 §12.1 的 B7。 ⚠️ 相应地,EFUSE_EN 必须永远是 ESP32 GPIO 直连(金手指 A11 独占一针)—— 它是保护链第三层唯一的执行手段,任何时候都不许挪到 I²C 后面。

  2. mux 是全板唯一的单点。 它一挂,五路的电压电流全部看不见,SR-5 的整条监控链 同时断掉 —— 这时「继续供电但看不见」比「停机」危险,所以判据是全断, 与上面「某片 INA226 无应答就保持供电」的处理方向相反。区别在于范围: 一片失联时还有四路看得见,mux 失联时一路都看不见。 之所以先脉冲一次 MUX_RST# 再判,是因为一次总线毛刺就停掉五台手机代价太大。

  3. 整片刀片不应答 ≠ 一颗器件不应答(2026-09-28 加,用户拍板)。第一行「INA226 无应答 → 盲跑」 和 PCA9555 那一行的前提是刀片上另一颗还答应。两颗同时不答应、而 mux 正常(选不中的话读不到这一步), 只剩两种可能:PRSNT# 假在位、刀片其实不在(板 B 第 2 槽实测:PRSNT2# 卡低,刀片挪走后空槽 eFuse 开了约 4 h);或刀片在但这一路 V/I、PG/FLT、VBUS_EN 全都够不着。两种都是「继续供电但看不见」, 按第 2 条同一个道理处理,范围是一路。关着的路不数(假在位的空槽不会每 200 ms 刷一条)。

降级模式的定义:只保留「关断能力」与「事件记录」,不再自动上电、不接受调压命令, 持续告警。宁可五台手机不开机,也不要在盲态下带电跑。

10.11 关于 FR-12「模拟电量曲线」的一个说明

这条需求的实现方式和它的字面意思不一样,值得在架构层写清楚:手机的电量计是从 电池端电压推算 SoC 的,我们给的电压就是它看到的全部信息。所以「模拟电量曲线」 = 按锂电放电曲线缓慢改变输出电压(比如 12 小时从 4.01 V 走到 3.60 V,再跳回去)。

两个后果:

  • 它不需要任何新硬件,是 rail_set_mv() 之上的一个慢速调度器;
  • 它与 SW-4(单路电压微调)共用同一个执行器,两者必须互斥 —— 否则运维把某台调到 4.0 V,曲线任务转头又把它拉回 3.6 V。

FR-12 是「中(可选)」,建议首版先不做,把接口留出来即可。

10.12 架构层面的待定项

项 状态
上位机链路选型(10.8) ✅ 2026-09-06 用户拍板:方案 C,WiFi 常态 + USB CDC 兜底
OTA 升级 ✅ 2026-09-27 C2 完成:双槽 + 失败回滚,定稿见 §10.8「C2 定稿的 OTA」
WiFi 掉线重连策略 指数退避 + 上限;重连过程不得阻塞 t_safety(10.3),链路任务优先级最低就是为这个
MQTT broker 与 topic 2026-09-27 起由本项目自定(D7,broker 在 home 服务器);topic 约定随 A14 写进 §10.8
时间同步 当前方案是上位机下发墙钟做偏移,不加 RTC。若日志要脱机长期归档需重议
FR-12 电量曲线 建议首版不做,只留接口(10.11)
input_suspend 节点路径 与 SW-7 / V-8 是同一件事,首台机上确认后写死

11. 约束与假设

11.1 硬约束

ID 约束
C-1 全部为 1S 机型,负载固定为小米8(dipper)主板
C-2 原厂电池保护板被旁路 ⇒ 失去一层与本板独立的防线,全部安全责任落在 SR-2 / 3 / 5 上
C-3 无电池 ⇒ 失去一个内阻只有几十 mΩ 的缓冲电容。瞬态阻抗设计(§6)比电压精度重要得多,是这类板子的主要故障来源
C-4 ESP32-S3-WROOM-1 可用 IO 有限(19 根信号已排满),新增功能只能走移位寄存器或 I²C 扩展
C-5 USB2517I 要求 D± 走线 ≤ 6 inch(152 mm) ⇒ Hub 只能放在载板通道正中
C-6 BM24(尾插)连接器只有 10 次插拔寿命 ⇒ 装配流程必须减少反复插拔
C-7 小米8 原理图 PDF 是第三方扫描件,一切引脚 / 机械结论以实测为准

11.2 假设(若不成立需重新评估)

ID 假设
A-1 上游 USB 恒为 5 V(只接电脑 / Hub,不接快充头)
A-2 手机关机待机电流在 ≈50 µA 量级
A-3 目标 ROM 上存在可写的「关充电」sysfs 节点(SW-7,路径待确认)
A-4 手机常态功耗 0.3 ~ 1 A / 路,五路同时 4 A 满载不会发生

12. 验收准则

12.1 首板上电验收(17 步,严禁第一次上电就接手机)

完整步骤与通过标准见设计文档 §11。其中:

  • 第 5 步(人为断开 R2,模拟致命故障) 与 第 6 步(负载阶跃 + 阻尼验证) 是这块板子的两个核心验证项,不得跳过 —— 它们分别验证 SR-3 和 PR-12 / C-3。
  • 第 12 步(ESP32 复位安全测试) 验证 SR-6。
  • 第 14 步(USB 反灌测试) 验证 SR-10 / SW-8。

12.2 需求 → 验收方法对照

需求 验收方法 通过标准
PR-4 / PR-5 扫 DAC 全量程 输出扫过 3.52 ~ 4.01 V,与公式误差 <1%;任何码值都到不了 4.2 V
SR-2 固件写越界值 输出被拓扑限死,不越过上限
SR-3 / SR-4 人为断开反馈下电阻 eFuse 断开,输出侧尖峰不超过 4.7 V
SR-5 注入 +150 / −300 mV 偏差 20 ms 内关路并产生告警记录
SR-6 手动复位 ESP32 示波器确认五路立即关断、15 路按键无一误触发、EDL 无触发
SR-7 固件同时置位 EN 与放电 硬件互斥,10 Ω 上无持续功耗
SR-9 可调电源把母线升到 13.5 V 泄放导通并拉回 13.2 V 以下
SR-10 / SW-8 不关充电直接接 USB 电流转负 → 固件 200 ms 内断 VBUS
SR-15 排线加 4.40 V 不断开(断开说明焊到了 B+/B−)
PR-12 0→4 A 阶跃(带 20 cm 线 + 电容组) 压降 <300 mV,无 6 kHz 振铃
PR-13 满载 4 A 跑 30 min 温升 <40 K
PR-14 断电重启时序 输出 200 ms 内 <0.5 V
FR-4 / SW-5 逐项试按键 短按唤醒 / 长按 10 s 强制重启 / VOL−+PWR 进 Fastboot 均有效
FR-5 / SW-6 下发 EDL 序列 主机可见 9008 设备
FR-7 五台全接 adb devices 与 fastboot devices 均可见
FR-10 / TR-6 拔掉一个风扇 TACH 频率下降约 1/5,产生告警
SW-12 断开 WiFi(拔 AP / 关热点),从 USB 下命令 命令照常执行;五路输出不受影响(10.2 的失联行为)
SW-12 同一序号的命令从两条链路各发一遍 只执行一次,第二条被判重丢弃并记事件
总体目标 五台长稳 24 h 无重启、无告警

12.3 装配前必做的实物验证(阻塞项)

项 内容 出处
V-1 排线旁路验证:确认焊在 P+/P−,4.40 V 不断开 §13.7
V-2 侧键触点定位:万用表 5 分钟,拍照存档 §13.3
V-3 环路补偿实测扫频(三个位置已留焊盘) §9.2
V-4 量两个新连接器实物高度(XT30PW-M / 2510-3A)—— 余量只有 1~3 mm,别信估计值 §26.16
V-5 两条排线路径试装(电池 / 尾插穿槽走背面) §25.16
V-6 J7n 镜像位按底层装法复做;装好后做两点通断复核 §24.3 / §25.16
V-7 风扇线装配顺序:必须「先插五根风扇线,再合上载板」 §26.16
V-8 确认目标 ROM 上 input_suspend 的实际路径 §15.2

完整的待办 / 已关闭清单以设计文档 §12 为准,本表只列对需求验收有约束的。


13. 已知风险与已接受的取舍

# 风险 处置
R1 去掉原厂保护板 = 少一层独立防线 已接受。定量核算 eFuse 余量 ≈11 倍;且 eFuse 断开后 2200 µF 仍撑着,手机继续运行不掉电,表现优于保护板断开
R2 最坏角落下 eFuse 可能到 4.519 V 才跳,超 1S 上限约 60 mV 已接受。走到这一步说明第一层已失效,输出以 6.4 mV/µs 冲过这 70 mV 只需 ≈10 µs,远短于锂电过压损伤时间常数
R3 两个同款 USB-C 插错 已接受(用户在 USB-B / USB-C 之间选了 C)。靠丝印 + 颜色 + 外壳标记缓解
R4 低边调速导致 PWM 关断期间读不到转速 已接受。若最终选 4 线 PWM 风扇,MOS + 续流管 + 分压可全删
R5 IF-5 的 100 µF MLCC 在 3.9 V 偏压下只剩 ≈40 µF,中频储能从 200 µF 降到 ≈80 µF 待复核。严格照原方案需换真聚合物
R6 电感 SPM6530T-4R7M 的 Isat 6.2 A < 原定的 7 A 已接受,裕量分析见设计文档「已知遗留」
R7 BM24 仅 10 次插拔寿命 靠装配流程规避(C-6)
R8 手机主板机械固定方式(是否连中框)未定 不阻塞打样

14. 需求 → 设计追溯表(速查)

需求域 设计文档章节
规格 / 输入输出 §1、§3
电压范围与本质安全 §4(含 §4.4 失效态验算)
三层保护链 §5
瞬态阻抗与阻尼(C-3) §6
断电重启与放电 §7
MCU 接口 / GPIO / I²C §8、§26.18
选型与 BOM §9
PCB 与叠层 §10、§25、§26
验收与调试 §11、§14
手机侧接口 §13
USB 子系统 §15、§18、§21
两板架构与机械 §22、§25
散热 §22.6
EDL §22.4
软件架构 设计文档里没有对应章节 —— 见本文 §10

15. 术语

术语 含义
直供 拆掉电芯与保护板,从电池座直接供给受控电压
dipper 小米8 的代号
P+ / P− 电池保护板的输出侧焊点(本项目的接入点)
B+ / B− 电芯侧焊点(接错这里是本项目最隐蔽的装配错误)
eFuse 带可调过压 / 限流 / 缓启动的电子保险丝(TPS25940A)
EDL / 9008 高通紧急下载模式
本质安全 靠拓扑与元件取值在物理上限死危险状态,而不是靠软件判断
下板 / 载板 下板 = 电源板;载板(上板)= 承载五块手机主板的板