文件
phonerail/firmware/main/t_actuator.c
T
zhou yang和Claude Opus 5 b3206005ab 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]>
2026-09-07 08:08:28 +08:00

419 行
13 KiB
C
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
/* 三个执行器状态机 + 互斥。契约、时间常量的出处、三条规矩都在 t_actuator.h。 */
#include "t_actuator.h"
#include <assert.h>
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#include "hal_lock.h"
#include "hal_time.h"
#include "rail_cfg.h"
#include "sr595.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;
uint32_t release_mask; /* 收尾时要在链上清掉的位。0 = 这个序列没按下任何东西 */
act_result_t result;
uint32_t done;
uint32_t cleanup_retries;
} act_rail_t;
static act_rail_t s_rail[PHONERAIL_RAIL_COUNT];
static act_pg_fn s_pg;
static void *s_pg_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];
}
/* 逻辑键位 → 链上位号。查 `sr595_key_bit()`,**不算**(sr595.h 的禁令)。 */
static uint32_t key_chain_mask(int rail, uint32_t keys)
{
uint32_t m = 0;
for (int k = 0; k < SR595_KEY_COUNT; k++) {
if ((keys & ((uint32_t)1u << k)) != 0) {
m |= (uint32_t)1u << sr595_key_bit(rail, (sr595_key_t)k);
}
}
return m;
}
/* 动 24 位链的**唯一**入口:拿 595 那把锁 → 改影子 + 整帧发下去 → 放锁。
一帧原子(RCLK 之前输出不变)⇒ 组合键的两位天然落在同一帧里。
⚠️ 这里绝不会去拿 I²C 那把锁,两把锁因此永远不会嵌套(hal_lock.h)。 */
static hal_err_t chain_write(uint32_t mask, uint32_t value)
{
hal_err_t err = hal_lock_take(HAL_LOCK_SR595, ACT_LOCK_TIMEOUT_MS);
if (err != HAL_OK) {
return err; /* 影子没动过,下一拍重来 */
}
err = sr595_write(mask, value);
hal_lock_give(HAL_LOCK_SR595);
return err;
}
static bool pg_now(int rail, bool *pg)
{
if (s_pg == NULL) {
return false; /* 「读不到」,不是「没好」 */
}
return s_pg(s_pg_ctx, rail, pg);
}
static void enter(act_rail_t *r, act_state_t st, uint32_t now)
{
r->state = st;
r->t0_ms = now;
}
/* 序列结束(成功、超时、出错、被打断走的是同一条路)。
按下去过东西就必须经 CLEANUP,那是「键一定会松开」的唯一保证。 */
static void finish(act_rail_t *r, act_result_t res, uint32_t now)
{
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);
}
}
/* ---- 状态机本体:一个状态一段,返回「还要多久再来」 ---------------------- */
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) {
finish(r, ACT_RES_OK, now);
return ACT_IDLE_WAIT_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_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 (rail_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 (chain_write(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_inited = true;
}
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 uint32_t km = sr595_key_mask(rail);
if ((sr595_shadow() & km) != 0) {
(void)chain_write(km, 0); /* 失败也照断电:断电永远比不断电安全 */
}
const hal_err_t err = rail_efuse_enable(rail, false);
if (err != HAL_OK) {
return err; /* 序列没开始 */
}
r->seq = ACT_SEQ_HARD_RESET;
r->release_mask = 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 uint32_t mask = key_chain_mask(rail, keys);
/* 组合键的两位在**同一次 `chain_write()`** 里 ⇒ 同一帧 ⇒ 同一个 RCLK 沿。
分两次写会先单按一个键(§10.6)。 */
const hal_err_t err = chain_write(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) {
/* 影子可能已经改成「按下」了(拿到锁但整帧没发出去)。直接返回错误
就等于把一个「按下」的脏影子留在那儿,谁下一次 flush 谁就替我们把键
按下去 —— 而没有人会来松开它。所以照样进 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;
}
const uint32_t mask = (uint32_t)1u << sr595_edl_bit(rail); /* 查表,不算 */
const hal_err_t err = chain_write(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);
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()); /* 立刻试一次收尾,别等下一拍 */
}
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++) {
const uint32_t w = rail_step(rail, now);
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_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";
}
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;
}