/* `t_actuator` —— 每路三个序列 + 互斥(需求 §10.6,§10.3 的第三行任务)。 三个序列,**同一路上同时只能跑一个**(§10.6 原话「彼此互斥、不可重入」): | 序列 | 时序 | 出处 | |---|---|---| | 硬重启 | `EFUSE_EN↓` → 等 3~5 s → `EFUSE_EN↑` → 等 **17 ms** → 等 PG 置位 → 长按电源键 5 s 开机 →(接了电流回调时)看电流、没开就重按,最多 3 次 | 设计文档 §7.4;开机那一步见 `ACT_HR_BOOT_KEY_MS` / `ACT_HR_BOOT_TRIES` | | 按键 | 置位 → 保持 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 的 P1.3 上,而 `t_safety` 已经每 20 ms 无条件读一圈 (§10.3)。执行器再去读一遍等于凭空多一份总线占用和一份两边不一致的可能, 所以 PG 从外面**喂**进来(`act_pg_fn`)。 ---- ⚠️ R2 改了这一层的锁前提 --------------------------------------------- R1 里执行器只碰 GPIO 与 74HC595,与 I²C 无交集,所以两把锁永远不会嵌套。 **R2 里按键 / EDL / VBUS 全在刀片的 PCA9555 上 ⇒ 本文件必须拿 I²C 锁** (经 `slot_*`,它们自带 `i2cbus_xact`)。 好消息是 595 那把锁没了,全系统**只剩一把锁**,嵌套在物理上不可能。 代价是与巡检争用同一把锁,于是: · 每一次动作都是一次短事务,**不许在持锁期间等时间** (按键保持 100 ms 是状态机的事,不是持锁的事); · `ACT_ST_CLEANUP` 的「不放弃」比 R1 更要紧:总线忙时写不进去必须一直重试。 */ #ifndef T_ACTUATOR_H #define T_ACTUATOR_H #include #include #include #include "hal_err.h" #include "phonerail.h" #include "slot.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 /* 硬重启的最后一步:等到 PG 之后长按电源键开机。电池端上电的手机**停在关机态** (小米8 实测:重新上电后只漏 ~2 mA,不按键就一直关着),不按这一下的硬重启 等于「关机」。时长就是 SW-5 的开机档。 ⚠️ 只在**等到 PG** 时按:NO_PG 多半是这一片 PCA9555 读不到,键也在同一片上, 去按只会让这一路卡在 CLEANUP 里重试。 */ #define ACT_HR_BOOT_KEY_MS ACT_KEY_MS_ON /* ---- 开机校验与重按(0.1.2,接了 `t_actuator_set_boot_check()` 才生效) ---- 2026-09-28 板 B 第 1 路实测:R2 的电源键是**主板弹片压刀片铜盘**(决策 14), 接触时好时坏 —— 7 次硬重启只开起来 1 次,失败那几次 PCA9555 输出确实置位了, 手机侧电流却一直 1~2 mA(PMIC 根本没醒,屏幕不亮),镊子短接 J603 就能开机。 所以按完之后看电流:开起来了是 ~210 mA 平台(logo 段,之后 0.5~1.7 A),没开是 1~2 mA。 · 松键后先等 ACT_HR_VERIFY_DELAY_MS(3 s)再看:按 2 s 那种「PMIC 醒了、SoC 没接住 PS_HOLD」的情况,电流会先冲到 ~415 mA、松手 3 s 内掉回去 —— 早看会误判成功; 真开起来的那一段是 ~210 mA 平台、持续约 45 s,3~5 s 时稳稳在上面; · 连续 ACT_HR_VERIFY_GOOD_N 个样本 ≥ ACT_HR_BOOT_UA_MIN 才算开机, 窗口 ACT_HR_VERIFY_WINDOW_MS 内等不到就重按,一共最多按 ACT_HR_BOOT_TRIES 次; · **读不到电流不重按**:看不见 ≠ 没开(ACT_RES_NO_PG 同一条理由), 对一台正在开机的手机再长按 5 s 反而可能把它按进关机菜单。 */ #define ACT_HR_BOOT_TRIES 3 #define ACT_HR_BOOT_UA_MIN 100000 /* 100 mA:logo 段 ~210 mA,关机态 1~2 mA */ #define ACT_HR_VERIFY_DELAY_MS 3000 #define ACT_HR_VERIFY_WINDOW_MS 2000 /* 松键后 3~5 s 内判 */ #define ACT_HR_VERIFY_POLL_MS 100 /* 巡检 20 ms 一圈,100 ms 取一个样本足够 */ #define ACT_HR_VERIFY_GOOD_N 5 /* 连续 0.5 s 在线才算 */ /* SW-5 的三档按键时长。 ⚠️ 开机那一档 2026-09-28 小米8 实测:按 2 s 时 PMIC 起来了(415 mA),一松手 3 s 内 掉回 ~1 mA —— SoC 还没接住 PS_HOLD。按 5 s 松手后稳在 ~210 mA、约 45 s 进系统。 上限是 ACT_KEY_MS_FORCE:按到 10 s 就成了强制重启。 */ #define ACT_KEY_MS_TAP 100 /* 短按:唤醒 / 息屏 */ #define ACT_KEY_MS_ON 5000 /* 长按:开机 */ #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 /* 每次动作等 I²C 锁最多等多久(`slot.h` 的 `SLOT_LOCK_TIMEOUT_MS` 就是它)。 争用来自每 20 ms 一圈的巡检;等不到就下一拍再来(CLEANUP 会一直重试)。 */ #define ACT_LOCK_TIMEOUT_MS SLOT_LOCK_TIMEOUT_MS /* CLEANUP 重试间隔,以及空闲时建议任务睡多久(A12 会换成「带超时地等队列」)。 */ #define ACT_CLEANUP_RETRY_MS 5 #define ACT_IDLE_WAIT_MS 100 /* ---- 按键:逻辑键位,不是器件上的位号 ----------------------------------- 上层一律用这三个位(和它们的组合)表达「按哪几个键」,PCA9555 Port0 上的 位号由本文件经 `pca9555_key_bit()` 查表换算。 ⚠️ R2 里位号**不再随路号变**(五片刀片同构),但仍然查表不算。 */ typedef enum { ACT_KEYS_PWR = 1u << PCA9555_KEY_PWR, ACT_KEYS_VUP = 1u << PCA9555_KEY_VUP, ACT_KEYS_VDN = 1u << PCA9555_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_HR_BOOT, /* PG 到了,电源键按着,保持 ACT_HR_BOOT_KEY_MS 开机 */ ACT_ST_HR_VERIFY, /* 键已松开,看电流判断开没开起来;没开就回 HR_BOOT 重按 */ 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, /* 写 PCA9555 或写 EN 引脚失败(含拿不到 I²C 锁)。序列中止,但收尾照做。 */ ACT_RES_IO, /* 被 `t_actuator_abort()` 打断。 */ ACT_RES_ABORTED, /* 硬重启专用(0.1.2):电开着、PG 也到了,按了 ACT_HR_BOOT_TRIES 次电源键 手机电流始终起不来 —— 多半是电源键弹片没压上刀片铜盘。电**继续开着**。 */ ACT_RES_NO_BOOT, } 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); /* 开机校验用的电流取值(µA,有符号)。返回 false = 这一刻读不到。和 PG 一样从巡检快照喂进来。 */ typedef bool (*act_ua_fn)(void *ctx, int rail, int32_t *ua); /* 开机校验有了结论时回调一次(只在接了 `act_ua_fn` 时): booted = 电流起来了;presses = 一共按了几次(1 = 第一次就开了);ua = 判定那一刻的电流。 执行器自己不写事件(宿主机测试不带 evlog),由 app_main 接到 LOG_EV_BOOT_CHECK。 「读不到电流、判不了」不回调。 */ typedef void (*act_boot_fn)(void *ctx, int rail, bool booted, uint8_t presses, int32_t ua); /* ---- 接口 ---------------------------------------------------------------- */ /* 幂等,五路全部回到 IDLE。**不碰硬件**:SW-11 的「上电全关」是 `rail_en_init()`(GPIO)+ `slot_init()`(PCA9555 的输出锁存器先清零) 干的(A15),不是这里。 */ void t_actuator_init(act_pg_fn pg, void *ctx); /* 打开硬重启的开机校验与重按(见 ACT_HR_BOOT_TRIES 那一段)。ua = NULL 关掉, 关掉时硬重启按完一次电源键就记 OK(0.1.1 的行为)。done 可为 NULL。 在 `t_actuator_init()` 之后调;init 会把它清掉。 */ void t_actuator_set_boot_check(act_ua_fn ua, act_boot_fn done, 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 */