修复 P2 全部 10 项:错误传播/竞态/门控/文档失真

待办池 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。
This commit is contained in:
2026-10-04 08:30:30 +08:00
parent 13eeba944b
commit e0b02d397c
16 changed files with 332 additions and 93 deletions
+16 -5
View File
@@ -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
+78 -3
View File
@@ -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);
+45 -4
View File
@@ -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 → 重启,否则上电 */
+7
View File
@@ -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
}
+23 -18
View File
@@ -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();