/* 三个执行器状态机 + 互斥。契约、时间常量的出处、三条规矩都在 t_actuator.h。 */ #include "t_actuator.h" #include #include #include #include #include "hal_time.h" #include "rail_cfg.h" #include "slot.h" typedef struct { act_state_t state; act_seq_t seq; uint32_t t0_ms; /* 进入当前状态的时刻(绝对,回绕安全) */ uint32_t keys; /* KEY 序列:逻辑键位(ACT_KEYS_*) */ uint32_t hold_ms; uint8_t release_mask; /* 收尾时要在 Port0 上清掉的位。0 = 这个序列没按下任何东西 */ act_result_t result; uint32_t done; uint32_t cleanup_retries; uint8_t presses; /* HR:这一次硬重启已经按了几次电源键 */ uint8_t good_n; /* HR_VERIFY:连续几个样本电流达标 */ bool ua_seen; /* HR_VERIFY:这一轮窗口里读到过电流没有 */ int32_t last_ua; /* HR_VERIFY:最近一个读到的样本 */ bool vbus_back; /* HR:开始时断了 VBUS,开机校验通过后要接回(0.1.11) */ bool vbus_pending; /* 已开机,等到 vbus_at 再接回 VBUS(ACT_HR_VBUS_BACK_MS) */ uint32_t vbus_at; } act_rail_t; static act_rail_t s_rail[PHONERAIL_RAIL_COUNT]; static act_pg_fn s_pg; static void *s_pg_ctx; static act_ua_fn s_ua; static act_boot_fn s_boot_done; static void *s_boot_ctx; static bool s_inited; /* ---- 小工具 -------------------------------------------------------------- */ static bool rail_valid(int rail) { return rail >= 1 && rail <= PHONERAIL_RAIL_COUNT; } static act_rail_t *at(int rail) { assert(rail_valid(rail)); return &s_rail[rail - 1]; } /* 逻辑键位 → PCA9555 Port0 位号。查 `pca9555_key_bit()`,**不算**。 ⚠️ R2 里位号与路号无关(五片刀片同构),所以这个函数不收 rail。 */ static uint8_t key_out_mask(uint32_t keys) { uint8_t m = 0; for (int k = 0; k < PCA9555_KEY_COUNT; k++) { if ((keys & ((uint32_t)1u << k)) != 0) { m |= (uint8_t)(1u << pca9555_key_bit((pca9555_key_t)k)); } } return m; } /* 动这一路 Port0 的**唯一**入口:`slot_out0_write()` 自己开一次 `i2cbus_xact` (拿锁 → 选通道 → 读改写一个字节 → 放锁)。 一次写就是一个字节 ⇒ 组合键的两位天然落在同一次写里(§10.6)。 ⚠️ 这一步会拿 I²C 锁,所以**不许**在持锁的上下文里调它(R2 的新前提,见头文件)。 */ static hal_err_t out_write(int rail, uint8_t mask, uint8_t value) { return slot_out0_write(rail, mask, value); } static bool pg_now(int rail, bool *pg) { if (s_pg == NULL) { return false; /* 「读不到」,不是「没好」 */ } return s_pg(s_pg_ctx, rail, pg); } static bool ua_now(int rail, int32_t *ua) { if (s_ua == NULL) { return false; } return s_ua(s_boot_ctx, rail, ua); } static void enter(act_rail_t *r, act_state_t st, uint32_t now) { r->state = st; r->t0_ms = now; } /* HR:按下电源键开机(HR_PG 第一次、HR_VERIFY 重按走同一个口)。 和 KEY 序列一样:先记 release_mask 再看写没写成 —— 写失败时器件里那个字节 是未知的,照样走 CLEANUP 把它显式清一遍。返回 false = 写失败,已 finish(IO)。 */ static void finish(act_rail_t *r, act_result_t res, uint32_t now); static bool hr_press(act_rail_t *r, int rail, uint32_t now) { const uint8_t mask = key_out_mask(ACT_KEYS_PWR); r->release_mask = mask; r->hold_ms = ACT_HR_BOOT_KEY_MS; r->presses++; if (out_write(rail, mask, mask) != HAL_OK) { finish(r, ACT_RES_IO, now); /* 电是开着的,只是没按上键 */ return false; } enter(r, ACT_ST_HR_BOOT, now); return true; } static void boot_report(act_rail_t *r, int rail, bool booted) { if (s_boot_done != NULL) { s_boot_done(s_boot_ctx, rail, booted, r->presses, r->last_ua); } } /* 序列结束(成功、超时、出错、被打断走的是同一条路)。 按下去过东西就必须经 CLEANUP,那是「键一定会松开」的唯一保证。 */ static void finish(act_rail_t *r, act_result_t res, uint32_t now) { r->vbus_back = false; /* 只有 hr_ok() 会接回 VBUS;别的结局一律让它断着 */ r->result = res; r->done++; if (r->release_mask != 0) { enter(r, ACT_ST_CLEANUP, now); } else { r->seq = ACT_SEQ_NONE; enter(r, ACT_ST_IDLE, now); } } /* HR 成功收尾:开始时断过 VBUS 的,排一个 ACT_HR_VBUS_BACK_MS 之后的「接回」 (t_actuator_step 里做,那时执行器已空闲)。 */ static void hr_ok(act_rail_t *r, int rail, uint32_t now) { (void)rail; if (r->vbus_back) { r->vbus_pending = true; r->vbus_at = now + ACT_HR_VBUS_BACK_MS; } finish(r, ACT_RES_OK, now); } /* 到点了就接回 VBUS。返回下次最晚多久再来。 这一路被关了电(安全判据 / 拔刀片)就作废;写失败下一拍再试(影子如实留在 off)。 */ static uint32_t vbus_back_step(act_rail_t *r, int rail, uint32_t now) { if (!r->vbus_pending || r->state != ACT_ST_IDLE) { return ACT_IDLE_WAIT_MS; } const int32_t left = (int32_t)(r->vbus_at - now); if (left > 0) { return (uint32_t)left; } if (!rail_efuse_is_on(rail)) { r->vbus_pending = false; return ACT_IDLE_WAIT_MS; } if (slot_vbus_enable(rail, true) == HAL_OK) { r->vbus_pending = false; } return ACT_IDLE_WAIT_MS; } /* ---- 状态机本体:一个状态一段,返回「还要多久再来」 ---------------------- */ static uint32_t state_step(act_rail_t *r, int rail, uint32_t now) { const uint32_t el = now - r->t0_ms; /* 无符号减法自然回绕 */ switch (r->state) { case ACT_ST_IDLE: return ACT_IDLE_WAIT_MS; case ACT_ST_HR_OFF: if (el < ACT_HR_OFF_MS) { return ACT_HR_OFF_MS - el; } /* 放电这时早就由硬件做完了(τ 22 ms,5τ ≈ 110 ms),固件什么也不用管。 */ if (rail_efuse_enable(rail, true) != HAL_OK) { /* 开不回来是安全关键的失败:这一路现在是关着的。记 IO,让上层看见。 */ finish(r, ACT_RES_IO, now); return ACT_CLEANUP_RETRY_MS; } enter(r, ACT_ST_HR_RAMP, now); return ACT_HR_RAMP_MS; case ACT_ST_HR_RAMP: /* tON 5.1 ms + 缓启动斜坡 11.5 ms。这段时间里 PG 本来就不该置位, 所以**根本不看它** —— 提前去看只会看到「还没好」。 */ if (el < ACT_HR_RAMP_MS) { return ACT_HR_RAMP_MS - el; } enter(r, ACT_ST_HR_PG, now); return ACT_HR_PG_POLL_MS; case ACT_ST_HR_PG: { bool pg = false; if (pg_now(rail, &pg) && pg) { /* 电好了 ⇒ 长按电源键开机(ACT_HR_BOOT_KEY_MS 的注释)。 */ return hr_press(r, rail, now) ? ACT_HR_BOOT_KEY_MS : ACT_CLEANUP_RETRY_MS; } if (el >= ACT_HR_PG_TIMEOUT_MS) { /* ⚠️ 电**继续开着**。见 ACT_RES_NO_PG 的注释:把「看不见」当成 「坏了」去断电,等于一片 PCA9555 失联就让五台手机一起停机。 */ finish(r, ACT_RES_NO_PG, now); return ACT_IDLE_WAIT_MS; } const uint32_t left = ACT_HR_PG_TIMEOUT_MS - el; return left < ACT_HR_PG_POLL_MS ? left : ACT_HR_PG_POLL_MS; } case ACT_ST_HR_BOOT: if (el < r->hold_ms) { return r->hold_ms - el; } if (s_ua == NULL) { hr_ok(r, rail, now); /* 不校验(0.1.1 行为):CLEANUP 松键 */ return ACT_CLEANUP_RETRY_MS; } /* 要校验:先在这里松键(松不掉就走 CLEANUP 的「不放弃」,不再校验)。 */ if (out_write(rail, r->release_mask, 0) != HAL_OK) { finish(r, ACT_RES_IO, now); return ACT_CLEANUP_RETRY_MS; } r->release_mask = 0; r->good_n = 0; r->ua_seen = false; enter(r, ACT_ST_HR_VERIFY, now); return ACT_HR_VERIFY_DELAY_MS; case ACT_ST_HR_VERIFY: { if (el < ACT_HR_VERIFY_DELAY_MS) { return ACT_HR_VERIFY_DELAY_MS - el; } int32_t ua = 0; if (ua_now(rail, &ua)) { r->ua_seen = true; r->last_ua = ua; r->good_n = ua >= ACT_HR_BOOT_UA_MIN ? (uint8_t)(r->good_n + 1) : 0; if (r->good_n >= ACT_HR_VERIFY_GOOD_N) { boot_report(r, rail, true); hr_ok(r, rail, now); return ACT_IDLE_WAIT_MS; } } if (el < ACT_HR_VERIFY_DELAY_MS + ACT_HR_VERIFY_WINDOW_MS) { const uint32_t left = ACT_HR_VERIFY_DELAY_MS + ACT_HR_VERIFY_WINDOW_MS - el; return left < ACT_HR_VERIFY_POLL_MS ? left : ACT_HR_VERIFY_POLL_MS; } if (!r->ua_seen) { /* 整个窗口都读不到电流:判不了,**不重按**(ACT_HR_BOOT_TRIES 那段注释)。 */ hr_ok(r, rail, now); return ACT_IDLE_WAIT_MS; } if (r->presses < ACT_HR_BOOT_TRIES) { return hr_press(r, rail, now) ? ACT_HR_BOOT_KEY_MS : ACT_CLEANUP_RETRY_MS; } /* ⚠️ 电**继续开着**:关掉也不会让弹片压上,反而丢了人工到场前的现场。 */ boot_report(r, rail, false); finish(r, ACT_RES_NO_BOOT, now); return ACT_IDLE_WAIT_MS; } case ACT_ST_KEY_HOLD: if (el < r->hold_ms) { return r->hold_ms - el; } finish(r, ACT_RES_OK, now); /* release_mask 非 0 ⇒ 一定进 CLEANUP 松键 */ return ACT_CLEANUP_RETRY_MS; case ACT_ST_EDL_SHORT: /* SW-6:先短接、再插 USB。这两句的先后就是这条需求的全部内容。 */ if (el < ACT_EDL_SHORT_MS) { return ACT_EDL_SHORT_MS - el; } if (slot_vbus_enable(rail, true) != HAL_OK) { finish(r, ACT_RES_IO, now); /* CLEANUP 会把 EDL_EN 清掉 */ return ACT_CLEANUP_RETRY_MS; } enter(r, ACT_ST_EDL_ENUM, now); return ACT_EDL_ENUM_MS; case ACT_ST_EDL_ENUM: if (el < ACT_EDL_ENUM_MS) { return ACT_EDL_ENUM_MS - el; } /* `EDL_EN=0`(收尾那一步)。**VBUS 留着不动** —— 手机现在正挂在 9008 上。 */ finish(r, ACT_RES_OK, now); return ACT_CLEANUP_RETRY_MS; case ACT_ST_CLEANUP: if (out_write(rail, r->release_mask, 0) == HAL_OK) { r->release_mask = 0; r->seq = ACT_SEQ_NONE; enter(r, ACT_ST_IDLE, now); return ACT_IDLE_WAIT_MS; } /* ⚠️ **不放弃。** 放弃 = 电源键留在按下状态,手机再也起不来, 而五路电压电流看上去一切正常。这一路一直算 busy,重试次数报出去。 */ r->cleanup_retries++; return ACT_CLEANUP_RETRY_MS; } assert(0 && "unknown state"); return ACT_IDLE_WAIT_MS; } /* 换了状态就当场再走一步:CLEANUP 该立刻试一次,不该先睡 5 ms。 上限 8 是防呆 —— 状态机里任何一个环都会在这里被截住,而不是把任务钉死。 */ static uint32_t rail_step(int rail, uint32_t now) { act_rail_t *r = at(rail); for (int guard = 0; guard < 8; guard++) { const act_state_t before = r->state; const uint32_t wait = state_step(r, rail, now); if (r->state == before) { return wait; } } return ACT_CLEANUP_RETRY_MS; } /* ---- 接口 ---------------------------------------------------------------- */ void t_actuator_init(act_pg_fn pg, void *ctx) { memset(s_rail, 0, sizeof(s_rail)); s_pg = pg; s_pg_ctx = ctx; s_ua = NULL; s_boot_done = NULL; s_boot_ctx = NULL; s_inited = true; } void t_actuator_set_boot_check(act_ua_fn ua, act_boot_fn done, void *ctx) { s_ua = ua; s_boot_done = done; s_boot_ctx = ctx; } hal_err_t t_actuator_hard_reset(int rail) { if (!s_inited) { return HAL_ERR_STATE; } if (!rail_valid(rail)) { return HAL_ERR_ARG; } act_rail_t *r = at(rail); if (r->state != ACT_ST_IDLE) { return HAL_ERR_STATE; /* §10.6:不可重入。要抢占先 abort,写成显式的 */ } /* 防御性松开这一路的三个键:断电重启期间有键被按着,手机会开进 Recovery / Fastboot 而不是正常起来。正常情况下这里一定已经是 0(有序列在跑的话 上面那一句就把请求挡了),所以影子不变 ⇒ 不脏 ⇒ 一个时钟都不会发。 只有上一次收尾没落地时才真的发一帧。 */ const uint8_t km = pca9555_key_mask(); if ((slot_shadow(rail) & km) != 0) { (void)out_write(rail, km, 0); /* 失败也照断电:断电永远比不断电安全 */ } /* 0.1.11(2026-09-28 板 B):VBUS 通着就**先断**,开机校验通过后再接回(hr_ok)。 不断的三个后果,现场全见过: · 手机靠 USB 撑着,根本没断电 —— 硬重启是假的; · 手机吃 USB 的电,直供电流 ≈ 0 ⇒ 开机校验判「没开起来」、再按两下电源键; · 断电那一刻 USB 还在 ⇒ 被当成「插充电器开机」,进关机充电模式,一拔 USB 就关机。 断不掉也照样断电(断电永远比不断电安全),只是事后不接回。 */ r->vbus_back = false; r->vbus_pending = false; /* 上一次硬重启排的接回作废 */ const bool had_vbus = slot_vbus_is_on(rail); if (had_vbus && slot_vbus_enable(rail, false) == HAL_OK) { r->vbus_back = true; } const hal_err_t err = rail_efuse_enable(rail, false); if (err != HAL_OK) { if (r->vbus_back) { (void)slot_vbus_enable(rail, true); /* 序列没开始:恢复原样 */ r->vbus_back = false; } return err; /* 序列没开始 */ } r->seq = ACT_SEQ_HARD_RESET; r->release_mask = 0; /* 断电这一段没按任何东西;开机那一下在 HR_PG 里才记 */ r->presses = 0; r->last_ua = 0; r->result = ACT_RES_NONE; enter(r, ACT_ST_HR_OFF, hal_time_now_ms()); return HAL_OK; } hal_err_t t_actuator_key(int rail, uint32_t keys, uint32_t hold_ms) { if (!s_inited) { return HAL_ERR_STATE; } if (!rail_valid(rail) || keys == 0 || (keys & ~(uint32_t)ACT_KEYS_ALL) != 0) { return HAL_ERR_ARG; } if (hold_ms == 0 || hold_ms > ACT_KEY_MAX_MS) { return HAL_ERR_ARG; /* 拦手误,不是保护 */ } act_rail_t *r = at(rail); if (r->state != ACT_ST_IDLE) { return HAL_ERR_STATE; } const uint8_t mask = key_out_mask(keys); /* 组合键的两位在**同一次写**里 ⇒ 输出锁存器一次更新。 分两次写会先单按一个键(§10.6)。 */ const hal_err_t err = out_write(rail, mask, mask); r->seq = ACT_SEQ_KEY; r->keys = keys; r->hold_ms = hold_ms; r->result = ACT_RES_NONE; r->release_mask = mask; if (err != HAL_OK) { /* 写失败时驱动**不更新影子**,但器件里那个字节的状态是未知的 (事务可能发出去一半)。所以照样进 CLEANUP 把这几位显式清一遍 —— 「按下去的东西一定要松开」这条规矩不看我们以为发生了什么。 */ finish(r, ACT_RES_IO, hal_time_now_ms()); return err; } enter(r, ACT_ST_KEY_HOLD, hal_time_now_ms()); return HAL_OK; } hal_err_t t_actuator_edl(int rail) { if (!s_inited) { return HAL_ERR_STATE; } if (!rail_valid(rail)) { return HAL_ERR_ARG; } act_rail_t *r = at(rail); if (r->state != ACT_ST_IDLE) { return HAL_ERR_STATE; } r->vbus_pending = false; /* EDL 自己管 VBUS */ const uint8_t mask = (uint8_t)(1u << PCA9555_BIT_EDL_EN); const hal_err_t err = out_write(rail, mask, mask); r->seq = ACT_SEQ_EDL; r->result = ACT_RES_NONE; r->release_mask = mask; if (err != HAL_OK) { finish(r, ACT_RES_IO, hal_time_now_ms()); return err; } enter(r, ACT_ST_EDL_SHORT, hal_time_now_ms()); return HAL_OK; } void t_actuator_abort(int rail) { if (!s_inited || !rail_valid(rail)) { return; } act_rail_t *r = at(rail); r->vbus_pending = false; /* 被打断的路(多半是安全判据关了电)不再接回 VBUS */ if (r->state == ACT_ST_IDLE || r->state == ACT_ST_CLEANUP) { return; /* CLEANUP 已经在收尾了,别把它的结果覆盖掉 */ } finish(r, ACT_RES_ABORTED, hal_time_now_ms()); (void)rail_step(rail, hal_time_now_ms()); /* 立刻试一次收尾,别等下一拍 */ } void t_actuator_cancel_vbus_back(int rail) { if (s_inited && rail_valid(rail)) { at(rail)->vbus_pending = false; } } bool t_actuator_vbus_back_pending(int rail) { return s_inited && rail_valid(rail) && at(rail)->vbus_pending; } uint32_t t_actuator_step(void) { if (!s_inited) { return ACT_IDLE_WAIT_MS; } const uint32_t now = hal_time_now_ms(); uint32_t wait = ACT_IDLE_WAIT_MS; for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) { uint32_t w = rail_step(rail, now); const uint32_t v = vbus_back_step(at(rail), rail, now); if (v < w) { w = v; } if (w < wait) { wait = w; } } return wait; } /* ---- 观测 ---------------------------------------------------------------- */ bool t_actuator_busy(int rail) { return rail_valid(rail) && at(rail)->state != ACT_ST_IDLE; } act_state_t t_actuator_state(int rail) { return at(rail)->state; } act_seq_t t_actuator_seq(int rail) { return at(rail)->seq; } act_result_t t_actuator_last_result(int rail) { return at(rail)->result; } uint32_t t_actuator_done(int rail) { return at(rail)->done; } uint32_t t_actuator_cleanup_retries(int rail) { return at(rail)->cleanup_retries; } const char *t_actuator_state_str(act_state_t st) { switch (st) { case ACT_ST_IDLE: return "idle"; case ACT_ST_HR_OFF: return "hr-off"; case ACT_ST_HR_RAMP: return "hr-ramp"; case ACT_ST_HR_PG: return "hr-pg"; case ACT_ST_HR_BOOT: return "hr-boot"; case ACT_ST_HR_VERIFY: return "hr-verify"; case ACT_ST_KEY_HOLD: return "key-hold"; case ACT_ST_EDL_SHORT: return "edl-short"; case ACT_ST_EDL_ENUM: return "edl-enum"; case ACT_ST_CLEANUP: return "cleanup"; } return "?"; } const char *t_actuator_result_str(act_result_t r) { switch (r) { case ACT_RES_NONE: return "-"; case ACT_RES_OK: return "ok"; case ACT_RES_NO_PG: return "no-pg"; case ACT_RES_IO: return "io"; case ACT_RES_ABORTED: return "aborted"; case ACT_RES_NO_BOOT: return "no-boot"; } return "?"; } static void app_fmt(char *buf, size_t len, size_t *off, const char *fmt, ...) { if (*off + 1 >= len) { return; } va_list ap; va_start(ap, fmt); const int k = vsnprintf(buf + *off, len - *off, fmt, ap); va_end(ap); if (k < 0) { return; } *off += (size_t)k; if (*off >= len) { *off = len - 1; /* 截断了 */ } } int t_actuator_format(char *buf, size_t len) { assert(buf != NULL && len > 0); size_t off = 0; uint32_t done = 0; uint32_t retries = 0; app_fmt(buf, len, &off, "t_actuator:"); for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) { const act_rail_t *r = at(rail); app_fmt(buf, len, &off, " ch%d=%s", rail, t_actuator_state_str(r->state)); done += r->done; retries += r->cleanup_retries; } app_fmt(buf, len, &off, " done=%u retry=%u", (unsigned)done, (unsigned)retries); return (int)off; }