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