固件 0.1.3-c5:硬重启开机校验 + 重按(0.1.2)、status 上报上游电脑在不在(host)

- 0.1.2:power_cycle 按完电源键后看电流,没开就重按,最多 3 次;新事件 19 BOOT_CHECK
- 0.1.3:UP_VBUS_DET(IO6)启动时配成输入,status 加 "host":true|false|null
- rack:单板页显示「上游:已连电脑 / 未连电脑」
- HANDOFF:损耗两档的来历(Hub 工作 ≈1.1 W)、板 B 第 2 槽 PRSNT2# 卡低

Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
zhou yang
2026-09-28 08:59:12 +08:00
共同撰写人 Claude Opus 5.5
父节点 830b4cdff8
当前提交 ef91daf07d
共修改 13 个文件,包含 834 行新增和 366 行删除
+23 -2
查看文件
@@ -1,5 +1,26 @@
# 交接
更新:2026-09-28 固件 **0.1.1-c5**(硬重启末尾自动开机 + MQTT 发布死锁 + 欠压放宽到 −250 mV);板 B 经 USB 整片烧上,WiFi / MQTT 已通
更新:2026-09-28 固件 **0.1.3-c5**(status 加 `host`);板 B 经 OTA 推上(热重启,第 1 路没断电)
## 0.1.3:上游电脑在不在进遥测(2026-09-28)
- `UP_VBUS_DET`(IO6,R721/R722 分压,R722 下拉)启动时配成输入;status 新增 `"host":true|false|null`(读不到 null)。
只进遥测、不参与判据。rack 单板页显示「上游:已连电脑 / 未连电脑」。
- 起因:面板「损耗」在 ~0.5 W / ~1.6 W 两档间跳,与刀片、手机无关;高档时 NTC1(U31 旁)高 6~10 °C ⇒ USB2517I 工作
(+ AMS1117 线性降压)约 1.1 W。⚠️ `host` 是 VBUS 在不在,不是 Hub 是否挂起:电脑休眠时多半仍是 true。
- ⚠️ 热重启后 DAC 设定点回默认(实测 set 4010 → 3900 mV,输出 4.04 → 3.93 V)—— 供电保住了,设定没保住。
- **板 B 第 2 槽 `PRSNT2#` 卡低**(09-28 02:09 第一次插刀片起,拔走后仍报在位,跨多次重启):硬件问题,待量 PRSNT2#(R607 / U20.9)对地、
看插座 B31 弹片。副作用:每圈 `i2c_err` +2;02:13~06:25 空槽 eFuse 开着约 4 h(见 0.1.4 的判据)。
## 0.1.2:硬重启按完电源键之后看电流,没开就重按(2026-09-28)
- **起因**:板 B 第 1 路 7 次 power_cycle 只开起来 1 次,失败时电流 1~2 mA、屏不亮;PCA9555 按键输出确实置位(命令都 ok),
**镊子短接 J603 能开** ⇒ R2 电源键是主板弹片压刀片铜盘(决策 14),接触时好时坏。这是机械问题,固件只做兜底。
- **做法**(`t_actuator`,新状态 `hr-verify`):按满 5 s → 松键 → **松键后 3~5 s** 看电流,连续 5 个样本(0.5 s)≥ 100 mA 算开了;
否则重按,**一共最多 3 次**;3 次都不行结果 `no-boot`,**电继续开着**。读不到电流不重按(对正在开机的手机再长按会进关机菜单)。
为什么 3 s 后才看:「按 2 s」那种 PMIC 醒了、SoC 没接住的情况,松手后 415 mA 撑约 3 s 才掉(宿主机测试把判定延迟改回 1 s 会被它骗)。
- 电流与 PG 一样从巡检快照喂进来(`t_actuator_set_boot_check()`,app_main 接);**不接就是 0.1.1 的行为**,老测试一行没改。
- 新事件 **19 `LOG_EV_BOOT_CHECK`**「开机重按」:arg 低 4 位 = 按了几次,bit7 = 最终没开起来;一次就开的不记。
rack 已同步(名称「按了 N 次,开起来了 / 没开起来」,只有没开起来标红),已部署。
- 手动 `key` 命令不校验、不重按(人的意图)。宿主机 26 ctest 全绿(`test_t_actuator` 30 → 37 例),rack 43 例全绿。
- ⚠️ 实机只验到「第一下就开」(接触已恢复),重按路径靠宿主机测试。频繁出现事件 19 = 那一台弹片要处理。
## c4:两处修复(2026-09-28,第一片刀片 + 小米8 上板时发现)
- **硬重启后手机不开机**:电池端上电的小米8 停在关机态(只漏 ~2 mA)。现在 `power_cycle` 等到 PG 后
@@ -17,7 +38,7 @@
## 一句话现状
OTA 换固件 / 超时回滚现在走 `warm_restart()`(`main/warm_esp.c`):目标固件认接管 ⇒ 手机不断电;
崩溃 / 看门狗 / 上电 / 兜底回滚仍全关(SR-6)。**26 个 ctest、448 个用例全绿**。板 B 跑 **0.1.1-c3**(经 OTA 推上去的)。
崩溃 / 看门狗 / 上电 / 兜底回滚仍全关(SR-6)。**26 个 ctest、448 个用例全绿**。板 B 跑 **0.1.2-c5**(经 OTA 推上去的)。
定稿写在需求 §10.9「重启不断电」。C2 OTA 的说明在下面。
## 0.1.1:I²C 不应答被报成 STATE(真板独有,sim 测不出)
+25
查看文件
@@ -100,6 +100,30 @@ static bool act_pg_from_safety(void *ctx, int rail, bool *pg)
return true;
}
/* 喂给 t_actuator 开机校验的电流(0.1.2)。同上,读不到就是读不到。 */
static bool act_ua_from_safety(void *ctx, int rail, int32_t *ua)
{
(void)ctx;
if (rail < 1 || rail > PHONERAIL_RAIL_COUNT || !s_snap.rail_ok[rail - 1]) {
return false;
}
*ua = s_snap.rail[rail - 1].current_ua;
return true;
}
/* 开机校验的结论 → 事件(evlog.h `LOG_EV_BOOT_CHECK`)。一次就开的不记。 */
static void act_boot_result(void *ctx, int rail, bool booted, uint8_t presses, int32_t ua)
{
(void)ctx;
if (booted && presses <= 1) {
return;
}
const uint8_t arg = (uint8_t)((presses & 0x0Fu) | (booted ? 0u : 0x80u));
evlog_put((uint8_t)rail, LOG_EV_BOOT_CHECK, arg, s_snap.rail[rail - 1].bus_mv, ua);
ESP_LOGW(TAG, "CH%d 开机校验:按了 %u 次,%s(%ld uA)", rail, (unsigned)presses,
booted ? "开起来了" : "没开起来", (long)ua);
}
/* A10 的事件出口,A11 把它接到了环形缓冲上(§10.7:`t_safety` 只入队、不落盘)。
`evlog_put_safety()` 自己就是 `safety_ev_fn` 的形状,这里包一层只为了顺带
打一行串口日志 —— 板子插着 USB 调试时,事件同时进日志流与 flash。
@@ -420,6 +444,7 @@ void app_main(void)
⚠️ 必须在 `spawn(TASK_SAFETY, ...)` 之前:判据要关路时会先 `t_actuator_abort()`,
而没 init 的执行器只会直接返回,那时序列还在跑,电关掉几秒又会被抬回来。 */
t_actuator_init(act_pg_from_safety, NULL);
t_actuator_set_boot_check(act_ua_from_safety, act_boot_result, NULL);
/* 命令处理器(A12)。status 读巡检与热管理两份快照。
`power` 命令的意图经 `want_save` 存 NVS(A15)。
+6
查看文件
@@ -209,6 +209,12 @@ hal_err_t bringup_devices(const bringup_in_t *in, bringup_report_t *r)
hub_pulse();
}
/* `UP_VBUS_DET`:上游 USB-C 的 VBUS 经 R721/R722 分压进来,高 = 电脑插着。
板上 R722 下拉到地,所以不开内部上下拉。只进遥测(status 的 `host`),不参与任何判据:
Hub 连着电脑时比挂起多吃约 1 W,没有它面板上的「损耗」会无缘无故跳一档。
失败不 abort —— 读不到时 status 报 null。每次启动都配:配成输入没有副作用。 */
(void)hal_gpio_config(BOARD_PIN_UP_VBUS_DET, HAL_GPIO_INPUT);
/* 锁必须早于任何一个用总线的人(R2 之后全系统只剩这一把)。 */
(void)hal_lock_init();
const hal_i2c_cfg_t i2c = {
+10
查看文件
@@ -6,8 +6,10 @@
上位机的卡死判据(电流曲线)拿 0 当真值会直接误判。 */
#include <stdio.h>
#include "board_pins.h"
#include "cmd_priv.h"
#include "evlog.h"
#include "hal_gpio.h"
#include "hal_time.h"
#include "ota.h"
#include "pca9555.h"
@@ -101,6 +103,14 @@ void cmd_report_status(cj_writer_t *w, const cmd_env_t *env)
{
const safety_snap_t *s = env->safety;
cj_w(w, ",\"fw\":\"%s\",\"up\":%lu", phonerail_version(), (unsigned long)hal_time_now_ms());
/* 上游 USB-C 插没插电脑(`UP_VBUS_DET`,bringup 里配的输入)。读不到写 null。
⚠️ 这是 VBUS 在不在,不是 Hub 有没有枚举 / 挂起:电脑休眠时 VBUS 多半还在。 */
int host = 0;
if (hal_gpio_read(BOARD_PIN_UP_VBUS_DET, &host) == HAL_OK) {
cj_w(w, ",\"host\":%s", b(host != 0));
} else {
cj_w(w, ",\"host\":null");
}
if (s != NULL) {
if (s->input_ok) {
cj_w(w, ",\"in\":{\"mv\":%ld,\"ua\":%ld}", (long)s->input.bus_mv,
+1
查看文件
@@ -573,6 +573,7 @@ const char *evlog_ev_str(log_ev_t ev)
case LOG_EV_PD_LIMIT: return "PD限功率";
case LOG_EV_PG_TIMEOUT: return "上电等不到PG";
case LOG_EV_OTA: return "固件升级";
case LOG_EV_BOOT_CHECK: return "开机重按";
case LOG_EV_COUNT: break;
}
return "?";
+7
查看文件
@@ -81,6 +81,13 @@ typedef enum {
current_ua = 镜像字节数。整板级(rail = 0)。 */
LOG_EV_OTA = 18,
/* 0.1.2 新增:硬重启按电源键之后的开机校验(t_actuator.h `ACT_HR_BOOT_TRIES`)。
只在「重按过」或「最终没开起来」时记,一次就开的不记(每次硬重启都记一条太吵)。
arg:bit0~3 = 一共按了几次,bit7 = 1 最终没开起来(手机电流始终 < 100 mA)。
bus_mv = 判定时这一路电压,current_ua = 判定时电流。
频繁出现 = 这一台的电源键弹片和刀片铜盘接触不良(2026-09-28 板 B 第 1 路)。 */
LOG_EV_BOOT_CHECK = 19,
LOG_EV_COUNT,
} log_ev_t;
+1 -1
查看文件
@@ -3,7 +3,7 @@
#define PHONERAIL_H
/* 固件版本。规则:<主>.<次>.<修订>-<最后完成的任务包> */
#define PHONERAIL_FW_VERSION "0.1.1-c5"
#define PHONERAIL_FW_VERSION "0.1.3-c5"
/* 通道数。五路直供,见设计文档 §2。 */
#define PHONERAIL_RAIL_COUNT 5
+108 -12
查看文件
@@ -20,11 +20,18 @@ typedef struct {
act_result_t result;
uint32_t done;
uint32_t cleanup_retries;
uint8_t presses; /* HR:这一次硬重启已经按了几次电源键 */
uint8_t good_n; /* HR_VERIFY:连续几个样本电流达标 */
bool ua_seen; /* HR_VERIFY:这一轮窗口里读到过电流没有 */
int32_t last_ua; /* HR_VERIFY:最近一个读到的样本 */
} act_rail_t;
static act_rail_t s_rail[PHONERAIL_RAIL_COUNT];
static act_pg_fn s_pg;
static void *s_pg_ctx;
static act_ua_fn s_ua;
static act_boot_fn s_boot_done;
static void *s_boot_ctx;
static bool s_inited;
/* ---- 小工具 -------------------------------------------------------------- */
@@ -67,12 +74,46 @@ static bool pg_now(int rail, bool *pg)
return s_pg(s_pg_ctx, rail, pg);
}
static bool ua_now(int rail, int32_t *ua)
{
if (s_ua == NULL) {
return false;
}
return s_ua(s_boot_ctx, rail, ua);
}
static void enter(act_rail_t *r, act_state_t st, uint32_t now)
{
r->state = st;
r->t0_ms = now;
}
/* HR:按下电源键开机(HR_PG 第一次、HR_VERIFY 重按走同一个口)。
和 KEY 序列一样:先记 release_mask 再看写没写成 —— 写失败时器件里那个字节
是未知的,照样走 CLEANUP 把它显式清一遍。返回 false = 写失败,已 finish(IO)。 */
static void finish(act_rail_t *r, act_result_t res, uint32_t now);
static bool hr_press(act_rail_t *r, int rail, uint32_t now)
{
const uint8_t mask = key_out_mask(ACT_KEYS_PWR);
r->release_mask = mask;
r->hold_ms = ACT_HR_BOOT_KEY_MS;
r->presses++;
if (out_write(rail, mask, mask) != HAL_OK) {
finish(r, ACT_RES_IO, now); /* 电是开着的,只是没按上键 */
return false;
}
enter(r, ACT_ST_HR_BOOT, now);
return true;
}
static void boot_report(act_rail_t *r, int rail, bool booted)
{
if (s_boot_done != NULL) {
s_boot_done(s_boot_ctx, rail, booted, r->presses, r->last_ua);
}
}
/* 序列结束(成功、超时、出错、被打断走的是同一条路)。
按下去过东西就必须经 CLEANUP,那是「键一定会松开」的唯一保证。 */
static void finish(act_rail_t *r, act_result_t res, uint32_t now)
@@ -122,18 +163,8 @@ static uint32_t state_step(act_rail_t *r, int rail, uint32_t now)
case ACT_ST_HR_PG: {
bool pg = false;
if (pg_now(rail, &pg) && pg) {
/* 电好了 ⇒ 长按电源键开机(ACT_HR_BOOT_KEY_MS 的注释)。
和 KEY 序列一样:先记 release_mask 再看写没写成 —— 写失败时器件里那个字节
是未知的,照样走 CLEANUP 把它显式清一遍。 */
const uint8_t mask = key_out_mask(ACT_KEYS_PWR);
r->release_mask = mask;
r->hold_ms = ACT_HR_BOOT_KEY_MS;
if (out_write(rail, mask, mask) != HAL_OK) {
finish(r, ACT_RES_IO, now); /* 电是开着的,只是没按上键 */
return ACT_CLEANUP_RETRY_MS;
}
enter(r, ACT_ST_HR_BOOT, now);
return ACT_HR_BOOT_KEY_MS;
/* 电好了 ⇒ 长按电源键开机(ACT_HR_BOOT_KEY_MS 的注释)。 */
return hr_press(r, rail, now) ? ACT_HR_BOOT_KEY_MS : ACT_CLEANUP_RETRY_MS;
}
if (el >= ACT_HR_PG_TIMEOUT_MS) {
/* ⚠️ 电**继续开着**。见 ACT_RES_NO_PG 的注释:把「看不见」当成
@@ -146,6 +177,57 @@ static uint32_t state_step(act_rail_t *r, int rail, uint32_t now)
}
case ACT_ST_HR_BOOT:
if (el < r->hold_ms) {
return r->hold_ms - el;
}
if (s_ua == NULL) {
finish(r, ACT_RES_OK, now); /* 不校验(0.1.1 行为):CLEANUP 松键 */
return ACT_CLEANUP_RETRY_MS;
}
/* 要校验:先在这里松键(松不掉就走 CLEANUP 的「不放弃」,不再校验)。 */
if (out_write(rail, r->release_mask, 0) != HAL_OK) {
finish(r, ACT_RES_IO, now);
return ACT_CLEANUP_RETRY_MS;
}
r->release_mask = 0;
r->good_n = 0;
r->ua_seen = false;
enter(r, ACT_ST_HR_VERIFY, now);
return ACT_HR_VERIFY_DELAY_MS;
case ACT_ST_HR_VERIFY: {
if (el < ACT_HR_VERIFY_DELAY_MS) {
return ACT_HR_VERIFY_DELAY_MS - el;
}
int32_t ua = 0;
if (ua_now(rail, &ua)) {
r->ua_seen = true;
r->last_ua = ua;
r->good_n = ua >= ACT_HR_BOOT_UA_MIN ? (uint8_t)(r->good_n + 1) : 0;
if (r->good_n >= ACT_HR_VERIFY_GOOD_N) {
boot_report(r, rail, true);
finish(r, ACT_RES_OK, now);
return ACT_IDLE_WAIT_MS;
}
}
if (el < ACT_HR_VERIFY_DELAY_MS + ACT_HR_VERIFY_WINDOW_MS) {
const uint32_t left = ACT_HR_VERIFY_DELAY_MS + ACT_HR_VERIFY_WINDOW_MS - el;
return left < ACT_HR_VERIFY_POLL_MS ? left : ACT_HR_VERIFY_POLL_MS;
}
if (!r->ua_seen) {
/* 整个窗口都读不到电流:判不了,**不重按**(ACT_HR_BOOT_TRIES 那段注释)。 */
finish(r, ACT_RES_OK, now);
return ACT_IDLE_WAIT_MS;
}
if (r->presses < ACT_HR_BOOT_TRIES) {
return hr_press(r, rail, now) ? ACT_HR_BOOT_KEY_MS : ACT_CLEANUP_RETRY_MS;
}
/* ⚠️ 电**继续开着**:关掉也不会让弹片压上,反而丢了人工到场前的现场。 */
boot_report(r, rail, false);
finish(r, ACT_RES_NO_BOOT, now);
return ACT_IDLE_WAIT_MS;
}
case ACT_ST_KEY_HOLD:
if (el < r->hold_ms) {
return r->hold_ms - el;
@@ -213,9 +295,19 @@ void t_actuator_init(act_pg_fn pg, void *ctx)
memset(s_rail, 0, sizeof(s_rail));
s_pg = pg;
s_pg_ctx = ctx;
s_ua = NULL;
s_boot_done = NULL;
s_boot_ctx = NULL;
s_inited = true;
}
void t_actuator_set_boot_check(act_ua_fn ua, act_boot_fn done, void *ctx)
{
s_ua = ua;
s_boot_done = done;
s_boot_ctx = ctx;
}
hal_err_t t_actuator_hard_reset(int rail)
{
if (!s_inited) {
@@ -244,6 +336,8 @@ hal_err_t t_actuator_hard_reset(int rail)
}
r->seq = ACT_SEQ_HARD_RESET;
r->release_mask = 0; /* 断电这一段没按任何东西;开机那一下在 HR_PG 里才记 */
r->presses = 0;
r->last_ua = 0;
r->result = ACT_RES_NONE;
enter(r, ACT_ST_HR_OFF, hal_time_now_ms());
return HAL_OK;
@@ -368,6 +462,7 @@ const char *t_actuator_state_str(act_state_t st)
case ACT_ST_HR_RAMP: return "hr-ramp";
case ACT_ST_HR_PG: return "hr-pg";
case ACT_ST_HR_BOOT: return "hr-boot";
case ACT_ST_HR_VERIFY: return "hr-verify";
case ACT_ST_KEY_HOLD: return "key-hold";
case ACT_ST_EDL_SHORT: return "edl-short";
case ACT_ST_EDL_ENUM: return "edl-enum";
@@ -384,6 +479,7 @@ const char *t_actuator_result_str(act_result_t r)
case ACT_RES_NO_PG: return "no-pg";
case ACT_RES_IO: return "io";
case ACT_RES_ABORTED: return "aborted";
case ACT_RES_NO_BOOT: return "no-boot";
}
return "?";
}
+39 -1
查看文件
@@ -4,7 +4,7 @@
| 序列 | 时序 | 出处 |
|---|---|---|
| 硬重启 | `EFUSE_EN↓` → 等 3~5 s → `EFUSE_EN↑` → 等 **17 ms** → 等 PG 置位 → 长按电源键 5 s 开机 | 设计文档 §7.4;开机那一步见 `ACT_HR_BOOT_KEY_MS` |
| 硬重启 | `EFUSE_EN↓` → 等 3~5 s → `EFUSE_EN↑` → 等 **17 ms** → 等 PG 置位 → 长按电源键 5 s 开机 →(接了电流回调时)看电流、没开就重按,最多 3 次 | 设计文档 §7.4;开机那一步见 `ACT_HR_BOOT_KEY_MS` / `ACT_HR_BOOT_TRIES` |
| 按键 | 置位 → 保持 T(100 ms / 2 s / 10 s)→ 清零 | SW-5 / §13.3 |
| EDL | `EDL_EN=1` → `VBUS_EN=1` → 等枚举 → `EDL_EN=0` | SW-6 / §22.4 |
@@ -87,6 +87,26 @@ extern "C" {
去按只会让这一路卡在 CLEANUP 里重试。 */
#define ACT_HR_BOOT_KEY_MS ACT_KEY_MS_ON
/* ---- 开机校验与重按(0.1.2,接了 `t_actuator_set_boot_check()` 才生效) ----
2026-09-28 板 B 第 1 路实测:R2 的电源键是**主板弹片压刀片铜盘**(决策 14),
接触时好时坏 —— 7 次硬重启只开起来 1 次,失败那几次 PCA9555 输出确实置位了,
手机侧电流却一直 1~2 mA(PMIC 根本没醒,屏幕不亮),镊子短接 J603 就能开机。
所以按完之后看电流:开起来了是 ~210 mA 平台(logo 段,之后 0.5~1.7 A),没开是 1~2 mA。
· 松键后先等 ACT_HR_VERIFY_DELAY_MS(3 s)再看:按 2 s 那种「PMIC 醒了、SoC 没接住
PS_HOLD」的情况,电流会先冲到 ~415 mA、松手 3 s 内掉回去 —— 早看会误判成功;
真开起来的那一段是 ~210 mA 平台、持续约 45 s,3~5 s 时稳稳在上面;
· 连续 ACT_HR_VERIFY_GOOD_N 个样本 ≥ ACT_HR_BOOT_UA_MIN 才算开机,
窗口 ACT_HR_VERIFY_WINDOW_MS 内等不到就重按,一共最多按 ACT_HR_BOOT_TRIES 次;
· **读不到电流不重按**:看不见 ≠ 没开(ACT_RES_NO_PG 同一条理由),
对一台正在开机的手机再长按 5 s 反而可能把它按进关机菜单。 */
#define ACT_HR_BOOT_TRIES 3
#define ACT_HR_BOOT_UA_MIN 100000 /* 100 mA:logo 段 ~210 mA,关机态 1~2 mA */
#define ACT_HR_VERIFY_DELAY_MS 3000
#define ACT_HR_VERIFY_WINDOW_MS 2000 /* 松键后 3~5 s 内判 */
#define ACT_HR_VERIFY_POLL_MS 100 /* 巡检 20 ms 一圈,100 ms 取一个样本足够 */
#define ACT_HR_VERIFY_GOOD_N 5 /* 连续 0.5 s 在线才算 */
/* SW-5 的三档按键时长。
⚠️ 开机那一档 2026-09-28 小米8 实测:按 2 s 时 PMIC 起来了(415 mA),一松手 3 s 内
掉回 ~1 mA —— SoC 还没接住 PS_HOLD。按 5 s 松手后稳在 ~210 mA、约 45 s 进系统。
@@ -149,6 +169,7 @@ typedef enum {
ACT_ST_HR_RAMP, /* `EFUSE_EN` 已拉高,等 17 ms 走完 tON + 斜坡 */
ACT_ST_HR_PG, /* 等 PG 置位 */
ACT_ST_HR_BOOT, /* PG 到了,电源键按着,保持 ACT_HR_BOOT_KEY_MS 开机 */
ACT_ST_HR_VERIFY, /* 键已松开,看电流判断开没开起来;没开就回 HR_BOOT 重按 */
ACT_ST_KEY_HOLD, /* 键已按下,保持 T */
ACT_ST_EDL_SHORT, /* `EDL_EN=1`,等短接建立 */
ACT_ST_EDL_ENUM, /* `VBUS_EN=1`,等枚举 */
@@ -167,6 +188,9 @@ typedef enum {
ACT_RES_IO,
/* 被 `t_actuator_abort()` 打断。 */
ACT_RES_ABORTED,
/* 硬重启专用(0.1.2):电开着、PG 也到了,按了 ACT_HR_BOOT_TRIES 次电源键
手机电流始终起不来 —— 多半是电源键弹片没压上刀片铜盘。电**继续开着**。 */
ACT_RES_NO_BOOT,
} act_result_t;
/* PG 取值回调。返回 false = **这一刻读不到**(PCA9555 失联 / 还没读过一圈),
@@ -174,6 +198,15 @@ typedef enum {
传 NULL 给 `t_actuator_init()` 就是「永远读不到」,硬重启会走完并记 ACT_RES_NO_PG。 */
typedef bool (*act_pg_fn)(void *ctx, int rail, bool *pg);
/* 开机校验用的电流取值(µA,有符号)。返回 false = 这一刻读不到。和 PG 一样从巡检快照喂进来。 */
typedef bool (*act_ua_fn)(void *ctx, int rail, int32_t *ua);
/* 开机校验有了结论时回调一次(只在接了 `act_ua_fn` 时):
booted = 电流起来了;presses = 一共按了几次(1 = 第一次就开了);ua = 判定那一刻的电流。
执行器自己不写事件(宿主机测试不带 evlog),由 app_main 接到 LOG_EV_BOOT_CHECK。
「读不到电流、判不了」不回调。 */
typedef void (*act_boot_fn)(void *ctx, int rail, bool booted, uint8_t presses, int32_t ua);
/* ---- 接口 ---------------------------------------------------------------- */
/* 幂等,五路全部回到 IDLE。**不碰硬件**:SW-11 的「上电全关」是
@@ -181,6 +214,11 @@ typedef bool (*act_pg_fn)(void *ctx, int rail, bool *pg);
干的(A15),不是这里。 */
void t_actuator_init(act_pg_fn pg, void *ctx);
/* 打开硬重启的开机校验与重按(见 ACT_HR_BOOT_TRIES 那一段)。ua = NULL 关掉,
关掉时硬重启按完一次电源键就记 OK(0.1.1 的行为)。done 可为 NULL。
在 `t_actuator_init()` 之后调;init 会把它清掉。 */
void t_actuator_set_boot_check(act_ua_fn ua, act_boot_fn done, void *ctx);
/* 三个请求。rail 是 **1~5**。
HAL_ERR_ARG —— 路号 / 键位 / 时长越界;
HAL_ERR_STATE —— 没 init,或者**这一路正在跑另一个序列**(§10.6 不可重入)。
+8
查看文件
@@ -645,7 +645,15 @@ static void test_status_fits_and_reports_unknowns_as_null(void)
const cmd_env_t env = { .safety = &s, .thermal = &t };
cmd_init(&env);
/* 上游 VBUS:没配过的脚读不到 ⇒ null;配成输入后如实报高低。 */
ASSERT_HAS(send("{\"cmd\":\"status\"}"), "\"host\":null");
TEST_ASSERT_EQUAL(HAL_OK, hal_gpio_config(BOARD_PIN_UP_VBUS_DET, HAL_GPIO_INPUT));
sim_gpio_drive(BOARD_PIN_UP_VBUS_DET, 0);
ASSERT_HAS(send("{\"cmd\":\"status\"}"), "\"host\":false");
sim_gpio_drive(BOARD_PIN_UP_VBUS_DET, 1); /* 留着高电平:下面量满配长度 */
const char *r = send("{\"seq\":4000000000,\"cmd\":\"status\"}");
ASSERT_HAS(r, "\"host\":true");
ASSERT_HAS(r, "{\"seq\":4000000000,\"ok\":true,\"fw\":\"" PHONERAIL_FW_VERSION "\"");
ASSERT_HAS(r, "\"in\":{\"mv\":12030,\"ua\":1234567}");
ASSERT_HAS(r, "{\"r\":2,\"pr\":true,\"en\":false,\"vb\":false,\"set\":3900,\"mv\":null,\"ua\":null");
+2 -1
查看文件
@@ -86,7 +86,8 @@ static void test_flash_format_is_frozen(void)
TEST_ASSERT_EQUAL_INT(16, LOG_EV_PD_LIMIT);
TEST_ASSERT_EQUAL_INT(17, LOG_EV_PG_TIMEOUT); /* A15 */
TEST_ASSERT_EQUAL_INT(18, LOG_EV_OTA); /* C2 */
TEST_ASSERT_EQUAL_INT(19, LOG_EV_COUNT);
TEST_ASSERT_EQUAL_INT(19, LOG_EV_BOOT_CHECK); /* 0.1.2 开机校验 */
TEST_ASSERT_EQUAL_INT(20, LOG_EV_COUNT);
}
/* §10.7 的「必记事件」有八类,A10 只产生其中两类。剩下的号先占住,
+252
查看文件
@@ -1096,6 +1096,250 @@ static void test_format_prints_every_rail(void)
TEST_ASSERT_EQUAL_CHAR('\0', small[sizeof(small) - 1]);
}
/* ==== 0.1.2:硬重启的开机校验与重按 =======================================
2026-09-28 板 B 第 1 路:电源键弹片压刀片铜盘接触不良,7 次硬重启只开起来 1 次,
失败时电流一直 1~2 mA(屏都不亮)。假手机照实测建模:
· 第 `boots_on` 次按满 5 s 松手后开机 ⇒ 电流 ~210 mA(logo 段);
· 其余情况 1.5 mA(关机态);
· `half_on`:第一次按下去 PMIC 醒了、SoC 没接住 —— 松手后 415 mA 撑 2.5 s 再掉回去
(照「按 2 s」那次的实测形态),校验不能被它骗过去。 */
static bool s_ua_readable;
static int s_phone_boots_on; /* 第几次按能开;0 = 永远开不了(弹片没压上) */
static bool s_phone_half_on;
static bool s_phone_on;
static uint32_t s_phone_half_until;
static int s_edges; /* 看到的「按下」次数 */
static int s_last_pin;
static uint32_t s_press_t, s_release_t, s_min_gap;
static int s_boot_calls;
static bool s_boot_booted;
static uint8_t s_boot_presses;
static bool fake_ua(void *ctx, int rail, int32_t *ua)
{
(void)ctx;
(void)rail;
if (!s_ua_readable) {
return false;
}
if (s_phone_on) {
*ua = 210000;
} else if ((int32_t)(s_phone_half_until - hal_time_now_ms()) > 0) {
*ua = 415000;
} else {
*ua = 1500;
}
return true;
}
static void fake_boot_done(void *ctx, int rail, bool booted, uint8_t presses, int32_t ua)
{
(void)ctx;
(void)rail;
(void)ua;
s_boot_calls++;
s_boot_booted = booted;
s_boot_presses = presses;
}
static void phone_up(int boots_on, bool half_on)
{
s_ua_readable = true;
s_phone_boots_on = boots_on;
s_phone_half_on = half_on;
s_phone_on = false;
s_phone_half_until = hal_time_now_ms();
s_edges = 0;
s_last_pin = 0;
s_press_t = s_release_t = 0;
s_min_gap = UINT32_MAX;
s_boot_calls = 0;
t_actuator_set_boot_check(fake_ua, fake_boot_done, NULL);
}
/* 看电源键那根栅极的边沿,推假手机的状态。 */
static void phone_watch(int rail)
{
const int p = key_pin(rail, PCA9555_KEY_PWR);
const uint32_t now = hal_time_now_ms();
if (p && !s_last_pin) {
s_edges++;
if (s_release_t != 0 && now - s_release_t < s_min_gap) {
s_min_gap = now - s_release_t;
}
s_press_t = now;
}
if (!p && s_last_pin) {
s_release_t = now;
if (now - s_press_t >= ACT_KEY_MS_ON) {
if (s_edges == s_phone_boots_on) {
s_phone_on = true;
} else if (s_phone_half_on && s_edges == 1) {
s_phone_half_until = now + 2500;
}
}
}
s_last_pin = p;
}
/* `run_ms()` 加上每一步之后看一眼按键。 */
static void run_phone_ms(int rail, uint32_t ms)
{
const uint32_t target = hal_time_now_ms() + ms;
for (;;) {
uint32_t w = t_actuator_step();
phone_watch(rail);
const uint32_t now = hal_time_now_ms();
if ((int32_t)(now - target) >= 0) {
return;
}
const uint32_t left = target - now;
if (w == 0 || w > left) {
w = left;
}
sim_time_advance_ms(w);
}
}
/* 一轮按键 + 校验最长多久:按 5 s + 松键后判到窗口末尾。 */
#define ONE_TRY_MS (ACT_HR_BOOT_KEY_MS + ACT_HR_VERIFY_DELAY_MS + ACT_HR_VERIFY_WINDOW_MS + 50)
#define HR_TO_PRESS_MS (ACT_HR_OFF_MS + ACT_HR_RAMP_MS + ACT_HR_PG_POLL_MS)
static void test_boot_check_first_press_boots(void)
{
const int rail = 1;
s_pg_val[rail - 1] = true;
phone_up(1, false);
const uint32_t t0 = hal_time_now_ms();
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail));
run_phone_ms(rail, HR_TO_PRESS_MS + ACT_HR_BOOT_KEY_MS + 10);
TEST_ASSERT_EQUAL_INT(ACT_ST_HR_VERIFY, t_actuator_state(rail));
TEST_ASSERT_EQUAL_INT(0, key_pin(rail, PCA9555_KEY_PWR)); /* 校验期间键是松开的 */
run_phone_ms(rail, ONE_TRY_MS);
TEST_ASSERT_FALSE(t_actuator_busy(rail));
TEST_ASSERT_EQUAL_INT(ACT_RES_OK, t_actuator_last_result(rail));
TEST_ASSERT_EQUAL_INT(1, s_edges);
TEST_ASSERT_EQUAL_INT(1, s_boot_calls);
TEST_ASSERT_TRUE(s_boot_booted);
TEST_ASSERT_EQUAL_UINT8(1, s_boot_presses);
/* 开起来就收工:不把整个窗口等满。 */
TEST_ASSERT_TRUE(hal_time_now_ms() - t0 >= HR_TO_PRESS_MS + ACT_HR_BOOT_KEY_MS + ACT_HR_VERIFY_DELAY_MS);
TEST_ASSERT_EQUAL_UINT32(1, t_actuator_done(rail));
TEST_ASSERT_EQUAL_UINT32(0, sim_lock_violations(HAL_LOCK_I2C));
}
/* ⭐ 这次的主症状:第一下没压上,第二下开了。 */
static void test_boot_check_presses_again_when_the_phone_stays_off(void)
{
const int rail = 3;
s_pg_val[rail - 1] = true;
phone_up(2, false);
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail));
run_phone_ms(rail, HR_TO_PRESS_MS + 2 * ONE_TRY_MS + 1000);
TEST_ASSERT_FALSE(t_actuator_busy(rail));
TEST_ASSERT_EQUAL_INT(ACT_RES_OK, t_actuator_last_result(rail));
TEST_ASSERT_EQUAL_INT(2, s_edges);
TEST_ASSERT_TRUE(s_boot_booted);
TEST_ASSERT_EQUAL_UINT8(2, s_boot_presses);
TEST_ASSERT_EQUAL_INT(1, s_boot_calls); /* 只在有结论时报一次 */
/* 两次按之间至少隔开整段判定延迟 —— 连着按等于一次 10 s 长按(强制重启档)。 */
TEST_ASSERT_TRUE(s_min_gap >= ACT_HR_VERIFY_DELAY_MS);
TEST_ASSERT_EQUAL_INT(0, key_pin(rail, PCA9555_KEY_PWR));
}
/* 按满三次都不开:记 NO_BOOT,**电继续开着**,键松开,不再按第四下。 */
static void test_boot_check_gives_up_after_three_presses_and_keeps_power(void)
{
const int rail = 2;
s_pg_val[rail - 1] = true;
phone_up(0, false);
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail));
run_phone_ms(rail, HR_TO_PRESS_MS + ACT_HR_BOOT_TRIES * ONE_TRY_MS + 1000);
TEST_ASSERT_FALSE(t_actuator_busy(rail));
TEST_ASSERT_EQUAL_INT(ACT_RES_NO_BOOT, t_actuator_last_result(rail));
TEST_ASSERT_EQUAL_INT(ACT_HR_BOOT_TRIES, s_edges);
TEST_ASSERT_EQUAL_INT(1, s_boot_calls);
TEST_ASSERT_FALSE(s_boot_booted);
TEST_ASSERT_EQUAL_UINT8(ACT_HR_BOOT_TRIES, s_boot_presses);
TEST_ASSERT_EQUAL_INT(1, sim_gpio_level(rail_cfg(rail)->efuse_en));
TEST_ASSERT_EQUAL_INT(0, key_pin(rail, PCA9555_KEY_PWR));
TEST_ASSERT_EQUAL_STRING("no-boot", t_actuator_result_str(ACT_RES_NO_BOOT));
run_phone_ms(rail, 30000); /* 收工之后再也不按 */
TEST_ASSERT_EQUAL_INT(ACT_HR_BOOT_TRIES, s_edges);
}
/* 读不到电流 ⇒ 判不了 ⇒ **不重按**,也不报结论。 */
static void test_boot_check_does_not_press_again_when_current_is_unreadable(void)
{
const int rail = 4;
s_pg_val[rail - 1] = true;
phone_up(0, false);
s_ua_readable = false;
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail));
run_phone_ms(rail, HR_TO_PRESS_MS + ACT_HR_BOOT_TRIES * ONE_TRY_MS + 1000);
TEST_ASSERT_FALSE(t_actuator_busy(rail));
TEST_ASSERT_EQUAL_INT(ACT_RES_OK, t_actuator_last_result(rail));
TEST_ASSERT_EQUAL_INT(1, s_edges);
TEST_ASSERT_EQUAL_INT(0, s_boot_calls);
}
/* 「PMIC 醒了、SoC 没接住」:松手后先 415 mA 再掉回去。校验必须等它掉完再看,
判成「开了」就不会有第二下。 */
static void test_boot_check_is_not_fooled_by_a_pmic_that_wakes_and_drops(void)
{
const int rail = 5;
s_pg_val[rail - 1] = true;
phone_up(2, true);
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail));
run_phone_ms(rail, HR_TO_PRESS_MS + 2 * ONE_TRY_MS + 1000);
TEST_ASSERT_EQUAL_INT(ACT_RES_OK, t_actuator_last_result(rail));
TEST_ASSERT_EQUAL_INT(2, s_edges);
TEST_ASSERT_EQUAL_UINT8(2, s_boot_presses);
}
/* 校验期间被打断(比如判据要关这一路):收工、不再按。 */
static void test_abort_during_boot_check_stops_pressing(void)
{
const int rail = 1;
s_pg_val[rail - 1] = true;
phone_up(0, false);
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail));
run_phone_ms(rail, HR_TO_PRESS_MS + ACT_HR_BOOT_KEY_MS + 500);
TEST_ASSERT_EQUAL_INT(ACT_ST_HR_VERIFY, t_actuator_state(rail));
t_actuator_abort(rail);
TEST_ASSERT_FALSE(t_actuator_busy(rail));
TEST_ASSERT_EQUAL_INT(ACT_RES_ABORTED, t_actuator_last_result(rail));
run_phone_ms(rail, 3 * ONE_TRY_MS);
TEST_ASSERT_EQUAL_INT(1, s_edges);
TEST_ASSERT_EQUAL_INT(0, s_boot_calls);
}
/* 单独的按键命令不校验:那是人按的,按什么、按多久是人的意图。 */
static void test_plain_key_command_is_never_repeated(void)
{
const int rail = 2;
phone_up(0, false);
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_key(rail, ACT_KEYS_PWR, ACT_KEY_MS_ON));
run_phone_ms(rail, ACT_KEY_MS_ON + 3 * ONE_TRY_MS);
TEST_ASSERT_EQUAL_INT(1, s_edges);
TEST_ASSERT_EQUAL_INT(ACT_RES_OK, t_actuator_last_result(rail));
TEST_ASSERT_EQUAL_INT(0, s_boot_calls);
}
int main(void)
{
UNITY_BEGIN();
@@ -1130,6 +1374,14 @@ int main(void)
RUN_TEST(test_actuator_and_safety_share_one_lock_without_starving);
RUN_TEST(test_boot_check_first_press_boots);
RUN_TEST(test_boot_check_presses_again_when_the_phone_stays_off);
RUN_TEST(test_boot_check_gives_up_after_three_presses_and_keeps_power);
RUN_TEST(test_boot_check_does_not_press_again_when_current_is_unreadable);
RUN_TEST(test_boot_check_is_not_fooled_by_a_pmic_that_wakes_and_drops);
RUN_TEST(test_abort_during_boot_check_stops_pressing);
RUN_TEST(test_plain_key_command_is_never_repeated);
RUN_TEST(test_bad_arguments_are_rejected);
RUN_TEST(test_timings_match_the_documents);
RUN_TEST(test_the_lock_discipline_check_actually_fires);
+3
查看文件
@@ -13,6 +13,7 @@
<div><dt>ID</dt><dd class="readout">{{ b.id }}</dd></div>
<div><dt>固件</dt><dd><span class="readout">{{ b.status?.fw || b.fw }}</span> <OtaState :status="b.status" quiet /> <router-link to="/firmware">推固件</router-link></dd></div>
<div><dt>IP</dt><dd class="readout">{{ b.ip || '—' }}</dd></div>
<div><dt>上游</dt><dd :title="'上游 USB-C 的 VBUS(UP_VBUS_DET)。Hub 连着电脑工作时比挂起多耗约 1 W'">{{ HOST[b.status?.host] ?? '—' }}</dd></div>
</dl>
</div>
<div class="gauges" v-if="b.status">
@@ -92,6 +93,8 @@ const pw = computed(() => {
const rails = railsMw(s?.rails)
return { rails, loss: input == null || rails == null ? null : input - rails }
})
// 固件 0.1.3 起才有 host 字段;老固件没有 ⇒ 显示「—」
const HOST = { true: '已连电脑', false: '未连电脑' }
const fmtW = v => (v == null ? '—' : (v / 1000).toFixed(2))
const sel = ref(1)
const name = ref('')