Files
STM32F4-Base/App/app/app_main.c
T
wandering1 78248a1cdf feat(S-5): 收编 SD2506/TPAFE 并给 dhara 失败分类,门禁基线 19→17
前一笔立契约与门禁(`Drivers/BSP/bsp_err.h` + `bsp-err` 钩子 + RS485 试点);这一笔按用户
指定顺序 C3 → C1 → C2 把三个模块真正收编,并让存储失败第一次变得可读、可分类。

**C3 · SD2506 整模块(11 个公共 API + 6 个内部 helper)**:三枚私有宏 `SD2506_OK`/
`SD2506_ERROR`/`SD2506_I2C_ERROR` **删除而不是留别名**——留别名等于新代码还能用旧形状,
把迁移洗掉;编译器替我们找全所有调用点。新增 `sd2506_err_from_hal()`,**把 I2C 超时从
"和没人应答共用一个 −2"里拆出来归成 `BSP_ERR_TIMEOUT`(可重试)**——SD2506 是启动链第一站、
I2C1 上电时序敏感,这是本波唯一真正的语义改善,不是改名。`sd2506_init` 的 charge 寄存器
**回读校验不符**原返参数类错误,改归 `BSP_ERR_IO`(那不是调用方的错)。

**C1 · 同形两义**:`GD5F_BUSY_TIMEOUT` → `GD5F_WAIT_EXPIRED`(**值 −2 不变**,纯消"名字叫
TIMEOUT 其实是返回码")。**GD5F 私有码集整体收编刻意没做**:169 处引用里 48 处在板级套件、
还要动 Trap 记录用词,属另一波带真机+套件回归的工作,不夹带。TPAFE5160 收编 23 处:
`BUSY_TIMEOUT → BSP_ERR_TIMEOUT`、`ERROR → BSP_ERR_PARAM`(**同值 −1,行为零变化**)、
`FRSTDATA_ERR → BSP_ERR_NO_DEVICE`(E-44 的本义就是"芯片没驱动 DB 总线",归到器件无响应比
私有 −3 有用,且避开与 `BSP_ERR_TIMEOUT` 撞值)。实测 `tpafe5160.o`/`gd5f2gq5ue.o` 的 Code
增量为 **0**——改名不产生一条指令。

⚠ **`tpafe5160_snapshot()` 刻意不套成败码**:它是 1=有新帧 / 0=无新帧 / −1=参数错 的三态
**状态**返回,0 不是错误而是"这一拍没有新数据",强行收编会把慢消费者的正常路径变成失败。
顺带订正我上一版在 `bsp_err.h` 的断言——"本层只有 `net_*` 把数量与错误挤同一返回值"是错的,
`snapshot`/`adc_input_poll` 就是第二族;两族现都已写明并各注保留理由。

**C2 · FTL 坍缩点分类**:`ftl_flush_cache`/`ftl_read_page` 不再把 dhara 失败压成
  `GD5F_ERROR`,
按**处置差异**分三类,并在最早同时知道 LPN 与原始码的位置就地打印:`ECC`/`TOO_BAD` →
`BSP_ERR_CRC`(页级,重试或换页,不代表卷坏);`JOURNAL_FULL`/`MAP_FULL` →
`BSP_ERR_NO_SPACE`(介质健康但块池/日志见底,此时重格式化只是把问题推回去);
`BAD_BLOCK`/`RECOVER`/`CORRUPT_MAP`/越界 → `BSP_ERR_IO`(完整性故障,保卷 + 上报,不许自愈
重格式化)。这与 **E-31** 是同一条纪律的两端。可读名用自建短表:**本仓 dhara 没有编
`error.c`,用 `dhara_strerror()` 会直接链接失败**。`disk_read`/`disk_write` 仍回
  `RES_ERROR`
(FatFs 契约);要让 defaultTask 卷健康探针按类分支还差跨调用存活的快照 → C4 待还。

**两次靠核实拦住的自我错误**:① 我一度认定 `DBG_ERROR("... err=%d", err)` 是把结构体塞进
varargs 的格式 bug——核 `Lib/dhara/error.h` 后确认 `dhara_error_t` 是 `enum`(标量),那行日志
本来合法,改了反而是引入错误;② `bsp-err` 一次 `replace_all` 越界把 8 处 **GD5F 码空间**的
判据改成 `!= BSP_OK`(数值同为 0、无行为变化),但那正是"把两个码空间混起来",已逐一还原。
`cppcheck` 亦抓到 `cls` 声明作用域过宽,已收进失败分支。

**验证**:`@build` 0 错 0 警告(`Code` 213,648、RO 18,860、`ER_IROM1` 232,508 B /
  22.2%、
`ROM 合计` 233,696;**RAM 三项零增量**);`@test` PASS(692 ms,烧入戳 `Oct 23 01:35:32`)
且逐条读到被改模块的锚点:`SD2506 init: OK (ret=0)` 与 ID/时间/温度/电池四条读回正常、
`ADC init: OK`、`NAND init: OK`、`Map resumed: 14094/97504`、`adc=` 帧计数持续递增(证明
snapshot 三态未被改坏)、`[ERR]`/Fault/FAIL 0 条。主机单测:系统时钟 100/100、dhara NAND
移植层 149/149(两者都因本次改动需补 `Drivers/BSP` include 路径,补后在钩子内实跑 Passed)。
`pre-commit run` 全量 RC=0(20 个实跑钩子 Passed);`bsp-err` 基线 **19 → 17**(还掉两个
`*_BUSY_TIMEOUT = -2`)、`--prune` 干净;`doc-links --all --sections-strict` 147 篇
  0 问题;
`id-check --strict`、`layer-deps`、`mem-numbers`、`mem-budget`、`ui-golden` 全过;33
  个文件
字节卫生 0 异常(无 CR / 无 U+FFFD / 无行尾空格 / 均有尾换行)。

**口径与数字联动**:新增 `docs/架构设计/STM32F4-Base存储架构说明.md` §10.4(三类码与处置
理由);`pre-commit使用说明.md` §2.27 与 `AGENTS.md` 基线计数 19 → 17;TPAFE README、
GD5F 测试指南与 Trap 记录均**保留旧名留痕**以便对旧日志;配图桶按 .map 对象级增量重算
(`dhara/FTL` 10,931 → 11,222,+291 全部落在 `nand_ftl.o`;应用代码/UI +36 落在
`app_main.o`;其余散量入"其他"7,557;桶和 = Code+RO 断言通过、文本溢出 0);
`ram_baseline --update/--write`、`ram_report_20261023.md` 快照、报告 §1.3/§5.1/§5.4
  与
部署文档 §3.3 数字全部同步;报告 §0.4/§9 的 S-5 状态改为 🟡 主体已落地。
2026-10-23 07:42:02 +08:00

316 lines
15 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称:Application Main Entry
* 模块功能:应用初始化流程编排、各模块启动、主循环
* 适用平台:STM32F407ZGTx
* 作者:王建锋
* 创建日期:2026-07-19
* 修改记录:
* v1.0 2026-07-19 王建锋 创建初始版本
* v1.1 2026-08-29 王建锋 将原 main.c 中各外设初始化统一收归 app_main_init,
* 并按"时间子系统优先"的顺序编排启动流程
* v1.2 2026-09-14 王建锋 新增自检埋点(RTC/NAND/ADC 失败 → sys_log 自检记录)+ syslog_mcu_init()
*/
#include "app_main.h"
#include "main.h" /* ST_DIR4_GPIO_Port / ST_DIR4_Pin 等 CubeMX 宏 */
#include "stm32f4xx_it.h" /* fault_handler_enable() */
#include "usart.h" /* huart5 */
#include "tim.h" /* htim3:1 kHz 采样节拍(TIM3 更新中断触发 CONVST + TRGO 触发 ADC1) */
#include "rs485.h"
#include "sd2506.h"
#include "sys_clock.h"
#include "gd5f2gq5ue.h"
#include "tpafe5160.h"
#include "adc_input.h" /* L1 采集层:adc_input_init()(语义映射 + STM32 ADC1 扫描) */
#include "phasor.h" /* L2 相量层:phasor_init()(系数表 + 实例 + 双区交换) */
#include "freq.h" /* L2 派生量:freq_init()(相位解缠绕测频,B8) */
#include "prot_set.h" /* L3 定值模型:prot_set_init()(B5,内核前封存) */
#include "prot_elm.h" /* L3 元件框架:prot_elm_init()(B5,解析相量索引) */
#include "sys_log.h"
#include "task_watchdog.h" /* task_wd_seg_report():round 8 复位不丢"进段打点"上报 */
#include "test_config.h" /* 套件开关:身份戳的构建变体串(FW_BUILD_VARIANT)依赖本头,
* 改 TEST_SUITE_* / ENABLE_DESTRUCTIVE_TESTS 必重编本 TU →
* __DATE__/__TIME__ 随之刷新,见 app_main_init() 的戳注释 */
#define DBG_TAG "[APP]"
#include "dbg_log.h"
/* g_rs485 由 rs485.c 定义,经 rs485.h 的 extern 引用;接收缓冲内聚于句柄 rx_buf */
/* 复位原因锁存值(Src/main.c 在 HAL_Init() 后立即写入并清除 RCC_CSR)。
* 放这里而非 main.c,是因为需要与其它应用级全局量同层、且日志侧更好引用。 */
volatile uint32_t g_rcc_csr_latched;
/*
* 构建变体串(2026-10-11):把"测试套件开关"编进固件身份戳,见 app_main_init() 的戳注释。
* TEST_SUITE_NAME 由 test/board/test_config.h 定义(产品构建 = "NONE");
* 模拟器等未纳入该头的编译单元退化为空串,不影响戳的可搜索性。
*/
#if defined(TEST_SUITE_NAME)
#define FW_BUILD_SUITE " suite=" TEST_SUITE_NAME
#else
#define FW_BUILD_SUITE ""
#endif
#if defined(ENABLE_DESTRUCTIVE_TESTS)
#define FW_BUILD_DSTR " dstr=on"
#else
#define FW_BUILD_DSTR " dstr=off"
#endif
/* 身份戳后缀:一段连续 ASCII,保证 .hex 里可原样搜到(run_boot_test.py 抠戳用) */
#define FW_BUILD_VARIANT FW_BUILD_SUITE FW_BUILD_DSTR
/*
* 函数功能:把 RCC_CSR 锁存值翻译为可读的复位原因串(见手册 RM0090 §6.3.22)
* 入口参数:csr - 锁存的 RCC_CSR 值
* buf - 输出缓冲
* len - 缓冲容量
* 返回值:无
* 函数说明:按"最可信归因"的优先级组织——IWDG/WWDG 优先于软件复位
* (看门狗复位是我们真正关心的自动恢复事件;软件复位多为崩溃钩子
* 兜底,两者同时置位时报告看门狗更有价值)。纯 ASCII,防编码坑。
*/
static void reset_cause_str(uint32_t csr, char *buf, unsigned int len) {
const char *cause;
if ((csr & RCC_CSR_IWDGRSTF) != 0u) {
cause = "IWDG (independent watchdog)";
} else if ((csr & RCC_CSR_WWDGRSTF) != 0u) {
cause = "WWDG (window watchdog)";
} else if ((csr & RCC_CSR_SFTRSTF) != 0u) {
cause = "software (NVIC_SystemReset)";
} else if ((csr & RCC_CSR_PORRSTF) != 0u) {
cause = "power-on (POR)";
} else if ((csr & RCC_CSR_PINRSTF) != 0u) {
cause = "external pin (NRST)";
} else if ((csr & RCC_CSR_BORRSTF) != 0u) {
cause = "brown-out (BOR)";
} else {
cause = "unknown";
}
/* 简单有界拼接(不引 snprintf,避免依赖与栈开销) */
if (buf == NULL || len == 0u) {
return;
}
{
unsigned int i = 0u;
while ((i < (len - 1u)) && (cause[i] != '\0')) {
buf[i] = cause[i];
i++;
}
buf[i] = '\0';
}
}
/*
* 函数功能:应用层初始化
* 入口参数:无
* 返 回 值:0 - 成功,-1 - 失败
* 限定条件:CubeMX 外设初始化已完成
* 函数说明:在 CubeMX 外设初始化之后、FreeRTOS 调度器启动之前调用。
* 启动顺序以"时间子系统优先"为原则,使后续所有日志均带时间戳:
* 1) 时间子系统(SD2506 RTC → 墙钟基准)
* 2) RS-485
* 3) GD5F NAND
* 4) TPAFE5160 ADC → ADC 输入层(L1,语义映射 + STM32 ADC1)
* 5) SD2506 诊断信息
* 6) 启动 TIM3 1 kHz 采样节拍(ISR 内脉冲 CONVST;同一更新事件经 TRGO 硬件触发 ADC1 + DMA)
*/
int app_main_init(void) {
/* 0) 使能 MemManage / BusFault / UsageFault(复位后默认只有 HardFault 使能)。
* 必须尽早——放在所有业务初始化**之前**,这样从第一行业务代码起,
* 任何内存/总线/非法指令类故障都会被独立捕获并打出故障名,
* 而不是被升级成 HardFault 后只看到笼统的 "HARDFAULT"。
* 纯寄存器写(SCB->SHCSR),不依赖日志/时钟,故放在最前无副作用。 */
fault_handler_enable();
/* 日志引擎初始化(创建待发队列;须先于任何 syslog_post) */
(void)syslog_mcu_init();
/* 启动锚点(2026-10-02 S10):本函数在内核启动前执行、不创建任何 RTOS 对象,
* 恒成功且无副作用;本行是"启动进度轨迹"的起点——之后的每一步都有结果日志,
* 故某次启动卡死时,最后一行日志即可指向卡住的步骤。 */
DBG_INFO("boot: syslog init OK");
/* 固件身份戳(2026-10-10):__DATE__/__TIME__ 由编译器展开,与前面的字面量做
* C 字符串拼接 → .rodata 里是一段连续 ASCII,可直接在 .hex 里原样搜到。
* 用途:@test 把"芯片里正在跑的镜像"和"刚编出来的镜像"对上号。此前开机标记
* 全是固定串(App init OK / Network Init / ...),旧镜像也照样全中,于是
* "反复重下载、芯片一直是旧镜像"这类事故在自动闭环里完全不可见
* (AGENTS.md 烧录铁律记的正是它)。代价:1 条 DBG ≈ 90 B Flash + 30 B 常量。
* ⚠ 戳只在**本 TU 被重编**时才刷新(增量构建),故本文件必须 #include
* "test_config.h":否则只切套件宏(TEST_SUITE_* / ENABLE_DESTRUCTIVE_TESTS)
* 不重编本 TU,产品镜像与测试镜像顶着同一个戳,戳校验对"两种构建互换"失明
* (2026-10-11 实测:切宏后的测试构建与产品构建的戳一模一样,只有回读校验
* 才发现镜像不同)。两条防线:① 头依赖 → 改宏必重编、戳必刷新;
* ② FW_BUILD_VARIANT 把套件名与破坏性开关编进戳内容 → 即便戳时间相同,
* 产品串与测试串也不同。代价:约 20 B 常量。 */
DBG_INFO("fw build: " __DATE__ " " __TIME__ FW_BUILD_VARIANT);
/* 0) 复位原因上报(锁存值由 Src/main.c 在 HAL_Init() 后立即采得)。
* 放在日志引擎之后、业务初始化之前:任何一次非上电复位都值得在
* 开机第一时间看到——尤其 IWDG,它意味着上一轮运行有任务卡死。
* 注意:RCC_CSR 已被 main.c 清零,本处只读锁存副本。 */
{
char cause[40];
uint32_t csr = g_rcc_csr_latched;
reset_cause_str(csr, cause, sizeof(cause));
DBG_INFO("reset cause: %s (CSR=0x%08lX)", cause, (unsigned long)csr);
/* 看门狗复位记入自检记录(只记载故障,符合既有约定) */
if ((csr & (RCC_CSR_IWDGRSTF | RCC_CSR_WWDGRSTF)) != 0u) {
syslog_post(SYSLOG_SELFTEST, "watchdog reset");
}
}
/* 0b) round 8 取证:上报上一轮 defaultTask 主循环的**最后进段**。
* 轨迹槽在 .noinit(scatter UNINIT_CCM),IWDG/引脚复位不清 SRAM →
* 误复位后开机第一时间可见 `PREVRUN seg=N`,直接指向主循环阻塞点
* (round 7 观察"判定分支未执行"经 round 8b 更正:真因是 IWDG 超时被静默
* 配成复位默认值,见 docs/子系统设计/看门狗设计说明.md 2026-10-02 变更记录)。
* 裸 UART 直发,且一次复位只报一次(内部清 magic)。 */
task_wd_seg_report();
/*
* 1) 时间子系统初始化(尽早完成,后续所有日志带时间戳)
* sd2506_init() 从 RTC 读取初始时间
* sys_clock_init() 将 RTC 时间转为 Unix epoch,建立墙钟基准
* S10(2026-10-02):sd2506_init 原先所有失败路径静默返回,是启动段
* 唯一无日志的初始化;现内部已逐路径打 DBG_ERROR,此处再补一行
* 带错误码的结果回执,使 I2C 不通时能看到"在哪一步、什么错"。
*/
{
bsp_err_t rtc_ret = sd2506_init();
DBG_INFO("SD2506 init: %s (ret=%d)",
(rtc_ret == BSP_OK) ? "OK" : "FAILED", rtc_ret);
}
if (sys_clock_init() != 0) {
syslog_post(SYSLOG_SELFTEST, "clock chip fault");
}
/* 2) RS-485 初始化(UART4 + PD0 方向控制),初始化即自动开启接收 */
rs485_init(&g_rs485, &huart4, ST_DIR4_GPIO_Port, ST_DIR4_Pin);
DBG_INFO("RS485 init: OK (UART4, DE=PA15)");
/* 3) GD5F 初始化测试(BBT 全片扫描,启动段最耗时的一步) */
int ret = gd5f2gq5ue_init();
DBG_INFO("NAND init: %s",
(ret == GD5F_OK) ? "OK" :
(ret == GD5F_ERROR) ? "ERROR" :
(ret == GD5F_WAIT_EXPIRED) ? "BUSY TIMEOUT" :
(ret == GD5F_ECC_ERROR) ? "ECC ERROR" :
(ret == GD5F_PROGRAM_FAIL) ? "PROGRAM FAIL" :
(ret == GD5F_ERASE_FAIL) ? "ERASE FAIL" :
(ret == GD5F_ID_MISMATCH) ? "ID MISMATCH" : "UNKNOWN");
if (ret != GD5F_OK) {
syslog_post(SYSLOG_SELFTEST, "NAND fault");
}
/* 4) TPAFE5160 ADC 测试 */
bsp_err_t tp_ret = tpafe5160_init();
DBG_INFO("ADC init: %s",
(tp_ret == BSP_OK) ? "OK" :
(tp_ret == BSP_ERR_TIMEOUT) ? "TIMEOUT" :
(tp_ret == BSP_ERR_NO_DEVICE) ? "NO CHIP" : "FAIL");
if (tp_ret != BSP_OK) {
syslog_post(SYSLOG_SELFTEST, "ADC fault");
}
/* 4b) ADC 输入层(L1)初始化:清原始码值缓冲与统计 + **语义映射表自洽性自检**
* (映射指向了不会被扫描的通道 → 返回 -1,避免"读到恒为陈旧值"的静默错)
* + 挂载一次 STM32 ADC1 的 DMA(失败返回 -2)。
* ADC1 外设已由 MX_ADC1_Init()(main.c)配好 9 通道 / 144 cycles /
* **TIM3 TRGO 外部触发 + DMA 连续请求**,本层不重复配置、也不触发转换。
* 须在启动采样节拍(步骤 6)之前——先挂 DMA 再放节拍,才不会漏第一拍。 */
{
int adc_rc = adc_input_init();
if (adc_rc == -1) {
DBG_ERROR("ADC input layer: map self-check FAILED");
syslog_post(SYSLOG_SELFTEST, "ADC map fault");
} else if (adc_rc != 0) {
DBG_ERROR("ADC input layer: DMA arm FAILED (rc=%d)", adc_rc);
syslog_post(SYSLOG_SELFTEST, "ADC dma fault");
} else {
DBG_INFO("ADC input layer init: OK");
}
}
/* 4c) 相量层(L2)初始化:清双区结果与有效标志 + 复位采样环缓冲。
* 纯算法层(无外设 / 无 RTOS 依赖,见 docs/算法/继电保护功能复现规划.md
* §3.2),须在启动采样节拍(步骤 6)之前。幂等、当前无失败路径
* (返回值保留供后续自检扩展)。 */
(void)phasor_init();
DBG_INFO("phasor layer init: OK (inst=%u)", (unsigned)phasor_inst_count());
/* 4c-2) 测频层(L2 派生量,B8)初始化:清平均窗/相位基准/有效标志。
* 纯算法(输入 = phasor 使用区),消费在 adcTask(freq_run 每拍)
* 与 protectTask(OF/UF 判据经 prot_elm_run 取数)。 */
(void)freq_init();
DBG_INFO("freq layer init: OK");
/* 4d) 保护定值 + 元件层初始化(B5):出厂默认定值封存校验和 + 元件状态
* 复位并解析相量索引(须在 phasor_init 之后)。内核前完成 → iec104
* 定值读/protectTask 首拍前定值必已装载。判据消费在 protectTask。 */
prot_set_init();
prot_elm_init();
DBG_INFO("protection init: OK (set=%u elm=%u)",
(unsigned)PS_ID_COUNT, (unsigned)PE_COUNT);
/* 5) SD2506 RTC 诊断信息 */
{
/* 读取 ID */
uint8_t sd_id[8];
sd2506_get_id(sd_id);
DBG_INFO("SD2506 ID: %02X%02X%02X%02X%02X%02X%02X%02X",
sd_id[0], sd_id[1], sd_id[2], sd_id[3],
sd_id[4], sd_id[5], sd_id[6], sd_id[7]);
/* 读取当前时间 */
sd2506_time_t sd_time;
sd2506_get_time(&sd_time);
DBG_INFO("SD2506 time: %04d-%02d-%02d %02d:%02d:%02d (week %d)",
sd_time.year, sd_time.month, sd_time.day,
sd_time.hour, sd_time.minute, sd_time.second,
sd_time.week);
/* 读取温度 */
int8_t sd_temp;
sd2506_get_temperature(&sd_temp);
DBG_INFO("SD2506 temp: %d C", sd_temp);
/* 读取电池电压 */
uint16_t sd_vbat;
sd2506_get_battery_voltage(&sd_vbat);
DBG_INFO("SD2506 battery: %d.%02d V",
sd_vbat / 1000, (sd_vbat % 1000) / 10);
}
/* 6) 启动 1 kHz 采样节拍:TIM3 更新事件 → ① ISR 内脉冲 CONVST(Src/main.c)
* ② 同一事件经 TRGO 硬件触发 STM32 ADC1 一轮 9 通道转换(DMA 自动搬运)。
* ⚠ 必须在 tpafe5160_init() 成功之后启动,否则 ISR 会在驱动首帧自检
* (E-44 有效性探针)期间乱发 CONVST。消费端 adcTask 退化为纯消费者。
* ⚠ 节拍源必须能触发 ADC1,故用 **TIM3**:F4 上 ADC1 的外部触发只认
* TIM1/TIM2/TIM3/TIM4/TIM5/TIM8 的 CC 或 TRGO,**TIM6/TIM7 的 TRGO 只接
* DAC**(查 CubeMX DB 与本仓库 stm32f4xx_hal_adc.h 的 EXTSEL 表证实)
* → 要让两片转换器由同一个硬件事件驱动,节拍源必须换成 TIM3。 */
(void)HAL_TIM_Base_Start_IT(&htim3);
DBG_INFO("App init OK");
return 0;
}
/*
* 函数功能:应用主循环
* 入口参数:无
* 返 回 值:无
* 限定条件:FreeRTOS 调度器启动后不再返回
* 函数说明:空闲任务可调用的应用层处理入口
*/
void app_main_run(void) {
for (;;) {
/* 应用层空闲处理 */
}
}