/* 启动顺序 + 降级矩阵。契约与 §10.10 各行的落点见 bringup.h。 */ #include "bringup.h" #include #include #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); } } }