- 硬重启等到 PG 后长按电源键 5 s(新状态 hr-boot):电池端上电的小米8 停在关机态,原来的 power_cycle 等于关机。ACT_KEY_MS_ON 2000 → 5000 (实测 2 s 松手即掉电)。NO_PG 时不按。 - 修死锁:执行器持 s_mtx 等 esp-mqtt 的 api_lock,esp-mqtt 任务持 api_lock 分发 DISCONNECTED 时等 s_mtx。热点一断 USB/WiFi 全失联、按键可能松不开。 执行器现在只往 s_outbox 拷贝,由 t_link 交给 esp-mqtt。 - SR-5 欠压一侧 −100 → −250 mV(T_SAFETY_UNDER_MV),过压仍 +100 mV: 带屏开机峰值 2.1 A 时实测 −115 mV 被误关。需求 SR-5 / SW-2 / SW-5 / 验收表同步。 板 B 上板:power_cycle → hr-boot → 手机开机,最低 3778 mV / 2.12 A 不再跳闸。 Co-Authored-By: Claude Opus 5.5 <[email protected]>
124 行
12 KiB
Markdown
124 行
12 KiB
Markdown
# 交接
|
||
更新:2026-09-28 固件 **0.1.1-c5**(硬重启末尾自动开机 + MQTT 发布死锁 + 欠压放宽到 −250 mV);板 B 经 USB 整片烧上,WiFi / MQTT 已通
|
||
|
||
## c4:两处修复(2026-09-28,第一片刀片 + 小米8 上板时发现)
|
||
- **硬重启后手机不开机**:电池端上电的小米8 停在关机态(只漏 ~2 mA)。现在 `power_cycle` 等到 PG 后
|
||
**长按电源键 5 s**(新状态 `hr-boot`,`ACT_HR_BOOT_KEY_MS`);NO_PG 时不按。`ACT_KEY_MS_ON` 2000 → **5000**:
|
||
实测按 2 s 时 PMIC 起来了(415 mA),一松手就掉回 1 mA。需求 SW-5 已同步。rack 的二级恢复(power_cycle)因此真的能拉起手机了。
|
||
上板:power_cycle → 4.1 s 上电 → hr-boot 5 s → res ok,手机进入开机电流曲线。
|
||
- **热点一断整板失联(死锁)**:执行器「持 `s_mtx` → `esp_mqtt_client_enqueue` 等 `api_lock`」,
|
||
esp-mqtt 任务「持 `api_lock` 分发 DISCONNECTED → `on_mqtt` 的 `ST_EDIT` 等 `s_mtx`」。
|
||
现象:USB 命令无应答、WiFi 不重连、遥测停,只有 t_safety / t_thermal 的日志还在 —— **按到一半的键也会松不开**。
|
||
修:执行器只往 `s_outbox`(深度 4)拷一份,`t_link` 里 `outbox_drain()` 交给 esp-mqtt;持 `s_mtx` 时不调 esp-mqtt。
|
||
⚠️ 死锁状态下 OTA 进不去,只能 USB 烧(esptool `--before usb_reset`,第一次常报 ClearCommError,再跑一次就过)。
|
||
- **c5:带载欠压误关**。小米8 插屏幕后开机峰值 ~2.1 A,轨 1 读回 3785 mV(设定 3900,−115 mV),SR-5 的 −100 mV 直接关路。
|
||
用户拍板:**欠压一侧放宽到 −250 mV**(`T_SAFETY_UNDER_MV`),过压仍 +100 mV;需求 SR-5 / SW-2 / 验收表同步。
|
||
压降 ≈ 55 mΩ × 2 A 的来源(Buck 负载调整 / eFuse / 采样电阻 / 走线)还没拆开查。
|
||
|
||
## 一句话现状
|
||
OTA 换固件 / 超时回滚现在走 `warm_restart()`(`main/warm_esp.c`):目标固件认接管 ⇒ 手机不断电;
|
||
崩溃 / 看门狗 / 上电 / 兜底回滚仍全关(SR-6)。**26 个 ctest、448 个用例全绿**。板 B 跑 **0.1.1-c3**(经 OTA 推上去的)。
|
||
定稿写在需求 §10.9「重启不断电」。C2 OTA 的说明在下面。
|
||
|
||
## 0.1.1:I²C 不应答被报成 STATE(真板独有,sim 测不出)
|
||
- 根因:IDF 5.4 的 `i2c_master_transmit/receive/transmit_receive` 在**从机 NACK / SCL 超时**时返回 `ESP_ERR_INVALID_STATE`,
|
||
HAL 照翻成 `HAL_ERR_STATE`(= 没 init)。于是:I2C_LOST 事件的错误码是 5;命令层 `from_hal` 又把 STATE 当成「执行器忙」⇒
|
||
PCA9555 不应答时 `power off` 回 `busy`(eFuse 其实已经关了、意图也清了),上位机会以为没执行。
|
||
- 修:`hal_i2c_esp.c` 传输函数用 `map_xfer_err`(INVALID_STATE → NAK);`cmd_exec.c` 新增 `from_bus`,
|
||
I²C / GPIO 路径(set_voltage / vbus / power)失败一律 `io`,`from_hal` 只留给执行器。
|
||
- 测试:sim 加 `sim_i2c_inject_err_ch()`(指定注入的错误码);`test_cmd` 加一段 STATE 注入 ⇒ 必须是 io 不是 busy
|
||
(把关电那行改回 from_hal 时这段确实红,回应就是台架上看到的 `{"err":"busy"}`)。
|
||
- 上板(台架镜像,CH1 强制在位):`power off` 回 `io`、CH1 关掉;I2C_LOST 的错误码 5 → **2**。
|
||
|
||
## 重启不断电(c3)
|
||
- 三道闸:RTC 标记(软件复位 + 魔数 / 反码,一次性)· 目标镜像能力标记(第 288 字节 `PRCAPS1`,老固件不认就不锁)· 起来后读 EN 真电平核对。
|
||
- `rail_en_init_keep(adopt)` **五根一律放开 hold**:崩溃时上一条命的锁可能还在。
|
||
- `pca9555_init_keep()`:方向寄存器是我们写的才留 VBUS;键 / EDL 清零。
|
||
- 实测:台架镜像(空槽强制 CH1 在位,**没提交**)开 CH1 → OTA 带重启 → 事件 `18/7 mv=1`(新固件读到 IO9 仍为高、接管)。
|
||
⚠️ 没测到的:示波器上看 EN 在重启期间有没有毛刺(`warm_take` 读到高只能说明「起来时还是高」);
|
||
「锁着 + 非计划复位」这条路径只有宿主机测试(真板上造不出来)。插上刀片 + 手机后要看一次手机有没有掉电重启。
|
||
- **第一次从 c2 升到 c3 仍会断电一次**:锁 EN 的是**正在跑**的固件,c2 不会锁。
|
||
|
||
## 下一步(没人定,候选)
|
||
- 示波器验证:插一片刀片,CH 开着,OTA 带重启,抓 EN / 输出电压(`scope_*` 工具)。
|
||
- 在手机上看一眼 rack 新界面(390 px 左右):无头 Chrome 最窄只到 ~500 px,窄屏只验到那里。
|
||
- 下面「其它挂着的事」里 A15 / A16 留下的几条(I²C 总线挂死恢复、PD 降载、风扇阈值实测)照旧。
|
||
|
||
## rack 侧(服务器前后端,详见 `rack/README.md`)
|
||
- **固件页**(顶栏「固件」):上传 `build/phonerail.bin` → 选板推送;上传时核镜像头(非本工程 / 非 app 镜像 / >3 MB 直接拒),
|
||
显示每个镜像「重启不断电:支持 / 不支持」,勾「立刻重启」时按它给不同提示。通用 `/cmd` 不放行 `ota`(URL / 哈希只能由固件库生成)。
|
||
- **下载目录自动清理**(每小时):库里登记的不删;`ota_push.py` 直传的放满 7 天删;半截 `.tmp` 1 小时删。
|
||
- **界面改版**:仪器前面板风格 + 暗色模式(跟随系统);总览每个槽带最近两分钟电流小曲线(平线 = 疑似卡死)。
|
||
- **底板温度曲线**(单板页):两路 NTC + 风扇占空比,10 分钟 ~ 7 天;后端 `board_tele_min` 分钟聚合留 90 天,
|
||
接口 `GET /api/boards/{id}/history`。NTC1 贴 Hub 稳压 U31,NTC2 贴输入降压 U37。
|
||
|
||
## 踩到的坑(c3)
|
||
- `.rodata_custom_desc` 没人引用会被 `--gc-sections` 丢掉 ⇒ `main/CMakeLists.txt` 加 `-u g_phonerail_caps`。
|
||
- `PROJECT_VER` 在 CMake 配置时读 ⇒ 改 `phonerail.h` 不会重配,镜像头版本会是旧的 ⇒ 顶层加了 `CMAKE_CONFIGURE_DEPENDS`。
|
||
- 撤掉台架补丁后要**重新编译**再推:`build/phonerail.bin` 还是台架版(这次推错了一次,已换回)。
|
||
- Git Bash heredoc 里的 Python 写 `\0` 又变成真 NUL 了(这次在 rack/firmware.py)—— 含反斜杠的一律写 .py 文件。
|
||
|
||
## 怎么推固件
|
||
```
|
||
python tools/ota_push.py b81f3fc9c8bc # 推 build/phonerail.bin:只下载 + 校验 + 切分区,下次复位生效
|
||
python tools/ota_push.py b81f3fc9c8bc --reboot # 校验过就立刻重启(板上 ≥c3 且目标镜像认接管 ⇒ 手机不断电,否则五路全断一次)
|
||
python tools/ota_push.py b81f3fc9c8bc --status
|
||
```
|
||
它把镜像传到服务器 `/root/phonerail/fw/<sha 前 16 位>.bin`(nginx 7765,只放行这种文件名,不过门户;
|
||
**不进固件库,7 天后 rack 后端自动删** —— 要长期留着就从界面上传),
|
||
再在服务器上用后端账号经 1884 发命令、盯遥测。
|
||
**rack 管理界面「固件」页也能推**(2026-09-27 加,上传 → 选板 → 可选立刻重启并二次确认;见 `rack/README.md`「推固件」)。
|
||
|
||
|
||
## C2 改了什么
|
||
- `partitions.csv` 重排(16 MB):**nvs 0x9000 没动**;ota_0 / ota_1 各 3 MB;otadata 0x610000;evlog 挪到 0x612000(历史事件清空了一次)。
|
||
- `sdkconfig.defaults` + `sdkconfig`:flash 16 MB(之前误写 8 MB);`CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y`(bootloader 配置,只有 USB 整片烧才生效)。
|
||
- `main/ota.{h,c}` **新**(宿主机可测):URL / 哈希校验、判定(busy / 待验证拒 / 同镜像不重下)、健康判据。
|
||
- `main/ota_esp.{h,c}` **新**:`t_ota` 任务(esp_http_client + mbedtls sha256 + `OTA_WITH_SEQUENTIAL_WRITES`,每块让一个 tick)、
|
||
核 project_name、切分区;`t_link` 里判健康 / 到点重启(先等 2 s 让回应发出去、事件落盘);esp_timer 150 s 兜底回滚。
|
||
- `cmd` 新命令 `ota`=13(`cmd_net.c` 的 `do_ota`);status 多 `ota` 一块;`evlog` 新事件 `LOG_EV_OTA`=18。
|
||
- 顶层 `CMakeLists.txt`:`PROJECT_VER` 取 `PHONERAIL_FW_VERSION`(否则镜像头版本是 git describe,回滚后 `ota.bad` 报 `79c9f8b-dirty`)。
|
||
- `test/test_ota.c` **新**(13 例);`test_cmd` / `test_evlog` 的冻结数字表各加一行。
|
||
- `tools/ota_push.py` **新**;`rack/deploy/nginx-phonerail-fw.conf` **新**(已部署);`rack/backend/prrack/protocol.py` 加事件 18 / 命令 13(已部署)。
|
||
|
||
## 上板实测(板 B,五槽空)
|
||
```
|
||
正常:c2 → c2-t1 经 MQTT --reboot:下载 ≈8 s,推送到自证健康 19 s;事件 18/1 → 18/2 → 18/6 → BOOT → 18/4
|
||
哈希错:err "hash"、got 996064、固件不变;下载 20 s 里 t_safety skipped 0
|
||
回滚:服务器删掉板子 broker 账号 → USB 发同镜像 reboot:true → 起在 c2-rb(pend true)→ 120 075 ms 记 18/5 → 回到 c2-t1、bad 报版本
|
||
(账号已原样恢复,备份已删)
|
||
收尾:c2-t1 → 正式 0.1.0-c2 经 MQTT --reboot 成功
|
||
```
|
||
|
||
## 踩到的坑(只写会再犯的)
|
||
- **路由器不是整机 DMZ**:新开的 7765 公网 TCP 都连不上,用户手动加了转发才通(本机测公网要 `curl --noproxy '*'`,
|
||
否则 7890 代理回的 502 会被当成服务器的)。
|
||
- 本机 PowerShell `Set-Content -Encoding ascii` 改 `phonerail.h` 把中文注释写成 `??`(已 `git checkout` 还原)—— 改它一律用 sed。
|
||
- Git Bash heredoc 里的 Python 写 `'\0'` 会变成真 NUL 字节进源码;含反斜杠的替换写成 .py 文件再跑。
|
||
- 宿主机 `bash host/test.sh` 用 `-j 4` 在 cygwin 上偶发「link: Subprocess aborted」,`cmake --build build -j 1` 就过。
|
||
|
||
## 其它挂着的事
|
||
- **风扇还没下单购买**(2026-09-27):板 B 两路报 `rpm 0` + 停转是预期的。到货插上后看 `status.fan.rpm`;在转却仍是 0 再查 Q9/Q10 与 PCNT。
|
||
- 风扇闭环四个阈值 + 占空比下限 + PPR 都是「待上板实测」(t_thermal.h),手机上架跑满载之后再定。
|
||
- A15 留下的:I²C 总线挂死恢复(HAL 层);PD 上限的降载动作(§8.3);CH224A 读到 100 W 与早上记的 20V/3A 不一致,待核。
|
||
|
||
## 本次新增/修改的文件
|
||
- `main/ntc.{h,c}` **新**(宿主机可测)— mV → Ω → 查 Murata NCP18XH103F03RB 的 R-T 表(−40~125 °C、5 °C 一格、格内线性插值);
|
||
贴 +3V3 / 贴地 / 表外一律「读不到」(⇒ t_thermal 满速)
|
||
- `main/thermal_esp.{h,c}` **新**(只进 IDF)— ADC1 oneshot + 曲线拟合校准(8 次平均,引脚反查通道);两个 PCNT 单元数 TACH 上升沿(累计计数、1 µs 毛刺滤波)
|
||
- `main/app_main.c` — `task_thermal` 任务体(每 10 s 打一行温度 / mV / 占空比 / 转速);`thermal_esp_init()` + `t_thermal_init()` 放在**上电恢复之前**;`cmd_env.thermal = &s_therm`
|
||
- `main/CMakeLists.txt` REQUIRES 加 `esp_adc esp_driver_pcnt`;`host/CMakeLists.txt` 加 `ntc.c`
|
||
- `test/test_ntc.c` **新**(4 例:表点、⭐方向「越热电压越低」、⭐开路/短路不是温度、比例式);版本 `0.1.0-a16`
|
||
|
||
## 上板实测(板 B,五槽空,风扇状态未知)
|
||
```
|
||
status: "temp":[38249,34027] 分压端 1266 / 1384 mV ← 两路 NTC 读数合理(NTC1 在电感区更热)
|
||
"fan":{"duty":300,"en":true,"rpm":[0,0],"stall":[true,true]}
|
||
events: [31, 14318, 0, 11(FAN_STALL), 0] [32, …, 11, 1] ← 启动宽限 5 s + 一个窗口之后各报一次,不刷屏
|
||
```
|
||
|
||
## 踩到的坑(只写会再犯的)
|
||
- 比例式 NTC 的电源误差估算:+3V3 偏 1 % 在 25 °C 附近是 **~0.6 °C**(中点处电阻误差是电源误差的 2 倍、NTC 每度 ~4 %),不是 0.3 °C。
|
||
- PowerShell 5.1 的 `Set-Content -Encoding utf8` 会写 BOM —— 改 `phonerail.h` 这种文件用 sed,别用它。
|
||
- `python` 在 Git Bash 里是 Windows Store 占位程序,会挂住;一律用 `/c/Python314/python`。
|