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]>
这个提交包含在:
zhou yang
2026-09-07 08:08:28 +08:00
共同撰写人 Claude Opus 5
父节点 ec31891429
当前提交 b3206005ab
共修改 9 个文件,包含 2572 行新增和 1214 行删除
+203
查看文件
@@ -0,0 +1,203 @@
/* `t_actuator` —— 每路三个序列 + 互斥(需求 §10.6,§10.3 的第三行任务)。
三个序列,**同一路上同时只能跑一个**(§10.6 原话「彼此互斥、不可重入」):
| 序列 | 时序 | 出处 |
|---|---|---|
| 硬重启 | `EFUSE_EN↓` → 等 3~5 s → `EFUSE_EN↑` → 等 **17 ms** → 等 PG 置位 | 设计文档 §7.4 |
| 按键 | 置位 → 保持 T(100 ms / 2 s / 10 s)→ 清零 | SW-5 / §13.3 |
| EDL | `EDL_EN=1` → `VBUS_EN=1` → 等枚举 → `EDL_EN=0` | SW-6 / §22.4 |
---- 三条不能改的规矩 ----------------------------------------------------
1. **一个函数一步,不许写 `while(1)`。** 死循环在宿主机上一行也跑不到,
而这一包的全部价值就是那几个时间常量与它们的先后 —— 那正是要被测的东西。
`t_actuator_step()` 只看时钟、推状态、返回「下次最晚多久再来」,
任务体在 app_main.c 里,还是三行。
2. **放电由硬件联锁自动完成(SR-7),固件一个字都不要写。** 放电 MOS 的栅极
经一级反相器接在 `EFUSE_EN` 上(设计文档 §7.3),物理上不可能与 eFuse 同时导通。
固件里再补一根「放电控制」= 10 Ω 上持续 1.76 W,电阻烧掉。
所以本文件里**没有任何放电接口**,硬重启只有一根 `EFUSE_EN`。
3. **按下去的东西一定要松开。** 这一层最坏的失败不是「序列没走完」,
而是**键留在按下状态** —— 电源键被一直按着,手机再也起不来,而且遥测上
五路电压电流全都正常。所以每个序列都以 `ACT_ST_CLEANUP` 收尾(超时、
出错、正常结束走的是同一条路),而且 **CLEANUP 不放弃**:写不进去就一直重试,
这一路一直算 busy。放弃比重试危险得多。
---- PG 为什么是别人喂进来的 ----------------------------------------------
`PGOOD` 挂在 PCA9555 的 Port1 上,而 PCA9555 在 I²C 上,`t_safety` 已经每 20 ms
无条件读一次(§10.3)。执行器再去读一遍等于凭空多一份总线占用和一份两边不一致的
可能,所以 PG 从外面**喂**进来(`act_pg_fn`)。这条回调也是「不许在这里拿 I²C 锁」
的物理保证:本文件只碰 GPIO 与 595,与 I²C 无交集 ⇒ 两把锁永远不会嵌套。 */
#ifndef T_ACTUATOR_H
#define T_ACTUATOR_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "hal_err.h"
#include "phonerail.h"
#include "sr595.h"
#ifdef __cplusplus
extern "C" {
#endif
/* ---- 时间常量:每一个都有出处,不许拍脑袋改 ----------------------------- */
/* 硬重启断电时长。§7.4:放到 <0.5 V 只要 ~110 ms(τ = 2200 µF × 10 Ω = 22 ms),
其余是等手机彻底断电,窗口 3~5 s,取中值。
⚠️ 缩到 1 s 的现象**不是**「重启快了」,而是「有的机型压根没重启」
(某些机型要 VBAT 掉到 2.5 V 以下才算真断电,见 §7.1)。 */
#define ACT_HR_OFF_MS 4000
#define ACT_HR_OFF_MIN_MS 3000 /* §7.4 的窗口,自检与测试拿它对账 */
#define ACT_HR_OFF_MAX_MS 5000
/* `EFUSE_EN↑` 之后到输出稳定:tON 延迟 5.1 ms(C = 33 nF)+ 缓启动斜坡 11.5 ms
(0.36 V/ms)≈ **17 ms**(设计文档 §5「缓启动实际值」与 §7.4)。
这段时间之内 PG 本来就不该置位,去看它只会看到「还没好」。 */
#define ACT_HR_RAMP_MS 17
/* 等 PG 的上限。TPS25940A 的 PGOOD 在输出过 2.97 V 时释放,也就是斜坡中段就该来了;
500 ms 是 ~30 倍余量。等不到**不代表要再断一次电**,见 `ACT_RES_NO_PG`。 */
#define ACT_HR_PG_TIMEOUT_MS 500
/* 等 PG 期间多久回来看一眼。PG 是 `t_safety` 每 20 ms 刷新的快照,
比这个再密也没有新值。⚠️ 不能直接睡满 500 ms —— 那样「等到 PG 就结束」
会退化成「一律等满 500 ms」,而测试与遥测都看不出区别。 */
#define ACT_HR_PG_POLL_MS 5
/* SW-5 的三档按键时长。 */
#define ACT_KEY_MS_TAP 100 /* 短按:唤醒 / 息屏 */
#define ACT_KEY_MS_ON 2000 /* 长按:开机 */
#define ACT_KEY_MS_FORCE 10000 /* 长按:强制重启(两级重启的「一级(软)」) */
/* 按键时长上限。上层传进来一个 10 分钟,等于把电源键按死 ——
这不是保护,是**拦调用方的手误**(SR-13 拦 4.2 V 是同一个道理)。 */
#define ACT_KEY_MAX_MS 15000
/* EDL:先短接、再插 USB(SW-6 的顺序是硬要求)。
模拟开关 TS5A3157 的 Ron ~1 Ω、导通时间 µs 级,10 ms 是给它和 1.8 kΩ 上的建立留的。 */
#define ACT_EDL_SHORT_MS 10
/* 「等枚举」。⚠️ **固件看不见枚举** —— 手机挂在板载 Hub 的下行口上,ESP32 这一侧
没有任何信号能告诉它 9008 有没有起来。所以这里只能定时等,3 s 是按 USB 枚举
典型 <1 s 给的余量。真要确认枚举成功,只能由上位机 `lsusb` 看 05c6:9008
(那是 §10.8 上位机那一侧的事)。 */
#define ACT_EDL_ENUM_MS 3000
/* 拿 595 那把锁最多等多久。这把锁的争用只可能来自 A15 的初始化与 A10 的收尾,
都是短事务;等不到就下一拍再来(CLEANUP 会一直重试)。 */
#define ACT_LOCK_TIMEOUT_MS 20
/* CLEANUP 重试间隔,以及空闲时建议任务睡多久(A12 会换成「带超时地等队列」)。 */
#define ACT_CLEANUP_RETRY_MS 5
#define ACT_IDLE_WAIT_MS 100
/* ---- 按键:逻辑键位,不是链上位号 ---------------------------------------
上层一律用这三个位(和它们的组合)表达「按哪几个键」,链上的 24 位位号
由本文件经 `sr595_key_bit()` 查表换算。**任何地方都不许出现 `1 << (rail-1)`
这类算出来的位号**(sr595.h 的那条禁令)。 */
typedef enum {
ACT_KEYS_PWR = 1u << SR595_KEY_PWR,
ACT_KEYS_VUP = 1u << SR595_KEY_VUP,
ACT_KEYS_VDN = 1u << SR595_KEY_VDN,
/* SW-5 的两个组合键。**必须同一帧写进去**(§10.6)——
分两次写会先单按一个键,而「先单按 VOL−」在有的机型上就是音量调节。 */
ACT_KEYS_FASTBOOT = ACT_KEYS_VDN | ACT_KEYS_PWR,
ACT_KEYS_RECOVERY = ACT_KEYS_VUP | ACT_KEYS_PWR,
ACT_KEYS_ALL = ACT_KEYS_PWR | ACT_KEYS_VUP | ACT_KEYS_VDN,
} act_keys_t;
/* ---- 状态与结果 ---------------------------------------------------------- */
typedef enum {
ACT_SEQ_NONE = 0,
ACT_SEQ_HARD_RESET,
ACT_SEQ_KEY,
ACT_SEQ_EDL,
} act_seq_t;
typedef enum {
ACT_ST_IDLE = 0,
ACT_ST_HR_OFF, /* `EFUSE_EN` 已拉低,等 3~5 s(放电由硬件自己做) */
ACT_ST_HR_RAMP, /* `EFUSE_EN` 已拉高,等 17 ms 走完 tON + 斜坡 */
ACT_ST_HR_PG, /* 等 PG 置位 */
ACT_ST_KEY_HOLD, /* 键已按下,保持 T */
ACT_ST_EDL_SHORT, /* `EDL_EN=1`,等短接建立 */
ACT_ST_EDL_ENUM, /* `VBUS_EN=1`,等枚举 */
ACT_ST_CLEANUP, /* 把本序列按下去的东西松开,落不下去就重试(不放弃) */
} act_state_t;
typedef enum {
ACT_RES_NONE = 0,
ACT_RES_OK,
/* 硬重启专用:电**已经开着**,只是 500 ms 内没等到 PG(也可能是 PCA9555
读不到)。⚠️ 这时**不许**再把这一路关掉:§10.10 里「PCA9555 不应答 =
失去 FAULT/PG 可见性,**不关输出**」。把「看不见」当成「坏了」去断电,
等于一片 I/O 扩展器失联就让五台手机一起停机。 */
ACT_RES_NO_PG,
/* 写 595 或写 EN 引脚失败。序列中止,但收尾照做(键一定松开)。 */
ACT_RES_IO,
/* 被 `t_actuator_abort()` 打断。 */
ACT_RES_ABORTED,
} act_result_t;
/* PG 取值回调。返回 false = **这一刻读不到**(PCA9555 失联 / 还没读过一圈),
与「读到了,是低」是两回事 —— 后者是「电源还没好」,前者是「不知道」。
传 NULL 给 `t_actuator_init()` 就是「永远读不到」,硬重启会走完并记 ACT_RES_NO_PG。 */
typedef bool (*act_pg_fn)(void *ctx, int rail, bool *pg);
/* ---- 接口 ---------------------------------------------------------------- */
/* 幂等,五路全部回到 IDLE。**不碰硬件**:SW-11 的「上电全关」是
`rail_en_init()` + `sr595_init()` 的安全影子干的(A15),不是这里。 */
void t_actuator_init(act_pg_fn pg, void *ctx);
/* 三个请求。rail 是 **1~5**。
HAL_ERR_ARG —— 路号 / 键位 / 时长越界;
HAL_ERR_STATE —— 没 init,或者**这一路正在跑另一个序列**(§10.6 不可重入)。
要抢占的话调用方得先 `t_actuator_abort()`,写成显式的。
⚠️ 返回非 HAL_OK 时序列**没有开始**,只有一个例外:按下 / 置位那一步真的
发到链上但失败了 —— 那时影子可能已经改了,所以那一路会进 CLEANUP 把它撤回来,
函数仍然返回错误码,但 `t_actuator_busy()` 会短暂为 true。 */
hal_err_t t_actuator_hard_reset(int rail);
hal_err_t t_actuator_key(int rail, uint32_t keys, uint32_t hold_ms);
hal_err_t t_actuator_edl(int rail);
/* 打断这一路正在跑的序列:立刻收尾(键松开 / `EDL_EN` 清零),结果记 ACT_RES_ABORTED。
**不动 `EFUSE_EN` / `VBUS_EN`** —— 硬重启走到一半被打断时,把电再关一次
或者再开一次都是自作主张;断电是 A10 的判据和上位机命令的事。 */
void t_actuator_abort(int rail);
/* 推一步。返回**下次最晚多久再来**(ms):五路里最近的那个截止时刻。
全空闲返回 ACT_IDLE_WAIT_MS。醒早了没关系 —— 每个状态都是拿绝对时刻算的,
早来就再返回一个剩余时间。(ESP-IDF 默认 `CONFIG_FREERTOS_HZ=100`,
`vTaskDelay(pdMS_TO_TICKS(17))` 只有 1 个 tick,本来就会早醒。) */
uint32_t t_actuator_step(void);
/* ---- 观测 ---------------------------------------------------------------- */
bool t_actuator_busy(int rail);
act_state_t t_actuator_state(int rail);
act_seq_t t_actuator_seq(int rail);
act_result_t t_actuator_last_result(int rail);
uint32_t t_actuator_done(int rail); /* 走完几个序列(含失败的) */
uint32_t t_actuator_cleanup_retries(int rail); /* 收尾重试次数,遥测里报出去 */
const char *t_actuator_state_str(act_state_t st);
const char *t_actuator_result_str(act_result_t r);
/* 一行人话,给启动日志和遥测用。返回写进去的字符数(不含结尾 0)。 */
int t_actuator_format(char *buf, size_t len);
#ifdef __cplusplus
}
#endif
#endif /* T_ACTUATOR_H */