A7:五路配置表 + 表驱动遍历 + 启动自检

六组编号(INA 地址 / DAC 片通道 / 三个按键位 / EDL 位 / 两根 EN 的 GPIO)拼成
一张 const 表 rail_cfg(1..5),没有一列是顺排的。开机 rail_selftest() 把它和
A3~A6 四个驱动的表逐格对账,再查全板引脚重复 / 越界,不过就 abort ——
那四个自检谓词至此全部有了调用点。

判据拎成 rail_selftest_table(t) 单独可调:真表是 const 的,不这么做就没法证明
「真有事的时候查得出来」。测试复制一张表出来逐样改坏,六种坏法都验过。

顺带修一个真 bug:HAL 两侧 GPIO 边界写的是 pin < 48,而 EFUSE_EN2 就在 IO48
上 —— CH2 的 eFuse 既开不了也关不掉。sim 与 ESP 两份都改成 0~48 含。

9 条 ctest(131 个用例)全绿,idf.py build 通过,16 条注错 15 条变红
(唯一没红的那条与原因记在 HANDOFF.md)。

Co-Authored-By: Claude Opus 5 <[email protected]>
这个提交包含在:
zhou yang
2026-09-07 07:13:04 +08:00
共同撰写人 Claude Opus 5
父节点 eb3df9ff49
当前提交 45de84ed3c
共修改 13 个文件,包含 1249 行新增和 63 行删除
+1
查看文件
@@ -5,5 +5,6 @@ idf_component_register(
SRCS "app_main.c"
"version.c"
"rail.c"
"rail_cfg.c"
INCLUDE_DIRS "."
REQUIRES phonerail_hal drv)
+38 -1
查看文件
@@ -1,13 +1,50 @@
/* ESP32-S3 入口。只在 ESP-IDF 目标里编译,宿主机构建不包含本文件。 */
#include <stdlib.h>
#include "esp_log.h"
#include "hal_time.h"
#include "phonerail.h"
#include "rail_cfg.h"
static const char *TAG = "phonerail";
/* 启动自检(需求 §10.4):五路的地址 / 位号 / 引脚无重复、无越界,
而且那张五路表和四个驱动自己的表没有分家。
⚠️ **不过就 abort,不降级。** 这里查的全是编译进固件的常量表,
查不过 = 固件本身写错了,不是硬件坏了 —— §10.10 的降级矩阵管的是后者。
带着一张错表跑起来的样子是「命令发给了另一路」,五台手机全在跑、
一个错误码都不报,谁也不会去怀疑它。 */
static void selftest_or_die(void)
{
const uint32_t bad = rail_selftest();
if (bad == 0) {
ESP_LOGI(TAG, "五路配置表自检通过");
return;
}
for (uint32_t bit = 1; bit != 0; bit <<= 1) {
if ((bad & bit) != 0) {
ESP_LOGE(TAG, "自检失败:%s (0x%02x)", rail_selftest_str(bit), (unsigned)bit);
}
}
abort();
}
void app_main(void)
{
/* 引脚表与外设初始化留到 A7/A8;这里只确认 HAL 真的被链进来了。 */
ESP_LOGI(TAG, "PhoneRail %s boot, %d rails, t=%u ms", phonerail_version(),
PHONERAIL_RAIL_COUNT, (unsigned)hal_time_now_ms());
selftest_or_die();
/* 十根 EN 线配成输出并全部拉低。**先于一切外设**:DAC / eFuse 一上电就有
输出,这两根线这时必须已经确定是低的。 */
const hal_err_t err = rail_en_init();
if (err != HAL_OK) {
ESP_LOGE(TAG, "EN 引脚初始化失败:%s", hal_err_str(err));
abort();
}
ESP_LOGI(TAG, "五路输出与 VBUS 全部关闭,等待 A8 的任务骨架");
/* 器件初始化与五个任务留到 A8/A15。 */
}
+67
查看文件
@@ -0,0 +1,67 @@
/* ESP32-S3 引脚分配 —— 全固件唯一一份(设计文档 §8.1,2026-09-06 按 PCB 几何
重排的那一版,§26.18)。
⚠️ **引脚号是从「板上要去哪儿」倒推的,不是逻辑顺序。** 和 INA226 地址、
595 位号一样:查表,不要算。哪怕看上去像顺排(IO9~IO14 那一段),
也是巧合 —— `EFUSE_EN` 那五根就一个规律都没有(39 / 48 / 6 / 41 / 8)。
五路各自的 `EFUSE_ENn` / `VBUS_ENn` **不在这里**,在 `rail_cfg.h` 的那张五路表里
(它们是「每路一个」的东西,和 INA 地址、DAC 通道、按键位号是同一类)。
本文件只放**全板独一份**的那些脚。`rail_selftest()` 会把两边合起来查重。
⚠️ 这里的值与 `.claude/pcb1-parts.json` 里 U20(WROOM-1 模组)的真实焊盘网络
逐脚核对过(2026-09-07)。改动之前先回去核一遍,别照着记忆改。 */
#ifndef BOARD_PINS_H
#define BOARD_PINS_H
#ifdef __cplusplus
extern "C" {
#endif
/* ---- 全板独一份的信号(模组脚号 → GPIO) -------------------------------- */
#define BOARD_PIN_I2C_SDA 14 /* 模组脚 22 —— 6×INA226 + 2×DAC + PCA9555(+ 载板 LM75) */
#define BOARD_PIN_I2C_SCL 15 /* 模组脚 8 */
#define BOARD_PIN_FAN_TACH 16 /* 模组脚 9 → R731/R732 分压 */
#define BOARD_PIN_FAN_PWM 18 /* 模组脚 11 → R733 */
#define BOARD_PIN_UP_VBUS_DET 47 /* 模组脚 24 → J24.25,上游 VBUS 有没有插 */
#define BOARD_PIN_NTC1 1 /* 模组脚 39,ADC1_CH0 —— 必须是 IO1~IO10 */
#define BOARD_PIN_NTC2 2 /* 模组脚 38,ADC1_CH1 */
/* USB-Serial-JTAG 的 D±。**S3 上是硬连线,动不了**(模组脚 13/14 → J22)。
下载 / 调试口就是它,板上没有 JTAG 排针、也没有 USB 转串口芯片。
⚠️ 永远别烧 `DIS_USB_JTAG` eFuse —— 烧了引脚 JTAG 和 USB 两边都没有。 */
#define BOARD_PIN_USB_DM 19
#define BOARD_PIN_USB_DP 20
/* ---- 不许占用的脚 -------------------------------------------------------- */
/* strapping:挂负载会影响上电模式(S3 只有这四个是)。 */
#define BOARD_PINS_STRAPPING \
{ \
0, 3, 45, 46 \
}
/* WROOM-1 的 **R8 版(八线 PSRAM)占用**,留空以便换料。 */
#define BOARD_PINS_RESERVED \
{ \
35, 36, 37 \
}
/* ⚠️ **IO22~IO25 在 ESP32-S3 上根本不存在**(编号是连续的,芯片不是),
IO26~IO32 是模组内部的 SPI flash / PSRAM,引不出来。
写进配置表不会报错 —— `gpio_config()` 对不存在的脚只是无声无息什么也不做。 */
#define BOARD_PINS_ABSENT \
{ \
22, 23, 24, 25, \
26, 27, 28, 29, 30, 31, 32 \
}
/* ESP32-S3 的 GPIO 编号上限(IO48 **存在**,`EFUSE_EN2` 就在上面)。
⚠️ HAL 两侧的边界都写成 `pin <= BOARD_PIN_NUM_MAX`;A7 之前那里写的是
`pin < 48`,正好把 IO48 关在门外 —— 现象是 CH2 的 eFuse 既开不了也**关不掉**。 */
#define BOARD_PIN_NUM_MAX 48
#ifdef __cplusplus
}
#endif
#endif /* BOARD_PINS_H */
+1 -1
查看文件
@@ -3,7 +3,7 @@
#define PHONERAIL_H
/* 固件版本。规则:<主>.<次>.<修订>-<最后完成的任务包> */
#define PHONERAIL_FW_VERSION "0.1.0-a6"
#define PHONERAIL_FW_VERSION "0.1.0-a7"
/* 通道数。五路直供,见设计文档 §2。 */
#define PHONERAIL_RAIL_COUNT 5
+278
查看文件
@@ -0,0 +1,278 @@
/* 五路配置表 + 启动自检 + 两根 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 "sr595.h"
/* ---- 那张表 --------------------------------------------------------------
六列的出处:INA 地址 §8.2;DAC 片/通道 §8.2 + §26.18;按键位与 EDL 位
§17.2 + §26.18 C(从 `.claude/pcb1-parts.json` 的真实焊盘网络逐脚核过);
两根 EN 的 GPIO §8.1(同样按 U20 的焊盘网络核过,2026-09-07)。
⚠️ **一列都不许改成算式。** 六列里没有一列是顺排的。 */
static const rail_cfg_t k_cfg[PHONERAIL_RAIL_COUNT] = {
/* INA DAC片 ch EFUSE VBUS PWR VUP VDN EDL FLT PG */
/* CH1 */ { 0x40, 0x4A, 2, 39, 40, { 1, 2, 3 }, 16, 0, 0 },
/* CH2 */ { 0x41, 0x4A, 0, 48, 10, { 19, 17, 18 }, 7, 1, 1 },
/* CH3 */ { 0x44, 0x4B, 0, 6, 11, { 22, 21, 20 }, 6, 2, 2 },
/* CH4 */ { 0x45, 0x4A, 1, 41, 42, { 9, 10, 11 }, 0, 3, 3 },
/* CH5 */ { 0x42, 0x4B, 1, 8, 17, { 15, 14, 13 }, 5, 4, 4 },
};
_Static_assert(PHONERAIL_RAIL_COUNT == INA226_RAIL_COUNT, "五路数对不上 INA226");
_Static_assert(PHONERAIL_RAIL_COUNT == DAC63204_RAIL_COUNT, "五路数对不上 DAC63204");
_Static_assert(PHONERAIL_RAIL_COUNT == PCA9555_RAIL_COUNT, "五路数对不上 PCA9555");
const rail_cfg_t *rail_cfg(int rail)
{
assert(rail >= 1 && rail <= PHONERAIL_RAIL_COUNT); /* 悄悄返回 CH1 比崩掉危险 */
return &k_cfg[rail - 1];
}
/* ---- 自检之一:本表与四个驱动的表逐格对账 --------------------------------
分家的现象是「命令发给了另一路」,功能测试测不出来,所以这里是**逐格**比,
不是抽查。四个驱动的取值函数越界会 abort,但这里的 rail 恒在 1~5。 */
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;
}
for (int k = 0; k < SR595_KEY_COUNT; k++) {
if (c->key_bit[k] != (uint8_t)sr595_key_bit(rail, (sr595_key_t)k)) {
return false;
}
}
if (c->edl_bit != (uint8_t)sr595_edl_bit(rail)) {
return false;
}
if (c->flt_bit != (uint8_t)pca9555_flt_bit(rail)) {
return false;
}
if (c->pg_bit != (uint8_t)pca9555_pg_bit(rail)) {
return false;
}
}
return true;
}
/* ---- 自检之二:引脚无重复、无越界 ----------------------------------------
查的是**全板**,不只是五路那十个脚:重复的两个脚一定分属两个模块,
只查自己那十个的话,「`VBUS_EN2` 写成了 IO14」这种错(撞上 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;
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; /* R8 版八线 PSRAM 占着,留空以便换料 */
}
return true;
}
static bool pins_ok(const rail_cfg_t *t)
{
/* 全板用掉的每一个脚。顺序无所谓,这里只查「都能用」和「不重复」。 */
int used[2 * PHONERAIL_RAIL_COUNT + 14];
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].vbus_en;
}
used[n++] = BOARD_PIN_I2C_SDA;
used[n++] = BOARD_PIN_I2C_SCL;
used[n++] = BOARD_PIN_FAN_TACH;
used[n++] = BOARD_PIN_FAN_PWM;
used[n++] = BOARD_PIN_UP_VBUS_DET;
used[n++] = BOARD_PIN_NTC1;
used[n++] = BOARD_PIN_NTC2;
used[n++] = BOARD_PIN_USB_DM;
used[n++] = BOARD_PIN_USB_DP;
/* 四根移位链的线与 PCA9555 的中断脚:**引用驱动里的宏,不重抄一遍数字**。 */
used[n++] = SR595_PIN_SER;
used[n++] = SR595_PIN_SRCLK;
used[n++] = SR595_PIN_RCLK;
used[n++] = SR595_PIN_OE_N;
used[n++] = PCA9555_PIN_INT_N;
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; /* 两个信号抢同一个脚 */
}
}
}
/* 两路 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;
}
if (!table_matches_drivers(t)) {
bad |= RAIL_SELFTEST_TABLE;
}
if (!pins_ok(t)) {
bad |= RAIL_SELFTEST_PINS;
}
return bad;
}
uint32_t rail_selftest(void)
{
uint32_t bad = 0;
/* 四个驱动各自的表(A3~A6 都留了这个谓词,A7 是它们的归宿)。 */
if (!ina226_addr_table_ok()) {
bad |= RAIL_SELFTEST_INA;
}
if (!dac63204_map_ok()) {
bad |= RAIL_SELFTEST_DAC;
}
if (!sr595_map_ok()) {
bad |= RAIL_SELFTEST_SR595;
}
if (!pca9555_map_ok() || !pca9555_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_SR595:
return "74HC595 位表";
case RAIL_SELFTEST_PCA9555:
return "PCA9555 位表/地址";
case RAIL_SELFTEST_TABLE:
return "五路表与驱动分家";
case RAIL_SELFTEST_PINS:
return "引脚重复或越界";
default:
return "?";
}
}
/* ---- 两根 EN 线 ---------------------------------------------------------- */
/* 高有效。**全固件唯一一处极性**(和 `sr595_set_hub_reset()` 里那个反相、
`rail_mv_to_code()` 里那个减法同一个道理:多一处就多一处写反的机会)。 */
static hal_err_t en_write(int pin, bool on)
{
return hal_gpio_write(pin, on ? 1 : 0);
}
hal_err_t rail_en_init(void)
{
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
const rail_cfg_t *c = &k_cfg[rail - 1];
const int pins[2] = { c->efuse_en, c->vbus_en };
for (int i = 0; i < 2; i++) {
hal_err_t err = hal_gpio_config(pins[i], HAL_GPIO_OUTPUT);
if (err != HAL_OK) {
return err;
}
/* 配成输出的那一刻两边的 HAL 都已经在输出 0 了,这一句是把「关」
写成明文 —— 别让「五路上电即全关」依赖某个默认值。 */
err = en_write(pins[i], false);
if (err != HAL_OK) {
return err;
}
}
}
return HAL_OK;
}
hal_err_t rail_efuse_enable(int rail, bool on)
{
if (rail < 1 || rail > PHONERAIL_RAIL_COUNT) {
return HAL_ERR_ARG;
}
return en_write(k_cfg[rail - 1].efuse_en, on);
}
hal_err_t rail_vbus_enable(int rail, bool on)
{
if (rail < 1 || rail > PHONERAIL_RAIL_COUNT) {
return HAL_ERR_ARG;
}
return en_write(k_cfg[rail - 1].vbus_en, on);
}
hal_err_t rail_all_off(void)
{
hal_err_t first = HAL_OK;
/* 先断五路 VBUS,再关五路 eFuse:反过来的话中间有一小段「手机只靠 USB
供着电」,那既不是开也不是关。十个都要试,不许中途 return。 */
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
hal_err_t err = rail_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;
}
+120
查看文件
@@ -0,0 +1,120 @@
/* 五路一张表(需求 §10.4)—— **禁止用通道号算任何东西**。
板上和「第几路」有关的编号有六组,**没有一组是顺排的,也没有两组是一致的**:
CH INA226 DAC 按键位(PWR/VUP/VDN) EDL 位 EFUSE_EN VBUS_EN
1 0x40 U6.OUT2 1 2 3 16 IO39 IO40
2 0x41 U6.OUT0 19 17 18 7 IO48 IO10
3 0x44 **U7**.OUT0 22 21 20 6 IO6 IO11
4 0x45 U6.OUT1 9 10 11 0 IO41 IO42
5 0x42 U7.OUT1 15 14 13 5 IO8 IO17
六组编号各有各的来历(I²C 地址按布线难度、595 位号按 PCB 几何、GPIO 按出线
方向),三条都在设计文档里:§8.2 / §17.2 + §26.18 C / §8.1。
⚠️ **这一片写错了,板子照样正常工作** —— 只是「重启 CH1」重启了 CH4、
「读 CH3 电流」读的是 CH5。功能测试测不出来,五台手机跑起来也看不出来,
要等到某一路真出故障、而你关掉了另一路的时候才暴露。所以:
**除了本文件里这张表,任何地方都不许出现 `0x40 + rail`、`rail - 1`
这类由通道号算出来的器件编号。**
---- 这张表和四个驱动里那四张表是什么关系 ----------------------------------
A3~A6 四个驱动各自带着自己那一张表(`ina226_rail_addr()` 等),因为驱动要能
单独测。本表是**把它们横着拼起来**的那一张,多出来的只有两列 GPIO
(`EFUSE_EN` / `VBUS_EN` 是 GPIO 直连,没有驱动可挂)。
两处写同一个数就有分家的风险,所以 `rail_selftest()` 会**逐格**拿本表和四个
驱动的取值函数对账,对不上直接判失败(`RAIL_SELFTEST_TABLE`)。
宿主机 `ctest` 每次都跑,上板 `app_main()` 开机也跑一次。
—— 之所以不做成「开机时从驱动现算」,是因为那样**按定义就查不出分家**,
而且表就不是 const 的了,用之前得先记着初始化。 */
#ifndef RAIL_CFG_H
#define RAIL_CFG_H
#include <stdbool.h>
#include <stdint.h>
#include "hal_err.h"
#include "phonerail.h"
#include "sr595.h" /* sr595_key_t:按键位那三格的下标就是它 */
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
uint8_t ina_addr; /* 0x40 0x41 0x44 0x45 0x42 —— 查表,不能算 */
uint8_t dac_addr; /* 0x4A: CH1/2/4 0x4B: CH3/5 —— 也不是「前三路在 U6」 */
uint8_t dac_ch; /* 该片上的 OUT 号(0~3) */
/* GPIO 直连,安全关键(§8.1「任何情况下都要能关电」)。
类型是 `int`,因为 `hal_gpio_*()` 收的就是 int;本表是唯一的出处。 */
int efuse_en;
int vbus_en;
/* 24 位移位链上的位号。下标用 `SR595_KEY_PWR/VUP/VDN`,别用 0/1/2。 */
uint8_t key_bit[SR595_KEY_COUNT];
uint8_t edl_bit;
uint8_t flt_bit; /* PCA9555 **Port0** bit0~4 */
uint8_t pg_bit; /* PCA9555 **Port1** bit0~4 —— 两个端口长得一模一样,别搞反 */
} rail_cfg_t;
/* rail 是 **1~5**(不是 0~4)。越界直接 abort:那是调用方算错了路号,
悄悄返回 CH1 的配置比崩掉危险得多。 */
const rail_cfg_t *rail_cfg(int rail);
/* ---- 启动自检(需求 §10.4 的验收项) ------------------------------------
全部是纯计算,不碰总线也不碰引脚,什么时候调都行。
返回 0 = 全过;非 0 是下面这些位的或。 */
typedef enum {
RAIL_SELFTEST_INA = 1u << 0, /* ina226_addr_table_ok() 不过 */
RAIL_SELFTEST_DAC = 1u << 1, /* dac63204_map_ok() 不过 */
RAIL_SELFTEST_SR595 = 1u << 2, /* sr595_map_ok() 不过 */
RAIL_SELFTEST_PCA9555 = 1u << 3, /* pca9555_map_ok() / _addr_ok() 不过 */
RAIL_SELFTEST_TABLE = 1u << 4, /* 本表和四个驱动的表分家了 */
RAIL_SELFTEST_PINS = 1u << 5, /* EN 引脚重复 / 越界 / 撞上别的信号 */
} rail_selftest_bit_t;
uint32_t rail_selftest(void);
/* 只查**一张给定的**五路表(`RAIL_SELFTEST_TABLE` / `RAIL_SELFTEST_PINS` 两位;
另外四位是四个驱动自己的表,和 t 无关)。`t` 要指向 PHONERAIL_RAIL_COUNT 行。
存在的理由:**自检逻辑本身也得被测。** 拿真表跑一遍只能证明「现在没事」,
证明不了「真有事的时候查得出来」—— 而真表是 const 的,测试没法把它改坏。
所以判据单拎出来,测试喂几张故意写坏的表进去看它认不认。 */
uint32_t rail_selftest_table(const rail_cfg_t *t);
/* 单个失败位的人话名字,用来打日志。传 0 或者多于一位返回 "?"。 */
const char *rail_selftest_str(uint32_t bit);
/* ---- 两根 EN 线(表驱动遍历) --------------------------------------------
`EFUSE_EN` / `VBUS_EN` 都是**高有效**,而且都在板上带 100 kΩ 下拉
(R114 / R701~705)—— ESP32 复位期间 GPIO 高阻,五路输出全关、VBUS 全断。
极性只在 rail_cfg.c 的 `en_write()` 里出现这一次,上层一律传 bool。 */
/* 十个脚配成输出并**全部拉低**(= 五路全关、VBUS 全断),然后才谈别的。
⚠️ 必须在 `rail_volt_init()` 之前调:DAC 上电那一刻输出就有电压了,
eFuse 的 EN 这时得已经确定是低的。任何一个脚配失败就返回 HAL_ERR_ARG。 */
hal_err_t rail_en_init(void);
/* rail 越界 → HAL_ERR_ARG;没 `rail_en_init()` 过 → HAL_ERR_ARG(HAL 会拦)。 */
hal_err_t rail_efuse_enable(int rail, bool on);
hal_err_t rail_vbus_enable(int rail, bool on);
/* 五路全关:**先断 VBUS 再关 eFuse**,中间不留「只靠 USB 供着电」的窗口。
十个脚全部试一遍,不因为某一个失败就提前返回 —— 关电路径上「剩下九个也别关了」
是最坏的选择。返回第一个错误码(全成功就是 HAL_OK)。
⚠️ 这是复位 / 收尾路径。A10 的故障快路径要直接调 `rail_efuse_enable(rail,false)`,
别绕这里:过压时该立刻断的是出事那一路,不是等着遍历五路。 */
hal_err_t rail_all_off(void);
#ifdef __cplusplus
}
#endif
#endif /* RAIL_CFG_H */