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]>
这个提交包含在:
+14
-13
@@ -1,13 +1,14 @@
|
||||
# ESP-IDF 侧的 main 组件。
|
||||
# app_main.c 是 ESP 专用入口;其余 .c 必须同时在宿主机上编得过
|
||||
# (新增文件时两处都要加:这里 + host/CMakeLists.txt 的 PHONERAIL_CORE_SRCS)。
|
||||
idf_component_register(
|
||||
SRCS "app_main.c"
|
||||
"version.c"
|
||||
"rail.c"
|
||||
"rail_cfg.c"
|
||||
"sched.c"
|
||||
"tasks.c"
|
||||
"t_safety.c"
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES phonerail_hal drv)
|
||||
# ESP-IDF 侧的 main 组件。
|
||||
# app_main.c 是 ESP 专用入口;其余 .c 必须同时在宿主机上编得过
|
||||
# (新增文件时两处都要加:这里 + host/CMakeLists.txt 的 PHONERAIL_CORE_SRCS)。
|
||||
idf_component_register(
|
||||
SRCS "app_main.c"
|
||||
"version.c"
|
||||
"rail.c"
|
||||
"rail_cfg.c"
|
||||
"sched.c"
|
||||
"tasks.c"
|
||||
"t_safety.c"
|
||||
"t_actuator.c"
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES phonerail_hal drv)
|
||||
|
||||
+204
-174
@@ -1,174 +1,204 @@
|
||||
/* ESP32-S3 入口。只在 ESP-IDF 目标里编译,宿主机构建不包含本文件。 */
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "board_pins.h"
|
||||
#include "esp_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "hal_i2c.h"
|
||||
#include "hal_lock.h"
|
||||
#include "hal_time.h"
|
||||
#include "phonerail.h"
|
||||
#include "rail_cfg.h"
|
||||
#include "sched.h"
|
||||
#include "t_safety.h"
|
||||
#include "tasks.h"
|
||||
|
||||
static const char *TAG = "phonerail";
|
||||
|
||||
/* 启动自检(需求 §10.4):五路的地址 / 位号 / 引脚无重复、无越界,
|
||||
而且那张五路表和四个驱动自己的表没有分家。
|
||||
|
||||
⚠️ **不过就 abort,不降级。** 这里查的全是编译进固件的常量表,
|
||||
查不过 = 固件本身写错了,不是硬件坏了 —— §10.10 的降级矩阵管的是后者。
|
||||
带着一张错表跑起来的样子是「命令发给了另一路」,五台手机全在跑、
|
||||
一个错误码都不报,谁也不会去怀疑它。 */
|
||||
static void selftest_or_die(void)
|
||||
{
|
||||
/* 五个任务那张表(§10.3):周期与优先级的相对顺序。同样是编译进去的常量,
|
||||
同样不过就 abort —— 优先级写反了板子照跑,只是巡检会在总线忙时被挤后。 */
|
||||
if (!task_table_ok()) {
|
||||
ESP_LOGE(TAG, "任务表自检失败:周期或优先级与 §10.3 对不上");
|
||||
abort();
|
||||
}
|
||||
|
||||
const uint32_t bad = rail_selftest();
|
||||
if (bad == 0) {
|
||||
ESP_LOGI(TAG, "五路配置表自检通过");
|
||||
return;
|
||||
}
|
||||
for (uint32_t bit = 1; bit != 0; bit <<= 1) {
|
||||
if ((bad & bit) != 0) {
|
||||
ESP_LOGE(TAG, "自检失败:%s (0x%02x)", rail_selftest_str(bit), (unsigned)bit);
|
||||
}
|
||||
}
|
||||
abort();
|
||||
}
|
||||
|
||||
/* ---- 五个任务(需求 §10.3) ---------------------------------------------
|
||||
任务体只在这个文件里,而且都短得能一眼看完 —— 真正的活在 main/ 的其它 .c 里,
|
||||
那些在宿主机上编得过、测得到。**任务体里不许出现判据与时序**:写进死循环里的
|
||||
东西,宿主机一行也跑不到。 */
|
||||
|
||||
static void task_safety(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
static safety_snap_t snap; /* static:不占任务栈,也方便挂调试器看 */
|
||||
t_safety_init(&snap);
|
||||
|
||||
sched_periodic_t tick;
|
||||
sched_periodic_init(&tick, task_desc(TASK_SAFETY)->period_ms);
|
||||
|
||||
char line[128];
|
||||
for (;;) {
|
||||
(void)t_safety_step(&snap); /* 器件读失败不在这里处理:§10.2 失联 ≠ 断电 */
|
||||
|
||||
/* 每 5 s 报一次总线占用。§10.3 手算是「约 1.5 ms / 约 8 %」,
|
||||
首板上电时要拿逻辑分析仪复核这个数(§10.3 末尾那句)。 */
|
||||
if ((snap.cycles % 250u) == 0u) {
|
||||
t_safety_format(&snap, line, sizeof(line));
|
||||
ESP_LOGI(TAG, "%s (预算 %u us, 过载 %u, 最迟 %u ms)", line,
|
||||
(unsigned)t_safety_budget_us(), (unsigned)tick.overruns,
|
||||
(unsigned)tick.worst_late_ms);
|
||||
}
|
||||
sched_periodic_wait(&tick);
|
||||
}
|
||||
}
|
||||
|
||||
/* 下面四个是骨架:周期与优先级已经按 §10.3 定好,任务体等各自的包。
|
||||
现在就把它们建起来,是为了让 A9~A16 只需要往里填,不用再动启动流程。 */
|
||||
|
||||
static void task_thermal(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
sched_periodic_t tick;
|
||||
sched_periodic_init(&tick, task_desc(TASK_THERMAL)->period_ms);
|
||||
for (;;) {
|
||||
/* A16:两路 NTC + 载板 LM75 → 风扇 PWM 闭环(TR-6 的 TACH 采样窗口)。 */
|
||||
sched_periodic_wait(&tick);
|
||||
}
|
||||
}
|
||||
|
||||
static void task_actuator(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
for (;;) {
|
||||
/* A9:三个状态机 + 互斥 + 超时兜底。队列在那一包里建。 */
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
}
|
||||
}
|
||||
|
||||
static void task_link(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
for (;;) {
|
||||
/* A12~A14:命令处理器 + 两条传输链路。 */
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
}
|
||||
}
|
||||
|
||||
static void task_log(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
for (;;) {
|
||||
/* A11:事件环形缓冲落盘。 */
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
}
|
||||
}
|
||||
|
||||
static void spawn(task_id_t id, TaskFunction_t fn)
|
||||
{
|
||||
const task_desc_t *d = task_desc(id);
|
||||
/* ⚠️ ESP-IDF 的 xTaskCreate 收的 stack_depth 是**字节**(原版 FreeRTOS 是字)。 */
|
||||
if (xTaskCreate(fn, d->name, d->stack_bytes, NULL, d->prio, NULL) != pdPASS) {
|
||||
ESP_LOGE(TAG, "建任务失败:%s", d->name);
|
||||
abort();
|
||||
}
|
||||
}
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "PhoneRail %s boot, %d rails, t=%u ms", phonerail_version(),
|
||||
PHONERAIL_RAIL_COUNT, (unsigned)hal_time_now_ms());
|
||||
|
||||
selftest_or_die();
|
||||
|
||||
/* 十根 EN 线配成输出并全部拉低。**先于一切外设**:DAC / eFuse 一上电就有
|
||||
输出,这两根线这时必须已经确定是低的。 */
|
||||
hal_err_t err = rail_en_init();
|
||||
if (err != HAL_OK) {
|
||||
ESP_LOGE(TAG, "EN 引脚初始化失败:%s", hal_err_str(err));
|
||||
abort();
|
||||
}
|
||||
ESP_LOGI(TAG, "五路输出与 VBUS 全部关闭");
|
||||
|
||||
/* 两把锁必须早于任何一个用总线的任务。 */
|
||||
(void)hal_lock_init();
|
||||
|
||||
const hal_i2c_cfg_t i2c = {
|
||||
.sda_gpio = BOARD_PIN_I2C_SDA,
|
||||
.scl_gpio = BOARD_PIN_I2C_SCL,
|
||||
.freq_hz = 400000,
|
||||
};
|
||||
err = hal_i2c_init(&i2c);
|
||||
if (err != HAL_OK) {
|
||||
ESP_LOGE(TAG, "I²C 初始化失败:%s", hal_err_str(err));
|
||||
abort();
|
||||
}
|
||||
|
||||
/* ⚠️ 器件初始化(INA226 的 Config/Calibration、DAC、PCA9555 方向、595 安全值)
|
||||
与**降级矩阵**是 A15 的启动顺序那一包。在那之前 t_safety 读回来的电流恒为 0
|
||||
(Calibration 还没写),这是预期的,不是故障。 */
|
||||
|
||||
spawn(TASK_SAFETY, task_safety);
|
||||
spawn(TASK_THERMAL, task_thermal);
|
||||
spawn(TASK_ACTUATOR, task_actuator);
|
||||
spawn(TASK_LINK, task_link);
|
||||
spawn(TASK_LOG, task_log);
|
||||
|
||||
ESP_LOGI(TAG, "五个任务已启动(t_safety %u ms / t_thermal %u ms),"
|
||||
"I²C 预算 %u us 每周期",
|
||||
(unsigned)task_desc(TASK_SAFETY)->period_ms,
|
||||
(unsigned)task_desc(TASK_THERMAL)->period_ms,
|
||||
(unsigned)t_safety_budget_us());
|
||||
}
|
||||
/* ESP32-S3 入口。只在 ESP-IDF 目标里编译,宿主机构建不包含本文件。 */
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "board_pins.h"
|
||||
#include "esp_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "hal_i2c.h"
|
||||
#include "hal_lock.h"
|
||||
#include "hal_time.h"
|
||||
#include "phonerail.h"
|
||||
#include "rail_cfg.h"
|
||||
#include "sched.h"
|
||||
#include "t_actuator.h"
|
||||
#include "t_safety.h"
|
||||
#include "tasks.h"
|
||||
|
||||
static const char *TAG = "phonerail";
|
||||
|
||||
/* 启动自检(需求 §10.4):五路的地址 / 位号 / 引脚无重复、无越界,
|
||||
而且那张五路表和四个驱动自己的表没有分家。
|
||||
|
||||
⚠️ **不过就 abort,不降级。** 这里查的全是编译进固件的常量表,
|
||||
查不过 = 固件本身写错了,不是硬件坏了 —— §10.10 的降级矩阵管的是后者。
|
||||
带着一张错表跑起来的样子是「命令发给了另一路」,五台手机全在跑、
|
||||
一个错误码都不报,谁也不会去怀疑它。 */
|
||||
static void selftest_or_die(void)
|
||||
{
|
||||
/* 五个任务那张表(§10.3):周期与优先级的相对顺序。同样是编译进去的常量,
|
||||
同样不过就 abort —— 优先级写反了板子照跑,只是巡检会在总线忙时被挤后。 */
|
||||
if (!task_table_ok()) {
|
||||
ESP_LOGE(TAG, "任务表自检失败:周期或优先级与 §10.3 对不上");
|
||||
abort();
|
||||
}
|
||||
|
||||
const uint32_t bad = rail_selftest();
|
||||
if (bad == 0) {
|
||||
ESP_LOGI(TAG, "五路配置表自检通过");
|
||||
return;
|
||||
}
|
||||
for (uint32_t bit = 1; bit != 0; bit <<= 1) {
|
||||
if ((bad & bit) != 0) {
|
||||
ESP_LOGE(TAG, "自检失败:%s (0x%02x)", rail_selftest_str(bit), (unsigned)bit);
|
||||
}
|
||||
}
|
||||
abort();
|
||||
}
|
||||
|
||||
/* ---- 五个任务(需求 §10.3) ---------------------------------------------
|
||||
任务体只在这个文件里,而且都短得能一眼看完 —— 真正的活在 main/ 的其它 .c 里,
|
||||
那些在宿主机上编得过、测得到。**任务体里不许出现判据与时序**:写进死循环里的
|
||||
东西,宿主机一行也跑不到。 */
|
||||
|
||||
/* 巡检快照。**文件作用域**(A8 时它还在 task_safety 里):t_actuator 的硬重启
|
||||
序列要等 PG,而 PG 挂在 PCA9555 上、已经被巡检每 20 ms 读一遍了(§10.3)。
|
||||
执行器再去读一次等于凭空多一份总线占用和一份两边不一致的可能。
|
||||
⚠️ 这是跨任务读,但读的只有两个 `bool`(`io_ok` 与一个位),
|
||||
字节级读写在 Xtensa 上是原子的,不值得为它加第三把锁(hal_lock.h:只有两把)。 */
|
||||
static safety_snap_t s_snap;
|
||||
|
||||
/* 喂给 t_actuator 的 PG 取值。返回 false = **这一刻读不到**
|
||||
(PCA9555 失联,或者巡检还没跑过一圈)—— 与「读到了,是低」是两回事。 */
|
||||
static bool act_pg_from_safety(void *ctx, int rail, bool *pg)
|
||||
{
|
||||
(void)ctx;
|
||||
if (!s_snap.io_ok) {
|
||||
return false;
|
||||
}
|
||||
*pg = pca9555_rail_pg(&s_snap.io, rail); /* 极性只在驱动里出现一次 */
|
||||
return true;
|
||||
}
|
||||
|
||||
static void task_safety(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
safety_snap_t *const snapp = &s_snap;
|
||||
t_safety_init(snapp);
|
||||
|
||||
sched_periodic_t tick;
|
||||
sched_periodic_init(&tick, task_desc(TASK_SAFETY)->period_ms);
|
||||
|
||||
char line[128];
|
||||
for (;;) {
|
||||
(void)t_safety_step(snapp); /* 器件读失败不在这里处理:§10.2 失联 ≠ 断电 */
|
||||
|
||||
/* 每 5 s 报一次总线占用。§10.3 手算是「约 1.5 ms / 约 8 %」,
|
||||
首板上电时要拿逻辑分析仪复核这个数(§10.3 末尾那句)。 */
|
||||
if ((snapp->cycles % 250u) == 0u) {
|
||||
t_safety_format(snapp, line, sizeof(line));
|
||||
ESP_LOGI(TAG, "%s (预算 %u us, 过载 %u, 最迟 %u ms)", line,
|
||||
(unsigned)t_safety_budget_us(), (unsigned)tick.overruns,
|
||||
(unsigned)tick.worst_late_ms);
|
||||
}
|
||||
sched_periodic_wait(&tick);
|
||||
}
|
||||
}
|
||||
|
||||
/* 下面四个是骨架:周期与优先级已经按 §10.3 定好,任务体等各自的包。
|
||||
现在就把它们建起来,是为了让 A9~A16 只需要往里填,不用再动启动流程。 */
|
||||
|
||||
static void task_thermal(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
sched_periodic_t tick;
|
||||
sched_periodic_init(&tick, task_desc(TASK_THERMAL)->period_ms);
|
||||
for (;;) {
|
||||
/* A16:两路 NTC + 载板 LM75 → 风扇 PWM 闭环(TR-6 的 TACH 采样窗口)。 */
|
||||
sched_periodic_wait(&tick);
|
||||
}
|
||||
}
|
||||
|
||||
/* A9 的三个状态机。任务体还是三行:推一步 / 拿它算出来的下一个截止时刻 / 睡到那时。
|
||||
判据与时序全在 `t_actuator_step()` 里,那份在宿主机上跑得到。 */
|
||||
static void task_actuator(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
for (;;) {
|
||||
/* A12 会把这里换成「带超时地等命令队列」:超时值就是这个返回值。 */
|
||||
const TickType_t t = pdMS_TO_TICKS(t_actuator_step());
|
||||
/* ⚠️ `CONFIG_FREERTOS_HZ=100` ⇒ 一个 tick 是 10 ms,`pdMS_TO_TICKS(5)`
|
||||
是 **0**,而 `vTaskDelay(0)` 只是让一次 CPU —— 那会让这个中优先级任务
|
||||
在收尾重试时空转,把 t_link / t_log 饿死。至少睡一个 tick。
|
||||
早醒没关系:每个状态都是拿绝对时刻算的,早来就再返回一个剩余时间。 */
|
||||
vTaskDelay(t == 0 ? 1 : t);
|
||||
}
|
||||
}
|
||||
|
||||
static void task_link(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
for (;;) {
|
||||
/* A12~A14:命令处理器 + 两条传输链路。 */
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
}
|
||||
}
|
||||
|
||||
static void task_log(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
for (;;) {
|
||||
/* A11:事件环形缓冲落盘。 */
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
}
|
||||
}
|
||||
|
||||
static void spawn(task_id_t id, TaskFunction_t fn)
|
||||
{
|
||||
const task_desc_t *d = task_desc(id);
|
||||
/* ⚠️ ESP-IDF 的 xTaskCreate 收的 stack_depth 是**字节**(原版 FreeRTOS 是字)。 */
|
||||
if (xTaskCreate(fn, d->name, d->stack_bytes, NULL, d->prio, NULL) != pdPASS) {
|
||||
ESP_LOGE(TAG, "建任务失败:%s", d->name);
|
||||
abort();
|
||||
}
|
||||
}
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "PhoneRail %s boot, %d rails, t=%u ms", phonerail_version(),
|
||||
PHONERAIL_RAIL_COUNT, (unsigned)hal_time_now_ms());
|
||||
|
||||
selftest_or_die();
|
||||
|
||||
/* 十根 EN 线配成输出并全部拉低。**先于一切外设**:DAC / eFuse 一上电就有
|
||||
输出,这两根线这时必须已经确定是低的。 */
|
||||
hal_err_t err = rail_en_init();
|
||||
if (err != HAL_OK) {
|
||||
ESP_LOGE(TAG, "EN 引脚初始化失败:%s", hal_err_str(err));
|
||||
abort();
|
||||
}
|
||||
ESP_LOGI(TAG, "五路输出与 VBUS 全部关闭");
|
||||
|
||||
/* 两把锁必须早于任何一个用总线的任务。 */
|
||||
(void)hal_lock_init();
|
||||
|
||||
const hal_i2c_cfg_t i2c = {
|
||||
.sda_gpio = BOARD_PIN_I2C_SDA,
|
||||
.scl_gpio = BOARD_PIN_I2C_SCL,
|
||||
.freq_hz = 400000,
|
||||
};
|
||||
err = hal_i2c_init(&i2c);
|
||||
if (err != HAL_OK) {
|
||||
ESP_LOGE(TAG, "I²C 初始化失败:%s", hal_err_str(err));
|
||||
abort();
|
||||
}
|
||||
|
||||
/* ⚠️ 器件初始化(INA226 的 Config/Calibration、DAC、PCA9555 方向、595 安全值)
|
||||
与**降级矩阵**是 A15 的启动顺序那一包。在那之前 t_safety 读回来的电流恒为 0
|
||||
(Calibration 还没写),这是预期的,不是故障。 */
|
||||
|
||||
/* 执行器:PG 从巡检的快照里取,本任务自己不碰 I²C(t_actuator.h 开头那一段)。 */
|
||||
t_actuator_init(act_pg_from_safety, NULL);
|
||||
|
||||
spawn(TASK_SAFETY, task_safety);
|
||||
spawn(TASK_THERMAL, task_thermal);
|
||||
spawn(TASK_ACTUATOR, task_actuator);
|
||||
spawn(TASK_LINK, task_link);
|
||||
spawn(TASK_LOG, task_log);
|
||||
|
||||
ESP_LOGI(TAG, "五个任务已启动(t_safety %u ms / t_thermal %u ms),"
|
||||
"I²C 预算 %u us 每周期",
|
||||
(unsigned)task_desc(TASK_SAFETY)->period_ms,
|
||||
(unsigned)task_desc(TASK_THERMAL)->period_ms,
|
||||
(unsigned)t_safety_budget_us());
|
||||
}
|
||||
|
||||
+23
-23
@@ -1,23 +1,23 @@
|
||||
/* PhoneRail 固件公共头 —— 与硬件无关,宿主机与 ESP32-S3 上都编得过。 */
|
||||
#ifndef PHONERAIL_H
|
||||
#define PHONERAIL_H
|
||||
|
||||
/* 固件版本。规则:<主>.<次>.<修订>-<最后完成的任务包> */
|
||||
#define PHONERAIL_FW_VERSION "0.1.0-a8"
|
||||
|
||||
/* 通道数。五路直供,见设计文档 §2。 */
|
||||
#define PHONERAIL_RAIL_COUNT 5
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* 返回 PHONERAIL_FW_VERSION。存在的意义是让宿主机测试有一个真的会被链接进去的
|
||||
符号 —— 骨架期用来证明「同一份 main/ 源码两个目标都编得过」。 */
|
||||
const char *phonerail_version(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* PHONERAIL_H */
|
||||
/* PhoneRail 固件公共头 —— 与硬件无关,宿主机与 ESP32-S3 上都编得过。 */
|
||||
#ifndef PHONERAIL_H
|
||||
#define PHONERAIL_H
|
||||
|
||||
/* 固件版本。规则:<主>.<次>.<修订>-<最后完成的任务包> */
|
||||
#define PHONERAIL_FW_VERSION "0.1.0-a9"
|
||||
|
||||
/* 通道数。五路直供,见设计文档 §2。 */
|
||||
#define PHONERAIL_RAIL_COUNT 5
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* 返回 PHONERAIL_FW_VERSION。存在的意义是让宿主机测试有一个真的会被链接进去的
|
||||
符号 —— 骨架期用来证明「同一份 main/ 源码两个目标都编得过」。 */
|
||||
const char *phonerail_version(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* PHONERAIL_H */
|
||||
|
||||
@@ -0,0 +1,418 @@
|
||||
/* 三个执行器状态机 + 互斥。契约、时间常量的出处、三条规矩都在 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;
|
||||
}
|
||||
@@ -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 */
|
||||
在新工单中引用
屏蔽一个用户