固件 0.1.0-c3:计划内重启不断电(OTA / 回滚锁住 EFUSE_EN,新固件接管;SR-6 改写)

- warm_esp.{h,c}:warm_restart() 记下开着的路(RTC_NOINIT,魔数 + 掩码 + 反码)→ gpio_hold 锁五根 EN → 重启;
  目标镜像没有能力标记(第 288 字节 PRCAPS1,.rodata_custom_desc)就不锁 —— 老固件写不动锁住的 EN。
  warm_take():软件复位 + 标记对得上才接管,一次性;再读 EN 真电平核对
- bringup:adopt 掩码;rail_en_init_keep() 接管的路写高、五根一律放开 hold(崩溃时旧锁可能还在);
  刀片不在 / mux 失联的接管路立刻关(SLOT_EMPTY arg 1);第二段已开着的路不重开、不等 PG
- pca9555_init_keep():方向寄存器是本固件写的才留 VBUS(掉过电的片子锁存器是 0xFF),键 / EDL 清零
- HAL:hal_gpio_hold();ESP 输出脚配 INPUT_OUTPUT(读得到真电平);sim 给 hold 建模
- 事件 LOG_EV_OTA arg 7 = 接管;兜底定时器回滚故意不走接管
- 顶层 CMake:phonerail.h 进 CMAKE_CONFIGURE_DEPENDS(否则镜像头版本停在旧值);-u g_phonerail_caps 防 gc
- test_bringup 加 6 例(接管 / 非计划复位全关 / 空槽 / mux 失联 / PCA 掉过电 / hold 冻结),全工程 448 例全绿
- rack:固件库识别能力标记,推送对话框按「支持 / 不支持重启不断电」给不同提示(已部署)
- 需求文档:SR-6 改为「非计划复位全关」+ 计划内重启例外;§10.9 新增「重启不断电」
- 板 B:台架镜像(空槽强制 CH1 在位,未提交)开 CH1 → OTA 带重启 → 事件 18/7 mv=1,CH1 一直开着;
  现跑 0.1.0-c3。未测:示波器看重启期间 EN / 输出毛刺(要插刀片)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
zhou yang
2026-09-27 14:35:52 +08:00
共同撰写人 Claude Opus 5.5
父节点 271c649f66
当前提交 7bf30e3c38
共修改 29 个文件,包含 690 行新增和 57 行删除
+161
查看文件
@@ -558,6 +558,161 @@ static void test_dac_nvm_not_checked_on_warm_reboot(void)
/* ---- 意图掩码 ------------------------------------------------------------ */
/* ---- 「重启不断电」:计划内重启的接管(warm_esp.c 给 adopt) ----------------- */
/* 上一条命:这一路开着(EN 被 hold 在高)、PG 在、PCA9555 已被配过、锁存器是 out0。 */
static void prev_life(int rail, uint8_t out0)
{
const int ch = ch_of(rail);
sim_i2c_set_reg_ch(ch, PCA9555_ADDR, PCA9555_REG_CONFIG0, PCA9555_CONFIG0_INIT);
sim_i2c_set_reg_ch(ch, PCA9555_ADDR, PCA9555_REG_CONFIG1, PCA9555_CONFIG1_INIT);
sim_i2c_set_reg_ch(ch, PCA9555_ADDR, PCA9555_REG_OUTPUT0, out0);
sim_i2c_set_reg_ch(ch, PCA9555_ADDR, PCA9555_REG_OUTPUT1, 0);
sim_pca9555_drive(rail - 1, 1, P1_PG);
sim_gpio_preheld(rail_cfg(rail)->efuse_en, 1);
}
static int g_en_fell[N]; /* 这一条命里 EN 线上出现过几次 1→0 */
static int g_en_last[N];
static void watch_en(int pin, int level)
{
world(pin, level);
for (int rail = 1; rail <= N; rail++) {
if (pin == rail_cfg(rail)->efuse_en) {
if (g_en_last[rail - 1] == 1 && level == 0) {
g_en_fell[rail - 1]++;
}
g_en_last[rail - 1] = level;
}
}
}
static void warm(uint8_t adopt)
{
for (int rail = 1; rail <= N; rail++) {
g_en_fell[rail - 1] = 0;
g_en_last[rail - 1] = sim_gpio_level(rail_cfg(rail)->efuse_en);
}
sim_gpio_set_hook(watch_en);
const bringup_in_t in = { .power_on_reset = false, .adopt = adopt };
TEST_ASSERT_EQUAL_INT(HAL_OK, bringup_devices(&in, &g_r));
}
#define VBUS ((uint8_t)(1u << PCA9555_BIT_VBUS_EN))
#define KPWR ((uint8_t)(1u << PCA9555_BIT_KEY_PWR))
/* ⭐ 接管:EN 一次都没掉下来过;hold 放开了(固件又管得住它了);VBUS 留着、正按着的键松开;
第二段不重开、不等 PG,只把没接管的「想开的」那一路按老规矩开。 */
static void test_planned_restart_keeps_the_rails_it_adopts(void)
{
prev_life(1, (uint8_t)(VBUS | KPWR)); /* 重启那一刻 CH1 正按着电源键 */
prev_life(3, VBUS);
warm(BIT(1) | BIT(3));
TEST_ASSERT_EQUAL_UINT8(BIT(1) | BIT(3), g_r.adopted);
TEST_ASSERT_EQUAL_UINT8(0, g_r.adopt_dropped);
for (int rail = 1; rail <= N; rail++) {
const int pin = rail_cfg(rail)->efuse_en;
TEST_ASSERT_FALSE_MESSAGE(sim_gpio_held(pin), "hold 必须放开,否则 t_safety 关不掉");
const bool adopted = rail == 1 || rail == 3;
TEST_ASSERT_EQUAL_INT(adopted ? 1 : 0, sim_gpio_level(pin));
TEST_ASSERT_EQUAL(adopted, rail_efuse_is_on(rail));
TEST_ASSERT_EQUAL_INT(0, g_en_fell[rail - 1]);
}
TEST_ASSERT_EQUAL_UINT8(VBUS, sim_pca9555_output(0, 0)); /* 键松开,VBUS 留着 */
TEST_ASSERT_EQUAL_UINT8(VBUS, sim_pca9555_output(2, 0));
TEST_ASSERT_TRUE(slot_vbus_is_on(1));
TEST_ASSERT_TRUE(slot_vbus_is_on(3));
/* (sim 的 unsafe_enable 在 VBUS 合法开着时也会计数,这里不看它;直接看键 / EDL 位。) */
const uint8_t keys_edl = (uint8_t)(pca9555_key_mask() | (1u << PCA9555_BIT_EDL_EN));
TEST_ASSERT_EQUAL_UINT8(0, sim_pca9555_output(0, 0) & keys_edl);
bringup_power_restore(BIT(1) | BIT(2) | BIT(3), &g_r);
TEST_ASSERT_EQUAL_UINT8(BIT(1) | BIT(2) | BIT(3), g_r.on);
TEST_ASSERT_EQUAL_UINT64(0, sim_gpio_rise_us(rail_cfg(1)->efuse_en)); /* 这条命里没「上电」过 */
TEST_ASSERT_EQUAL_UINT64(0, sim_gpio_rise_us(rail_cfg(3)->efuse_en));
TEST_ASSERT_TRUE(sim_gpio_rise_us(rail_cfg(2)->efuse_en) != 0);
TEST_ASSERT_EQUAL_INT(0, g_en_fell[0] + g_en_fell[2]);
}
/* ⭐ SR-6:**非计划**复位(崩溃 / 看门狗)时上一条命的 hold 可能还锁着 ——
adopt = 0 就必须全关,而且 hold 放开(不放开 = 固件写低也落不到线上)。 */
static void test_unplanned_reset_drops_held_rails(void)
{
prev_life(1, VBUS);
prev_life(2, VBUS);
warm(0);
for (int rail = 1; rail <= 2; rail++) {
TEST_ASSERT_FALSE(sim_gpio_held(rail_cfg(rail)->efuse_en));
TEST_ASSERT_EQUAL_INT(0, sim_gpio_level(rail_cfg(rail)->efuse_en));
TEST_ASSERT_FALSE(rail_efuse_is_on(rail));
TEST_ASSERT_EQUAL_UINT8(0, sim_pca9555_output(rail - 1, 0)); /* VBUS 也断 */
}
TEST_ASSERT_EQUAL_UINT8(0, g_r.adopted);
}
/* 重启前还开着、重启后刀片不在了:关,记一条 SLOT_EMPTY(arg 1 = 接管时发现的)。 */
static void test_adopt_on_an_empty_slot_is_dropped(void)
{
prev_life(2, VBUS);
pull_slot(2);
warm(BIT(2));
TEST_ASSERT_EQUAL_UINT8(BIT(2), g_r.adopt_dropped);
TEST_ASSERT_EQUAL_UINT8(0, g_r.adopted);
TEST_ASSERT_EQUAL_INT(0, sim_gpio_level(rail_cfg(2)->efuse_en));
TEST_ASSERT_FALSE(rail_efuse_is_on(2));
evlog_rec_t rec;
TEST_ASSERT_TRUE(last_ev(LOG_EV_SLOT_EMPTY, &rec));
TEST_ASSERT_EQUAL_UINT8(2, rec.rail);
TEST_ASSERT_EQUAL_UINT8(1, rec.arg);
}
/* mux 失联:五路都看不见(§5.8)⇒ 接管的也关。 */
static void test_adopt_with_mux_lost_is_dropped(void)
{
prev_life(1, VBUS);
sim_i2c_set_present(TCA9548A_ADDR, false);
warm(BIT(1));
TEST_ASSERT_EQUAL_UINT8(BIT(1), g_r.adopt_dropped);
TEST_ASSERT_EQUAL_INT(0, sim_gpio_level(rail_cfg(1)->efuse_en));
bringup_power_restore(ALL, &g_r);
TEST_ASSERT_EQUAL_UINT8(0, g_r.on);
}
/* ⭐ PCA9555 掉过电(方向 / 锁存器都是复位值 0xFF):**不许**照留锁存器 —— 那是凭空打开 VBUS
与三个键。EN 照样接管(它由 ESP 的 hold 管,与这片无关)。 */
static void test_adopt_does_not_trust_a_pca_that_lost_power(void)
{
sim_gpio_preheld(rail_cfg(4)->efuse_en, 1); /* 只有 EN 是上一条命的;PCA9555 是复位值 */
sim_pca9555_drive(3, 1, P1_PG);
warm(BIT(4));
TEST_ASSERT_EQUAL_UINT8(BIT(4), g_r.adopted);
TEST_ASSERT_EQUAL_INT(1, sim_gpio_level(rail_cfg(4)->efuse_en));
TEST_ASSERT_EQUAL_UINT8(0, sim_pca9555_output(3, 0));
TEST_ASSERT_FALSE(slot_vbus_is_on(4));
TEST_ASSERT_EQUAL_UINT32(0, sim_pca9555_unsafe_enable());
}
/* 重启前一刻锁 EN:锁住之后固件**写不到**线上(这正是只许在 esp_restart 前一刻锁的理由),
rail_efuse_mask 报的是锁之前的意图。 */
static void test_hold_all_freezes_the_pins(void)
{
devices(true);
bringup_power_restore(BIT(1) | BIT(5), &g_r);
TEST_ASSERT_EQUAL_UINT8(BIT(1) | BIT(5), rail_efuse_mask());
TEST_ASSERT_EQUAL_INT(HAL_OK, rail_en_hold_all());
TEST_ASSERT_EQUAL_INT(HAL_OK, rail_efuse_enable(1, false));
TEST_ASSERT_EQUAL_INT(1, sim_gpio_level(rail_cfg(1)->efuse_en)); /* 线上没动 */
TEST_ASSERT_EQUAL_INT(HAL_OK, rail_en_init_keep(BIT(5))); /* 下一条命:只接管 CH5 */
TEST_ASSERT_EQUAL_INT(0, sim_gpio_level(rail_cfg(1)->efuse_en));
TEST_ASSERT_EQUAL_INT(1, sim_gpio_level(rail_cfg(5)->efuse_en));
TEST_ASSERT_FALSE(sim_gpio_held(rail_cfg(5)->efuse_en));
}
static void test_want_update(void)
{
TEST_ASSERT_EQUAL_UINT8(BIT(3), bringup_want_update(0, 3, true));
@@ -592,6 +747,12 @@ int main(void)
RUN_TEST(test_pg_timeout_turns_that_rail_back_off);
RUN_TEST(test_dac_nvm_mismatch_is_flagged_on_power_on);
RUN_TEST(test_dac_nvm_not_checked_on_warm_reboot);
RUN_TEST(test_planned_restart_keeps_the_rails_it_adopts);
RUN_TEST(test_unplanned_reset_drops_held_rails);
RUN_TEST(test_adopt_on_an_empty_slot_is_dropped);
RUN_TEST(test_adopt_with_mux_lost_is_dropped);
RUN_TEST(test_adopt_does_not_trust_a_pca_that_lost_power);
RUN_TEST(test_hold_all_freezes_the_pins);
RUN_TEST(test_want_update);
return UNITY_END();
}