Files
STM32F4-Base/Src/freertos.c
T
wandering1 b623a406ad fix(栈·存储·UI 收尾): 断掉临界区↔断言幻影环回到 1,464,标定门控单点化,卷锁五态可观测
这四笔在主树里已经互相纠缠(同一次 @build 编译、同一份 .htm 度量),拆成四笔就不
存在任何一个被真编过的中间态,所以合一笔入库;下面按模块分段陈述,作者与出证归属
逐段写明。

—— 一、崩溃证据路径的整行独占收尾(log-maint-writer,task-26)——
上午那笔 1d4b6a17 之后,stack_report 报 iec104Task 静态最坏链 1,544 / margin 1,016,
低于 1,024 的 ISR 底线。真因不是新增局部缓冲,而是本模块与内核临界区之间新长出
一条环:taskEXIT_CRITICAL ⇒ vPortExitCritical ⇒ configASSERT ⇒ vApplicationAssertFailed
⇒ dbg_console_write_raw ⇒ 回到本模块。armlink 把这一整坨计入**每一个含临界区的
任务链**(vApplicationAssertFailed 的 Max Depth 由约 40 B 涨到 128 B + In Cycle),
所以九条链同涨约 80 B——同涨本身就排除了任何单个调用方。
改法两处:① 本模块三处窗口(取凭证 / 还凭证 / 整行入环)改用 CON_CRIT_ENTER/EXIT
(PRIMASK 保存恢复到局部变量;注释写全为什么不能直接 __enable_irq()——嵌套时会
替别人打开任务级临界区——以及屏蔽面变宽是有意为之);② 异常/终点钩子里那对
自证空转的 begin/end 摘掉(异常态 begin 放行但不持有、end 是幂等空操作,调它只有
成本没有收益),任务态 STALL 行保留 begin/end,这个区分写进两处注释与文档 §4.4.1。
实测四棵树逐个 .htm 解析(身份钉 tree + Linker run 时刻 + 产物来源):A=77779d49
链 1,456;B=1d4b6a17 链 1,464 / margin 1,096 PASS;C=含未提交钩子接线的工作树
1,544 / 1,016 FAIL;D=本轮修复后(Linker run 22:52)回到 **1,464 / 1,096 PASS**,
九条链齐降 80 B。断环图证:vPortExitCritical 由 136 B + In Cycle 变 8 B 且不再
In Cycle,con_tx_release 子树 152→8;Error_Handler 回到 280 + In Cycle = A 树同值
(既有发送环无人拆动)。顺带删掉 Src/freertos.c 崩溃路径里最后一个 HAL_Delay(200)
(该文件真实调用现为 0)。
测试:console_log 152→**153** 条(只增不减);换钉不删防——015 现在钉「PRIMASK
窗口 2 次、深度 1、内核临界区 0 次」,新钉 015c 绝不等待,160 注入点搬到
con_pm_hook,176 强化到未调度路径连 PRIMASK 窗口都不开;红线水位
con_pm_at_tx_max 与 con_uart_crit_at_tx_max 双 0;判据⑦(延后的行与当场直发逐
字节相同)170b/171/178 仍全绿。作者自报并纠正一处:换钉时曾把 TC-CONLOG-015b 改
编号,而 015b 在生产注释与文档里都被引用——已恢复「编号不退役、语义搬过去」。
覆盖率 App/log/console_log.c 199/200 → 256/257 行、21/21 函数(只增)。

—— 二、未标定闭锁判据收成单点访问器(ui-param-writer,task-16)——
「UA/UN/IA 三相 VALID 全置位才允许跑判据」这条门控原先在两处各自镜像实现
(protect_task.c 的 s_cal_locked 与 ui_data_feed.c 的 feed_cal_block),改一处另一处
静默失效且无门禁可照。现新增 adc_cal_prot_blocked() 落在 App/algo/adc_cal,DTU_BENCH
特例收进一处;两个消费者同批改调、本地副本删除。作者还查实并纠正了我先前一句
错判:全仓 App/app→App/task 的 include 实为 0 条,故访问器不能落在 protect_task。
新增 TC-ADCCAL-018 八条(含冷启动即闭锁),adc_cal 套件 99/99。

—— 三、共享卷锁五态可观测性(ui-param-writer,task-17)——
原状比登记簿写的更糟:五个可区分态(未调用 / try 超时 / 分片超时 / fail-open /
未持锁却 release)生产侧零留痕,且 vol_lock.c 的返回值注释与代码相反——
fail-open(锁没建成、互斥已失效却在读写盘)被返 1 **谎报成取到锁**,而那正是
vol_lock.h 记录的台架实证故障(并发写坏 FTL → 喂狗点饥饿 → IWDG 误复位)的前置。
锁层加四个 volatile 计数(16 B 静态,零新局部、零新 callee、零日志、零 FatFs)与
不取锁的 vol_lock_get_stats();13 处 try 调用点一字未动。**刻意不在锁层记时刻**:
记时刻要读 tick,等于给 defaultTask 取锁链添帧——诊断不值那几个字节;消费方若
自盖时间戳,那只能是被观测到的时刻而非发生时刻,.h 里已写明不得写成首次/末次。
四套本地假件各加 s_last_wait_to(字段名逐字一致),把「喂狗点取锁一律 timeout=0」
与「ftpTask 分片一律 100 ms」从注释纪律升级成会 FAIL 的断言;红-绿反证:临时把
sys_log_mcu.c 的 0u 改 50u → TC-SYSLOG-010/012 双红,把 lftpd_io.c 的 100u 改 0u →
TC-LFTPIO-034 红,两处按原串改回并用 SHA-256 证明逐字节还原。TC-VOLLOCK-015~019
共 45/45(无新桩)。

—— 四、UI 死接口清理(ui-maint-writer,task-6 收尾)——
删掉 ui_data_maint_ready():全仓零消费者、只有它自己的用例在读。**更值得记的是
它为什么能活过一整轮评审**:头注释写的是「页面用它决定这页能不能操作」,而页面
从来没用过它——注释陈述的是意图,评审读的是意图,门禁读的是可达路径,两者都不
反驳「意图未实现」。作者对「连用例一起删」做了显式抗命:TC-UIDATA-025 的 13 条里
只有 4 条读那个死口,另 9 条钉的是真防线(六口未注入安全返回 / 残表整表拒绝 /
被拒不覆盖已生效的好表),故删冗余的 025a、把 025i/025k/025m 换成用 reset_req()
的可观察返回值判定,性质不丢。ui_data 套件 244 → 243(净减 1),黄金 40 页与
池 5448 B 未变(该刀零像素影响,参考图无 diff)。

—— 门禁与未跑项(本笔如实留账)——
cubemx-survival 基线已 --write(漂移 6 项全部是上述意图内区域:断言/栈溢出钩子
实现、看门狗模块行等),基线文件同笔入库,随后 --check 通过。
本笔未跑的钩子只有两条:mem-numbers 与 mem-budget——磁盘 .map 出自 22:52 那次
构建、含尚未入库的 UI 与工具改动,与 HEAD 台账必然不同码;按仓库既有约定允许
--no-verify + 正文留证,回填在紧随的台账一笔统一做(含 ram_baseline --update 与
覆盖率基线两条 [NOTE]:ui_data.c 65→316、iec104_comm.c 36/36)。
两条已挂着的账:① id-check 不可判定引用 1,162 / 上限 1,166,**余量只剩 4**,
主要吃进者是我今天推进的 UI 那笔(68 条文本断言的注释引用),账在 Lead;
② 崩溃注入腿(故意复位)台架复验未跑,需先由代码道提交触发器,且判据只判原子性
不判 [ERR]=0——IPSR 路径的主张归主机侧套件,硬件照不出来。

产物身份(按 D 表要求钉死,避免数字与树脱钩):本笔包含的固件树就是那张 D 表对应的
那一棵——MDK-ARM/.../STM32F407-Demo.htm 的 mtime = 2026-10-26 22:52:12(.htm/.map 按
约定不入库,故把时刻写进正文)。提交前在本树复跑 stack_report.py:exit 0,
iec104Task 1,464 / margin 1,096(42%)PASS、defaultTask 3,040 / 2,080(40%)PASS,
与 D 表逐项一致;本次运行不含任何未提交固件源码之外的新改动。
2026-10-26 23:31:05 +08:00

611 lines
29 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* USER CODE BEGIN Header */
/**
******************************************************************************
* File Name : freertos.c
* Description : Code for freertos applications
******************************************************************************
* @attention
*
* Copyright (c) 2026 STMicroelectronics.
* All rights reserved.
*
* This software is licensed under terms that can be found in the LICENSE file
* in the root directory of this software component.
* If no LICENSE file comes with this software, it is provided AS-IS.
*
******************************************************************************
*/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "FreeRTOS.h"
#include "task.h"
#include "main.h"
#include "cmsis_os.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "test_config.h" /* 套件选择:STORAGE 套件下 defaultTask 让出存储所有权 */
#include <string.h>
#include "net_task.h"
#include "ch395f.h"
#include "dbg_log.h"
#include "net_socket.h"
#include "lftpd.h"
#include "task_watchdog.h"
#include "../test/board/ch395f_test_task.h"
#include "../test/board/ch395f_test.h" /* ENABLE_NET_LAYER_TESTS:netTask 条件创建 */
#include "time_sync.h"
#include "sys_events.h"
#include "default_task.h"
#include "adc_task.h" /* B1(2026-10-10):ADC 采集消费者(手写任务,见 Variables 区) */
#include "iec104_serv.h" /* B4(2026-10-12):IEC 104 上行服务(手写任务,见 Variables 区) */
#include "protect_task.h" /* B5(2026-10-12):保护判据节拍任务(手写任务,同上) */
/* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
/* USER CODE END PTD */
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/*
* 名称指针合法性校验(栈溢出/越界可能已破坏 TCB,指针不可信):
* 只接受落在 Flash / 主 SRAM / CCM 映射范围内的指针;不校验就直接交给 UART,
* 钩子自身也会以同样方式崩(2026-09-24 实测 HardFault:BFAR=0x10010000 恰为
* CCM 末尾+1,PC 落在 HAL_UART_Transmit 的缓冲读取处)。
* ⚠ 本辅助函数须定义在钩子**之前**:两个钩子本体已迁入 CubeMX 生成的
* `USER CODE BEGIN 4 / 5` 区(位置在 `MX_FREERTOS_Init` 原型之后,比
* `Application` 区靠前),故 helper 一并搬到此标准保护位(L2)。
*/
static int hook_ptr_ok(const void *p)
{
uint32_t a = (uint32_t)p;
uint32_t win = 32u; /* 将要读取的最大字节数(任务名 ≤ 32) */
if ((a >= 0x08000000u) && (a <= (0x08100000u - win))) {
return 1; /* Flash(RO 区) */
}
if ((a >= 0x20000000u) && (a <= (0x20020000u - win))) {
return 1; /* SRAM1 + SRAM2 */
}
if ((a >= 0x10000000u) && (a <= (0x10010000u - win))) {
return 1; /* CCM(FreeRTOS 堆:TCB/栈所在) */
}
return 0;
}
/* USER CODE END PD */
/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
/* USER CODE END PM */
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN Variables */
/*
* ===== 任务栈核算依据(受保护记录,勿删本块)=====
* ⚠ 栈**值**的事实源是 `.ioc` 的 `FREERTOS.Tasks01`——改栈改那里,CubeMX 会生成到下面各
* `.stack_size`。本块只记录"为什么是这个值":原先这些注释贴在各自 `.stack_size` 旁边,
* 但那里位于生成区之外,2026-09-29 与 2026-10-09 共四次重生成里丢了三次,故集中搬进
* `USER CODE BEGIN Variables` 这个标准保护位(模板真实生成、名字从未变过 → 重生成不丢)。
* 数据来源:`scripts/stack_report.py`(Keil 静态调用图最坏链)+ 串口 `[STACK]` 运行时水位;
* 底线规则:余量 ≥ max(1024 B ISR, 30%)。完整核算见 docs/内存管理/RAM使用分析与优化方案.md,
* 全表(含 ftpTask 1024 / uiTask 4096 的现值)见 AGENTS.md 任务表。
*
* defaultTask 1280 words:扛 mount+mkfs+FAT 探针+cfg 加载+syslog 写+配置引擎
* (全深层 FatFS I/O)。原 512,2KB 实测峰值溢出——开启
* configCHECK_FOR_STACK_OVERFLOW=2 后被钩子抓到(2026-09-21)。
* 2026-10-10 栈整改(**未抬栈**,靠削链达标):静态最坏链 5312 → 2776 B、
* 实测峰值 3392 → 2152 B(余 1944 B / 47%)。四个杠杆:① 删旧档 NCFG 迁移
* (net_cfg_legacy,−600 B 链);② 6 处局部 `FIL` 改函数级 static(FF_FS_TINY=0
* → 单个 FIL = 528 B 栈);③ 日志行缓冲按模块收紧 `DBG_LINE_BUF_MAX 128`
* + 时间戳缓冲并入行缓冲(每个日志调用点原内联 192+32 B);④ 断言/故障钩子
* 脱离 HAL 发送回退 + 缓冲静态化(configASSERT 在 FreeRTOS 内部可达 →
* 钩子在**每个**任务的最坏链尾)。详见 docs/内存管理/RAM使用分析与优化方案.md
* 栈余量收口(2026-10-12):B7 定值持久化把静态最坏链推到 **3016 B**(storage_bring_up →
* cfg 写 → f_rename → sync_fs → dhara GC → 控制台回显尾),1024 words 时余
* 1080 B < 30% 线(4096×30% = 1228 B)→ 抬至 **1280 words**(5120 B),余
* 2104 B / 41%;堆开销 +1024 B。栈值事实源仍是 `.ioc` 的 `FREERTOS.Tasks01`。
* netTask 960 words:静态最坏链 1944 B(2026-10-12 监听生命周期模块化后复测,链尾经
* net_listener_pump→net_relisten_locked→SPI 故障→日志尾;2026-10-10 整改后曾为
* 1872 B,整改前 2664 B);3840 B 配置余量 1896 B
* (49%),过 stack_report 规则。运行时水位仅 ~0.9 KB(错误尾未
* 触发,不可作依据)。曾试 net_drain_chip_rx scratch 静态化,实测仅省 16 B 栈
* (ARMCC 已叠放不重叠局部量)反增 256 B BSS,已回退——见 net_socket.c 注释。
* testTask 2048 words:存储套件 deep call chain + 局部缓冲,原 1024(4 KB)实测溢出
* (2026-09-20)。仅启用测试套件时创建。
* timeSyncTask 640 words:本区下方手工定义(不在 `.ioc` 的 Tasks01 里 → 重生成会丢,
* 靠审计 WARN 提示),建监听 → net_bind → CH395F → 日志尾。2026-10-10 整改后
* 静态最坏链 1024 B(整改前 1736 B)、实测峰值 616 B;⚠ 余量 1024 B **正好**
* 等于 ISR 底线 → 本任务链上再加任何调用(或新日志行)都必须先抬栈。
* 栈余量收口(2026-10-12):链已加深到 **1264 B**(同一控制台回显尾:`dbg_console_drain`
* → `MX_USART1_UART_Init` → `Error_Handler` → 回显 Cycle),512 words 时余
* 784 B < 1024 B ISR 底线 → 抬至 **640 words**(2.5 KB),余 1296 B / 50%;
* 堆开销 +512 B。
* adcTask 640 words:本区下方手工定义(同上,不在 `.ioc` 里)。B1(2026-10-10)
* ADC 采集消费者;L2 相量(2026-10-11)+ L1-6 有效性校验(同日)后链上为
* adc_input_poll + sample_ring/phasor + adc_health_report(DBG_ERROR +
* syslog_post 回显尾)。静态最坏链 1080 B,512 words 时余 968 B < 1024 B
* ISR 底线 → 抬至 640 words(2.5 KB,堆余量 min_ever 7152→≈6640 B,可承受)。
* 2026-10-12 复测链 1320 B(L3 保护接线后),余 1240 B / 48% 仍 PASS。
* 栈值以 `scripts/stack_report.py` 实测最坏链为准,改动前后各跑一次。
* iec104Task 640 words:B4(2026-10-12)104 上行服务(2404 监听 + link 状态
* 机)。link/pts 缓冲全静态;stack_report 实测静态最坏链 1456 B(拆链
* 重建监听 → net_bind → SPI 故障 → 日志尾,日志主导)。512 words 时余
* 592 B < 1024 B ISR 底线 → 抬至 640(2.5 KB)。
* protectTask 512 words:B5(2026-10-12)保护判据节拍(prot_elm_run 消费者
* + L5-4 采样闭锁)。stack_report 实测静态最坏链 872 B(float 判据 +
* phasor 快照 + 日志尾),余 1176 B / 57% ≥ ISR 底线 → 512 保持。
*/
/* 存储测试变量(g_storage_stats / s_perf_buf)已迁至 test/board/storage_test_task.c */
osThreadId_t timeSyncTaskHandle;
const osThreadAttr_t timeSyncTask_attributes = {
.name = "timeSyncTask",
.stack_size = 640 * 4,
.priority = (osPriority_t) osPriorityNormal,
};
/* B1:ADC 采集消费者(TIM3 1 kHz 采样节拍 + 原子快照 + 帧统计)。
* 手写于此而非 .ioc(同 timeSyncTask:不在 Tasks01 里 → 重生成会丢,靠审计 WARN 提示)。
* ⚠ 优先级 AboveNormal:消费由 TIM3 ISR 事件驱动,提优先级使"被唤醒 → 立即抢占其余
* Normal 任务",唤醒延迟固定为 µs 级。
* 代价:本任务单拍最坏 adcm≈2 ms 会在 AboveNormal 上挤占 net/ftp/default/timeSync;
* 实测峰值来自每 10 s 心跳日志而非采集(STM32 侧已由 DMA 搬运,单拍取数 µs 量级)
* → 耗时特征与代价见 docs/子系统设计/ADC采样节拍设计说明.md §9、§10。 */
osThreadId_t adcTaskHandle;
const osThreadAttr_t adcTask_attributes = {
.name = "adcTask",
.stack_size = 640 * 4,
.priority = (osPriority_t) osPriorityAboveNormal,
};
/* B4(2026-10-12):IEC 60870-5-104 被控站服务(2404 监听 + 链路状态机消费)。
* 手写于此而非 .ioc(同 timeSyncTask/adcTask:不在 Tasks01 里 → 重生成会丢,
* 靠审计 WARN 提示)。640 words:stack_report 实测静态最坏链 1456 B(拆链→
* 重建监听→net_bind→SPI 故障→日志尾,日志主导);512 words 时余 592 B
* < 1024 B ISR 底线 → 抬至 640(2.5 KB,CCM 堆 min_ever 再降 512 B,可承受)。 */
osThreadId_t iec104TaskHandle;
const osThreadAttr_t iec104Task_attributes = {
.name = "iec104Task",
.stack_size = 640 * 4,
.priority = (osPriority_t) osPriorityNormal,
};
/* B5(2026-10-12):保护判据节拍(prot_elm_run 消费者 + L5-4 采样闭锁)。
* 手写于此而非 .ioc(同上)。512 words:stack_report 实测静态最坏链 872 B
* (float 判据 + phasor 快照 + 日志尾),余 1176 B / 57% ≥ ISR 底线 → 保持。
* 优先级 Normal:V0.1 无出口硬件(风险项见 docs/算法/继电保护功能复现规划.md §9),
* 判据是 20 ms 软实时,不需要 adcTask 的 AboveNormal;该文档 §4 设想的"1 ms / 高
* 优先级"要等瞬时值元件落地后再评(当前无出口硬件,快一拍不改变任何出口时序)。 */
osThreadId_t protectTaskHandle;
const osThreadAttr_t protectTask_attributes = {
.name = "protectTask",
.stack_size = 512 * 4,
.priority = (osPriority_t) osPriorityNormal,
};
/* USER CODE END Variables */
/* Definitions for defaultTask */
osThreadId_t defaultTaskHandle;
const osThreadAttr_t defaultTask_attributes = {
.name = "defaultTask",
.stack_size = 1280 * 4,
.priority = (osPriority_t) osPriorityNormal,
};
/* Definitions for netTask */
osThreadId_t netTaskHandle;
const osThreadAttr_t netTask_attributes = {
.name = "netTask",
.stack_size = 960 * 4,
.priority = (osPriority_t) osPriorityNormal,
};
/* Definitions for ftpTask */
osThreadId_t ftpTaskHandle;
const osThreadAttr_t ftpTask_attributes = {
.name = "ftpTask",
.stack_size = 1024 * 4,
.priority = (osPriority_t) osPriorityNormal,
};
/* Definitions for testTask */
osThreadId_t testTaskHandle;
const osThreadAttr_t testTask_attributes = {
.name = "testTask",
.stack_size = 2048 * 4,
.priority = (osPriority_t) osPriorityBelowNormal,
};
/* Definitions for uiTask */
osThreadId_t uiTaskHandle;
const osThreadAttr_t uiTask_attributes = {
.name = "uiTask",
.stack_size = 4096 * 4,
.priority = (osPriority_t) osPriorityBelowNormal,
};
/* Definitions for netMsgQueue */
osMessageQueueId_t netMsgQueueHandle;
const osMessageQueueAttr_t netMsgQueue_attributes = {
.name = "netMsgQueue"
};
/* Definitions for sys_evt */
osEventFlagsId_t sys_evtHandle;
const osEventFlagsAttr_t sys_evt_attributes = {
.name = "sys_evt"
};
/* Private function prototypes -----------------------------------------------*/
/* USER CODE BEGIN FunctionPrototypes */
void ui_task_func(void *argument);
/* USER CODE END FunctionPrototypes */
void StartDefaultTask(void *argument);
void StartNetTask(void *argument);
void StartFtpTask(void *argument);
void start_test_task(void *argument);
void StartUiTask(void *argument);
void MX_FREERTOS_Init(void); /* (MISRA C 2004 rule 8.1) */
/* Hook prototypes */
void vApplicationStackOverflowHook(xTaskHandle xTask, signed char *pcTaskName);
void vApplicationMallocFailedHook(void);
/* USER CODE BEGIN 4 */
/*
* 栈溢出钩子(configCHECK_FOR_STACK_OVERFLOW=2,2026-09-20 存储套件死机事故后加):
* 溢出 = 内核状态已不可信,禁止走 printf/内核对象/控制台锁,裸发固定文本后复位。
* 2026-09-24 加固:任务名先经范围校验并拷入本地缓冲(不再把 TCB 指针交给 UART),
* 指针不可信时打印占位文本——避免"上报崩溃的代码自己再崩一次"。
* ⚠ 落点说明(2026-10-09):整个函数放在 CubeMX 生成的 `USER CODE BEGIN 4` 区内。
* 该区的存在由 `.ioc` 启用钩子(FREERTOS.configCHECK_FOR_STACK_OVERFLOW=2)保证;
* 若实现留在 `Application` 区、又让 CubeMX 生成空桩,两处同名定义 = 编译错。
* 签名与 CubeMX 生成的钩子原型一致(xTaskHandle → TaskHandle_t,即 void *)。
*/
void vApplicationStackOverflowHook(xTaskHandle xTask, signed char *pcTaskName)
{
static const char msg_head[] = "\r\n[ERR] STACK OVERFLOW task=";
static const char msg_bad[] = "(name unavailable)";
uint16_t len = 0u;
(void)xTask;
/* ⚠ 刻意**不**用 dbg_console_tx_begin()/tx_end() 包这三条裸发(道G task-26 定案,
* 2026-10-26;早先的版本包过,这一笔摘掉,三条理由都要看):
* ① 语义上空转:异常上下文 IPSR != 0 时 begin 是**放行、不持有凭证**("绝不等待"
* 这条约束的必然结果),未持有时 end 又是幂等空操作 —— 包上得不到任何整行保护。
* 本行的整行性来自"Fault 优先级 -1、外设中断 ≥5、任务全冻结 ⇒ 没有第二个写者
* 能跑进来",与 begin 无关。
* ② 代价却是全局的:本钩子挂在 configASSERT 子树上(vPortEnterCritical /
* vPortExitCritical 都可能进断言),挂在那上面的每一句都会被 armlink ×9 条任务链
* 记账。实测包上后 con_tx_release 子树 24→152 B、发送环单趟贡献 128→208 B,
* default/net/iec104 三条链各 +80 B,把 iec104Task 的 margin 顶到 1,016 < 1,024
* (红线数字见 App/log/console_log.c 的 CON_CRIT_ENTER 注释)。
* ③ 归还所有权在这里没有下游读者:这一行发完就 NVIC_SystemReset()。
* 对照:**任务态**多段裸发(App/util/task_watchdog.c 的 STALL 行)仍然包 begin/end
* —— 那里既不空转(同任务后面还要接着发),也不在断言子树上。这条区分写进
* docs/子系统设计/控制台单写者日志设计说明.md §4.4.1,别让下一个人以为是我们漏了。 */
dbg_console_write_raw(msg_head, (unsigned)(sizeof(msg_head) - 1u));
if ((pcTaskName != NULL) && (hook_ptr_ok(pcTaskName) != 0)) {
while ((len < 32u) && (pcTaskName[len] != '\0')) {
len++;
}
}
if (len == 0u) {
dbg_console_write_raw(msg_bad, (unsigned)(sizeof(msg_bad) - 1u));
} else {
char name[33]; /* 拷入本地缓冲:UART 永不接触 TCB 指针 */
memcpy(name, pcTaskName, (size_t)len);
name[len] = '\0';
dbg_console_write_raw(name, (unsigned)len);
}
dbg_console_write_raw("\r\n", 2u);
/* 这里不需要任何延时:唯一理由点写在 Src/stm32f4xx_it.c 的 hard_fault_report 函数头
* 第 ② 条(发送口自身已等到 TC;多留一条 HAL_Delay 就多一条 TIM7 tick 依赖,tick
* 一停摆就卡在复位之前,而启动段 IWDG 尚未启动、没有兜底),此处只引用不复述。
* 顺序不变:三条裸发全部发完 → 立即 NVIC_SystemReset()。 */
NVIC_SystemReset();
}
/* USER CODE END 4 */
/* USER CODE BEGIN 5 */
/*
* 堆分配失败钩子:只告警不复位(NULL 由各调用点自行处理并上报自检)。
* 同样走裸 UART 有界发送——钩子可能在临界区内被调,printf/信号量不可用。
* ⚠ 落点同上:整个函数在 CubeMX 生成的 `USER CODE BEGIN 5` 区内
* (由 `.ioc` 的 FREERTOS.configUSE_MALLOC_FAILED_HOOK=1 保证该区存在)。
*/
void vApplicationMallocFailedHook(void)
{
static const char msg[] = "\r\n[ERR] MALLOC FAILED (heap exhausted)\r\n";
dbg_console_write_raw(msg, (unsigned)(sizeof(msg) - 1u));
}
/* USER CODE END 5 */
/**
* @brief FreeRTOS initialization
* @param None
* @retval None
*/
void MX_FREERTOS_Init(void) {
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* USER CODE BEGIN RTOS_MUTEX */
/* add mutexes, ... */
/* USER CODE END RTOS_MUTEX */
/* USER CODE BEGIN RTOS_SEMAPHORES */
/* add semaphores, ... */
/* USER CODE END RTOS_SEMAPHORES */
/* USER CODE BEGIN RTOS_TIMERS */
/* start timers, add new ones, ... */
/* USER CODE END RTOS_TIMERS */
/* Create the queue(s) */
/* creation of netMsgQueue */
netMsgQueueHandle = osMessageQueueNew (8, sizeof(net_msg_t), &netMsgQueue_attributes);
/* USER CODE BEGIN RTOS_QUEUES */
/* add queues, ... */
/* 队列对象由上面生成的语句创建(创建点不可外移:osKernelInitialize() 之前的
创建类 API 会杀死启动)。这里立刻把句柄交给唯一使用者 net_socket,之后本模块
不再对外暴露这个全局变量;少了这一行,net_socket 的句柄恒为 NULL,所有 net_*
请求会被判空丢弃(表现为网络全线静默不通)。 */
(void)net_msg_queue_adopt(netMsgQueueHandle);
/* USER CODE END RTOS_QUEUES */
/* Create the thread(s) */
/* creation of defaultTask */
defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes);
/* creation of netTask */
netTaskHandle = osThreadNew(StartNetTask, NULL, &netTask_attributes);
/* creation of ftpTask */
ftpTaskHandle = osThreadNew(StartFtpTask, NULL, &ftpTask_attributes);
/* creation of testTask */
testTaskHandle = osThreadNew(start_test_task, NULL, &testTask_attributes);
/* creation of uiTask */
uiTaskHandle = osThreadNew(StartUiTask, NULL, &uiTask_attributes);
/* USER CODE BEGIN RTOS_THREADS */
/* netTask = 基础线程:上方生成区的实例是全构建唯一实例(旧
* #ifdef ENABLE_NET_LAYER_TESTS 的重复创建已删除)。
* - 产品 / NET 层测试模式:正常运行 bring_up + net_poll
* - CH395F 硬件层测试模式:net_task_func 入口自挂起(testTask 独占 CH395F) */
/* 本地时间同步任务(PC 推送 TCP):依赖 netTask 处理消息队列 */
timeSyncTaskHandle = osThreadNew(time_sync_task, NULL, &timeSyncTask_attributes);
/* B1(2026-10-10):ADC 采集任务。1ms 采样节拍触发 CONVST,消费 tpafe5160
* 原子快照并喂 IWDG 心跳(TASK_WD_ADC)。不依赖任何就绪屏障(无网络/存储依赖)
* → 任何构建都创建、都置心跳位。驱动 tpafe5160_init() 已在 app_main_init
* (内核启动前)完成;本任务只做运行期采样。 */
adcTaskHandle = osThreadNew(adc_task_func, NULL, &adcTask_attributes);
/* B4(2026-10-12):IEC 60870-5-104 上行服务任务。任务体内先等 NET_READY
* 屏障(100ms 分片 + 每片喂心跳,timeSyncTask 先例),再建 2404 监听并
* 100ms 循环服务;纳入 IWDG required_mask(TASK_WD_IEC104,任何构建都创建)。 */
iec104TaskHandle = osThreadNew(iec104_serv_task, NULL, &iec104Task_attributes);
protectTaskHandle = osThreadNew(protect_task, NULL, &protectTask_attributes);
/* 2026-10-04:产品构建(未选任何 TEST_SUITE_*)不需要 testTask。
* 上方生成区按 .ioc FREERTOS.Tasks01 无条件创建它,其 2048 words(8KB) 栈 + TCB
* 取自 CCM 堆;而产品构建下 start_test_task() 仅打印一行后 osDelay(1000) 永久空转,
* 白白占用 8KB CCM。此处于内核启动前(MX_FREERTOS_Init 先于 osKernelStart 调用)
* 立即回收:调度器未运行时 vTaskDelete 对非当前任务同步 prvDeleteTCB,栈/TCB 归还
* 堆(INCLUDE_vTaskDelete=1);置句柄 NULL 后栈水位探针(default_task.c 既有 NULL
* 分支)自动跳过 test 项。不改生成区 / .ioc —— CubeMX 重生成不丢本逻辑。 */
#if !defined(TEST_SUITE_SELECTED)
if (testTaskHandle != NULL) {
vTaskDelete(testTaskHandle);
testTaskHandle = NULL;
}
#endif
/* USER CODE END RTOS_THREADS */
/* Create the event(s) */
/* creation of sys_evt */
sys_evtHandle = osEventFlagsNew(&sys_evt_attributes);
/* USER CODE BEGIN RTOS_EVENTS */
/* add events, ... */
/* USER CODE END RTOS_EVENTS */
}
/* USER CODE BEGIN Header_StartDefaultTask */
/**
* @brief Function implementing the defaultTask thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartDefaultTask */
void StartDefaultTask(void *argument)
{
/* USER CODE BEGIN StartDefaultTask */
/* 任务体已迁出至 App/task/default_task.c(2026-09-25):
* 本文件只保留 CubeMX 接线(任务创建)与崩溃钩子 */
default_task_func(argument);
/* USER CODE END StartDefaultTask */
}
/* USER CODE BEGIN Header_StartNetTask */
/**
* @brief Function implementing the netTask thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartNetTask */
void StartNetTask(void *argument)
{
/* USER CODE BEGIN StartNetTask */
net_task_func(argument);
/* USER CODE END StartNetTask */
}
/* USER CODE BEGIN Header_StartFtpTask */
/**
* @brief Function implementing the ftpTask thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartFtpTask */
__weak void StartFtpTask(void *argument)
{
/* USER CODE BEGIN StartFtpTask */
task_wd_bind(TASK_WD_FTP); /* FTL 心跳路由:登记本任务句柄(入口一次) */
/* 等网络 + 存储都真正就绪后再 listen,取代拍脑袋的 7s 魔法延时 */
/*
sys_evtHandle:等待哪个事件组
SYS_EVT_NET_READY | SYS_EVT_STORAGE_READY:要等哪几位(1u<<0 网络、1u<<1 存储)
osFlagsWaitAll:请求的位全部置位才唤醒(即"网络且存储都就绪")
100u:单次等待分片时长——2026-10-02 台架修复由 osWaitForever 改为 100ms
分片循环:IWDG 必需掩码含 FTP 位,屏障等待态必须能喂心跳(见下方注释)
*/
if (sys_evtHandle != NULL) {
/* osFlagsNoClear 必须带:就绪位是**状态**不是命令,消费型等待会把位清掉,
* 导致后面 WaitAll(两位) 的等待者永远等不到(2026-09-21 实测:netTask 先
* 消费 STORAGE_READY、uiTask/timeSync 再消费 NET_READY → 本任务永不唤醒、
* FTP 21 端口从未监听)。
* 2026-10-02 台架修复:等待改 100ms 分片 + 每片喂心跳(与 timeSync/uiTask/
* netTask 入口同模式)——IWDG 必需掩码含 FTP 位,原 osWaitForever 无心跳,
* IWDG 启动(存储就绪)时网络未必就绪 → 掩码 800ms 不可达 → 停喂 → 复位循环 */
for (;;) {
uint32_t r = osEventFlagsWait(sys_evtHandle,
SYS_EVT_NET_READY | SYS_EVT_STORAGE_READY,
osFlagsWaitAll | osFlagsNoClear, 100u);
if ((r & osFlagsError) == 0u) {
break; /* 两位均就绪(状态位,未被消费) */
}
lftpd_task_wd_beat(); /* 看门狗:分片喂心跳(屏障等待态) */
}
}
lftpd_t ftp;
lftpd_start("/", 21, &ftp);
/* 广播"FTP 服务就绪"(预留给依赖 FTP 的后续功能) */
if (sys_evtHandle != NULL) {
osEventFlagsSet(sys_evtHandle, SYS_EVT_FTP_READY);
}
/* USER CODE END StartFtpTask */
}
/* USER CODE BEGIN Header_start_test_task */
/**
* @brief Function implementing the testTask thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_start_test_task */
__weak void start_test_task(void *argument)
{
/* USER CODE BEGIN start_test_task */
/* Infinite loop */
for(;;)
{
osDelay(1);
}
/* USER CODE END start_test_task */
}
/* USER CODE BEGIN Header_StartUiTask */
/**
* @brief Function implementing the uiTask thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartUiTask */
void StartUiTask(void *argument)
{
/* USER CODE BEGIN StartUiTask */
ui_task_func(argument);
/* USER CODE END StartUiTask */
}
/* Private application code --------------------------------------------------*/
/* USER CODE BEGIN Application */
/*
* 断言失败钩子(2026-09-29,配合 IWDG;见 Inc/FreeRTOSConfig.h 的 configASSERT)。
* 与栈溢出钩子同为"内核状态已不可信"路径:裸 UART 有界输出 + 主动复位,
* 不走 printf / 内核对象 / 控制台锁。
* 设计取舍(docs/子系统设计/看门狗设计说明.md §4.4):
* - 上报 file/line 是断言唯一有价值的现场信息,故必须打印(早于 TCB 名之类);
* - 打印后**主动 NVIC_SystemReset()** 而非空转:断言点未必能让 IWDG 抓到
* (若恰在启动段,IWDG 尚未启动),主动复位保证任何断言都能自恢复;
* 代价是 RCC_CSR 记 SFTRSTF 而非 IWDGRSTF——故上报串里写死 ASSERT 字样,
* 使"重启原因"可由日志区分(软件复位 + 上一条是 ASSERT = 断言导致)。
*/
void vApplicationAssertFailed(const char *file, long line)
{
static const char msg_head[] = "\r\n[ERR] ASSERT FAILED ";
const char *p = file;
/* 缓冲一律 static(不放栈上):本钩子是"内核状态已不可信"的终点路径,打印完即
* 复位,不存在并发/重入;放栈上会让 armlink 把这 81 B 挂进每个任务的最坏链
* (configASSERT 在 FreeRTOS 内部可达 → 每条链尾都有本钩子) */
static char num[16];
int i;
/* 文件名只取末段(全路径太长且无信息量) */
if (p != NULL) {
const char *q = p;
while (*q != '\0') {
if ((*q == '/') || (*q == '\\')) {
p = q + 1;
}
q++;
}
/* 有界拷贝:上限 48 字符 */
{
static char name[49]; /* static 存储(见上),作用域收到本块 */
uint16_t len = 0u;
while ((len < 48u) && (p[len] != '\0')) {
name[len] = p[len];
len++;
}
name[len] = '\0';
/* ⚠ 与栈溢出钩子同理,刻意不包 begin/end(三条完整理由写在同文件
* vApplicationStackOverflowHook 上方那一处,这里不复述以免长成第二真源)。
* 另外修正先前版本自身的一处不完整:那对 begin/end 只包住了"有文件名"
* 这一支,else 分支(无文件名)根本不在窗口里 —— 半套独比亚没有更误导人。 */
dbg_console_write_raw(msg_head, (unsigned)(sizeof(msg_head) - 1u));
dbg_console_write_raw(name, (unsigned)len);
}
} else {
dbg_console_write_raw(msg_head, (unsigned)(sizeof(msg_head) - 1u));
dbg_console_write_raw("(no file)", 9u);
}
dbg_console_write_raw(":", 1u);
/* 行号转十进制(不用 snprintf:钩子可能在临界区,且避免库依赖) */
i = 0;
if (line <= 0) {
num[i++] = '0';
} else {
int n = 0;
int j;
while ((line > 0) && (n < 15)) {
num[n++] = (char)('0' + (line % 10)); /* 先逆序填入 */
line /= 10;
}
for (j = 0; j < n / 2; j++) { /* 原地反转:省一个 16 B 缓冲 */
char t = num[j];
num[j] = num[n - 1 - j];
num[n - 1 - j] = t;
}
i = n;
}
dbg_console_write_raw(num, (unsigned)i);
dbg_console_write_raw(" (resetting)\r\n", 15u);
/* 无需 HAL_Delay:dbg_console_write_raw → con_tx_bounded 已等到 TC(移位寄存器
* 排空)才返回;去掉 HAL_Delay 亦少一条 HAL 依赖(断言现场 tick 未必可信) */
NVIC_SystemReset();
}
/* USER CODE END Application */