文件
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

759 行
32 KiB
C

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
/* A15:启动顺序 + 降级矩阵(需求 §10.9 / §10.10,计划 A15 那一行)。
验收原话:「逐个『拔掉』器件,行为与 §10.10 的表逐行对上;五路上电错开 100 ms;
`HUB_RST` 只脉冲一次」,外加 ⭐ 第一条用例必须是 `ina226_set_reversed()` 那一行。
---- 这块假板子和别的测试有什么不一样 ------------------------------------
· **PGOOD 跟着 EN 走**:`sim_gpio_set_hook()` 让某一路 `EFUSE_EN` 抬高的那一刻,
那一片 PCA9555 的 P1.3 跟着变高。没有这一条,「等 PG」要么永远等到(PG 静态拉高,
测不出超时),要么永远等不到。
· 时刻一律从 `sim_gpio_rise_us()` 取(线上真实的边),**不信** `bringup_report_t`
自己报的 `on_ms` —— 那是自己证明自己。
· 事件从 flash 读回来数(evlog 的真路径),不在被测代码里插探针。 */
#include <string.h>
#include "board_pins.h"
#include "bringup.h"
#include "ch224a.h"
#include "dac63204.h"
#include "evlog.h"
#include "hal_gpio.h"
#include "i2cbus.h"
#include "ina226.h"
#include "pca9555.h"
#include "rail.h"
#include "rail_cfg.h"
#include "sim_hal.h"
#include "slot.h"
#include "tca9548a.h"
#include "unity.h"
#define N PHONERAIL_RAIL_COUNT
#define ALL ((uint8_t)((1u << N) - 1u))
#define BIT(rail) ((uint8_t)(1u << ((rail) - 1)))
/* Port1 静息:FLT# 高(无故障)、Buck PG 高、ALERT 高、**eFuse PG 低**(还没上电)。 */
#define P1_IDLE ((uint8_t)(0x0Fu & ~(1u << PCA9555_BIT_EFUSE_PG)))
#define P1_PG ((uint8_t)0x0Fu)
static bool g_pg_follows[N]; /* false = 这一路的 PG 永远不来 */
static bool g_flt[N]; /* true = 这一路的 FLT# 在报 */
static bool g_pca_gone[N]; /* 这一片被拔了:钩子别再去驱它 */
static bringup_report_t g_r;
static bool g_pd_on_trunk = true; /* false = board() 不在主干上挂 CH224A */
static int ch_of(int rail) { return (int)rail_cfg(rail)->mux_ch; }
/* 外部世界:EN 一抬高 PG 就来(真板上是 ~17 ms 之后;这里早来晚来只改变「等了几圈」)。 */
static void world(int pin, int level)
{
for (int rail = 1; rail <= N; rail++) {
if (pin != rail_cfg(rail)->efuse_en || g_pca_gone[rail - 1]) {
continue;
}
uint8_t v = (level != 0 && g_pg_follows[rail - 1]) ? P1_PG : P1_IDLE;
if (g_flt[rail - 1]) {
v &= (uint8_t)~(1u << PCA9555_BIT_EFUSE_FLT);
}
sim_pca9555_drive(rail - 1, 1, v);
}
}
static void put_ina_ids(int ch, uint8_t addr)
{
if (ch < 0) {
sim_i2c_set_reg(addr, INA226_REG_MFG_ID, INA226_MFG_ID);
sim_i2c_set_reg(addr, INA226_REG_DIE_ID, INA226_DIE_ID);
} else {
sim_i2c_set_reg_ch(ch, addr, INA226_REG_MFG_ID, INA226_MFG_ID);
sim_i2c_set_reg_ch(ch, addr, INA226_REG_DIE_ID, INA226_DIE_ID);
}
}
/* 满配、五槽全插的假板子。各用例在 `bringup_devices()` 之前「拔掉」自己关心的那一个。 */
static void board(void)
{
sim_i2c_add_mux(TCA9548A_ADDR, BOARD_PIN_MUX_RST_N);
for (int i = 0; i < DAC63204_COUNT; i++) {
const uint8_t a = dac63204_all_addr[i];
sim_i2c_add(a, 2);
sim_i2c_set_reg(a, DAC63204_REG_GENERAL_STATUS, (uint16_t)(DAC63204_DEVICE_ID << 2));
sim_i2c_set_reg(a, DAC63204_REG_COMMON_CONFIG, 0x0FFF);
}
/* NVM 默认:五路都是 3.90 V(上电复位时读回来比对的就是它)。 */
for (int rail = 1; rail <= N; rail++) {
sim_i2c_set_reg(dac63204_rail_addr(rail), DAC63204_REG_DATA(dac63204_rail_ch(rail)),
DAC63204_CODE_TO_REG(rail_mv_to_code(RAIL_MV_SAFE)));
}
sim_i2c_add(INA226_ADDR_INPUT, 2);
put_ina_ids(-1, INA226_ADDR_INPUT);
sim_i2c_set_reg(INA226_ADDR_INPUT, INA226_REG_BUS_V, (uint16_t)(12000 * 4 / 5));
/* CH224A:实测在主干(PROGRESS 顶部),20 V / 3 A 档 = 60 W。 */
if (g_pd_on_trunk) {
sim_i2c_add(CH224A_ADDR_DEFAULT, 1);
sim_i2c_set_reg(CH224A_ADDR_DEFAULT, CH224A_REG_CURRENT, 3000 / 50);
}
for (int rail = 1; rail <= N; rail++) {
const int ch = ch_of(rail);
sim_i2c_add_ch(ch, rail_cfg(rail)->ina_addr, 2);
put_ina_ids(ch, rail_cfg(rail)->ina_addr);
sim_pca9555_attach_ch(rail - 1, ch, PCA9555_ADDR);
sim_pca9555_drive(rail - 1, 1, P1_IDLE);
sim_gpio_drive(rail_cfg(rail)->prsnt_n, 0); /* 插着:刀片把 PRSNT# 短到地 */
g_pg_follows[rail - 1] = true;
g_flt[rail - 1] = false;
g_pca_gone[rail - 1] = false;
}
sim_gpio_set_hook(world);
}
static void pull_slot(int rail) /* 整片刀片拔走:PRSNT# 高 + 两片器件都不在 */
{
sim_gpio_drive(rail_cfg(rail)->prsnt_n, 1);
sim_i2c_set_present_ch(ch_of(rail), rail_cfg(rail)->ina_addr, false);
sim_i2c_set_present_ch(ch_of(rail), PCA9555_ADDR, false);
g_pca_gone[rail - 1] = true;
}
static void devices(bool por)
{
const bringup_in_t in = { .power_on_reset = por };
TEST_ASSERT_EQUAL_INT(HAL_OK, bringup_devices(&in, &g_r));
}
/* 某类事件(可限定路号,-1 = 不限)在 flash 里有几条。 */
static int count_ev(log_ev_t type, int rail)
{
(void)evlog_flush(0);
evlog_stats_t st;
evlog_stats(&st);
int n = 0;
evlog_rec_t rec;
for (uint32_t seq = st.oldest_seq; seq < st.next_seq; seq++) {
if (evlog_read(seq, &rec, 1) != 1) {
break;
}
if (rec.type == (uint8_t)type && (rail < 0 || rec.rail == (uint8_t)rail)) {
n++;
}
}
return n;
}
static bool last_ev(log_ev_t type, evlog_rec_t *out)
{
(void)evlog_flush(0);
evlog_stats_t st;
evlog_stats(&st);
bool hit = false;
evlog_rec_t rec;
for (uint32_t seq = st.oldest_seq; seq < st.next_seq; seq++) {
if (evlog_read(seq, &rec, 1) == 1 && rec.type == (uint8_t)type) {
*out = rec;
hit = true;
}
}
return hit;
}
void setUp(void)
{
sim_hal_reset();
pca9555_shadow_reset();
rail_setpoint_reset();
ina226_set_reversed(INA226_ADDR_INPUT, false); /* 进程里是活的:清掉上一条用例留下的 */
board();
TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init_ex(0));
}
void tearDown(void) { TEST_ASSERT_EQUAL_UINT32(0, sim_i2c_ambiguous()); }
/* ---- ⭐ 第一条:输入侧 INA226 的对调接 ------------------------------------ */
/* 计划 A15 的 ⭐:这一行以前只在 `app_main.c` 里,唯一的拦网是人眼。
板上 U13 的 IN+/IN− 对调 ⇒ 正常供电时芯片自己读出的电流是**负**的;
启动之后驱动必须把它翻回来,否则整板功率与反灌判据全拿的是反号。
⚠️ 断的是「读出来的电流是正的」这个**后果**,不只是那个标志位。 */
static void test_first_input_ina_is_marked_reversed(void)
{
/* 12 V × 2 A 正常供电,芯片按对调接读出 −2 A。 */
sim_i2c_set_reg(INA226_ADDR_INPUT, INA226_REG_CURRENT, (uint16_t)(int16_t)(-2000000 / 500));
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_init(NULL));
ina226_vi_t vi;
TEST_ASSERT_EQUAL_INT(HAL_OK, ina226_read_vi(INA226_ADDR_INPUT, &vi));
TEST_ASSERT_TRUE_MESSAGE(vi.current_ua < 0, "前提:不登记对调时读出来是负的");
devices(true);
TEST_ASSERT_TRUE(ina226_is_reversed(INA226_ADDR_INPUT));
TEST_ASSERT_EQUAL_INT(HAL_OK, ina226_read_vi(INA226_ADDR_INPUT, &vi));
TEST_ASSERT_EQUAL_INT32(2000000, vi.current_ua);
/* 刀片上那五片是正接的,别被一起翻了。 */
TEST_ASSERT_FALSE(ina226_is_reversed(INA226_ADDR_RAIL));
}
/* ---- 满配:什么都在 ------------------------------------------------------ */
/* 六片 INA226 都写了 Config / Calibration。A14 之前这一步**根本没做**
(app_main 里那句「在那之前 t_safety 读回来的电流恒为 0」)。 */
static void test_full_board_initialises_every_device(void)
{
devices(true);
TEST_ASSERT_EQUAL_UINT32(0, g_r.deg);
TEST_ASSERT_EQUAL_UINT8(0, g_r.empty | g_r.blind | g_r.io_lost);
TEST_ASSERT_EQUAL_UINT16(INA226_CALIB, sim_i2c_get_reg(INA226_ADDR_INPUT, INA226_REG_CALIB));
TEST_ASSERT_EQUAL_UINT16(INA226_CONFIG, sim_i2c_get_reg(INA226_ADDR_INPUT, INA226_REG_CONFIG));
for (int rail = 1; rail <= N; rail++) {
TEST_ASSERT_EQUAL_UINT16(INA226_CALIB, sim_i2c_get_reg_ch(ch_of(rail), INA226_ADDR_RAIL,
INA226_REG_CALIB));
/* PCA9555 按板级方向字配好,锁存器全 0(键全松、VBUS 断)。 */
TEST_ASSERT_EQUAL_UINT8(PCA9555_CONFIG0_INIT, sim_pca9555_config(rail - 1, 0));
TEST_ASSERT_EQUAL_UINT8(0, sim_pca9555_output(rail - 1, 0));
TEST_ASSERT_EQUAL_UINT16(RAIL_MV_SAFE, (uint16_t)g_r.dac_nvm_mv[rail - 1]);
/* 第一段一路电都不上。 */
TEST_ASSERT_FALSE(rail_efuse_is_on(rail));
TEST_ASSERT_EQUAL_INT(0, sim_gpio_level(rail_cfg(rail)->efuse_en));
}
TEST_ASSERT_EQUAL_UINT32(0, sim_pca9555_unsafe_enable());
TEST_ASSERT_EQUAL_UINT32(0, sim_pca9555_gnd_driven());
TEST_ASSERT_EQUAL_UINT32(60000, g_r.pd_limit_mw);
TEST_ASSERT_EQUAL_INT(0, count_ev(LOG_EV_I2C_LOST, -1));
TEST_ASSERT_EQUAL_INT(1, count_ev(LOG_EV_PD_LIMIT, 0));
}
/* ⭐ §10.9 第 5 步:五路上电**错开 100 ms**,按 CH1→CH5 的顺序。
时刻取线上的边,不取被测代码自报的 `on_ms`。上界 = 100 ms + 一次轮询 + 总线时间。 */
static void test_five_rails_come_up_100ms_apart(void)
{
devices(true);
bringup_power_restore(ALL, &g_r);
TEST_ASSERT_EQUAL_UINT8(ALL, g_r.on);
TEST_ASSERT_EQUAL_UINT8(0, g_r.pg_fail | g_r.unverified);
for (int rail = 2; rail <= N; rail++) {
const uint64_t a = sim_gpio_rise_us(rail_cfg(rail - 1)->efuse_en);
const uint64_t b = sim_gpio_rise_us(rail_cfg(rail)->efuse_en);
TEST_ASSERT_TRUE_MESSAGE(a != 0 && b > a, "后一路必须在前一路之后抬高");
TEST_ASSERT_TRUE_MESSAGE(b - a >= BRINGUP_STAGGER_MS * 1000u, "错开不足 100 ms");
TEST_ASSERT_TRUE_MESSAGE(b - a < (BRINGUP_STAGGER_MS + BRINGUP_PG_POLL_MS + 2u) * 1000u,
"错开拖得太久:前一路的 PG 等待不该拖住后一路");
TEST_ASSERT_TRUE(rail_efuse_is_on(rail));
}
/* 上电序列从不开 VBUS(那是命令的事)。 */
for (int rail = 1; rail <= N; rail++) {
TEST_ASSERT_FALSE(slot_vbus_is_on(rail));
}
}
/* 只恢复「上次开着的」那几路;跳过的路**不占** 100 ms 的格子。 */
static void test_only_wanted_rails_come_up(void)
{
devices(true);
bringup_power_restore(BIT(1) | BIT(4), &g_r);
TEST_ASSERT_EQUAL_UINT8(BIT(1) | BIT(4), g_r.on);
TEST_ASSERT_EQUAL_UINT64(0, sim_gpio_rise_us(rail_cfg(2)->efuse_en));
TEST_ASSERT_EQUAL_UINT64(0, sim_gpio_rise_us(rail_cfg(3)->efuse_en));
const uint64_t d = sim_gpio_rise_us(rail_cfg(4)->efuse_en) -
sim_gpio_rise_us(rail_cfg(1)->efuse_en);
TEST_ASSERT_TRUE(d >= 100000u && d < 110000u);
}
/* 新板(NVS 里什么都没有)= 0 = 一路都不开,而且一个时钟都不花。 */
static void test_nothing_wanted_nothing_powered(void)
{
devices(true);
const uint32_t delays = sim_time_delay_calls();
bringup_power_restore(0, &g_r);
TEST_ASSERT_EQUAL_UINT8(0, g_r.on);
TEST_ASSERT_EQUAL_UINT32(delays, sim_time_delay_calls());
}
/* ---- `HUB_RST#` 只脉冲一次 ----------------------------------------------- */
/* 上电复位:正好一个低脉冲(写两次:0 再 1),而且第二段**一个字都不再写它**。 */
static void test_hub_reset_pulses_exactly_once_on_power_on(void)
{
devices(true);
bringup_power_restore(ALL, &g_r);
TEST_ASSERT_EQUAL_UINT32(2, sim_gpio_writes(BOARD_PIN_HUB_RST_N));
TEST_ASSERT_EQUAL_INT(1, sim_gpio_level(BOARD_PIN_HUB_RST_N));
}
/* 其余复位原因(看门狗 / esptool 的 USB 复位 / esp_restart):**碰都不碰**,
连「配成输出」都不做 —— 配成输出那一刻 HAL 两侧都是输出 0 = 就是一次复位。 */
static void test_hub_reset_untouched_on_warm_reboot(void)
{
devices(false);
bringup_power_restore(ALL, &g_r);
TEST_ASSERT_EQUAL_UINT32(0, sim_gpio_writes(BOARD_PIN_HUB_RST_N));
TEST_ASSERT_EQUAL_INT(-1, sim_gpio_level(BOARD_PIN_HUB_RST_N)); /* 没配过 */
}
/* ---- §10.10 逐行 --------------------------------------------------------- */
/* 行 1:某片 INA226 无应答 ⇒ **保持供电**、标盲跑、记事件。PG 照样从 PCA9555 来。 */
static void test_row_rail_ina_missing_keeps_power(void)
{
sim_i2c_set_present_ch(ch_of(3), INA226_ADDR_RAIL, false);
devices(true);
bringup_power_restore(ALL, &g_r);
TEST_ASSERT_EQUAL_UINT8(BIT(3), g_r.blind);
TEST_ASSERT_EQUAL_UINT8(ALL, g_r.on);
TEST_ASSERT_TRUE(rail_efuse_is_on(3));
evlog_rec_t rec;
TEST_ASSERT_TRUE(last_ev(LOG_EV_I2C_LOST, &rec));
TEST_ASSERT_EQUAL_UINT8(3, rec.rail);
TEST_ASSERT_EQUAL_UINT8(INA226_ADDR_RAIL, rec.arg);
TEST_ASSERT_EQUAL_INT(1, count_ev(LOG_EV_I2C_LOST, -1));
}
/* 行 3:DAC 无应答 ⇒ **不关输出**;另一片照样初始化;影子回到 NVM 默认。 */
static void test_row_dac_missing_does_not_stop_the_other_or_the_rails(void)
{
rail_setpoint_reset();
sim_i2c_set_present(DAC63204_ADDR_U7, false);
devices(true);
bringup_power_restore(ALL, &g_r);
TEST_ASSERT_TRUE((g_r.deg & BRINGUP_DEG_DAC_U7) != 0);
TEST_ASSERT_FALSE((g_r.deg & BRINGUP_DEG_DAC_U6) != 0);
/* U6 真的被初始化了:内部基准开、用到的通道上电(不再是复位值 0x0FFF)。 */
TEST_ASSERT_EQUAL_UINT16(dac63204_cc_run(DAC63204_ADDR_U6),
sim_i2c_get_reg(DAC63204_ADDR_U6, DAC63204_REG_COMMON_CONFIG));
TEST_ASSERT_EQUAL_UINT8(ALL, g_r.on);
for (int rail = 1; rail <= N; rail++) {
TEST_ASSERT_EQUAL_INT(RAIL_MV_SAFE, rail_setpoint_mv(rail));
}
evlog_rec_t rec;
TEST_ASSERT_TRUE(last_ev(LOG_EV_I2C_LOST, &rec));
TEST_ASSERT_EQUAL_UINT8(0, rec.rail);
TEST_ASSERT_EQUAL_UINT8(DAC63204_ADDR_U7, rec.arg);
}
/* 行 4:某片 PCA9555 无应答 ⇒ **不关输出**,PG 改按 INA226 电压判。电压到了就算好。 */
static void test_row_pca_missing_falls_back_to_voltage(void)
{
sim_i2c_set_present_ch(ch_of(2), PCA9555_ADDR, false);
g_pca_gone[1] = true;
sim_i2c_set_reg_ch(ch_of(2), INA226_ADDR_RAIL, INA226_REG_BUS_V, (uint16_t)(3900 * 4 / 5));
devices(true);
bringup_power_restore(ALL, &g_r);
TEST_ASSERT_EQUAL_UINT8(BIT(2), g_r.io_lost);
TEST_ASSERT_EQUAL_UINT8(ALL, g_r.on);
TEST_ASSERT_EQUAL_UINT8(0, g_r.pg_fail);
TEST_ASSERT_EQUAL_INT(1, count_ev(LOG_EV_I2C_LOST, 2));
}
/* 同一行的反面:PCA9555 看不见、电压也一直起不来 ⇒ 超时关回去,arg 标明是按电压判的。 */
static void test_row_pca_missing_and_no_voltage_times_out(void)
{
sim_i2c_set_present_ch(ch_of(2), PCA9555_ADDR, false);
g_pca_gone[1] = true;
devices(true);
bringup_power_restore(ALL, &g_r);
TEST_ASSERT_EQUAL_UINT8(BIT(2), g_r.pg_fail);
TEST_ASSERT_FALSE(rail_efuse_is_on(2));
TEST_ASSERT_EQUAL_UINT8((uint8_t)(ALL & ~BIT(2)), g_r.on);
evlog_rec_t rec;
TEST_ASSERT_TRUE(last_ev(LOG_EV_PG_TIMEOUT, &rec));
TEST_ASSERT_EQUAL_UINT8(2, rec.rail);
TEST_ASSERT_EQUAL_UINT8(BRINGUP_ARG_BY_VOLT, rec.arg);
}
/* PCA9555 与 INA226 都看不见:**开着**、报「确认不了」,不当成坏了去断电。 */
static void test_row_pca_and_ina_missing_stays_on_unverified(void)
{
sim_i2c_set_present_ch(ch_of(5), PCA9555_ADDR, false);
sim_i2c_set_present_ch(ch_of(5), INA226_ADDR_RAIL, false);
g_pca_gone[4] = true;
devices(true);
bringup_power_restore(ALL, &g_r);
TEST_ASSERT_EQUAL_UINT8(BIT(5), g_r.unverified);
TEST_ASSERT_TRUE(rail_efuse_is_on(5));
TEST_ASSERT_EQUAL_INT(0, count_ev(LOG_EV_PG_TIMEOUT, -1));
}
/* 行 5:mux 选不中 ⇒ 先脉冲一次 `MUX_RST#`,仍不通 ⇒ **五路一路都不上电** + MUX_LOST。 */
static void test_row_mux_missing_pulses_reset_then_powers_nothing(void)
{
sim_i2c_set_present(TCA9548A_ADDR, false);
devices(true);
const uint32_t resets = sim_i2c_mux_resets();
bringup_power_restore(ALL, &g_r);
TEST_ASSERT_TRUE((g_r.deg & BRINGUP_DEG_MUX) != 0);
TEST_ASSERT_EQUAL_UINT32(1, i2cbus_recoveries()); /* 就一次,不是每试一次复位一次 */
TEST_ASSERT_EQUAL_UINT32(resets, sim_i2c_mux_resets()); /* 第二段不再动它 */
TEST_ASSERT_EQUAL_UINT8(0, g_r.on);
for (int rail = 1; rail <= N; rail++) {
TEST_ASSERT_EQUAL_UINT64(0, sim_gpio_rise_us(rail_cfg(rail)->efuse_en));
}
TEST_ASSERT_EQUAL_INT(1, count_ev(LOG_EV_MUX_LOST, 0));
/* 整板级记一条就够:不逐路刷十条,也不替够不着的主干器件刷 I2C_LOST
(主干要经 mux 写「全关」,mux 死了主干也够不着 —— bringup.c 的那段)。 */
TEST_ASSERT_EQUAL_INT(0, count_ev(LOG_EV_I2C_LOST, -1));
TEST_ASSERT_EQUAL_UINT32(60000, g_r.pd_limit_mw);
for (int rail = 1; rail <= N; rail++) {
TEST_ASSERT_EQUAL_INT(RAIL_MV_SAFE, rail_setpoint_mv(rail));
}
}
/* 同一行:复位**救回来了**(第一次认件失败、复位后应答)⇒ 照常起。 */
static void test_row_mux_recovers_after_one_reset(void)
{
sim_i2c_inject_nak(TCA9548A_ADDR, 1); /* 只 NAK 认件那一次 */
devices(true);
bringup_power_restore(ALL, &g_r);
TEST_ASSERT_FALSE((g_r.deg & BRINGUP_DEG_MUX) != 0);
TEST_ASSERT_EQUAL_UINT32(1, i2cbus_recoveries());
TEST_ASSERT_EQUAL_UINT8(ALL, g_r.on);
TEST_ASSERT_EQUAL_INT(0, count_ev(LOG_EV_MUX_LOST, -1));
}
/* 行 6:`PRSNT#` 高 ⇒ 不初始化(一次总线都不发)、不上电;想开的才记 SLOT_EMPTY。 */
static void test_row_empty_slot_is_never_touched(void)
{
pull_slot(4);
devices(true);
TEST_ASSERT_EQUAL_UINT8(BIT(4), g_r.empty);
TEST_ASSERT_EQUAL_UINT32(0, sim_i2c_dev_ch(ch_of(4), PCA9555_ADDR)->n_write);
TEST_ASSERT_EQUAL_UINT32(0, sim_i2c_dev_ch(ch_of(4), INA226_ADDR_RAIL)->n_write);
TEST_ASSERT_EQUAL_INT(0, count_ev(LOG_EV_I2C_LOST, -1)); /* 空槽不是失联 */
bringup_power_restore(ALL, &g_r);
TEST_ASSERT_EQUAL_UINT8((uint8_t)(ALL & ~BIT(4)), g_r.on);
TEST_ASSERT_EQUAL_UINT64(0, sim_gpio_rise_us(rail_cfg(4)->efuse_en));
TEST_ASSERT_EQUAL_INT(1, count_ev(LOG_EV_SLOT_EMPTY, 4));
}
static void test_row_empty_slot_not_wanted_is_silent(void)
{
pull_slot(4);
devices(true);
bringup_power_restore((uint8_t)(ALL & ~BIT(4)), &g_r);
TEST_ASSERT_EQUAL_INT(0, count_ev(LOG_EV_SLOT_EMPTY, -1)); /* 三槽机器每次开机不刷事件 */
}
/* 行 7:CH224A 读不到 ⇒ **不关输出**,按 60 W。 */
static void test_row_pd_missing_assumes_60w(void)
{
sim_i2c_set_present(CH224A_ADDR_DEFAULT, false);
devices(true);
bringup_power_restore(ALL, &g_r);
TEST_ASSERT_TRUE((g_r.deg & BRINGUP_DEG_PD) != 0);
TEST_ASSERT_EQUAL_UINT32(60000, g_r.pd_limit_mw);
TEST_ASSERT_EQUAL_UINT8(ALL, g_r.on);
evlog_rec_t rec;
TEST_ASSERT_TRUE(last_ev(LOG_EV_PD_LIMIT, &rec));
TEST_ASSERT_EQUAL_UINT8(1, rec.arg);
TEST_ASSERT_EQUAL_INT32(600, rec.bus_mv); /* mW/100 */
}
/* 同一行:档位偏低 ⇒ 照读到的来(真相比保守值更可信),标 PD_LOW。 */
static void test_row_pd_low_uses_what_it_reads(void)
{
sim_i2c_set_reg(CH224A_ADDR_DEFAULT, CH224A_REG_CURRENT, 2250 / 50); /* 45 W */
devices(true);
TEST_ASSERT_TRUE((g_r.deg & BRINGUP_DEG_PD_LOW) != 0);
TEST_ASSERT_FALSE((g_r.deg & BRINGUP_DEG_PD) != 0);
TEST_ASSERT_EQUAL_UINT32(45000, g_r.pd_limit_mw);
}
/* 设计文档说 CH224A 在 mux 通道 5,实测在主干 —— 两处都得找得到。 */
static void test_pd_found_behind_channel_5_too(void)
{
sim_hal_reset();
pca9555_shadow_reset();
g_pd_on_trunk = false; /* 主干上不挂 —— 同址挂两处,sim 会先匹配到主干那颗 */
board();
g_pd_on_trunk = true;
TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init_ex(0));
sim_i2c_add_ch(TCA9548A_CH_PD, CH224A_ADDR_DEFAULT, 1);
sim_i2c_set_reg_ch(TCA9548A_CH_PD, CH224A_ADDR_DEFAULT, CH224A_REG_CURRENT, 5000 / 50);
devices(true);
TEST_ASSERT_EQUAL_UINT32(0, g_r.deg);
TEST_ASSERT_EQUAL_UINT32(100000, g_r.pd_limit_mw);
}
/* 行 8:看门狗复位 ⇒ BOOT 事件带复位原因;输出照 §10.9 恢复。 */
static void test_row_watchdog_reset_is_recorded_in_boot(void)
{
sim_hal_reboot();
pca9555_shadow_reset();
board();
TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init_ex(7)); /* ESP_RST_TASK_WDT = 7 */
devices(false);
bringup_power_restore(ALL, &g_r);
evlog_rec_t rec;
TEST_ASSERT_TRUE(last_ev(LOG_EV_BOOT, &rec));
TEST_ASSERT_EQUAL_UINT8(7, rec.arg);
TEST_ASSERT_EQUAL_UINT8(ALL, g_r.on);
}
/* ---- 等 PG --------------------------------------------------------------- */
/* §10.9 第 6 步:PG 等不到 ⇒ 500 ms 后关回去 + 记事件(带 FLT#);别的路不受牵连。 */
static void test_pg_timeout_turns_that_rail_back_off(void)
{
g_pg_follows[2] = false;
g_flt[2] = true;
devices(true);
bringup_power_restore(ALL, &g_r);
TEST_ASSERT_EQUAL_UINT8(BIT(3), g_r.pg_fail);
TEST_ASSERT_FALSE(rail_efuse_is_on(3));
TEST_ASSERT_EQUAL_INT(0, sim_gpio_level(rail_cfg(3)->efuse_en));
TEST_ASSERT_EQUAL_UINT8((uint8_t)(ALL & ~BIT(3)), g_r.on);
evlog_rec_t rec;
TEST_ASSERT_TRUE(last_ev(LOG_EV_PG_TIMEOUT, &rec));
TEST_ASSERT_EQUAL_UINT8(3, rec.rail);
TEST_ASSERT_EQUAL_UINT8(BRINGUP_ARG_FLT, rec.arg);
/* 关回去的时刻 ≈ 抬高 + 500 ms(不早于)。 */
TEST_ASSERT_TRUE(hal_time_now_ms() - g_r.on_ms[2] >= BRINGUP_PG_TIMEOUT_MS);
/* 后面两路没有被它拖住:仍是 100 ms 一格。 */
const uint64_t d = sim_gpio_rise_us(rail_cfg(4)->efuse_en) -
sim_gpio_rise_us(rail_cfg(3)->efuse_en);
TEST_ASSERT_TRUE(d < 110000u);
}
/* ---- DAC 的 NVM 核对 ----------------------------------------------------- */
/* 上电复位时读回的不是 3.90 V ⇒ 「DAC 不应答时停在安全值」这条前提不成立了,要报。 */
static void test_dac_nvm_mismatch_is_flagged_on_power_on(void)
{
sim_i2c_set_reg(dac63204_rail_addr(3), DAC63204_REG_DATA(dac63204_rail_ch(3)),
DAC63204_CODE_TO_REG(rail_mv_to_code(3600)));
devices(true);
TEST_ASSERT_TRUE((g_r.deg & BRINGUP_DEG_DAC_NVM) != 0);
TEST_ASSERT_EQUAL_UINT16(3600, g_r.dac_nvm_mv[2]);
}
/* 热重启时 DAC 没断过电,里面是上次命令设的值 —— 不许报 NVM 不对。 */
static void test_dac_nvm_not_checked_on_warm_reboot(void)
{
sim_i2c_set_reg(dac63204_rail_addr(3), DAC63204_REG_DATA(dac63204_rail_ch(3)),
DAC63204_CODE_TO_REG(rail_mv_to_code(3600)));
devices(false);
TEST_ASSERT_FALSE((g_r.deg & BRINGUP_DEG_DAC_NVM) != 0);
/* 而初始化照样把它写回安全值。 */
int mv = 0;
TEST_ASSERT_EQUAL_INT(HAL_OK, rail_get_mv(3, &mv));
TEST_ASSERT_EQUAL_INT(RAIL_MV_SAFE, mv);
}
/* ---- 意图掩码 ------------------------------------------------------------ */
/* ---- 「重启不断电」:计划内重启的接管(warm_esp.c 给 adopt) ----------------- */
/* 上一条命:这一路开着(EN 被 hold 在高)、PG 在、PCA9555 已被配过、锁存器是 out0。 */
static void prev_life(int rail, uint8_t out0)
{
const int ch = ch_of(rail);
sim_i2c_set_reg_ch(ch, PCA9555_ADDR, PCA9555_REG_CONFIG0, PCA9555_CONFIG0_INIT);
sim_i2c_set_reg_ch(ch, PCA9555_ADDR, PCA9555_REG_CONFIG1, PCA9555_CONFIG1_INIT);
sim_i2c_set_reg_ch(ch, PCA9555_ADDR, PCA9555_REG_OUTPUT0, out0);
sim_i2c_set_reg_ch(ch, PCA9555_ADDR, PCA9555_REG_OUTPUT1, 0);
sim_pca9555_drive(rail - 1, 1, P1_PG);
sim_gpio_preheld(rail_cfg(rail)->efuse_en, 1);
}
static int g_en_fell[N]; /* 这一条命里 EN 线上出现过几次 1→0 */
static int g_en_last[N];
static void watch_en(int pin, int level)
{
world(pin, level);
for (int rail = 1; rail <= N; rail++) {
if (pin == rail_cfg(rail)->efuse_en) {
if (g_en_last[rail - 1] == 1 && level == 0) {
g_en_fell[rail - 1]++;
}
g_en_last[rail - 1] = level;
}
}
}
static void warm(uint8_t adopt)
{
for (int rail = 1; rail <= N; rail++) {
g_en_fell[rail - 1] = 0;
g_en_last[rail - 1] = sim_gpio_level(rail_cfg(rail)->efuse_en);
}
sim_gpio_set_hook(watch_en);
const bringup_in_t in = { .power_on_reset = false, .adopt = adopt };
TEST_ASSERT_EQUAL_INT(HAL_OK, bringup_devices(&in, &g_r));
}
#define VBUS ((uint8_t)(1u << PCA9555_BIT_VBUS_EN))
#define KPWR ((uint8_t)(1u << PCA9555_BIT_KEY_PWR))
/* ⭐ 接管:EN 一次都没掉下来过;hold 放开了(固件又管得住它了);VBUS 留着、正按着的键松开;
第二段不重开、不等 PG,只把没接管的「想开的」那一路按老规矩开。 */
static void test_planned_restart_keeps_the_rails_it_adopts(void)
{
prev_life(1, (uint8_t)(VBUS | KPWR)); /* 重启那一刻 CH1 正按着电源键 */
prev_life(3, VBUS);
warm(BIT(1) | BIT(3));
TEST_ASSERT_EQUAL_UINT8(BIT(1) | BIT(3), g_r.adopted);
TEST_ASSERT_EQUAL_UINT8(0, g_r.adopt_dropped);
for (int rail = 1; rail <= N; rail++) {
const int pin = rail_cfg(rail)->efuse_en;
TEST_ASSERT_FALSE_MESSAGE(sim_gpio_held(pin), "hold 必须放开,否则 t_safety 关不掉");
const bool adopted = rail == 1 || rail == 3;
TEST_ASSERT_EQUAL_INT(adopted ? 1 : 0, sim_gpio_level(pin));
TEST_ASSERT_EQUAL(adopted, rail_efuse_is_on(rail));
TEST_ASSERT_EQUAL_INT(0, g_en_fell[rail - 1]);
}
TEST_ASSERT_EQUAL_UINT8(VBUS, sim_pca9555_output(0, 0)); /* 键松开,VBUS 留着 */
TEST_ASSERT_EQUAL_UINT8(VBUS, sim_pca9555_output(2, 0));
TEST_ASSERT_TRUE(slot_vbus_is_on(1));
TEST_ASSERT_TRUE(slot_vbus_is_on(3));
/* (sim 的 unsafe_enable 在 VBUS 合法开着时也会计数,这里不看它;直接看键 / EDL 位。) */
const uint8_t keys_edl = (uint8_t)(pca9555_key_mask() | (1u << PCA9555_BIT_EDL_EN));
TEST_ASSERT_EQUAL_UINT8(0, sim_pca9555_output(0, 0) & keys_edl);
bringup_power_restore(BIT(1) | BIT(2) | BIT(3), &g_r);
TEST_ASSERT_EQUAL_UINT8(BIT(1) | BIT(2) | BIT(3), g_r.on);
TEST_ASSERT_EQUAL_UINT64(0, sim_gpio_rise_us(rail_cfg(1)->efuse_en)); /* 这条命里没「上电」过 */
TEST_ASSERT_EQUAL_UINT64(0, sim_gpio_rise_us(rail_cfg(3)->efuse_en));
TEST_ASSERT_TRUE(sim_gpio_rise_us(rail_cfg(2)->efuse_en) != 0);
TEST_ASSERT_EQUAL_INT(0, g_en_fell[0] + g_en_fell[2]);
}
/* ⭐ SR-6:**非计划**复位(崩溃 / 看门狗)时上一条命的 hold 可能还锁着 ——
adopt = 0 就必须全关,而且 hold 放开(不放开 = 固件写低也落不到线上)。 */
static void test_unplanned_reset_drops_held_rails(void)
{
prev_life(1, VBUS);
prev_life(2, VBUS);
warm(0);
for (int rail = 1; rail <= 2; rail++) {
TEST_ASSERT_FALSE(sim_gpio_held(rail_cfg(rail)->efuse_en));
TEST_ASSERT_EQUAL_INT(0, sim_gpio_level(rail_cfg(rail)->efuse_en));
TEST_ASSERT_FALSE(rail_efuse_is_on(rail));
TEST_ASSERT_EQUAL_UINT8(0, sim_pca9555_output(rail - 1, 0)); /* VBUS 也断 */
}
TEST_ASSERT_EQUAL_UINT8(0, g_r.adopted);
}
/* 重启前还开着、重启后刀片不在了:关,记一条 SLOT_EMPTY(arg 1 = 接管时发现的)。 */
static void test_adopt_on_an_empty_slot_is_dropped(void)
{
prev_life(2, VBUS);
pull_slot(2);
warm(BIT(2));
TEST_ASSERT_EQUAL_UINT8(BIT(2), g_r.adopt_dropped);
TEST_ASSERT_EQUAL_UINT8(0, g_r.adopted);
TEST_ASSERT_EQUAL_INT(0, sim_gpio_level(rail_cfg(2)->efuse_en));
TEST_ASSERT_FALSE(rail_efuse_is_on(2));
evlog_rec_t rec;
TEST_ASSERT_TRUE(last_ev(LOG_EV_SLOT_EMPTY, &rec));
TEST_ASSERT_EQUAL_UINT8(2, rec.rail);
TEST_ASSERT_EQUAL_UINT8(1, rec.arg);
}
/* mux 失联:五路都看不见(§5.8)⇒ 接管的也关。 */
static void test_adopt_with_mux_lost_is_dropped(void)
{
prev_life(1, VBUS);
sim_i2c_set_present(TCA9548A_ADDR, false);
warm(BIT(1));
TEST_ASSERT_EQUAL_UINT8(BIT(1), g_r.adopt_dropped);
TEST_ASSERT_EQUAL_INT(0, sim_gpio_level(rail_cfg(1)->efuse_en));
bringup_power_restore(ALL, &g_r);
TEST_ASSERT_EQUAL_UINT8(0, g_r.on);
}
/* ⭐ PCA9555 掉过电(方向 / 锁存器都是复位值 0xFF):**不许**照留锁存器 —— 那是凭空打开 VBUS
与三个键。EN 照样接管(它由 ESP 的 hold 管,与这片无关)。 */
static void test_adopt_does_not_trust_a_pca_that_lost_power(void)
{
sim_gpio_preheld(rail_cfg(4)->efuse_en, 1); /* 只有 EN 是上一条命的;PCA9555 是复位值 */
sim_pca9555_drive(3, 1, P1_PG);
warm(BIT(4));
TEST_ASSERT_EQUAL_UINT8(BIT(4), g_r.adopted);
TEST_ASSERT_EQUAL_INT(1, sim_gpio_level(rail_cfg(4)->efuse_en));
TEST_ASSERT_EQUAL_UINT8(0, sim_pca9555_output(3, 0));
TEST_ASSERT_FALSE(slot_vbus_is_on(4));
TEST_ASSERT_EQUAL_UINT32(0, sim_pca9555_unsafe_enable());
}
/* 重启前一刻锁 EN:锁住之后固件**写不到**线上(这正是只许在 esp_restart 前一刻锁的理由),
rail_efuse_mask 报的是锁之前的意图。 */
static void test_hold_all_freezes_the_pins(void)
{
devices(true);
bringup_power_restore(BIT(1) | BIT(5), &g_r);
TEST_ASSERT_EQUAL_UINT8(BIT(1) | BIT(5), rail_efuse_mask());
TEST_ASSERT_EQUAL_INT(HAL_OK, rail_en_hold_all());
TEST_ASSERT_EQUAL_INT(HAL_OK, rail_efuse_enable(1, false));
TEST_ASSERT_EQUAL_INT(1, sim_gpio_level(rail_cfg(1)->efuse_en)); /* 线上没动 */
TEST_ASSERT_EQUAL_INT(HAL_OK, rail_en_init_keep(BIT(5))); /* 下一条命:只接管 CH5 */
TEST_ASSERT_EQUAL_INT(0, sim_gpio_level(rail_cfg(1)->efuse_en));
TEST_ASSERT_EQUAL_INT(1, sim_gpio_level(rail_cfg(5)->efuse_en));
TEST_ASSERT_FALSE(sim_gpio_held(rail_cfg(5)->efuse_en));
}
static void test_want_update(void)
{
TEST_ASSERT_EQUAL_UINT8(BIT(3), bringup_want_update(0, 3, true));
TEST_ASSERT_EQUAL_UINT8(BIT(1), bringup_want_update(BIT(1) | BIT(3), 3, false));
TEST_ASSERT_EQUAL_UINT8(BIT(2), bringup_want_update(BIT(2), 0, true)); /* 越界不动 */
TEST_ASSERT_EQUAL_UINT8(BIT(2), bringup_want_update(BIT(2), N + 1, true));
}
int main(void)
{
UNITY_BEGIN();
RUN_TEST(test_first_input_ina_is_marked_reversed);
RUN_TEST(test_full_board_initialises_every_device);
RUN_TEST(test_five_rails_come_up_100ms_apart);
RUN_TEST(test_only_wanted_rails_come_up);
RUN_TEST(test_nothing_wanted_nothing_powered);
RUN_TEST(test_hub_reset_pulses_exactly_once_on_power_on);
RUN_TEST(test_hub_reset_untouched_on_warm_reboot);
RUN_TEST(test_row_rail_ina_missing_keeps_power);
RUN_TEST(test_row_dac_missing_does_not_stop_the_other_or_the_rails);
RUN_TEST(test_row_pca_missing_falls_back_to_voltage);
RUN_TEST(test_row_pca_missing_and_no_voltage_times_out);
RUN_TEST(test_row_pca_and_ina_missing_stays_on_unverified);
RUN_TEST(test_row_mux_missing_pulses_reset_then_powers_nothing);
RUN_TEST(test_row_mux_recovers_after_one_reset);
RUN_TEST(test_row_empty_slot_is_never_touched);
RUN_TEST(test_row_empty_slot_not_wanted_is_silent);
RUN_TEST(test_row_pd_missing_assumes_60w);
RUN_TEST(test_row_pd_low_uses_what_it_reads);
RUN_TEST(test_pd_found_behind_channel_5_too);
RUN_TEST(test_row_watchdog_reset_is_recorded_in_boot);
RUN_TEST(test_pg_timeout_turns_that_rail_back_off);
RUN_TEST(test_dac_nvm_mismatch_is_flagged_on_power_on);
RUN_TEST(test_dac_nvm_not_checked_on_warm_reboot);
RUN_TEST(test_planned_restart_keeps_the_rails_it_adopts);
RUN_TEST(test_unplanned_reset_drops_held_rails);
RUN_TEST(test_adopt_on_an_empty_slot_is_dropped);
RUN_TEST(test_adopt_with_mux_lost_is_dropped);
RUN_TEST(test_adopt_does_not_trust_a_pca_that_lost_power);
RUN_TEST(test_hold_all_freezes_the_pins);
RUN_TEST(test_want_update);
return UNITY_END();
}