Files
STM32F4-Base/App/app/app_main.c
T
wandering1 3528644e9d feat(采集标定): 卡2 adc_cal 折算模块 + 卡4 系数落盘 + L5-2 码值贴轨检出
卡 2(App/algo/adc_cal.*,新增;adc_task.c:335 折算点收口):
- 模型:工程量 =(code+b)×k(「数据可信」§4.1);k 以 int32 Q16(µV/码×65536)
  落盘、b 以 int32 码值落盘;ROM 默认 = 卡 1 定死的引脚侧 LSB,Q16 下整除无
  舍入(TPAFE 10,000,000=152.587890625 µV/码;STM32 52,800,000=805.6640625)
- adc_task 每拍 frame[s] 改 adc_cal_apply()(现单位 = ADC 引脚电压 V,eng=0,
  二次值语义等 VALID);TEMP 透传码值(决策 5);不缓存 k_f 副本——逐点由
  RAM 现值派生(无回调设计下杜绝陈旧系数;18k flops/s 可忽略)
- 三态 flags(§4.2):VALID = 文件位图 ∧ k 在合法带;CLIPPED 按来源运行期
  派生(V0.2/N-18 后自动恒 0);消费者判可用性只查 adc_cal_flags()
- 合法带哨兵:k∉ROM×[0.5,2.0] 或 |b|>8192 → 折算回退 ROM + 每通道每开机
  一条 [ERR] + VALID 压制(位图说谎也兜住)——同时挡坏文件与坏标定
- 两点标定公式 adc_cal_two_point() 算术核齐备(int64、±舍入界、斜率/零点
  守卫 -1/-2 分档);台架两点标定(卡 12)按用户指示未跑,留台架注入口径:
  心跳新增 cal: 行(ua/un/ia_mv 定点 + vl/cl/rail 位图,未标定 vl 应恒 0x00、
  cl 应恒 0xE6,注入饱和电平 rail 位应起)

卡 4([adc_cal] k_ua..k_in+valid / [adc_cal_ofs] b_ua..b_in,§4.4 布局):
- 两段注册进 cfg 引擎(data_size 33/32,不挂回调——RAM 副本即生效值);
  adc_cal_init() 落 app_main_init 内核前(注册必须先于 defaultTask 的
  cfg_file_load;跨任务读走 4 字节对齐字,µs 级混窗为「数据可信」§4.4 明载
  设计);文件缺失/损坏 = 引擎回退工厂默认 = ROM 表本体(默认即理论系数)
- adc_cal_set():整定写入口(属主上下文),越带拒绝 RAM 不动
- 单测走真实 cfg_file 引擎集成:文件值生效 / 越界 k 哨兵兜住且不报 VALID /
  超长损坏文件回退默认且 flags 无 VALID(卡 4 DoD 原句)/ 段缺失默认

L5-2(adc_health v1.2,越界门限按 FS 百分比、码值域与标定正交):
- adc_health_rail_watch():连续 20 拍(=一周波)贴同侧轨带(TPAFE±98%半量程、
  STM32 顶 98%/底 2%,门限由 adc_cal_rail_ths 供)且此前见过中带释放沿才
  锁存——空载恒 0 / 开机恒 0 合法工况不误报(短板评测 §6.2 判据①);
  adc_task 编排:边沿 DBG_ERROR + SYSLOG 每开机一次 + 心跳 rail/edge 位图
- 健康用例 60/60(+13):释放沿/清零/交替/双侧/init 复位/越界通道守卫

测试与注册:新套件 test/host/adc_cal(89/89,8 通道×≥3 码点期望值由 LSB
常数独立推导、Q16 无舍入、set 边界含端点、两点还原合成 k/b 往返);
编号形态返工一轮:TC-ADCCAL-00A~00D 改 014~017、注释 FS-1 字样改写、
id_registry 族 pattern 转义修正、族游标登至 017;coding-standard 返工:
公开实现函数补齐函数头注释块(CODE-RULE-09)
uvprojx 仅增 adc_cal.c 一条挂载(diff 已核零抖动);SIM_EXEMPT/id_registry
(TC-HOST-ADCCAL 族)/adc-cal-unit-test 钩子行按 new-source-file 与 B 层口径
补齐;覆盖率 34/34 套件 89.6%,adc_cal.c 99.2%(baseline --update 属共享
台账文件,留 lead done 后统一回填)

gate:@build 0 错 0 警(Code=215440 RO=19780 RW=1220 ZI=131980);
RAM 增量 +144 B(RW+16/ZI+128:贴轨 9×6B + 两段 RAM 副本,IRAM1 57.7%→
约 57.8%);Flash +1336B/+920B RO。adcTask 栈 worst=1344B margin 1216B
(47%) PASS(stack_report 实测)。staged 门禁除 mem-numbers/mem-budget 外
全绿(该两条 = 台账漂移,回填 --write 落 docs/内存管理/** 单写者区,按
Lead 既定例外归 lead done 后处理):
gate: only mem-budget/mem-numbers pending lead ledger backfill
BOM/CR/U+FFFD/字面量 ASCII 已逐文件自查(本次未触碰任何带 BOM 文件)
2026-10-25 04:21:12 +08:00

327 lines
16 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 "adc_cal.h" /* L1.5 标定层:adc_cal_init()(ROM 系数 + cfg 两段注册,道A 卡2/4) */
#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");
/* 4c-3) 通道标定层(L1.5,「数据可信」卡 2/卡 4)初始化:RAM 系数副本置
* ROM 默认(卡 1 定死的引脚侧 LSB,docs/硬件平台/ADC通道映射说明.md §7.1)
* + 向配置引擎注册 [adc_cal] / [adc_cal_ofs] 两段。注册必须先于
* defaultTask 的 cfg_file_load(「先注册后 load」契约)→ 只能落内核前;
* 折算消费在 adcTask 每拍(adc_cal_apply),标定文件值随 load/poll 原地
* 生效,无回调。 */
adc_cal_init();
DBG_INFO("adc cal init: OK (k_q16 tpafe=%ld stm32=%ld)",
(long)ADC_CAL_K_Q16_TPAFE, (long)ADC_CAL_K_Q16_STM32);
/* 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 (;;) {
/* 应用层空闲处理 */
}
}