文件
phonerail/firmware/main/rail_cfg.c
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

350 行
12 KiB
C

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
/* 五路配置表 + 启动自检 + `EFUSE_EN`。契约与来历见 rail_cfg.h。 */
#include "rail_cfg.h"
#include <assert.h>
#include <stddef.h>
#include "board_pins.h"
#include "dac63204.h"
#include "hal_gpio.h"
#include "ina226.h"
#include "pca9555.h"
#include "slot.h"
#include "tca9548a.h"
/* ---- 那张表 --------------------------------------------------------------
六列的出处(全部 2026-09-16 从板上缓存逐格核过):
· INA 地址 —— `.claude/r2/_blade_pcb_nets.json` 的 `U3` pin1/pin2 都接 GND ⇒ 0x40
· mux 通道 —— `.claude/r2/_base_pcb_nets.json` 的 `U35`:SD0/SC0=SDA1/SCL1 → 槽 1,
一路到 SD4/SC4=SDA5/SCL5 → 槽 5(通道 5 给 CH224A,通道 6/7 悬空)
· DAC 片/通道 —— R1 §8.2 + §26.18,**D11b 之后一个字没改**
· `EFUSE_EN` / `PRSNT#` —— `_base_pcb_nets.json` 的 `U20`(模组脚 → GPIO 见 board_pins.h)
⚠️ 顺排的那几列也不许改成算式,`PRSNT#` 那一列就是现成的反例。 */
static const rail_cfg_t k_cfg[PHONERAIL_RAIL_COUNT] = {
/* INA mux DAC片 ch EFUSE_EN PRSNT# */
/* CH1 */ { INA226_ADDR_RAIL, 0, 0x4A, 2, 9, 15 },
/* CH2 */ { INA226_ADDR_RAIL, 1, 0x4A, 0, 10, 16 },
/* CH3 */ { INA226_ADDR_RAIL, 2, 0x4B, 0, 11, 17 },
/* CH4 */ { INA226_ADDR_RAIL, 3, 0x4A, 1, 12, 18 },
/* CH5 */ { INA226_ADDR_RAIL, 4, 0x4B, 1, 13, 7 }, /* ⚠️ PRSNT5# 不接着顺排 */
};
_Static_assert(PHONERAIL_RAIL_COUNT == INA226_RAIL_COUNT, "五路数对不上 INA226");
_Static_assert(PHONERAIL_RAIL_COUNT == DAC63204_RAIL_COUNT, "五路数对不上 DAC63204");
_Static_assert(PHONERAIL_RAIL_COUNT == PCA9555_SLOT_COUNT, "五路数对不上 PCA9555 实例数");
const rail_cfg_t *rail_cfg(int rail)
{
assert(rail >= 1 && rail <= PHONERAIL_RAIL_COUNT); /* 悄悄返回 CH1 比崩掉危险 */
return &k_cfg[rail - 1];
}
/* ---- 自检之一:本表与驱动的表逐格对账 ------------------------------------
分家的现象是「命令发给了另一路」,功能测试测不出来,所以这里是**逐格**比。 */
static bool table_matches_drivers(const rail_cfg_t *t)
{
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
const rail_cfg_t *c = &t[rail - 1];
if (c->ina_addr != ina226_rail_addr(rail)) {
return false;
}
if (c->dac_addr != dac63204_rail_addr(rail) || c->dac_ch != dac63204_rail_ch(rail)) {
return false;
}
}
return true;
}
/* ---- 自检之二:mux 通道(R2 新增) ---------------------------------------
三条:都在 0~7 之内、五路互不重复、**都不等于 PD 那一路**。
第三条不是洁癖:通道 5 上是 CH224A,选到它再去读 0x40 会读不到任何东西,
而现象是「这一路的电压电流突然全是 0」—— 看上去像刀片拔了。 */
static bool mux_ok(const rail_cfg_t *t)
{
for (int i = 0; i < PHONERAIL_RAIL_COUNT; i++) {
const uint8_t ch = t[i].mux_ch;
if (ch >= TCA9548A_CH_COUNT) {
return false;
}
if (ch == TCA9548A_CH_PD) {
return false;
}
for (int j = i + 1; j < PHONERAIL_RAIL_COUNT; j++) {
if (t[j].mux_ch == ch) {
return false; /* 两路指向同一片刀片 */
}
}
}
return true;
}
/* ---- 自检之三:引脚无重复、无越界 ----------------------------------------
查的是**全板**,不只是五路那十个脚:重复的两个脚一定分属两个模块,
只查自己那十个的话,「`PRSNT3#` 写成了 IO41」这种错(撞上 SDA)查不出来。 */
static bool pin_in(const int *list, size_t n, int pin)
{
for (size_t i = 0; i < n; i++) {
if (list[i] == pin) {
return true;
}
}
return false;
}
static bool pin_usable(int pin)
{
static const int absent[] = BOARD_PINS_ABSENT;
static const int strapping[] = BOARD_PINS_STRAPPING;
static const int reserved[] = BOARD_PINS_RESERVED;
static const int uart0[] = BOARD_PINS_UART0;
if (pin < 0 || pin > BOARD_PIN_NUM_MAX) {
return false;
}
if (pin_in(absent, sizeof(absent) / sizeof(absent[0]), pin)) {
return false; /* 这个号在 S3 上根本不存在,或者接着模组内部的 flash */
}
if (pin_in(strapping, sizeof(strapping) / sizeof(strapping[0]), pin)) {
return false;
}
if (pin_in(reserved, sizeof(reserved) / sizeof(reserved[0]), pin)) {
return false; /* N16R8 的八线 PSRAM 占着 */
}
if (pin_in(uart0, sizeof(uart0) / sizeof(uart0[0]), pin)) {
return false; /* J33 应急口:Hub 挂掉之后唯一进得来的路(R2 D13) */
}
return true;
}
static bool pins_ok(const rail_cfg_t *t)
{
/* 全板用掉的每一个脚。顺序无所谓,这里只查「都能用」和「不重复」。 */
int used[2 * PHONERAIL_RAIL_COUNT + 11];
size_t n = 0;
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
used[n++] = t[rail - 1].efuse_en;
used[n++] = t[rail - 1].prsnt_n;
}
used[n++] = BOARD_PIN_I2C_SDA;
used[n++] = BOARD_PIN_I2C_SCL;
used[n++] = BOARD_PIN_MUX_RST_N;
used[n++] = BOARD_PIN_IOEXP_INT_N;
used[n++] = BOARD_PIN_INA_ALERT_N;
used[n++] = BOARD_PIN_HUB_RST_N;
used[n++] = BOARD_PIN_UP_VBUS_DET;
used[n++] = BOARD_PIN_NTC1;
used[n++] = BOARD_PIN_NTC2;
used[n++] = BOARD_PIN_FAN_PWM;
used[n++] = BOARD_PIN_FAN_EN;
assert(n == sizeof(used) / sizeof(used[0]));
for (size_t i = 0; i < n; i++) {
if (!pin_usable(used[i])) {
return false;
}
for (size_t j = i + 1; j < n; j++) {
if (used[i] == used[j]) {
return false; /* 两个信号抢同一个脚 */
}
}
}
/* 两路 TACH 与 USB 的 D± 单独查:TACH 会与上面那些撞,而 D± 是硬连线,
它们只要不和五路那十个脚撞就行。 */
const int more[] = { BOARD_PIN_FAN_TACH_IN, BOARD_PIN_FAN_TACH_OUT };
for (size_t k = 0; k < sizeof(more) / sizeof(more[0]); k++) {
if (!pin_usable(more[k])) {
return false;
}
for (size_t i = 0; i < n; i++) {
if (used[i] == more[k]) {
return false;
}
}
}
if (BOARD_PIN_FAN_TACH_IN == BOARD_PIN_FAN_TACH_OUT) {
return false;
}
/* 原生 USB 的两个脚是硬连线(IO19/IO20),谁也不能占。 */
for (size_t i = 0; i < n; i++) {
if (used[i] == BOARD_PIN_USB_DM || used[i] == BOARD_PIN_USB_DP) {
return false;
}
}
/* 两路 NTC 必须在 ADC1(IO1~IO10)上:ADC2 在 WiFi 开着的时候读不了,
而本板 WiFi 是常开的(§10.8 传输 A)。挪到 ADC2 上不会报错,
只会在连上 WiFi 之后开始读失败 —— 台架上很难复现。 */
if (BOARD_PIN_NTC1 < 1 || BOARD_PIN_NTC1 > 10 || BOARD_PIN_NTC2 < 1 || BOARD_PIN_NTC2 > 10) {
return false;
}
return true;
}
/* ---- 启动自检 ------------------------------------------------------------ */
uint32_t rail_selftest_table(const rail_cfg_t *t)
{
uint32_t bad = 0;
if (t == NULL) {
return RAIL_SELFTEST_TABLE | RAIL_SELFTEST_PINS | RAIL_SELFTEST_MUX;
}
if (!table_matches_drivers(t)) {
bad |= RAIL_SELFTEST_TABLE;
}
if (!pins_ok(t)) {
bad |= RAIL_SELFTEST_PINS;
}
if (!mux_ok(t)) {
bad |= RAIL_SELFTEST_MUX;
}
return bad;
}
uint32_t rail_selftest(void)
{
uint32_t bad = 0;
/* 三个驱动各自的表(A3/A4/R-2 都留了这个谓词)。 */
if (!ina226_addr_table_ok()) {
bad |= RAIL_SELFTEST_INA;
}
if (!dac63204_map_ok()) {
bad |= RAIL_SELFTEST_DAC;
}
if (!pca9555_map_ok() || !pca9555_addr_ok() || !tca9548a_addr_ok()) {
bad |= RAIL_SELFTEST_PCA9555;
}
return bad | rail_selftest_table(k_cfg);
}
const char *rail_selftest_str(uint32_t bit)
{
switch (bit) {
case RAIL_SELFTEST_INA:
return "INA226 地址表";
case RAIL_SELFTEST_DAC:
return "DAC63204 映射表";
case RAIL_SELFTEST_MUX:
return "mux 通道重复或越界";
case RAIL_SELFTEST_PCA9555:
return "PCA9555/TCA9548A 位表或地址";
case RAIL_SELFTEST_TABLE:
return "五路表与驱动分家";
case RAIL_SELFTEST_PINS:
return "引脚重复或越界";
default:
return "?";
}
}
/* ---- `EFUSE_EN` ---------------------------------------------------------- */
/* 高有效。**全固件唯一一处极性**(和 `rail_mv_to_code()` 里那个减法、
`slot.c` 里 VBUS 那一处同一个道理:多一处就多一处写反的机会)。 */
static hal_err_t en_write(int pin, bool on)
{
return hal_gpio_write(pin, on ? 1 : 0);
}
/* 影子(契约见 rail_cfg.h)。写失败时**不更新**。 */
static bool s_efuse_on[PHONERAIL_RAIL_COUNT];
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) { 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 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 了,这一句是把「关」
写成明文 —— 别让「五路上电即全关」依赖某个默认值。
接管的那几路写 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;
}
}
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) {
return HAL_ERR_ARG;
}
const hal_err_t err = en_write(k_cfg[rail - 1].efuse_en, on);
if (err == HAL_OK) {
s_efuse_on[rail - 1] = on;
}
return err;
}
hal_err_t rail_all_off(void)
{
hal_err_t first = HAL_OK;
/* 先断五路 VBUS(走 I²C,可能失败),再关五路 eFuse(GPIO,永远成功)。
反过来的话中间有一小段「手机只靠 USB 供着电」,那既不是开也不是关。
十次都要试,不许中途 return —— 关电路径上「剩下的也别关了」是最坏的选择。 */
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
hal_err_t err = slot_vbus_enable(rail, false);
if (err != HAL_OK && first == HAL_OK) {
first = err;
}
}
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
hal_err_t err = rail_efuse_enable(rail, false);
if (err != HAL_OK && first == HAL_OK) {
first = err;
}
}
return first;
}