固件 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]>
这个提交包含在:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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 */
|
||||
在新工单中引用
屏蔽一个用户