判据全在 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]>
135 行
7.2 KiB
C
135 行
7.2 KiB
C
/* 五路一张表(需求 §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);
|
||
|
||
/* 这两根线**最后一次成功写进去**的值(上电与 `rail_en_init()` 之后都是 false)。
|
||
|
||
存在的理由是 A10 的判据要知道「这一路现在该不该有电」:关着的路电压本来就是 0,
|
||
拿它去和设定值比,每一圈都会得出「偏低 3900 mV」,于是巡检会没完没了地
|
||
「再关一次已经关着的路」并刷事件。判据只在这两个影子为 true 时武装。
|
||
|
||
⚠️ **只在写成功时更新**,而且方向是有讲究的:关的那一次写失败 ⇒ 影子留在 on
|
||
⇒ 判据继续武装(还会再关一次);开的那一次写失败 ⇒ 影子留在 off
|
||
⇒ 判据不武装(不会对着一路没开成的电报偏差)。两边都倒向安全的那一侧。
|
||
—— 不去读回引脚,是因为 `hal_gpio_read()` 读输出脚在两个目标上的语义不一样,
|
||
而这里要的本来就是「固件的意图」,不是「线上的电平」。 */
|
||
bool rail_efuse_is_on(int rail);
|
||
bool rail_vbus_is_on(int rail);
|
||
|
||
/* 五路全关:**先断 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 */
|