A9:三个执行器状态机 + 互斥
需求 §10.6 的三个序列,每路一份状态机,全部以「收尾」收口:
硬重启 EFUSE_EN↓ → 4 s(§7.4 的 3~5 s 窗口)→ EFUSE_EN↑ → 17 ms
(tON 5.1 + 缓启动斜坡 11.5)→ 等 PG
按键 按下 → 保持 T(100 ms / 2 s / 10 s)→ 松开,组合键一帧发完
EDL EDL_EN=1 → VBUS_EN=1 → 等枚举 → EDL_EN=0(SW-6 的顺序)
一个函数一步,不写 while(1):t_actuator_step() 只看时钟、推状态、返回「下次
最晚多久再来」,任务体在 app_main.c 里还是三行。放电由硬件联锁做(SR-7),
本文件里没有任何放电接口。
四条关键判断:
· 按下去的东西一定要松开 —— 正常结束 / 超时 / 出错 / abort 走同一条收尾路径,
写不进 595 就一直重试,不设放弃上限(放弃 = 电源键永久按住,而遥测正常);
· 等不到 PG 不许再断电(§10.10:PCA9555 不应答不关输出);
· PG 由 t_safety 的快照喂进来,执行器不碰 I²C ⇒ 两把锁永远不会嵌套;
· 不可重入直接拒(HAL_ERR_STATE),被拒的请求一个字节都不发。
23 个用例;21 条注错逐个跑过全部变红。其中一条第一遍没抓住 ——「等 PG 时一觉
睡满 500 ms」:测试自己掌握节奏就测不到 step 返回的睡眠时长,补了一个完全听
step 的任务循环测试。
Co-Authored-By: Claude Opus 5 <[email protected]>
这个提交包含在:
@@ -0,0 +1,670 @@
|
||||
/* A9:三个执行器状态机 + 互斥(需求 §10.6)。计划里这一包的验收原话是
|
||||
「场景测试:硬重启走完 17 ms 上电时序;按键序列不重入;EDL 顺序固定」。
|
||||
|
||||
⚠️ 这三条都**不是**「函数返回了 HAL_OK」能证明的东西,所以本文件里的断言
|
||||
一律打在**线上真实发生的事**上:
|
||||
· 24 位链看 `sim_sr595_*`(RCLK 沿的个数、栅极上的电平),不看驱动自己的影子
|
||||
—— 对着影子断言等于自己证明自己,「分两次写把组合键拆开了」影子看不出来;
|
||||
· 两根 EN 线看 `sim_gpio_level()` 与**全局写序号** `sim_gpio_write_seq()`
|
||||
—— 只看终值的话,「先插 USB 再短接」和「先短接再插 USB」长得一模一样。
|
||||
|
||||
虚拟时钟下这些场景是零耗时的:`run_ms()` 干的就是任务体干的事
|
||||
—— 调一次 `t_actuator_step()`,按它返回的时间往前拨表。 */
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "hal_gpio.h"
|
||||
#include "hal_lock.h"
|
||||
#include "hal_time.h"
|
||||
#include "phonerail.h"
|
||||
#include "rail_cfg.h"
|
||||
#include "sim_hal.h"
|
||||
#include "sr595.h"
|
||||
#include "t_actuator.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;
|
||||
}
|
||||
|
||||
static void board_up(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_lock_init());
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, rail_en_init()); /* 十根 EN 线,全部拉低 */
|
||||
|
||||
sim_sr595_attach(SR595_PIN_SER, SR595_PIN_SRCLK, SR595_PIN_RCLK, SR595_PIN_OE_N);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, sr595_init()); /* 安全影子 + 使能输出(A5) */
|
||||
|
||||
/* 强制加锁纪律 —— 但要在 `sr595_init()` **之后**打开:初始化是启动期
|
||||
独占跑的,本来就不该被要求先拿锁。打开之后,动那四根线却没持锁就记违规。 */
|
||||
sim_lock_require(HAL_LOCK_SR595, true);
|
||||
|
||||
s_pg_readable = true;
|
||||
memset(s_pg_val, 0, sizeof(s_pg_val));
|
||||
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 内部把虚拟时钟
|
||||
花掉(hal_lock_sim.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);
|
||||
}
|
||||
}
|
||||
|
||||
static uint32_t keys_on_chain(void) { return sim_sr595_outputs() & sr595_all_keys_mask(); }
|
||||
|
||||
/* ==== 验收主项一:硬重启走完 17 ms 上电时序 =============================== */
|
||||
|
||||
/* 用 CH3 —— 它的 `EFUSE_EN` 是 IO6、`VBUS_EN` 是 IO11、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 latches0 = sim_sr595_latches();
|
||||
const uint32_t t_req = hal_time_now_ms();
|
||||
|
||||
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);
|
||||
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));
|
||||
|
||||
/* 硬重启在 24 位链上什么都不该发(键本来就没按着 ⇒ 影子不变 ⇒ 不脏 ⇒ 不发帧)。 */
|
||||
TEST_ASSERT_EQUAL_UINT32(latches0, sim_sr595_latches());
|
||||
/* 也不该碰 I²C 那把锁 —— 两把锁因此永远不会嵌套。 */
|
||||
TEST_ASSERT_EQUAL_UINT32(0, sim_lock_takes(HAL_LOCK_I2C));
|
||||
TEST_ASSERT_EQUAL_UINT32(0, sim_lock_violations(HAL_LOCK_SR595));
|
||||
}
|
||||
|
||||
/* 别的四路的两根 EN 线,从头到尾一次都不该被写。 */
|
||||
static void test_hard_reset_touches_only_its_own_two_pins(void)
|
||||
{
|
||||
const int rail = 2;
|
||||
uint32_t before[N][2];
|
||||
|
||||
for (int r = 1; r <= N; r++) {
|
||||
before[r - 1][0] = sim_gpio_writes(rail_cfg(r)->efuse_en);
|
||||
before[r - 1][1] = sim_gpio_writes(rail_cfg(r)->vbus_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);
|
||||
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][0], sim_gpio_writes(rail_cfg(r)->efuse_en));
|
||||
TEST_ASSERT_EQUAL_UINT32(before[r - 1][1], sim_gpio_writes(rail_cfg(r)->vbus_en));
|
||||
}
|
||||
/* VBUS 是 EDL 的事,硬重启不碰。 */
|
||||
TEST_ASSERT_EQUAL_UINT32(before[rail - 1][1], sim_gpio_writes(rail_cfg(rail)->vbus_en));
|
||||
}
|
||||
|
||||
/* 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)); /* ⭐ 不许再关掉 */
|
||||
}
|
||||
|
||||
/* 读得到、但一直是低(电源确实没好)走的是同一条路:照样不自作主张断电。 */
|
||||
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_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」,功能测试看不出来)。 */
|
||||
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_busy(rail)) {
|
||||
total = hal_time_now_ms() - t0; /* 收工的**那一刻**,不是下一次醒来 */
|
||||
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_FALSE(t_actuator_busy(rail));
|
||||
TEST_ASSERT_EQUAL_INT(ACT_RES_OK, t_actuator_last_result(rail));
|
||||
TEST_ASSERT_TRUE(total <= pg_at + 2 * ACT_HR_PG_POLL_MS);
|
||||
}
|
||||
|
||||
/* 硬重启之前先把这一路的三个键松开:断电重启时有键按着,手机会开进
|
||||
Recovery / Fastboot 而不是正常起来。这里手工造一个「上一次收尾没落地」的脏位。 */
|
||||
static void test_hard_reset_releases_a_stuck_key_before_cutting_power(void)
|
||||
{
|
||||
const int rail = 1;
|
||||
const int bit = sr595_key_bit(rail, SR595_KEY_PWR);
|
||||
const int other = sr595_key_bit(4, SR595_KEY_VUP);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_lock_take(HAL_LOCK_SR595, 10));
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK,
|
||||
sr595_write(((uint32_t)1u << bit) | ((uint32_t)1u << other),
|
||||
((uint32_t)1u << bit) | ((uint32_t)1u << other)));
|
||||
hal_lock_give(HAL_LOCK_SR595);
|
||||
TEST_ASSERT_EQUAL_INT(1, sim_sr595_pin(bit));
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail));
|
||||
TEST_ASSERT_EQUAL_INT(0, sim_sr595_pin(bit)); /* 松开了 */
|
||||
TEST_ASSERT_EQUAL_INT(1, sim_sr595_pin(other)); /* 别路的键一动没动 */
|
||||
|
||||
/* 顺序也要对:先松键,再断电。 */
|
||||
TEST_ASSERT_TRUE(sim_gpio_write_seq(SR595_PIN_RCLK) <
|
||||
sim_gpio_write_seq(rail_cfg(rail)->efuse_en));
|
||||
TEST_ASSERT_EQUAL_INT(0, sim_gpio_level(rail_cfg(rail)->efuse_en));
|
||||
}
|
||||
|
||||
/* ==== 验收主项二:按键序列不重入 ========================================= */
|
||||
|
||||
/* 短按 100 ms(SW-5 的第一档)。位号一律查 `sr595_key_bit()`:CH5 的电源键
|
||||
在 U22.QH(bit15),按通道号算怎么算都算不出来。 */
|
||||
static void test_key_tap_presses_the_table_bit_and_releases_it(void)
|
||||
{
|
||||
const int rail = 5;
|
||||
const int bit = sr595_key_bit(rail, SR595_KEY_PWR);
|
||||
const uint32_t latches0 = sim_sr595_latches();
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_key(rail, ACT_KEYS_PWR, ACT_KEY_MS_TAP));
|
||||
TEST_ASSERT_EQUAL_INT(1, sim_sr595_pin(bit)); /* 栅极真的被拉起来了 */
|
||||
TEST_ASSERT_EQUAL_UINT32(latches0 + 1, sim_sr595_latches());
|
||||
TEST_ASSERT_EQUAL_UINT32((uint32_t)1u << bit, keys_on_chain());
|
||||
|
||||
run_ms(ACT_KEY_MS_TAP - 1);
|
||||
TEST_ASSERT_EQUAL_INT(1, sim_sr595_pin(bit));
|
||||
TEST_ASSERT_TRUE(t_actuator_busy(rail));
|
||||
|
||||
run_ms(1);
|
||||
TEST_ASSERT_EQUAL_INT(0, sim_sr595_pin(bit));
|
||||
TEST_ASSERT_EQUAL_UINT32(latches0 + 2, sim_sr595_latches()); /* 按下一帧、松开一帧 */
|
||||
TEST_ASSERT_EQUAL_UINT32(0, keys_on_chain());
|
||||
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_SR595));
|
||||
}
|
||||
|
||||
/* ⭐ §10.6 的硬约束:组合键必须**同一帧**写进去。
|
||||
分两次写的实现在这里 `latches` 会 +2,而且中间那一帧只按下了一个键
|
||||
——「先单按了一下 VOL−」在有的机型上就是调音量。 */
|
||||
static void test_combo_keys_land_in_exactly_one_frame(void)
|
||||
{
|
||||
const int rail = 2;
|
||||
const uint32_t want = ((uint32_t)1u << sr595_key_bit(rail, SR595_KEY_VDN)) |
|
||||
((uint32_t)1u << sr595_key_bit(rail, SR595_KEY_PWR));
|
||||
const uint32_t latches0 = sim_sr595_latches();
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_key(rail, ACT_KEYS_FASTBOOT, ACT_KEY_MS_ON));
|
||||
TEST_ASSERT_EQUAL_UINT32(latches0 + 1, sim_sr595_latches());
|
||||
TEST_ASSERT_EQUAL_UINT32(want, keys_on_chain());
|
||||
|
||||
run_ms(ACT_KEY_MS_ON);
|
||||
TEST_ASSERT_EQUAL_UINT32(0, keys_on_chain());
|
||||
TEST_ASSERT_EQUAL_UINT32(latches0 + 2, sim_sr595_latches());
|
||||
}
|
||||
|
||||
/* 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 uint32_t recovery = keys_on_chain();
|
||||
run_ms(ACT_KEY_MS_ON);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_key(rail, ACT_KEYS_FASTBOOT, ACT_KEY_MS_ON));
|
||||
const uint32_t fastboot = keys_on_chain();
|
||||
run_ms(ACT_KEY_MS_ON);
|
||||
|
||||
TEST_ASSERT_NOT_EQUAL(recovery, fastboot);
|
||||
TEST_ASSERT_EQUAL_UINT32((uint32_t)1u << sr595_key_bit(rail, SR595_KEY_PWR),
|
||||
recovery & fastboot); /* 共有的只有电源键 */
|
||||
}
|
||||
|
||||
/* ⭐ 不重入:一路上正在跑序列时,三个请求全部被拒,而且**一个字节都不发**。 */
|
||||
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 latches1 = sim_sr595_latches();
|
||||
const uint32_t shadow1 = sr595_shadow();
|
||||
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(latches1, sim_sr595_latches());
|
||||
TEST_ASSERT_EQUAL_UINT32(shadow1, sr595_shadow());
|
||||
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)); /* 只跑过一个序列 */
|
||||
}
|
||||
|
||||
/* 互斥是**每路一个**:别的路照样能同时跑。这也是影子寄存器存在的理由 ——
|
||||
§10.6 那句「另一台手机的按键被顺手清掉」说的就是这里。 */
|
||||
static void test_two_rails_run_at_the_same_time_without_clobbering(void)
|
||||
{
|
||||
const int b1 = sr595_key_bit(1, SR595_KEY_PWR);
|
||||
const int b4 = sr595_key_bit(4, SR595_KEY_VUP);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_key(1, ACT_KEYS_PWR, ACT_KEY_MS_ON)); /* 2 s */
|
||||
run_ms(50);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_key(4, ACT_KEYS_VUP, ACT_KEY_MS_TAP)); /* 100 ms */
|
||||
TEST_ASSERT_EQUAL_INT(1, sim_sr595_pin(b1));
|
||||
TEST_ASSERT_EQUAL_INT(1, sim_sr595_pin(b4));
|
||||
|
||||
run_ms(ACT_KEY_MS_TAP); /* CH4 到点 */
|
||||
TEST_ASSERT_EQUAL_INT(0, sim_sr595_pin(b4));
|
||||
TEST_ASSERT_EQUAL_INT(1, sim_sr595_pin(b1)); /* ⭐ CH1 没被顺手清掉 */
|
||||
TEST_ASSERT_TRUE(t_actuator_busy(1));
|
||||
TEST_ASSERT_FALSE(t_actuator_busy(4));
|
||||
|
||||
run_ms(ACT_KEY_MS_ON); /* CH1 到点 */
|
||||
TEST_ASSERT_EQUAL_UINT32(0, keys_on_chain());
|
||||
TEST_ASSERT_FALSE(t_actuator_busy(1));
|
||||
}
|
||||
|
||||
/* ==== 验收主项三:EDL 顺序固定 =========================================== */
|
||||
|
||||
/* SW-6:`EDL_EN=1` → `VBUS_EN=1` → 等枚举 → `EDL_EN=0`。**先短接再插 USB**。
|
||||
只看终值是分不出顺序的(终值都是「EDL 松开、VBUS 开着」),所以这里比的是
|
||||
全局写序号。 */
|
||||
static void test_edl_order_is_short_then_vbus_then_release(void)
|
||||
{
|
||||
const int rail = 3;
|
||||
const int edl = sr595_edl_bit(rail);
|
||||
const int vb = rail_cfg(rail)->vbus_en;
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, sim_gpio_level(vb));
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_edl(rail));
|
||||
TEST_ASSERT_EQUAL_INT(1, sim_sr595_pin(edl));
|
||||
TEST_ASSERT_EQUAL_INT(0, sim_gpio_level(vb)); /* USB 还没插 */
|
||||
const uint32_t seq_short = sim_gpio_write_seq(SR595_PIN_RCLK);
|
||||
|
||||
run_ms(ACT_EDL_SHORT_MS - 1);
|
||||
TEST_ASSERT_EQUAL_INT(0, sim_gpio_level(vb));
|
||||
run_ms(1);
|
||||
TEST_ASSERT_EQUAL_INT(1, sim_gpio_level(vb));
|
||||
TEST_ASSERT_EQUAL_INT(1, sim_sr595_pin(edl)); /* 还短接着 */
|
||||
const uint32_t seq_vbus = sim_gpio_write_seq(vb);
|
||||
|
||||
run_ms(ACT_EDL_ENUM_MS - 1);
|
||||
TEST_ASSERT_EQUAL_INT(1, sim_sr595_pin(edl));
|
||||
run_ms(1);
|
||||
TEST_ASSERT_EQUAL_INT(0, sim_sr595_pin(edl)); /* 枚举完才松短接 */
|
||||
const uint32_t seq_release = sim_gpio_write_seq(SR595_PIN_RCLK);
|
||||
|
||||
TEST_ASSERT_TRUE(seq_short < seq_vbus);
|
||||
TEST_ASSERT_TRUE(seq_vbus < seq_release);
|
||||
|
||||
/* VBUS **留着** —— 手机现在正挂在 9008 上,把它断了等于白跑一趟。 */
|
||||
TEST_ASSERT_EQUAL_INT(1, sim_gpio_level(vb));
|
||||
TEST_ASSERT_FALSE(t_actuator_busy(rail));
|
||||
TEST_ASSERT_EQUAL_INT(ACT_RES_OK, t_actuator_last_result(rail));
|
||||
}
|
||||
|
||||
/* EDL 用的是 `sr595_edl_bit()` 那张表,一路都不能串。CH1 在 U33.QA(16),
|
||||
CH2~5 在 U21 上而且不是顺序的 —— 算出来的位号必错。 */
|
||||
static void test_edl_uses_the_lookup_table_not_arithmetic(void)
|
||||
{
|
||||
for (int rail = 1; rail <= N; rail++) {
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_edl(rail));
|
||||
TEST_ASSERT_EQUAL_UINT32((uint32_t)1u << sr595_edl_bit(rail),
|
||||
sim_sr595_outputs() & ~sr595_all_keys_mask() &
|
||||
~((uint32_t)1u << SR595_BIT_HUB_RST));
|
||||
run_ms(ACT_EDL_SHORT_MS + ACT_EDL_ENUM_MS);
|
||||
TEST_ASSERT_FALSE(t_actuator_busy(rail));
|
||||
}
|
||||
}
|
||||
|
||||
/* ==== 收尾:按下去的东西一定要松开 ======================================= */
|
||||
|
||||
/* 595 那把锁被别人占着时,松键落不下去。**不许放弃** ——
|
||||
放弃 = 电源键留在按下状态,而遥测上五路电压电流一切正常。 */
|
||||
static void test_cleanup_retries_until_the_key_is_really_released(void)
|
||||
{
|
||||
const int rail = 1;
|
||||
const int bit = sr595_key_bit(rail, SR595_KEY_PWR);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_key(rail, ACT_KEYS_PWR, ACT_KEY_MS_TAP));
|
||||
|
||||
/* 扮演第二个任务,把 595 那把锁占住(A15 的初始化、A10 的收尾都会拿它)。 */
|
||||
sim_lock_set_task(1);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_lock_take(HAL_LOCK_SR595, 10));
|
||||
sim_lock_set_task(0);
|
||||
|
||||
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, sim_sr595_pin(bit)); /* 键还按着 */
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
(void)t_actuator_step();
|
||||
}
|
||||
TEST_ASSERT_TRUE(t_actuator_cleanup_retries(rail) > 0);
|
||||
/* 收尾期间这一路照样算 busy,新请求进不来。 */
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_STATE, t_actuator_key(rail, ACT_KEYS_PWR, ACT_KEY_MS_TAP));
|
||||
|
||||
sim_lock_set_task(1);
|
||||
hal_lock_give(HAL_LOCK_SR595);
|
||||
sim_lock_set_task(0);
|
||||
|
||||
(void)t_actuator_step();
|
||||
TEST_ASSERT_EQUAL_INT(0, sim_sr595_pin(bit)); /* 锁一放开就落地 */
|
||||
TEST_ASSERT_FALSE(t_actuator_busy(rail));
|
||||
}
|
||||
|
||||
/* 按下那一步就失败(595 那把锁被别人占着)。请求返回错误码,但**不能就这么算了** ——
|
||||
影子有可能已经被改成「按下」了(拿到锁而整帧没发出去的那种失败),直接回 IDLE
|
||||
等于把一个脏的「按下」留在那儿:谁下一次 flush 谁就替我们把键按下去,
|
||||
而没有任何人会来松开它。所以失败的按下照样要走收尾。 */
|
||||
static void test_a_failed_press_still_goes_through_cleanup(void)
|
||||
{
|
||||
const int rail = 4;
|
||||
const int bit = sr595_key_bit(rail, SR595_KEY_PWR);
|
||||
|
||||
sim_lock_set_task(1);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_lock_take(HAL_LOCK_SR595, 10));
|
||||
sim_lock_set_task(0);
|
||||
|
||||
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, sim_sr595_pin(bit)); /* 这一次键根本没按下去 */
|
||||
|
||||
sim_lock_set_task(1);
|
||||
hal_lock_give(HAL_LOCK_SR595);
|
||||
sim_lock_set_task(0);
|
||||
|
||||
(void)t_actuator_step();
|
||||
TEST_ASSERT_FALSE(t_actuator_busy(rail));
|
||||
TEST_ASSERT_EQUAL_INT(0, sim_sr595_pin(bit));
|
||||
}
|
||||
|
||||
/* `t_actuator_abort()`:立刻收尾,键当场松开,但**不动两根 EN 线** ——
|
||||
把电再关一次或再开一次都是自作主张(那是 A10 的判据和上位机命令的事)。 */
|
||||
static void test_abort_releases_the_key_and_leaves_the_pins_alone(void)
|
||||
{
|
||||
const int rail = 2;
|
||||
const int bit = sr595_key_bit(rail, SR595_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, sim_sr595_pin(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_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, 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));
|
||||
TEST_ASSERT_EQUAL_UINT32(0, sim_lock_violations(HAL_LOCK_SR595));
|
||||
}
|
||||
|
||||
/* 时间常量与文档逐格对账。改了这里就是改了需求,得先去改文档。 */
|
||||
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(100, ACT_KEY_MS_TAP); /* SW-5 三档 */
|
||||
TEST_ASSERT_EQUAL_UINT32(2000, ACT_KEY_MS_ON);
|
||||
TEST_ASSERT_EQUAL_UINT32(10000, ACT_KEY_MS_FORCE);
|
||||
TEST_ASSERT_TRUE(ACT_KEY_MS_FORCE <= ACT_KEY_MAX_MS); /* 最长那一档不能被自己的上限拦掉 */
|
||||
|
||||
/* 逻辑键位就是 sr595 的那三个下标,组合键是它们的并。 */
|
||||
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);
|
||||
}
|
||||
|
||||
/* 加锁纪律那条检查本身不能是句空话:不拿锁直接写一帧,违规必须 +1。
|
||||
(没有这条反向测试,上面那些「violations == 0」在检查失灵时会永远绿。) */
|
||||
static void test_the_lock_discipline_check_actually_fires(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_UINT32(0, sim_lock_violations(HAL_LOCK_SR595));
|
||||
(void)sr595_write((uint32_t)1u << sr595_key_bit(1, SR595_KEY_PWR), 0xFFFFFFFFu &
|
||||
SR595_MASK_VALID);
|
||||
TEST_ASSERT_TRUE(sim_lock_violations(HAL_LOCK_SR595) > 0);
|
||||
}
|
||||
|
||||
/* 空闲时 `t_actuator_step()` 什么也不做,也不该去动锁或引脚。 */
|
||||
static void test_idle_step_is_free(void)
|
||||
{
|
||||
const uint32_t takes = sim_lock_takes(HAL_LOCK_SR595);
|
||||
const uint32_t latches = sim_sr595_latches();
|
||||
|
||||
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_SR595));
|
||||
TEST_ASSERT_EQUAL_UINT32(latches, sim_sr595_latches());
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
|
||||
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_two_pins);
|
||||
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_key_tap_presses_the_table_bit_and_releases_it);
|
||||
RUN_TEST(test_combo_keys_land_in_exactly_one_frame);
|
||||
RUN_TEST(test_recovery_and_fastboot_are_different_key_sets);
|
||||
RUN_TEST(test_a_running_sequence_rejects_every_new_request);
|
||||
RUN_TEST(test_two_rails_run_at_the_same_time_without_clobbering);
|
||||
|
||||
RUN_TEST(test_edl_order_is_short_then_vbus_then_release);
|
||||
RUN_TEST(test_edl_uses_the_lookup_table_not_arithmetic);
|
||||
|
||||
RUN_TEST(test_cleanup_retries_until_the_key_is_really_released);
|
||||
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_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();
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户