From e0b02d397cefd636ff2e9576a6ce8b92090e94eb Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Sun, 4 Oct 2026 08:30:30 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20P2=20=E5=85=A8=E9=83=A8=20?= =?UTF-8?q?10=20=E9=A1=B9=EF=BC=9A=E9=94=99=E8=AF=AF=E4=BC=A0=E6=92=AD/?= =?UTF-8?q?=E7=AB=9E=E6=80=81/=E9=97=A8=E6=8E=A7/=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E5=A4=B1=E7=9C=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 待办池 docs/待处理内容.md §3.2 的 10 个 P2 全部闭环;逐项落代码 + 注释说明。 P2-1 cfg_file: 受管键行超 CFG_LINE_TEST_MAX 拒绝保存(防重复键行) P2-2 ch395f: DMA 失败传播状态 + 锁存通道故障 + 读不拷不可信数据 P2-7 net_select: LISTENING 不再恒判可读(消除 select 忙轮询地雷) P2-8 time_sync: 核实 L3 pending 改写已闭环(net_recv_to 分片 + 总预算) P2-9 sys_log: 挂载改幂等 + 卷锁,defaultTask 2s 重试建 /log P2-10 sys_log: s_read_raw 纳入卷锁 try(读者/写者 unlink 互斥) P2-11 net_task: RECONFIG 改 osEventFlagsWait 原子取走 + 清除 P2-12 app_cfg: APP_*_ENABLE 如实标注为未接线占位,删“开关”说法 P2-13 freertos: 产品构建 vTaskDelete(testTask) 回收 8KB CCM 栈 P2-15 download.bat: 先删旧日志 + 增“未产出日志”判定 P2-13 用 USER CODE 区回收而非生成区 #if 门控:生成区改动会被 CubeMX 重生成覆盖,本方案重生成不丢。P2-12 因产品定义 B0 未定、APP_NET_ENABLE 无单一接线点,按修复建议备选(删“开关”说法)处理。 验证:@build 0 错 0 警;主机单测 watchdog 39/39 + sys_clock 100/100 + cfg_file 149/149(新增 TC-CFG-044);pre-commit --all-files 17/17。 --- App/app/app_cfg.h | 21 +++++-- App/cfg/cfg_file.c | 81 +++++++++++++++++++++++++- App/log/sys_log_mcu.c | 49 ++++++++++++++-- App/task/default_task.c | 7 +++ App/task/net_task.c | 41 +++++++------ Drivers/BSP/CH395F/ch395f.c | 64 +++++++++++++------- Drivers/BSP/CH395F/ch395f.h | 4 +- Drivers/BSP/NET/net_select.c | 19 +++--- MDK-ARM/download.bat | 10 ++++ Src/freertos.c | 14 +++++ docs/内存管理/RAM使用分析与优化方案.md | 4 +- docs/待处理内容.md | 37 ++++++------ docs/架构设计/项目架构总览.md | 4 +- docs/环境与部署/项目部署环境要求.md | 8 +-- test/board/test_config.h | 14 +++-- test/host/cfg_file/cfg_file_test.c | 48 +++++++++++++++ 16 files changed, 332 insertions(+), 93 deletions(-) diff --git a/App/app/app_cfg.h b/App/app/app_cfg.h index 7d89e76d..c7464ff5 100644 --- a/App/app/app_cfg.h +++ b/App/app/app_cfg.h @@ -1,11 +1,18 @@ /* * 模块名称:Application Configuration - * 模块功能:应用层全局配置宏,控制功能模块的编译开关 + * 模块功能:应用层全局配置宏(**预留占位,当前均未接线**) * 适用平台:STM32F407ZGTx * 作者:王建锋 * 创建日期:2026-07-19 * 修改记录: * v1.0 2026-07-19 王建锋 创建初始版本 + * v1.1 2026-10-04 王建锋 P2-12:下列宏此前被表述为"功能模块使能开关",但全库 + * 零代码引用(grep 仅本文件有定义)——"开关名存实亡"会 + * 误导评审者以为模块可裁剪。现如实标注为**预留占位**: + * 当前**不门控任何代码**,各模块一律编译进产品。是否/ + * 如何接线取决于产品定义(待处理内容.md §2 阶段 B0), + * 确认后再在真实初始化点(如 app_main.c 的 rs485_init / + * tpafe5160_init)加门控;在此之前**不要**把它们当开关用。 */ #ifndef APP_CFG_H @@ -16,14 +23,18 @@ extern "C" { #endif /* - * 系统时钟源选择 - * 0 - DWT CYCCNT(默认,0 外设开销) - * 1 - TIM2(32-bit 定时器,独立于 DWT) + * 系统时钟源选择(**预留占位,未接线**) + * 0 - DWT CYCCNT(当前实现:console_log.c 直接使用 DWT,未读本宏) + * 1 - TIM2(未实现) + * 说明:本宏当前无任何消费点;实际的"与时间源无关硬上界"由 console_log.c 内联 + * 使能 DWT 实现,不经过本宏。 */ #define APP_SYS_TIME_SRC 0 /* - * 功能模块使能开关 + * 功能模块"预留开关"(**当前均未接线,不门控任何代码**) + * 三个宏一律保持定义(= 模块编译进产品);它们**不产生任何编译期效果**, + * 仅作为产品定义(B0)落定后接入真实门控的占位锚点。 */ #define APP_NET_ENABLE #define APP_ADC_ENABLE diff --git a/App/cfg/cfg_file.c b/App/cfg/cfg_file.c index f1520eac..3df39ced 100644 --- a/App/cfg/cfg_file.c +++ b/App/cfg/cfg_file.c @@ -18,6 +18,9 @@ * 回工厂默认 → 基线粘住 → 用户配置销毁链);save 输出上限 * 由 CFG_OUT_MAX 收紧至 CFG_FILE_MAX_LEN;s_write_doc 写盘 * 唯一闸口拒写超长文档;load/poll 超长处置补自检留痕 + * v1.3 2026-10-04 王建锋 P2-1:受管键行超 CFG_LINE_TEST_MAX 时拒绝保存—— + * 原实现 Pass1 静默跳过超长行,Pass2 遂在段头补写新 KV 且 + * 原文超长行原样保留 → 同一键出现两行(旧值残留、静默截断) * 函数说明:引擎状态非重入——调用方须收敛同一任务(defaultTask:poll/save,见 P1-3; * 启动期 load 先于任务竞争);FS 卷并发由 FatFS REENTRANT 卷锁保证 * (见《配置文件设计说明》D15 A+ 方案) @@ -822,13 +825,67 @@ static int s_surgical(char *p_in, size_t in_len, char *p_out, size_t out_cap, si return 0; } +/* + * 函数功能:判断一行(非修改式)的键名是否为指定段的受管键 + * 入口参数:p_line - 行文本(只读) const char * + * i_sec - 段索引 int + * 返回值:1 - 是受管键;0 - 不是(无 '=' 或键名不匹配) int + * 限定条件:仅比较 '=' 前的键名(大小写无关、剥尾部空白),不解析值 + * 函数说明:P2-1 用于"超长行是否落在受管键上"的判定(值可能很长,无法拷入定长缓冲); + * 非修改式,故可安全用于仍供 Pass2 原样保留的行 + */ +static int s_line_key_managed(const char *p_line, int i_sec) { + const char *p_eq = strchr(p_line, '='); + size_t kl; + int k; + + if (p_eq == NULL) { + return 0; + } + kl = (size_t)(p_eq - p_line); + while (kl > 0u && (p_line[kl - 1u] == ' ' || p_line[kl - 1u] == '\t')) { + kl--; + } + for (k = 0; k < (int)s_sections[i_sec].key_count; k++) { + const char *p_name = s_sections[i_sec].p_keys[k].name; + size_t i = 0u; + int eq = 1; + + while (i < kl) { + char ca = p_line[i]; + char cb = p_name[i]; + + if (ca >= 'A' && ca <= 'Z') { + ca = (char)(ca + ('a' - 'A')); + } + if (cb >= 'A' && cb <= 'Z') { + cb = (char)(cb + ('a' - 'A')); + } + if (cb == '\0' || ca != cb) { + eq = 0; + break; + } + i++; + } + if (eq != 0 && p_name[i] == '\0') { + return 1; + } + } + return 0; +} + /* * 函数功能:保存 Pass 1——定位段头行号与各受管键行号 * 入口参数:p_in - 原文本(可修改) char * - * 返回值:无 + * 返回值:0 - 成功;-1 - 受管键行超 CFG_LINE_TEST_MAX(拒绝保存,见 P2-1) int * 限定条件:行末被 NUL 化且不复原——调用方须传一次性副本 + * 函数说明:P2-1(2026-10-04):受管键行超过 CFG_LINE_TEST_MAX 时**不能**静默跳过 + * ——Pass2 会因该行未登记而在段头补写新 KV,同时原文超长行原样保留,导致 + * 同一键出现两行(旧值残留 + 新值,解析取谁取决于行序)。故此处显式拒绝: + * 返回 -1,调用方中止保存、原文件不动、自检留痕。非受管键的超长行照旧跳过 + * (属未知内容,Pass2 原样保留,无冲突)。 */ -static void s_surgical_pass1(char *p_in) { +static int s_surgical_pass1(char *p_in) { char *p = p_in; int line_no = 0; int i; @@ -883,8 +940,16 @@ static void s_surgical_pass1(char *p_in) { } } } + else if (i_cur >= 0) { + /* 超长行(> CFG_LINE_TEST_MAX):若其键名为本段受管键 → 拒绝保存(P2-1); + * 否则按未知内容放过(Pass2 原样保留) */ + if (s_line_key_managed(p_line, i_cur) != 0) { + return -1; + } + } line_no++; } + return 0; } /* @@ -1091,7 +1156,17 @@ int cfg_file_save(void) { } (void)memcpy(p_in1, p_in, in_len + 1u); - s_surgical_pass1((char *)p_in1); + if (s_surgical_pass1((char *)p_in1) != 0) { + /* P2-1:受管键行超 CFG_LINE_TEST_MAX → 中止保存(原文件不动)。此类超长行 + * 只可能来自手工编辑(代码生成的受管 KV 行恒 <127B);若强行外科保存会 + * 产生重复键行,故宁可拒存并留痕,也不静默写坏。 */ + cfg_store_free(p_in1); + cfg_store_free(p_in); + DBG_ERROR("save: managed key line > %uB, aborted (file untouched)", + (unsigned)CFG_LINE_TEST_MAX); + syslog_post(SYSLOG_SELFTEST, "cfg save line too long"); + return -1; + } cfg_store_free(p_in1); diff --git a/App/log/sys_log_mcu.c b/App/log/sys_log_mcu.c index 36e6124b..f6b883f9 100644 --- a/App/log/sys_log_mcu.c +++ b/App/log/sys_log_mcu.c @@ -16,6 +16,12 @@ * v1.3 2026-10-02 王建锋 新增 sys_log_evidence():把看门狗停喂现场 * 证据落 /log/wdev.log(掉电不丢)+ flush 诱导 * (round 7 纯取证,见 docs/子系统设计/看门狗设计说明.md 2026-10-02 变更记录) + * v1.4 2026-10-04 王建锋 P2-9:syslog_mcu_mounted() 改为幂等 + 卷锁保护; + * defaultTask 每 2s 复查,建 /log 曾失败时自动重试 + * (原"仅调用一次"→ 失败后 drain 永久 no-op)。 + * P2-10:s_read_raw() 纳入卷锁 try 语义,与写者 + * s_append 的"容量满 unlink 重建"互斥,消除 + * "UI 读记录(FA_READ) 时文件被 unlink"竞态窗口。 */ /* 头文件包含区 */ @@ -49,6 +55,7 @@ static volatile uint8_t s_preboot_cnt; /* 数组条目数 */ static osMessageQueueId_t s_queue; /* 运行期消息队列(syslog_drain 首次调用时惰性创建) */ static const osMessageQueueAttr_t s_queue_attr = { .name = "syslog" }; static volatile uint8_t s_fs_ready; /* 卷挂载 + /log 目录就绪后置位 */ +static volatile uint8_t s_mount_fail_reported; /* P2-9:建目录失败日志去重(成功复位) */ /* 函数定义区 */ @@ -153,18 +160,28 @@ static int s_append(syslog_id_t id, const char *desc) { * 入口参数:id - 日志 ID syslog_id_t * idx - 记录序号(0 起) uint32_t * p_rec- 输出缓冲(SYSLOG_REC_SIZE 字节,不可为 NULL) uint8_t * - * 返回值:0 - 成功;-1 - 越界/读失败 int + * 返回值:0 - 成功;-1 - 越界/读失败/卷忙 int + * 函数说明:P2-10(2026-10-04):读者纳入卷锁(try),与写者 s_append 的 + * "容量满 unlink 重建"互斥——原实现读时无锁,写者 unlink 与读者 + * open/read 可交错,读到的可能是半删除态文件。try 语义(timeout=0): + * 本路径可由 uiTask 调用,uiTask 在 required_mask 内、须每 100ms 喂 + * 心跳,绝不能阻塞等锁;busy = 本次读失败(UI 下轮刷新重读,非故障)。 */ static int s_read_raw(syslog_id_t id, uint32_t idx, uint8_t *p_rec) { FIL f; FRESULT fr; UINT br = 0u; + int rc; if (p_rec == NULL || idx >= syslog_count(id)) { return -1; } + if (vol_lock_wait(0u) == 0u) { + return -1; /* 卷被占用:本次读失败(非故障,下轮重读) */ + } fr = f_open(&f, s_paths[id], FA_READ); if (fr != FR_OK) { + vol_lock_release(); return -1; } fr = f_lseek(&f, (FSIZE_t)idx * SYSLOG_REC_SIZE); @@ -172,7 +189,9 @@ static int s_read_raw(syslog_id_t id, uint32_t idx, uint8_t *p_rec) { fr = f_read(&f, p_rec, SYSLOG_REC_SIZE, &br); } (void)f_close(&f); - return (fr == FR_OK && br == SYSLOG_REC_SIZE) ? 0 : -1; + rc = (fr == FR_OK && br == SYSLOG_REC_SIZE) ? 0 : -1; + vol_lock_release(); + return rc; } /* @@ -194,20 +213,42 @@ int syslog_mcu_init(void) { * 函数功能:卷挂载成功后的一次性处理(建 /log 目录 + 写"上电/重启"分类记录) * 入口参数:无 * 返回值:无 - * 限定条件:defaultTask f_mount/f_mkfs 成功且置位 SYS_EVT_STORAGE_READY 前调用一次 + * 限定条件:defaultTask f_mount/f_mkfs 成功后调用;**可重复调用(幂等)**—— + * 产品运行期 defaultTask 每 2s 复查一次(见下 P2-9 说明) + * 函数说明:P2-9(2026-10-04):原实现"仅调用一次",若此处建 /log 失败则 + * s_fs_ready 恒 0 → syslog_drain 永久 no-op(运行期日志与自检全部 + * 滞留/丢弃),且无任何重试。现改为幂等:s_fs_ready 已置位则立即 + * 返回;否则重试建目录。defaultTask 在 STORAGE_READY 置位后每 2s + * 调用本函数——卷已就绪但日志后端未就绪时自动补建目录。 + * FS 操作纳入卷锁 try:运行期重试时 ftpTask 可能正操作卷,try 失败 + * = 本轮顺延(下轮再试),绝不阻塞(本任务 = 看门狗喂狗点)。 */ void syslog_mcu_mounted(void) { FILINFO finfo; uint32_t last = 0u; const char *desc = "power on"; + if (s_fs_ready != 0u) { + return; /* 幂等:已就绪 */ + } + /* 卷锁 try:运行期重试时与 ftpTask 卷访问串行;busy = 本轮顺延 */ + if (vol_lock_wait(0u) == 0u) { + return; + } if (f_stat("/log", &finfo) != FR_OK) { FRESULT fr = f_mkdir("/log"); if (fr != FR_OK && fr != FR_EXIST) { - DBG_ERROR("mkdir /log fail (%d)", (int)fr); + /* 失败日志去重:持续失败只报第一次(防 2s 周期刷屏),成功后复位 */ + if (s_mount_fail_reported == 0u) { + s_mount_fail_reported = 1u; + DBG_ERROR("mkdir /log fail (%d), will retry", (int)fr); + } + vol_lock_release(); return; } } + vol_lock_release(); + s_mount_fail_reported = 0u; s_fs_ready = 1u; /* 上电/重启分类:末条 epoch 距 now < SYSLOG_REBOOT_WINDOW_S → 重启,否则上电 */ diff --git a/App/task/default_task.c b/App/task/default_task.c index 705e993a..0783e410 100644 --- a/App/task/default_task.c +++ b/App/task/default_task.c @@ -217,6 +217,13 @@ void default_task_func(void *argument) { syslog_post(SYSLOG_SELFTEST, "filesystem fault"); } } + /* P2-9:卷已就绪(STORAGE_READY)但日志后端未就绪(建 /log 曾失败, + * s_fs_ready 恒 0 → syslog_drain 永久 no-op)→ 2s 周期重试(幂等; + * 就绪后 syslog_mcu_mounted 立即返回,仅一次 volatile 读)。 */ + if (sys_evtHandle != NULL + && (osEventFlagsGet(sys_evtHandle) & SYS_EVT_STORAGE_READY) != 0u) { + syslog_mcu_mounted(); + } cfg_file_poll(); #endif } diff --git a/App/task/net_task.c b/App/task/net_task.c index ba7564b0..67d93e09 100644 --- a/App/task/net_task.c +++ b/App/task/net_task.c @@ -240,26 +240,31 @@ void net_task_func(void const *arg) { task_wd_beat(TASK_WD_NET); /* UI 保存网口配置后请求立即应用:在此重建网络(新 IP/掩码/网关/MAC)。 - * 重建完成后代次号已递增,各 TCP 服务据此重建监听;期间连接会断开(预期)。 */ - if (sys_evtHandle != NULL - && (osEventFlagsGet(sys_evtHandle) & SYS_EVT_NET_RECONFIG) != 0u) { - (void)osEventFlagsClear(sys_evtHandle, SYS_EVT_NET_RECONFIG); - DBG_INFO("netTask: apply new net config (re-init)"); - if (net_stack_bring_up() == 0) { - (void)osEventFlagsSet(sys_evtHandle, SYS_EVT_NET_READY); + * 重建完成后代次号已递增,各 TCP 服务据此重建监听;期间连接会断开(预期)。 + * P2-11(2026-10-04):用 osEventFlagsWait(timeout=0) 原子"取走并清除"RECONFIG + * 位,替换原 Get+Clear 两步;原写法在 Get 与 Clear 之间若有生产者在同一位上 + * Set,该次请求会被 Clear 一并吞掉(丢事件窗口)。 */ + if (sys_evtHandle != NULL) { + uint32_t evt = osEventFlagsWait(sys_evtHandle, SYS_EVT_NET_RECONFIG, + osFlagsWaitAny, 0U); + if ((evt & osFlagsError) == 0u) { + DBG_INFO("netTask: apply new net config (re-init)"); + if (net_stack_bring_up() == 0) { + (void)osEventFlagsSet(sys_evtHandle, SYS_EVT_NET_READY); + s_reinit_retry_at = 0U; + } else { + /* P1-8(2026-10-02):重建失败 → 自检留痕 + 30s 延迟重试(重事件位), + * 新配置不再静默丢失(原实现仅一条 DBG_ERROR 后丢弃) */ + DBG_ERROR("netTask: re-init FAIL, retry in 30s"); + (void)syslog_post(SYSLOG_SELFTEST, "net reinit fail"); + s_reinit_retry_at = (uint32_t)HAL_GetTick() + 30000U; + } + } else if ((s_reinit_retry_at != 0U) + && ((int32_t)((uint32_t)HAL_GetTick() - s_reinit_retry_at) >= 0)) { + /* P1-8:30s 倒计时到 → 重新触发重建(符号差处理 tick 回绕) */ s_reinit_retry_at = 0U; - } else { - /* P1-8(2026-10-02):重建失败 → 自检留痕 + 30s 延迟重试(重事件位), - * 新配置不再静默丢失(原实现仅一条 DBG_ERROR 后丢弃) */ - DBG_ERROR("netTask: re-init FAIL, retry in 30s"); - (void)syslog_post(SYSLOG_SELFTEST, "net reinit fail"); - s_reinit_retry_at = (uint32_t)HAL_GetTick() + 30000U; + (void)osEventFlagsSet(sys_evtHandle, SYS_EVT_NET_RECONFIG); } - } else if ((sys_evtHandle != NULL) && (s_reinit_retry_at != 0U) - && ((int32_t)((uint32_t)HAL_GetTick() - s_reinit_retry_at) >= 0)) { - /* P1-8:30s 倒计时到 → 重新触发重建(符号差处理 tick 回绕) */ - s_reinit_retry_at = 0U; - (void)osEventFlagsSet(sys_evtHandle, SYS_EVT_NET_RECONFIG); } net_poll(); diff --git a/Drivers/BSP/CH395F/ch395f.c b/Drivers/BSP/CH395F/ch395f.c index 7335b6b3..2177ab22 100644 --- a/Drivers/BSP/CH395F/ch395f.c +++ b/Drivers/BSP/CH395F/ch395f.c @@ -10,6 +10,9 @@ * 2026-07-18 王建锋 改进write_send_buf/read_recv_buf使用SPI2 DMA批量传输 * 2026-10-02 王建锋 E-34:3 处字节级 SPI 调用由 HAL_MAX_DELAY 改有限超时(100ms) * + 返回值检查 + 故障锁存(沿程只记一条日志);原 SPI 异常时调用方永久挂死 + * 2026-10-04 王建锋 P2-2:ch395f_spi_dma_transfer 由 void 改为返回状态(0 成功/-1 失败), + * DMA 初始化失败/超时中止均锁存通道故障;read_recv_buf 在 DMA 失败时不再把 + * rx_buf 内容拷入调用方缓冲(改零填充),消除"失败当成功"的静默数据损坏 */ /* 头文件包含区 - 先包含系统头文件,再包含自定义头文件 */ @@ -40,7 +43,7 @@ * 静态变量定义区 - DMA传输相关 */ static volatile uint8_t s_spi2_dma_done = 0U; /* DMA传输完成标志 */ -static uint8_t s_spi_byte_fault = 0U; /* E-34:字节级SPI故障锁存(超时/出错置位,MCU复位清除) */ +static uint8_t s_spi_fault = 0U; /* E-34/P2-2:SPI 通道故障锁存(字节级或 DMA 级超时/出错置位,MCU复位清除) */ __attribute__((aligned(4))) static uint8_t s_spi2_dma_tx_buf[CH395F_DMA_BUF_SIZE]; /* DMA发送缓冲区 */ @@ -52,8 +55,8 @@ static uint8_t s_spi2_dma_rx_buf[CH395F_DMA_BUF_SIZE]; /* DMA接收缓冲区 */ */ static void ch395f_spi_begin(void); static void ch395f_spi_end(void); -static void ch395f_spi_dma_transfer(uint16_t len); -static void ch395f_spi_byte_fault_set(void); +static int ch395f_spi_dma_transfer(uint16_t len); +static void ch395f_spi_fault_set(const char *kind); /* * 私有函数定义区 @@ -82,31 +85,34 @@ static void ch395f_spi_end(void) { } /* - * 函数功能:锁存一次字节级SPI故障(E-34:仅首条故障记日志) - * 入口参数:无 + * 函数功能:锁存一次 SPI 通道故障(E-34/P2-2:仅首条故障记日志) + * 入口参数:kind - 故障类别标签("byte"/"dma-hal"/"dma-timeout") const char* * 返回值:无 * 限定条件:无 - * 函数说明:任一字节级SPI调用超时/失败时调用;置位 s_spi_byte_fault 并记一条日志, + * 函数说明:任一字节级或 DMA 级 SPI 调用超时/失败时调用;置位 s_spi_fault 并记一条日志, * 后续故障静默(防串口刷屏);上层经 ch395f_spi_byte_fault() 查询 */ -static void ch395f_spi_byte_fault_set(void) { - if (s_spi_byte_fault == 0U) { - s_spi_byte_fault = 1U; - DBG_ERROR("SPI byte op fail, channel latched"); +static void ch395f_spi_fault_set(const char *kind) { + if (s_spi_fault == 0U) { + s_spi_fault = 1U; + DBG_ERROR("SPI channel fault latched: %s", kind); } } /* * 函数功能:DMA全双工传输并等待完成 * 入口参数:len - 传输长度(字节) uint16_t 1 - CH395F_DMA_BUF_SIZE + * 返回值:0 - 传输完成且无错误 -1 - 传输失败(HAL 启动失败 / 超时中止) * 限定条件:1. s_spi2_dma_tx_buf中已填充待发送数据 * 2. 需确保DMA传输缓冲区已对齐到4字节边界 * 函数说明:1. 发送s_spi2_dma_tx_buf内容,同时接收数据存入s_spi2_dma_rx_buf * 2. 阻塞等待DMA完成回调置标志位 * 3. 超时(1秒)时强制中止SPI DMA并打印错误信息 * 4. 传输完成后检查SPI错误码并清除溢出标志 + * 5. P2-2(2026-10-04):失败路径锁存通道故障并返回 -1,调用方据此 + * 判定 rx_buf 内容不可信,不再"失败当成功"继续消费 */ -static void ch395f_spi_dma_transfer(uint16_t len) { +static int ch395f_spi_dma_transfer(uint16_t len) { HAL_StatusTypeDef status; uint32_t tickstart; volatile uint32_t loop_cnt = 0U; @@ -119,7 +125,8 @@ static void ch395f_spi_dma_transfer(uint16_t len) { (int)hspi2.State, hspi2.ErrorCode, (int)hspi2.hdmarx->State); - return; + ch395f_spi_fault_set("dma-hal"); + return -1; } tickstart = HAL_GetTick(); @@ -142,7 +149,8 @@ static void ch395f_spi_dma_transfer(uint16_t len) { SPI2->CR1, SPI2->CR2, SPI2->SR); /* 强制中止SPI2 DMA状态,避免死锁 */ HAL_SPI_Abort(&hspi2); - return; + ch395f_spi_fault_set("dma-timeout"); + return -1; } } @@ -152,6 +160,8 @@ static void ch395f_spi_dma_transfer(uint16_t len) { hspi2.ErrorCode, (int)hspi2.State); __HAL_SPI_CLEAR_OVRFLAG(&hspi2); } + + return 0; } /* @@ -200,7 +210,7 @@ uint8_t ch395f_write_cmd(uint8_t cmd) { /* 通过SPI发送命令并接收回显(E-34:有限超时+返回值检查;失败返回0xFF占位并锁存故障) */ if (HAL_SPI_TransmitReceive(&hspi2, &cmd, &rx_data, 1, CH395F_SPI_BYTE_TIMEOUT_MS) != HAL_OK) { - ch395f_spi_byte_fault_set(); + ch395f_spi_fault_set("byte"); } return rx_data; } @@ -217,7 +227,7 @@ uint8_t ch395f_write_data(uint8_t data) { /* 通过SPI发送数据并接收回显(E-34:有限超时+返回值检查) */ if (HAL_SPI_TransmitReceive(&hspi2, &data, &rx_data, 1, CH395F_SPI_BYTE_TIMEOUT_MS) != HAL_OK) { - ch395f_spi_byte_fault_set(); + ch395f_spi_fault_set("byte"); } return rx_data; } @@ -235,20 +245,20 @@ uint8_t ch395f_read_data(void) { /* 发送0xFF作为时钟源,读取芯片返回数据(E-34:有限超时+返回值检查) */ if (HAL_SPI_TransmitReceive(&hspi2, &tx_dummy, &rx_data, 1, CH395F_SPI_BYTE_TIMEOUT_MS) != HAL_OK) { - ch395f_spi_byte_fault_set(); + ch395f_spi_fault_set("byte"); } return rx_data; } /* - * 函数功能:查询字节级SPI故障锁存(E-34) - * 返回值:0 - 正常 1 - 曾发生字节级SPI超时/错误 + * 函数功能:查询 SPI 通道故障锁存(E-34 / P2-2) + * 返回值:0 - 正常 1 - 曾发生 SPI 超时/错误(字节级或 DMA 级) * 限定条件:无 * 函数说明:锁存式(不自动清除,MCU 复位前一直有效);上层据此判定 * "SPI 通道异常"(区别于"无芯片"/"PHY 未链接");仅首条故障记日志 */ uint8_t ch395f_spi_byte_fault(void) { - return s_spi_byte_fault; + return s_spi_fault; } /* @@ -1173,9 +1183,10 @@ void ch395f_write_send_buf(uint8_t sock, uint8_t *p_data, uint16_t len) { /* 数据同步屏障:确保memcpy写入对DMA可见 */ __DSB(); - /* CS低 → DMA批量传输 → CS高 */ + /* CS低 → DMA批量传输 → CS高(P2-2:失败已由 transfer 锁存通道故障并记日志, + * 调用方经 ch395f_spi_byte_fault() 查询;此处无后续处理,失败即放弃本次发送) */ ch395f_spi_begin(); - ch395f_spi_dma_transfer(total); + (void)ch395f_spi_dma_transfer(total); ch395f_spi_end(); } @@ -1193,6 +1204,7 @@ void ch395f_write_send_buf(uint8_t sock, uint8_t *p_data, uint16_t len) { */ void ch395f_read_recv_buf(uint8_t sock, uint8_t *p_data, uint16_t len) { uint16_t total; + int rc; /* 检查输入参数合法性 */ if (p_data == NULL) { @@ -1221,9 +1233,17 @@ void ch395f_read_recv_buf(uint8_t sock, uint8_t *p_data, uint16_t len) { /* CS低 → DMA全双工传输(发送命令头 + 接收数据) → CS高 */ ch395f_spi_begin(); - ch395f_spi_dma_transfer(total); + rc = ch395f_spi_dma_transfer(total); ch395f_spi_end(); + /* P2-2(2026-10-04):DMA 失败时 rx_buf 内容不可信,禁止拷入调用方缓冲 + * (否则把总线残留/半截数据当有效载荷 → 静默数据损坏);改零填充并返回, + * 调用方据 ch395f_spi_byte_fault() 判定通道异常。 */ + if (rc != 0) { + memset(p_data, 0, len); + return; + } + /* 从DMA接收缓冲区偏移4处拷贝有效数据到用户缓冲区 */ memcpy(p_data, &s_spi2_dma_rx_buf[4], len); } diff --git a/Drivers/BSP/CH395F/ch395f.h b/Drivers/BSP/CH395F/ch395f.h index 909f3ed2..cf834643 100644 --- a/Drivers/BSP/CH395F/ch395f.h +++ b/Drivers/BSP/CH395F/ch395f.h @@ -302,8 +302,8 @@ uint8_t ch395f_write_data(uint8_t data); uint8_t ch395f_read_data(void); /* - * 函数功能:查询字节级SPI故障锁存(E-34) - * 返回值:0 - 正常 1 - 曾发生字节级SPI超时/错误 + * 函数功能:查询 SPI 通道故障锁存(E-34 / P2-2) + * 返回值:0 - 正常 1 - 曾发生 SPI 超时/错误(字节级或 DMA 级) * 限定条件:无 * 函数说明:锁存式(MCU 复位前一直有效);上层据此判定"SPI 通道异常" * (区别于"无芯片"/"PHY 未链接");仅首条故障记日志 diff --git a/Drivers/BSP/NET/net_select.c b/Drivers/BSP/NET/net_select.c index b5605df2..ab1146df 100644 --- a/Drivers/BSP/NET/net_select.c +++ b/Drivers/BSP/NET/net_select.c @@ -241,10 +241,16 @@ int net_poll_events(net_pollfd_t *fds, int nfds, int timeout) { * 函数功能:检查指定 Socket 当前是否可读 * 入口参数:sockfd - 待检查的 Socket 描述符 int 0 - NET_MAX_SOCKETS-1 * 出口参数:无 - * 返回值:1 - 可读(监听态有新连接,或已连接态接收缓冲非空);0 - 不可读或描述符无效 int + * 返回值:1 - 可读(接收缓冲非空);0 - 不可读或描述符无效 int * 限定条件:net_init() 已调用 - * 函数说明:1. 监听态 Socket 视为可读(存在可 accept 的新连接) - * 2. 已建立连接且接收缓冲区长度大于 0 时视为可读 + * 函数说明:1. P2-7(2026-10-04):LISTENING 态**不再**恒判可读。本网络层无 + * "待 accept 的挂起连接队列"——新连接到达时 CONNECT 事件把该 socket + * 自身直接转为 ESTABLISHED(handle_connect_event:"监听自身转数据通道"), + * 故监听态恒无数据可读。原实现对 LISTENING 恒返回 1,会使 + * select()/poll() 在监听 socket 上永久立即就绪(忙轮询地雷;当前无 + * 调用方,属潜在地雷)。连接到达经 NET_EVENT_CONNECTED 回调通知。 + * 2. 其余态:readable 由 net_poll 在 RECV_OK 时置位、net_recv_locked + * 读空后清零。避免应用任务直接访问 CH395F SPI(见 Trap 16) */ static int check_socket_readable(int sockfd) { const net_sock_t *p_sock; @@ -254,13 +260,12 @@ static int check_socket_readable(int sockfd) { return 0; } - /* 监听 Socket 有新连接时可读 */ + /* 监听态恒不可读(见上方 P2-7 说明) */ if (NET_SOCK_STATE_LISTENING == p_sock->state) { - return 1; + return 0; } - /* 已建立连接或 UDP 等有数据可读:readable 由 net_poll 在 RECV_OK 时置位、 - * net_recv_locked 读空后清零。避免应用任务直接访问 CH395F SPI(见 Trap 16) */ + /* 有数据可读 */ if (p_sock->readable) { return 1; } diff --git a/MDK-ARM/download.bat b/MDK-ARM/download.bat index 066a4af4..aa8765fb 100644 --- a/MDK-ARM/download.bat +++ b/MDK-ARM/download.bat @@ -22,8 +22,18 @@ exit /b 2 :uv4_ok set UV4=%DTU_KEIL_UV4% +:: Remove stale log first: UV4 -o writes the log only when a download actually +:: runs. A leftover log would otherwise be misreported as this download's +:: result (same guard as build.bat). +if exist "%LOG%" del "%LOG%" + "%UV4%" -f "%PROJ%" -t%TARGET% -j0 -o "%LOG%" +if not exist "%LOG%" ( + echo DOWNLOAD_FAIL: no download log produced - UV4 did not run or hung + exit /b 1 +) + findstr /C:"Application running" "%LOG%" >nul if %ERRORLEVEL% equ 0 ( echo DOWNLOAD_OK: flashed and running diff --git a/Src/freertos.c b/Src/freertos.c index 26d51acb..8312bf15 100644 --- a/Src/freertos.c +++ b/Src/freertos.c @@ -184,6 +184,20 @@ void MX_FREERTOS_Init(void) { /* 本地时间同步任务(PC 推送 TCP):依赖 netTask 处理消息队列 */ timeSyncTaskHandle = osThreadNew(time_sync_task, NULL, &timeSyncTask_attributes); + + /* P2-13(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) */ diff --git a/docs/内存管理/RAM使用分析与优化方案.md b/docs/内存管理/RAM使用分析与优化方案.md index 475f19e3..82fd22e9 100644 --- a/docs/内存管理/RAM使用分析与优化方案.md +++ b/docs/内存管理/RAM使用分析与优化方案.md @@ -36,10 +36,10 @@ | 区域 | 用量 | 容量 | 占比 | |---|---|---|---| -| ER_IROM1 | 265,940 B (259.71 KB) | 1,048,576 B (1024 KB) | 25.4% | +| ER_IROM1 | 266,548 B (260.30 KB) | 1,048,576 B (1024 KB) | 25.4% | | RW_IRAM1 | 84,960 B (82.97 KB) | 131,072 B (128 KB) | 64.8% | | RW_CCM | 49,184 B (48.03 KB) | 65,280 B (64 KB) | 75.3% | -| **Flash (ROM 合计)** | **266,676 B (260.4 KB)** | 1,048,576 B (1024 KB) | **25.4%** | +| **Flash (ROM 合计)** | **267,288 B (261.0 KB)** | 1,048,576 B (1024 KB) | **25.5%** | 各区的容量与常量来源: diff --git a/docs/待处理内容.md b/docs/待处理内容.md index 2b54262b..81d63d98 100644 --- a/docs/待处理内容.md +++ b/docs/待处理内容.md @@ -39,7 +39,7 @@ |---|---|---| | **P0** | 0 | **P0-1 已闭环**:2026-09-18 修复(cfg_file 临时缓冲改走 FreeRTOS 堆)+ 2026-10-03 真机 `@test` PASS + 2026-10-04 独立复跑 PASS(见 §4 说明与 `子系统设计/配置文件设计说明.md` v1.13) | | **P1** | 2 (+1 收尾) | **P1-1 ADC 采样路径产品构建中死亡**;**P1-7 MAC 写入顺序**(待 CH395F 手册 §9.2.1);P1-10 测试固件构建实测闭环(主体已闭环,见 §3.1) | -| **P2** | 10 | 健壮性/错误传播/文档失真:P2-1/2/7/8/9/10/11/12/13/15(详见 §3.2) | +| **P2** | 0 | **全部销项(2026-10-04)**:P2-1/2/7/8/9/10/11/12/13/15 共 10 项已全部闭环,明细与证据见 §3.2 | | **P3** | 6 | 卫生项:P3-1/2/4/6/8/11(其余已销项),可随其他修改顺手处理 | | 登记簿 | 10 | 网络:N-2(49s 日志静默,**低风险观察项**)、N-3(L5 基线重验)、N-4(pending 二期)、N-16(A2/A4 专项负向验证)、N-17(探针窗口心跳缺口);存储/日志:**S-1(defaultTask 栈余量 10%,需尽快处理)**、S-2(boot_log 存档)、S-3(USART1 TX-only 根治)、S-4(dhara 重试自旋)、S-5(可选全序列卷锁) | | 测试/基础设施 | 5 | T-7(CI runner 未就绪)+ 测试架构专项 **S12**(断言宏)、**S1-c**(可选单测)、**A-4**(采集链路测试约束)、**A-5 残余**(CI 体积门禁盲区) | @@ -92,7 +92,7 @@ | **C1** | **pending 二期(ACCEPT + SEND)** | 沿一期机制扩展;同期消化 E-25(net_select 语义)、E-24/E-27/E-28(跨上下文收口);基线重锁 + 探针族扩充 + 看门狗接 selftest。**设计基线见 §5.1** | 第二档 #2 / N-4 | | **C3** | **存储侧日志管线**(消化 N-2) | 丢弃可观测性**代码已具备**(`console_log.c` `s_con_drop`+`[ERR] CONSOLE dropped`、`sys_log_mcu.c` 队列满 `DBG_ERROR`、TX 僵尸通道自愈);V5 30 min 日志**未复现 49s 静默**(运行期 >10s 静默全为 ~28s、均在 `e02_probe` 窗口内=N-17)。剩余:与 S8(非阻塞日志)合并评估 + **S-3(USART1 TX-only 结构根治)** | N-2 / S-3 | | **C4** ⚠️ | **defaultTask 栈余量**(S-1,**需尽快处理**) | **2026-10-04 实测 3664/4096(余量 432 B ≈ 10%,远低于 30% 判据)**。处置:栈 4096→4608(CCM 堆余量可覆盖 +512B)或 GC 链局部削减;**建议随阶段 B 之前先做** | S-1 | -| **C5** | 一档剩余 + P2 收口 | E-49(RS485 DE 收尾)、E-15(ringbuf 定去留——**已确认零调用者,倾向删除**)、E-61(SD2506 注释订正)、E-16(cfg_file 段名长度)、E-26(UDP 定案,倾向移除)、E-23、E-30 + P2-1/2/7/8/9/10/11/12/13/15 | 一档/二档 | +| **C5** | 一档剩余 + P2 已收口 | E-49(RS485 DE 收尾)、E-15(ringbuf 定去留——**已确认零调用者,倾向删除**)、E-61(SD2506 注释订正)、E-16(cfg_file 段名长度)、E-26(UDP 定案,倾向移除)、E-23、E-30(P2 十项已于 2026-10-04 全部销项,见 §3.2) | 一档/二档 | | **C6** | **USART1 TX-only 结构根治**(S-3) | 灭 ORE 僵尸机制根源:改 `.ioc` 令 USART1 不使能 RX / PA10 不复用为 `USART1_RX` → CubeMX 重生成 → 跑 `check_cubemx_survival.py`。属结构性改动,需单开一轮 + 复跑台架闭环 | S-3 | ### 2.4 阶段 D:验证与卫生(常态化) @@ -128,7 +128,7 @@ #### P1-1 ADC 采样数据路径产品构建中死亡 - **位置**:`App/app/app_main.c`(`tpafe5160_start_conv_irq()` 仅启动时调用一次)+ `Drivers/BSP/TPAFE5160/tpafe5160.c` - **证据**:启动序列调用一次 `tpafe5160_start_conv_irq()`;全仓库无周期性 CONVST 重触发;`data_ready` / `get_buf` 无任何消费者(`adc_task.c` 是空桩)。 -- **影响**:上电后仅采集 1 个样本,此后数据永久陈旧;`APP_ADC_ENABLE` 开关又无人引用(见 P2-12),形成"看似启用实则失效"的假象。 +- **影响**:上电后仅采集 1 个样本,此后数据永久陈旧;且 `APP_ADC_ENABLE` 为未接线占位(P2-12 已销项:已如实标注,不再冒充开关),形成"看似启用实则失效"的假象。 - **修复建议**:周期 CONVST 重触发(定时器或任务)+ 数据消费任务;若基线版本有意不实现采集,应删除启动调用并以 `APP_ADC_ENABLE` 真实门控。 - **置信度**:high(代码级确证);P0/P1 定级取决于产品定义(见 §4 待确认)。 @@ -149,20 +149,23 @@ - **剩余**:① CH395F 套件(`TEST_SUITE_CH395F`,HW 全阶段 / NET 层 7 配置)实际构建确认链接通过——PHASE5 三静态缓冲 3×4093B 仍在,理论余量足够但未实测;② `@test` 闭环;③ **不建议**扩 scatter 到 192KB(破坏既有克隆芯片兼容决策)。 - **置信度**:high(基线数据来自 2026-09-21 map;测试固件构建未复验)。 -### 3.2 P2(未闭环 10 项) +### 3.2 P2 —— ✅ 全部销项(2026-10-04) -| # | 位置 | 问题 | 修复建议 | 置信度 | -|---|---|---|---|---| -| P2-1 | `App/cfg/cfg_file.c` | 受管键行 >127 字符时外科保存产生重复键行(值静默截断,旧行残留;Pass1 匹配仍受 `CFG_LINE_TEST_MAX=127` 限制) | 行长超限时拒绝保存并报错,或整卷重写 | high | -| P2-2 | `Drivers/BSP/CH395F/ch395f.c` | `ch395f_spi_dma_transfer` 为 void:DMA 超时/abort 后不向调用方传播错误(调用方当成功继续 → 静默数据损坏) | 超时路径置错误标志并经返回值/查询接口上报 | high | -| P2-7 | `Drivers/BSP/NET/net_select.c` `check_socket_readable` | LISTENING 视为恒可读 → 单连接模式下对监听 socket select(readfds) 永久立即返回(当前无调用方,潜在地雷) | LISTENING 仅在写集合/异常集合计数可读,或明确文档化 | high | -| P2-8 | `App/sync/time_sync.c` + `Drivers/BSP/NET/net_config.h` | `NET_RECV_TIMEOUT_MS=30000`;且 `time_sync.c` 的 `net_recv` 现传 timeout=0(**无限等待**)——静默对端使同步任务永久卡死 | 同步收包单独用短超时(如 3s),与通用收发超时分离 | high | -| P2-9 | `App/log/sys_log_mcu.c` | 挂载时 mkdir 失败 → `s_fs_ready` 不置位,drain 永久 no-op 无重试(`syslog_mcu_mounted` 仅调用一次) | 周期重试挂载/建目录 | medium | -| P2-10 | `App/log/sys_log_mcu.c` | 容量满 unlink 与 UI 读记录(FA_READ)存在竞态窗口 | unlink 前等待读者释放,或双文件轮转 | medium | -| P2-11 | `App/task/net_task.c` | RECONFIG 事件用 `osEventFlagsGet`+Clear,非原子(检查与清除间可丢事件) | 改 `osEventFlagsWait` 原子获取 | high | -| P2-12 | `App/app/app_cfg.h` | `APP_NET_ENABLE`/`APP_ADC_ENABLE`/`APP_RS485_ENABLE` 全仓库无 .c 引用,纯装饰 | 真实接入模块门控,或从文档/注释中删除"开关"说法 | high | -| P2-13 | `Src/freertos.c` | testTask 无条件创建,产品构建空转占 2048 words(8KB,CCM 堆) | 按 `test_config.h` 门控创建 | high | -| P2-15 | `MDK-ARM/download.bat` | 调用 UV4 前不删旧 `download_log.txt`:陈旧 "Application running" 掩盖失败烧录 | 调用前先删旧日志 | high | +P2-1/2/7/8/9/10/11/12/13/15 共 10 项已全部闭环(代码 + 主机单测 + `@build` 0/0)。 +**按"只保留待办"约定,明细条目已删除**;结论与证据沉淀进各模块文档,此处只留索引: + +| # | 位置 | 闭环方式(摘要) | 证据落点 | +|---|---|---|---| +| P2-1 | `App/cfg/cfg_file.c` | 受管键行 > `CFG_LINE_TEST_MAX`(127) 时 `s_surgical_pass1` 返回 -1 → 拒绝保存、原文件不动、自检留痕(消除"超长行静默跳过 → 重复键行") | 本文件 §3.2;主机单测 TC-CFG-044(`test/host/cfg_file/cfg_file_test.c`) | +| P2-2 | `Drivers/BSP/CH395F/ch395f.c` | `ch395f_spi_dma_transfer` 改返回状态;失败锁存通道故障(`ch395f_spi_byte_fault()` 现覆盖字节级 + DMA 级);`read_recv_buf` 失败时不再拷入不可信 rx 数据(改零填充) | 本文件 §3.2;`ch395f.c` v 变更记录 | +| P2-7 | `Drivers/BSP/NET/net_select.c` | `check_socket_readable` 对 LISTENING 不再恒返回 1(本层无挂起 accept 队列,CONNECT 事件直接把该 socket 转 ESTABLISHED)→ 消除 select/poll 忙轮询地雷 | 本文件 §3.2;`net_select.c` 函数注释 | +| P2-8 | `App/sync/time_sync.c` | **核实已由 L3 pending 改写闭环**:收包用 `net_recv_to`(≤50ms 分片 + 30s 总预算 + 每片喂心跳),无 `net_recv` 无限等待 | 本文件 §3.2;`time_sync.c` 头注释 | +| P2-9 | `App/log/sys_log_mcu.c` | `syslog_mcu_mounted()` 改幂等 + 卷锁保护;`defaultTask` 每 2s 复查(`STORAGE_READY` 已置位但日志后端未就绪时重试建 `/log`) | 本文件 §3.2;`sys_log_mcu.c` v1.4 | +| P2-10 | `App/log/sys_log_mcu.c` | `s_read_raw()` 纳入卷锁 try(读者 vs 写者 unlink 互斥),消除"UI 读记录时文件被 unlink"竞态 | 本文件 §3.2;`sys_log_mcu.c` v1.4 | +| P2-11 | `App/task/net_task.c` | RECONFIG 位改用 `osEventFlagsWait(timeout=0)` 原子取走+清除,替换 `Get`+`Clear` 两步(消除丢事件窗口) | 本文件 §3.2;`net_task.c` 主循环注释 | +| P2-12 | `App/app/app_cfg.h` | 三个 `APP_*_ENABLE`(及 `APP_SYS_TIME_SRC`)全库零引用 → 如实标注为"未接线占位",删除"开关"说法(同步 `项目部署环境要求.md`) | 本文件 §3.2;`app_cfg.h` v1.1 | +| P2-13 | `Src/freertos.c` | 产品构建(`!TEST_SUITE_SELECTED`)下 `MX_FREERTOS_Init()` 于内核启动前 `vTaskDelete(testTask)` 回收 8KB CCM 栈(不改生成区/.ioc,重生成不丢) | 本文件 §3.2;`项目架构总览.md` §2.1 | +| P2-15 | `MDK-ARM/download.bat` | 调 UV4 前先删旧 `download_log.txt` + 增"未产出日志"判定(对齐 `build.bat`),消除陈旧 "Application running" 掩盖烧录失败 | 本文件 §3.2;`download.bat` 注释 | > **已销项 P2(勿重复报)**:P2-3(`gd5f_bbt_save` 返回值检查)、P2-4(`dhara_nand_read` 检查 `gd5f_read_from_cache`)、P2-5(`sd2506.c` 负温 `-(int8_t)(val & 0x7F)`)、P2-6(SD2506 CTR1 写禁止)、P2-14(`build.bat` 删旧日志)、P2-16(AGENTS.md 栈表)。 @@ -304,7 +307,7 @@ S14 已在 workflow 显式提示"体积门禁未执行"(`::notice::`)并在 1. TPAFE5160 是否有自动扫描模式(决定 P1-1 定 P0 还是 P1)。 2. CH395F 手册 §9.2.1:`INIT_CH395` 是否锁存 MAC 寄存器(决定 P1-7 修复紧迫度)。 -3. `APP_ADC_ENABLE` / `APP_RS485_ENABLE` 是否应在产品构建真实门控模块(关联 P1-1、P2-12、P3-11)。 +3. `APP_ADC_ENABLE` / `APP_RS485_ENABLE` 是否应在产品构建真实门控模块(关联 P1-1、P3-11;P2-12 已销项——宏已如实标注为"未接线占位",此处确认的是**是否接入真实门控**)。 4. RS-485 PD0 DE 的半双工方向语义需硬件实测确认(本次仅静态审查)。 --- diff --git a/docs/架构设计/项目架构总览.md b/docs/架构设计/项目架构总览.md index 46b8f966..d2613178 100644 --- a/docs/架构设计/项目架构总览.md +++ b/docs/架构设计/项目架构总览.md @@ -55,13 +55,13 @@ | `defaultTask` | 4096 | Normal | FatFS 挂载(无卷 `f_mkfs`)→ 卷健康探针/自愈 → `net_cfg_load`(配置引擎属主)→ 置 `STORAGE_READY`(= 存储+配置就绪)→ 日志落盘(唯一写者,`syslog_drain`)+ 配置保存执行/2s poll + LED 闪烁 + **栈水位观测**(`stack_probe_poll`:首次 5s 后报告,此后仅在出现更深峰值时打印) | `StartDefaultTask` | | `netTask` | 4096 | Normal | 等存储(配置已由 defaultTask 就绪,本任务只读 RAM 副本)→ `net_stack_bring_up()`(不触碰配置文件引擎)→ 置 `NET_READY` → `net_poll` 轮询(CH395F 硬件层测试模式下入口自挂起) | `StartNetTask` | | `ftpTask` | 4096 | Normal | 等网络**且**存储(`WaitAll`)→ `lftpd_start("/", 21)` → 单客户端会话循环(断开后重开监听) | `StartFtpTask` | -| `testTask` | 8192 | BelowNormal | 测试套件入口(`test/board/test_config.h` 选择;产品构建空转) | `start_test_task` | +| `testTask` | 8192 | BelowNormal | 测试套件入口(`test/board/test_config.h` 选择)。**产品构建不创建**:CubeMX 生成区无条件建后,`MX_FREERTOS_Init()` 在未选套件时(`!TEST_SUITE_SELECTED`)立即 `vTaskDelete` 回收其 8KB CCM 栈(P2-13,2026-10-04) | `start_test_task` | | `uiTask` | 16384 | BelowNormal | 等网络 → `ui_port_init`/`ui_menu_init`/`ui_standby_init`(空闲 60s 切待机屏)/`ui_net_sink` → 10ms 刷新 | `ui_task_func` | | `timeSyncTask` | 2048 | Normal | 等网络 → TCP :8888 收 PC 推送时间 → 写 RTC | `time_sync_task` | > `idle`(128 words)/ `timer`(256 words)由 FreeRTOS 内核创建。所有任务栈从 48KB heap_4 堆分配。 > **栈实测与预算**(2026-09-21,工具 `scripts/stack_report.py` = Keil 静态调用图 + 串口 `[STACK]` 水位;详见 `docs/内存管理/RAM使用分析与优化方案.md` §0.6): -> defaultTask 静态最坏 2776B/4096B(余量 32%)、ftpTask 实测 **1184B/4096B(余量 71%)**(lftpd 大局部缓冲静态化前为 3480B / 15%,见内存方案 §1.4)、uiTask 实测 3824B/16384B(缩栈空间大,内存方案 §1.3 目标 6KB)、netTask 588B、timeSyncTask 420B、testTask(产品构建)348B。 +> defaultTask 静态最坏 2776B/4096B(余量 32%)、ftpTask 实测 **1184B/4096B(余量 71%)**(lftpd 大局部缓冲静态化前为 3480B / 15%,见内存方案 §1.4)、uiTask 实测 3824B/16384B(缩栈空间大,内存方案 §1.3 目标 6KB)、netTask 588B、timeSyncTask 420B、testTask 348B(2026-09-21 实测;P2-13 后产品构建已不再创建该任务,仅测试构建保留)。 > ⚠ 改栈或加大栈内局部变量前后**必须**重跑该脚本——静态分析低估含递归/间接调用的链(优化前 ftpTask:静态 2528B vs 实测 3480B)。 ### 2.2 就绪屏障(Event Flags,取代魔法延时) diff --git a/docs/环境与部署/项目部署环境要求.md b/docs/环境与部署/项目部署环境要求.md index 33a4eabb..be0e4d62 100644 --- a/docs/环境与部署/项目部署环境要求.md +++ b/docs/环境与部署/项目部署环境要求.md @@ -488,9 +488,9 @@ $env:DTU_KEIL_UV4 = 'D:\NotExist\UV4.exe' # 仅当前会话,不动用户级 |---|---|---|---| | CH395F 以太网 | SPI2 | PB12 CS / PB13 SCK / PB14 SDO / PB15 SDI | PHY 强制 100M 全双工;SPI 分频系数必须为 8(PCB 设计约束,速度受限) | | GD5F2GQ5UE NAND(2 Gbit) | SPI1 | PE0 CS / PB3 SCK / PB4 MISO / PB5 MOSI / PB8 WP / PE1 HOLD | FatFS(FAT32)+ dhara FTL;上电无卷自动 `f_mkfs` | -| TPAFE5160 ADC | 并行 16 位 | PG0~PG15 数据 / PD3 RD / PD4 CONVST / PD7 BUSY | `APP_ADC_ENABLE` 开关 | +| TPAFE5160 ADC | 并行 16 位 | PG0~PG15 数据 / PD3 RD / PD4 CONVST / PD7 BUSY | 编译期常驻(`app_cfg.h` 的 `APP_ADC_ENABLE` 为**未接线占位**,不门控本模块) | | USART1(调试串口) | UART | PA9 TX / PA10 RX | 115200 8N1;printf 日志 + `@test` 采集通道 | -| UART5(RS-485) | UART | PC12 TX / PD2 RX / PD0 DE | 半双工;`APP_RS485_ENABLE` 开关 | +| UART5(RS-485) | UART | PC12 TX / PD2 RX / PD0 DE | 半双工;编译期常驻(`APP_RS485_ENABLE` 为**未接线占位**) | | 状态 LED ×6 | GPIO | PC4 / PC5 / PB1 / PB2 / PF11 / PF12 | **低电平点亮** | ### 3.3 资源预算(部署红线) @@ -529,9 +529,7 @@ $env:DTU_KEIL_UV4 = 'D:\NotExist\UV4.exe' # 仅当前会话,不动用户级 | 开关 | 位置 | 当前值 | 说明 | |---|---|---|---| -| `APP_NET_ENABLE` | `App/app/app_cfg.h` | 开 | 网络(CH395F + 全部 TCP 服务) | -| `APP_ADC_ENABLE` | 同上 | 开 | TPAFE5160 采样 | -| `APP_RS485_ENABLE` | 同上 | 开 | RS-485 通信 | +| `APP_NET_ENABLE` / `APP_ADC_ENABLE` / `APP_RS485_ENABLE` | `App/app/app_cfg.h` | 全定义 | **未接线占位,不门控任何代码**(P2-12,2026-10-04 核实):三个宏全库零消费点,各模块一律编译进产品;"能否裁剪"取决于产品定义(待处理内容.md §2 阶段 B0),落定后再接入真实门控。**不要把它们当开关** | | `TEST_SUITE_*` | `test/board/test_config.h` | 全注释(产品构建) | 测试套件**互斥**:一次构建只开一个(CH395F / 存储 / GD5F / RTC);全注释 = 正常产品固件 | --- diff --git a/test/board/test_config.h b/test/board/test_config.h index bfcfc2d0..5d9aef92 100644 --- a/test/board/test_config.h +++ b/test/board/test_config.h @@ -46,12 +46,14 @@ extern "C" { #error "测试套件互斥:一次构建只能启用一个 TEST_SUITE_*" #endif -/* 派生标记:当前**仅作语义标记,全仓无任何代码引用**(2026-10-03 实机核实: - * grep 全仓仅本文件有定义,无使用点)。testTask 的创建与调度见 Src/freertos.c - * (无条件创建 testTask)+ test/board/test_config.c 的 start_test_task(),后者**直接 - * 按 TEST_SUITE_RTC 等宏分支**,与本标记无关——故"RTC 构建下 testTask 不被创建" - * 的旧疑点不成立(实测 RTC 套件正常执行)。 - * 4 个套件(含 RTC)在此补齐:若将来真引入"测试构建下 defaultTask 空闲"等分支而复用 +/* 派生标记:任一套件被选中时置位。**2026-10-04 起有唯一消费点**——`Src/freertos.c` + * 的 MX_FREERTOS_Init() 用它门控 testTask 的回收(P2-13):产品构建下 CubeMX 生成区 + * 无条件创建 testTask,随后本标记未定义 → 立即 vTaskDelete 回收其 8KB CCM 栈 + TCB, + * 避免空转占堆。testTask 的调度见 test/board/test_config.c 的 start_test_task(), + * 后者**直接按 TEST_SUITE_RTC 等宏分支**,与本标记无关——故"RTC 构建下 testTask 不被 + * 创建"的旧疑点不成立(实测 RTC 套件正常执行;RTC 已计入下方析取,故 RTC 构建下本 + * 标记亦置位、testTask 正常保留)。 + * 4 个套件(含 RTC)在此补齐:若将来再引入"测试构建下 defaultTask 空闲"等分支而复用 * 本标记,可避免重演"漏 RTC"的隐患。详见 docs/测试说明/实机测试实施指南.md §8.3。 */ #if defined(TEST_SUITE_CH395F) || defined(TEST_SUITE_STORAGE) || defined(TEST_SUITE_GD5F) || defined(TEST_SUITE_RTC) #define TEST_SUITE_SELECTED diff --git a/test/host/cfg_file/cfg_file_test.c b/test/host/cfg_file/cfg_file_test.c index ff7a9d2a..cc963aab 100644 --- a/test/host/cfg_file/cfg_file_test.c +++ b/test/host/cfg_file/cfg_file_test.c @@ -44,6 +44,8 @@ * TC-CFG-041 外科手术:保留注释/未知段/未知键,受管键原位改写 * TC-CFG-042 缺失受管段/键补写 * TC-CFG-043 输出超长 -> 拒绝保存(-1) 且原文件不动 + * TC-CFG-044 受管键行超 CFG_LINE_TEST_MAX -> 拒绝保存(-1) 原文件不动(P2-1); + * 非受管键的超长行照旧放过(原样保留) * —— poll 契约(cfg_file_poll)—— * TC-CFG-050 首轮建立基线(不重载) * TC-CFG-051 沉降去抖:连续 2 次稳定才重载 @@ -698,6 +700,52 @@ static void tc_save(void) { CHECK(s_syslog_count >= 1, "overflow self-test logged"); } } + + printf("[TC-CFG-044] over-long managed key line -> reject (P2-1)\n"); + { + char buf[512]; + size_t n; + size_t i; + size_t before; + int r; + + /* 构造受管键 u8 的超长行(行总长 > CFG_LINE_TEST_MAX):值为 200 位数字。 + * 修复前:Pass1 静默跳过该行 → Pass2 在段头补写新 KV + 原文超长行原样保留 + * → 同一键两行。修复后:显式拒绝保存,原文件不动。 */ + vfs_reset(); + register_std(); + n = (size_t)snprintf(buf, sizeof(buf), "[net]\r\nu8 = "); + for (i = 0u; i < 200u && n < sizeof(buf) - 3u; i++) { + buf[n++] = '9'; + } + buf[n++] = '\r'; + buf[n++] = '\n'; + buf[n] = '\0'; + vfs_put(buf); + s_net_data.u8 = 7u; + s_syslog_count = 0; + before = s_vfs_len; + r = cfg_file_save(); + CHECK(r == -1, "over-long managed key line rejected (-1)"); + CHECK(s_vfs_len == before, "original file untouched on over-long line"); + CHECK(s_syslog_count >= 1, "line-too-long self-test logged"); + + /* 反向:**非受管键**的超长行不属本守卫范围,应照旧原样保留、保存成功 */ + vfs_reset(); + register_std(); + n = (size_t)snprintf(buf, sizeof(buf), "[net]\r\nu8=1\r\nvendor = "); + for (i = 0u; i < 200u && n < sizeof(buf) - 3u; i++) { + buf[n++] = 'k'; + } + buf[n++] = '\r'; + buf[n++] = '\n'; + buf[n] = '\0'; + vfs_put(buf); + s_net_data.u8 = 9u; + CHECK(cfg_file_save() == 0, "over-long unknown key line tolerated"); + CHECK(strstr(vfs_get(), "u8=9") != NULL, "managed key still rewritten"); + CHECK(strstr(vfs_get(), "vendor = kkk") != NULL, "unknown long line preserved"); + } } /* TC-CFG-050~054:poll 契约 */