970e574433
- 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
41 KiB
41 KiB
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关闭);ForbiddenFAIL/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,探针 SN0B250D12A218303030303032)→ 完整用法/复位语义/故障排查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-commitcubemx-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区的实现要搬走,否则同名函数两处定义 = 编译错;配套 helperhook_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均为手工__asmHandler(取 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.ccreate_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_MAX192 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.hEvent 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_mountforced(无卷自动f_mkfsFAT32;挂载后卷健康探针:小写两击确认,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+ 2scfg_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),纳入 IWDGrequired_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-fileskill) 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 --servepython 进程——口句柄已死但进程仍占着串口与 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-verifyskill 五步门禁;含新汉字的 UI 文案提交前先跑font-regenskill 重生成中文字体
文档与图形
- 配置文件
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-diagramskill:精确布局图(协议帧/分层/屏障)用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 可装)