Files
STM32F4-Base/AGENTS.md
T
wandering1 970e574433 feat(adc): 1 kHz 采样节拍改由 TIM6 硬件定时器触发
- CubeMX 加 TIM6(PSC=83 / ARR=999 / ARR 预装载 / NVIC 抢占优先级 6)
- Src/main.c 回调加 TIM6 分支触发 CONVST(USER CODE 区)
- app_main.c 用 HAL_TIM_Base_Start_IT(&htim6) 替换单次触发
- adc_task.c 退化为纯消费者(1 ms 轮询仅作消费节奏)
- @build 0 错 0 警;台架 @test 验收待做
- 同步 ADC采样节拍设计说明 / 继电保护复现规划 / AGENTS / TPAFE README
2026-10-10 15:24:02 +08:00

41 KiB
Raw Blame History

STM32F4-Base

STM32F407ZGTx @168MHz(HSE 8MHz → PLL M=4/N=168/P=2);仅 Keil MDK-ARM v5 (ARMCC, C99) 工具链;FreeRTOS V10.3.1 / CMSIS-RTOS2;192KB SRAM / 1MB Flash。

任何代码修改后必须先 @build 验证(0 错误 0 警告)再提交;硬件闭环用 @test。

铁律:源码编辑与编码

  • 改代码只用 Edit 工具(精确子串替换);批量替换用 Edit 的 replaceAll。绝不用 Get-Content/Set-Content/cmd 重写含中文的文件——整文件重写会按系统默认编码重解码 → U+FFFD 乱码且不可逆
  • 字符串字面量必须纯 ASCII(2026-09-15 实测):本机 ARMCC 对无 BOM 源文件按 CP936 解读,字符串内含奇数个汉字时末尾高字节吞掉结束引号 → 编译错(偶数个汉字侥幸通过);中文只写注释(注释不参与解析)。带 BOM 的文件(Drivers/BSP/UI/*、App/task/ui_task.c、App/cfg/cfg_file.c、Lib/lv_conf.h)经 BOM 按 UTF-8 解码,不受此条限制
  • 确需脚本级重写:工程源码为 UTF-8(多数文件无 BOM,UI 系带 BOM),按原样同编码回写、保持 BOM 状态;拿不准就别碰
  • 发现乱码:先统计 EF BF BD 字节数确认真损坏(勿信终端/Read 渲染);不要急著 git checkout(会丢弃全部未提交工作)——先用 Edit 只修坏行
  • Edit 工具会剥掉文件原有的 UTF-8 BOM(2026-09-18 实测):带 BOM 文件(如 App/cfg/cfg_file.c)被 Edit 后 BOM 静默消失 → ARMCC 改按 CP936 解码 → 中文字符串字面量报 #870-D: invalid multibyte character sequence(只有重编译该文件才暴露,历史"0 警告"不代表安全)。改完带中文字面量的文件须核对 BOM 并按原样补回(只在文件头前缀 3 字节 EF BB BF,勿整文件重写)
  • 行尾策略已写死进 .gitattributes(2026-10-09):* text=auto eol=lf(仓库 blob 与工作区统一 LF)+ *.bat/*.cmd 强制 CRLF(cmd.exe 对 LF-only 的 goto/标签不可靠)+ PNG/TTF/DLL/LIB 等显式 binary + *.jpg 走 LFS 且 -text(该条必须排在 * 之后才能覆盖)。不要再依赖 core.autocrlf,也不要把工作区文件"改回 CRLF"——CubeMX 重生成写的就是 LF;此前每次重生成后 40+ 个第三方文件在 git status 里显示"已修改"(内容归一化后一致),根因就是行尾由每台机器各自的 autocrlf 决定。第三方/生成文件的"文件末尾空行"已一次性对齐工具输出(53bd9af),勿再用改写型钩子去修剪(已在 trailing-whitespace/end-of-file-fixer 豁免)

构建与验证

  • @build = & "MDK-ARM\build.bat"(仓库根目录用相对路径);退出码 0 = 0 错误 0 警告,1 = 有警告或错误(查 MDK-ARM/build_log.txt),2 = DTU_KEIL_UV4 未设/无效(换机必设,见 docs/环境与部署/本地工具路径环境变量方案.md);UV4 路径经 DTU_KEIL_UV4 环境变量解析(不再硬编码;显式设置但无效会立即报错、不静默回退)
  • @test / .dsh/skills/serial-boot-test/scripts/run_boot_test.py 一键闭环(2026-09-20 起为纯 Python 标准库实现,替代 PowerShell 版;& '.venv\Scripts\python.exe' .dsh/skills/serial-boot-test/scripts/run_boot_test.py):开串口+清日志 → 烧录(MDK-ARM/download.bat,下载后 Reset&Run;快路径 --flash-cmd "MDK-ARM\reset.bat flash")→ 固件身份戳(从刚烧进去的 hex 里抠 fw build: <DATE> <TIME> 追加成 Required 标记)→ wait-idle(等 RX 静默 8s,超时 30s,首字节 12s 未到判 no_data)→ 提取日志 → 烧录回读校验(reset.bat cmp = -CmpFile;不一致 = 本次运行作废 exit 2)→ .dsh/skills/serial-boot-test/scripts/verify_log.py 标记校验(含启动锚点计数:reset cause: 出现 0 次或 >1 次 = FAIL,--max-boot-count 调;Forbidden 命中逐行打印行号+内容;boot_log 每行带 [+Nms] daemon 毫秒时基 → 可查标记顺序 --require-order 与开机耗时 --done-marker/--max-boot-ms,实测 net init: OK = +537ms);退出码 0=PASS / 1=FAIL / 2=采集或烧录错误(含回读不一致)/ 3=用法/预检错误
  • 默认校验标记:Required App init OK / Network Init / net init: OK + 自动追加的固件身份戳 fw build: <__DATE__> <__TIME__>(App/app/app_main.c 开机打印,run_boot_test.py 从待烧 hex 里抠出该串当 Required——此前所有标记都是固定串,旧镜像/错镜像照样全中,"反复重下载、芯片一直是旧镜像"在自动闭环里不可见,2026-10-10 补;--no-fw-check 关闭);Forbidden FAIL / ERROR / HardFault / [ERR](四个 Fault 的崩溃日志都以 [ERR] 开头 → 一律判 FAIL,这是刻意的:出现 Fault 即本次运行不通过);按模块定制标记(网络/存储/RTC/ADC/RS485/UI)见 docs/测试说明/自动化测试流程.md
  • ⚠️ 旧绝对路径 D:\Code\DTU 程序\... 出现即失效(仓库已搬迁),一律改用仓库相对路径
  • 下载/烧录铁律(2026-09-20 实测):走 & "MDK-ARM\reset.bat" flash [文件](= ST-LINK_CLI -c SWD -P <文件> -Rst -Run),烧后 -Rst -Run 不可省略——裸 -P 会报 Programming Complete,但目标核处于死循环/异常状态时写入并不落地(芯片内容不变;这就是 09/19~09/20 "反复重下载、芯片一直是旧镜像、二分法失效" 的根因);.hex/.bin 均可直烧(1MB ≈ 6.5s,快于 UV4 路径);只重启不烧录用 reset.bat 无参(= -c SWD -Rst -Run,本机 3 线接线唯一复位手段:nRST 未接、-HardRst/RM= 不可用)
  • 烧录后必须回读校验:& "MDK-ARM\reset.bat" cmp [文件](= ST-LINK_CLI -c SWD -CmpFile <文件> 0x08000000,全片逐字节比对,No difference found 才算真正写入);run_boot_test.py 已默认自动跑(--no-verify-flash 关闭,不一致 → exit 2 本次运行作废)。⚠ -CmpFile 连接时会复位目标(2026-10-10 实测:比对瞬间板子重打一遍开机日志)→ 在自动闭环里必须排在日志采集之后,否则开机日志被截断且启动锚点计数会误判成"中途复位";CLI 经 DTU_STLINK_CLI 环境变量解析(未设时回退 PATH;本机已入 PATH,未设也能工作)(v3.5.0.0,探针 SN 0B250D12A218303030303032)→ 完整用法/复位语义/故障排查 docs/测试说明/ST-LINK_CLI_复位使用说明.md

Python 环境

  • 统一入口 & '.venv\Scripts\python.exe'(3.12,fontTools/Pillow 已装);PATH 默认 python 是 msys64 的,不要用它跑依赖第三方库的脚本;纯标准库脚本任意 python3 均可
  • 重建/换机:& <本机任一 python ≥3.10> -m venv .venv + pip install -r requirements.txt(含 pre-commit,重建后 L0 门禁直接可跑;DTU_PRECOMMIT_PYTHON 可指向装了 pre-commit 的其他解释器)(.venv/ 已 gitignore)
  • requirements.txt 必须纯 ASCII(Windows 下 pip 按 GBK 代码页解码,中文注释会 UnicodeDecodeError)

提交门禁(pre-commit)

  • 任一失败即拦截:cppcheck(warning/performance/portability/style)+ C 代码规范机械检查 + U+FFFD 乱码检查 + gitlint;范围仅项目代码(第三方 Lib//Middlewares//CMSIS/HAL/lftpd/dhara/MDK-ARM/test/ 排除;规范检查另排除 CubeMX 生成的 Src//Inc/)
  • 改写型钩子(trailing-whitespace + end-of-file-fixer)豁免 CubeMX/Keil 生成文件(2026-10-09 定;配置里 YAML 锚点 &cubemx-generated 两处共用):Src/*.{c,h}、Inc/*.{c,h}、Drivers/CMSIS/、Drivers/STM32F4xx_HAL_Driver/、Middlewares/、MDK-ARM/*.{uvprojx,uvoptx,sct,map}、.mxproject、*.ioc——这类文件每次重生成按工具自己的风格写回(实测每次 10+ 个文件各被补 1 行末尾空行),改写只会制造提交抖动。⚠ 只读检查不受影响:cppcheck / 乱码 / cubemx-survival / task-table / mem-numbers 仍覆盖 Src/、Inc/;App/、Drivers/BSP/ 自研代码仍受改写型钩子检查
  • 提交信息:单行中文标题 <100 字符(gitlint),UTF-8 无 BOM + git commit -F <文件>(防终端转码破坏)
  • 代码规范 V1.0:小写下划线命名(g_/s_/p_/a_ 前缀)、头文件保护宏 XXX_H、常量在右、文件头模块注释块 → docs/代码规范/
  • 全库扫描 pre-commit run --all-files;详见 docs/代码规范/pre-commit使用说明.md
  • CI(Gitea Actions,2026-10-01 T-6):.gitea/workflows/ci.yml 在 push/PR 跑 L0 静态门禁 + L0.5 主机侧单测(Windows runner);不含 L1/L2/L3(需真机+ST-LINK)、刻意不跑 @build(需 Keil 授权)、无 .map 时自适应跳过 mem-numbers。本地等价自查:& '.venv\Scripts\python.exe' scripts/ci_local.py(runner 未就绪时这是 L0 的唯一保障,不能因"已配 CI"放宽本地门禁)。详见 docs/测试说明/测试开发标准流程.md §9

平台约束

  • Inc//Src/ 中 CubeMX 生成文件带 USER CODE BEGIN/END 标记:自定义代码只写标记区之间;Drivers/BSP/ 为纯手工代码
  • CubeMX 重生成会覆盖标记区之外的一切(.ioc 的 KeepUserCode=true 只保护 USER CODE 区;2026-09-29 两次重生成各覆盖 4~5 处定制,其中 P1 两处不报编译错、只在真出故障时暴露)。防止定制丢失按三层优先级:L1 写进 .ioc(凡 CubeMX 界面能表达的配置,如任务栈/优先级/队列/事件组/堆大小/IWDG 参数——权限最高,重生成只会照写)→ L2 搬进 USER CODE 区(只信 CubeMX 当前模板真实生成的区名,优先文件头 USER CODE BEGIN 0 这类从未变过的标准位)→ L3 审计兜底。每次重生成后立刻跑 python scripts/check_cubemx_survival.py(已挂 pre-commit cubemx-survival 钩子;改动 .ioc/Src//Inc//task_watchdog/.uvprojx 触发)——清单 21 项 + 哈希基线漂移检测,直接列出"被覆盖了哪几处"。2026-10-09 第三次重生成(启用 ADC1)补充:Inc/FreeRTOSConfig.h 的自造 FREE_RTOS_CONFIG_HARDENING 区第二次被抹 → 已改走 L1:configCHECK_FOR_STACK_OVERFLOW=2 / configUSE_MALLOC_FAILED_HOOK=1 在 GUI 的 FreeRTOS → Config parameters → Hook function related definitions 里设(写进 .ioc 由模板生成,永不再丢;教训:别凭印象断言"界面里没有该项",先翻标签页或查 db/mcu/IP/*_Modes.xml)。⚠️ 启用后 CubeMX 会在 Src/freertos.c 生成 USER CODE BEGIN 4 / 5 区(默认内容是空桩)——钩子实现必须整体写进这两个区(原先在 USER CODE Application 区的实现要搬走,否则同名函数两处定义 = 编译错;配套 helper hook_ptr_ok() 须在钩子之前 → 放 USER CODE BEGIN PD 区)。审计项 stack-overflow-hook-impl / malloc-failed-hook-impl / stack-overflow-hook-helper 分别校验 4 / 5 / PD 三个区。⚠️ 不要依赖"自造标记区":2026-09-29 实测 CubeMX 换了一代中断函数模板(旧 HardFault_Handler/HardFault_Report 自造区 → 新函数体内 HardFault_IRQn 0/W1_HardFault_IRQn 0),旧区连同内容一起消失(看起来仍像"遵守了规则")。生成区内无标记位可用的改动分两种处理(2026-10-09 更新):① 要去掉/替换生成区里的一句调用 → 用宏改名升级为 L2:例 Src/iwdg.c 的 USER CODE BEGIN 0 里放空实现 iwdg_deferred_start() + #define HAL_IWDG_Init(a_h) iwdg_deferred_start(a_h),生成那句 HAL_IWDG_Init(&hiwdg) 自动变空操作(.map 证实 Removing ... HAL_IWDG_Init, 94 bytes),生成区保持原样、重生成零 diff,不必再手工删。⚠ IWDG 启动后无法停止(唯一手段是复位),所以不能"反初始化",只能在启动前拦截。② 要替换生成实体的定义(4 个 Fault Handler 本体——本代模板整函数重写)→ 同一招升级为 L2(2026-10-09 已做):我们的 __asm void X_Handler 定义放 USER CODE BEGIN 0,必须写在 #define X X_Gen 之前(写在之后会被自己的宏改名),生成区恢复 CubeMX 原样(HardFault_IRQn 0 / W1_HardFault_IRQn 0 标记区照留),不再用锚点注释。.map 复核口径:startup_*.o(RESET) refers to stm32f4xx_it.o(.emb_text) for HardFault_Handler + 4 个 *_Gen 各 Removing ... (2 bytes)。L3 清单已清空:重生成应当零 diff;若仍有 diff,跑审计脚本定位(protected 项的断言只能用代码符号,注释会被剥掉)。方法论详见 docs/子系统设计/看门狗设计说明.md §10.1/§10.2
  • 4 个 Cortex-M4 Fault 全部采集现场(2026-09-29):HardFault / MemManage / BusFault / UsageFault 均为手工 __asm Handler(取 MSP/PSP 帧指针)→ B 到 per-fault C 入口 fault_entry_{hard,mem,bus,usage}(各自带故障名)→ hard_fault_report(tag, frame) 打 [ERR] <TAG> PC/LR/xPSR/R0-R3/CFSR/HFSR[/BFAR][/MMFAR] 后 NVIC_SystemReset()。后三者默认禁用(复位后 SHCSR 只开 HardFault),不使能就会被升级成 HardFault、日志看不出根因 → fault_handler_enable()(Src/stm32f4xx_it.c 的 USER CODE BEGIN 1 区)在 app_main_init() 第一行调用。刻意不启用 CCR.UNALIGN_TRP / CCR.DIV_0_TRP(会把普通 bug 升级成死机)。⚠️ ARMCC __asm 取不到 C 的数据符号(ADR r1, xxx_tag 报 A1516E: Bad symbol,去 static 也无效)且无法传字符串参数 → 故障名必须由 C 侧 per-fault 入口带上,不要在 __asm 里取 C 变量
  • SHCSR 的 Fault 使能位无法在 CubeMX 配置(2026-10-01 查模板源码证实):CubeMX 对 System Exception 只生成优先级(且仅当优先级 ≠ 0),从不生成使能代码——tpl_main_c.ftl 的 MX_NVIC_Init() 用 [#if vector.systemHandler=="false"] 挡住 HAL_NVIC_EnableIRQ,而 NVIC-STM32F417_Modes.xml 里 MemoryManagement/BusFault/UsageFault_IRQn 都标 Y(=systemHandler);全部模板中 SHCSR 只出现 1 次且是 MPU 清理的 &= ~MEMFAULTENA(禁用),无任何 |= 置位;MPU 路径也堵死(.ioc 无 MPU,且 HAL_MPU_Enable 只置 MEMFAULTENA 一位)。⚠️ .ioc 的 NVIC.MemoryManagement_IRQn=true 不代表已使能(第 1 段只是"向量在 NVIC 表里可见")——别误以为勾了 Enabled 就等于开了故障。结论:只能手工写(现放 USER CODE BEGIN 1 区,属 L2,重生成不丢)+ 审计项 fault-handler-enable 兜底
  • MDK-ARM/*.uvprojx 每次 @build 后被 Keil UV4 自动改写,改动一律不入库(2026-09-29 实测):UV4 构建时会把工程 XML 归一化成它本机风格的旧式写法(<Tag /> → <Tag></Tag>、XML 声明加 standalone="no"),并把 PackID 降到本机装的 DFP 版本(本机实测 3.1.1 → 3.0.0,属降级)。这些差异是环境相关的,提交后会把别人的 Pack 版本也一起改掉。判断方法:git diff MDK-ARM/STM32F407-Demo.uvprojx 里若只见 XML 格式/PackID 差异而无源文件挂载增删 → git checkout -- MDK-ARM/STM32F407-Demo.uvprojx 丢弃。只有真的新增/删除源文件(grep 文件名 计数变化)才提交
  • ARMCC 内联汇编语法(2026-09-29 踩坑,共 3 坑):① __asm volatile ("MRS %0, lr" : "=r" (x)) 是 GCC 语法,ARMCC 报 #18: expected a ")";② __asm { MRS x, lr } 块内联会把寄存器名当 C 标识符报 #20: identifier "lr" is undefined;③ __asm 块里取不到 C 的数据符号,ADR r1, xxx_tag 报 A1516E: Bad symbol 'xxx_tag'(去掉 static 也无效——汇编器只认自己作用域内的符号)。结论:整个函数用 __asm void f(void) { ... };要传"字符串/表格"这类 C 侧数据,就让 __asm 只取寄存器参数然后 B 到一个普通 C 函数,由 C 侧补上。若要覆盖 CubeMX 生成的同名函数,用 #define f f_Gen(在生成实体之前)把生成实体改名——链接器会把未被引用的 f_Gen 自动移除(.map 可见 Removing ... (N bytes)),零代码开销且不删生成区任何一行
  • FreeRTOS V10.3.1(CMSIS-RTOS V2);HAL 时基 = TIM7(非 SysTick),tick = 1ms;NVIC 4 位优先级分组,外设中断优先级 ≥ 5
  • 内核启动(osKernelStart)前禁止任何 FreeRTOS/CMSIS-RTOS API 调用(2026-09-20 启动失败实证根因,修复 82411cf):osMessageQueueNew/osMessageQueuePut/osMutexNew/osSemaphoreNew 等在内核启动前调用会杀死启动(09/19~09/20 "每次必死" 的代码层根因 = syslog_mcu_init 在内核启动前调用 osMessageQueueNew)。内核启动前代码的标准模式:纯寄存器/HAL 轮询操作 + 无锁静态缓冲(如 s_preboot[8] 暂存内核前日志)+ 内核启动后惰性创建真队列(任务上下文首次使用时,见 syslog_drain);凡在内核启动前执行的初始化函数里不得创建任何 CMSIS 对象
  • CH395F SPI:每次事务须 ch395f_spi_begin()/end() 包裹;分频系数必须为 8(PCB 约束,勿"优化")
  • 就绪屏障位必须 osFlagsNoClear(2026-09-21 实证):osEventFlagsWait 默认消费所等位,而 SYS_EVT_NET_READY/SYS_EVT_STORAGE_READY 是状态位——被 netTask/uiTask/timeSync 先消费掉后,ftpTask 的 WaitAll(两位) 永不成立 → FTP 21 端口静默不监听(无日志、无报错,纯靠可观测性才揪出来)。就绪位一律 osFlagsWaitAny|osFlagsNoClear / osFlagsWaitAll|osFlagsNoClear;命令位(SYS_EVT_CFG_SAVE/SYS_EVT_NET_RECONFIG)保持消费型等待
  • 内存预算紧张:新增任何任务/缓冲/栈前必须先核算 RAM 余量(docs/内存管理/、待处理内容.md);栈数据来源:& '.venv\Scripts\python.exe' scripts/stack_report.py -s .dsh\skills\serial-assistant\scripts\state\serial.log(Keil 静态调用图 → 每任务最坏链,+ 串口 [STACK] 运行时水位)与 scripts/ram_report.py(.map 静态占用);改栈或加大局部变量前后各跑一次
  • 自研代码禁用 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 不同,勿照抄上游代码示例
  • FatFs 文件名约束(2026-09-20 台架实证,.fschk 无限重格式化事故根因):本移植 Lib/FatFs/ffconf.h 为 FF_USE_LFN=0,只支持严格 8.3 短名——主名≤8 字符、扩展≤3 字符、不得以点开头(.fschk 会被 ff.c create_short 把点后当扩展域解析超限 → f_open 恒返 FR_INVALID_NAME(6),纯 RAM 名字判定、不碰盘)。ff.c create_short 全部拒绝点已加 DEBUG 打印:[ERR][FATFS] SFN reject (原因) seg='名字片段' bad_ch=坏字符,非法名字调用看开机日志即可定位。自愈类逻辑严禁把名字/参数类错误码(FR_INVALID_NAME 等)归入卷损坏触发集(整卷重格式化不可逆)
  • ADC 取数只用 tpafe5160_snapshot()(原子快照)(2026-10-10 修 E-44/E-45/E-58):旧的 get_buf() / data_ready() / clear_ready() 已从驱动删除——get_buf 只给缓冲区指针、任务再逐通道拷,两步之间被 BUSY EXTI 抢占并跨过一帧周期就拷回两帧混合数据(8 通道失去同一时刻性,且无错误指示);s_ready 电平标志有三个写者,start_conv_irq 触发前清 0 = 重触发本身就会抹掉一帧未消费数据。现口径:帧序号只增不清(tpafe5160_frame_seq())、取数=临界区内拷贝(tpafe5160_snapshot(dst, count) 返回 1/0/-1,仅任务上下文)、FRSTDATA 全程无跳变(芯片不驱动并行总线)由 tpafe5160_frstdata_err() 计数且 tpafe5160_init() 首帧自检直接返回 -3 + [ERR]
  • 接 ADC / RS-485 消费者前的强制门禁(E-59,2026-10-10 固化):adc_task / rs485_task 从空桩转为真实消费者之前必须确认——① 驱动契约已修:TPAFE E-44 / E-45 / E-58 已修 ✅、RS485 E-36 / E-37 已修 ✅ 但 E-50(单缓冲无同步 → 包撕裂)未修 ❌、E-49(TX 超时拉 DE)未修 ❌;② 栈预算:建任务前后各跑一次 scripts/stack_report.py。adc_task 已过闸(B1,2026-10-10):消费者本体 + adcTask(512 words / Normal,USER CODE 区手工创建,同 timeSyncTask)+ 栈预算(静态 504 B / 余 75%)+ IWDG 心跳(TASK_WD_ADC)均已落地;rs485_task 仍被挡(E-50 / E-49 未修)。另按 N-18:V0.1 上两片转换器之间禁止做相位/功率/功率因数类运算
  • DBG_* 每条日志约 90 B Flash + 216 B 栈(2026-10-02 S10 实测;2026-10-10 修正栈量):dbg_log.h 的 DBG_INFO/ERROR/DEBUG 在每个调用点内联行缓冲 char __con_line[DBG_LINE_BUF_MAX](默认 = console_log.h 的 DBG_CON_LINE_MAX 192 B)+ 时间戳缓冲 24 B(已并入同一数组,原为独立 32 B)+ 完整 snprintf 调用序列 → 单条约 90 B Flash(字符串常量另计)。给新模块接入日志时按"条数 × 90 B"估算 Flash 代价(实测 sd2506_init 加 6 条 = +576 B;app_main 加 3 条 = +180 B);深调用链模块另需核算栈——每条日志在栈上多占一份行缓冲、会进 stack_report.py 的最坏链:实测 defaultTask 那条链上 9 个帧各含一份 → 约 1.5 KB 纯粹是日志行缓冲(2026-10-10 把该链 5312 → 2776 B 的主要杠杆之一)。故日志不是免费的——已覆盖的步骤不要再加"进入前/完成后"重复锚点,只在真正黑洞(原先静默返回)处补
  • 深调用链模块收紧日志行缓冲:#define DBG_LINE_BUF_MAX 128U,写在 #include "dbg_log.h" 之前(2026-10-10 加)。已收紧:nand_ftl.c/gd5f2gq5ue.c/cfg_file.c/net_cfg.c/net_socket.c/ch395f.c/time_sync.c/default_task.c。⚠ 取值必须 ≥ 该模块最长日志行(时间戳 20 + 标签 + 正文),否则该行被截断——各文件注释已写出实测行长。改前/改后各跑一次 stack_report.py
Task 栈 (words) 优先级
defaultTask 1024 Normal
netTask 960 Normal
ftpTask 1024 Normal
testTask 2048 BelowNormal
uiTask 4096 BelowNormal
timeSyncTask 512 Normal
adcTask 512 Normal

(实际值以 Src/freertos.c 为准;testTask 仅在启用测试套件时创建。改动栈/缓冲前先核对此表与实测。2026-09-21 上调:defaultTask 512→1024(扛 mount/mkfs/FAT探针/cfg/syslog 深层 FatFS I/O,旧值经 configCHECK_FOR_STACK_OVERFLOW=2 钩子实测溢出)、testTask 1024→2048(存储套件 deep call chain + 局部缓冲溢出)。FreeRTOS 堆已迁 CCM,任务栈合计已不占 SRAM;2026-09-21 可观测性落地后实测:ftpTask 峰值曾达 3480B/4096B(余量 15%,低于 1024B 底线)→ 已按 docs/内存管理/RAM使用分析与优化方案.md §1.4 把 lftpd 9 处大局部缓冲静态化(BSS +4.6KB),复测峰值 1184B/4096B(余量 71%),6 任务全 PASS)

架构要点

Src/main.c(CubeMX 初始化)→ app_main_init → osKernelStart → 7 任务(osKernelStart 之前全是内核启动前代码:禁调任何 FreeRTOS/CMSIS-RTOS API,见「平台约束」)

  • app_main_init(时间子系统优先,使后续日志带时间戳):sd2506_init → sys_clock_init(墙钟;RTC 故障时降级默认时间基准并返回 -1 触发"时钟芯片异常"自检;sys_clock_get 每 30 天惰性再基准化防 49.7 天 tick 回绕)→ rs485_init → gd5f2gq5ue_init → tpafe5160_init → SD2506 诊断 → 启动 TIM6 1 kHz 采样节拍(HAL_TIM_Base_Start_IT(&htim6),2026-10-10;ISR 内触发 CONVST,见下)
  • 就绪屏障(App/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;挂载后卷健康探针:小写两击确认,FAT 区陈旧/擦除时自动重格式化自愈,/config.ini 回落工厂默认——2026-09-20 /log 写失败修复)→ 网络配置加载(net_cfg_load;旧档迁移已于 2026-10-10 删除,见「模块约定」网口配置条)→ 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)
  • adcTask(B1,2026-10-10):纯消费者——tpafe5160_snapshot() 临界区原子取最新一帧(8 通道)→ 帧统计 + 每 10 s 心跳日志;只取数、不做保护运算(V0.1 仅 TPAFE 三路 UA/UN/IA 测量有效,见 N-18)。采样节拍(1 kHz)由 TIM6 更新中断硬件触发(Src/main.c 的 HAL_TIM_PeriodElapsedCallback 在 TIM6 分支内调 tpafe5160_start_conv_irq(),2026-10-10);本任务 1 ms 轮询仅作消费节奏。手写于 Src/freertos.c 的 USER CODE 区(同 timeSyncTask,512 words / Normal),纳入 IWDG required_mask(TASK_WD_ADC)。详见 docs/子系统设计/ADC采样节拍设计说明.md
  • 全景 docs/架构设计/项目架构总览.md(六层分层 / 启动时序 / 存储·网络·UI 栈 / 目录组织)

硬件引脚

外设 接口 引脚
CH395F 以太网 SPI2 PB12 CS, PB13 SCK, PB14 SDO, PB15 SDI
GD5F2GQ5UE NAND SPI1 PE0 CS, PB3 SCK, PB4 MISO, PB5 MOSI, PB8 WP, PE1 HOLD
TPAFE5160 ADC 并行16位 PG0-PG15 数据, PC8 RD, PC9 CONVST, PC7 BUSY(V0.1;FRSTDATA=PC6,OS0/OS1/OS2=PA12/PA11/PA8)
ADC 通道语义(V0.1,17 路 = 8×TPAFE + 9×STM32) 见映射表 网名 AINn ≠ 板级 ADCn ≠ 转换器通道号(13/17 路置换);语义 UA/UB/UC/UN=AIN1~4、IA/IB/IC/IN=AIN8~11、V_ZL2=温度、其余 8 路备用——取数必须查 docs/硬件平台/ADC通道映射说明.md(TPAFE buf[2]=IA/buf[5]=UN/buf[6]=UA;网名 ADC17=PC4 用 ADC_CHANNEL_14,不是 ADC_CHANNEL_12)
ADC 通道归属(V0.1 硬件缺陷) 8 路电气量被分片 UA/UN/IA 走 TPAFE5160、UB/UC/IB/IC/IN 走 STM32 ADC 是缺陷,不是有意分配(2026-10-10 更正;跨转换器无共同采样触发 → 无同一时刻性/相位不确定,16 位 ±5 V 双极性 vs 12 位 0~3.3 V 单极性)→ V0.1 上主板侧禁止跨转换器做相位/功率/功率因数/矢量运算;下一版硬件(V0.2)须把 8 路电气量全部接 TPAFE(登记簿 N-18,理由与改版方案见 docs/硬件平台/ADC通道映射说明.md §1)
USART1 调试串口 UART PA9 TX, PA10 RX (115200)
RS-485 UART4 PC10 TX, PC11 RX, PA15 DE(半双工)
6x LED GPIO PC5, PB0, PB1, PB2, PF11, PF12(低电平点亮)

模块约定

  • CH395F 单连接 socket:监听与数据通道复用同一 Socket(CONNECT 后转数据通道,DISCONNECT/TIMEOUT 后重开 → LISTEN);所有 CH395F 中断在 net_poll() 内经 GET_GLOB_INT_STATUS 集中读取并清除,不得在 ISR 或其他地方单独处理
  • 所有 net_* socket 操作经 netMsgQueue 消息队列在 netTask 串行执行(线程安全);非 netTask 任务直接调 net_* API,勿直接碰 CH395F 驱动
  • net_* 公开 API 的错误码经"任务通知值"回传(2026-09-21 实证):net_msg_t 是按值拷入队列的,netTask 改的是自己那份副本,调用方永远看不到 → 错误码搭 xTaskNotify() 通知值回来(失败=负的具体错误码如 NET_ERR_WOULDBLOCK、成功=字节数/描述符),公开 API 恢复 errno 后对外统一返回 -1。勿给 net_msg_t 加"回传用"字段(调用方只会读到栈垃圾;曾据此"修"错误码,导致 ui_net_sink 每次空轮询都误判为真错误 → 连上即 net_close)
  • 单连接 socket 的断开语义:监听 Socket 在 CONNECT 后自身即数据通道(g_client == g_listen),EOF 时 net 层已 auto_relisten;应用层不得在 EOF 时 net_close 该 fd(会连监听一起毁掉 → 端口"连接被拒"),仅当数据 fd ≠ 监听 fd(模拟器多连接后端)时才关
  • 网口配置:/config.ini [network] 段(工厂默认 192.168.1.100/24、gw .1;MAC 全零 = 芯片内置;UI 保存立即生效;旧档 /cfg/netcfg.bin 迁移已于 2026-10-10 删除——net_cfg_legacy.c/h 整体移除,旧档若存在则被忽略(不读/不删/不迁移)、配置回落工厂默认;删除理由 = 该一次性路径把整条配置写链挂进启动期调用图,defaultTask 静态最坏链 +600 B,且本仓库无携带旧档的设备)→ docs/子系统设计/网络配置说明.md
  • 新 .c 文件必须手动注册到两处才能双端编译:MDK-ARM/STM32F407-Demo.uvprojx + simulator/CMakeLists.txt(用 new-source-file skill)
  • MDK-ARM/*.uvprojx 每次 @build 后被 Keil UV4 自动改写,改动一律不入库(2026-09-29 实测):UV4 构建时会把工程 XML 归一化成它本机风格的旧式写法(<Tag /> → <Tag></Tag>、XML 声明加 standalone="no"),并把 PackID 降到本机装的 DFP 版本(本机实测 3.1.1 → 3.0.0,属降级)。这些差异是环境相关的,提交后会把别人的 Pack 版本也一起改掉。判断方法:git diff MDK-ARM/STM32F407-Demo.uvprojx 里若只见 XML 格式/PackID 差异而无源文件挂载增删 → git checkout -- MDK-ARM/STM32F407-Demo.uvprojx 丢弃。只有真的新增/删除源文件(grep 文件名 计数变化)才提交
  • 测试构建:test/board/test_config.h 的 TEST_SUITE_CH395F/STORAGE/GD5F/RTC 互斥(一次只能启用一个,#error);全部注释 = 产品构建;该开关受 pre-commit 钩子 test-suite-switch 门禁(默认要求产品构建,跑完套件回归须改回;确需提交测试构建用 ALLOW_TEST_BUILD=1 显式放行——2026-10-01 实测已提交 HEAD 里 TEST_SUITE_CH395F 长期开启,纯靠人记得改,故补此门禁);破坏性用例默认关闭:同文件 ENABLE_DESTRUCTIVE_TESTS(默认注释)控制写/擦 NAND、格式化、坏块注入与 FatFS 写类用例——关闭时套件为只读模式(开机横幅 mode: READ-ONLY,可随时在在用设备上跑、不动卷内容与容量),只在产品首次上电/产线首启或明确需要时打开(mode: DESTRUCTIVE);2026-09-21 实测一轮破坏性 GD5F 运行即让 dhara 多记一个坏块(容量 95248 → 95200;该计数持久化在 dhara journal checkpoint 头里,普通恢复模式清不掉,需全片擦除——见 docs/测试说明/存储维护与容量恢复.md);CH395F 套件 test/board/ch395f_test.h:硬件层 Phase 15 与 NET 层 Phase 611 互斥;CH395F 硬件层模式下 netTask(基础线程:无条件创建、全构建唯一实例)入口自挂起(不运行),testTask 独占 CH395F
  • 改驱动前先读对应 docs/问题记录/*_Trap_Records.md(CH395F / GD5F2GQ5UE / SD2506);各模块测试指南见 docs/测试说明/;存储维护/容量恢复(全片擦除恢复 dhara 坏块记账)见 docs/测试说明/存储维护与容量恢复.md
  • LVGL I1 绘制缓冲含 palette 头(2026-09-21 实证):索引色格式 draw buffer 布局 = [palette 8B][pixel data stride×h](lv_draw_buf_create 加 8B、lv_draw_buf_goto_xy/lv_refr get_max_row 都按此跳/扣),但 lv_display_set_buffers/lv_draw_buf_init 的容量校验只看 stride×h——只给 3200B 会被静默接受:末 8 个像素字节写到缓冲外(曾打脏相邻 s_frame_box[0] 指针 → HardFault),且 flush 收到的首地址含 palette(整帧错位 8 字节)。ui_port.c 按 stride×h + palette 供给并在 flush 时 +palette 取像素;详见 docs/UI设计/UI_设计架构.md §1.4 第 8 条
  • MCU 端必须显式应用 dark mono 主题(2026-09-21 实证,远端"纯白屏"根因):lv_display_create() 会按 lv_theme_mono_init(disp, false)(白底黑字)初始化单例主题,且 lv_display_set_theme() 只把主题重应用到 screens[0](底层)→ 活动屏幕仍是 LVGL 类默认浅灰 F5F5F5;而 UI 内容恒用 lv_color_white() 绘制 → 白底白字不可见、远端整帧 99% 为亮(客户端显示纯白)。修法(ui_port.c):lv_theme_mono_init(disp, true, ...) + lv_display_set_theme() + 对活动屏幕再 lv_theme_apply()(实测 F5F5F5 → 000000);同时显式写 I1 调色板头 8B([0]=黑 [1]=白,LVGL 不替调用方初始化);详见 docs/UI设计/UI_设计架构.md §1.4 第 9 条

串口助手 serial_tool

  • 入口:& '.venv\Scripts\python.exe' .dsh/skills/serial-assistant/scripts/serial_tool.py <命令>(daemon 自动拉起;全部命令 5s 超时、JSON 输出、exit 0/1/2/3)
  • 全量历史 = 日志文件 .dsh/skills/serial-assistant/scripts/state/serial.log(RX/TX/SYS 时间戳行);agent 直接 Read/Grep 文件,无采集窗口仪式;20MB 上限自动截头保尾 10MB
  • 常用:open(幂等)/ send <文本>(或 --hex 0D0A)/ log --tail N --grep P [-i] [--regex](先过滤后截尾)/ clear(代际+1)/ wait-idle --settle-ms 8000 --max-ms 30000 [--first-timeout-ms 12000] / status / close
  • 串口端口单一配置源:.serialtest/config.json(当前 COM6,2026-09-21 台架重插后由 COM7 变更;2026-10-10 由 .opencode/serial_config.json 迁入;串口采集/测试实现已收敛到 .dsh/skills/serial-assistant + serial-boot-test,仓库内不再有 tools/serial_tool/test/host/run_boot_test 副本);端口变化只改这个文件
  • 命令/工作流/故障排查 docs/测试说明/串口助手使用指南.md;契约/daemon 行为/陷阱 docs/测试说明/串口助手设计说明.md
  • 抓开机日志铁律(2026-09-20 实测;②③ 已于 2026-10-10 修复):① 顺序 open → clear → 烧录/复位 → wait-idle → 读日志校验标记(采集必须先于烧录,否则错过开机头几行);② wait-idle 的 CLI 超时现按 --max-ms 自动推导,不再需要手工带 --timeout(旧版固定 5s 会在 daemon 还在等时掐断连接、daemon.log 刷 traceback);③ log --grep 已改为先过滤后截尾——旧实现先 tail(默认 50)后 grep,开机 marker 必然查不到,这才是当年"log --grep 实测不可信"的真根因,现可直接作判定依据(Read/Grep 文件仍等效)
  • wait-idle 判据(2026-10-10 修):no_data = 本次等待期间零字节(旧实现比的是 daemon 累计 rx_bytes 是否为 0,常驻 daemon 下永不触发 → 板子全程静默也误报 idle:true);首字节到达前不启动静默计时(--first-timeout-ms 兜底,run_boot_test.py --first-wait-ms 默认 12000);返回的 rx_bytes 是本次增量而非累计
  • 采集卫生与查询面(2026-10-10 第三批):无换行残行 64KB 封顶(板子跑飞/烧录期的二进制垃圾旧实现无限攒内存,还会把后续每条正常行拼成一条读不出的长行);open/close/clear = 会话边界,残行一并丢弃(旧实现里上一段被复位截断的半行会粘进新会话第一行,开机日志首行因此不可读);cap_truncate 按行边界截头(旧实现按字节切 → 截出来的首行是半行,破坏"只含完整行"不变量);open 不带 --dtr/--rts = 保持当前线路状态(旧实现把"没给"当 False,重开即静默拉低),显式给则现场应用并在 note 回显;ports 回 configured_present + hint(系统里只有一个候选才给,多候选不猜),open 的 port not found 也带 [hint: ...];status 回 rx_age_ms
  • ⚠️ 看到两个 serial_tool.py --serve 进程是正常的(2026-10-10 实测):.venv\Scripts\python.exe 是 venv stub,会派生基解释器(本机 = C:\ProgramData\anaconda3\python.exe)子进程跑真 daemon,status.daemon.pid 报的是子进程;别把这对父子当成"多 daemon 并存"去 taskkill(HTTP 口独占绑定 SO_EXCLUSIVEADDRUSE 已生效,status.daemon.exclusive_bind: true 可验)。真要清的是 status/meta.json 里那个 pid 之外的独立残留实例
  • open 报"拒绝访问/端口被占用"时:先查残留旧 daemon(上一会话遗留的 serial_tool.py --serve python 进程——口句柄已死但进程仍占着串口与 HTTP 口 18765)→ taskkill /F /PID <旧PID> → 重新 open(自动拉起新 daemon,系统自动释放口);改 .serialtest/config.json 后同样必须先杀旧 daemon 再 open(daemon 启动时缓存配置)

LVGL PC 模拟器

  • simulator/(CMake + SDL2 预编译库)与 MCU 端共用 UI 源码(ui_menu.c、ui_font_cn_12.c),免烧录开发 UI:& "simulator\build_sim.bat"(按本机 VS 自动选生成器:16=2019 / 17=2022 / 18=2026,勿把生成器版本写死;build/ 缓存生成器与本机 VS 不符时自动删除重建)→ 运行 simulator\build\Debug\lvgl_sim.exe
  • 自检:--cfgtest(配置闭环)/ --logtest(日志引擎)/ --standby shot.bin(待机屏截图)/ --idle <秒>
  • UI 改动一律走 ui-verify skill 五步门禁;含新汉字的 UI 文案提交前先跑 font-regen skill 重生成中文字体

文档与图形

  • 配置文件 docs/子系统设计/配置文件设计说明.md;墙钟 docs/子系统设计/系统墙钟时间维护说明.md;控制台单写者日志 docs/子系统设计/控制台单写者日志设计说明.md;代码规范 docs/代码规范/;测试说明 docs/测试说明/;审计发现与待办 docs/待处理内容.md(统一登记簿:P-x 审计缺陷 / N·S·T-x 观察 / S1x·A-x 测试架构专项 / 阶段排序 / 诊断基线);引用关系图 docs/架构设计/引用关系图说明.md
  • 登记簿类文档「完成即销项」(2026-09-26 定规):docs/待处理内容.md 这类跨文档待办登记簿,事项修复/关闭后直接从表中删除该行(不保留"已消化"行、不建归档区)——结论与证据应已沉淀进对应模块文档/问题记录,登记簿只留未办项。销项时必须:① 先核对目标文档确已含完整记录(删了不丢信息)② 同步清理其他文档对该编号的引用(改为指向目标文档,或注明"已销项")③ 改完跑 check_doc_links.py --all + pre-commit
  • 图形走 draw-diagram skill:精确布局图(协议帧/分层/屏障)用 tools/arch_diagram/gen_*.py 生成器(产物放 docs/images/,脚本与 .svg 一并提交);拓扑图(流程/时序/状态机)用 Mermaid;勿手绘 ASCII 框图
  • 文件引用关系图走 tools/refgraph:& 'tools\refgraph\gen_refgraph.bat' 生成文件级 + 模块级引用图(产物 docs/images/引用关系图.*,脚本与 .dot/.svg/.json 一并提交,PNG 本地预览不提交);项目文件变化后重跑更新;渲染依赖 Graphviz(winget 可装)