固件 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 行删除
+6 -1
查看文件
@@ -1,5 +1,5 @@
# ESP-IDF 侧的 main 组件。
# app_main.c / ulink_esp.c / mlink_esp.c / thermal_esp.c / ota_esp.c 是 ESP 专用;其余 .c 必须同时在宿主机上编得过
# app_main.c / ulink_esp.c / mlink_esp.c / thermal_esp.c / ota_esp.c / warm_esp.c 是 ESP 专用;其余 .c 必须同时在宿主机上编得过
# (新增文件时两处都要加:这里 + host/CMakeLists.txt 的 PHONERAIL_CORE_SRCS)。
idf_component_register(
SRCS "app_main.c"
@@ -29,9 +29,14 @@ idf_component_register(
"thermal_esp.c"
"ota.c"
"ota_esp.c"
"warm_esp.c"
INCLUDE_DIRS "."
EMBED_TXTFILES "certs/evse_ca.pem"
REQUIRES phonerail_hal drv esp_driver_usb_serial_jtag
esp_wifi esp_netif esp_event esp_timer nvs_flash mqtt
esp_adc esp_driver_pcnt
app_update esp_http_client mbedtls)
# warm_esp.c 的能力标记(.rodata_custom_desc,镜像第 288 字节)没有代码引用它,
# 不加 -u 会被 --gc-sections 丢掉 —— 那样每个新固件都会被当成「不认接管」,EN 永远不锁(安全,但功能白做)。
target_link_libraries(${COMPONENT_LIB} INTERFACE "-u g_phonerail_caps")
+12 -1
查看文件
@@ -31,7 +31,9 @@
#include "mlink_esp.h"
#include "nvs.h"
#include "nvs_flash.h"
#include "ota.h"
#include "ota_esp.h"
#include "warm_esp.h"
#include "ulink_esp.h"
static const char *TAG = "phonerail";
@@ -355,6 +357,10 @@ void app_main(void)
const esp_reset_reason_t why = esp_reset_reason();
/* 「重启不断电」:是不是我们自己计划的重启、重启前哪几路开着(warm_esp.c)。
**必须在 bringup 之前**:那几根 EN 此刻还被上一条命锁在高,bringup 第一步决定留还是放。 */
const uint8_t adopt = warm_take(why);
/* 事件日志**最先**起(A15 挪到这里):下面的启动序列会记事件(缺件 / mux / PD),
它们必须排在这次的 BOOT 之后,否则时间线上看着像是上一次运行留下的。
BOOT 的 arg = 复位原因 —— §10.10「看门狗复位 … 记一条复位事件」就是它。
@@ -385,13 +391,18 @@ void app_main(void)
整段在 bringup.c 里、宿主机上测得到(A15 之前它在这里,一行也测不到)。
缺件一律降级不 abort;返回错误 = 引脚 / 配置常量写错了,那才 abort。 */
static bringup_report_t boot;
const bringup_in_t in = { .power_on_reset = (why == ESP_RST_POWERON) };
const bringup_in_t in = { .power_on_reset = (why == ESP_RST_POWERON), .adopt = adopt };
err = bringup_devices(&in, &boot);
if (err != HAL_OK) {
ESP_LOGE(TAG, "启动序列失败(固件配置错误):%s", hal_err_str(err));
abort();
}
log_report(&boot);
if (adopt != 0) {
evlog_put(0, LOG_EV_OTA, OTA_EV_WARM, (int32_t)boot.adopted, (int32_t)boot.adopt_dropped);
ESP_LOGI(TAG, "接管:一直开着 0x%02x,关掉 0x%02x(刀片不在 / mux 失联)",
(unsigned)boot.adopted, (unsigned)boot.adopt_dropped);
}
/* §10.9 第 7 步的前半:风扇闭环。**放在上电之前**:t_thermal_init 先满速、再拉 FAN_EN,
第一圈之前风扇就在转(t_thermal.h)。ADC / PCNT 起不来不 abort ——
+22 -3
查看文件
@@ -198,7 +198,8 @@ hal_err_t bringup_devices(const bringup_in_t *in, bringup_report_t *r)
/* ① 五根 `EFUSE_EN` 配成输出并拉低。**先于一切外设**:DAC / eFuse 一上电就有
输出,这五根线这时必须已经确定是低的。VBUS 此刻靠硬件关着(PCA9555 复位后
全是输入 + R701 下拉),真正「固件说了算」要等下面的 `slot_init()`。 */
hal_err_t err = rail_en_init();
const uint8_t all = (uint8_t)((1u << PHONERAIL_RAIL_COUNT) - 1u);
hal_err_t err = rail_en_init_keep((uint8_t)(in->adopt & all));
if (err != HAL_OK) {
return err;
}
@@ -256,16 +257,30 @@ hal_err_t bringup_devices(const bringup_in_t *in, bringup_report_t *r)
/* ③ 五片刀片。空槽不去碰(只会白等 NAK);mux 失联时一路也够不着。 */
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
const bool adopt = (in->adopt & bit_of(rail)) != 0;
if (!slot_present(rail)) {
r->empty |= bit_of(rail);
if (adopt) {
/* 重启前还开着,重启后刀片不在了(或 PRSNT# 虚接):关,记一条。 */
(void)rail_efuse_enable(rail, false);
r->adopt_dropped |= bit_of(rail);
evlog_put((uint8_t)rail, LOG_EV_SLOT_EMPTY, 1, 0, 0);
}
continue;
}
if (!mux_ok) {
if (adopt) {
(void)rail_efuse_enable(rail, false); /* §5.8:看不见就不许带电 */
r->adopt_dropped |= bit_of(rail);
}
continue; /* 整板级已经记过 MUX_LOST,不再逐路刷十条 I2C_LOST */
}
/* PCA9555:驱动保证「输出锁存器先清零、方向最后改」(计划 §5.3)。
这一步之后 VBUS 才真的由固件说了算。 */
hal_err_t e = slot_init(rail);
这一步之后 VBUS 才真的由固件说了算。接管的路 VBUS 留着(按键 / EDL 照样清零)。 */
hal_err_t e = adopt ? slot_init_keep_vbus(rail) : slot_init(rail);
if (adopt) {
r->adopted |= bit_of(rail);
}
if (e != HAL_OK) {
r->io_lost |= bit_of(rail);
lost(rail, PCA9555_ADDR, e);
@@ -342,6 +357,10 @@ void bringup_power_restore(uint8_t want, bringup_report_t *r)
/* 能开的:想开、在位、mux 在。空槽且想开 = 刀片被拔走了(或 PRSNT# 虚接),记一条。 */
uint8_t todo = 0;
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
if (rail_efuse_is_on(rail)) {
r->on |= bit_of(rail); /* 第一段接管的:一直开着,不重开、不等 PG */
continue;
}
if ((want & bit_of(rail)) == 0) {
continue;
}
+10
查看文件
@@ -87,6 +87,12 @@ typedef struct {
其余复位原因下脉冲 = 自己把自己的 USB 控制台拔了),也只有它为真才核对
DAC 的 NVM 默认值(热重启时 DAC 没断过电,里面是上一次命令设的值)。 */
bool power_on_reset;
/* 「重启不断电」(warm_esp.c):计划内重启前开着的路,bit0 = CH1。0 = 普通启动。
这几路 EN 起来就是高(上一条命 hold 住的),第一段直接认领、不关;VBUS 也留着。
刀片不在 / mux 失联的,第一段里立刻关掉(`adopt_dropped`)—— 看不见的路不许带电(§5.8)。
⚠️ 只有软件复位 + RTC 里的标记对得上才会非 0;崩溃 / 看门狗 / 上电一律 0(SR-6)。 */
uint8_t adopt;
} bringup_in_t;
typedef struct {
@@ -96,6 +102,8 @@ typedef struct {
uint8_t empty; /* `PRSNT#` 高:刀片不在位 */
uint8_t blind; /* 刀片 INA226 不应答:失去遥测、判据不武装(「盲跑」) */
uint8_t io_lost; /* 刀片 PCA9555 不应答:FAULT/PG 看不见,VBUS 关不掉 */
uint8_t adopted; /* 接管了、一直开着没断过的路 */
uint8_t adopt_dropped;/* 想接管、但刀片不在 / mux 失联,已关掉 */
/* 第二段填。 */
uint8_t want; /* 请求恢复的(上次的意图) */
@@ -119,6 +127,8 @@ hal_err_t bringup_devices(const bringup_in_t *in, bringup_report_t *r);
等不到就关回去并记 `LOG_EV_PG_TIMEOUT`。阻塞,最坏约 0.4 + 0.5 s。
不能开的(空槽 / mux 失联)直接跳过,空槽且想开的记一条 SLOT_EMPTY。 */
void bringup_power_restore(uint8_t want, bringup_report_t *r);
/* ↑ 已经开着的路(第一段接管的)不再上电、不等 PG,直接算进 `on` —— 不管 `want` 里有没有它:
接管的是「重启前的真实状态」,意图与之不符的那种(一路正在硬重启的断电阶段)本来就不会被接管。 */
/* 「意图」掩码的更新(`power` 命令改它,app_main 把结果存 NVS)。纯函数。 */
uint8_t bringup_want_update(uint8_t want, int rail, bool on);
+7 -4
查看文件
@@ -12,9 +12,11 @@
启动序列走完 + 连上 broker(已发出 state)。做不到 ⇒ 标记无效、重启回旧版。
新固件在自证之前崩了 / 看门狗了,bootloader 自己回滚(CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE)。
---- ⚠️ 重启 = 五路全断一次 -------------------------------------------------
ESP 一复位,`EFUSE_EN` 靠下拉关掉(bringup.h),五台手机**硬断电**,
新固件起来后再按意图逐路恢复(§10.9)。所以 `ota` 命令的 `reboot` **默认 0**:
---- 重启还断不断电 ------------------------------------------------------
2026-09-27 起 OTA 的重启与超时回滚走 `warm_restart()`(warm_esp.c):目标固件认接管 ⇒
EN 锁住、**手机不断电**;目标固件不认(推老版本)/ 兜底定时器回滚 / 新固件崩溃 ⇒
ESP 一复位 `EFUSE_EN` 靠下拉关掉,五台手机**硬断电一次**,起来后按意图逐路恢复(§10.9)。
不管哪种,`ota` 命令的 `reboot` **默认 0**:
只下载 + 校验 + 切启动分区,**下次复位才生效**;要立刻换就显式带 `"reboot":true`。
已经下好的同一个镜像(sha256 相同)再发一次带 `"reboot":true` 的,不重下,直接重启。
回滚也是一次复位,同样五路全断一次。
@@ -74,7 +76,8 @@ typedef enum {
OTA_EV_FAIL = 3, /* 失败,bus_mv = ota_err_t */
OTA_EV_VALID = 4, /* 新固件自证健康,取消回滚 */
OTA_EV_ROLLBACK = 5, /* 新固件 OTA_HEALTH_MS 内没自证 ⇒ 标记无效、重启回旧版 */
OTA_EV_REBOOT = 6, /* 为了换固件而重启(五路全断一次) */
OTA_EV_REBOOT = 6, /* 为了换固件而重启 */
OTA_EV_WARM = 7, /* 计划内重启后接管(没断电),bus_mv = 接管的路掩码,current_ua = 想接管却关掉的 */
} ota_ev_t;
typedef enum {
+5 -7
查看文件
@@ -38,6 +38,7 @@
#include "ota.h"
#include "ota_esp.h"
#include "tasks.h"
#include "warm_esp.h"
static const char *TAG = "ota";
@@ -258,7 +259,8 @@ static void task_ota(void *arg)
}
/* 兜底:判据的正常路径在 t_link 里;app_main 卡死 / t_link 起不来时它来回滚。
esp_timer 任务里调,事件来不及落盘 —— 这条路本来就只在出大事时走。 */
esp_timer 任务里调,事件来不及落盘 —— 这条路本来就只在出大事时走。
⚠️ **故意不走 warm_restart()**:走到这里说明这份固件卡死了,它的「开着哪几路」不可信 ⇒ 按非计划复位全关。 */
static void backstop_cb(void *arg)
{
(void)arg;
@@ -366,11 +368,7 @@ void ota_esp_poll(void)
}
if (s_reboot_at != 0 && (int32_t)(now - s_reboot_at) >= 0) {
if (s_rollback) {
esp_ota_mark_app_invalid_rollback_and_reboot(); /* 成功就不返回 */
ESP_LOGE(TAG, "回滚失败(没有可回的旧版?),就地重启");
}
ESP_LOGW(TAG, "为换固件重启:五路全断一次,起来后按意图恢复");
esp_restart();
/* 计划内重启:目标固件认接管就锁住 EN、手机不断电;不认就五路全断一次(warm_esp.c)。 */
warm_restart(s_rollback);
}
}
+1 -1
查看文件
@@ -3,7 +3,7 @@
#define PHONERAIL_H
/* 固件版本。规则:<主>.<次>.<修订>-<最后完成的任务包> */
#define PHONERAIL_FW_VERSION "0.1.0-c2"
#define PHONERAIL_FW_VERSION "0.1.0-c3"
/* 通道数。五路直供,见设计文档 §2。 */
#define PHONERAIL_RAIL_COUNT 5
+38 -5
查看文件
@@ -261,26 +261,59 @@ bool rail_efuse_is_on(int rail)
return rail >= 1 && rail <= PHONERAIL_RAIL_COUNT && s_efuse_on[rail - 1];
}
hal_err_t rail_en_init(void)
hal_err_t rail_en_init(void) { return rail_en_init_keep(0); }
hal_err_t rail_en_init_keep(uint8_t keep)
{
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
const int pin = k_cfg[rail - 1].efuse_en;
const int pin = k_cfg[rail - 1].efuse_en;
const bool on = (keep & (1u << (rail - 1))) != 0;
hal_err_t err = hal_gpio_config(pin, HAL_GPIO_OUTPUT);
if (err != HAL_OK) {
return err;
}
/* 配成输出的那一刻两边的 HAL 都已经在输出 0 了,这一句是把「关」
写成明文 —— 别让「五路上电即全关」依赖某个默认值。 */
err = en_write(pin, false);
写成明文 —— 别让「五路上电即全关」依赖某个默认值。
接管的那几路写 1:此刻引脚还锁着(上一条命 hold 的),寄存器先对上,下面放开才不会抖。 */
err = en_write(pin, on);
if (err != HAL_OK) {
return err;
}
s_efuse_on[rail - 1] = on;
/* ⭐ **无条件**放开:不接管的那几路,锁着的高电平在这一句之后才真的落到低 ——
崩溃 / 看门狗复位时上一条命的 hold 还在,不放开就是固件永远关不掉它。 */
err = hal_gpio_hold(pin, false);
if (err != HAL_OK) {
return err;
}
s_efuse_on[rail - 1] = false;
}
return HAL_OK;
}
uint8_t rail_efuse_mask(void)
{
uint8_t m = 0;
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
if (s_efuse_on[rail - 1]) {
m |= (uint8_t)(1u << (rail - 1));
}
}
return m;
}
hal_err_t rail_en_hold_all(void)
{
hal_err_t first = HAL_OK;
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
const hal_err_t err = hal_gpio_hold(k_cfg[rail - 1].efuse_en, true);
if (err != HAL_OK && first == HAL_OK) {
first = err;
}
}
return first;
}
hal_err_t rail_efuse_enable(int rail, bool on)
{
if (rail < 1 || rail > PHONERAIL_RAIL_COUNT) {
+13
查看文件
@@ -111,6 +111,19 @@ const char *rail_selftest_str(uint32_t bit);
有电压了,eFuse 的 EN 这时得已经确定是低的。任何一个脚配失败就返回错误码。 */
hal_err_t rail_en_init(void);
/* 同上,但 `keep` 里的那几路(bit0 = CH1)写**高**:「重启不断电」的接管
(warm_esp.c 记下的、重启前开着的路)。写完电平之后**五个脚一律放开 hold**,
不管接不接管 —— 崩溃 / 看门狗复位时上一条命的 hold 可能还锁着,不放开固件就关不掉它。
`rail_en_init()` = `rail_en_init_keep(0)`。 */
hal_err_t rail_en_init_keep(uint8_t keep);
/* 五路影子的掩码(bit0 = CH1)。计划内重启前记下「哪几路开着」用。 */
uint8_t rail_efuse_mask(void);
/* 把五根 EN 脚**此刻的电平**锁住(hal_gpio_hold),跨软件复位保持。
⚠️ 锁住之后 `rail_efuse_enable()` 写不到线上 —— 只许在 esp_restart() 的前一刻调。 */
hal_err_t rail_en_hold_all(void);
/* rail 越界 → HAL_ERR_ARG;没 `rail_en_init()` 过 → HAL_ERR_ARG(HAL 会拦)。 */
hal_err_t rail_efuse_enable(int rail, bool on);
+15
查看文件
@@ -71,6 +71,12 @@ static hal_err_t do_init(void *arg)
return pca9555_init(rail - 1);
}
static hal_err_t do_init_keep_vbus(void *arg)
{
const int rail = *(const int *)arg;
return pca9555_init_keep(rail - 1, (uint8_t)(1u << PCA9555_BIT_VBUS_EN));
}
/* ---- 输出 ---------------------------------------------------------------- */
/* 已持锁版本:影子下标与通道由调用方保证一致 —— 调用方只可能是
@@ -101,6 +107,15 @@ hal_err_t slot_init(int rail)
return i2cbus_xact(slot_mux_ch(rail), SLOT_LOCK_TIMEOUT_MS, do_init, &r);
}
hal_err_t slot_init_keep_vbus(int rail)
{
if (!rail_valid(rail)) {
return HAL_ERR_ARG;
}
int r = rail;
return i2cbus_xact(slot_mux_ch(rail), SLOT_LOCK_TIMEOUT_MS, do_init_keep_vbus, &r);
}
hal_err_t slot_out0_write(int rail, uint8_t mask, uint8_t value)
{
return out0_xact(rail, mask, value);
+5
查看文件
@@ -61,6 +61,11 @@ void slot_prsnt_poll(void);
⚠️ 必须在 `rail_en_init()` 之后:这一步之后 VBUS 才真的受控。 */
hal_err_t slot_init(int rail);
/* 「重启不断电」接管的那一路:同 slot_init(),但 VBUS **保持重启前的样子**
(PCA9555 不随 ESP32 复位,锁存器里还是上一条命写的值);按键 / EDL 照样清零。
这片掉过电(方向寄存器不是我们写的)就不留,等同 slot_init()(pca9555_init_keep)。 */
hal_err_t slot_init_keep_vbus(int rail);
/* ---- VBUS(PCA9555 P0.4 → TPS22918) ------------------------------------- */
hal_err_t slot_vbus_enable(int rail, bool on);
+135
查看文件
@@ -0,0 +1,135 @@
/* 「重启不断电」(2026-09-27 用户定:**只管计划内重启**,VBUS 也保持)。只进 IDF 目标。
---- SR-6 改成了什么 --------------------------------------------------------
原文:ESP32 复位 / 上电期间五路全关,靠上下拉保证,不靠固件。现在分两类:
· **非计划复位**(上电、崩溃、看门狗、按 EN、esptool)—— 仍然全关,仍然靠下拉;
· **计划内重启**(OTA 换固件、自证超时回滚 —— 都经 `warm_restart()`)—— 五根 EFUSE_EN
用 gpio_hold 锁在重启前的电平,新固件起来**接管**:不重新上电、不等 PG;
PCA9555 不随 ESP 复位,VBUS 照留,按键 / EDL 清零(bringup.h 的 `adopt`)。
代价:从 esp_restart 到新固件 t_safety 起来 ≈1~2 s,SR-5 / SW-8 不判(eFuse 自己的硬件保护还在)。
---- 三道闸 ----------------------------------------------------------------
1. **RTC 里的标记**(RTC_NOINIT,跨软件复位保留,上电是垃圾):魔数 + 掩码 + 反码。
只有「复位原因 = ESP_RST_SW」且标记对得上才接管;**读一次就作废**。
⚠️ gpio_hold 本身不分复位原因:崩溃 / 看门狗时锁照样还在 —— 所以不接管的路
`rail_en_init_keep()` 也**无条件放开 hold**,那一刻才真的落到低。
2. **目标固件认不认**:镜像头后面放一个能力标记(`.rodata_custom_desc`,紧跟 esp_app_desc_t)。
推一个老固件(比如 0.1.0-c2)再锁 EN,它起来会照常写低、却写不到锁住的线上 ——
**五路从此关不掉**。所以目标不认就不锁,照老样子全断重启。
3. **线上核对**:起来后先读 EN 脚的真电平(上一条命配的是 INPUT_OUTPUT,锁住的输入缓冲是开的)。
标记说开着、线上却是低 ⇒ hold 没生效,那一路不接管,交给按意图恢复(错开 + 等 PG)。
---- 最坏情况 --------------------------------------------------------------
新固件在接管之前就崩了:这次复位原因是 PANIC,不接管 ⇒ 全关。多穿过的是一次启动(≈1 s)。 */
#include <string.h>
#include "driver/gpio.h"
#include "esp_attr.h"
#include "esp_log.h"
#include "esp_ota_ops.h"
#include "phonerail.h"
#include "rail_cfg.h"
#include "warm_esp.h"
static const char *TAG = "warm";
#define WARM_MAGIC 0x5752414Du /* "WRAM" */
typedef struct {
uint32_t magic;
uint8_t mask;
uint8_t nmask; /* ~mask:RTC 内存上电是随机值,魔数碰巧对上也过不了这一道 */
} warm_mark_t;
static RTC_NOINIT_ATTR warm_mark_t s_mark;
/* ---- 能力标记 -------------------------------------------------------------
镜像里的位置:镜像头 24 B + 第一个段头 8 B + esp_app_desc_t 256 B = **288**。
IDF 的链接脚本保证 `.rodata_custom_desc` 紧跟 `.rodata_desc`(sections.ld.in)。
rack 后端上传时也可以读它(firmware.py)。 */
#define CAPS_OFFSET (24 + 8 + sizeof(esp_app_desc_t))
#define CAPS_TAG "PRCAPS1"
#define CAP_WARM (1u << 0) /* 认「重启不断电」的接管 */
typedef struct {
char tag[8];
uint32_t caps;
} caps_t;
const caps_t g_phonerail_caps __attribute__((section(".rodata_custom_desc"), used)) = {
.tag = CAPS_TAG,
.caps = CAP_WARM,
};
bool warm_target_ok(const esp_partition_t *part)
{
caps_t c;
if (part == NULL || esp_partition_read(part, CAPS_OFFSET, &c, sizeof(c)) != ESP_OK) {
return false;
}
return memcmp(c.tag, CAPS_TAG, sizeof(c.tag)) == 0 && (c.caps & CAP_WARM) != 0;
}
uint8_t warm_take(esp_reset_reason_t why)
{
const bool valid = s_mark.magic == WARM_MAGIC && (uint8_t)~s_mark.mask == s_mark.nmask;
const uint8_t all = (uint8_t)((1u << PHONERAIL_RAIL_COUNT) - 1u);
const uint8_t before = valid ? (uint8_t)(s_mark.mask & all) : 0u;
uint8_t m = (why == ESP_RST_SW) ? before : 0u;
memset(&s_mark, 0, sizeof(s_mark)); /* 一次性:下一次(哪怕是软件复位)不再算数 */
/* 此刻 EN 线上的真电平(没有 hold 时是下拉的 0;上一条命锁过的才可能是 1)。
非计划复位时这里是 1 的那几路,bringup 第一步放开 hold 之后才落到低 —— 日志里留一笔。 */
uint8_t pads = 0;
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
if (gpio_get_level(rail_cfg(rail)->efuse_en) != 0) {
pads |= (uint8_t)(1u << (rail - 1));
}
}
if (pads != 0 && !(valid && why == ESP_RST_SW)) {
ESP_LOGW(TAG, "非计划复位(原因 %d)但 EN 线上还锁着 0x%02x:马上放开、全关(SR-6)", (int)why,
(unsigned)pads);
}
if (!valid) {
return 0;
}
if (why != ESP_RST_SW) {
ESP_LOGW(TAG, "有接管标记但复位原因是 %d(不是软件复位)—— 按非计划复位全关", (int)why);
return 0;
}
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
const uint8_t bit = (uint8_t)(1u << (rail - 1));
if ((m & bit) == 0) {
continue;
}
const int pin = rail_cfg(rail)->efuse_en;
if (gpio_get_level(pin) == 0) {
ESP_LOGE(TAG, "CH%d:标记说开着,EN 线上却是低(hold 没生效?)—— 不接管,按意图重开", rail);
m &= (uint8_t)~bit;
}
}
ESP_LOGI(TAG, "计划内重启:接管 0x%02x(重启前开着 0x%02x),不重新上电", (unsigned)m,
(unsigned)before);
return m;
}
void warm_restart(bool rollback)
{
const esp_partition_t *target =
rollback ? esp_ota_get_next_update_partition(NULL) : esp_ota_get_boot_partition();
if (warm_target_ok(target)) {
const uint8_t m = rail_efuse_mask();
s_mark = (warm_mark_t){ .magic = WARM_MAGIC, .mask = m, .nmask = (uint8_t)~m };
(void)rail_en_hold_all(); /* ⚠️ 从这一句起 t_safety 写不动 EN —— 下一句就是重启 */
ESP_LOGW(TAG, "%s:锁住 EN(开着 0x%02x),手机不断电", rollback ? "回滚" : "换固件", (unsigned)m);
} else {
ESP_LOGW(TAG, "%s:目标固件(%s)不认接管 —— 五路全断一次", rollback ? "回滚" : "换固件",
target ? target->label : "?");
}
if (rollback) {
esp_ota_mark_app_invalid_rollback_and_reboot(); /* 成功不返回 */
ESP_LOGE(TAG, "回滚失败(没有可回的旧版?),就地重启");
}
esp_restart();
}
+24
查看文件
@@ -0,0 +1,24 @@
/* 「重启不断电」的 ESP 胶水(2026-09-27,需求 SR-6 改写)。只在 IDF 目标里有;说明在 warm_esp.c 文件头。 */
#ifndef WARM_ESP_H
#define WARM_ESP_H
#include <stdbool.h>
#include <stdint.h>
#include "esp_partition.h"
#include "esp_system.h"
/* 启动时调(app_main 最前面、bringup 之前):这次是不是**我们自己计划的**重启、
重启前哪几路开着。返回 bringup 的 `adopt`(bit0 = CH1);不是 = 0。标记一次性,读完就作废。
还会核对那几根 EN 脚**线上**真的是高(hold 真的生效了);是低的那几路不接管。 */
uint8_t warm_take(esp_reset_reason_t why);
/* 这个分区里的固件认不认「接管」(镜像头后面的能力标记)。认不了的固件起来会照常把 EN 写低,
但写不到被锁住的线上 —— 五路从此关不掉,所以对它**不许**锁。 */
bool warm_target_ok(const esp_partition_t *part);
/* 计划内重启:目标固件认接管 ⇒ 记下开着的路、锁住五根 EN、重启;不认 ⇒ 照老样子重启(五路全断一次)。
`rollback` = 标记当前固件无效、回滚到另一个槽(esp_ota_mark_app_invalid_rollback_and_reboot)。不返回。 */
void warm_restart(bool rollback);
#endif /* WARM_ESP_H */