文件
phonerail/firmware/test/test_t_actuator.c
T
zhou yang和Claude Opus 5.5 ef91daf07d 固件 0.1.3-c5:硬重启开机校验 + 重按(0.1.2)、status 上报上游电脑在不在(host)
- 0.1.2:power_cycle 按完电源键后看电流,没开就重按,最多 3 次;新事件 19 BOOT_CHECK
- 0.1.3:UP_VBUS_DET(IO6)启动时配成输入,status 加 "host":true|false|null
- rack:单板页显示「上游:已连电脑 / 未连电脑」
- HANDOFF:损耗两档的来历(Hub 工作 ≈1.1 W)、板 B 第 2 槽 PRSNT2# 卡低

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 08:59:12 +08:00

1393 行
60 KiB
C
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
/* A9(**R2 版**):三个执行器状态机 + 互斥(需求 §10.6)。计划里这一包的验收原话是
「场景测试:硬重启走完 17 ms 上电时序;按键序列不重入;EDL 顺序固定」。
⚠️ 这三条都**不是**「函数返回了 HAL_OK」能证明的东西,所以本文件里的断言
一律打在**线上真实发生的事**上:
· 按键 / EDL / VBUS 看五片假 PCA9555 的**输出锁存器与栅极电平**
(`sim_pca9555_output/pin`)与**写事务计数**(`sim_i2c_dev_ch(...)->n_write`),
不看驱动自己的影子 —— 对着影子断言等于自己证明自己,
「组合键分两次写」「影子记到了另一片上」影子变量一律看不出来;
· `EFUSE_EN` 看 `sim_gpio_level()` 与全局写序号 `sim_gpio_write_seq()`。
---- R1 → R2 这一层换了什么 ----------------------------------------------
R1 里按键 / EDL 在 74HC595 的 24 位链上(纯 GPIO),`VBUS_EN` 是直连 GPIO,
所以执行器**与 I²C 无交集**。R2 里这三样全在刀片那颗 PCA9555 的 Port0 上:
bit0 KEY_PWR bit1 KEY_VUP bit2 KEY_VDN bit3 EDL_EN bit4 VBUS_EN
(位号五片逐格相同,区分五路的是 **mux 通道**)
于是本层的失败形态整个换了一批,测试也跟着换:
| R1 的失败形态 | R2 的对应形态 | 本文件里哪一条抓 |
|---|---|---|
| 位号算错(链上算成另一路的位) | **mux 通道选错 / 影子下标传错** | `..._every_action_selects_this_rails_mux_channel`、`..._pressing_ch1_does_not_touch_ch4` |
| 组合键分两帧 RCLK | 组合键**分两次 `pca9555_write_out0`** | `..._combo_keys_land_in_exactly_one_write` |
| 收尾时 595 锁被别人占着 | 收尾时 **I²C 锁**被别人占着 | `..._cleanup_retries_until_the_key_is_really_released` |
| 两把锁不许嵌套 | **只剩一把锁**,改成与巡检**争用** | `..._and_safety_share_one_lock_without_starving` |
虚拟时钟下这些场景是零耗时的:`run_ms()` 干的就是任务体干的事
—— 调一次 `t_actuator_step()`,按它返回的时间往前拨表。
⚠️ 假总线上的事务**会花掉虚拟时钟**(`sim_i2c_set_bus_time` 默认开),
这里**故意不关**:执行器与巡检争用同一把锁这件事,靠的就是总线时间是真的。 */
#include <stdio.h>
#include <string.h>
#include "board_pins.h"
#include "hal_gpio.h"
#include "hal_i2c.h"
#include "hal_lock.h"
#include "hal_time.h"
#include "i2cbus.h"
#include "ina226.h"
#include "pca9555.h"
#include "phonerail.h"
#include "rail_cfg.h"
#include "sim_hal.h"
#include "slot.h"
#include "t_actuator.h"
#include "t_safety.h"
#include "tca9548a.h"
#include "unity.h"
#define N PHONERAIL_RAIL_COUNT
/* ---- 假 PG ---------------------------------------------------------------
真板上这个值来自 `t_safety` 每 20 ms 读回来的那一路 PCA9555 快照(app_main.c 里接的)。
`readable = false` 模拟「这一片 PCA9555 失联 / 巡检还没跑过一圈」—— 那是**读不到**,
和「读到了,是低」(电源还没好)是两回事,两条路径下面都要测到。 */
static bool s_pg_readable;
static bool s_pg_val[N];
static bool fake_pg(void *ctx, int rail, bool *pg)
{
(void)ctx;
if (!s_pg_readable) {
return false;
}
*pg = s_pg_val[rail - 1];
return true;
}
/* ---- 五片刀片的观测口 ----------------------------------------------------
`idx`(假器件下标)与 `rail - 1` 对应,但通道一律查 `rail_cfg()`,**不算**
—— 「mux_ch = rail − 1」是当下的事实,不是不变量(rail_cfg.h 那条禁令)。 */
static sim_i2c_dev_t *blade(int rail)
{
sim_i2c_dev_t *d = sim_i2c_dev_ch(rail_cfg(rail)->mux_ch, PCA9555_ADDR);
TEST_ASSERT_NOT_NULL(d);
return d;
}
static uint8_t latch(int rail) { return sim_pca9555_output(rail - 1, 0); }
static int pin(int rail, int bit) { return sim_pca9555_pin(rail - 1, 0, bit); }
static int key_pin(int rail, pca9555_key_t k) { return pin(rail, pca9555_key_bit(k)); }
static void snap_latches(uint8_t out[N])
{
for (int r = 1; r <= N; r++) {
out[r - 1] = latch(r);
}
}
static void snap_writes(uint32_t out[N])
{
for (int r = 1; r <= N; r++) {
out[r - 1] = blade(r)->n_write;
}
}
/* 「除了 `except` 那一路,别的四片一个字节都没动」。
⭐ R2 里「按了另一台手机的电源键」就长这个样子:本路的位号完全正确,
只是通道选错 / 影子下标传错,事务落到了另一片上。五路电压电流全部正常。 */
static void assert_only_this_blade_moved(int except, const uint8_t l0[N],
const uint32_t w0[N])
{
for (int r = 1; r <= N; r++) {
if (r == except) {
continue;
}
TEST_ASSERT_EQUAL_HEX8(l0[r - 1], latch(r));
TEST_ASSERT_EQUAL_UINT32(w0[r - 1], blade(r)->n_write);
}
TEST_ASSERT_EQUAL_UINT32(0, sim_i2c_ambiguous()); /* 同时开两路 = 两片一起应答 */
}
/* ---- 窃听钩子:把「一次 I²C 写」和「一次 GPIO 写」排进同一条时间轴 --------
R1 靠比较 `RCLK` 与 `EFUSE_EN` 两根脚的全局写序号证明「先松键、再断电」。
R2 里松键是一次 I²C 写、断电仍是一次 GPIO 写,两边的计数器不通用,
所以在假器件上挂一个钩子:每次写落地时记下「`EFUSE_EN` 到此刻被写过几次」。
钩子**链到**假 PCA9555 自己的那一个,不是把它顶掉(顶掉就没人查短路了)。 */
static sim_i2c_hook_t s_inner_hook;
static int s_spy_pin = -1;
static uint32_t s_spy_writes;
static uint32_t s_spy_gpio_writes_at_first; /* 第一次写发生时,那根脚的写次数 */
static void spy_hook(sim_i2c_dev_t *d, sim_i2c_ev_t ev)
{
if (ev == SIM_I2C_EV_POST_WRITE) {
if (s_spy_writes == 0 && s_spy_pin >= 0) {
s_spy_gpio_writes_at_first = sim_gpio_writes(s_spy_pin);
}
s_spy_writes++;
}
if (s_inner_hook != NULL) {
s_inner_hook(d, ev);
}
}
static void spy_on(int rail, int gpio_pin)
{
sim_i2c_dev_t *d = blade(rail);
s_inner_hook = d->hook;
d->hook = spy_hook;
s_spy_pin = gpio_pin;
s_spy_writes = 0;
s_spy_gpio_writes_at_first = 0;
}
/* ---- 假板子 --------------------------------------------------------------
底板一颗 TCA9548A,五个通道后面各挂一片同地址(0x20)的 PCA9555 ——
**五片同址**正是 R2 D8 的全部意义,不是笔误。 */
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++) {
sim_pca9555_attach_ch(rail - 1, rail_cfg(rail)->mux_ch, PCA9555_ADDR);
}
TEST_ASSERT_EQUAL_INT(HAL_OK, i2cbus_init());
TEST_ASSERT_EQUAL_INT(HAL_OK, rail_en_init()); /* 五根 EN 线,全部拉低 */
for (int rail = 1; rail <= N; rail++) {
/* 输出锁存器先清零、方向最后改(pca9555.h 那个框)。之后影子与锁存器都是 0。 */
TEST_ASSERT_EQUAL_INT(HAL_OK, slot_init(rail));
TEST_ASSERT_EQUAL_HEX8(0x00, latch(rail));
}
/* 初始化一步都不许把接地脚配成输出,也不许在锁存器还是 0xFF 时就开方向。 */
TEST_ASSERT_EQUAL_UINT32(0, sim_pca9555_gnd_driven());
TEST_ASSERT_EQUAL_UINT32(0, sim_pca9555_unsafe_enable());
/* 强制加锁纪律 —— 但要在初始化**之后**打开:启动期是独占跑的,
本来就不该被要求先拿锁。打开之后,任何一次 I²C 事务没持锁就记一次违规。 */
sim_lock_require(HAL_LOCK_I2C, true);
s_pg_readable = true;
memset(s_pg_val, 0, sizeof(s_pg_val));
s_inner_hook = NULL;
s_spy_pin = -1;
s_spy_writes = 0;
s_spy_gpio_writes_at_first = 0;
t_actuator_init(fake_pg, NULL);
}
void setUp(void)
{
sim_hal_reset();
board_up();
}
void tearDown(void) {}
/* 把虚拟时钟往前推 ms,期间按 `t_actuator_step()` 自己要的节奏叫它 ——
这正是 app_main.c 里那个任务体在干的事。
⚠️ 用**绝对目标时刻**而不是「累减 left」:等锁超时会在 step 内部把虚拟时钟
花掉 20 ms(hal_lock_sim.c),I²C 事务本身也花时钟,累减的写法会因此推过头。 */
static void run_ms(uint32_t ms)
{
const uint32_t target = hal_time_now_ms() + ms;
for (;;) {
uint32_t w = t_actuator_step();
const uint32_t now = hal_time_now_ms();
if ((int32_t)(now - target) >= 0) {
return; /* 到点了,而且最后这一步已经推过 */
}
const uint32_t left = target - now;
if (w == 0 || w > left) {
w = left;
}
sim_time_advance_ms(w);
}
}
/* 扮演第二个任务,把 I²C 锁占住 / 放开(单线程 sim 里「别人正持着锁」就这么造)。 */
static void other_task_takes_the_bus(void)
{
sim_lock_set_task(1);
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_lock_take(HAL_LOCK_I2C, 10));
sim_lock_set_task(0);
}
static void other_task_frees_the_bus(void)
{
sim_lock_set_task(1);
hal_lock_give(HAL_LOCK_I2C);
sim_lock_set_task(0);
}
/* ==== 验收主项一:硬重启走完 17 ms 上电时序 =============================== */
/* 用 CH3 —— 它的 `EFUSE_EN` 是 IO11、mux 通道 2、DAC 在**另一片**上,
全是「按路号算就一定算错」的那一路。 */
static void test_hard_reset_walks_the_full_power_up_sequence(void)
{
const int rail = 3;
const int en = rail_cfg(rail)->efuse_en;
/* 先让这一路开着 —— 硬重启是给**正在跑的**手机用的。 */
TEST_ASSERT_EQUAL_INT(HAL_OK, rail_efuse_enable(rail, true));
TEST_ASSERT_EQUAL_INT(1, sim_gpio_level(en));
s_pg_val[rail - 1] = true; /* PG 一直是好的 */
const uint32_t takes0 = sim_lock_takes(HAL_LOCK_I2C);
const uint32_t t_req = hal_time_now_ms();
uint8_t l0[N];
uint32_t w0[N];
snap_latches(l0);
snap_writes(w0);
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail));
TEST_ASSERT_EQUAL_INT(0, sim_gpio_level(en)); /* 请求那一刻就断电,不等下一拍 */
TEST_ASSERT_TRUE(t_actuator_busy(rail));
TEST_ASSERT_EQUAL_INT(ACT_SEQ_HARD_RESET, t_actuator_seq(rail));
/* 断电窗口是 §7.4 的 3~5 s(放电由硬件联锁做,τ 22 ms)。差一毫秒都不许上电:
缩短它的现象**不是**「重启快了」,而是「有的机型压根没重启」。 */
run_ms(ACT_HR_OFF_MIN_MS - 1);
TEST_ASSERT_EQUAL_INT(0, sim_gpio_level(en));
TEST_ASSERT_EQUAL_INT(ACT_ST_HR_OFF, t_actuator_state(rail));
run_ms(ACT_HR_OFF_MS - (ACT_HR_OFF_MIN_MS - 1));
TEST_ASSERT_EQUAL_INT(1, sim_gpio_level(en)); /* EN↑ */
TEST_ASSERT_EQUAL_INT(ACT_ST_HR_RAMP, t_actuator_state(rail));
const uint32_t t_on = hal_time_now_ms();
const uint32_t off_ms = t_on - t_req;
TEST_ASSERT_TRUE(off_ms >= ACT_HR_OFF_MIN_MS && off_ms <= ACT_HR_OFF_MAX_MS);
/* ⭐ 这一条就是「17 ms 上电时序」:**PG 早就是 1 了**,序列也一步都不许提前完成。
tON 5.1 ms + 缓启动斜坡 11.5 ms 之内,输出还在爬。
少等这 17 ms 的实现在这里当场变红。 */
run_ms(ACT_HR_RAMP_MS - 1);
TEST_ASSERT_EQUAL_INT(ACT_ST_HR_RAMP, t_actuator_state(rail));
TEST_ASSERT_TRUE(t_actuator_busy(rail));
run_ms(1);
TEST_ASSERT_EQUAL_UINT32(ACT_HR_RAMP_MS, hal_time_now_ms() - t_on);
/* ⭐ PG 到了 ⇒ 当场按下电源键开机。电池端上电的手机停在关机态,
不按这一下的硬重启等于「关机」(小米8 实测,t_actuator.h)。 */
TEST_ASSERT_EQUAL_INT(ACT_ST_HR_BOOT, t_actuator_state(rail));
TEST_ASSERT_EQUAL_INT(1, key_pin(rail, PCA9555_KEY_PWR));
TEST_ASSERT_EQUAL_INT(0, key_pin(rail, PCA9555_KEY_VUP));
TEST_ASSERT_EQUAL_INT(0, key_pin(rail, PCA9555_KEY_VDN));
run_ms(ACT_HR_BOOT_KEY_MS - 1);
TEST_ASSERT_EQUAL_INT(1, key_pin(rail, PCA9555_KEY_PWR));
TEST_ASSERT_TRUE(t_actuator_busy(rail));
run_ms(1);
TEST_ASSERT_EQUAL_INT(0, key_pin(rail, PCA9555_KEY_PWR)); /* 松开 */
TEST_ASSERT_FALSE(t_actuator_busy(rail));
TEST_ASSERT_EQUAL_INT(ACT_RES_OK, t_actuator_last_result(rail));
TEST_ASSERT_EQUAL_INT(1, sim_gpio_level(en));
TEST_ASSERT_EQUAL_UINT32(1, t_actuator_done(rail));
/* ⭐ 总线上只有按下、松开这两次事务,而且只动这一片。断电那一段一个都不发:
键本来就没按着 ⇒ 影子不脏。R2 里每一次多余的事务都是从巡检嘴里抢锁。 */
TEST_ASSERT_EQUAL_UINT32(takes0 + 2, sim_lock_takes(HAL_LOCK_I2C));
assert_only_this_blade_moved(rail, l0, w0);
TEST_ASSERT_EQUAL_HEX8(l0[rail - 1], latch(rail)); /* 收工后这一片回到原样 */
TEST_ASSERT_EQUAL_UINT32(0, sim_lock_violations(HAL_LOCK_I2C));
}
/* 别的四路的 `EFUSE_EN`,从头到尾一次都不该被写。 */
static void test_hard_reset_touches_only_its_own_en_pin(void)
{
const int rail = 2;
uint32_t before[N];
for (int r = 1; r <= N; r++) {
before[r - 1] = sim_gpio_writes(rail_cfg(r)->efuse_en);
}
s_pg_val[rail - 1] = true;
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail));
run_ms(ACT_HR_OFF_MS + ACT_HR_RAMP_MS + ACT_HR_BOOT_KEY_MS);
TEST_ASSERT_FALSE(t_actuator_busy(rail));
for (int r = 1; r <= N; r++) {
if (r == rail) {
continue;
}
TEST_ASSERT_EQUAL_UINT32(before[r - 1], sim_gpio_writes(rail_cfg(r)->efuse_en));
}
/* VBUS 是 EDL 的事,硬重启不碰 —— R2 里它在 PCA9555 的 P0.4 上。 */
for (int r = 1; r <= N; r++) {
TEST_ASSERT_EQUAL_INT(0, pin(r, PCA9555_BIT_VBUS_EN));
TEST_ASSERT_FALSE(slot_vbus_is_on(r));
}
}
/* PG 等不到(这一片 PCA9555 失联)⇒ 记 NO_PG,但**电继续开着**。
§10.10:这片不应答 = 失去 PG 可见性,**不关输出**。把「看不见」当「坏了」去断电,
等于一片 I/O 扩展器失联就让五台手机一起停机。 */
static void test_no_pg_leaves_the_rail_powered(void)
{
const int rail = 4;
const int en = rail_cfg(rail)->efuse_en;
s_pg_readable = false;
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail));
run_ms(ACT_HR_OFF_MS + ACT_HR_RAMP_MS + ACT_HR_PG_TIMEOUT_MS);
TEST_ASSERT_FALSE(t_actuator_busy(rail));
TEST_ASSERT_EQUAL_INT(ACT_RES_NO_PG, t_actuator_last_result(rail));
TEST_ASSERT_EQUAL_INT(1, sim_gpio_level(en)); /* ⭐ 不许再关掉 */
/* 开机键也不按:键和 PG 在同一片 PCA9555 上,读不到 PG 时去按只会卡在 CLEANUP。 */
TEST_ASSERT_EQUAL_INT(0, key_pin(rail, PCA9555_KEY_PWR));
}
/* 读得到、但一直是低(电源确实没好)走的是同一条路:照样不自作主张断电。 */
static void test_pg_stuck_low_also_leaves_the_rail_powered(void)
{
const int rail = 1;
s_pg_readable = true;
s_pg_val[rail - 1] = false;
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail));
run_ms(ACT_HR_OFF_MS + ACT_HR_RAMP_MS + ACT_HR_PG_TIMEOUT_MS);
TEST_ASSERT_EQUAL_INT(ACT_RES_NO_PG, t_actuator_last_result(rail));
TEST_ASSERT_EQUAL_INT(1, sim_gpio_level(rail_cfg(rail)->efuse_en));
}
/* PG 迟到(斜坡走完之后 200 ms 才置位)也要认,不能只在进 HR_PG 那一刻看一眼。 */
static void test_late_pg_is_still_accepted(void)
{
const int rail = 5;
s_pg_readable = true;
s_pg_val[rail - 1] = false;
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail));
run_ms(ACT_HR_OFF_MS + ACT_HR_RAMP_MS + 200);
TEST_ASSERT_EQUAL_INT(ACT_ST_HR_PG, t_actuator_state(rail));
s_pg_val[rail - 1] = true;
run_ms(ACT_HR_PG_POLL_MS);
TEST_ASSERT_EQUAL_INT(ACT_ST_HR_BOOT, t_actuator_state(rail));
run_ms(ACT_HR_BOOT_KEY_MS);
TEST_ASSERT_FALSE(t_actuator_busy(rail));
TEST_ASSERT_EQUAL_INT(ACT_RES_OK, t_actuator_last_result(rail));
}
/* ⚠️ 上面那些用的都是 `run_ms()` —— 它按**测试自己的**节奏叫 step,
所以测不出「step 返回的睡眠时长本身对不对」。真板上的任务体是**完全听 step 的**:
它说睡多久就睡多久。这个测试照那样跑,PG 在斜坡走完之后才置位,
序列必须在一个轮询间隔之内收工 —— 「等 PG 时一觉睡满 500 ms 超时」的写法在这里变红
(那种写法把「等到 PG 就结束」悄悄变成了「一律等满 500 ms」,
而测试与遥测都看不出区别:结果照样是 OK,只是每次硬重启都慢半秒)。 */
static void test_the_task_loop_only_sleeps_what_step_asks_for(void)
{
const int rail = 2;
const uint32_t pg_at = ACT_HR_OFF_MS + ACT_HR_RAMP_MS + 1;
s_pg_readable = true;
s_pg_val[rail - 1] = false;
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail));
const uint32_t t0 = hal_time_now_ms();
bool pg_raised = false;
uint32_t total = 0;
for (int i = 0; i < 10000; i++) {
const uint32_t w = t_actuator_step(); /* 任务体:它说睡多久就睡多久 */
if (t_actuator_state(rail) == ACT_ST_HR_BOOT) {
total = hal_time_now_ms() - t0; /* 等到 PG 的**那一刻**,不是下一次醒来 */
break;
}
sim_time_advance_ms(w == 0 ? 1 : w);
if (!pg_raised && hal_time_now_ms() - t0 >= pg_at) {
s_pg_val[rail - 1] = true;
pg_raised = true;
}
}
TEST_ASSERT_TRUE(pg_raised);
TEST_ASSERT_TRUE(total > 0 && total <= pg_at + 2 * ACT_HR_PG_POLL_MS);
/* 按键保持期也照样听 step 的:睡到点就松键收工,不多睡一整段。 */
for (int i = 0; i < 10000 && t_actuator_busy(rail); i++) {
const uint32_t w = t_actuator_step();
if (t_actuator_busy(rail)) {
sim_time_advance_ms(w == 0 ? 1 : w);
}
}
TEST_ASSERT_FALSE(t_actuator_busy(rail));
TEST_ASSERT_EQUAL_INT(ACT_RES_OK, t_actuator_last_result(rail));
TEST_ASSERT_TRUE(hal_time_now_ms() - t0 <= total + ACT_HR_BOOT_KEY_MS + ACT_CLEANUP_RETRY_MS);
}
/* 硬重启之前先把这一路的三个键松开:断电重启时有键按着,手机会开进
Recovery / Fastboot 而不是正常起来。这里手工造一个「上一次收尾没落地」的脏位。
⭐ 顺序也要对:**先松键、再断电**。R2 里这两件事一件在 I²C 上、一件在 GPIO 上,
两边的计数器不通用,所以靠钩子记下「松键那一刻 EN 被写过几次」。
反过来写(先断电再松键)的现象是:手机在断电那一刻键还按着 —— 而重启之后
它进的是 Recovery,遥测上电压电流一切正常。 */
static void test_hard_reset_releases_a_stuck_key_before_cutting_power(void)
{
const int rail = 1;
const int other = 4;
const int en = rail_cfg(rail)->efuse_en;
const uint8_t km = pca9555_key_mask();
/* 造脏位:这一路三个键按着,另一路按着音量上(它一动都不许动)。 */
TEST_ASSERT_EQUAL_INT(HAL_OK, slot_out0_write(rail, km, km));
TEST_ASSERT_EQUAL_INT(
HAL_OK, slot_out0_write(other, (uint8_t)(1u << pca9555_key_bit(PCA9555_KEY_VUP)),
(uint8_t)(1u << pca9555_key_bit(PCA9555_KEY_VUP))));
TEST_ASSERT_EQUAL_INT(1, key_pin(rail, PCA9555_KEY_PWR));
TEST_ASSERT_EQUAL_INT(1, key_pin(other, PCA9555_KEY_VUP));
spy_on(rail, en);
const uint32_t en_writes0 = sim_gpio_writes(en);
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail));
TEST_ASSERT_EQUAL_HEX8(0x00, (uint8_t)(latch(rail) & km)); /* 三个键一起松开 */
TEST_ASSERT_EQUAL_INT(1, key_pin(other, PCA9555_KEY_VUP)); /* 别路的键一动没动 */
/* 松键**只发一帧**(三位在同一次写里),而且发的时候 EN 还没被碰过。 */
TEST_ASSERT_EQUAL_UINT32(1, s_spy_writes);
TEST_ASSERT_EQUAL_UINT32(en_writes0, s_spy_gpio_writes_at_first);
TEST_ASSERT_EQUAL_UINT32(en_writes0 + 1, sim_gpio_writes(en));
TEST_ASSERT_EQUAL_INT(0, sim_gpio_level(en));
}
/* 影子是干净的时候(正常情况)**一个事务都不发** —— 每一次多余的写都是从
每 20 ms 一圈的巡检嘴里抢同一把锁。 */
static void test_the_defensive_release_sends_nothing_when_the_shadow_is_clean(void)
{
const uint32_t takes0 = sim_lock_takes(HAL_LOCK_I2C);
uint8_t l0[N];
uint32_t w0[N];
snap_latches(l0);
snap_writes(w0);
for (int rail = 1; rail <= N; rail++) {
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail));
}
for (int rail = 1; rail <= N; rail++) {
TEST_ASSERT_EQUAL_UINT32(w0[rail - 1], blade(rail)->n_write);
TEST_ASSERT_EQUAL_HEX8(l0[rail - 1], latch(rail));
}
TEST_ASSERT_EQUAL_UINT32(takes0, sim_lock_takes(HAL_LOCK_I2C));
}
/* ==== 验收主项二:按键序列不重入 ========================================= */
/* 短按 100 ms(SW-5 的第一档)。位号一律查 `pca9555_key_bit()`:R2 里它确实
与路号无关,但「与路号无关」是当下的事实,不是写死 `1 << 0` 的理由。 */
static void test_key_tap_presses_the_table_bit_and_releases_it(void)
{
const int rail = 5;
const int bit = pca9555_key_bit(PCA9555_KEY_PWR);
const uint32_t w0 = blade(rail)->n_write;
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_key(rail, ACT_KEYS_PWR, ACT_KEY_MS_TAP));
TEST_ASSERT_EQUAL_INT(1, pin(rail, bit)); /* 栅极真的被拉起来了 */
TEST_ASSERT_EQUAL_HEX8((uint8_t)(1u << bit), latch(rail));
TEST_ASSERT_EQUAL_UINT32(w0 + 1, blade(rail)->n_write);
/* ⭐ 这一次写落在**这一路的**通道上。选错通道 = 按了另一台手机的电源键。 */
TEST_ASSERT_EQUAL_INT(rail_cfg(rail)->mux_ch, sim_i2c_mux_ch());
run_ms(ACT_KEY_MS_TAP - 1);
TEST_ASSERT_EQUAL_INT(1, pin(rail, bit));
TEST_ASSERT_TRUE(t_actuator_busy(rail));
run_ms(1);
TEST_ASSERT_EQUAL_INT(0, pin(rail, bit));
TEST_ASSERT_EQUAL_HEX8(0x00, latch(rail));
TEST_ASSERT_EQUAL_UINT32(w0 + 2, blade(rail)->n_write); /* 按下一次、松开一次 */
TEST_ASSERT_FALSE(t_actuator_busy(rail));
TEST_ASSERT_EQUAL_INT(ACT_RES_OK, t_actuator_last_result(rail));
TEST_ASSERT_EQUAL_UINT32(0, sim_lock_violations(HAL_LOCK_I2C));
}
/* ⭐ §10.6 的硬约束:组合键必须**一次写**进去。
R1 靠「同一帧 RCLK」证明,R2 靠「一次 `pca9555_write_out0`」——
假器件的写事务计数是唯一能证明这件事的东西:分两次写的实现,影子最终值、
栅极最终电平、函数返回码**全部一模一样**,只有 `n_write` 会是 2。
而分两次写的真实后果是「先单按了一下 VOL−」,在有的机型上就是调音量。 */
static void test_combo_keys_land_in_exactly_one_write(void)
{
const int rail = 2;
const uint8_t want = (uint8_t)((1u << pca9555_key_bit(PCA9555_KEY_VDN)) |
(1u << pca9555_key_bit(PCA9555_KEY_PWR)));
const uint32_t w0 = blade(rail)->n_write;
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_key(rail, ACT_KEYS_FASTBOOT, ACT_KEY_MS_ON));
TEST_ASSERT_EQUAL_UINT32(w0 + 1, blade(rail)->n_write); /* ⭐ 正好一次 */
TEST_ASSERT_EQUAL_HEX8(want, latch(rail));
TEST_ASSERT_EQUAL_INT(1, key_pin(rail, PCA9555_KEY_VDN));
TEST_ASSERT_EQUAL_INT(1, key_pin(rail, PCA9555_KEY_PWR));
TEST_ASSERT_EQUAL_INT(0, key_pin(rail, PCA9555_KEY_VUP)); /* 第三个键没被顺手带上 */
run_ms(ACT_KEY_MS_ON);
TEST_ASSERT_EQUAL_HEX8(0x00, latch(rail));
TEST_ASSERT_EQUAL_UINT32(w0 + 2, blade(rail)->n_write); /* 松开也是一次 */
}
/* Recovery 是 VOL+ + PWR,和 Fastboot 差在哪一个音量键上 —— 两者不能相等。 */
static void test_recovery_and_fastboot_are_different_key_sets(void)
{
const int rail = 3;
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_key(rail, ACT_KEYS_RECOVERY, ACT_KEY_MS_ON));
const uint8_t recovery = latch(rail);
run_ms(ACT_KEY_MS_ON);
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_key(rail, ACT_KEYS_FASTBOOT, ACT_KEY_MS_ON));
const uint8_t fastboot = latch(rail);
run_ms(ACT_KEY_MS_ON);
TEST_ASSERT_TRUE(recovery != fastboot);
TEST_ASSERT_EQUAL_HEX8((uint8_t)(1u << pca9555_key_bit(PCA9555_KEY_PWR)),
(uint8_t)(recovery & fastboot)); /* 共有的只有电源键 */
}
/* ⭐ R2 新增:**不许在持锁期间等时间。** 按键保持 100 ms / 2 s / 10 s 是状态机的事,
不是持锁的事 —— 持着锁等 10 s,那 10 s 里巡检一圈都跑不了,SR-5 整条判据停摆,
而按键功能本身完全正常(测试与遥测都看不出区别)。
证明方式:保持期的每一拍,这把锁都必须是**没人持着**的;
整个序列上锁的次数正好 **2**(按下一次、松开一次)。 */
static void test_the_key_hold_never_holds_the_i2c_lock(void)
{
const int rail = 3;
const uint32_t takes0 = sim_lock_takes(HAL_LOCK_I2C);
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_key(rail, ACT_KEYS_PWR, ACT_KEY_MS_ON));
TEST_ASSERT_EQUAL_INT(-1, sim_lock_owner(HAL_LOCK_I2C)); /* 写完当场就放掉 */
TEST_ASSERT_FALSE(hal_lock_held(HAL_LOCK_I2C));
for (int i = 0; i < (int)(ACT_KEY_MS_ON / 20) + 20; i++) {
(void)t_actuator_step();
TEST_ASSERT_EQUAL_INT(-1, sim_lock_owner(HAL_LOCK_I2C));
TEST_ASSERT_FALSE(hal_lock_held(HAL_LOCK_I2C));
sim_time_advance_ms(20); /* 巡检的节拍 */
}
TEST_ASSERT_FALSE(t_actuator_busy(rail));
TEST_ASSERT_EQUAL_HEX8(0x00, latch(rail));
TEST_ASSERT_EQUAL_UINT32(takes0 + 2, sim_lock_takes(HAL_LOCK_I2C));
}
/* ⭐ 不重入:一路上正在跑序列时,三个请求全部被拒,而且**一个字节都不发**。 */
static void test_a_running_sequence_rejects_every_new_request(void)
{
const int rail = 1;
const int en = rail_cfg(rail)->efuse_en;
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_key(rail, ACT_KEYS_PWR, ACT_KEY_MS_FORCE));
const uint32_t writes1 = blade(rail)->n_write;
const uint8_t latch1 = latch(rail);
const uint32_t en_writes1 = sim_gpio_writes(en);
TEST_ASSERT_EQUAL_INT(HAL_ERR_STATE, t_actuator_key(rail, ACT_KEYS_VUP, ACT_KEY_MS_TAP));
TEST_ASSERT_EQUAL_INT(HAL_ERR_STATE, t_actuator_hard_reset(rail));
TEST_ASSERT_EQUAL_INT(HAL_ERR_STATE, t_actuator_edl(rail));
TEST_ASSERT_EQUAL_UINT32(writes1, blade(rail)->n_write);
TEST_ASSERT_EQUAL_HEX8(latch1, latch(rail));
TEST_ASSERT_EQUAL_UINT32(en_writes1, sim_gpio_writes(en)); /* 被拒的硬重启没断电 */
/* 被拒的请求也不许改掉正在跑的那一个:仍然按 10 s 松开,不是 100 ms。 */
run_ms(ACT_KEY_MS_TAP + 1);
TEST_ASSERT_TRUE(t_actuator_busy(rail));
run_ms(ACT_KEY_MS_FORCE - ACT_KEY_MS_TAP - 1);
TEST_ASSERT_FALSE(t_actuator_busy(rail));
TEST_ASSERT_EQUAL_UINT32(1, t_actuator_done(rail)); /* 只跑过一个序列 */
}
/* ⭐ 改 CH1 的键不许动 CH4 的。
R1 里这是「同一条链上的不同位」,位号算错就串路;R2 里这是**两片不同的器件**,
串路的形态变成了「mux 通道选错」或「影子下标传错」——
后果一样是「按了另一台手机的电源键」,而五路电压电流全部正常。
所以断言打在两片假器件**各自的输出锁存器**上,外加「另外三片一个字节没动」。 */
static void test_pressing_ch1_does_not_touch_ch4(void)
{
const int pwr = pca9555_key_bit(PCA9555_KEY_PWR);
const int vup = pca9555_key_bit(PCA9555_KEY_VUP);
uint8_t l0[N];
uint32_t w0[N];
snap_latches(l0);
snap_writes(w0);
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_key(1, ACT_KEYS_PWR, ACT_KEY_MS_ON)); /* 2 s */
TEST_ASSERT_EQUAL_INT(rail_cfg(1)->mux_ch, sim_i2c_mux_ch());
assert_only_this_blade_moved(1, l0, w0);
run_ms(50);
snap_latches(l0);
snap_writes(w0);
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_key(4, ACT_KEYS_VUP, ACT_KEY_MS_TAP)); /* 100 ms */
TEST_ASSERT_EQUAL_INT(rail_cfg(4)->mux_ch, sim_i2c_mux_ch());
assert_only_this_blade_moved(4, l0, w0); /* CH1 的电源键仍按着,没被顺手改掉 */
TEST_ASSERT_EQUAL_INT(1, pin(1, pwr));
TEST_ASSERT_EQUAL_INT(1, pin(4, vup));
run_ms(ACT_KEY_MS_TAP); /* CH4 到点 */
TEST_ASSERT_EQUAL_INT(0, pin(4, vup));
TEST_ASSERT_EQUAL_INT(1, pin(1, pwr)); /* ⭐ CH1 没被顺手清掉 */
TEST_ASSERT_TRUE(t_actuator_busy(1));
TEST_ASSERT_FALSE(t_actuator_busy(4));
run_ms(ACT_KEY_MS_ON); /* CH1 到点 */
for (int r = 1; r <= N; r++) {
TEST_ASSERT_EQUAL_HEX8(0x00, latch(r));
}
TEST_ASSERT_FALSE(t_actuator_busy(1));
}
/* ⭐ R2 新增:**每一次动作发生时,mux 通道选的是这一路。**
五路逐个跑一遍完整按键序列,每一次写都要满足两件事:
① 写完之后当前通道就是 `rail_cfg(rail)->mux_ch`(查表,不算);
② 只有这一片的锁存器和写计数变了,别的四片一个字节没动。
⚠️ 「mux_ch = rail − 1」现在确实成立,所以把它写死也能过 ① ——
② 才是真正拦得住「影子下标传错」的那一条(那时通道是对的,影子记到了别片上,
下一次写会把那一片的键顺手改掉)。 */
static void test_every_action_selects_this_rails_mux_channel(void)
{
for (int rail = 1; rail <= N; rail++) {
uint8_t l0[N];
uint32_t w0[N];
snap_latches(l0);
snap_writes(w0);
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_key(rail, ACT_KEYS_RECOVERY, ACT_KEY_MS_TAP));
TEST_ASSERT_EQUAL_INT(rail_cfg(rail)->mux_ch, sim_i2c_mux_ch());
TEST_ASSERT_EQUAL_UINT32(w0[rail - 1] + 1, blade(rail)->n_write);
assert_only_this_blade_moved(rail, l0, w0);
snap_latches(l0);
snap_writes(w0);
run_ms(ACT_KEY_MS_TAP);
TEST_ASSERT_FALSE(t_actuator_busy(rail));
TEST_ASSERT_EQUAL_INT(rail_cfg(rail)->mux_ch, sim_i2c_mux_ch());
TEST_ASSERT_EQUAL_HEX8(0x00, latch(rail));
assert_only_this_blade_moved(rail, l0, w0);
}
}
/* ==== 验收主项三:EDL 顺序固定 =========================================== */
/* SW-6:`EDL_EN=1` → 等 10 ms → `VBUS_EN=1` → 等枚举 3 s → `EDL_EN=0`。
**先短接再插 USB** 是硬要求。R1 里这两样一个在 595 上、一个是 GPIO,只能比写序号;
R2 里它们是**同一个字节的两位**,所以改成在三个时刻各看一眼 ——
顺序写反的实现,中间那一眼会看到「USB 已经插上、短接还没建立」。 */
static void test_edl_order_is_short_then_vbus_then_release(void)
{
const int rail = 3;
const uint32_t w0 = blade(rail)->n_write;
TEST_ASSERT_EQUAL_INT(0, pin(rail, PCA9555_BIT_VBUS_EN));
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_edl(rail));
TEST_ASSERT_EQUAL_INT(1, pin(rail, PCA9555_BIT_EDL_EN));
TEST_ASSERT_EQUAL_INT(0, pin(rail, PCA9555_BIT_VBUS_EN)); /* USB 还没插 */
TEST_ASSERT_EQUAL_UINT32(w0 + 1, blade(rail)->n_write);
run_ms(ACT_EDL_SHORT_MS - 1);
TEST_ASSERT_EQUAL_INT(0, pin(rail, PCA9555_BIT_VBUS_EN));
run_ms(1);
TEST_ASSERT_EQUAL_INT(1, pin(rail, PCA9555_BIT_VBUS_EN));
TEST_ASSERT_EQUAL_INT(1, pin(rail, PCA9555_BIT_EDL_EN)); /* 还短接着 */
TEST_ASSERT_TRUE(slot_vbus_is_on(rail));
TEST_ASSERT_EQUAL_UINT32(w0 + 2, blade(rail)->n_write);
run_ms(ACT_EDL_ENUM_MS - 1);
TEST_ASSERT_EQUAL_INT(1, pin(rail, PCA9555_BIT_EDL_EN));
run_ms(1);
TEST_ASSERT_EQUAL_INT(0, pin(rail, PCA9555_BIT_EDL_EN)); /* 枚举完才松短接 */
TEST_ASSERT_EQUAL_UINT32(w0 + 3, blade(rail)->n_write);
/* VBUS **留着** —— 手机现在正挂在 9008 上,把它断了等于白跑一趟。 */
TEST_ASSERT_EQUAL_INT(1, pin(rail, PCA9555_BIT_VBUS_EN));
TEST_ASSERT_TRUE(slot_vbus_is_on(rail));
TEST_ASSERT_FALSE(t_actuator_busy(rail));
TEST_ASSERT_EQUAL_INT(ACT_RES_OK, t_actuator_last_result(rail));
/* 一个键都没按过:EDL 只该动 bit3 和 bit4。 */
TEST_ASSERT_EQUAL_HEX8(0x00, (uint8_t)(latch(rail) & pca9555_key_mask()));
}
/* 五路逐个跑 EDL:位号五片相同(`PCA9555_BIT_EDL_EN`),但**器件是五片不同的**,
所以每一路都要落在自己那一片上,别的四片不许出现 `EDL_EN`。 */
static void test_edl_lands_on_its_own_blade_for_every_rail(void)
{
for (int rail = 1; rail <= N; rail++) {
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_edl(rail));
TEST_ASSERT_EQUAL_INT(rail_cfg(rail)->mux_ch, sim_i2c_mux_ch());
for (int r = 1; r <= N; r++) {
TEST_ASSERT_EQUAL_INT(r == rail ? 1 : 0, pin(r, PCA9555_BIT_EDL_EN));
}
run_ms(ACT_EDL_SHORT_MS + ACT_EDL_ENUM_MS);
TEST_ASSERT_FALSE(t_actuator_busy(rail));
TEST_ASSERT_EQUAL_INT(0, pin(rail, PCA9555_BIT_EDL_EN));
TEST_ASSERT_EQUAL_INT(1, pin(rail, PCA9555_BIT_VBUS_EN));
}
/* 跑完五路:五片的 VBUS 都开着、EDL 都松了,一个键都没按着。 */
for (int r = 1; r <= N; r++) {
TEST_ASSERT_EQUAL_HEX8((uint8_t)(1u << PCA9555_BIT_VBUS_EN), latch(r));
}
}
/* ==== 收尾:按下去的东西一定要松开 ======================================= */
/* ⭐ I²C 锁被别人占着时,松键落不下去。**不许放弃** ——
放弃 = 电源键留在按下状态,手机再也起不来,而遥测上五路电压电流一切正常。
R1 靠占住 595 那把锁制造写失败,R2 里只剩一把锁,占的就是巡检也在用的那一把,
所以这条路径在真板上**每天都会走到**(不是异常路径)。 */
static void test_cleanup_retries_until_the_key_is_really_released(void)
{
const int rail = 1;
const int bit = pca9555_key_bit(PCA9555_KEY_PWR);
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_key(rail, ACT_KEYS_PWR, ACT_KEY_MS_TAP));
other_task_takes_the_bus();
run_ms(ACT_KEY_MS_TAP);
TEST_ASSERT_EQUAL_INT(ACT_ST_CLEANUP, t_actuator_state(rail));
TEST_ASSERT_TRUE(t_actuator_busy(rail));
TEST_ASSERT_EQUAL_INT(1, pin(rail, bit)); /* 键还按着 */
for (int i = 0; i < 5; i++) {
(void)t_actuator_step();
}
TEST_ASSERT_TRUE(t_actuator_cleanup_retries(rail) > 0);
TEST_ASSERT_EQUAL_INT(1, pin(rail, bit)); /* 重试期间键仍然按着,没被当成已松开 */
/* 收尾期间这一路照样算 busy,新请求进不来。 */
TEST_ASSERT_EQUAL_INT(HAL_ERR_STATE, t_actuator_key(rail, ACT_KEYS_PWR, ACT_KEY_MS_TAP));
other_task_frees_the_bus();
(void)t_actuator_step();
TEST_ASSERT_EQUAL_INT(0, pin(rail, bit)); /* 锁一放开就落地 */
TEST_ASSERT_FALSE(t_actuator_busy(rail));
TEST_ASSERT_EQUAL_HEX8(0x00, latch(rail));
}
/* 收尾重试**永远不会自己放弃**:把锁占住不放,跑上几百拍,
这一路必须一直是 CLEANUP、一直 busy、键一直按着。
写成「重试 N 次就算了」的实现在这里会悄悄回到 IDLE —— 那正是最坏的失败。 */
static void test_cleanup_never_gives_up_no_matter_how_long_the_bus_is_busy(void)
{
const int rail = 2;
const int bit = pca9555_key_bit(PCA9555_KEY_PWR);
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_key(rail, ACT_KEYS_PWR, ACT_KEY_MS_TAP));
other_task_takes_the_bus();
run_ms(ACT_KEY_MS_TAP);
for (int i = 0; i < 500; i++) {
(void)t_actuator_step();
TEST_ASSERT_EQUAL_INT(ACT_ST_CLEANUP, t_actuator_state(rail));
TEST_ASSERT_TRUE(t_actuator_busy(rail));
TEST_ASSERT_EQUAL_INT(1, pin(rail, bit));
}
TEST_ASSERT_TRUE(t_actuator_cleanup_retries(rail) >= 500);
other_task_frees_the_bus();
(void)t_actuator_step();
TEST_ASSERT_EQUAL_INT(0, pin(rail, bit));
TEST_ASSERT_FALSE(t_actuator_busy(rail));
}
/* 按下那一步就失败(I²C 锁被别人占着)。请求返回错误码,但**不能就这么算了** ——
事务有可能发出去一半(驱动的影子不更新,可器件里那个字节是未知的),
直接回 IDLE 等于把一个也许已经按下的键留在那儿,而没有任何人会来松开它。
所以失败的按下照样要走收尾。 */
static void test_a_failed_press_still_goes_through_cleanup(void)
{
const int rail = 4;
const int bit = pca9555_key_bit(PCA9555_KEY_PWR);
other_task_takes_the_bus();
TEST_ASSERT_EQUAL_INT(HAL_ERR_TIMEOUT,
t_actuator_key(rail, ACT_KEYS_PWR, ACT_KEY_MS_TAP));
TEST_ASSERT_EQUAL_INT(ACT_ST_CLEANUP, t_actuator_state(rail));
TEST_ASSERT_EQUAL_INT(ACT_RES_IO, t_actuator_last_result(rail));
TEST_ASSERT_EQUAL_INT(0, pin(rail, bit)); /* 这一次键根本没按下去 */
other_task_frees_the_bus();
(void)t_actuator_step();
TEST_ASSERT_FALSE(t_actuator_busy(rail));
TEST_ASSERT_EQUAL_INT(0, pin(rail, bit));
TEST_ASSERT_EQUAL_HEX8(0x00, latch(rail)); /* 收尾真的把那几位显式清了一遍 */
}
/* `t_actuator_abort()`:立刻收尾,键当场松开,但**不动 `EFUSE_EN` / `VBUS_EN`** ——
把电再关一次或再开一次都是自作主张(那是 A10 的判据和上位机命令的事)。 */
static void test_abort_releases_the_key_and_leaves_the_pins_alone(void)
{
const int rail = 2;
const int bit = pca9555_key_bit(PCA9555_KEY_PWR);
const int en = rail_cfg(rail)->efuse_en;
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_key(rail, ACT_KEYS_PWR, ACT_KEY_MS_FORCE));
run_ms(500);
const uint32_t en_writes = sim_gpio_writes(en);
t_actuator_abort(rail);
TEST_ASSERT_EQUAL_INT(0, pin(rail, bit));
TEST_ASSERT_FALSE(t_actuator_busy(rail));
TEST_ASSERT_EQUAL_INT(ACT_RES_ABORTED, t_actuator_last_result(rail));
TEST_ASSERT_EQUAL_UINT32(en_writes, sim_gpio_writes(en));
}
/* 硬重启走到一半被打断:序列停下,但已经断掉的电**不会**被 abort 悄悄开回来。 */
static void test_abort_during_hard_reset_does_not_restore_power(void)
{
const int rail = 5;
const int en = rail_cfg(rail)->efuse_en;
TEST_ASSERT_EQUAL_INT(HAL_OK, rail_efuse_enable(rail, true));
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail));
run_ms(1000);
t_actuator_abort(rail);
TEST_ASSERT_FALSE(t_actuator_busy(rail));
TEST_ASSERT_EQUAL_INT(ACT_RES_ABORTED, t_actuator_last_result(rail));
TEST_ASSERT_EQUAL_INT(0, sim_gpio_level(en)); /* 还是关着的,等上层拿主意 */
}
/* 开机键按着的时候被打断(过压关路走的就是这条):键当场松开,电不动。
键留在按下状态的现象是手机再也起不来,而遥测上一切正常。 */
static void test_abort_during_boot_press_releases_the_key(void)
{
const int rail = 2;
const int en = rail_cfg(rail)->efuse_en;
s_pg_val[rail - 1] = true;
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail));
run_ms(ACT_HR_OFF_MS + ACT_HR_RAMP_MS + 1000);
TEST_ASSERT_EQUAL_INT(ACT_ST_HR_BOOT, t_actuator_state(rail));
TEST_ASSERT_EQUAL_INT(1, key_pin(rail, PCA9555_KEY_PWR));
t_actuator_abort(rail);
TEST_ASSERT_FALSE(t_actuator_busy(rail));
TEST_ASSERT_EQUAL_INT(ACT_RES_ABORTED, t_actuator_last_result(rail));
TEST_ASSERT_EQUAL_INT(0, key_pin(rail, PCA9555_KEY_PWR));
TEST_ASSERT_EQUAL_INT(1, sim_gpio_level(en));
}
/* EDL 跑到「已插 USB」那一段被打断:`EDL_EN` 立刻松(那是本序列按下去的),
**`VBUS_EN` 一动不动** —— R2 里它和按键在同一个字节里,一不小心就会被
收尾那次读改写顺手清掉,而现象是「手机莫名其妙从 9008 掉线」。 */
static void test_abort_during_edl_releases_the_short_but_keeps_vbus(void)
{
const int rail = 1;
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_edl(rail));
run_ms(ACT_EDL_SHORT_MS + 100);
TEST_ASSERT_EQUAL_INT(ACT_ST_EDL_ENUM, t_actuator_state(rail));
TEST_ASSERT_EQUAL_INT(1, pin(rail, PCA9555_BIT_VBUS_EN));
t_actuator_abort(rail);
TEST_ASSERT_FALSE(t_actuator_busy(rail));
TEST_ASSERT_EQUAL_INT(ACT_RES_ABORTED, t_actuator_last_result(rail));
TEST_ASSERT_EQUAL_INT(0, pin(rail, PCA9555_BIT_EDL_EN));
TEST_ASSERT_EQUAL_INT(1, pin(rail, PCA9555_BIT_VBUS_EN)); /* ⭐ USB 还插着 */
TEST_ASSERT_TRUE(slot_vbus_is_on(rail));
}
/* ==== ⭐ R2 新增:执行器与巡检争用同一把锁 =============================== */
/* R1 里两把锁永远不会嵌套,所以这一层和巡检井水不犯河水。
R2 只剩一把锁,按键 / EDL / VBUS 全在上面 ⇒ 两个任务开始抢。
抢锁写错了的现象**不是崩**,是慢性的:巡检每圈都跳过(SR-5 判据停摆),
或者某一路的键永远松不开(手机再也起不来)—— 两样都是遥测上一切正常。
这里交替叫 `t_safety_step()` 与 `t_actuator_step()` 跑一千多圈,
并且每隔 13 圈让第三个任务把总线占住一拍(制造真实的争用),断言三件事:
① 巡检 `skipped` **有界**(只在被占住的那些圈里跳,不会雪崩);
② 键最终一定全部松开;
③ 没有哪一路卡在 CLEANUP 出不来。 */
static void test_actuator_and_safety_share_one_lock_without_starving(void)
{
/* 巡检要读的六片 INA226:五路各一片(同址 0x40,靠通道区分)+ 输入侧在主干。 */
for (int rail = 1; rail <= N; rail++) {
sim_i2c_dev_t *d = sim_i2c_add_ch(rail_cfg(rail)->mux_ch, rail_cfg(rail)->ina_addr, 2);
d->regs[INA226_REG_BUS_V] = 3120; /* 3.9 V ÷ 1.25 mV */
d->regs[INA226_REG_CURRENT] = (uint16_t)(rail * 100);
}
sim_i2c_add(INA226_ADDR_INPUT, 2);
safety_snap_t snap;
t_safety_init(&snap);
const int rounds = 1300;
uint32_t busy_rounds = 0;
uint32_t issued = 0;
for (int i = 0; i < rounds; i++) {
const bool bus_busy = (i % 13 == 0);
if (bus_busy) {
other_task_takes_the_bus(); /* 第三个任务(比如 A12 的命令处理器)占住总线 */
busy_rounds++;
}
(void)t_safety_step(&snap);
(void)t_actuator_step();
if (bus_busy) {
other_task_frees_the_bus();
}
/* 不断有新的按键请求进来 —— 五路轮着来,包括总线正忙的那些圈。 */
if (i % 7 == 0) {
const int rail = (i / 7) % N + 1;
if (t_actuator_key(rail, ACT_KEYS_PWR, ACT_KEY_MS_TAP) == HAL_OK) {
issued++;
}
}
sim_time_advance_ms(20);
}
/* 收场:不再下新请求,总线空着,所有序列必须走完。 */
for (int i = 0; i < 300; i++) {
(void)t_safety_step(&snap);
(void)t_actuator_step();
sim_time_advance_ms(20);
}
TEST_ASSERT_TRUE(issued > 50); /* 争用之下照样接得下活,不是被拒到底 */
for (int rail = 1; rail <= N; rail++) {
TEST_ASSERT_FALSE(t_actuator_busy(rail)); /* ③ 没卡在 CLEANUP */
TEST_ASSERT_EQUAL_INT(ACT_ST_IDLE, t_actuator_state(rail));
TEST_ASSERT_EQUAL_HEX8(0x00, (uint8_t)(latch(rail) & pca9555_key_mask())); /* ② */
}
/* ① `skipped` 有界:只可能落在被占住的那些圈里。真跳过了才算测到了争用。 */
TEST_ASSERT_TRUE(snap.skipped > 0);
TEST_ASSERT_TRUE(snap.skipped <= busy_rounds);
/* 抢了一千多圈,加锁纪律一次都没破,也没出现「两片一起应答」。 */
TEST_ASSERT_EQUAL_UINT32(0, sim_lock_violations(HAL_LOCK_I2C));
TEST_ASSERT_EQUAL_UINT32(0, sim_i2c_ambiguous());
TEST_ASSERT_EQUAL_UINT32(0, sim_pca9555_gnd_driven());
printf(" 争用 %d 圈:巡检跳过 %u 圈(总线被占 %u 圈),下发按键 %u 次\n", rounds,
(unsigned)snap.skipped, (unsigned)busy_rounds, (unsigned)issued);
}
/* ==== 参数与常量 ========================================================== */
static void test_bad_arguments_are_rejected(void)
{
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, t_actuator_hard_reset(0));
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, t_actuator_hard_reset(N + 1));
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, t_actuator_edl(0));
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, t_actuator_key(0, ACT_KEYS_PWR, 100));
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, t_actuator_key(1, 0, 100)); /* 一个键都没选 */
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, t_actuator_key(1, 1u << 5, 100)); /* 不存在的键 */
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, t_actuator_key(1, 1u << PCA9555_BIT_EDL_EN, 100));
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, t_actuator_key(1, ACT_KEYS_PWR, 0)); /* 按 0 ms */
/* 上限拦的是调用方的手误:传一个 10 分钟进来等于把电源键按死。 */
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG,
t_actuator_key(1, ACT_KEYS_PWR, ACT_KEY_MAX_MS + 1));
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_key(1, ACT_KEYS_PWR, ACT_KEY_MAX_MS));
/* 被拒的请求一律不留痕迹:别的四片一个字节都没写过。 */
TEST_ASSERT_FALSE(t_actuator_busy(2));
for (int r = 2; r <= N; r++) {
TEST_ASSERT_EQUAL_HEX8(0x00, latch(r));
}
TEST_ASSERT_EQUAL_UINT32(0, sim_lock_violations(HAL_LOCK_I2C));
}
/* 时间常量与文档逐格对账。改了这里就是改了需求,得先去改文档。 */
static void test_timings_match_the_documents(void)
{
TEST_ASSERT_EQUAL_UINT32(17, ACT_HR_RAMP_MS); /* tON 5.1 + 斜坡 11.5,§7.4 */
TEST_ASSERT_EQUAL_UINT32(3000, ACT_HR_OFF_MIN_MS); /* §7.4 的 3~5 s 窗口 */
TEST_ASSERT_EQUAL_UINT32(5000, ACT_HR_OFF_MAX_MS);
TEST_ASSERT_TRUE(ACT_HR_OFF_MS >= ACT_HR_OFF_MIN_MS && ACT_HR_OFF_MS <= ACT_HR_OFF_MAX_MS);
TEST_ASSERT_EQUAL_UINT32(500, ACT_HR_PG_TIMEOUT_MS);
/* 轮询间隔必须远小于超时,否则「等到 PG 就结束」退化成「一律等满 500 ms」。 */
TEST_ASSERT_TRUE(ACT_HR_PG_POLL_MS > 0 && ACT_HR_PG_POLL_MS * 10 <= ACT_HR_PG_TIMEOUT_MS);
TEST_ASSERT_EQUAL_UINT32(100, ACT_KEY_MS_TAP); /* SW-5 三档 */
TEST_ASSERT_EQUAL_UINT32(5000, ACT_KEY_MS_ON); /* 小米8 实测 2 s 不够(t_actuator.h) */
TEST_ASSERT_TRUE(ACT_KEY_MS_ON < ACT_KEY_MS_FORCE); /* 开机那一下不能按成强制重启 */
TEST_ASSERT_EQUAL_UINT32(ACT_KEY_MS_ON, ACT_HR_BOOT_KEY_MS);
TEST_ASSERT_EQUAL_UINT32(10000, ACT_KEY_MS_FORCE);
TEST_ASSERT_TRUE(ACT_KEY_MS_FORCE <= ACT_KEY_MAX_MS); /* 最长那一档不能被自己的上限拦掉 */
TEST_ASSERT_EQUAL_UINT32(10, ACT_EDL_SHORT_MS); /* SW-6:先短接再插 USB */
TEST_ASSERT_EQUAL_UINT32(3000, ACT_EDL_ENUM_MS);
/* ⭐ R2:等锁的超时就是 `slot` 那一个,不许在这里另配一个数 ——
两个数分家之后,「执行器等 20 ms、巡检等 5 ms」这套优先级就悄悄失效了。 */
TEST_ASSERT_EQUAL_UINT32(SLOT_LOCK_TIMEOUT_MS, ACT_LOCK_TIMEOUT_MS);
TEST_ASSERT_EQUAL_UINT32(20, ACT_LOCK_TIMEOUT_MS);
/* 逻辑键位就是 PCA9555 那三个键的下标,组合键是它们的并。 */
TEST_ASSERT_EQUAL_UINT32(1u << PCA9555_KEY_PWR, (uint32_t)ACT_KEYS_PWR);
TEST_ASSERT_EQUAL_UINT32(1u << PCA9555_KEY_VUP, (uint32_t)ACT_KEYS_VUP);
TEST_ASSERT_EQUAL_UINT32(1u << PCA9555_KEY_VDN, (uint32_t)ACT_KEYS_VDN);
TEST_ASSERT_EQUAL_UINT32(ACT_KEYS_VDN | ACT_KEYS_PWR, (uint32_t)ACT_KEYS_FASTBOOT);
TEST_ASSERT_EQUAL_UINT32(ACT_KEYS_VUP | ACT_KEYS_PWR, (uint32_t)ACT_KEYS_RECOVERY);
TEST_ASSERT_TRUE((uint32_t)ACT_KEYS_FASTBOOT != (uint32_t)ACT_KEYS_RECOVERY);
/* 三个键的位号一律查表得到,而且必须落在 Port0 那五个输出位里。 */
TEST_ASSERT_EQUAL_HEX8(0x07, pca9555_key_mask());
TEST_ASSERT_EQUAL_HEX8(0x00, (uint8_t)(pca9555_key_mask() & ~PCA9555_P0_OUT_MASK));
}
/* 加锁纪律那条检查本身不能是句空话:不拿锁直接写一片 PCA9555,违规必须 +1。
(没有这条反向测试,上面那些「violations == 0」在检查失灵时会永远绿。) */
static void test_the_lock_discipline_check_actually_fires(void)
{
TEST_ASSERT_EQUAL_UINT32(0, sim_lock_violations(HAL_LOCK_I2C));
const uint8_t wr[2] = { PCA9555_REG_OUTPUT0, 0x00 }; /* 值本身无害,越权的是「没拿锁」 */
(void)hal_i2c_write(PCA9555_ADDR, wr, sizeof(wr));
TEST_ASSERT_TRUE(sim_lock_violations(HAL_LOCK_I2C) > 0);
}
/* 空闲时 `t_actuator_step()` 什么也不做,也不该去动锁或任何一片器件 ——
R2 里「顺手刷一次影子」的代价是每一拍都从巡检手里抢一次锁。 */
static void test_idle_step_is_free(void)
{
const uint32_t takes = sim_lock_takes(HAL_LOCK_I2C);
uint8_t l0[N];
uint32_t w0[N];
snap_latches(l0);
snap_writes(w0);
for (int i = 0; i < 100; i++) {
TEST_ASSERT_EQUAL_UINT32(ACT_IDLE_WAIT_MS, t_actuator_step());
sim_time_advance_ms(ACT_IDLE_WAIT_MS);
}
TEST_ASSERT_EQUAL_UINT32(takes, sim_lock_takes(HAL_LOCK_I2C));
assert_only_this_blade_moved(0, l0, w0);
}
static void test_format_prints_every_rail(void)
{
char buf[160];
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_key(2, ACT_KEYS_PWR, ACT_KEY_MS_ON));
const int n = t_actuator_format(buf, sizeof(buf));
TEST_ASSERT_TRUE(n > 0 && n < (int)sizeof(buf));
TEST_ASSERT_NOT_NULL(strstr(buf, "ch2=key-hold"));
TEST_ASSERT_NOT_NULL(strstr(buf, "ch5=idle"));
printf(" %s\n", buf);
/* 缓冲太小也不许越界。 */
char small[12];
memset(small, 0x7F, sizeof(small));
const int m = t_actuator_format(small, sizeof(small));
TEST_ASSERT_TRUE(m < (int)sizeof(small));
TEST_ASSERT_EQUAL_CHAR('\0', small[sizeof(small) - 1]);
}
/* ==== 0.1.2:硬重启的开机校验与重按 =======================================
2026-09-28 板 B 第 1 路:电源键弹片压刀片铜盘接触不良,7 次硬重启只开起来 1 次,
失败时电流一直 1~2 mA(屏都不亮)。假手机照实测建模:
· 第 `boots_on` 次按满 5 s 松手后开机 ⇒ 电流 ~210 mA(logo 段);
· 其余情况 1.5 mA(关机态);
· `half_on`:第一次按下去 PMIC 醒了、SoC 没接住 —— 松手后 415 mA 撑 2.5 s 再掉回去
(照「按 2 s」那次的实测形态),校验不能被它骗过去。 */
static bool s_ua_readable;
static int s_phone_boots_on; /* 第几次按能开;0 = 永远开不了(弹片没压上) */
static bool s_phone_half_on;
static bool s_phone_on;
static uint32_t s_phone_half_until;
static int s_edges; /* 看到的「按下」次数 */
static int s_last_pin;
static uint32_t s_press_t, s_release_t, s_min_gap;
static int s_boot_calls;
static bool s_boot_booted;
static uint8_t s_boot_presses;
static bool fake_ua(void *ctx, int rail, int32_t *ua)
{
(void)ctx;
(void)rail;
if (!s_ua_readable) {
return false;
}
if (s_phone_on) {
*ua = 210000;
} else if ((int32_t)(s_phone_half_until - hal_time_now_ms()) > 0) {
*ua = 415000;
} else {
*ua = 1500;
}
return true;
}
static void fake_boot_done(void *ctx, int rail, bool booted, uint8_t presses, int32_t ua)
{
(void)ctx;
(void)rail;
(void)ua;
s_boot_calls++;
s_boot_booted = booted;
s_boot_presses = presses;
}
static void phone_up(int boots_on, bool half_on)
{
s_ua_readable = true;
s_phone_boots_on = boots_on;
s_phone_half_on = half_on;
s_phone_on = false;
s_phone_half_until = hal_time_now_ms();
s_edges = 0;
s_last_pin = 0;
s_press_t = s_release_t = 0;
s_min_gap = UINT32_MAX;
s_boot_calls = 0;
t_actuator_set_boot_check(fake_ua, fake_boot_done, NULL);
}
/* 看电源键那根栅极的边沿,推假手机的状态。 */
static void phone_watch(int rail)
{
const int p = key_pin(rail, PCA9555_KEY_PWR);
const uint32_t now = hal_time_now_ms();
if (p && !s_last_pin) {
s_edges++;
if (s_release_t != 0 && now - s_release_t < s_min_gap) {
s_min_gap = now - s_release_t;
}
s_press_t = now;
}
if (!p && s_last_pin) {
s_release_t = now;
if (now - s_press_t >= ACT_KEY_MS_ON) {
if (s_edges == s_phone_boots_on) {
s_phone_on = true;
} else if (s_phone_half_on && s_edges == 1) {
s_phone_half_until = now + 2500;
}
}
}
s_last_pin = p;
}
/* `run_ms()` 加上每一步之后看一眼按键。 */
static void run_phone_ms(int rail, uint32_t ms)
{
const uint32_t target = hal_time_now_ms() + ms;
for (;;) {
uint32_t w = t_actuator_step();
phone_watch(rail);
const uint32_t now = hal_time_now_ms();
if ((int32_t)(now - target) >= 0) {
return;
}
const uint32_t left = target - now;
if (w == 0 || w > left) {
w = left;
}
sim_time_advance_ms(w);
}
}
/* 一轮按键 + 校验最长多久:按 5 s + 松键后判到窗口末尾。 */
#define ONE_TRY_MS (ACT_HR_BOOT_KEY_MS + ACT_HR_VERIFY_DELAY_MS + ACT_HR_VERIFY_WINDOW_MS + 50)
#define HR_TO_PRESS_MS (ACT_HR_OFF_MS + ACT_HR_RAMP_MS + ACT_HR_PG_POLL_MS)
static void test_boot_check_first_press_boots(void)
{
const int rail = 1;
s_pg_val[rail - 1] = true;
phone_up(1, false);
const uint32_t t0 = hal_time_now_ms();
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail));
run_phone_ms(rail, HR_TO_PRESS_MS + ACT_HR_BOOT_KEY_MS + 10);
TEST_ASSERT_EQUAL_INT(ACT_ST_HR_VERIFY, t_actuator_state(rail));
TEST_ASSERT_EQUAL_INT(0, key_pin(rail, PCA9555_KEY_PWR)); /* 校验期间键是松开的 */
run_phone_ms(rail, ONE_TRY_MS);
TEST_ASSERT_FALSE(t_actuator_busy(rail));
TEST_ASSERT_EQUAL_INT(ACT_RES_OK, t_actuator_last_result(rail));
TEST_ASSERT_EQUAL_INT(1, s_edges);
TEST_ASSERT_EQUAL_INT(1, s_boot_calls);
TEST_ASSERT_TRUE(s_boot_booted);
TEST_ASSERT_EQUAL_UINT8(1, s_boot_presses);
/* 开起来就收工:不把整个窗口等满。 */
TEST_ASSERT_TRUE(hal_time_now_ms() - t0 >= HR_TO_PRESS_MS + ACT_HR_BOOT_KEY_MS + ACT_HR_VERIFY_DELAY_MS);
TEST_ASSERT_EQUAL_UINT32(1, t_actuator_done(rail));
TEST_ASSERT_EQUAL_UINT32(0, sim_lock_violations(HAL_LOCK_I2C));
}
/* ⭐ 这次的主症状:第一下没压上,第二下开了。 */
static void test_boot_check_presses_again_when_the_phone_stays_off(void)
{
const int rail = 3;
s_pg_val[rail - 1] = true;
phone_up(2, false);
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail));
run_phone_ms(rail, HR_TO_PRESS_MS + 2 * ONE_TRY_MS + 1000);
TEST_ASSERT_FALSE(t_actuator_busy(rail));
TEST_ASSERT_EQUAL_INT(ACT_RES_OK, t_actuator_last_result(rail));
TEST_ASSERT_EQUAL_INT(2, s_edges);
TEST_ASSERT_TRUE(s_boot_booted);
TEST_ASSERT_EQUAL_UINT8(2, s_boot_presses);
TEST_ASSERT_EQUAL_INT(1, s_boot_calls); /* 只在有结论时报一次 */
/* 两次按之间至少隔开整段判定延迟 —— 连着按等于一次 10 s 长按(强制重启档)。 */
TEST_ASSERT_TRUE(s_min_gap >= ACT_HR_VERIFY_DELAY_MS);
TEST_ASSERT_EQUAL_INT(0, key_pin(rail, PCA9555_KEY_PWR));
}
/* 按满三次都不开:记 NO_BOOT,**电继续开着**,键松开,不再按第四下。 */
static void test_boot_check_gives_up_after_three_presses_and_keeps_power(void)
{
const int rail = 2;
s_pg_val[rail - 1] = true;
phone_up(0, false);
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail));
run_phone_ms(rail, HR_TO_PRESS_MS + ACT_HR_BOOT_TRIES * ONE_TRY_MS + 1000);
TEST_ASSERT_FALSE(t_actuator_busy(rail));
TEST_ASSERT_EQUAL_INT(ACT_RES_NO_BOOT, t_actuator_last_result(rail));
TEST_ASSERT_EQUAL_INT(ACT_HR_BOOT_TRIES, s_edges);
TEST_ASSERT_EQUAL_INT(1, s_boot_calls);
TEST_ASSERT_FALSE(s_boot_booted);
TEST_ASSERT_EQUAL_UINT8(ACT_HR_BOOT_TRIES, s_boot_presses);
TEST_ASSERT_EQUAL_INT(1, sim_gpio_level(rail_cfg(rail)->efuse_en));
TEST_ASSERT_EQUAL_INT(0, key_pin(rail, PCA9555_KEY_PWR));
TEST_ASSERT_EQUAL_STRING("no-boot", t_actuator_result_str(ACT_RES_NO_BOOT));
run_phone_ms(rail, 30000); /* 收工之后再也不按 */
TEST_ASSERT_EQUAL_INT(ACT_HR_BOOT_TRIES, s_edges);
}
/* 读不到电流 ⇒ 判不了 ⇒ **不重按**,也不报结论。 */
static void test_boot_check_does_not_press_again_when_current_is_unreadable(void)
{
const int rail = 4;
s_pg_val[rail - 1] = true;
phone_up(0, false);
s_ua_readable = false;
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail));
run_phone_ms(rail, HR_TO_PRESS_MS + ACT_HR_BOOT_TRIES * ONE_TRY_MS + 1000);
TEST_ASSERT_FALSE(t_actuator_busy(rail));
TEST_ASSERT_EQUAL_INT(ACT_RES_OK, t_actuator_last_result(rail));
TEST_ASSERT_EQUAL_INT(1, s_edges);
TEST_ASSERT_EQUAL_INT(0, s_boot_calls);
}
/* 「PMIC 醒了、SoC 没接住」:松手后先 415 mA 再掉回去。校验必须等它掉完再看,
判成「开了」就不会有第二下。 */
static void test_boot_check_is_not_fooled_by_a_pmic_that_wakes_and_drops(void)
{
const int rail = 5;
s_pg_val[rail - 1] = true;
phone_up(2, true);
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail));
run_phone_ms(rail, HR_TO_PRESS_MS + 2 * ONE_TRY_MS + 1000);
TEST_ASSERT_EQUAL_INT(ACT_RES_OK, t_actuator_last_result(rail));
TEST_ASSERT_EQUAL_INT(2, s_edges);
TEST_ASSERT_EQUAL_UINT8(2, s_boot_presses);
}
/* 校验期间被打断(比如判据要关这一路):收工、不再按。 */
static void test_abort_during_boot_check_stops_pressing(void)
{
const int rail = 1;
s_pg_val[rail - 1] = true;
phone_up(0, false);
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail));
run_phone_ms(rail, HR_TO_PRESS_MS + ACT_HR_BOOT_KEY_MS + 500);
TEST_ASSERT_EQUAL_INT(ACT_ST_HR_VERIFY, t_actuator_state(rail));
t_actuator_abort(rail);
TEST_ASSERT_FALSE(t_actuator_busy(rail));
TEST_ASSERT_EQUAL_INT(ACT_RES_ABORTED, t_actuator_last_result(rail));
run_phone_ms(rail, 3 * ONE_TRY_MS);
TEST_ASSERT_EQUAL_INT(1, s_edges);
TEST_ASSERT_EQUAL_INT(0, s_boot_calls);
}
/* 单独的按键命令不校验:那是人按的,按什么、按多久是人的意图。 */
static void test_plain_key_command_is_never_repeated(void)
{
const int rail = 2;
phone_up(0, false);
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_key(rail, ACT_KEYS_PWR, ACT_KEY_MS_ON));
run_phone_ms(rail, ACT_KEY_MS_ON + 3 * ONE_TRY_MS);
TEST_ASSERT_EQUAL_INT(1, s_edges);
TEST_ASSERT_EQUAL_INT(ACT_RES_OK, t_actuator_last_result(rail));
TEST_ASSERT_EQUAL_INT(0, s_boot_calls);
}
int main(void)
{
UNITY_BEGIN();
RUN_TEST(test_hard_reset_walks_the_full_power_up_sequence);
RUN_TEST(test_hard_reset_touches_only_its_own_en_pin);
RUN_TEST(test_no_pg_leaves_the_rail_powered);
RUN_TEST(test_pg_stuck_low_also_leaves_the_rail_powered);
RUN_TEST(test_late_pg_is_still_accepted);
RUN_TEST(test_the_task_loop_only_sleeps_what_step_asks_for);
RUN_TEST(test_hard_reset_releases_a_stuck_key_before_cutting_power);
RUN_TEST(test_the_defensive_release_sends_nothing_when_the_shadow_is_clean);
RUN_TEST(test_key_tap_presses_the_table_bit_and_releases_it);
RUN_TEST(test_combo_keys_land_in_exactly_one_write);
RUN_TEST(test_recovery_and_fastboot_are_different_key_sets);
RUN_TEST(test_the_key_hold_never_holds_the_i2c_lock);
RUN_TEST(test_a_running_sequence_rejects_every_new_request);
RUN_TEST(test_pressing_ch1_does_not_touch_ch4);
RUN_TEST(test_every_action_selects_this_rails_mux_channel);
RUN_TEST(test_edl_order_is_short_then_vbus_then_release);
RUN_TEST(test_edl_lands_on_its_own_blade_for_every_rail);
RUN_TEST(test_cleanup_retries_until_the_key_is_really_released);
RUN_TEST(test_cleanup_never_gives_up_no_matter_how_long_the_bus_is_busy);
RUN_TEST(test_a_failed_press_still_goes_through_cleanup);
RUN_TEST(test_abort_releases_the_key_and_leaves_the_pins_alone);
RUN_TEST(test_abort_during_hard_reset_does_not_restore_power);
RUN_TEST(test_abort_during_boot_press_releases_the_key);
RUN_TEST(test_abort_during_edl_releases_the_short_but_keeps_vbus);
RUN_TEST(test_actuator_and_safety_share_one_lock_without_starving);
RUN_TEST(test_boot_check_first_press_boots);
RUN_TEST(test_boot_check_presses_again_when_the_phone_stays_off);
RUN_TEST(test_boot_check_gives_up_after_three_presses_and_keeps_power);
RUN_TEST(test_boot_check_does_not_press_again_when_current_is_unreadable);
RUN_TEST(test_boot_check_is_not_fooled_by_a_pmic_that_wakes_and_drops);
RUN_TEST(test_abort_during_boot_check_stops_pressing);
RUN_TEST(test_plain_key_command_is_never_repeated);
RUN_TEST(test_bad_arguments_are_rejected);
RUN_TEST(test_timings_match_the_documents);
RUN_TEST(test_the_lock_discipline_check_actually_fires);
RUN_TEST(test_idle_step_is_free);
RUN_TEST(test_format_prints_every_rail);
return UNITY_END();
}