/* * 模块名称: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 "app_cfg.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" /* htim6:1 kHz 采样节拍(TIM6 更新中断触发 CONVST) */ #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 "sys_log.h" #include "task_watchdog.h" /* task_wd_seg_report():round 8 复位不丢"进段打点"上报 */ #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; /* * 函数功能:把 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) 启动 TIM6 1 kHz 采样节拍(ISR 内触发 CONVST) */ 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 常量。 */ DBG_INFO("fw build: " __DATE__ " " __TIME__); /* 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 不通时能看到"在哪一步、什么错"。 */ { int rtc_ret = sd2506_init(); DBG_INFO("SD2506 init: %s (ret=%d)", (rtc_ret == SD2506_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_BUSY_TIMEOUT) ? "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 测试 */ int tp_ret = tpafe5160_init(); DBG_INFO("ADC init: %s", (tp_ret == TPAFE5160_OK) ? "OK" : "TIMEOUT"); if (tp_ret != TPAFE5160_OK) { syslog_post(SYSLOG_SELFTEST, "ADC fault"); } /* 4b) ADC 输入层(L1)初始化:清原始码值缓冲与统计 + **语义映射表自洽性自检** * (映射指向了不会被扫描的通道 → 返回 -1,避免"读到恒为陈旧值"的静默错)。 * STM32 ADC1 外设已由 MX_ADC1_Init()(main.c)配好 9 通道 / 144 cycles / * 软件触发,本层不重复配置、也不启动转换(软件触发按拍扫描)。 * 须在启动采样节拍(步骤 6)之前。 */ if (adc_input_init() != 0) { DBG_ERROR("ADC input layer: map self-check FAILED"); syslog_post(SYSLOG_SELFTEST, "ADC map 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()); /* 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 采样节拍:TIM6 更新中断 → ISR 内触发 CONVST(Src/main.c)。 * ⚠ 必须在 tpafe5160_init() 成功之后启动,否则 ISR 会在驱动首帧自检 * (E-44 有效性探针)期间乱发 CONVST。消费端 adcTask 退化为纯消费者。 */ (void)HAL_TIM_Base_Start_IT(&htim6); DBG_INFO("App init OK"); return 0; } /* * 函数功能:应用主循环 * 入口参数:无 * 返 回 值:无 * 限定条件:FreeRTOS 调度器启动后不再返回 * 函数说明:空闲任务可调用的应用层处理入口 */ void app_main_run(void) { for (;;) { /* 应用层空闲处理 */ } }