文件
phonerail/firmware/main/rail_cfg.c
T
zhou yang和Claude Opus 5 273c53347e A10:安全巡检的判据(SR-5 / SW-8)
判据全在 t_safety_step() 里、**放锁之后**、直接写引脚(§10.3 原话:不经任何
队列或抽象层):

  SR-5   |实测 − 该路设定值| > 100 mV **连着两圈** → 先断 VBUS 再关 eFuse
  SW-8   I < −50 mA,或 VBUS 期间电压被顶高 > +100 mV → **一圈就断 VBUS**

七条实现约定(详见需求文档 §10.2):

· 「持续 20 ms」数的是圈,不是墙钟毫秒;
· 反灌不设确认窗口,与过压那 20 ms 的不对称是有意的 —— 误断 VBUS 只丢一次
  USB 数据,漏判会把能量泵回 12V 母线连累另外四台(设计文档 §15.2);
· 判据只在「该路该有电 + 这一圈的值新鲜 + 过了 60 ms 缓启动窗口」时武装
  (对应 §10.10 的「盲跑 → 保持供电,禁用该路判据」);
· 比的是该路 DAC 设定值的影子,不是 3900 这个常数(SW-4 要单路微调);
· SR-5 关路关干净(不留「只靠 USB 供着电」的窗口),SW-8 只断 VBUS 不碰 eFuse;
· 断电之前先 t_actuator_abort(),断 VBUS 时只打断 EDL(只有它写 VBUS_EN);
· 动作在前、记录在后,事件走回调出去,A11 在那儿接环形缓冲。

判据一条 I²C 事务都不发(设定值、两根 EN、正在跑的序列全部走影子),
§10.3 那 7.8 % 的预算不变。顺带新增两处影子:rail_efuse_is_on()/rail_vbus_is_on()
与 rail_setpoint_mv(),都**只在写成功时更新**,两个方向都倒向安全的那一侧。

29 个用例;25 条注错逐个跑过,24 条变红。第一版把「持续 20 ms」按墙钟算,
测试一半红一半绿 —— 巡检的采样时刻带亚毫秒残差(一圈占 1.56 ms 总线,
而睡到下一拍按整毫秒算),相邻两次判据的时刻是 21/19 ms 交替,19 ms 那一半
要拖到第三圈 = 40 ms,超验收且间歇性。改成数圈之后一次不飘,
并留了一条跑 20 轮、两种相位都踩得到的回归拦网。

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-07 08:41:53 +08:00

304 行
9.9 KiB
C
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
/* 五路配置表 + 启动自检 + 两根 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);
}
/* 两根线的影子(契约见 rail_cfg.h)。写失败时**不更新** —— 那一次的线上电平
是未知的,而「保留上一次成功的意图」在两个方向上都倒向安全的那一侧。 */
static bool s_efuse_on[PHONERAIL_RAIL_COUNT];
static bool s_vbus_on[PHONERAIL_RAIL_COUNT];
bool rail_efuse_is_on(int rail)
{
return rail >= 1 && rail <= PHONERAIL_RAIL_COUNT && s_efuse_on[rail - 1];
}
bool rail_vbus_is_on(int rail)
{
return rail >= 1 && rail <= PHONERAIL_RAIL_COUNT && s_vbus_on[rail - 1];
}
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;
}
}
s_efuse_on[rail - 1] = false;
s_vbus_on[rail - 1] = false;
}
return HAL_OK;
}
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_vbus_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].vbus_en, on);
if (err == HAL_OK) {
s_vbus_on[rail - 1] = on;
}
return err;
}
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;
}