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]>
这个提交包含在:
+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());
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户