A8:任务骨架与调度(5 个任务、两把锁)

需求 §10.3 落地。四件事:

1. 五任务表(tasks.c):周期与优先级写在一张 const 表里,开机自检查的是
   相对顺序(safety 唯一最高、log 唯一最低、thermal 与 actuator 同级),
   外加「巡检周期必须长于 INA226 一帧转换 8.8 ms」。不过就 abort。

2. 节拍器(sched.c):记下一次该醒的绝对时刻,回绕安全(int32_t 差判先后)。
   过载时跳过错过的整周期、保住相位,不补跑。

3. 两把锁(hal_lock):I²C 与 74HC595,禁止嵌套持有 —— 这块板上两者没有交集,
   「拿着 A 去拿 B」永远是写错了。ESP 侧用 mutex 取优先级继承。

4. t_safety 的一个周期:拿锁 → 6 片 INA226 的 V+I + PCA9555 两端口 → 放锁。
   不死等锁(5 ms 超时就跳过这圈),一片读失败不中断整圈、保留上一圈的值。

I²C 占用由 HAL 按线上真实字节数记账(两个目标同一份代码),静态预算与实测
必须相等:一次寄存器读 48 位,一圈 13 次事务 = 1560 µs / 20 ms = 7.8 %,
与 §10.3 手算的「约 1.5 ms / 约 8 %」相符(本节的 46 位没数 STOP 和重复 START)。

sim 侧配套两样:假 I²C 事务真的把虚拟时钟花掉(否则时序测试是空话),
以及可强制的加锁纪律检查(配一条反向测试,防止检查本身失灵)。

11 条 ctest(161 例)全绿,idf.py build 通过;16 个注错实验全部变红。

Co-Authored-By: Claude Opus 5 <[email protected]>
这个提交包含在:
zhou yang
2026-09-07 07:35:39 +08:00
共同撰写人 Claude Opus 5
父节点 45de84ed3c
当前提交 ec31891429
共修改 26 个文件,包含 3340 行新增和 1733 行删除
+174 -50
查看文件
@@ -1,50 +1,174 @@
/* ESP32-S3 入口。只在 ESP-IDF 目标里编译,宿主机构建不包含本文件。 */
#include <stdlib.h>
#include "esp_log.h"
#include "hal_time.h"
#include "phonerail.h"
#include "rail_cfg.h"
static const char *TAG = "phonerail";
/* 启动自检(需求 §10.4):五路的地址 / 位号 / 引脚无重复、无越界,
而且那张五路表和四个驱动自己的表没有分家。
⚠️ **不过就 abort,不降级。** 这里查的全是编译进固件的常量表,
查不过 = 固件本身写错了,不是硬件坏了 —— §10.10 的降级矩阵管的是后者。
带着一张错表跑起来的样子是「命令发给了另一路」,五台手机全在跑、
一个错误码都不报,谁也不会去怀疑它。 */
static void selftest_or_die(void)
{
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();
}
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 一上电就有
输出,这两根线这时必须已经确定是低的。 */
const 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 全部关闭,等待 A8 的任务骨架");
/* 器件初始化与五个任务留到 A8/A15。 */
}
/* 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());
}