W1 功能主线完成:内存台账三件套(W1-0) + IEC104 上行(B4) + protectTask 板端接线(B5)

W1-0 台账三件套:scripts/ram_baseline.py(四模式)+ ram_baseline.tsv 趋势 +
docs/内存管理/RAM预算台账.md + pre-commit mem-budget 钩子(RW_IRAM1/RW_CCM >92%
即 FAIL + Top-10 附表);审查报告 §5.5 三条闭环。

B4 IEC 60870-5-104 上行(被控站,TCP 2404):App/protocol/iec104/ 三模块
(asdu 编解码 / pts 点表+数据源 vtable / link APCI+FSM,纯逻辑零 I/O)+
App/task/iec104_serv.c(iec104Task,NET_READY 屏障 100ms 分片喂位)。
82 断言 L0.5 全绿 + iec104-unit-test 钩子(第 31 钩子);
设计说明 docs/子系统设计/IEC104上行设计说明.md 全量回填。

B5 protectTask(L3 板端接线):App/task/protect_task.c——20ms 判据节拍
prot_elm_run + L5-4 采样故障闭锁(adc_health 电平位→元件复位)+ L5-1 定值
校验和巡检;新增 prot_elm_request_reset() 收口跨上下文复归(104 YK11 落请求、
eval 头部消费);定值/元件初始化移入 app_main_init(内核前)。TC-PE-010
(prot_elm 28 用例)。

门禁与内存:TASK_WD_PROTECT=8 入 required_mask(产品 0x77→0x1F7、HW 层
0x1F5,看门狗单测 39 断言同步);FreeRTOS 堆 48→56KB(.ioc L1 参数 +
FreeRTOSConfig.h 同步,RAM预算台账 §6 登记配额 48→56KB)。
ZI 144,008→152,204(+8,196 B = 堆 +8KB,B5 扩容);Code=204,348。
stack_report:protectTask 512 words 静态链 872 B/余 57%、iec104Task
640 words 1456 B/余 43% 均 PASS;RW_IRAM1 73.1% / RW_CCM 87.9%(<92%)。

验证:@build 0 错 0 警;ci_local 全绿(31 钩子 + 13 主机侧单测);
doc-links 135 文档 PASS。登记簿 B4/B5 销项、W1 波次完成;保护规划/看门狗
说明/AGENTS(9 任务)/RAM 三文档全部同步。

遗留(验收窗口,非代码空洞):台架第三方 104 主站往返(送检窗口)、
标准源注入动作判据、@test PROT 锚点 + protect 栈水位 + 新堆 min_ever 复测。
This commit is contained in:
2026-10-12 12:36:23 +08:00
parent e4134a4723
commit 90db528032
49 changed files with 4300 additions and 223 deletions
+10 -2
View File
@@ -62,6 +62,10 @@ extern osThreadId_t timeSyncTaskHandle;
extern const osThreadAttr_t timeSyncTask_attributes;
extern osThreadId_t adcTaskHandle;
extern const osThreadAttr_t adcTask_attributes;
extern osThreadId_t iec104TaskHandle;
extern const osThreadAttr_t iec104Task_attributes;
extern osThreadId_t protectTaskHandle;
extern const osThreadAttr_t protectTask_attributes;
/* Private defines -----------------------------------------------------------*/
/* 栈水位观测节奏:首次报告须晚于 bring-up 深链(mount/mkfs/探针/cfg/syslog),
@@ -69,9 +73,11 @@ extern const osThreadAttr_t adcTask_attributes;
#define STACK_PROBE_FIRST_MS 5000u
#define STACK_PROBE_PERIOD_MS 2000u
/* S11(2026-10-03):堆低水位告警阈值(B)。
* 依据:2026-10-03 上板两次开机的 min_ever 实测下限 3,712 B(configTOTAL_HEAP_SIZE
* 依据:2026-10-03 上板两次开机的 min_ever 实测下限 3,712 B(当时 configTOTAL_HEAP_SIZE
* = 48KB)——低于此值说明堆余量 <8%,新增任务/缓冲随时可能分配失败(隐性风险)。
* 阈值取 3,072 B 留出运行间 ~600B 波动余量,避免把正常波动误报为故障。 */
* 阈值取 3,072 B 留出运行间 ~600B 波动余量,避免把正常波动误报为故障。
* 2026-10-12(B5):堆扩至 56KB(.ioc configTOTAL_HEAP_SIZE=57344,CCM 腾给
* protectTask/iec104Task 栈),阈值**不随堆缩放**——它是绝对字节安全线。 */
#define HEAP_LOW_WATER_B 3072u
/* E-31 自愈策略(2026-09-21 审计 P0 修复):启动期存储挂载失败后,
* 主循环按 2s 周期重跑完整 bring-up 的次数上限;耗尽则维持无存储降级
@@ -93,6 +99,8 @@ static const stack_probe_task_t s_stack_probe[] = {
{ "ui", &uiTaskHandle, &uiTask_attributes },
{ "timesync", &timeSyncTaskHandle, &timeSyncTask_attributes },
{ "adc", &adcTaskHandle, &adcTask_attributes },
{ "iec104", &iec104TaskHandle, &iec104Task_attributes },
{ "protect", &protectTaskHandle, &protectTask_attributes },
};
#define STACK_PROBE_COUNT (sizeof(s_stack_probe) / sizeof(s_stack_probe[0]))
+295
View File
@@ -0,0 +1,295 @@
/*
* 模块名称:IEC 60870-5-104 被控站服务(板端胶水任务)
* 模块功能:2404 监听 + 单连接 socket 收发 + 把字节流喂给 iec104_link 纯逻辑
* 层;对时经 sys_clock_set_unix 落墙钟;数据源走 iec104_pts 默认源
* (phasor / prot_elm / prot_set)。协议语义全部在 App/protocol/iec104,
* 本文件只做 I/O 编排(A-4 约束 1)。
* 线程约定:iec104Task 独占(NET_READY 屏障后循环,100ms 节拍 + 看门狗心跳)。
* 创建日期:2026-10-12(B4)
*/
#include "iec104_serv.h"
#define DBG_LINE_BUF_MAX 128U /* 深链模块日志缓冲收紧(time_sync 先例) */
#define DBG_TAG "[I104]"
#include "iec104_link.h"
#include "iec104_pts.h"
#include "net_socket.h"
#include "sys_clock.h"
#include "sys_events.h"
#include "task_watchdog.h"
#include "dbg_log.h"
#include "cmsis_os2.h"
#include <string.h>
/* 服务周期:COV 延迟 ≤100ms(远小于 SOE 5000ms 验收线);心跳间隔同值 */
#define IEC104_POLL_MS 100u
/* 接收搬运缓冲(静态,不入栈;link 层内部另有 512B 重组缓冲) */
#define IEC104_RX_SZ 128u
/* 兜底节拍:门控判"无数据"时每 32 轮无条件收一次(ui_net_sink RM 先例,
* 防 RECV_OK 中断丢失把数据永久藏住);须为 2 的幂 */
#define IEC104_LIVENESS_DIV 32u
/* 连续发送失败阈值:同 ui_net_sink E-04(短写=对端收坏帧) */
#define IEC104_SEND_FAIL_MAX 3u
/* ---------------- 服务状态(iec104Task 独占) ---------------- */
static iec104_link_t s_link;
static int s_listen = -1;
static int s_client = -1; /* 单连接模型:MCU 上即 s_listen */
static uint32_t s_gen; /* 网络代次(重建自愈,ui_net_sink 先例) */
static uint8_t s_rx[IEC104_RX_SZ];
static uint8_t s_liveness_ctr;
static uint8_t s_send_fail_run;
static uint8_t s_conn_dead; /* 发送连败 → 本轮 link 调用返回后拆链 */
/* 前向声明(cfg 初始化引用) */
static void s_send_apdu(void *ctx, const uint8_t *apdu, uint16_t len);
static int s_clock_sync(void *ctx, uint64_t epoch_ms);
/* V0.1 公共地址固定 1(点表口径;可配置项挂二期) */
static const iec104_link_cfg_t s_cfg = { 1u, s_send_apdu, s_clock_sync };
/*
* 函数功能:link 层发送回调——经 net_send 串行 RPC 发出一个 APDU
* 入口参数:ctx 未用;apdu/len = 完整 APCI 帧
* 返回值:无
* 限定条件:s_client >= 0(在 link 调用栈内执行,即本任务上下文)
* 函数说明:短写/失败累计(E-04 先例);连续 3 次 → 置 s_conn_dead,
* 由 s_service_link 在 link 调用返回后统一拆链(严禁在回调里拆)
*/
static void s_send_apdu(void *ctx, const uint8_t *apdu, uint16_t len) {
int ret;
(void)ctx;
if (s_client < 0) {
return;
}
ret = net_send(s_client, apdu, (int)len, 0);
if (ret == (int)len) {
s_send_fail_run = 0u;
return;
}
if (s_send_fail_run < 255u) {
s_send_fail_run++;
}
DBG_ERROR("i104 send short: ret=%d/%u run=%u", ret, (unsigned)len,
(unsigned)s_send_fail_run);
if (s_send_fail_run >= IEC104_SEND_FAIL_MAX) {
s_conn_dead = 1u;
}
}
/*
* 函数功能:link 层对时回调——epoch 毫秒落系统墙钟(含 SD2506 RTC 写回)
* 入口参数:ctx 未用;epoch_ms = C_CS 解析出的 UTC 时刻
* 返回值:0 成功 / -1 失败(link 层回 actcon+PN)
* 函数说明:uint32 秒域上限 2106-02-07,越界显式拒绝(CP56 域到 2127)
*/
static int s_clock_sync(void *ctx, uint64_t epoch_ms) {
uint64_t secs = epoch_ms / 1000ULL;
(void)ctx;
if (secs > 0xFFFFFFFFULL) {
return -1;
}
return sys_clock_set_unix((uint32_t)secs);
}
/*
* 函数功能:创建并监听 2404(socket+bind+listen,抄 ui_net_sink bind_listen)
* 返回值:0 成功(s_listen 写回)/ -1 失败
* 限定条件:net 子系统可用(NET_READY 屏障后)
*/
static int s_bind_listen(void) {
int ls;
struct net_sockaddr_in addr;
ls = net_socket(NET_AF_INET, NET_SOCK_STREAM);
if (ls < 0) {
return -1;
}
(void)memset(&addr, 0, sizeof(addr));
addr.sin_family = NET_AF_INET;
addr.sin_port = net_htons(IEC104_SERV_PORT);
addr.sin_addr.s_addr = NET_INADDR_ANY;
if (net_bind(ls, (struct net_sockaddr *)&addr, (int)sizeof(addr)) < 0) {
net_close(ls);
return -1;
}
if (net_listen(ls) < 0) {
net_close(ls);
return -1;
}
s_listen = ls;
return 0;
}
/*
* 函数功能:主动断开当前 104 连接并重建监听(协议故障/t1 超时/发送连败)
* 入口参数:why = 日志原因串(ASCII)
* 返回值:无
* 函数说明:与 EOF 路径不同——协议违例时主站不会自行断开,必须本端 close
* 促其重连;单连接模型下 client 即 listen,close 后统一重建
* (time_sync 会话收尾先例,非芯片侧 auto_relisten 风暴路径)
*/
static void s_drop_conn(const char *why) {
DBG_ERROR("i104 drop: %s", why);
if (s_client >= 0) {
net_close(s_client);
}
s_client = -1;
s_listen = -1;
iec104_link_close(&s_link);
s_send_fail_run = 0u;
s_conn_dead = 0u;
s_liveness_ctr = 0u;
if (s_bind_listen() < 0) {
s_listen = -1; /* 主循环下轮重试 */
}
}
/*
* 函数功能:link 层节拍服务 + 故障检测(poll 后查 IDLE 跳变/发送连败)
* 入口参数:now = 注入时基(tick ms)
* 返回值:无
*/
static void s_service_link(uint32_t now) {
iec104_link_poll(&s_link, now);
if (s_conn_dead != 0u) {
s_drop_conn("send fail");
return;
}
if (iec104_link_state(&s_link) == IEC104_ST_IDLE) {
s_drop_conn("protocol fault");
}
}
/*
* 函数功能:单次服务循环(代次自愈 → accept → 门控收 → 喂 link → 节拍)
* 返回值:无
* 限定条件:NET_READY 已置位;s_cfg/s_link 已 init
* 函数说明:门控(net_can_accept/net_can_read 纯 RAM 读)+ 兜底节拍照
* ui_net_sink I-1/I-2;EOF 时 client==listen 严禁 close(net 层
* 已 auto_relisten,close 会连监听一起毁)
*/
static void s_serv_poll(void) {
uint32_t now = osKernelGetTickCount();
int n;
/* 网络重建(代次变化):旧监听/客户端随芯片复位失效 → 重建 */
if (net_get_generation() != s_gen) {
if ((s_client >= 0) && (s_client != s_listen)) {
net_close(s_client);
}
if (s_listen >= 0) {
net_close(s_listen);
s_listen = -1;
}
s_client = -1;
iec104_link_close(&s_link);
s_send_fail_run = 0u;
s_conn_dead = 0u;
s_liveness_ctr = 0u;
s_gen = net_get_generation();
(void)s_bind_listen();
return;
}
if (s_listen < 0) {
(void)s_bind_listen(); /* 启动失败/被毁:重试 */
return;
}
if (s_client < 0) {
int c;
if (net_can_accept(s_listen) == 0) {
if (net_get_sock(s_listen) == NULL) {
s_listen = -1; /* 控制块消失:交回上方重建 */
}
return;
}
c = net_accept(s_listen, NULL, NULL);
if (c >= 0) {
s_client = c;
s_send_fail_run = 0u;
s_conn_dead = 0u;
s_liveness_ctr = 0u;
iec104_link_open(&s_link, now);
DBG_INFO("i104: accepted fd=%d", c);
}
return;
}
/* 门控 + 兜底:无数据多数轮直接走 link 节拍;每 32 轮无条件收一次 */
if (net_can_read(s_client) == 0) {
if (((++s_liveness_ctr) & (IEC104_LIVENESS_DIV - 1u)) != 0u) {
s_service_link(now);
return;
}
} else {
s_liveness_ctr = 0u;
}
n = net_recv(s_client, s_rx, (int)sizeof(s_rx), NET_MSG_DONTWAIT);
if (n > 0) {
iec104_link_on_rx(&s_link, s_rx, (uint16_t)n, now);
} else if (n == 0) {
/* 对端关闭:net 层已 auto_relisten;client==listen 时严禁 close */
if (s_client != s_listen) {
net_close(s_client); /* 模拟器多连接后端:数据通道独立 */
}
s_client = -1;
s_liveness_ctr = 0u;
iec104_link_close(&s_link);
if ((net_get_sock(s_listen) == NULL) && (s_bind_listen() < 0)) {
s_listen = -1;
}
return;
} else if (net_get_errno() != NET_ERR_WOULDBLOCK) {
s_drop_conn("recv err");
return;
}
s_service_link(now);
}
/*
* 函数功能:iec104Task 任务体(freertos.c USER CODE 区创建)
* 入口参数:argument 未用
* 返回值:无(不返回)
* 函数说明:NET_READY 屏障(100ms 分片 + 每片喂心跳,timeSyncTask 先例)→
* 点表/链路 init + 建监听 → 100ms 服务循环(每轮喂看门狗)
*/
void iec104_serv_task(void *argument) {
(void)argument;
if (sys_evtHandle != NULL) {
for (;;) {
uint32_t r = osEventFlagsWait(sys_evtHandle, SYS_EVT_NET_READY,
osFlagsWaitAny | osFlagsNoClear, 100u);
if ((r & osFlagsError) == 0u) {
break; /* 就绪位是状态位(NoClear),不消费 */
}
task_wd_beat(TASK_WD_IEC104);
}
}
iec104_pts_init();
iec104_link_init(&s_link, &s_cfg, NULL);
s_gen = net_get_generation();
if (s_bind_listen() < 0) {
DBG_ERROR("i104: bind port %u FAIL, retry in loop",
(unsigned)IEC104_SERV_PORT);
} else {
DBG_INFO("i104: listening on %u", (unsigned)IEC104_SERV_PORT);
}
for (;;) {
task_wd_beat(TASK_WD_IEC104);
s_serv_poll();
osDelay(IEC104_POLL_MS);
}
}
+20
View File
@@ -0,0 +1,20 @@
/*
* 模块名称:IEC 60870-5-104 被控站服务(对外接口)
* 模块功能:声明 iec104Task 任务体与监听端口;协议实现在 App/protocol/iec104/,
* 本接口仅供 Src/freertos.c 创建任务使用。
* 创建日期:2026-10-12(B4)
*/
#ifndef IEC104_SERV_H
#define IEC104_SERV_H
/* IEC 60870-5-104 被控站监听端口(DL/T 634.5104 标准端口) */
#define IEC104_SERV_PORT 2404u
/*
* 函数功能:iec104Task 任务体(在 Src/freertos.c USER CODE 区 osThreadNew 创建)
* 入口参数:argument - 未用
* 返回值:无(任务不返回)
*/
void iec104_serv_task(void *argument);
#endif /* IEC104_SERV_H */
+105
View File
@@ -0,0 +1,105 @@
/*
* 模块名称:保护任务(L3 板端接线,B5)——实现
* 模块功能:见 protect_task.h。每拍三件事:
* ① L5-4 采样故障闭锁:adc_health_flags() 电平位命中 → 元件状态
* 复位(防陈旧判据)+ 本拍不 eval;
* ② prot_set_selfcheck()(L5-1 定值完整性巡检,31 项 sum32,开销
* 可忽略;失配 → prot_set 内部闭锁,eval 自动复位全元件);
* ③ prot_elm_run()(phasor rms → 判据步进)+ 动作位图边沿日志。
* 定值/元件初始化在 app_main_init(内核前,B5);本任务只消费。
* 线程约定:prot_elm_run / prot_set_selfcheck 仅本任务上下文(prot_elm.h);
* 跨上下文复归(104 YK11)经 prot_elm_request_reset 落请求。
* 创建日期:2026-10-12(B5)
*/
#include "protect_task.h"
#include "cmsis_os2.h"
#define DBG_TAG "[PROT]"
#define DBG_LINE_BUF_MAX 128U
#include "dbg_log.h"
#include "adc_health.h"
#include "prot_elm.h"
#include "prot_set.h"
#include "task_watchdog.h"
/* L5-4 采样故障电平闭锁位:stall(可自愈)+ backward(锁存)都须停判据。
* 锁存类(FRSTDATA/HW_ERR 等)不在此列——它们是一次性事件痕迹,不构成
* "当前测量不可信";持续 stall/backward 才是。 */
#define PROT_HF_LOCK_MASK (ADC_HF_TPAFE_STALL | ADC_HF_STM32_STALL | ADC_HF_SEQ_BACKWARD)
static uint32_t s_prev_active; /* 上拍动作位图(边沿日志用) */
static uint8_t s_prev_hf_lock; /* 上拍采样闭锁态(边沿日志用) */
/*
* 函数功能:动作位图变化沿日志(逐变化元件打名字与状态)
* 入口参数:active - 本拍 prot_elm_active_mask()
* 函数说明:V0.1 无出口硬件(规划 R7),动作 = 遥信 + 日志;SOE 落盘随 L4。
*/
static void s_log_active_edge(uint32_t active) {
uint32_t chg = active ^ s_prev_active;
uint32_t i;
for (i = 0u; (i < (uint32_t)PE_COUNT) && (chg != 0u); i++) {
const uint32_t bit = 1u << i;
if ((chg & bit) != 0u) {
prot_elm_state_t st;
chg &= ~bit;
prot_elm_state((prot_elm_id_t)i, &st);
DBG_INFO("elm %s asserted=%u picked=%u",
prot_elm_name((prot_elm_id_t)i),
(unsigned)st.asserted, (unsigned)st.picked);
}
}
s_prev_active = active;
}
/*
* 函数功能:protectTask 线程入口
* 入口参数:argument - 未用
* 返 回 值:无(不返回)
* 函数说明:无就绪屏障(判据只依赖 phasor 使用区,未就绪时 prot_elm_run
* 整拍自跳;采样闭锁另有 adc_health 电平位兜底)→ 上电即跑。
* 每拍喂看门狗(TASK_WD_PROTECT,产品 required_mask 0x1F7)。
*/
void protect_task(void *argument) {
(void)argument;
DBG_INFO("protect task start: cycle=%ums elm=%u",
(unsigned)PROTECT_TASK_CYCLE_MS, (unsigned)PE_COUNT);
for (;;) {
uint32_t now = osKernelGetTickCount();
uint32_t hf = adc_health_flags();
if ((hf & PROT_HF_LOCK_MASK) != 0u) {
/* L5-4:测量链不可信 → 复位元件(本任务上下文,直接 init 合法)
* 并跳过判据;stall 自愈后下一拍自动恢复 */
if (s_prev_hf_lock == 0u) {
DBG_ERROR("sampling FAULT -> protection blocked (hf=0x%08lX)",
(unsigned long)(hf & PROT_HF_LOCK_MASK));
}
s_prev_hf_lock = 1u;
prot_elm_init();
} else {
if (s_prev_hf_lock != 0u) {
DBG_INFO("sampling recovered -> protection resumed");
s_prev_hf_lock = 0u;
}
/* L5-1 巡检:定值区与封存校验和不符 → prot_set 闭锁(下一拍起
* eval 自动复位全元件)。⚠ 与 104 定值写(iec104Task set+seal)
* 的竞态窗口为 µs 级:selfcheck 误锁后同一次写的 seal 即清锁
* (prot_set.c),V0.1 如实登记,P4 定值持久化时收口。 */
(void)prot_set_selfcheck();
prot_elm_run(now);
}
s_log_active_edge(prot_elm_active_mask());
task_wd_beat(TASK_WD_PROTECT);
osDelay(PROTECT_TASK_CYCLE_MS);
}
}
+25
View File
@@ -0,0 +1,25 @@
/*
* 模块名称:保护任务(L3 板端接线,B5)
* 模块功能:protectTask 线程入口——以固定节拍驱动保护判据
* (`prot_elm_run()`:phasor rms → 元件步进),并落实 L5-4 采样
* 故障闭锁(`adc_health_flags()` 电平位 → 元件状态复位 + 本拍不判据)。
* 判据本体在 `App/algo/prot_elm.c`(纯逻辑,L0.5 已覆盖),本文件
* 只做消费/编排(AGENTS 目录地图:task 不装算法)。
* 线程约定:仅本任务调 prot_elm_run / prot_set_selfcheck(prot_elm.h 线程约定);
* prot_set 写在 P4 前无第二写者(104 定值写经 pts→prot_set,见注)。
* 创建日期:2026-10-12(B5)
*/
#ifndef PROTECT_TASK_H
#define PROTECT_TASK_H
/* 判据节拍(ms):V0.1 元件吃 50 Hz 基波 rms(phasor 每工频周更新)→ 20 ms
* = 一个工频周;延时精度由 prot_elm 的 now_ms 差值法保证,与节拍无关。
* 规划 §4 的"1 ms 高优先级"是参考系统瞬时值元件口径,随 P2 瞬时值元件再评。 */
#define PROTECT_TASK_CYCLE_MS 20u
/*
* 函数功能:protectTask 线程入口(freertos.c USER CODE 区创建)
*/
void protect_task(void *argument);
#endif /* PROTECT_TASK_H */