/log 写失败修复:打开模式补 FA_WRITE 位 + 卷探针两击确认 + 自动重格式化自愈 + 错误去重

This commit is contained in:
2026-09-20 03:46:14 +08:00
parent 2bc2dcafeb
commit efe82668b7
4 changed files with 121 additions and 54 deletions
+2 -1
View File
@@ -43,6 +43,7 @@ STM32F407ZGTx @168MHz(HSE 8MHz → PLL M=4/N=168/P=2);**仅 Keil MDK-ARM v
- CH395F SPI:每次事务须 `ch395f_spi_begin()/end()` 包裹;分频系数**必须为 8**(PCB 约束,勿"优化")
- **内存预算紧张**:新增任何任务/缓冲/栈前必须先核算 RAM 余量(`docs/内存管理/`、`待处理内容.md`)
- **自研代码禁用 C 库堆分配**(2026-09-18,P0-1):禁止 `malloc/calloc/realloc/strdup/free`——本机工具链 MicroLIB 的 C 堆由 `startup_stm32f407xx.s` 的 `Heap_Size EQU 0x200` 限定为 **512 B**,KB 级分配必然返回 NULL 且**模拟器(宿主 CRT 堆)完全正常、照不出来**。需要动态内存只用 `pvPortMalloc/vPortFree`(FreeRTOS 堆 `configTOTAL_HEAP_SIZE`),或像配置引擎那样把分配收进平台后端(`cfg_store_alloc/free/heap_free`:MCU=FreeRTOS 堆、模拟器=宿主堆,共享核心零分配符号);分配失败必须有 `FAIL` 日志 + 自检上报,暂态峰值要写编译期预算
- **FatFs 打开模式陷阱(2026-09-20 台架实测,`Lib/FatFs` 移植版)**:`FA_OPEN_APPEND`(0x30) = `FA_OPEN_ALWAYS|FA_SEEKEND`,**不含 `FA_WRITE`(0x02)**——不带写位打开的文件 `f_write` 恒返回 `FR_DENIED`(7)(/log 写失败真根因);追加写必须用 `FA_WRITE|FA_OPEN_APPEND`。该移植的 `f_write` 簇分配失败时返回 `FR_OK`+短写(无错误码),判失败必须校验写回字节数。标志位为旧式编号(`FA_READ`=0x01 / `FA_WRITE`=0x02 / `FA_CREATE_NEW`=0x04 / `FA_CREATE_ALWAYS`=0x08 / `FA_OPEN_ALWAYS`=0x10 / `FA_OPEN_APPEND`=0x30 / `FA_SEEKEND`=0x20(ff.c 内部定义)),与上游 FatFs R0.14 不同,勿照抄上游代码示例
| Task | 栈 (words) | 优先级 |
|---|---|---|
@@ -62,7 +63,7 @@ STM32F407ZGTx @168MHz(HSE 8MHz → PLL M=4/N=168/P=2);**仅 Keil MDK-ARM v
- `app_main_init`(时间子系统优先,使后续日志带时间戳):`sd2506_init` → `sys_clock_init`(墙钟;RTC 故障时降级默认时间基准并返回 -1 触发"时钟芯片异常"自检;`sys_clock_get` 每 30 天惰性再基准化防 49.7 天 tick 回绕)→ `rs485_init` → `gd5f2gq5ue_init` → `tpafe5160_init` → SD2506 诊断 → `tpafe5160_start_conv_irq`
- **就绪屏障**(`App/sys_events.h` Event Flags,取代魔法延时):`SYS_EVT_NET_READY`(netTask 置位)/ `STORAGE_READY`(defaultTask:卷挂载 + 网络配置加载完成)/ `FTP_READY` / `TIMESYNC_READY`;消费者用 `osEventFlagsWait` 阻塞等待
- netTask:`net_stack_bring_up`(配置已由 defaultTask 在屏障前加载,本任务只读 RAM 副本 → `net_init` + PHY 强制 100M 全双工 → 递增网络代次号)→ 置 NET_READY → 循环 `net_poll` + `net_process_messages`;消费 `SYS_EVT_NET_RECONFIG` → 立即重建网络(新 IP 免重启生效,各 TCP 服务据代次号重建监听)
- defaultTask:`f_mount` forced(无卷自动 `f_mkfs` FAT32)→ 网络配置加载(`net_cfg_load`,首启含旧档迁移)→ STORAGE_READY → LED 闪烁;同时是 syslog(`/log/run.log`·`selftest.log`,定长 64B 记录)的**唯一写者**与**配置引擎属主**(`cfg_file` 静态态单任务收敛于此:UI 保存请求执行 `net_cfg_save_run` + 2s `cfg_file_poll`,经 `SYS_EVT_CFG_SAVE` 事件唤醒;netTask/uiTask 不得直接触碰配置引擎,见 P1-3)
- defaultTask:`f_mount` forced(无卷自动 `f_mkfs` FAT32;挂载后卷健康探针:小写两击确认,FAT 区陈旧/擦除时自动重格式化自愈,`/config.ini` 回落工厂默认——2026-09-20 /log 写失败修复)→ 网络配置加载(`net_cfg_load`,首启含旧档迁移)→ STORAGE_READY → LED 闪烁;同时是 syslog(`/log/run.log`·`selftest.log`,定长 64B 记录)的**唯一写者**与**配置引擎属主**(`cfg_file` 静态态单任务收敛于此:UI 保存请求执行 `net_cfg_save_run` + 2s `cfg_file_poll`,经 `SYS_EVT_CFG_SAVE` 事件唤醒;netTask/uiTask 不得直接触碰配置引擎,见 P1-3)
- 全景 `docs/项目架构总览.md`(六层分层 / 启动时序 / 存储·网络·UI 栈 / 目录组织)
## 硬件引脚
+38 -9
View File
@@ -89,18 +89,47 @@ static int s_append(syslog_id_t id, const char *desc) {
}
memcpy(rec + SYSLOG_DESC_OFFS, desc, dl);
fr = f_open(&f, s_paths[id], FA_OPEN_APPEND);
if (fr != FR_OK) {
DBG_ERROR("open %s fail (%d)", s_paths[id], (int)fr);
return -1;
/* L1 文件级自愈:陈旧/损坏条目 → 丢弃重建、重试一次(与上方容量满重建
* 同一机制);L2 错误去重:持续故障只报第一次,恢复前静默(消除"每次
* 开机同一错误刷屏")。打开模式必须带 FA_WRITE 位:本 FatFs 移植的
* FA_OPEN_APPEND(0x30) 不含 FA_WRITE(0x02),不带写位打开的文件 f_write
* 恒返回 FR_DENIED(7)——2026-09-20 台架实测 /log 写失败 ×5/开机 真根因 */
static uint8_t s_fault_reported[SYSLOG_COUNT];
int rc = 0;
fr = f_open(&f, s_paths[id], FA_WRITE | FA_OPEN_APPEND);
if (fr != FR_OK && (fr == FR_DENIED || fr == FR_INVALID_OBJECT ||
fr == FR_INT_ERR || fr == FR_DISK_ERR ||
fr == FR_NO_FILE || fr == FR_NO_PATH ||
fr == FR_INVALID_NAME || fr == FR_NO_FILESYSTEM)) {
(void)f_unlink(s_paths[id]);
fr = f_open(&f, s_paths[id], FA_WRITE | FA_OPEN_APPEND);
}
fr = f_write(&f, rec, SYSLOG_REC_SIZE, &bw);
(void)f_close(&f);
if (fr != FR_OK || bw != SYSLOG_REC_SIZE) {
if (fr == FR_OK) {
fr = f_write(&f, rec, SYSLOG_REC_SIZE, &bw);
(void)f_close(&f);
if (fr != FR_OK || bw != SYSLOG_REC_SIZE) {
(void)f_unlink(s_paths[id]);
bw = 0u;
fr = f_open(&f, s_paths[id], FA_WRITE | FA_OPEN_APPEND);
if (fr == FR_OK) {
fr = f_write(&f, rec, SYSLOG_REC_SIZE, &bw);
(void)f_close(&f);
}
if (fr != FR_OK || bw != SYSLOG_REC_SIZE) {
rc = -1;
}
}
} else {
rc = -1;
}
if (rc == 0) {
s_fault_reported[id] = 0u;
} else if (s_fault_reported[id] == 0u) {
s_fault_reported[id] = 1u;
DBG_ERROR("write %s fail (%d)", s_paths[id], (int)fr);
return -1;
}
return 0;
return rc;
}
/*
+81 -11
View File
@@ -210,7 +210,20 @@ void StartDefaultTask(void *argument)
* (dhara_map_resume)。此前默认构建从未挂载,导致 FTP 的
* LIST/STOR/RETR 全部返回 550(f_opendir 失败)。
* 若卷为空(FR_NO_FILESYSTEM,如恢复/坏块重建后清空),则首次自动
* 格式化建卷(MKFS_PARM 与存储测试一致:FM_FAT32 / 无分区表)。 */
* 格式化建卷(MKFS_PARM 与存储测试一致:FM_FAT32 / 无分区表)。
*
* 卷健康探针 + 自愈(2026-09-20,台架实测 /log 每次开机写失败 ×5 的
* 根因处置):真根因 = s_append 打开模式缺 FA_WRITE 位——本 FatFs 移植
* FA_OPEN_APPEND(0x30) = FA_OPEN_ALWAYS|FA_SEEKEND,不含 FA_WRITE(0x02),
* 未带写位打开的文件 f_write 恒返回 FR_DENIED(7) → 打开模式已改
* FA_WRITE|FA_OPEN_APPEND(见 sys_log_mcu.c)。残余风险 = f_mount 仅校验
* 首扇区 BPB,FAT 区陈旧/擦除(旧镜像时代遗留、换芯片、格式化中途掉电)
* 时挂载仍"成功",文件操作才报 FR_DENIED/FR_DISK_ERR/FR_INT_ERR 等结构
* 性错误(注意本移植 f_write 簇分配失败返回 FR_OK+短写,探针须校验写回
* 字节数,短写按 FR_DENIED 归一化)。挂载后做一次小写探针(两次确认,
* 防 FTL 瞬态抖动误触发);结构性错误 → 自动重格式化自愈。代价:卷被清空
* (/config.ini 丢失 → 配置引擎首启路径回落工厂默认并自动重建模板)——
* 仅当卷已不可用时触发,可用设备 > 一份配置文件。 */
static FATFS default_fatfs;
FRESULT fr = f_mount(&default_fatfs, "", 1);
if (fr == FR_NO_FILESYSTEM) {
@@ -225,16 +238,73 @@ void StartDefaultTask(void *argument)
}
}
if (fr == FR_OK) {
DBG_INFO("[FS] FATFS mounted");
/* 日志引擎挂载后处理:建 /log 目录 + 写"上电/重启"分类记录(唯一写者) */
syslog_mcu_mounted();
/* 加载网络配置进 RAM 副本(配置引擎属主 = 本任务;首启含旧档迁移;
* 文件缺失/损坏 = 工厂默认,不阻塞组网)。须在置位 STORAGE_READY 前,
* 使"存储就绪"屏障蕴含"配置已就绪",netTask 组网时配置恒有效 */
(void)net_cfg_load();
/* 广播"存储就绪"(= 卷挂载 + 配置加载完成),唤醒依赖文件系统的任务 */
if (sys_evtHandle != NULL) {
osEventFlagsSet(sys_evtHandle, SYS_EVT_STORAGE_READY);
/* 探针:/.fschk(8.3 合规)小写一次,验证 FAT 链真实可用。本移植
* f_write 簇分配失败返回 FR_OK+短写 → 必须校验写回字节数;短写按
* FR_DENIED 归一化(落入下方结构性集合触发重格式化) */
#define FS_VOL_PROBE(h) do { \
FIL __chk; \
static const uint8_t __chkbuf[8] = { 0 }; \
UINT __bw = 0u; \
FRESULT __wr; \
(h) = f_open(&__chk, "/.fschk", FA_WRITE | FA_OPEN_ALWAYS); \
if ((h) == FR_OK) { \
__wr = f_write(&__chk, __chkbuf, (UINT)sizeof(__chkbuf), &__bw); \
(void)f_close(&__chk); \
if (__wr == FR_OK && __bw == (UINT)sizeof(__chkbuf)) { \
(h) = FR_OK; \
(void)f_unlink("/.fschk"); \
} else { \
(h) = (__wr != FR_OK) ? __wr : FR_DENIED; \
} \
} \
} while (0)
FRESULT hfr;
FS_VOL_PROBE(hfr);
if (hfr != FR_OK) {
FS_VOL_PROBE(hfr); /* 两击确认 */
}
if (hfr != FR_OK) {
if (hfr == FR_DENIED || hfr == FR_INVALID_OBJECT ||
hfr == FR_INT_ERR || hfr == FR_DISK_ERR ||
hfr == FR_NO_FILE || hfr == FR_NO_PATH ||
hfr == FR_INVALID_NAME || hfr == FR_NO_FILESYSTEM) {
DBG_INFO("[FS] volume FAT invalid, reformatting...");
f_mount(NULL, "", 0);
{
static uint8_t fs_work2[FF_MAX_SS];
MKFS_PARM opts2 = { FM_FAT32, 0, 0, 0, 0 };
fr = f_mkfs("", &opts2, fs_work2, sizeof(fs_work2));
if (fr == FR_OK) {
fr = f_mount(&default_fatfs, "", 1);
}
}
if (fr == FR_OK) {
DBG_INFO("[FS] volume reformatted OK");
} else {
DBG_ERROR("[FS] volume repair failed (fr=%d)", (int)fr);
syslog_post(SYSLOG_SELFTEST, "storage fault");
}
} else {
/* 其余错误(写保护/卷锁/参数类)不重格式化——只上报,
* 防对写锁卷整卷擦除(FTL 磨损最大操作) */
DBG_ERROR("[FS] volume probe failed (fr=%d)", (int)hfr);
syslog_post(SYSLOG_SELFTEST, "storage fault");
fr = FR_INT_ERR;
}
}
#undef FS_VOL_PROBE
if (fr == FR_OK) {
DBG_INFO("[FS] FATFS mounted");
/* 日志引擎挂载后处理:建 /log 目录 + 写"上电/重启"分类记录(唯一写者) */
syslog_mcu_mounted();
/* 加载网络配置进 RAM 副本(配置引擎属主 = 本任务;首启含旧档迁移;
* 文件缺失/损坏 = 工厂默认,不阻塞组网)。须在置位 STORAGE_READY 前,
* 使"存储就绪"屏障蕴含"配置已就绪",netTask 组网时配置恒有效 */
(void)net_cfg_load();
/* 广播"存储就绪"(= 卷挂载 + 配置加载完成),唤醒依赖文件系统的任务 */
if (sys_evtHandle != NULL) {
osEventFlagsSet(sys_evtHandle, SYS_EVT_STORAGE_READY);
}
}
} else {
DBG_ERROR("[FS] f_mount failed (fr=%d)", (int)fr);
@@ -266,36 +266,3 @@ ST-LINK_CLI -c SWD -P "MDK-ARM\STM32F407-Demo\STM32F407-Demo.hex" -Rst -Run
3. 停核 = `-Rst -Halt`(停点不保证是复位向量,实测 0x08004664);一次性连接语义——断开即恢复运行。
4. **烧录路径打通**:`-P <hex>` 实测可用(1MB ≈ 6.5s,比 UV4 快得多),`reset.bat flash` 可作为快速烧录通道。
5. ✅ **问题已闭环(2026-09-20)**:09/19 启动卡死(PC=0x08000FA0)根因 = 芯片一直是合并前旧向量表镜像(09/19 之后所有下载均静默失败:裸 `-P` 不带 `-Rst -Run` 不落地)+ 内核启动前 `osMessageQueueNew` 代码缺陷;修复已提交(v2-on-ui `82411cf`),V0.1 板完整启动已验证(63 行日志 + 全片 `-CmpFile` 一致,见 §10)。
## 10. 2026-09-20 复测记录(V0.1 板:静默失败闭环 + 铁律)
### 10.1 静默失败模式(ⓞ 新实测)
- 裸 `-c SWD -P <文件>`(**不带 `-Rst -Run`**):报 `Memory programmed in 1s 906ms / Programming Complete`,但芯片 flash **不变**(回读向量表首字 = 旧镜像 SP 0x2001E998)。目标核在 flash `B .` 死循环中旋转时,"Normal 连接、不复位"会话的写入不落地。
- 同文件加 `-Rst -Run`(即 `reset.bat flash`):正常落地(6.47s / 1MB,`Programming Complete → MCU Reset → Application started`,exit=0),全片 `-CmpFile` 回读逐字节一致。
- **结论(铁律)**:任何编程命令必须以 `-Rst -Run` 收尾;**任何烧录之后必须 `-CmpFile` 全片回读校验**(`-c SWD -CmpFile <文件> 0x08000000` → `No difference found`);`Programming Complete` 单独不构成成功证据。
### 10.2 上板实测记录
| # | 时间 | 操作 | 结果 |
|---|---|---|---|
| 1 | ~00:10 | 裸 `-c SWD -P <bin> -NoPrompt`(不复位) | ❌ 报 `Programming Complete` 但芯片内容不变(向量表 = 旧镜像)→ **静默失败确认** |
| 2 | 00:24 | `reset.bat flash`(= `-P <hex> -Rst -Run`,245,704 B / 1MB) | ✅ 6.47s:`Programming Complete` → `MCU Reset` → `Application started`,exit=0 |
| 3 | 00:27 | 软件复位(`-c SWD -Rst -Run`)+ 串口采集 | ✅ 完整 63 行开机日志:`App init OK` / `Network Init` / `net init: OK` / `PHY: 100M FULL` / UI port/menu init OK / standby ready / net sink started,0 HardFault/FAIL/ERROR |
| 4 | 00:28 | `-c SWD -CmpFile <bin> 0x08000000` 全片比对 | ✅ `No difference found`(245,704 B 逐字节一致,@4MHz SWD 约 1 分钟) |
| 5 | 00:29 | `-c SWD HOTPLUG -SCore` / `-TVolt` | ✅ `Core is Running`;VDD 3.1V(PC 口供电偏低,建议 5V) |
> 09/20 开机日志含两条**非致命** ERR(与 ST-LINK 无关,固件/板级跟进项):`[ADC] Init wait BUSY timeout`(启动继续)/ `[SYSLOG] write /log/*.log fail (7)`(NAND 卷遗留旧 /log 文件不兼容,串口镜像正常)。
### 10.3 根因闭环(09/19 13:59 启动卡死 + "二分法失效")
- PC=0x08000FA0(启动区 `B .` 死循环)= 芯片上是**合并前(UI 时代)旧向量表镜像**:旧表把 DMA2_Stream2/3 处理函数值放在 v78/v79(S6/S7 槽,回读 0x9F1/0x9FD 为挪位实证),真实 DMA2_Stream3 槽(v75)无有效处理函数 → 开机第 3 行后 gd5f 第一笔 SPI1_TX DMA(DMA2_Stream3)完成中断 → 空槽 → 死循环,确定性死亡。
- **09/19 之后所有"重下载"(CLI/Keil 多次)均为静默失败**(芯片从未更新:向量表首字回读始终是旧值)→ 代码改动看似无效、二分法全程失效。
- 代码层缺陷(一并修复):`syslog_mcu_init` 在内核启动前调用 `osMessageQueueNew`(CMSIS 队列 API 内核启动前不可用)→ 改为 `syslog_drain` 内惰性创建(STORAGE_READY 之后,defaultTask 上下文)(提交 `82411cf`)。
- 新镜像(仓库现行 CubeMX 生成启动文件,该文件全程未人工改动)已逐槽核验:4 条活跃 DMA 流(SPI1 RX/TX → DMA2_Stream0/3,SPI2 RX/TX → DMA1_Stream3/4)+ EXTI9_5/TIM7/UART4 均为真实处理函数;USART1 调试 DMA(DMA2_Stream2/7)为休眠配置(代码从不启动)→ **新镜像无向量表层死点**。
### 10.4 选项字节异常(芯片状态,2026-09-20 实测,待清理)
- `0x1FFF0800 = 0x7AA16001`:RDP 字节 = **0x01**(合法出厂值仅 0xAA/0xFF/0xCC)+ WRP 残留(0x60/0xA1,nWRP 位置位)。
- 该状态**未阻碍** 09/20 的全片擦除 + 烧写(CLI/Keil 两次写入均成功),但为 09/19~09/20 烧录静默失败的头号嫌疑(疑似当时某次被中断的选项字节写入遗留)。
- 清理建议:RDP→0xAA、WRP→0x00。本机未装 STM32CubeProgrammer(ST-LINK_CLI v3.5.0.0 亦无选项字节写命令)→ 用 Keil 烧录算法的选项字节对话框操作(或升级 ST-LINK Utility / 安装 CubeProgrammer)。