文件
phonerail/firmware/main/warm_esp.c
T
zhou yang和Claude Opus 5.5 7bf30e3c38 固件 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]>
2026-09-27 14:35:52 +08:00

136 行
6.4 KiB
C
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
/* 「重启不断电」(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();
}