固件 A15:启动顺序 + 降级矩阵(bringup.{h,c},已上板)

- 启动序列从 app_main.c 拆进 main/bringup.{h,c},宿主机可测:
  bringup_devices()(§10.9 第 1~4 步)→ 起 t_safety → bringup_power_restore()(第 5~6 步)
- 六片 INA226 写 Config/Calibration(之前没做,电流恒 0);输入侧 0x43 登记对调接
- 按「意图」逐路上电、错开 100 ms、每路等 PG 500 ms,等不到关回去并记 LOG_EV_PG_TIMEOUT(17)
- 意图只由 power 命令改(cmd_env_t.intent → NVS phonerail/rail_want)
- §10.10 逐行降级:INA/DAC/PCA9555/mux/PRSNT#/CH224A;mux 失联时不碰主干
- BOOT 事件 arg = 复位原因;HUB_RST# 仍只在上电复位时脉冲
- rail_volt_init() 一片失败不再放弃另一片;CH224A 主干 / 通道 5 两处都找
- sim:sim_gpio_rise_us / sim_gpio_set_hook,sim_gpio_drive 可在配置前调
- test_bringup.c 24 例 + test_cmd 意图 1 例,共 425 例全绿;版本 0.1.0-a15
- rack:事件表加 17 PG_TIMEOUT(已部署)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
zhou yang
2026-09-27 13:24:17 +08:00
共同撰写人 Claude Opus 5.5
父节点 da6af4ef1c
当前提交 c85eec53e6
共修改 24 个文件,包含 1479 行新增和 201 行删除
+1
查看文件
@@ -24,6 +24,7 @@ idf_component_register(
"cmd_net.c"
"netcfg.c"
"mlink.c"
"bringup.c"
INCLUDE_DIRS "."
EMBED_TXTFILES "certs/evse_ca.pem"
REQUIRES phonerail_hal drv esp_driver_usb_serial_jtag
+116 -142
查看文件
@@ -3,6 +3,7 @@
#include <string.h>
#include "board_pins.h"
#include "bringup.h"
#include "cmd.h"
#include "cmd_queue.h"
#include "esp_log.h"
@@ -26,6 +27,8 @@
#include "t_safety.h"
#include "tasks.h"
#include "mlink_esp.h"
#include "nvs.h"
#include "nvs_flash.h"
#include "ulink_esp.h"
static const char *TAG = "phonerail";
@@ -236,44 +239,77 @@ static void task_log(void *arg)
}
}
/* USB2517I 的复位脉冲 —— **整个固件里唯一碰 `HUB_RST#` 的地方,而且只在启动调一次**。
/* ---- 每路「意图」的持久化(A15,§10.9 第 4 步「上次的每路使能状态」)--------
一个字节,bit0 = CH1。只有 `power` 命令改它(cmd.h 的 `cmd_intent_fn`)。
与网络配置同一个 NVS 命名空间(mlink_esp.c),各用各的键。
新板 / 读不到 = 0 = 一路都不开:宁可让人手动开一次,也不在不知道的情况下给五台手机上电。 */
#define WANT_NS "phonerail"
#define WANT_KEY "rail_want"
低有效(板上 R716 上拉到 +3V3_HUB),给 1 ms 足够:手册要的是几十微秒量级。
static uint8_t s_want;
⛔⛔ **运行期一个字都不许再写它**(R2 D13 代价 2)。ESP32 自己的
USB-Serial-JTAG 就挂在这个 Hub 的第 6 个下行口上,所以复位 Hub =
**自己把自己的控制台拔了**,而且下一次 `idf.py flash` 也进不来
—— 到那时候唯一的退路是 J33 那个 5P 应急串口(UART0,IO43/IO44),
得拆机接线。写一行 "Hub 卡了就复位一下" 的自愈代码,代价是整机变砖。
⚠️ 这一下放在 `rail_en_init()` 之后、其它一切之前:Hub 复位会让 USB 重新枚举,
期间串口日志是丢的。越早做,后面越多的启动日志能被电脑收到。
失败**不 abort**:Hub 没起来只影响调试与手机的数据口,五路供电照常
(§10.2:失联 ≠ 断电)。
⛔ **只在上电复位(`ESP_RST_POWERON`)时脉冲**(2026-09-27 上板改)。
原来是「每次启动都脉冲」,而 ESP32 的每一次重启都算启动:esptool 经 USB 复位芯片、
崩溃 / 看门狗重启、`esp_restart()` —— 每一次都把 Hub 复位、把自己的 COM 口拔掉。
实测后果:板上一旦有这份固件,`esptool --before usb_reset` 连三次全断
(`ClearCommError PermissionError 13`),只能走 J33 手动进下载模式。
上电复位时 Hub 自己也刚上电,脉冲没有代价;J33 的 EN 脚复位在 S3 上也报 POWERON,
应急路径不受影响。其余原因一律不碰 `HUB_RST#`(此时它仍是复位态的高阻,靠 R716 拉高)。 */
static void hub_reset_pulse_once(void)
static void nvs_up(void)
{
const esp_reset_reason_t why = esp_reset_reason();
if (why != ESP_RST_POWERON) {
ESP_LOGI(TAG, "复位原因 %d 不是上电复位 —— 不动 HUB_RST#(保住 USB 控制台)", (int)why);
return;
esp_err_t e = nvs_flash_init();
if (e == ESP_ERR_NVS_NO_FREE_PAGES || e == ESP_ERR_NVS_NEW_VERSION_FOUND) {
nvs_flash_erase(); /* 同 mlink_esp.c:格式不认就清掉重来 */
e = nvs_flash_init();
}
hal_err_t err = hal_gpio_config(BOARD_PIN_HUB_RST_N, HAL_GPIO_OUTPUT);
if (err != HAL_OK) {
ESP_LOGE(TAG, "HUB_RST# 配置失败:%s —— 跳过复位脉冲", hal_err_str(err));
return;
if (e != ESP_OK) {
ESP_LOGE(TAG, "NVS 起不来:%s —— 本次按「一路都不开」启动", esp_err_to_name(e));
}
}
static uint8_t want_load(void)
{
nvs_handle_t h;
uint8_t v = 0;
if (nvs_open(WANT_NS, NVS_READONLY, &h) == ESP_OK) {
if (nvs_get_u8(h, WANT_KEY, &v) != ESP_OK) {
v = 0;
}
nvs_close(h);
}
return v;
}
static void want_save(void *ctx, int rail, bool on)
{
(void)ctx;
const uint8_t v = bringup_want_update(s_want, rail, on);
if (v == s_want) {
return; /* 没变就不写 flash */
}
nvs_handle_t h;
esp_err_t e = nvs_open(WANT_NS, NVS_READWRITE, &h);
if (e == ESP_OK) {
e = nvs_set_u8(h, WANT_KEY, v);
if (e == ESP_OK) {
e = nvs_commit(h);
}
nvs_close(h);
}
if (e == ESP_OK) {
s_want = v;
} else {
/* 存不下不影响这一次的开关(已经生效了),只影响下次开机恢复成什么样。 */
ESP_LOGW(TAG, "意图没存进 NVS:%s", esp_err_to_name(e));
}
}
static void log_report(const bringup_report_t *r)
{
ESP_LOGI(TAG, "启动:降级位 0x%02x,空槽 0x%02x,盲跑 0x%02x,PCA 失联 0x%02x,PD %u mA → 上限 %u mW",
(unsigned)r->deg, (unsigned)r->empty, (unsigned)r->blind, (unsigned)r->io_lost,
(unsigned)r->pd_ma, (unsigned)r->pd_limit_mw);
if ((r->deg & BRINGUP_DEG_MUX) != 0) {
ESP_LOGE(TAG, "TCA9548A 复位后仍选不中 —— 五路一路都不上电(§5.8)");
}
if ((r->deg & BRINGUP_DEG_DAC_NVM) != 0) {
ESP_LOGW(TAG, "DAC 上电值不是 3.90 V(NVM 被改过?):%u %u %u %u %u mV",
r->dac_nvm_mv[0], r->dac_nvm_mv[1], r->dac_nvm_mv[2], r->dac_nvm_mv[3],
r->dac_nvm_mv[4]);
}
(void)hal_gpio_write(BOARD_PIN_HUB_RST_N, 0); /* 低 = 复位 */
hal_time_delay_ms(1);
(void)hal_gpio_write(BOARD_PIN_HUB_RST_N, 1); /* 放开,之后再也不动 */
ESP_LOGI(TAG, "USB Hub 已复位一次(运行期不再动 HUB_RST#)");
}
static void spawn(task_id_t id, TaskFunction_t fn)
@@ -299,96 +335,38 @@ void app_main(void)
selftest_or_die();
/* ⭐ R2:**五根** `EFUSE_EN` 配成输出并全部拉低(R1 是十根:另外五根 `VBUS_EN`
已经挪到刀片的 PCA9555 上了)。**先于一切外设**:DAC / eFuse 一上电就有
输出,这五根线这时必须已经确定是低的。 */
hal_err_t err = rail_en_init();
const esp_reset_reason_t why = esp_reset_reason();
/* 事件日志**最先**起(A15 挪到这里):下面的启动序列会记事件(缺件 / mux / PD),
它们必须排在这次的 BOOT 之后,否则时间线上看着像是上一次运行留下的。
BOOT 的 arg = 复位原因 —— §10.10「看门狗复位 … 记一条复位事件」就是它。
只碰 flash,不碰总线,放在 EN 脚配置之前也安全(那几根线此刻靠下拉关着)。
⚠️ 失败**不 abort**:日志是排查手段,不是安全链的一环(§10.2)。 */
hal_err_t err = evlog_init_ex((uint8_t)why);
if (err != HAL_OK) {
ESP_LOGE(TAG, "EN 引脚初始化失败:%s", hal_err_str(err));
ESP_LOGE(TAG, "事件日志落盘不可用:%s —— 事件只留在 RAM 里", hal_err_str(err));
} else {
evlog_stats_t st;
evlog_stats(&st);
ESP_LOGI(TAG, "事件日志:容量 %u 条,flash 里已有 %u~%u(复位原因 %d)",
(unsigned)evlog_capacity(), (unsigned)st.oldest_seq, (unsigned)st.next_seq,
(int)why);
}
nvs_up();
s_want = want_load();
/* §10.9 第 1~4 步:EN 脚安全态 → Hub 脉冲(仅上电复位)→ 总线 → 认件 / 初始化。
整段在 bringup.c 里、宿主机上测得到(A15 之前它在这里,一行也测不到)。
缺件一律降级不 abort;返回错误 = 引脚 / 配置常量写错了,那才 abort。 */
static bringup_report_t boot;
const bringup_in_t in = { .power_on_reset = (why == ESP_RST_POWERON) };
err = bringup_devices(&in, &boot);
if (err != HAL_OK) {
ESP_LOGE(TAG, "启动序列失败(固件配置错误):%s", hal_err_str(err));
abort();
}
/* ⚠️ R2:这一步关掉的**只有五路直供**(`EFUSE_EN` 是 GPIO 直连)。
`VBUS_EN` 已经挪到刀片的 PCA9555 P0.4 上了(计划 §5.4),此刻还不受控 ——
它靠的是硬件:PCA9555 复位后全是输入(高阻)+ R701 100 kΩ 下拉 = VBUS 断。
真正"固件说了算"要等到下面的 `slot_init()`。 */
ESP_LOGI(TAG, "五路 eFuse 使能已全部拉低(VBUS 此刻靠下拉电阻关着)");
/* USB Hub 复位脉冲 —— 只在上电复位时、只此一次,见函数头上那一段。 */
hub_reset_pulse_once();
/* 锁必须早于任何一个用总线的任务。
⚠️ R2 之后**全系统只剩一把** `HAL_LOCK_I2C`(595 那把随着移位链一起退役),
所以「两把锁嵌套」在物理上不可能了;代价是执行器与巡检开始争同一把锁
(计划 §5.6)。 */
(void)hal_lock_init();
const hal_i2c_cfg_t i2c = {
.sda_gpio = BOARD_PIN_I2C_SDA, /* ⭐ R2:IO41(R1 是 IO14) */
.scl_gpio = BOARD_PIN_I2C_SCL, /* ⭐ R2:IO42(R1 是 IO15) */
.freq_hz = 400000,
};
err = hal_i2c_init(&i2c);
if (err != HAL_OK) {
ESP_LOGE(TAG, "I²C 初始化失败:%s", hal_err_str(err));
abort();
}
/* ⭐ R2 新增:带 mux 的总线门面。配 `MUX_RST#`、认 TCA9548A(0x70)、把通道全关。
**必须在 `hal_i2c_init()` 与 `hal_lock_init()` 之后**(i2cbus.h)。
⚠️ 失败**不 abort**:mux 认不到是**运行期**的降级情形(计划 §5.8 —— 判据会
先脉冲 `MUX_RST#` 再决定要不要五路全断),不是固件写错了。这与
`selftest_or_die()` 的分界线始终是同一条:常量表错 = abort,器件不在 = 降级。 */
err = i2cbus_init();
if (err != HAL_OK) {
ESP_LOGE(TAG, "TCA9548A(0x%02x) 初始化失败:%s —— 五路的电压电流这一刻全看不见",
(unsigned)TCA9548A_ADDR, hal_err_str(err));
}
/* ⭐ R2 新增:五根 `PRSNT#` 配成上拉输入并立刻读一遍(`slot_prsnt_init()` 自带
一次 poll)。**必须在 `slot_init()` 之前**:下面要拿它决定哪几槽值得初始化。
⚠️ 这一步失败就 abort:它只可能因为引脚号非法而失败,而那正是
`rail_selftest()` 的 `PINS` 位刚刚查过的东西 —— 走到这里还失败 = 固件写错了。
而且降级的方向是**不安全**的那一侧:`slot_present()` 读不到时返回"在位",
于是空槽也会被允许上电 = 金手指裸露带 12V(计划 §5.8)。 */
err = slot_prsnt_init();
if (err != HAL_OK) {
ESP_LOGE(TAG, "PRSNT# 引脚初始化失败:%s", hal_err_str(err));
abort();
}
/* ⭐⭐ R2 必须有的一行(计划 §5.9):底板输入侧那片 INA226(U13, 0x43)的
`IN+`/`IN−` **在板上是对调接的**(pin9 = `VIN_SW` 上游、pin10 = `+12V` 下游)。
漏了这一行,正常供电时它读回来的电流是负的,SW-8 会把「板子在好好工作」
判成**反灌**,于是一上电就断 VBUS —— 而电压、功率、温度全都正常,
日志里只会看到反复的 `LOG_EV_REVERSE`。
⚠️ 这是纯软件状态(驱动里一个标志位),不碰总线,所以放在认件之前也没关系;
但必须早于 `t_safety` 起来,那才是读它的人。 */
ina226_set_reversed(INA226_ADDR_INPUT, true);
/* ⭐ R2 新增:逐片刀片的 PCA9555 初始化(自带 `i2cbus_xact`,各走各的 mux 通道)。
驱动里那个框保证了顺序 **输出锁存器先清零、方向最后改** —— 反过来的那一刻
P0.0~P0.4 全输出 1 = 三个键一起按下 + EDL 短接 + VBUS 上电(计划 §5.3)。
这一步之后 `VBUS_EN` 才真的由固件说了算。
⚠️ 失败**不 abort**(同 `i2cbus_init()`:这是运行期降级)。代价写在 §5.4:
PCA9555 失联 ⇒ 该路 VBUS **关不掉**,兜底只剩关 `EFUSE_EN`。 */
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
if (!slot_present(rail)) {
/* 空槽不去初始化:那一片器件本来就不在,只会白等五次 NAK。
这一路也不许上电 —— 判据在 `t_safety` 里每圈查(计划 §5.8)。 */
ESP_LOGW(TAG, "槽 %d 没插刀片(PRSNT# 为高):跳过初始化,且不许上电", rail);
continue;
}
const hal_err_t e = slot_init(rail);
if (e != HAL_OK) {
ESP_LOGE(TAG, "槽 %d 的 PCA9555(0x%02x) 初始化失败:%s —— 这一路的 VBUS "
"关不掉了,兜底只剩 EFUSE_EN",
rail, (unsigned)PCA9555_ADDR, hal_err_str(e));
}
}
/* ⚠️ 还没做的:INA226 的 Config/Calibration 与 DAC 的上电值(A15 那一包)。
在那之前 t_safety 读回来的电流恒为 0(Calibration 还没写),
这是预期的,不是故障。 */
log_report(&boot);
/* 执行器:PG 从巡检的快照里取,本任务自己不碰 I²C(t_actuator.h 开头那一段)。
⚠️ 必须在 `spawn(TASK_SAFETY, ...)` 之前:判据要关路时会先 `t_actuator_abort()`,
@@ -396,15 +374,31 @@ void app_main(void)
t_actuator_init(act_pg_from_safety, NULL);
/* 命令处理器(A12)。status 读巡检快照;热管理快照等 A16 把任务体接上再给。
`power` 命令的意图经 `want_save` 存 NVS(A15)。
队列要在执行器任务起来之前建好 —— 那个任务第一件事就是等它。 */
const cmd_env_t cmd_env = { .safety = &s_snap, .thermal = NULL };
const cmd_env_t cmd_env = { .safety = &s_snap, .thermal = NULL, .intent = want_save };
cmd_init(&cmd_env);
s_cmd_q = xQueueCreate(CMD_QUEUE_DEPTH, sizeof(cmd_msg_t));
if (s_cmd_q == NULL) {
ESP_LOGE(TAG, "命令队列建不起来");
abort();
}
/* A13:USB 命令链路。装驱动 + 把日志复制一份到 USB(UART0 照旧)。
/* 事件出口。要在巡检起来之前接上,否则第一批事件没人接。 */
t_safety_set_event_sink(log_safety_event, NULL);
/* ⭐ 巡检**先于上电**起来(bringup.h「分两段」那一段):
五路一开始带电,SR-5 / SW-8 就已经在看了。 */
spawn(TASK_SAFETY, task_safety);
/* §10.9 第 5~6 步:按上次的意图逐路上电、错开 100 ms、等 PG(阻塞,最坏约 0.9 s)。 */
bringup_power_restore(s_want, &boot);
ESP_LOGI(TAG, "上电恢复:意图 0x%02x → 开着 0x%02x(等不到 PG 0x%02x,确认不了 0x%02x)",
(unsigned)boot.want, (unsigned)boot.on, (unsigned)boot.pg_fail,
(unsigned)boot.unverified);
/* §10.9 第 7 步:链路与其余任务。
A13:USB 命令链路。装驱动 + 把日志复制一份到 USB(UART0 照旧)。
失败不 abort:USB 命令链路不可用,供电与别的链路照常(§10.2)。 */
err = ulink_esp_init();
if (err != HAL_OK) {
@@ -416,26 +410,6 @@ void app_main(void)
ESP_LOGE(TAG, "WiFi 命令链路不可用:%s", hal_err_str(err));
}
/* 事件日志(§10.7)。**要在接事件出口之前 init**,否则第一条事件进的是
一个还没扫过 flash 的缓冲。
⚠️ 失败**不 abort**:没有 `evlog` 分区时日志落不了盘,但五路照供电
—— 日志是排查手段,不是安全链的一环(§10.2)。这与 `selftest_or_die()`
的区别在于:那里错的是「命令会发到哪一路」,这里错的只是「记不下来」。 */
err = evlog_init();
if (err != HAL_OK) {
ESP_LOGE(TAG, "事件日志落盘不可用:%s —— 事件只留在 RAM 里", hal_err_str(err));
} else {
evlog_stats_t st;
evlog_stats(&st);
ESP_LOGI(TAG, "事件日志:容量 %u 条,flash 里已有 %u~%u",
(unsigned)evlog_capacity(), (unsigned)st.oldest_seq,
(unsigned)st.next_seq);
}
/* 事件出口。同样要在巡检起来之前接上,否则第一批事件没人接。 */
t_safety_set_event_sink(log_safety_event, NULL);
spawn(TASK_SAFETY, task_safety);
spawn(TASK_THERMAL, task_thermal);
spawn(TASK_ACTUATOR, task_actuator);
spawn(TASK_LINK, task_link);
+1 -1
查看文件
@@ -50,7 +50,7 @@ extern "C" {
/* USB2517I 的复位,上拉到 +3V3_HUB ⇒ 低有效。
⛔⛔ **运行期一个字都不许写它**(R2 D13 代价 2):ESP32 自己的控制台与烧录口
就挂在这个 Hub 的第 6 个下行口上,误复位 = 自断控制台,而且下一次烧录也进不来。
只在**上电复位**时脉冲一次(其余复位原因不碰,见 app_main.c);真出事的退路是 J33 那个 5P 应急串口。 */
只在**上电复位**时脉冲一次(其余复位原因不碰,见 bringup.c 的 `hub_pulse()`);真出事的退路是 J33 那个 5P 应急串口。 */
#define BOARD_PIN_HUB_RST_N 8 /* 模组脚 12 → U32.43 */
#define BOARD_PIN_UP_VBUS_DET 6 /* 模组脚 6 → R721/R722 分压,上游 USB-C 插没插 */
+410
查看文件
@@ -0,0 +1,410 @@
/* 启动顺序 + 降级矩阵。契约与 §10.10 各行的落点见 bringup.h。 */
#include "bringup.h"
#include <assert.h>
#include <string.h>
#include "board_pins.h"
#include "ch224a.h"
#include "dac63204.h"
#include "evlog.h"
#include "hal_gpio.h"
#include "hal_i2c.h"
#include "hal_lock.h"
#include "hal_time.h"
#include "i2cbus.h"
#include "ina226.h"
#include "pca9555.h"
#include "rail.h"
#include "rail_cfg.h"
#include "slot.h"
#include "tca9548a.h"
/* 启动期拿锁等多久。此刻只有巡检(第二段)在和我们抢,它一次最多持锁 ~2 ms。 */
#define BRINGUP_LOCK_MS SLOT_LOCK_TIMEOUT_MS
static uint8_t bit_of(int rail) { return (uint8_t)(1u << (rail - 1)); }
static void lost(int rail, uint8_t addr, hal_err_t err)
{
/* arg = 地址(evlog.h 的约定),current_ua 顺手带上 HAL 错误码 ——
NAK(不在)与 BUS(在但 ID 不对 = 贴错件)是两种完全不同的现场。 */
evlog_put((uint8_t)rail, LOG_EV_I2C_LOST, addr, 0, (int32_t)err);
}
/* ---- 一次 xact 的正文们 -------------------------------------------------- */
static hal_err_t nop_fn(void *arg)
{
(void)arg;
return HAL_OK; /* 选通道本身就是探测:选得中 = mux 在 */
}
static hal_err_t ina_up_fn(void *arg)
{
const uint8_t addr = *(const uint8_t *)arg;
hal_err_t err = ina226_probe(addr);
return (err == HAL_OK) ? ina226_init(addr) : err;
}
static hal_err_t dac_up_fn(void *arg)
{
return rail_volt_init_one(*(const uint8_t *)arg);
}
typedef struct {
uint16_t mv[PHONERAIL_RAIL_COUNT];
} dac_peek_t;
static hal_err_t dac_peek_fn(void *arg)
{
dac_peek_t *p = (dac_peek_t *)arg;
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
int mv = 0;
p->mv[rail - 1] = (rail_get_mv(rail, &mv) == HAL_OK) ? (uint16_t)mv : 0u;
}
return HAL_OK;
}
static hal_err_t pd_fn(void *arg)
{
return ch224a_read_max_ma(CH224A_ADDR_DEFAULT, (uint32_t *)arg);
}
/* ---- 第一段 -------------------------------------------------------------- */
/* USB2517I 的复位脉冲 —— **整个固件里唯一碰 `HUB_RST#` 的地方,而且只在启动调一次**
(A15 从 app_main.c 搬过来的,理由原样保留)。
低有效(板上 R716 上拉到 +3V3_HUB),给 1 ms 足够:手册要的是几十微秒量级。
⛔⛔ **运行期一个字都不许再写它**(R2 D13 代价 2)。ESP32 自己的
USB-Serial-JTAG 就挂在这个 Hub 的第 6 个下行口上,所以复位 Hub =
**自己把自己的控制台拔了**,而且下一次 `idf.py flash` 也进不来
—— 到那时候唯一的退路是 J33 那个 5P 应急串口(UART0,IO43/IO44),
得拆机接线。写一行 "Hub 卡了就复位一下" 的自愈代码,代价是整机变砖。
⛔ **只在上电复位(`ESP_RST_POWERON`)时脉冲**(2026-09-27 上板改)。
ESP32 的每一次重启都算启动:esptool 经 USB 复位芯片、崩溃 / 看门狗重启、
`esp_restart()` —— 原来每一次都把 Hub 复位、把自己的 COM 口拔掉,实测
`esptool --before usb_reset` 连三次全断(`ClearCommError PermissionError 13`)。
上电复位时 Hub 自己也刚上电,脉冲没有代价;J33 的 EN 脚复位在 S3 上也报 POWERON,
应急路径不受影响。其余原因**连配置都不做**(配成输出那一刻就是输出 0 = 一次复位),
它仍是复位态的高阻,靠 R716 拉高。
失败**不 abort**:Hub 没起来只影响调试与手机的数据口,五路供电照常(§10.2)。 */
static void hub_pulse(void)
{
if (hal_gpio_config(BOARD_PIN_HUB_RST_N, HAL_GPIO_OUTPUT) != HAL_OK) {
return;
}
(void)hal_gpio_write(BOARD_PIN_HUB_RST_N, 0); /* 低 = 复位 */
hal_time_delay_ms(1);
(void)hal_gpio_write(BOARD_PIN_HUB_RST_N, 1); /* 放开,之后再也不动 */
}
/* mux:`i2cbus_init()` 认不到 ⇒ 按 §5.8 先脉冲一次 `MUX_RST#` 再判。 */
static bool mux_up(bringup_report_t *r)
{
if (i2cbus_init() == HAL_OK) {
return true;
}
(void)i2cbus_recover(); /* 影子作废 ⇒ 下面那次 xact 一定真的发选通字节 */
if (i2cbus_xact(I2CBUS_CH_TRUNK, BRINGUP_LOCK_MS, nop_fn, NULL) == HAL_OK) {
return true;
}
r->deg |= BRINGUP_DEG_MUX;
evlog_put(0, LOG_EV_MUX_LOST, 0, 0, 0);
return false;
}
static void dac_up(const bringup_in_t *in, bringup_report_t *r)
{
/* §10.9 第 3 步:读回 NVM 默认值与期望比对。**只在上电复位时有意义** ——
热重启时 DAC 没断过电,里面是上一条 set_voltage 设的值,不是 NVM 默认。
它要证明的是「DAC 不应答时输出会停在 3.90 V」这条降级前提还成立。 */
if (in->power_on_reset) {
dac_peek_t p;
memset(&p, 0, sizeof(p));
(void)i2cbus_xact(I2CBUS_CH_TRUNK, BRINGUP_LOCK_MS, dac_peek_fn, &p);
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
r->dac_nvm_mv[rail - 1] = p.mv[rail - 1];
/* 读不到(0)归「不应答」那一行管,这里只抓「读到了但不对」。
容差一个码以内:码 ↔ mV 的换算本身有 ±1 mV 的取整。 */
const int d = (int)p.mv[rail - 1] - RAIL_MV_SAFE;
if (p.mv[rail - 1] != 0 && (d > 1 || d < -1)) {
r->deg |= BRINGUP_DEG_DAC_NVM;
}
}
}
static const uint32_t deg_bit[DAC63204_COUNT] = { BRINGUP_DEG_DAC_U6, BRINGUP_DEG_DAC_U7 };
_Static_assert(DAC63204_COUNT == 2, "deg_bit 跟着改");
bool any_fail = false;
for (int i = 0; i < DAC63204_COUNT; i++) {
uint8_t addr = dac63204_all_addr[i];
const hal_err_t err = i2cbus_xact(I2CBUS_CH_TRUNK, BRINGUP_LOCK_MS, dac_up_fn, &addr);
if (err != HAL_OK) {
r->deg |= deg_bit[i];
lost(0, addr, err);
any_fail = true;
}
}
/* §10.10:DAC 不应答 **不关输出** —— 输出停在 NVM 默认 3.90 V,是安全值。
影子也得说这句实话,否则判据拿一个「上次设过的值」去比,会把 3.90 判成偏离。 */
if (any_fail) {
rail_setpoint_reset();
}
}
static void pd_up(bringup_report_t *r)
{
/* ⚠️ CH224A 在哪条支路上,设计文档与实物**不一致**:驱动头写的是 mux 通道 5,
而 2026-09-27 板 B 实测它在**主干**应答(0x22/0x23,PROGRESS 顶部)。
两处都试:先主干(实测),不应答再试通道 5(设计)。
选到通道 5 时主干照样看得见,所以这不会读到别的东西。 */
uint32_t ma = 0;
hal_err_t err = i2cbus_xact(I2CBUS_CH_TRUNK, BRINGUP_LOCK_MS, pd_fn, &ma);
if (err != HAL_OK) {
err = i2cbus_xact(TCA9548A_CH_PD, BRINGUP_LOCK_MS, pd_fn, &ma);
}
uint8_t arg = 0;
if (err != HAL_OK || ma == 0) {
r->deg |= BRINGUP_DEG_PD;
r->pd_ma = 0;
r->pd_limit_mw = ch224a_power_limit_mw(BRINGUP_PD_FALLBACK_MA);
arg = 1; /* 1 = 按保守值 */
if (err != HAL_OK) {
lost(0, CH224A_ADDR_DEFAULT, err);
}
} else {
r->pd_ma = ma;
r->pd_limit_mw = ch224a_power_limit_mw(ma);
if (ma < BRINGUP_PD_FALLBACK_MA) {
r->deg |= BRINGUP_DEG_PD_LOW;
}
}
/* 不管读没读到都记一条:「这次上电按多少瓦跑」是排查降载的第一个问题。
⚠️ 本包只**定**上限;逼近上限时按优先级降载(§8.3)还没人做(HANDOFF)。 */
evlog_put(0, LOG_EV_PD_LIMIT, arg, (int32_t)(r->pd_limit_mw / 100u), (int32_t)ma);
}
hal_err_t bringup_devices(const bringup_in_t *in, bringup_report_t *r)
{
assert(in != NULL && r != NULL);
memset(r, 0, sizeof(*r));
/* ① 五根 `EFUSE_EN` 配成输出并拉低。**先于一切外设**:DAC / eFuse 一上电就有
输出,这五根线这时必须已经确定是低的。VBUS 此刻靠硬件关着(PCA9555 复位后
全是输入 + R701 下拉),真正「固件说了算」要等下面的 `slot_init()`。 */
hal_err_t err = rail_en_init();
if (err != HAL_OK) {
return err;
}
/* Hub 越早复位越好:它会让 USB 重新枚举,期间日志是丢的。 */
if (in->power_on_reset) {
hub_pulse();
}
/* 锁必须早于任何一个用总线的人(R2 之后全系统只剩这一把)。 */
(void)hal_lock_init();
const hal_i2c_cfg_t i2c = {
.sda_gpio = BOARD_PIN_I2C_SDA,
.scl_gpio = BOARD_PIN_I2C_SCL,
.freq_hz = 400000,
};
err = hal_i2c_init(&i2c);
if (err != HAL_OK) {
return err;
}
const bool mux_ok = mux_up(r);
/* `PRSNT#`:失败 = 引脚号非法 = 固件写错了,而且降级方向是不安全的那一侧
(`slot_present()` 读不到时返回「在位」⇒ 空槽也会被允许上电)。 */
err = slot_prsnt_init();
if (err != HAL_OK) {
return err;
}
/* ⭐⭐ 底板输入侧 U13(0x43)的 IN+/IN− **在板上是对调接的**(计划 §5.9)。
漏了这一行,正常供电时它读回来的电流是负的,被当成反灌 ——
`test_bringup.c` 的第一条用例就是它。纯软件状态,不碰总线。 */
ina226_set_reversed(INA226_ADDR_INPUT, true);
/* ② 主干:两片 DAC、输入侧 INA226、CH224A。
⚠️ **mux 失联时主干也够不着**:`i2cbus_xact(TRUNK)` 要先给 mux 写「全关」,
那一步失败 fn 就不会被调(i2cbus.h 规矩 2)。物理上主干器件还在,但这一层
没有「绕过 mux」的路 —— 也不该有,否则通道影子就不可信了。
这时五路反正一路都不开(§5.8),所以不去试、不刷三条误导性的 I2C_LOST,
只把状态说成实话:DAC 停在 NVM 默认、功率按保守值。 */
if (mux_ok) {
dac_up(in, r);
uint8_t addr = INA226_ADDR_INPUT;
const hal_err_t e = i2cbus_xact(I2CBUS_CH_TRUNK, BRINGUP_LOCK_MS, ina_up_fn, &addr);
if (e != HAL_OK) {
r->deg |= BRINGUP_DEG_INPUT_INA;
lost(0, addr, e);
}
pd_up(r);
} else {
rail_setpoint_reset();
r->pd_limit_mw = ch224a_power_limit_mw(BRINGUP_PD_FALLBACK_MA);
}
/* ③ 五片刀片。空槽不去碰(只会白等 NAK);mux 失联时一路也够不着。 */
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
if (!slot_present(rail)) {
r->empty |= bit_of(rail);
continue;
}
if (!mux_ok) {
continue; /* 整板级已经记过 MUX_LOST,不再逐路刷十条 I2C_LOST */
}
/* PCA9555:驱动保证「输出锁存器先清零、方向最后改」(计划 §5.3)。
这一步之后 VBUS 才真的由固件说了算。 */
hal_err_t e = slot_init(rail);
if (e != HAL_OK) {
r->io_lost |= bit_of(rail);
lost(rail, PCA9555_ADDR, e);
}
uint8_t addr = rail_cfg(rail)->ina_addr;
e = i2cbus_xact(slot_mux_ch(rail), BRINGUP_LOCK_MS, ina_up_fn, &addr);
if (e != HAL_OK) {
r->blind |= bit_of(rail);
lost(rail, addr, e);
}
}
return HAL_OK;
}
/* ---- 第二段 -------------------------------------------------------------- */
typedef struct {
int rail;
pca9555_in_t in;
ina226_vi_t vi;
} probe_t;
static hal_err_t read_io_fn(void *arg)
{
probe_t *p = (probe_t *)arg;
return slot_read_inputs_locked(p->rail, &p->in);
}
static hal_err_t read_vi_fn(void *arg)
{
probe_t *p = (probe_t *)arg;
return ina226_read_vi(rail_cfg(p->rail)->ina_addr, &p->vi);
}
typedef enum { PG_WAIT, PG_OK, PG_BLIND } pg_t;
/* 这一路的电源好了没有。优先看 PCA9555 的 PGOOD;读不到就退到 INA226 的电压
(§10.10 PCA9555 那一行「改用 INA226 的电压判据兜底」);两样都读不到 = 看不见。 */
static pg_t pg_of(int rail, const bringup_report_t *r, uint8_t *arg)
{
probe_t p = { .rail = rail };
if ((r->io_lost & bit_of(rail)) == 0 &&
i2cbus_xact(slot_mux_ch(rail), BRINGUP_LOCK_MS, read_io_fn, &p) == HAL_OK) {
*arg = pca9555_efuse_fault(&p.in) ? BRINGUP_ARG_FLT : 0u;
return pca9555_efuse_pg(&p.in) ? PG_OK : PG_WAIT;
}
*arg = BRINGUP_ARG_BY_VOLT;
if ((r->blind & bit_of(rail)) == 0 &&
i2cbus_xact(slot_mux_ch(rail), BRINGUP_LOCK_MS, read_vi_fn, &p) == HAL_OK) {
return (p.vi.bus_mv >= rail_setpoint_mv(rail) - BRINGUP_PG_VOLT_MARGIN_MV) ? PG_OK
: PG_WAIT;
}
return PG_BLIND;
}
uint8_t bringup_want_update(uint8_t want, int rail, bool on)
{
if (rail < 1 || rail > PHONERAIL_RAIL_COUNT) {
return want;
}
return on ? (uint8_t)(want | bit_of(rail)) : (uint8_t)(want & ~bit_of(rail));
}
void bringup_power_restore(uint8_t want, bringup_report_t *r)
{
assert(r != NULL);
const uint8_t all = (uint8_t)((1u << PHONERAIL_RAIL_COUNT) - 1u);
want &= all;
r->want = want;
r->on = 0;
r->pg_fail = 0;
r->unverified = 0;
/* 能开的:想开、在位、mux 在。空槽且想开 = 刀片被拔走了(或 PRSNT# 虚接),记一条。 */
uint8_t todo = 0;
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
if ((want & bit_of(rail)) == 0) {
continue;
}
if ((r->empty & bit_of(rail)) != 0) {
evlog_put((uint8_t)rail, LOG_EV_SLOT_EMPTY, 0, 0, 0);
continue;
}
if ((r->deg & BRINGUP_DEG_MUX) != 0) {
continue; /* §5.8:mux 失联时五路电压电流全看不见 ⇒ 一路也不开 */
}
todo |= bit_of(rail);
}
uint8_t pending = 0;
uint32_t last_on = 0;
bool any_on = false;
int next = 1;
while (todo != 0 || pending != 0) {
const uint32_t now = hal_time_now_ms();
/* §10.9 第 5 步:到点了就开下一路。**按上一路 EN 抬高的时刻算**,
不按「上一路 PG 来了没有」算 —— 后者会让一路慢的拖住后面四路。 */
if (todo != 0 && (!any_on || hal_time_elapsed_ms(last_on) >= BRINGUP_STAGGER_MS)) {
while ((todo & bit_of(next)) == 0) {
next++;
}
const int rail = next;
todo &= (uint8_t)~bit_of(rail);
if (rail_efuse_enable(rail, true) == HAL_OK) {
r->on_ms[rail - 1] = now;
r->on |= bit_of(rail);
pending |= bit_of(rail);
last_on = now;
any_on = true;
}
}
/* 第 6 步:每一路各自的 500 ms。 */
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
if ((pending & bit_of(rail)) == 0) {
continue;
}
uint8_t arg = 0;
const pg_t pg = pg_of(rail, r, &arg);
if (pg == PG_OK) {
pending &= (uint8_t)~bit_of(rail);
} else if (pg == PG_BLIND) {
/* 看不见 ≠ 坏了(`ACT_RES_NO_PG` 同一条理由):开着,报「确认不了」。 */
pending &= (uint8_t)~bit_of(rail);
r->unverified |= bit_of(rail);
} else if (hal_time_elapsed_ms(r->on_ms[rail - 1]) >= BRINGUP_PG_TIMEOUT_MS) {
/* 等不到:关回去(§10.9 原文)。VBUS 在这一段里从没开过,只关 eFuse。 */
(void)rail_efuse_enable(rail, false);
pending &= (uint8_t)~bit_of(rail);
r->on &= (uint8_t)~bit_of(rail);
r->pg_fail |= bit_of(rail);
evlog_put((uint8_t)rail, LOG_EV_PG_TIMEOUT, arg, 0, 0);
}
}
if (todo != 0 || pending != 0) {
hal_time_delay_ms(BRINGUP_PG_POLL_MS);
}
}
}
+130
查看文件
@@ -0,0 +1,130 @@
/* `bringup` —— 启动顺序 + 降级矩阵(A15,需求 §10.9 / §10.10)。
A14 之前这一段整个写在 `app_main.c` 里,而 `app_main.c` 只进 IDF 目标 ——
于是像 `ina226_set_reversed(INA226_ADDR_INPUT, true)` 这种「漏了就把正常供电
判成反灌」的单行,**唯一的拦网是人眼**(计划 A15 那一行的 ⭐)。
现在整段都在这里,宿主机上逐个「拔掉」器件跑得到。
---- 分两段,中间让 `t_safety` 起来 --------------------------------------
bringup_devices() §10.9 第 1~4 步:引脚安全态 → 总线 → 认件 / 初始化
(缺件按 §10.10 降级,**不 abort**)
── app_main 在这里 spawn(TASK_SAFETY) ──
bringup_power_restore() §10.9 第 5~6 步:按上次的意图逐路上电、错开 100 ms、等 PG
拆开的理由:上电那一刻巡检必须已经在跑。§10.9 原文把「起链路任务」放在第 7 步,
但没说巡检什么时候起;若等五路全开完(最坏 0.4 s 错开 + 0.5 s 等 PG)才起,
这将近一秒里 SR-5 / SW-8 是瞎的。两段之间锁是通的(一切都经 `i2cbus_xact`),
所以巡检与上电序列并发没有问题。
---- 分界线:什么 abort、什么降级 -----------------------------------------
与 `selftest_or_die()` 同一条线:**固件自己错了 = 返回错误码(app_main abort),
器件不在 = 降级、记事件、接着起**。前者只有三种:EN 脚配不上、I²C 外设起不来、
`PRSNT#` 脚配不上 —— 全是引脚号 / 配置常量的问题,换块板子也一样。
---- §10.10 各行在这里怎么落地 -----------------------------------------
某片 INA226 无应答 → 该路照常上电,`blind` 置位,记 I2C_LOST;
判据自己会因为 `rail_ok` 为假而不武装(t_safety.c 的三道闸)
DAC 无应答 → 不关输出,设定值影子回到 NVM 默认(3.90 V),记 I2C_LOST
某片 PCA9555 无应答 → 不关输出(照常上电),`io_lost` 置位,记 I2C_LOST;
等 PG 改按 INA226 电压判
mux 连续选不中 → 先脉冲一次 `MUX_RST#`;仍不通 ⇒ 五路一路都不上电,记 MUX_LOST
`PRSNT#` 为高 → 不初始化、不上电;只在「上次是开着的」时记 SLOT_EMPTY
CH224A 读不到 / 偏低 → 不关输出,按读到的档位(读不到按 60 W)定上限,记 PD_LIMIT
看门狗复位 → app_main 把复位原因填进 BOOT 事件的 arg(evlog_init_ex)
I²C 总线挂死 → ⚠️ **本包没做**(9 个时钟的总线恢复在 HAL 层,见 HANDOFF) */
#ifndef BRINGUP_H
#define BRINGUP_H
#include <stdbool.h>
#include <stdint.h>
#include "hal_err.h"
#include "phonerail.h"
#ifdef __cplusplus
extern "C" {
#endif
/* §10.9 第 5 步:路与路之间错开多久。单路缓启动涌流 0.8 A,五路同开是 4 A 叠在
12 V 输入上;错开 100 ms 后各路 11.5 ms 的斜坡完全不重叠。**是必须,不是讲究**。 */
#define BRINGUP_STAGGER_MS 100
/* §10.9 第 6 步:开了电之后等 PG 最多等多久。与硬重启的 `ACT_HR_PG_TIMEOUT_MS`
同一个数(PG 在斜坡中段就该来,~30 倍余量)。⚠️ 与硬重启**处理不同**:
硬重启等不到 PG 不断电(那是运行中的手机),启动时等不到就关回去(§10.9 原文)。
—— 但「PCA9555 读不到」不算「等不到」,那是看不见,见 `BRINGUP_ARG_BY_VOLT`。 */
#define BRINGUP_PG_TIMEOUT_MS 500
/* 等 PG 期间多久看一眼。ESP 上 `hal_time_delay_ms()` 至少一个 tick(10 ms),
所以实际是 10 ms;错开的 100 ms 因此会落在 100~110 ms 之间。 */
#define BRINGUP_PG_POLL_MS 5
/* PCA9555 读不到时的 PG 替身:INA226 母线电压到了「设定值 − 这个数」就算好了。
与 SR-5 的 ±100 mV 同一个数 —— 到了这个电压判据就不会再判它欠压。 */
#define BRINGUP_PG_VOLT_MARGIN_MV 100
/* CH224A 读不到 / 读到 0 时按这个电流算上限(20 V × 3 A = 60 W,§10.10「最保守的 60 W」)。 */
#define BRINGUP_PD_FALLBACK_MA 3000
/* `LOG_EV_PG_TIMEOUT` 的 arg 位。 */
#define BRINGUP_ARG_FLT 0x01u /* 超时那一刻 FLT# 在报 */
#define BRINGUP_ARG_BY_VOLT 0x02u /* 这一路 PCA9555 读不到,是按 INA226 电压判的 */
/* 整板级降级位(`bringup_report_t.deg`)。 */
typedef enum {
BRINGUP_DEG_MUX = 1u << 0, /* mux 复位后仍选不中:五路不上电 */
BRINGUP_DEG_DAC_U6 = 1u << 1, /* 0x4A 不应答:CH1/2/4 停在 NVM 默认 */
BRINGUP_DEG_DAC_U7 = 1u << 2, /* 0x4B 不应答:CH3/5 停在 NVM 默认 */
BRINGUP_DEG_DAC_NVM = 1u << 3, /* 上电复位时读回的码不是 3.90 V(NVM 被改过) */
BRINGUP_DEG_INPUT_INA = 1u << 4, /* 输入侧 0x43 不应答:整板功率看不见 */
BRINGUP_DEG_PD = 1u << 5, /* CH224A 读不到:按 60 W */
BRINGUP_DEG_PD_LOW = 1u << 6, /* CH224A 读到了,但低于 60 W */
} bringup_deg_t;
typedef struct {
/* 这次复位是不是上电复位。只有它为真才脉冲 `HUB_RST#`(app_main 的那一大段理由:
其余复位原因下脉冲 = 自己把自己的 USB 控制台拔了),也只有它为真才核对
DAC 的 NVM 默认值(热重启时 DAC 没断过电,里面是上一次命令设的值)。 */
bool power_on_reset;
} bringup_in_t;
typedef struct {
uint32_t deg; /* bringup_deg_t 的或 */
/* 每路一位,bit0 = CH1。 */
uint8_t empty; /* `PRSNT#` 高:刀片不在位 */
uint8_t blind; /* 刀片 INA226 不应答:失去遥测、判据不武装(「盲跑」) */
uint8_t io_lost; /* 刀片 PCA9555 不应答:FAULT/PG 看不见,VBUS 关不掉 */
/* 第二段填。 */
uint8_t want; /* 请求恢复的(上次的意图) */
uint8_t on; /* 最后真的开着的 */
uint8_t pg_fail; /* 等不到 PG、已关回去 */
uint8_t unverified; /* PCA9555 与 INA226 都读不到:开着,但确认不了 */
uint32_t on_ms[PHONERAIL_RAIL_COUNT]; /* 每路 EN 抬高的时刻(没开是 0) */
uint32_t pd_ma; /* CH224A 读到的档位电流(读不到是 0) */
uint32_t pd_limit_mw; /* 整板功率上限(读不到按 60 W) */
uint16_t dac_nvm_mv[PHONERAIL_RAIL_COUNT]; /* 上电复位时读回的每路设定(没读是 0) */
} bringup_report_t;
/* §10.9 第 1~4 步。返回非 HAL_OK **只**表示固件 / 引脚配置本身错了(见文件头),
调用方应当 abort;器件缺失一律 HAL_OK + `r` 里的降级位 + 事件。
⚠️ 调之前 `evlog_init_ex()` 要已经跑过:这里产生的事件要排在 BOOT 那一条之后。 */
hal_err_t bringup_devices(const bringup_in_t *in, bringup_report_t *r);
/* §10.9 第 5~6 步。`want` 是上次的每路意图(bit0 = CH1,来自 NVS;新板是 0 = 全不开)。
逐路:能开的开、错开 `BRINGUP_STAGGER_MS`、各自等 PG(`BRINGUP_PG_TIMEOUT_MS`),
等不到就关回去并记 `LOG_EV_PG_TIMEOUT`。阻塞,最坏约 0.4 + 0.5 s。
不能开的(空槽 / mux 失联)直接跳过,空槽且想开的记一条 SLOT_EMPTY。 */
void bringup_power_restore(uint8_t want, bringup_report_t *r);
/* 「意图」掩码的更新(`power` 命令改它,app_main 把结果存 NVS)。纯函数。 */
uint8_t bringup_want_update(uint8_t want, int rail, bool on);
#ifdef __cplusplus
}
#endif
#endif /* BRINGUP_H */
+9
查看文件
@@ -102,9 +102,18 @@ typedef enum {
/* 请求行的最大长度(含结尾 0)。超长的行由适配器整行丢弃(A13 的分帧规矩)。 */
#define CMD_LINE_MAX 256
/* `power` 命令改了某一路的「意图」(A15)。启动时按这个意图逐路恢复(§10.9 第 4 步
「上次的每路使能状态」),所以 app_main 在这里把它存进 NVS。
⚠️ 只有 `power` 命令改意图:判据跳闸、硬重启、mux 失联全断都**不改** ——
那些是事故,不是操作员的决定;重启之后按意图恢复,跳闸的那一路再由判据管。
在执行器任务里同步调(可以写 flash,毫秒级,命令本来就稀)。 */
typedef void (*cmd_intent_fn)(void *ctx, int rail, bool on);
typedef struct {
const safety_snap_t *safety; /* NULL = status 里不报这一块 */
const thermal_snap_t *thermal; /* NULL = 同上 */
cmd_intent_fn intent; /* NULL = 不记意图(宿主机测试) */
void *intent_ctx;
} cmd_env_t;
/* 清判重窗口、记下两份快照的位置。幂等。
+17 -1
查看文件
@@ -199,6 +199,15 @@ cmd_err_t do_vbus(ctx_t *c)
return from_hal(slot_vbus_enable(c->rail, on));
}
/* A15:意图出口(cmd.h 的 `cmd_intent_fn`)。 */
static void note_intent(int rail, bool on)
{
const cmd_env_t *env = cmd_env();
if (env != NULL && env->intent != NULL) {
env->intent(env->intent_ctx, rail, on);
}
}
cmd_err_t do_power(ctx_t *c)
{
cmd_err_t e = need_rail(c);
@@ -213,13 +222,20 @@ cmd_err_t do_power(ctx_t *c)
return CMD_ERR_BUSY; /* 硬重启走到一半再开关一次是自作主张(t_actuator_abort 同理) */
}
if (on) {
return from_hal(rail_efuse_enable(c->rail, true));
const hal_err_t ee = rail_efuse_enable(c->rail, true);
if (ee == HAL_OK) {
note_intent(c->rail, true);
}
return from_hal(ee);
}
/* 关:与 `rail_all_off()` 同序 —— **先断 VBUS 再关 eFuse**。
VBUS 走 I²C 可能断不掉,eFuse 是 GPIO 永远关得掉:前者失败也照样关后者,
回应报 io,上位机据此知道「直供断了,VBUS 可能还通着」。 */
const hal_err_t ev = slot_vbus_enable(c->rail, false);
const hal_err_t ee = rail_efuse_enable(c->rail, false);
if (ee == HAL_OK) {
note_intent(c->rail, false); /* 直供确实断了:下次开机也别开它 */
}
return from_hal(ee != HAL_OK ? ee : ev);
}
+7 -4
查看文件
@@ -437,7 +437,9 @@ static hal_err_t format_all(void)
return HAL_OK;
}
hal_err_t evlog_init(void)
hal_err_t evlog_init(void) { return evlog_init_ex(0); }
hal_err_t evlog_init_ex(uint8_t boot_arg)
{
/* RAM 侧无条件清干净:真板上这里刚上电,宿主机上这条让 `sim_hal_reboot()`
之后的行为和真重启一致(statics 在进程里是活着的)。 */
@@ -465,7 +467,7 @@ hal_err_t evlog_init(void)
if (err != HAL_OK) {
/* 没有分区。**不 abort、不影响入队**:日志是排查手段,不是安全链
(evlog.h 上面那段)。事件照进 RAM,只是没人能把它们写下去。 */
evlog_put(0, LOG_EV_BOOT, 0, 0, 0);
evlog_put(0, LOG_EV_BOOT, boot_arg, 0, 0);
return err;
}
@@ -474,7 +476,7 @@ hal_err_t evlog_init(void)
if (g.sector_bytes < 2u * sizeof(evlog_rec_t) ||
(g.sector_bytes % sizeof(evlog_rec_t)) != 0u || g.sectors < 2u) {
s_have_store = false;
evlog_put(0, LOG_EV_BOOT, 0, 0, 0);
evlog_put(0, LOG_EV_BOOT, boot_arg, 0, 0);
return HAL_ERR_STATE;
}
s_sector_bytes = g.sector_bytes;
@@ -525,7 +527,7 @@ hal_err_t evlog_init(void)
/* §10.7「上电从 0 计」⇒ 时间戳会跳回去。没有这一条的话,重启前后的记录
在时间轴上互相穿插,而日志里看不出重启发生过。 */
evlog_put(0, LOG_EV_BOOT, 0, 0, 0);
evlog_put(0, LOG_EV_BOOT, boot_arg, 0, 0);
return s_have_store ? HAL_OK : HAL_ERR_STATE;
}
@@ -569,6 +571,7 @@ const char *evlog_ev_str(log_ev_t ev)
case LOG_EV_MUX_LOST: return "mux失联全断";
case LOG_EV_SLOT_EMPTY: return "刀片不在位";
case LOG_EV_PD_LIMIT: return "PD限功率";
case LOG_EV_PG_TIMEOUT: return "上电等不到PG";
case LOG_EV_COUNT: break;
}
return "?";
+10
查看文件
@@ -72,6 +72,11 @@ typedef enum {
LOG_EV_SLOT_EMPTY = 15,/* `PRSNT#` 为高:刀片不在位,这一路不许带电 */
LOG_EV_PD_LIMIT = 16, /* PD 档位换了 / 触发限功率降载,bus_mv = 上限 mW/100(R-6) */
/* A15 新增(需求 §10.9 第 6 步「等各路 PG,超时则记事件并保持该路关闭」)。
与 `PG_CHANGE`(8)不是一回事:那是「PG 变了」,这是「开了电、PG 一直没来、已经关回去」。
arg:bit0 = 超时那一刻 FLT# 在报;bit1 = 这一片 PCA9555 读不到,改按 INA226 电压判的。 */
LOG_EV_PG_TIMEOUT = 17,
LOG_EV_COUNT,
} log_ev_t;
@@ -123,6 +128,11 @@ typedef struct {
这种情况下入队照常工作(事件留在 RAM 里),只是落不了盘。 */
hal_err_t evlog_init(void);
/* 同上,只是 `LOG_EV_BOOT` 那一条的 arg 由调用方给(A15:ESP 上填 `esp_reset_reason()`,
§10.10「看门狗复位 … 记一条复位事件」就是这一条 —— 不另开事件号,
因为「这次上电是怎么来的」本来就是 BOOT 的属性)。`evlog_init()` = `evlog_init_ex(0)`。 */
hal_err_t evlog_init_ex(uint8_t boot_arg);
/* 入队一条。**不阻塞、不碰 flash、不会失败** —— 缓冲满了就盖掉最旧的一条
并记一次 `dropped`(下一次落盘时会补一条 LOST)。
任何任务、任何优先级都可以调,包括 t_safety 断电之后的那一下。 */
+1 -1
查看文件
@@ -3,7 +3,7 @@
#define PHONERAIL_H
/* 固件版本。规则:<主>.<次>.<修订>-<最后完成的任务包> */
#define PHONERAIL_FW_VERSION "0.1.0-a14"
#define PHONERAIL_FW_VERSION "0.1.0-a15"
/* 通道数。五路直供,见设计文档 §2。 */
#define PHONERAIL_RAIL_COUNT 5
+17 -12
查看文件
@@ -46,23 +46,28 @@ int32_t rail_code_to_vd_uv(uint16_t code)
return dac63204_code_to_uv(code);
}
hal_err_t rail_volt_init_one(uint8_t addr)
{
hal_err_t err = dac63204_probe(addr);
if (err != HAL_OK) {
return err;
}
/* init 自己就把用到的通道写成 safe 了(上电之前写,见 dac63204_init 的 ③)。 */
return dac63204_init(addr, rail_mv_to_code(RAIL_MV_SAFE));
}
hal_err_t rail_volt_init(void)
{
const uint16_t safe = rail_mv_to_code(RAIL_MV_SAFE);
/* ⚠️ 一片失败**不放弃另一片**(A15):U6 不应答时 U7 那两路照样该被初始化,
而不是也停在 NVM 默认值上 —— 那样一片坏件就「降级」了五路。 */
hal_err_t first = HAL_OK;
for (int i = 0; i < DAC63204_COUNT; i++) {
uint8_t addr = dac63204_all_addr[i];
hal_err_t err = dac63204_probe(addr);
if (err != HAL_OK) {
return err;
}
/* init 自己就把用到的通道写成 safe 了(上电之前写,见 dac63204_init 的 ③)。 */
err = dac63204_init(addr, safe);
if (err != HAL_OK) {
return err;
const hal_err_t err = rail_volt_init_one(dac63204_all_addr[i]);
if (err != HAL_OK && first == HAL_OK) {
first = err;
}
}
return HAL_OK;
return first;
}
/* 五路设定值的影子(契约见 rail.h)。初值就是 DAC 的 NVM 出厂默认,
+5 -1
查看文件
@@ -57,9 +57,13 @@ int32_t rail_code_to_vd_uv(uint16_t code); /* 码 → DAC 自己的输出 Vd(
/* ---- 器件操作 ------------------------------------------------------------ */
/* 认件 + 初始化两片 DAC,并把五路都置到 RAIL_MV_SAFE。
任何一片失败就返回它的错误码(降级矩阵在 A15 处理)。 */
两片都会试;返回第一个错误码(降级矩阵在 bringup.c)。 */
hal_err_t rail_volt_init(void);
/* 只做其中一片(`dac63204_all_addr[]` 里的地址)。A15 的启动序列按片调它,
好分别记「哪一片没应答」(§10.10 DAC 那一行)。两片的失败互不影响。 */
hal_err_t rail_volt_init_one(uint8_t addr);
/* rail 是 **1~5**(不是 0~4),越界 → HAL_ERR_ARG。
mv > 4010 → HAL_ERR_ARG(SR-13:越界即拒,让操作者早点知道自己写错了)。
mv < 3524 → HAL_ERR_ARG(DAC 到不了)。