2026-09-27 底板上板(未插刀片)发现: - HUB_RST 只在上电复位(ESP_RST_POWERON)时脉冲。原先每次重启都脉冲, esptool 经 USB 复位芯片即把自己的 COM 口断掉,只能走 J33 手动下载。 实测:USB 复位(原因 11)后日志「不动 HUB_RST#」,COM 口全程不掉。 - t_safety 空槽不走总线。原先五路无条件读,空槽 NAK 每次十几 ms, 槽 1 就吃光 10 ms 整圈预算,槽 2~5 与主干输入侧 0x43 一圈都没读到。 新用例 test_empty_slots_are_never_touched_and_the_input_still_reads(去掉修复即红)。 - 关掉 i2c.master 驱动日志:NAK 日志在持锁期间同步打印,拖垮 20 ms 节拍。 失败仍计入 t_safety 的 i2c_err。 - 上板结果:空机 skipped=0 i2c_err=0 过载 0 最迟 0 ms。 tools/i2c_scan:独立的只读扫描固件(主干 + mux 8 通道,读身份寄存器,不写任何输出)。 实测主干 0x22/0x23/0x43/0x4A/0x4B/0x70 全在,另有 0x47 应答(推测 DAC63204 广播地址)。 Co-Authored-By: Claude Opus 5.5 <[email protected]>
661 行
34 KiB
C
661 行
34 KiB
C
/* A10-R2 之一:`t_safety` **一圈的形状与账**(需求 §10.3、计划 §5.5)。
|
||
判据本身在 `test_t_safety_rules.c` 里,这里只管「这一圈发了什么、发给了谁」。
|
||
|
||
---- R1 → R2,这个文件测的东西换了重心 ------------------------------------
|
||
|
||
R1:一条总线、六片不同地址的 INA226,一圈 13 次事务。那时唯一会出事的形态是
|
||
「地址算错」,而地址算错**会 NAK**,一眼就能看见。
|
||
|
||
R2:五片刀片完全同构(INA226 全是 0x40、PCA9555 全是 0x20),区分它们的
|
||
唯一手段是底板 TCA9548A 的通道。于是出现了一种 R1 根本没有的错误形态:
|
||
**通道选错 ⇒ 读到另一台手机的电压电流**,而量纲、范围、变化趋势全都正常,
|
||
没有任何一层会报错、会 NAK、会计数。⭐ 本文件里带星的那两条就是它的拦网。
|
||
|
||
⚠️ 两条能成立的前提:
|
||
· 假总线里有一颗**真的 mux**(`sim_i2c_add_mux`)—— 没有它,五片刀片会
|
||
塌成一片,「选错通道」在宿主机上一次也复现不出来;
|
||
· 假 I²C **真的把虚拟时钟花掉**(`sim_i2c_set_bus_time`,默认开)——
|
||
没有它,「20 ms 周期」就退化成「测 delay 参数传对了没」,而真正会出事的是
|
||
「巡检自己占掉了 2.34 ms,节拍还准不准」。 */
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
#include "board_pins.h"
|
||
#include "hal_gpio.h"
|
||
#include "hal_i2c.h"
|
||
#include "hal_i2c_acct.h"
|
||
#include "hal_lock.h"
|
||
#include "hal_time.h"
|
||
#include "i2cbus.h"
|
||
#include "ina226.h"
|
||
#include "pca9555.h"
|
||
#include "rail.h"
|
||
#include "rail_cfg.h"
|
||
#include "sched.h"
|
||
#include "sim_hal.h"
|
||
#include "slot.h"
|
||
#include "t_actuator.h"
|
||
#include "t_safety.h"
|
||
#include "tasks.h"
|
||
#include "tca9548a.h"
|
||
#include "unity.h"
|
||
|
||
#define N PHONERAIL_RAIL_COUNT
|
||
|
||
/* 主干上那两片 DAC(`rail.h` 的 U6 / U7)。巡检一次也不该碰它们 ——
|
||
设定值是影子里的(`rail_setpoint_mv()`),去读 DAC 就是每圈多五次事务。 */
|
||
#define DAC_ADDR_A 0x4A
|
||
#define DAC_ADDR_B 0x4B
|
||
|
||
/* ⭐ 五路预置成**互不相同**的电压:CH1 3600 → CH5 4000 mV。
|
||
只有值各不相同,「读到了另一台手机的数据」才留得下痕迹。 */
|
||
#define RAIL_MV(rail) (3600 + 100 * ((rail) - 1))
|
||
/* 电流也各不相同:CH1 1 A → CH5 5 A。 */
|
||
#define RAIL_UA(rail) ((rail) * 1000000)
|
||
|
||
/* 五片刀片的 Port1(四个故障/PG 信号,开漏上拉 ⇒ 静息 0x0F)各驱动成不同的图案,
|
||
每一片**恰好有一个信号在响**,而且响的不是同一个。
|
||
全驱成一样的话,「五路读的都是同一片」这条错就看不出来。 */
|
||
static const uint8_t k_port1[N] = {
|
||
(uint8_t)(0x0Fu & ~(1u << PCA9555_BIT_EFUSE_FLT)), /* CH1:eFuse 报故障 */
|
||
(uint8_t)(0x0Fu & ~(1u << PCA9555_BIT_BUCK_PG)), /* CH2:Buck PG 掉了 */
|
||
(uint8_t)(0x0Fu & ~(1u << PCA9555_BIT_INA_ALERT)), /* CH3:INA226 在告警 */
|
||
(uint8_t)(0x0Fu & ~(1u << PCA9555_BIT_EFUSE_PG)), /* CH4:eFuse PG 掉了 */
|
||
0x0Fu, /* CH5:什么事都没有 */
|
||
};
|
||
|
||
static safety_snap_t g_snap;
|
||
|
||
/* `board_up()` 跑完那一刻的基准值。启动本身会在总线和引脚上留下痕迹,
|
||
拿绝对值去断言等于把启动的账算进第一圈(那正是 `t_safety_init()` 对齐游标要防的事)。 */
|
||
static uint32_t g_resets0; /* `MUX_RST#` 复位次数 —— `i2cbus_init()` 必定留下一次,见下 */
|
||
|
||
static int ch_of(int rail) { return (int)rail_cfg(rail)->mux_ch; }
|
||
|
||
static sim_i2c_dev_t *ina_of(int rail)
|
||
{
|
||
return sim_i2c_dev_ch(ch_of(rail), rail_cfg(rail)->ina_addr);
|
||
}
|
||
static sim_i2c_dev_t *pca_of(int rail) { return sim_i2c_dev_ch(ch_of(rail), PCA9555_ADDR); }
|
||
static sim_i2c_dev_t *mux_dev(void) { return sim_i2c_dev(TCA9548A_ADDR); }
|
||
|
||
/* 从**假器件的寄存器**注入,不去改快照 —— 改快照等于跳过驱动和 mux,
|
||
那样连「读的是哪一片」都证明不了。 */
|
||
static void put_vi(int rail, int mv, int ua)
|
||
{
|
||
const int ch = ch_of(rail);
|
||
const uint8_t addr = rail_cfg(rail)->ina_addr;
|
||
TEST_ASSERT_EQUAL_INT(0, mv % 5); /* 母线 LSB 1.25 mV:不是 5 的倍数就注不准 */
|
||
sim_i2c_set_reg_ch(ch, addr, INA226_REG_BUS_V, (uint16_t)(mv * 4 / 5));
|
||
sim_i2c_set_reg_ch(ch, addr, INA226_REG_CURRENT,
|
||
(uint16_t)(int16_t)(ua / INA226_CURRENT_LSB_UA));
|
||
}
|
||
|
||
/* 假板子:主干(mux + 输入侧 INA226 + 两片 DAC)+ 五片刀片(各 INA226 + PCA9555)。 */
|
||
static void board_up(void)
|
||
{
|
||
const hal_i2c_cfg_t cfg = { .sda_gpio = BOARD_PIN_I2C_SDA,
|
||
.scl_gpio = BOARD_PIN_I2C_SCL,
|
||
.freq_hz = 400000 };
|
||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_init(&cfg));
|
||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_lock_init());
|
||
|
||
sim_i2c_add_mux(TCA9548A_ADDR, BOARD_PIN_MUX_RST_N);
|
||
|
||
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_vi(rail, RAIL_MV(rail), RAIL_UA(rail));
|
||
sim_pca9555_attach_ch(rail - 1, ch, PCA9555_ADDR);
|
||
sim_pca9555_drive(rail - 1, 1, k_port1[rail - 1]);
|
||
}
|
||
|
||
sim_i2c_add(INA226_ADDR_INPUT, 2);
|
||
sim_i2c_set_reg(INA226_ADDR_INPUT, INA226_REG_BUS_V, (uint16_t)(12000 * 4 / 5));
|
||
sim_i2c_set_reg(INA226_ADDR_INPUT, INA226_REG_CURRENT, (uint16_t)(2000000 / 500));
|
||
sim_i2c_add(DAC_ADDR_A, 2);
|
||
sim_i2c_add(DAC_ADDR_B, 2);
|
||
|
||
TEST_ASSERT_EQUAL_INT(HAL_OK, i2cbus_init());
|
||
TEST_ASSERT_EQUAL_INT(HAL_OK, rail_en_init());
|
||
TEST_ASSERT_EQUAL_INT(HAL_OK, slot_prsnt_init());
|
||
|
||
/* ⚠️ `PRSNT#` 配的是**上拉输入**,没人驱动就是高 = 刀片不在位。
|
||
不驱低的话每一圈都会触发「刀片不在位」判据,而现象是「所有用例都多出几条
|
||
事件、五路自己关掉」—— 看上去像判据写错了,其实是假板子插反了。 */
|
||
for (int rail = 1; rail <= N; rail++) {
|
||
sim_gpio_drive(rail_cfg(rail)->prsnt_n, 0);
|
||
}
|
||
slot_prsnt_poll();
|
||
|
||
for (int rail = 1; rail <= N; rail++) {
|
||
TEST_ASSERT_EQUAL_INT(HAL_OK, slot_init(rail));
|
||
}
|
||
t_actuator_init(NULL, NULL);
|
||
rail_setpoint_reset();
|
||
t_safety_init(&g_snap);
|
||
|
||
/* ⚠️ 这里一定是 1,不是 0:`i2cbus_init()` 先把 `MUX_RST#` 配成输出(两个 HAL
|
||
都保证「配完是 0」)再写 1,中间那一瞬间对真器件就是一次复位脉冲,假 mux
|
||
如实照做。这个窗口是已知且可接受的(那时还没人用总线),但**不能当它不存在**
|
||
—— 所以后面的用例一律和这个基准比差值。 */
|
||
g_resets0 = sim_i2c_mux_resets();
|
||
TEST_ASSERT_EQUAL_UINT32(1, g_resets0);
|
||
}
|
||
|
||
void setUp(void)
|
||
{
|
||
sim_hal_reset();
|
||
board_up();
|
||
}
|
||
|
||
/* 每个用例收尾都查一次「有没有同时开两路通道」。真板上那是两片同地址的 INA226
|
||
一起应答、读回来是线与 —— 数值正常、还很稳,没有任何一层会报错。
|
||
所以这个数**必须恒为 0**,放在 tearDown 里等于给每一条用例都加上这条断言。 */
|
||
void tearDown(void)
|
||
{
|
||
TEST_ASSERT_EQUAL_UINT32(0, sim_i2c_ambiguous());
|
||
}
|
||
|
||
/* ---- 一圈的事务账(计划 §5.5) ------------------------------------------- */
|
||
|
||
/* 一圈正好 `t_safety_cycle_txns()` 次事务,而且**每一片器件被读的次数是钉死的**。
|
||
顺手多读一个寄存器(比如 check 一下 Calibration)会当场变红,
|
||
然后写的人才会想起来去把预算那一行也改了。 */
|
||
static void test_one_cycle_is_exactly_the_budgeted_transaction_count(void)
|
||
{
|
||
uint32_t ina_rd[N], pca_rd[N], pca_wr[N];
|
||
for (int rail = 1; rail <= N; rail++) {
|
||
ina_rd[rail - 1] = ina_of(rail)->n_read;
|
||
pca_rd[rail - 1] = pca_of(rail)->n_read;
|
||
pca_wr[rail - 1] = pca_of(rail)->n_write;
|
||
}
|
||
const uint32_t in_rd = sim_i2c_dev(INA226_ADDR_INPUT)->n_read;
|
||
const uint32_t mux_wr = mux_dev()->n_write;
|
||
const uint32_t mux_rd = mux_dev()->n_read;
|
||
const uint32_t dac_rd = sim_i2c_dev(DAC_ADDR_A)->n_read + sim_i2c_dev(DAC_ADDR_B)->n_read;
|
||
|
||
const uint32_t before = hal_i2c_acct_txns();
|
||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&g_snap));
|
||
|
||
TEST_ASSERT_EQUAL_UINT32(t_safety_cycle_txns(), hal_i2c_acct_txns() - before);
|
||
TEST_ASSERT_EQUAL_UINT32(t_safety_cycle_txns(), g_snap.txns);
|
||
|
||
/* 逐片对账:五路各 2 次 INA226 寄存器读 + 1 次 PCA9555 输入读;
|
||
主干那片 INA226 2 次;mux 被写 6 次(五路各一次 + 回主干一次)。
|
||
⚠️ 比的是**这一圈的增量**:`slot_init()` 自己就读了 PCA9555 六次(写一个回读一个),
|
||
拿绝对值比等于把启动的账算进巡检。 */
|
||
for (int rail = 1; rail <= N; rail++) {
|
||
TEST_ASSERT_EQUAL_UINT32(2, ina_of(rail)->n_read - ina_rd[rail - 1]);
|
||
TEST_ASSERT_EQUAL_UINT32(1, pca_of(rail)->n_read - pca_rd[rail - 1]);
|
||
/* 「读输入」在线上是「写一个寄存器号 + 读两字节」,所以写计数也会 +1。
|
||
真正要拦的是**写输出锁存器**:平安无事的一圈里,最后落到这一片上的写
|
||
只能是那一个字节的寄存器指针(0x00),不能是 OUTPUT0(0x02)。 */
|
||
TEST_ASSERT_EQUAL_UINT32(1, pca_of(rail)->n_write - pca_wr[rail - 1]);
|
||
TEST_ASSERT_EQUAL_UINT8(PCA9555_REG_INPUT0, pca_of(rail)->last_write[0]);
|
||
TEST_ASSERT_EQUAL_UINT8(1, pca_of(rail)->last_write_len);
|
||
TEST_ASSERT_EQUAL_UINT8(0, sim_pca9555_output(rail - 1, 0)); /* 键与 VBUS 全是 0 */
|
||
}
|
||
TEST_ASSERT_EQUAL_UINT32(2, sim_i2c_dev(INA226_ADDR_INPUT)->n_read - in_rd);
|
||
TEST_ASSERT_EQUAL_UINT32((uint32_t)N + 1u, mux_dev()->n_write - mux_wr);
|
||
TEST_ASSERT_EQUAL_UINT32(0, mux_dev()->n_read - mux_rd); /* 选完不回读,那是 probe 的事 */
|
||
|
||
/* 设定值从影子里取(`rail_setpoint_mv()`),判据一次也不该去读 DAC。
|
||
读一次就是每圈多五次事务,把 §10.3 的预算直接顶穿。 */
|
||
TEST_ASSERT_EQUAL_UINT32(
|
||
0, sim_i2c_dev(DAC_ADDR_A)->n_read + sim_i2c_dev(DAC_ADDR_B)->n_read - dac_rd);
|
||
}
|
||
|
||
/* 静态预算与实测必须**一模一样**。不一样只有两种可能:巡检里多读/少读了东西,
|
||
或者有别的任务在同一圈里插了事务进来 —— 两种都该被这条拦住。
|
||
(这也是为什么预算函数不是一个写死的常数:它按同一个算式从事务形状来。) */
|
||
static void test_measured_bus_time_equals_the_static_budget(void)
|
||
{
|
||
for (int i = 0; i < 10; i++) {
|
||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&g_snap));
|
||
TEST_ASSERT_EQUAL_UINT32(t_safety_budget_us(), g_snap.bus_us);
|
||
TEST_ASSERT_EQUAL_UINT32(t_safety_cycle_txns(), g_snap.txns);
|
||
}
|
||
}
|
||
|
||
/* 计划 §5.5 那张表的可执行版本:23 次事务 ≈ 2.5 ms / 12.5 %。
|
||
这里按线上真实发生的位数算,得 6×20 + 17×48 = 936 位 = 2340 µs = 11.7 %。
|
||
§5.5 手算的 2.48 ms 用的是每次事务的近似值,差的那 140 µs 是近似的账。
|
||
⚠️ 这一条同时钉住「R1 的 13 次 / 1.56 ms 已经不是这块板子的数了」——
|
||
把预算函数改回 R1 那版,这里第一行就红。 */
|
||
static void test_the_bus_account_matches_plan_5_5(void)
|
||
{
|
||
TEST_ASSERT_EQUAL_UINT32(23, t_safety_cycle_txns());
|
||
TEST_ASSERT_EQUAL_UINT32(20, hal_i2c_txn_bits(1, 0)); /* mux 选通:裸写一个字节 */
|
||
TEST_ASSERT_EQUAL_UINT32(48, hal_i2c_txn_bits(1, 2)); /* 寄存器读 */
|
||
TEST_ASSERT_EQUAL_UINT32(2340, t_safety_budget_us());
|
||
|
||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&g_snap));
|
||
|
||
const uint32_t period = task_desc(TASK_SAFETY)->period_ms;
|
||
const uint32_t pm = t_safety_load_permille(g_snap.bus_us, period);
|
||
TEST_ASSERT_EQUAL_UINT32(117, pm);
|
||
/* 计划 §5.5 的「≈12.4 %」:11 %~13 % 都算对得上。超出就是巡检里多读/少读了东西。 */
|
||
TEST_ASSERT_TRUE(pm >= 110 && pm <= 130);
|
||
/* 一圈占的总线时间必须远小于周期,否则巡检自己就把节拍吃掉了。 */
|
||
TEST_ASSERT_TRUE(g_snap.bus_us < period * 1000u / 4u);
|
||
|
||
char line[160];
|
||
const int n = t_safety_format(&g_snap, line, sizeof(line));
|
||
TEST_ASSERT_TRUE(n > 0 && n < (int)sizeof(line));
|
||
TEST_ASSERT_NOT_NULL(strstr(line, "txns=23"));
|
||
TEST_ASSERT_NOT_NULL(strstr(line, "bus=2340 us/20 ms"));
|
||
TEST_ASSERT_NOT_NULL(strstr(line, "load=11.7%"));
|
||
printf(" %s(计划 §5.5:≈2.5 ms / 12.4 %%)\n", line);
|
||
}
|
||
|
||
/* ---- ⭐ R2 最危险的错误形态:读到了另一台手机 ---------------------------- */
|
||
|
||
/* 五个通道上的假 INA226 预置成**互不相同**的电压与电流,跑一圈,逐路对上。
|
||
通道选错的后果是「读到另一台手机的数据」—— 量纲、范围、变化趋势全都正常,
|
||
遥测曲线好看得很,五台机器的过压判据却全部盯错了对象。
|
||
R1 里这条错误不存在(六片六个地址,错了就 NAK);R2 里它不报错、不 NAK、
|
||
不计数,**只有值本身能拆穿它**。 */
|
||
static void test_each_rail_is_read_on_its_own_mux_channel(void)
|
||
{
|
||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&g_snap));
|
||
|
||
for (int rail = 1; rail <= N; rail++) {
|
||
TEST_ASSERT_TRUE(g_snap.rail_ok[rail - 1]);
|
||
TEST_ASSERT_EQUAL_INT32(RAIL_MV(rail), g_snap.rail[rail - 1].bus_mv);
|
||
TEST_ASSERT_EQUAL_INT32(RAIL_UA(rail), g_snap.rail[rail - 1].current_ua);
|
||
}
|
||
/* 主干那片(0x43)不在任何通道后面,它的值不许混进五路里。 */
|
||
TEST_ASSERT_TRUE(g_snap.input_ok);
|
||
TEST_ASSERT_EQUAL_INT32(12000, g_snap.input.bus_mv);
|
||
|
||
/* 换一组值再跑一圈:万一上面那组恰好被「按顺序发牌」蒙对,换一组就露馅。 */
|
||
for (int rail = 1; rail <= N; rail++) {
|
||
put_vi(rail, 4000 - 100 * (rail - 1), -(rail * 100000)); /* 顺序反过来、电流转负 */
|
||
}
|
||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&g_snap));
|
||
for (int rail = 1; rail <= N; rail++) {
|
||
TEST_ASSERT_EQUAL_INT32(4000 - 100 * (rail - 1), g_snap.rail[rail - 1].bus_mv);
|
||
TEST_ASSERT_EQUAL_INT32(-(rail * 100000), g_snap.rail[rail - 1].current_ua);
|
||
}
|
||
}
|
||
|
||
/* 同一件事,换 PCA9555 那一路:五片的四个故障信号各响一个,逐片对上。
|
||
INA226 那条对上了、这条没对上,说明「选通道」和「读哪一片」在两条路径上
|
||
各自算了一遍 —— 而 `slot.c` 存在的全部理由就是让这两件事只算一次。 */
|
||
static void test_each_blade_reports_its_own_pca9555_inputs(void)
|
||
{
|
||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&g_snap));
|
||
|
||
for (int rail = 1; rail <= N; rail++) {
|
||
TEST_ASSERT_TRUE(g_snap.io_ok[rail - 1]);
|
||
TEST_ASSERT_EQUAL_UINT8(k_port1[rail - 1], g_snap.io[rail - 1].port1);
|
||
}
|
||
/* 换成语义判读再验一遍(极性写反在原始字节上是看不出来的)。 */
|
||
TEST_ASSERT_TRUE(pca9555_efuse_fault(&g_snap.io[0])); /* CH1 FLT# 低 = 有故障 */
|
||
TEST_ASSERT_FALSE(pca9555_efuse_fault(&g_snap.io[1]));
|
||
TEST_ASSERT_FALSE(pca9555_buck_pg(&g_snap.io[1])); /* CH2 Buck PG 低 = 不良 */
|
||
TEST_ASSERT_TRUE(pca9555_buck_pg(&g_snap.io[0]));
|
||
TEST_ASSERT_TRUE(pca9555_ina_alert(&g_snap.io[2])); /* CH3 ALERT 低 = 在告警 */
|
||
TEST_ASSERT_FALSE(pca9555_ina_alert(&g_snap.io[3]));
|
||
TEST_ASSERT_FALSE(pca9555_efuse_pg(&g_snap.io[3])); /* CH4 eFuse PG 低 */
|
||
TEST_ASSERT_TRUE(pca9555_efuse_pg(&g_snap.io[4])); /* CH5 全好 */
|
||
}
|
||
|
||
/* 任何时刻都只许开一路通道,而且一圈走完停在**主干**上。
|
||
同时开两路在真板上是两片一起应答、读回线与(数值正常、还很稳)——
|
||
`sim_i2c_ambiguous()` 是这件事在宿主机上唯一的痕迹。 */
|
||
static void test_the_mux_never_has_two_channels_open(void)
|
||
{
|
||
for (int i = 0; i < 50; i++) {
|
||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&g_snap));
|
||
TEST_ASSERT_EQUAL_UINT32(0, sim_i2c_ambiguous());
|
||
/* 一圈的最后一次选通是回主干(读输入侧那片),所以收尾时通道是全关的。 */
|
||
TEST_ASSERT_EQUAL_INT(-1, sim_i2c_mux_ch());
|
||
TEST_ASSERT_EQUAL_UINT8(0, sim_i2c_mux_mask());
|
||
}
|
||
TEST_ASSERT_EQUAL_UINT32(0, i2cbus_select_fails());
|
||
TEST_ASSERT_EQUAL_UINT32(0, sim_i2c_mux_resets() - g_resets0); /* 一切正常时不该再复位 */
|
||
}
|
||
|
||
/* 通道 5 上是 CH224A(PD 受电芯片),通道 6 / 7 悬空。巡检一次也不许选到它们。
|
||
选到通道 5 再去读 0x40 会一个字节也读不到,而现象是「这一路电压电流突然全是 0」
|
||
—— 看上去像刀片拔了。这里在那三路上各挂一片 0x40,它们必须一次也没被碰过。 */
|
||
static void test_the_unused_channels_are_never_selected(void)
|
||
{
|
||
sim_i2c_dev_t *pd = sim_i2c_add_ch(TCA9548A_CH_PD, INA226_ADDR_RAIL, 2);
|
||
sim_i2c_dev_t *sp6 = sim_i2c_add_ch(6, INA226_ADDR_RAIL, 2);
|
||
sim_i2c_dev_t *sp7 = sim_i2c_add_ch(7, INA226_ADDR_RAIL, 2);
|
||
|
||
for (int i = 0; i < 5; i++) {
|
||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&g_snap));
|
||
}
|
||
TEST_ASSERT_EQUAL_UINT32(0, pd->n_read);
|
||
TEST_ASSERT_EQUAL_UINT32(0, sp6->n_read);
|
||
TEST_ASSERT_EQUAL_UINT32(0, sp7->n_read);
|
||
TEST_ASSERT_EQUAL_UINT32(0, sim_i2c_ambiguous()); /* 它们也没和刀片撞上过 */
|
||
}
|
||
|
||
/* ---- 节拍 ---------------------------------------------------------------- */
|
||
|
||
/* 500 圈。每一圈都要**正好**落在 20 ms 的格子上,而且真实唤醒时刻(µs 级)
|
||
对理想时刻的偏差始终 < 1 ms —— 这就是 §10.3 的「20 ms ±1 ms」。
|
||
会漂的实现(干完睡 20)在这里第一圈就出局:R2 一圈自己要占 2.34 ms,
|
||
那样周期是 22.34 ms,500 圈之后差出 1.17 s。 */
|
||
static void test_safety_holds_a_20ms_beat_for_500_cycles(void)
|
||
{
|
||
sched_periodic_t tick;
|
||
sched_periodic_init(&tick, task_desc(TASK_SAFETY)->period_ms);
|
||
|
||
/* 先确认「干活」在 sim 里真的花掉了时间。不然下面 500 圈测的只是
|
||
「delay 参数传对了没」—— 巡检耗时为零的话,什么实现都能踩准 20 ms。 */
|
||
const uint64_t t0 = hal_time_now_us();
|
||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&g_snap));
|
||
TEST_ASSERT_EQUAL_UINT64(t_safety_budget_us(), hal_time_now_us() - t0);
|
||
sched_periodic_wait(&tick);
|
||
|
||
/* ⚠️ 相位起点是**第一次醒来那一刻**,不是 0:R2 的启动自己就要跑 mux 认件 +
|
||
五片 PCA9555 初始化,上百次事务在虚拟时钟上是真花掉了的。拿绝对 0 当起点,
|
||
测出来的是「启动花了多久」,和节拍没关系。 */
|
||
const uint64_t base_us = hal_time_now_us();
|
||
uint32_t prev_ms = hal_time_now_ms();
|
||
for (int i = 1; i < 500; i++) {
|
||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&g_snap));
|
||
sched_periodic_wait(&tick);
|
||
|
||
const uint32_t now_ms = hal_time_now_ms();
|
||
TEST_ASSERT_EQUAL_UINT32(20, now_ms - prev_ms);
|
||
prev_ms = now_ms;
|
||
|
||
const int64_t ideal_us = (int64_t)base_us + (int64_t)i * 20000;
|
||
const int64_t err_us = (int64_t)hal_time_now_us() - ideal_us;
|
||
TEST_ASSERT_TRUE(err_us > -1000 && err_us < 1000);
|
||
}
|
||
TEST_ASSERT_EQUAL_UINT32(0, tick.overruns);
|
||
TEST_ASSERT_EQUAL_UINT32(500, g_snap.cycles);
|
||
TEST_ASSERT_EQUAL_UINT32(0, g_snap.skipped);
|
||
TEST_ASSERT_EQUAL_UINT32(0, g_snap.events);
|
||
}
|
||
|
||
/* 20 ms 的周期必须**长于** INA226 的一帧转换时间(AVG=4 × 2×1.1 ms = 8.8 ms)。
|
||
短了的话同一帧会被读两次,而现象是「电流特别稳」—— 不是任何一种错误,
|
||
不会 NAK、不会报警、遥测曲线还更好看。R2 一圈占 2.34 ms,离 8.8 ms 还很远,
|
||
所以每一圈拿到的都是新的一帧。 */
|
||
static void test_the_period_outlives_one_ina226_conversion(void)
|
||
{
|
||
const uint32_t period_us = task_desc(TASK_SAFETY)->period_ms * 1000u;
|
||
TEST_ASSERT_EQUAL_UINT32(8800, (uint32_t)INA226_CONV_US);
|
||
TEST_ASSERT_TRUE(period_us > (uint32_t)INA226_CONV_US);
|
||
/* 两倍余量:巡检自己占的总线时间加进去也不能逼近一帧。 */
|
||
TEST_ASSERT_TRUE(period_us >= 2u * (uint32_t)INA226_CONV_US);
|
||
TEST_ASSERT_TRUE(t_safety_budget_us() < (uint32_t)INA226_CONV_US);
|
||
TEST_ASSERT_TRUE(task_table_ok()); /* 任务表自检里钉着同一条 */
|
||
}
|
||
|
||
/* ---- 加锁纪律 ------------------------------------------------------------ */
|
||
|
||
/* 一整圈里的每一次事务都持着 I²C 锁;出了这个函数锁必须已经放掉。
|
||
R2 里这一圈要拿六次锁(五路 + 主干),每一次都得规规矩矩。 */
|
||
static void test_every_transaction_happens_under_the_i2c_lock(void)
|
||
{
|
||
sim_lock_require(HAL_LOCK_I2C, true);
|
||
const uint32_t takes0 = sim_lock_takes(HAL_LOCK_I2C);
|
||
for (int i = 0; i < 5; i++) {
|
||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&g_snap));
|
||
}
|
||
TEST_ASSERT_EQUAL_UINT32(0, sim_lock_violations(HAL_LOCK_I2C));
|
||
TEST_ASSERT_FALSE(hal_lock_held(HAL_LOCK_I2C)); /* 出门放锁 */
|
||
TEST_ASSERT_EQUAL_INT(-1, sim_lock_owner(HAL_LOCK_I2C));
|
||
/* 一圈六次 `xact` = 六次拿锁(R1 是一把大锁包住整圈,只拿一次)。
|
||
这个数正是「给 t_actuator 留的缝」有几条 —— 变少了缝就没了。 */
|
||
TEST_ASSERT_EQUAL_UINT32(5u * ((uint32_t)N + 1u), sim_lock_takes(HAL_LOCK_I2C) - takes0);
|
||
}
|
||
|
||
/* 上一条不是句空话:不拿锁直接读一片 INA226,违规计数必须 +1。
|
||
检查本身失灵的话,上一条会永远绿。 */
|
||
static void test_the_lock_check_itself_can_fail(void)
|
||
{
|
||
sim_lock_require(HAL_LOCK_I2C, true);
|
||
ina226_vi_t vi;
|
||
(void)ina226_read_vi(rail_cfg(1)->ina_addr, &vi);
|
||
TEST_ASSERT_EQUAL_UINT32(2, sim_lock_violations(HAL_LOCK_I2C)); /* 两次事务 */
|
||
}
|
||
|
||
/* 拿不到锁:**跳过,不死等**(t_safety.h 规矩 1)。死等的现象是巡检整个停摆,
|
||
而它是优先级最高的那个任务 —— 停摆了没有任何人会发现。
|
||
|
||
⚠️ R2 的一笔新账:一圈要等**六次**锁,最坏 6 × 5 = 30 ms,**比 20 ms 的周期还长**
|
||
(R1 只等一次,5 ms)。这条断言把这个数钉在这里:谁改了超时常量或者 `xact`
|
||
的次数,都得先来正面回答「一圈最坏等多久」这个问题。 */
|
||
static void test_a_busy_bus_skips_the_cycle_instead_of_waiting_forever(void)
|
||
{
|
||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&g_snap)); /* 先跑一圈好的,留下基准 */
|
||
const int32_t mv0 = g_snap.rail[0].bus_mv;
|
||
const uint32_t txns0 = hal_i2c_acct_txns();
|
||
const uint32_t en_writes0 = sim_gpio_writes(rail_cfg(1)->efuse_en);
|
||
|
||
sim_lock_set_task(1); /* 假装 t_actuator 正在按键,总线在它手里 */
|
||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_lock_take(HAL_LOCK_I2C, 10));
|
||
sim_lock_set_task(0);
|
||
|
||
const uint32_t t0 = hal_time_now_ms();
|
||
TEST_ASSERT_EQUAL_INT(HAL_ERR_TIMEOUT, t_safety_step(&g_snap));
|
||
|
||
/* 这一圈**什么也没做**:一次事务没发、一根引脚没动、快照原封不动。 */
|
||
TEST_ASSERT_EQUAL_UINT32(0, hal_i2c_acct_txns() - txns0);
|
||
TEST_ASSERT_EQUAL_UINT32(0, g_snap.txns);
|
||
TEST_ASSERT_EQUAL_UINT32(0, g_snap.bus_us);
|
||
TEST_ASSERT_EQUAL_INT32(mv0, g_snap.rail[0].bus_mv);
|
||
TEST_ASSERT_EQUAL_UINT32(en_writes0, sim_gpio_writes(rail_cfg(1)->efuse_en));
|
||
TEST_ASSERT_EQUAL_UINT32(0, g_snap.events);
|
||
|
||
/* 但这一圈**被记下来了**:不计数的跳过等于悄悄失明。 */
|
||
TEST_ASSERT_EQUAL_UINT32(1, g_snap.skipped);
|
||
TEST_ASSERT_EQUAL_UINT32(2, g_snap.cycles);
|
||
/* ⭐ **整圈的账,不是单次的账。** R2 一圈要拿六次锁,六次各等满 5 ms 就是
|
||
30 ms —— 比 20 ms 的周期还长,现象是巡检成片丢拍而每一次 `xact` 都老老实实
|
||
按时返回了。所以 `T_SAFETY_LOCK_BUDGET_MS` 是一条整圈的闸:
|
||
花到 10 ms 就不再试剩下的路。
|
||
|
||
这里断言的正是那条闸:**只试了 2 次**(5 + 5 = 10 ms 用完预算),
|
||
而不是六次全试一遍。把预算那一段删掉的话,下面两条会变成 6 和 30。 */
|
||
TEST_ASSERT_EQUAL_UINT32(T_SAFETY_LOCK_BUDGET_MS / T_SAFETY_LOCK_TIMEOUT_MS,
|
||
i2cbus_lock_timeouts());
|
||
TEST_ASSERT_EQUAL_UINT32(T_SAFETY_LOCK_BUDGET_MS, hal_time_elapsed_ms(t0));
|
||
/* 而且整圈花掉的时间**必须短于巡检周期**,否则节拍本身就保不住了。 */
|
||
TEST_ASSERT_TRUE(hal_time_elapsed_ms(t0) < task_desc(TASK_SAFETY)->period_ms);
|
||
|
||
/* 总线还回来,下一圈照常。 */
|
||
sim_lock_set_task(1);
|
||
hal_lock_give(HAL_LOCK_I2C);
|
||
sim_lock_set_task(0);
|
||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&g_snap));
|
||
TEST_ASSERT_EQUAL_UINT32(t_safety_cycle_txns(), g_snap.txns);
|
||
TEST_ASSERT_EQUAL_UINT32(1, g_snap.skipped);
|
||
}
|
||
|
||
/* ---- 器件出问题的时候(§10.2:失联 ≠ 断电) ------------------------------ */
|
||
|
||
/* 一片刀片的 INA226 掉线,**剩下四路的读照常**。中断这一圈的现象是
|
||
「一片 INA226 掉线,五路判据全瞎」—— 而那四台手机什么毛病都没有。 */
|
||
static void test_one_dead_ina226_does_not_stop_the_other_four(void)
|
||
{
|
||
sim_i2c_set_present_ch(ch_of(3), rail_cfg(3)->ina_addr, false);
|
||
|
||
TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, t_safety_step(&g_snap));
|
||
|
||
TEST_ASSERT_FALSE(g_snap.rail_ok[2]);
|
||
for (int rail = 1; rail <= N; rail++) {
|
||
if (rail == 3) {
|
||
continue;
|
||
}
|
||
TEST_ASSERT_TRUE(g_snap.rail_ok[rail - 1]);
|
||
TEST_ASSERT_EQUAL_INT32(RAIL_MV(rail), g_snap.rail[rail - 1].bus_mv);
|
||
TEST_ASSERT_TRUE(g_snap.io_ok[rail - 1]);
|
||
}
|
||
/* 坏掉那一路的 PCA9555 没跟着遭殃(是两颗器件,不是一颗)。 */
|
||
TEST_ASSERT_TRUE(g_snap.io_ok[2]);
|
||
TEST_ASSERT_EQUAL_UINT8(k_port1[2], g_snap.io[2].port1);
|
||
TEST_ASSERT_TRUE(g_snap.input_ok);
|
||
TEST_ASSERT_EQUAL_UINT32(1, g_snap.i2c_errors);
|
||
|
||
/* 读 V 那一次就 NAK 了,读 I 那一次不会再发 ⇒ 这一圈**少一次事务**。
|
||
把这个数写下来,是为了让「失联之后还傻读第二次」也会变红。 */
|
||
TEST_ASSERT_EQUAL_UINT32(t_safety_cycle_txns() - 1u, g_snap.txns);
|
||
TEST_ASSERT_EQUAL_UINT32(
|
||
t_safety_budget_us() - (uint32_t)hal_i2c_bits_to_us(hal_i2c_txn_bits(1, 2)),
|
||
g_snap.bus_us);
|
||
}
|
||
|
||
/* 空槽**一个字节也不许发**(2026-09-27 上板实测)。空机时五路无条件各读一次,
|
||
真板上每次 NAK 要十几 ms,槽 1 两次就把整圈预算吃光 —— 槽 2~5 和主干的 0x43
|
||
一圈都没读到,输入侧监控整个是瞎的。这里把空槽的器件也拔掉:谁碰了它就会 NAK、
|
||
`i2c_errors` 立刻非零;全空那一圈还要求主干那片照读不误。 */
|
||
static void test_empty_slots_are_never_touched_and_the_input_still_reads(void)
|
||
{
|
||
const int empty[] = { 2, 4 };
|
||
for (unsigned i = 0; i < sizeof(empty) / sizeof(empty[0]); i++) {
|
||
const int rail = empty[i];
|
||
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);
|
||
}
|
||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&g_snap));
|
||
TEST_ASSERT_EQUAL_UINT32(0, g_snap.i2c_errors);
|
||
TEST_ASSERT_EQUAL_UINT32(0, g_snap.skipped);
|
||
for (int rail = 1; rail <= N; rail++) {
|
||
const bool here = (rail != 2 && rail != 4);
|
||
TEST_ASSERT_EQUAL(here, g_snap.rail_ok[rail - 1]);
|
||
TEST_ASSERT_EQUAL(here, g_snap.io_ok[rail - 1]);
|
||
TEST_ASSERT_EQUAL(here, g_snap.present[rail - 1]);
|
||
}
|
||
TEST_ASSERT_TRUE(g_snap.input_ok);
|
||
|
||
/* 五槽全空(上板时的真实状态):总线上只剩主干那一片。 */
|
||
for (int rail = 1; rail <= N; rail++) {
|
||
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);
|
||
}
|
||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&g_snap));
|
||
TEST_ASSERT_EQUAL_UINT32(0, g_snap.i2c_errors);
|
||
TEST_ASSERT_TRUE(g_snap.input_ok);
|
||
TEST_ASSERT_EQUAL_INT32(12000, g_snap.input.bus_mv);
|
||
}
|
||
|
||
/* 读失败时保留**上一圈的值**并把 ok 标成 false —— 不清零。
|
||
清零的话,判据会看到一个 0 V 的「正常数」;而 `ok` 正是让它先看一眼
|
||
「这数是不是新的」。拿一个没更新的电压去判过压是最难查的那种错。 */
|
||
static void test_a_failed_read_keeps_the_stale_value_and_flags_it(void)
|
||
{
|
||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&g_snap));
|
||
TEST_ASSERT_EQUAL_INT32(RAIL_MV(1), g_snap.rail[0].bus_mv);
|
||
|
||
sim_i2c_set_present_ch(ch_of(1), rail_cfg(1)->ina_addr, false);
|
||
/* 器件掉线之后把寄存器也改掉:万一实现偷偷读到了,值就会跟着变。 */
|
||
put_vi(1, 3000, 0);
|
||
|
||
for (int i = 0; i < 5; i++) {
|
||
TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, t_safety_step(&g_snap));
|
||
TEST_ASSERT_FALSE(g_snap.rail_ok[0]);
|
||
TEST_ASSERT_EQUAL_INT32(RAIL_MV(1), g_snap.rail[0].bus_mv); /* 还是上一圈那个 */
|
||
TEST_ASSERT_EQUAL_INT32(RAIL_UA(1), g_snap.rail[0].current_ua);
|
||
}
|
||
/* 器件回来了就立刻用新值(`ok` 不是个一去不返的标志)。 */
|
||
sim_i2c_set_present_ch(ch_of(1), rail_cfg(1)->ina_addr, true);
|
||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&g_snap));
|
||
TEST_ASSERT_TRUE(g_snap.rail_ok[0]);
|
||
TEST_ASSERT_EQUAL_INT32(3000, g_snap.rail[0].bus_mv);
|
||
}
|
||
|
||
/* 同一路上的两颗器件互不牵连:PCA9555 读不到,不影响这一路 INA226 已经读到的值。
|
||
`cycle_rail()` 里「器件读失败不提前返回」那一条就是这个意思 ——
|
||
提前返回的现象是「一片 I/O 扩展器失联,这一路的过压判据也跟着停了」。 */
|
||
static void test_a_dead_pca9555_does_not_spoil_the_same_rails_ina226(void)
|
||
{
|
||
sim_i2c_set_present_ch(ch_of(2), PCA9555_ADDR, false);
|
||
|
||
TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, t_safety_step(&g_snap));
|
||
|
||
TEST_ASSERT_FALSE(g_snap.io_ok[1]);
|
||
TEST_ASSERT_TRUE(g_snap.rail_ok[1]); /* INA226 是好的,值是新的 */
|
||
TEST_ASSERT_EQUAL_INT32(RAIL_MV(2), g_snap.rail[1].bus_mv);
|
||
TEST_ASSERT_EQUAL_INT32(RAIL_UA(2), g_snap.rail[1].current_ua);
|
||
for (int rail = 1; rail <= N; rail++) {
|
||
if (rail != 2) {
|
||
TEST_ASSERT_TRUE(g_snap.io_ok[rail - 1]);
|
||
}
|
||
}
|
||
TEST_ASSERT_EQUAL_UINT32(1, g_snap.i2c_errors);
|
||
}
|
||
|
||
/* 器件掉线之后节拍照样是 20 ms(NAK 的事务也占总线时间,只是占得更少)。
|
||
测的是「巡检不会因为总线出错就乱了节奏」。 */
|
||
static void test_the_beat_survives_a_dead_blade(void)
|
||
{
|
||
sim_i2c_set_present_ch(ch_of(5), rail_cfg(5)->ina_addr, false);
|
||
sim_i2c_set_present_ch(ch_of(5), PCA9555_ADDR, false);
|
||
|
||
sched_periodic_t tick;
|
||
sched_periodic_init(&tick, 20);
|
||
|
||
uint32_t prev = hal_time_now_ms();
|
||
for (int i = 0; i < 50; i++) {
|
||
(void)t_safety_step(&g_snap);
|
||
sched_periodic_wait(&tick);
|
||
TEST_ASSERT_EQUAL_UINT32(20, hal_time_now_ms() - prev);
|
||
prev = hal_time_now_ms();
|
||
}
|
||
TEST_ASSERT_EQUAL_UINT32(0, tick.overruns);
|
||
TEST_ASSERT_EQUAL_UINT32(100, g_snap.i2c_errors); /* 每圈两颗器件各一次 */
|
||
/* 别的四路一点没受影响 —— 这才是「失联 ≠ 别的路也不看了」。 */
|
||
for (int rail = 1; rail <= 4; rail++) {
|
||
TEST_ASSERT_TRUE(g_snap.rail_ok[rail - 1]);
|
||
TEST_ASSERT_EQUAL_INT32(RAIL_MV(rail), g_snap.rail[rail - 1].bus_mv);
|
||
}
|
||
}
|
||
|
||
/* 第一圈的占用里不该混进启动期间的事务(游标要在 `t_safety_init()` 时对齐到「现在」)。
|
||
混进去的现象是「开机那一下占用率 40 %」,然后再也不复现。 */
|
||
static void test_startup_traffic_is_not_charged_to_the_first_cycle(void)
|
||
{
|
||
/* 假装启动时又扫了一遍总线。 */
|
||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_lock_take(HAL_LOCK_I2C, 10));
|
||
(void)hal_i2c_probe(TCA9548A_ADDR);
|
||
(void)hal_i2c_probe(INA226_ADDR_INPUT);
|
||
hal_lock_give(HAL_LOCK_I2C);
|
||
|
||
t_safety_init(&g_snap);
|
||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&g_snap));
|
||
TEST_ASSERT_EQUAL_UINT32(t_safety_budget_us(), g_snap.bus_us);
|
||
TEST_ASSERT_EQUAL_UINT32(t_safety_cycle_txns(), g_snap.txns);
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
UNITY_BEGIN();
|
||
RUN_TEST(test_one_cycle_is_exactly_the_budgeted_transaction_count);
|
||
RUN_TEST(test_measured_bus_time_equals_the_static_budget);
|
||
RUN_TEST(test_the_bus_account_matches_plan_5_5);
|
||
|
||
RUN_TEST(test_each_rail_is_read_on_its_own_mux_channel);
|
||
RUN_TEST(test_each_blade_reports_its_own_pca9555_inputs);
|
||
RUN_TEST(test_the_mux_never_has_two_channels_open);
|
||
RUN_TEST(test_the_unused_channels_are_never_selected);
|
||
|
||
RUN_TEST(test_safety_holds_a_20ms_beat_for_500_cycles);
|
||
RUN_TEST(test_the_period_outlives_one_ina226_conversion);
|
||
|
||
RUN_TEST(test_every_transaction_happens_under_the_i2c_lock);
|
||
RUN_TEST(test_the_lock_check_itself_can_fail);
|
||
RUN_TEST(test_a_busy_bus_skips_the_cycle_instead_of_waiting_forever);
|
||
|
||
RUN_TEST(test_one_dead_ina226_does_not_stop_the_other_four);
|
||
RUN_TEST(test_empty_slots_are_never_touched_and_the_input_still_reads);
|
||
RUN_TEST(test_a_failed_read_keeps_the_stale_value_and_flags_it);
|
||
RUN_TEST(test_a_dead_pca9555_does_not_spoil_the_same_rails_ina226);
|
||
RUN_TEST(test_the_beat_survives_a_dead_blade);
|
||
RUN_TEST(test_startup_traffic_is_not_charged_to_the_first_cycle);
|
||
return UNITY_END();
|
||
}
|