Files
STM32F4-Base/App/app/app_main.c
T
wandering1 4a4ad6bc5f chore(全仓): @build/@test 旧文档别名清退为技能调用名
现状口径:/keil-build 与 /serial-boot-test 是 DSH 会话的技能调用名,
不是 shell 命令;「名 + 命令行参数」的命令形态改为可执行体
run_boot_test.py 的真实调用。

- AGENTS.md:别名说明重写为「技能调用名不是 shell 命令」,两条目改
  「编译(/keil-build)」「串口回归(/serial-boot-test)」
- keil-build / serial-boot-test 两 skill 调用名与示例同步;task-worktree
  方案文档去掉 @build/@test 惯例从句;@task 保留(不在点名范围)
- 123 文件现状陈述全改;台架捕获、变更记录/时间线、rename 映射输入
  等 41 行历史证据保留旧名
- gen_memory_svg.py 6 处措辞随改,3 张 SVG 产物同批重跑
- 顺手补并行方案同行漏网计数 51→52 钩子

验证:提交期快钩子对 123 文件实跑全 Passed;check_doc_links --all
rc=0(3038 处、无法判定 0);check_mermaid --all rc=0;YAML 解析 52
钩子不变;skill-size rc=0;py_compile 全过;字节卫生零漂移。
2026-10-30 13:03:12 +08:00

383 lines
21 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 "adc.h" /* adc1_disable():挂 DMA 前把 ADC 关掉,让 Start_DMA 自己使能 */
#include "rs485.h"
#include "sb_link.h" /* 屏板链路传输层:sb_link_init()(USART1 收中断,批 1 的 1-3) */
#include "sd2506.h"
#include "boot_stats.h" /* 开机账本:上电次数(RTC 用户 SRAM)+ 本次复位原因快照(道G task-12) */
#include "sys_clock.h"
#include "gd5f2gq5ue.h"
#include "tpafe5160.h"
#include "adc_input.h" /* 语义取数与有效性层:adc_input_init()(语义映射 + STM32 ADC1 扫描) */
#include "adc_cal.h" /* 标定折算层:adc_cal_init()(ROM 系数 + cfg 两段注册,道A 标定折算与落盘整定) */
#include "run_par.h" /* 运行参数段:run_par_init()([run_par] 注册 + 104 地址映射真源,道D 这一项) */
#include "iec104_comm.h" /* [comm] 段:iec104_comm_init()(ca/端口注册,道G task-8) */
#include "phasor.h" /* 全周傅氏相量层:phasor_init()(系数表 + 实例 + 双区交换) */
#include "freq.h" /* 全周傅氏相量的派生量:freq_init()(相位解缠绕测频,B8) */
#include "prot_set.h" /* 保护判据定值模型:prot_set_init()(B5,内核前封存) */
#include "prot_elm.h" /* 保护判据元件框架:prot_elm_init()(B5,解析相量索引) */
#include "sys_log.h"
#include "task_watchdog.h" /* task_wd_seg_report():第 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) {
/* 优先级链**不在这里再写一遍**:改由 boot_stats 出一份(IWDG > WWDG > 软件 >
* POR > PIN > BOR)。原先这条 if-else 链与页面上要显示的编码各写一份,两份早晚
* 会漂——漂了的症状是串口说 "IWDG"、屏幕写 "software",两条却都"看着有据"。
* 顺序本身的理由见本函数上方原注释与 App/util/boot_stats.h 的函数头。 */
const char *cause = boot_stats_cause_str(boot_stats_cause_from_csr(csr));
/* 简单有界拼接(不引 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 输入层(语义取数与有效性,语义映射 + 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 加):本函数在内核启动前执行、不创建任何 RTOS 对象,
* 恒成功且无副作用;本行是"启动进度轨迹"的起点——之后的每一步都有结果日志,
* 故某次启动卡死时,最后一行日志即可指向卡住的步骤。 */
DBG_INFO("boot: syslog init OK");
/* 固件身份戳(2026-10-10):__DATE__/__TIME__ 由编译器展开,与前面的字面量做
* C 字符串拼接 → .rodata 里是一段连续 ASCII,可直接在 .hex 里原样搜到。
* 用途:/serial-boot-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) 第 8 轮取证:上报上一轮 defaultTask 主循环的**最后进段**。
* 轨迹槽在 .noinit(scatter UNINIT_CCM),IWDG/引脚复位不清 SRAM →
* 误复位后开机第一时间可见 `PREVRUN seg=N`,直接指向主循环阻塞点
* (第 7 轮观察"判定分支未执行"经第 8b 轮更正:真因是 IWDG 超时被静默
* 配成复位默认值,见 docs/子系统设计/看门狗设计说明.md 2026-10-02 变更记录)。
* 裸 UART 直发,且一次复位只报一次(内部清 magic)。 */
task_wd_seg_report();
/*
* 1) 时间子系统初始化(尽早完成,后续所有日志带时间戳)
* sd2506_init() 从 RTC 读取初始时间
* sys_clock_init() 将 RTC 时间转为 Unix epoch,建立墙钟基准
* 启动段可观测性(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);
/* 1b) 开机账本(道G task-12):紧接 sd2506_init 之后,是**唯一**的调用点。
* 为什么放这儿而不是 defaultTask 的存储 bring-up:
* ① 这一步只做 1 次 I2C 读 + 至多 1 次 I2C 写,落在内核启动前——
* 单线程、非 ISR、调度器还没跑(本函数不调任何创建类内核 API,
* AGENTS「内核前禁调创建类 API」那条与此无关);
* ② 保持介质是 RTC 用户 SRAM,不是文件系统:不必等 STORAGE_READY,
* 也就不必在喂狗点上碰卷锁(这条链上根本没有卷操作可言);
* ③ sd2506_init 失败时这里自然走"读不到"那一支:状态标成不可信、
* 且不回写,不会把可能还完好的记录覆盖掉。
* 失败一律不打日志(可观测性事件不进 [ERR]),状态留在 boot_stats_get()。 */
boot_stats_init();
}
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)");
/* 2b) 屏板链路初始化(USART1,700 kbaud 8N1,IDLE 中断接收)。
* 放在 NAND 全片扫描之前:这一步只是挂接收(µs 级),挂上之后屏板的周期召唤
* 就已经能被收下来并计数,台架不必等启动跑完才能看到链路状态。
* 波特率不在本调用里设——在 Src/usart.c 的 USER CODE 区(CubeMX 重生成只保护那里),
* 本调用只做一次交叉核对,对不上会报 BSP_ERR_STATE 并打 ERR 行。 */
{
bsp_err_t sb_ret = sb_link_init(&huart1);
/* BRR 直接读出来打:1-2 的判据是"实测位宽误差 < 1 %",而 BRR 是位宽的唯一决定量——
* 84 MHz / (16 x 700 kHz) = 7.5 → 期望 0x0078(整数 7、小数 0.5x16 = 8),误差 0 %。
* 有这一行就不必为了核波特率去架示波器:BRR 对不上就说明 USER CODE 覆盖没生效。 */
DBG_INFO("ScreenLink init: %s (ret=%d, USART1 %lu baud, BRR=0x%04lX)",
(sb_ret == BSP_OK) ? "OK" : "FAILED",
sb_ret, (unsigned long)SB_LINK_BAUD,
(unsigned long)huart1.Instance->BRR);
}
/* 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-pre) 挂 DMA 之前**先把 ADC 关掉**,让使能动作由 HAL_ADC_Start_DMA 内部完成
* (它自己 __HAL_ADC_ENABLE + 等 Tstab ~20 µs,然后紧接着开 CR2.DMA 并启流)。
*
* 为什么必须这样做:MX_ADC1_Init() 在 main() 里就调用了,而挂 DMA 在这里,
* 中间隔着 GD5F 全片 BBT 扫描(数秒)。台架实测(同一次开机内用无 DMA 单通道
* 探针取真值对照,9/9 完美置换):**ADC 使能后闲置数秒才挂 DMA,DMA 乒乓缓冲
* 会整体 +2 格旋转** —— 槽 i 装的是 rank ((i+1) mod 9)+1,9 路通道按 rank 序
* 整体错开 2 位。表象是"接地 AIN2、UB 却读到别的通道的值",极易被误判成
* 映射表写错或夹子位置不对。
* 把使能与挂 DMA 压到同一时刻后旋转消失(台架 4/4 次,同开机对照差值 ≤4 码)。
* 证据与复现方法见 docs/子系统设计/ADC采样节拍设计说明.md §4.5。
*
* ⚠ 顺序不可调换:本调用必须严格早于下面的 adc_input_init()。
* 若 ADC 在本步之后仍处于使能态,旋转会重新出现。 */
if (adc1_disable() != 0) {
DBG_ERROR("ADC1 disable before DMA arm: TIMEOUT");
}
/* 4b) ADC 输入层(语义取数与有效性)初始化:清原始码值缓冲与统计 + **语义映射表自洽性自检**
* (映射指向了不会被扫描的通道 → 返回 -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) 相量层(全周傅氏相量)初始化:清双区结果与有效标志 + 复位采样环缓冲。
* 纯算法层(无外设 / 无 RTOS 依赖,见 docs/算法/继电保护功能复现规划.md
* §3.2),须在启动采样节拍(步骤 6)之前。幂等、当前无失败路径
* (返回值保留供后续自检扩展)。 */
(void)phasor_init();
DBG_INFO("phasor layer init: OK (inst=%u)", (unsigned)phasor_inst_count());
/* 4c-2) 测频层(全周傅氏相量的派生量,B8)初始化:清平均窗/相位基准/有效标志。
* 纯算法(输入 = phasor 使用区),消费在 adcTask(freq_run 每拍)
* 与 protectTask(OF/UF 判据经 prot_elm_run 取数)。 */
(void)freq_init();
DBG_INFO("freq layer init: OK");
/* 4c-3) 通道标定层(标定折算,「数据可信」标定折算与落盘整定)初始化:RAM 系数副本置
* ROM 默认(量程口径冻结定死的引脚侧 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);
/* 4c2) 运行参数段初始化(「数据可信」这一项):死区 9 点工厂默认 + 注册
* [run_par] 段。同 adc_cal 的「先注册后 load」契约 → 必须内核前;
* 104 消费(COV 实时读 / 0x8020 段读写映射)在 iec104Task 启动后进行。 */
run_par_init();
DBG_INFO("run par init: OK (db9 k/b via adc_cal, evt=1<<8)");
/* 4c3) 通讯参数段初始化(道G task-8,「UI 补齐」第 5 项非 UI 前置):ca/端口
* 工厂默认 + 注册 [comm] 段。与 adc_cal / run_par 同「先注册后 load」契约
* → 必须内核前:defaultTask 在 storage_bring_up 里才做 net_cfg_load →
* cfg_file_load(引擎唯一读点),本注册必须排在它前面。生效语义 =
* 改后重启(iec104Task 启动读 getter,不挂回调,见 iec104_comm.h)。 */
if (iec104_comm_init() != 0) {
DBG_ERROR("comm init FAIL (dup/full?) - file values inactive");
} else {
DBG_INFO("comm init: OK (ca=%u port=%u)",
(unsigned)iec104_comm_ca(), (unsigned)iec104_comm_port());
}
/* 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 会在驱动首帧自检
* (有效性探针)期间乱发 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 (;;) {
/* 应用层空闲处理 */
}
}