固件 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 行删除
+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);