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]>
这个提交包含在:
+13
-10
@@ -1,10 +1,13 @@
|
||||
# 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"
|
||||
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"
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES phonerail_hal drv)
|
||||
|
||||
+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());
|
||||
}
|
||||
|
||||
+23
-23
@@ -1,23 +1,23 @@
|
||||
/* PhoneRail 固件公共头 —— 与硬件无关,宿主机与 ESP32-S3 上都编得过。 */
|
||||
#ifndef PHONERAIL_H
|
||||
#define PHONERAIL_H
|
||||
|
||||
/* 固件版本。规则:<主>.<次>.<修订>-<最后完成的任务包> */
|
||||
#define PHONERAIL_FW_VERSION "0.1.0-a7"
|
||||
|
||||
/* 通道数。五路直供,见设计文档 §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-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 */
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
#include "sched.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#include "hal_time.h"
|
||||
|
||||
void sched_periodic_init(sched_periodic_t *p, uint32_t period_ms)
|
||||
{
|
||||
assert(p != NULL);
|
||||
assert(period_ms > 0); /* 事件驱动的任务不该走这里 */
|
||||
p->period_ms = period_ms;
|
||||
p->next_ms = hal_time_now_ms() + period_ms;
|
||||
p->cycles = 0;
|
||||
p->overruns = 0;
|
||||
p->worst_late_ms = 0;
|
||||
}
|
||||
|
||||
uint32_t sched_periodic_delay_from(sched_periodic_t *p, uint32_t now_ms)
|
||||
{
|
||||
assert(p != NULL && p->period_ms > 0);
|
||||
p->cycles++;
|
||||
|
||||
/* ⚠️ 一律用有符号差判先后,不要写 `now >= next`。
|
||||
32 位 ms 在 49.7 天回绕,农场是长稳跑的,回绕一定会发生 ——
|
||||
裸比较会在回绕那一刻把「还早着呢」判成「已经迟到了」,
|
||||
现象是巡检突然连着空转 49.7 天份的周期。 */
|
||||
const int32_t late = (int32_t)(now_ms - p->next_ms);
|
||||
|
||||
if (late < 0) {
|
||||
const uint32_t delay = (uint32_t)(-late);
|
||||
p->next_ms += p->period_ms;
|
||||
return delay;
|
||||
}
|
||||
|
||||
/* 到点了活还没干完。跳过错过的整周期、保住相位,见 sched.h。 */
|
||||
p->overruns++;
|
||||
if ((uint32_t)late > p->worst_late_ms) {
|
||||
p->worst_late_ms = (uint32_t)late;
|
||||
}
|
||||
const uint32_t missed = (uint32_t)late / p->period_ms + 1u;
|
||||
p->next_ms += missed * p->period_ms;
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t sched_periodic_wait(sched_periodic_t *p)
|
||||
{
|
||||
const uint32_t delay = sched_periodic_delay_from(p, hal_time_now_ms());
|
||||
if (delay > 0) {
|
||||
hal_time_delay_ms(delay);
|
||||
}
|
||||
return delay;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/* 周期任务的节拍器(需求 §10.3)。
|
||||
|
||||
两个周期任务 —— `t_safety` 20 ms、`t_thermal` 1 s —— 都不能用
|
||||
「干完活睡 20 ms」这种写法:那样周期会变成 `20 ms + 干活时间`,
|
||||
而干活时间本身是变的(I²C 重试、NAK 超时),于是巡检的实际间隔会**漂**,
|
||||
还会跟着总线忙闲一起抖。§10.3 的 20 ms 是**间隔**,不是延时。
|
||||
|
||||
所以这里记的是**下一次该醒的绝对时刻**,每次把它 += 周期,
|
||||
睡「到那个时刻还差多久」。FreeRTOS 的 `vTaskDelayUntil()` 是同一个东西,
|
||||
这里自己写一份的唯一理由是:它得在宿主机上用虚拟时钟跑得起来。
|
||||
|
||||
---- 过载了怎么办 --------------------------------------------------------
|
||||
一个周期没干完就到点了(`sched_periodic_wait()` 算出来该睡 0 ms),
|
||||
**跳过错过的整周期、保住相位**,并计一次 `overruns`。
|
||||
不补跑 —— 补跑的现象是「卡了一下之后连着跑五圈巡检」,把总线瞬间打满、
|
||||
把已经落后的任务推得更后面。巡检读的是**当前**电压电流,补读三个周期前的没有意义。 */
|
||||
#ifndef SCHED_H
|
||||
#define SCHED_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
uint32_t period_ms;
|
||||
uint32_t next_ms; /* 下一次该醒的绝对时刻(32 位 ms,回绕安全) */
|
||||
uint32_t cycles; /* 已经等过几个周期 */
|
||||
uint32_t overruns; /* 其中「到点时活还没干完」的有几个 */
|
||||
uint32_t worst_late_ms; /* 最严重的一次迟到了多久 —— 遥测里报出去 */
|
||||
} sched_periodic_t;
|
||||
|
||||
/* 以「现在」为相位起点。period_ms 为 0 会 abort(那是事件驱动任务,不该用这个)。 */
|
||||
void sched_periodic_init(sched_periodic_t *p, uint32_t period_ms);
|
||||
|
||||
/* 纯计算:now_ms 这一刻还该睡多久,并把下一次的时刻推进。
|
||||
与 `sched_periodic_wait()` 分开是为了能不依赖时钟单独测它 ——
|
||||
回绕、过载、连续过载这三种情况用真时钟很难摆出来。 */
|
||||
uint32_t sched_periodic_delay_from(sched_periodic_t *p, uint32_t now_ms);
|
||||
|
||||
/* 睡到下一个节拍。返回实际睡了多久(0 = 这一圈超时了)。 */
|
||||
uint32_t sched_periodic_wait(sched_periodic_t *p);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* SCHED_H */
|
||||
@@ -0,0 +1,118 @@
|
||||
#include "t_safety.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "hal_i2c_acct.h"
|
||||
#include "hal_lock.h"
|
||||
#include "rail_cfg.h"
|
||||
#include "tasks.h"
|
||||
|
||||
void t_safety_init(safety_snap_t *s)
|
||||
{
|
||||
assert(s != NULL);
|
||||
memset(s, 0, sizeof(*s));
|
||||
/* 游标对齐到「现在」,否则第一圈会把启动期间(器件初始化)的事务算进来。 */
|
||||
s->bus_cursor = hal_i2c_acct_bits();
|
||||
}
|
||||
|
||||
hal_err_t t_safety_step(safety_snap_t *s)
|
||||
{
|
||||
assert(s != NULL);
|
||||
s->cycles++;
|
||||
|
||||
hal_err_t err = hal_lock_take(HAL_LOCK_I2C, T_SAFETY_LOCK_TIMEOUT_MS);
|
||||
if (err != HAL_OK) {
|
||||
/* 规矩 1:不死等。这一圈作废,20 ms 后重来。 */
|
||||
s->skipped++;
|
||||
s->bus_us = hal_i2c_acct_window_us(&s->bus_cursor);
|
||||
return err;
|
||||
}
|
||||
|
||||
hal_err_t first = HAL_OK;
|
||||
|
||||
/* 五路 —— 地址查表,**不许算**(rail_cfg.h 那条禁令)。 */
|
||||
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
|
||||
ina226_vi_t vi;
|
||||
const hal_err_t e = ina226_read_vi(rail_cfg(rail)->ina_addr, &vi);
|
||||
s->rail_ok[rail - 1] = (e == HAL_OK);
|
||||
if (e == HAL_OK) {
|
||||
s->rail[rail - 1] = vi; /* 规矩 2:失败就留着上一圈的值,别清零 */
|
||||
} else {
|
||||
s->i2c_errors++;
|
||||
if (first == HAL_OK) {
|
||||
first = e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 第六片:输入侧。 */
|
||||
{
|
||||
ina226_vi_t vi;
|
||||
const hal_err_t e = ina226_read_vi(INA226_ADDR_INPUT, &vi);
|
||||
s->input_ok = (e == HAL_OK);
|
||||
if (e == HAL_OK) {
|
||||
s->input = vi;
|
||||
} else {
|
||||
s->i2c_errors++;
|
||||
if (first == HAL_OK) {
|
||||
first = e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* PCA9555 的两个输入端口一次事务读完(自增指针)。 */
|
||||
{
|
||||
pca9555_in_t in;
|
||||
const hal_err_t e = pca9555_read_inputs(&in);
|
||||
s->io_ok = (e == HAL_OK);
|
||||
if (e == HAL_OK) {
|
||||
s->io = in;
|
||||
} else {
|
||||
s->i2c_errors++;
|
||||
if (first == HAL_OK) {
|
||||
first = e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hal_lock_give(HAL_LOCK_I2C);
|
||||
|
||||
/* A10 的判据接在这里:越界**直接**写 EFUSE_EN / VBUS_EN(规矩 3),
|
||||
而且要在放锁之后 —— 那两根线是 GPIO 直连,和 I²C 没关系,
|
||||
拿着总线锁去操作引脚只会把别的任务多挡一会儿。 */
|
||||
|
||||
s->bus_us = hal_i2c_acct_window_us(&s->bus_cursor);
|
||||
return first;
|
||||
}
|
||||
|
||||
uint32_t t_safety_budget_us(void)
|
||||
{
|
||||
/* 上面那一圈真正发出去的事务,逐条数:
|
||||
6 片 INA226 × 2 次寄存器读(母线电压 + 电流)
|
||||
1 次 PCA9555 读(一次事务读回两个输入端口)
|
||||
每次都是「写 1 字节寄存器号 + 读 2 字节」的形状。 */
|
||||
const uint32_t txns = INA226_COUNT * 2u + 1u;
|
||||
return (uint32_t)hal_i2c_bits_to_us((uint64_t)txns * hal_i2c_txn_bits(1, 2));
|
||||
}
|
||||
|
||||
uint32_t t_safety_load_permille(uint32_t bus_us, uint32_t period_ms)
|
||||
{
|
||||
if (period_ms == 0) {
|
||||
return 0;
|
||||
}
|
||||
return (uint32_t)(((uint64_t)bus_us * 1000ull) / ((uint64_t)period_ms * 1000ull));
|
||||
}
|
||||
|
||||
int t_safety_format(const safety_snap_t *s, char *buf, size_t len)
|
||||
{
|
||||
assert(s != NULL && buf != NULL && len > 0);
|
||||
const uint32_t period = task_desc(TASK_SAFETY)->period_ms;
|
||||
const uint32_t pm = t_safety_load_permille(s->bus_us, period);
|
||||
return snprintf(buf, len,
|
||||
"t_safety: cycles=%u skipped=%u i2c_err=%u bus=%u us/%u ms load=%u.%u%%",
|
||||
(unsigned)s->cycles, (unsigned)s->skipped, (unsigned)s->i2c_errors,
|
||||
(unsigned)s->bus_us, (unsigned)period, (unsigned)(pm / 10u),
|
||||
(unsigned)(pm % 10u));
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/* `t_safety` —— 20 ms 巡检任务的**一个周期**(需求 §10.3 第一行)。
|
||||
|
||||
本包(A8)只做骨架:拿 I²C 锁 → 读 6 片 INA226 的 V+I → 读 PCA9555 两个输入端口
|
||||
→ 放锁 → 记账。**判据(SW-2 / SW-8 / 过压 / 反灌)是 A10 的事**,这里一条都没有。
|
||||
|
||||
分成「一个周期是个函数」而不是直接写成 `while(1)`,是为了让它在宿主机上跑得起来:
|
||||
宿主机没有 FreeRTOS,任务体是死循环就一行也测不了。ESP 侧的 `while(1)`
|
||||
在 app_main.c 里,只有三行(拿节拍 / 调这个 / 睡到下一拍)。
|
||||
|
||||
---- 三条不能改的规矩 ----------------------------------------------------
|
||||
1. **不许死等锁。** 拿不到就跳过这一周期并计数(`skipped`)。死等的现象是
|
||||
巡检整个停摆,而它是优先级最高的那个任务 —— 停摆了没有任何人会发现。
|
||||
2. **一个器件读失败不能中断这一圈。** 六片里坏一片,剩下五路的判据照常要出。
|
||||
失联 ≠ 断电(§10.2),失联更不等于「别的路也不看了」。
|
||||
3. **越界要断电时直接写 `EFUSE_EN` / `VBUS_EN` 引脚**(§10.3 原话:不经任何队列
|
||||
或抽象层)。所以 A10 的判据要写在这个函数里,不是发个消息给 t_actuator。 */
|
||||
#ifndef T_SAFETY_H
|
||||
#define T_SAFETY_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "hal_err.h"
|
||||
#include "ina226.h"
|
||||
#include "pca9555.h"
|
||||
#include "phonerail.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* 等 I²C 锁最多等这么久。取周期的 1/4:等过头这一周期就废了,
|
||||
而**废一个周期**(20 ms 后重来)比**赖着不走**(把最高优先级任务钉死)好得多。 */
|
||||
#define T_SAFETY_LOCK_TIMEOUT_MS 5
|
||||
|
||||
typedef struct {
|
||||
/* 这一圈读回来的原始量。`ok` 为 false 时对应的值是**上一圈的**,不是 0 ——
|
||||
判据(A10)必须先看 ok 再看值,拿一个没更新的电压去判过压是最难查的那种错。 */
|
||||
ina226_vi_t rail[PHONERAIL_RAIL_COUNT];
|
||||
bool rail_ok[PHONERAIL_RAIL_COUNT];
|
||||
ina226_vi_t input; /* 输入侧 U13(0x43),整板功率预算 */
|
||||
bool input_ok;
|
||||
pca9555_in_t io; /* 五路 FLT# / PGOOD + Buck PG 或 + INA ALERT 或 */
|
||||
bool io_ok;
|
||||
|
||||
uint32_t cycles; /* 跑过几圈(含被跳过的) */
|
||||
uint32_t skipped; /* 其中因为拿不到 I²C 锁而整圈跳过的 */
|
||||
uint32_t i2c_errors; /* 累计器件级失败次数(一圈里可能不止一次) */
|
||||
uint32_t bus_us; /* **上一圈**在 I²C 上真正花掉的时间 */
|
||||
uint64_t bus_cursor; /* 内部:位数游标,别在外面改 */
|
||||
} safety_snap_t;
|
||||
|
||||
void t_safety_init(safety_snap_t *s);
|
||||
hal_err_t t_safety_step(safety_snap_t *s);
|
||||
|
||||
/* 按 §10.3 的算式静态算出来的「一圈该占总线多久」,单位 µs。
|
||||
它和 `s->bus_us` 应当**一模一样** —— 不一样只有两种可能:巡检里多读/少读了东西,
|
||||
或者有别的任务在同一圈里插了事务进来。测试拿这两个数对账。 */
|
||||
uint32_t t_safety_budget_us(void);
|
||||
|
||||
/* 占用率(千分之)。20 ms 周期、1560 µs → 78‰。§10.3 手算的是「约 8 %」。 */
|
||||
uint32_t t_safety_load_permille(uint32_t bus_us, uint32_t period_ms);
|
||||
|
||||
/* 一行人话,给启动日志和遥测用。返回写进去的字符数(不含结尾 0)。 */
|
||||
int t_safety_format(const safety_snap_t *s, char *buf, size_t len);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* T_SAFETY_H */
|
||||
@@ -0,0 +1,77 @@
|
||||
#include "tasks.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "ina226.h"
|
||||
|
||||
/* 优先级的绝对值本身不重要,**相对顺序**才是 §10.3 定的东西。
|
||||
留出空档(6/4/4/3/1)是为了以后插一个任务时不用整体重排 —— 重排的那次
|
||||
一定会有人只改一半。 */
|
||||
#define PRIO_SAFETY 6
|
||||
#define PRIO_THERMAL 4
|
||||
#define PRIO_ACTUATOR 4
|
||||
#define PRIO_LINK 3
|
||||
#define PRIO_LOG 1
|
||||
|
||||
static const task_desc_t s_tasks[TASK_COUNT] = {
|
||||
[TASK_SAFETY] = { "t_safety", 20, PRIO_SAFETY, 4096 },
|
||||
[TASK_THERMAL] = { "t_thermal", 1000, PRIO_THERMAL, 3072 },
|
||||
/* 执行器要跑三个状态机 + 超时兜底,栈给足;link 要收发帧与 JSON,最费。 */
|
||||
[TASK_ACTUATOR] = { "t_actuator", 0, PRIO_ACTUATOR, 4096 },
|
||||
[TASK_LINK] = { "t_link", 0, PRIO_LINK, 6144 },
|
||||
[TASK_LOG] = { "t_log", 0, PRIO_LOG, 3072 },
|
||||
};
|
||||
|
||||
const task_desc_t *task_desc(task_id_t id)
|
||||
{
|
||||
assert(id >= 0 && id < TASK_COUNT);
|
||||
return &s_tasks[id];
|
||||
}
|
||||
|
||||
bool task_table_ok(void)
|
||||
{
|
||||
for (int i = 0; i < TASK_COUNT; i++) {
|
||||
const task_desc_t *a = &s_tasks[i];
|
||||
if (a->name == NULL || a->name[0] == '\0' || a->stack_bytes < 2048) {
|
||||
return false;
|
||||
}
|
||||
for (int j = i + 1; j < TASK_COUNT; j++) {
|
||||
if (strcmp(a->name, s_tasks[j].name) == 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 周期:§10.3 那一列,逐格钉死。 */
|
||||
if (s_tasks[TASK_SAFETY].period_ms != 20 || s_tasks[TASK_THERMAL].period_ms != 1000) {
|
||||
return false;
|
||||
}
|
||||
if (s_tasks[TASK_ACTUATOR].period_ms != 0 || s_tasks[TASK_LINK].period_ms != 0 ||
|
||||
s_tasks[TASK_LOG].period_ms != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* 优先级:t_safety 唯一最高、t_log 唯一最低。 */
|
||||
for (int i = 0; i < TASK_COUNT; i++) {
|
||||
if (i != TASK_SAFETY && s_tasks[i].prio >= s_tasks[TASK_SAFETY].prio) {
|
||||
return false;
|
||||
}
|
||||
if (i != TASK_LOG && s_tasks[i].prio <= s_tasks[TASK_LOG].prio) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (s_tasks[TASK_THERMAL].prio != s_tasks[TASK_ACTUATOR].prio) {
|
||||
return false; /* §10.3 里两个都是「中」 */
|
||||
}
|
||||
if (s_tasks[TASK_LINK].prio >= s_tasks[TASK_THERMAL].prio) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* 巡检周期要长于 INA226 的一帧转换时间(见 tasks.h 的最后一条)。 */
|
||||
if (s_tasks[TASK_SAFETY].period_ms * 1000u <= (uint32_t)INA226_CONV_US) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/* 五个任务的那张表(需求 §10.3)。
|
||||
|
||||
§10.3 定了五个任务、各自的周期与优先级,还有一句最容易被写丢的话:
|
||||
**`t_safety` 越界时直接操作 `EFUSE_EN` / `VBUS_EN`,不经任何队列或抽象层。**
|
||||
这张表在这里的意义不是「方便建任务」,而是让那五行**有一个能被自检查的形状** ——
|
||||
优先级顺序写反了不会报错,只会让巡检在总线忙的时候被挤到几十毫秒之后。
|
||||
|
||||
---- 为什么周期与优先级要合在一张表里 ------------------------------------
|
||||
因为它们是同一个判断的两半:`t_safety` 之所以是最高优先级,正是因为它有一个
|
||||
20 ms 的硬节拍;`t_link` 之所以最低,正是因为它没有节拍(§10.2:失联不断电,
|
||||
上位机慢一点没有任何后果)。分成两处写,改了一半的那天没人看得出来。
|
||||
|
||||
⚠️ 事件驱动的三个任务(actuator / link / log)在本表里 `period_ms == 0`。
|
||||
它们的队列与消息定义在 A9(执行器)/ A12(命令)/ A11(日志)里,本包只搭骨架。 */
|
||||
#ifndef TASKS_H
|
||||
#define TASKS_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
TASK_SAFETY = 0, /* 20 ms,最高:SW-2 / SW-8 判据 + 直接操作 EN 引脚 */
|
||||
TASK_THERMAL, /* 1 s,中:两路 NTC + 载板 LM75 → 风扇 PWM 闭环 */
|
||||
TASK_ACTUATOR, /* 事件驱动,中:断电重启 / 按键序列 / EDL 序列 */
|
||||
TASK_LINK, /* 事件驱动,低:上位机命令与遥测 */
|
||||
TASK_LOG, /* 空闲,最低:事件环形缓冲落盘 */
|
||||
TASK_COUNT,
|
||||
} task_id_t;
|
||||
|
||||
typedef struct {
|
||||
const char *name;
|
||||
uint32_t period_ms; /* 0 = 事件驱动 */
|
||||
int prio; /* FreeRTOS 优先级,越大越高 */
|
||||
uint32_t stack_bytes; /* ESP-IDF 的 xTaskCreate 收的是**字节**,不是字 */
|
||||
} task_desc_t;
|
||||
|
||||
/* id 越界直接 abort。 */
|
||||
const task_desc_t *task_desc(task_id_t id);
|
||||
|
||||
/* 启动自检的一部分(和 `rail_selftest()` 一样:不过就 abort,不降级)。
|
||||
查的是**表本身的形状**,不碰硬件:
|
||||
· 名字非空且不重复;
|
||||
· `t_safety` 是**唯一**的最高优先级,`t_log` 是**唯一**的最低;
|
||||
· thermal 与 actuator 同级(§10.3 都是「中」),link 严格低于它们、高于 log;
|
||||
· 周期与 §10.3 逐格相符(20 ms / 1 s / 三个 0);
|
||||
· `t_safety` 的周期必须长于 INA226 的一帧转换时间,否则每次读到的是同一帧 ——
|
||||
现象是「电流看起来非常稳」,而不是任何一种错误。 */
|
||||
bool task_table_ok(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* TASKS_H */
|
||||
在新工单中引用
屏蔽一个用户