GP MCU 主应用入口
GP MCU 主应用入口(sdk/apps/gp_mcu/app_main.c)是 AC82N SDK 中 GP MCU 应用的启动与事件驱动核心:它负责片上外设初始化、按键/消息/USB 等系统事件的分发处理,并运行常驻主循环(喂狗、USB MassStorage 轮询、事件获取与派发)。
Purpose and Scope
本文档深入剖析 GP MCU 主应用入口的完整实现,包括:
app_main()的启动顺序与设计意图(初始化顺序、RTC 唤醒时序约束);- 事件循环(
while (1))的工作机制:看门狗喂狗、sys_event_get()、___idle低功耗空转; - 四大事件处理器的职责与实现:按键(
app_key_event_handler)、消息(app_msg_event_handler)、设备(app_device_event_handler)、USB(app_usb_event_handler); app_config.h中与主应用直接相关的全部配置项及其默认值;app_config.c中的编译期 Log 分级与系统常量配置。
以下内容属于姊妹页面、不在本文展开:按键驱动本身(key_driver.h)、USB 协议栈(usb_stack.h/usb_host.h)、解码器(decode/decode.h)等子系统实现细节,本文仅在它们被主入口调用时提及。
Overview
GP MCU 应用是一个典型的 事件驱动型嵌入式应用:系统启动后由链接脚本引导进入 app_main(),完成基础初始化后不再有"主动业务线程",而是通过 sys_event_get() 阻塞获取系统事件队列中的事件,再按事件类型分发到对应的处理器。这种设计把"何时发生"的控制权交给底层硬件中断与驱动,应用层只关心"发生后做什么",天然适合按键、USB 插拔、解码结束这类异步触发的场景。
主入口在系统中的定位如下:
- 启动编排者:决定
adc_init()、devices_init()、key_driver_init()、user_usb_otg_init()的先后顺序,并注释说明 RTC 初始化必须放在唤醒 IO 配置之后(见 app_main.c#L123-L126); - 事件路由器:将统一格式的
struct sys_event按event.type分发到按键/消息/设备三类处理器; - 电源与稳定性看护:主循环顶部调用
wdt_clear()喂狗,无事件时进入___idle让系统休眠,兼顾稳定性与低功耗; - USB 功能入口:通过
TCFG_USB_SLAVE_ENABLE && TCFG_USB_SLAVE_MSD_ENABLE条件编译,在 USB 设备在线时周期性调用USB_MassStorage()维持 U 盘/MSC 功能。
Architecture
下图展示了 GP MCU 主应用入口的整体架构:启动流程、初始化依赖、事件循环与各处理器之间的关系。
flowchart TD
subgraph sg_Boot["启动阶段 (Boot)"]
Reset["复位/链接脚本入口"] --> AppMain["app_main()"]
end
subgraph sg_Init["初始化 (onchip_device_init 等)"]
AppMain --> ADC["adc_init()"]
AppMain --> Dev["devices_init()"]
AppMain --> Key["key_driver_init()"]
AppMain --> OTG["user_usb_otg_init()"]
AppMain --> RTC["rtc_init() (注释说明时序约束)"]
end
subgraph sg_Loop["主事件循环 (while(1))"]
WDT["wdt_clear() 喂狗"]
MSD["USB_MassStorage() 轮询"]
Get["sys_event_get(&event)"]
Idle["___idle 低功耗空转"]
WDT --> MSD --> Get
Get -->|"无事件 (FALSE)"| Idle
Idle -.->|"被中断唤醒"| WDT
end
subgraph sg_Handler["事件分发与处理"]
Get -->|"SYS_KEY_EVENT"| KeyH["app_key_event_handler()"]
Get -->|"SYS_MSG_EVENT"| MsgH["app_msg_event_handler()"]
Get -->|"SYS_DEVICE_EVENT"| DevH["app_device_event_handler()"]
DevH -->|"OTG / USB_HOST 来源"| UsbH["app_usb_event_handler()"]
end
KeyH -->|"日志/扩展点"| Log["log_info 输出"]
MsgH -->|"MSG_EVENT_DECODE_END"| Loop2["app_play_file_loop() / a_player_stop()"]
UsbH -->|"DEVICE_EVENT_IN/OUT"| UsbOp["usb_start / usb_stop / usb_host_mount / usb_host_unmount"]
AppMain --> sg_Loop
sg_Loop --> sg_Handler
架构要点说明:
- 初始化阶段是单向线性依赖:
adc_init()先行(GPADC 采样、AD 按键依赖),随后devices_init()注册系统设备表,key_driver_init()启动按键扫描,user_usb_otg_init()配置 OTG 口。代码注释(app_main.c#L123-L126)特别强调:RTC 初始化必须安排在"需要唤醒的 IO(如按键)完成初始化之后",否则唤醒 IO 状态未配置正确时 RTC 内部关机(RTC 溢出)会导致后续唤醒异常——这是实际产品调试中踩过的坑,对应 gitee issue IJL54W。 - 主循环是典型的"喂狗 → 轮询外设 → 取事件 → 空转"模式:
sys_event_get()返回FALSE表示无待处理事件,此时进入___idle让 CPU 休眠,由中断唤醒后重新喂狗,从而在无业务时把功耗降到最低。 - 事件分发是单一入口多路分发:所有事件都打包为
struct sys_event,按event.type分流到三个处理器,设备事件内部再按event->arg区分 OTG 与 USB Host 来源,统一交给app_usb_event_handler()。
接下来深入每个阶段的实现细节。
主应用入口实现详解
1. app_main() 启动流程
app_main() 是 GP MCU 应用唯一由系统引导的入口函数,其原型在 app_main.h 中声明:
#ifndef APP_MAIN_H
#define APP_MAIN_H
void app_main(void);
#endif
Source: app_main.h
完整实现如下(app_main.c#L111-L152):
void app_main(void)
{
bool ret;
struct sys_event event;
log_info("app_main!\n");
onchip_device_init();
key_driver_init();
user_usb_otg_init();
//保证需要唤醒的IO已完成初始化(例如:按键等...)后,才能初始化rtc,避免唤醒IO状态没有配置正确就在RTC内部关机(RTC溢出),导致后续唤醒异常
//参考issues: https://gitee.com/Jieli-Tech/AC82N/issues/IJL54W
//rtc_init(...)
//显示相关初始化可以放到rtc_init之后,避免rtc溢出唤醒看到屏幕亮起
while (1) {
wdt_clear();
#if TCFG_USB_SLAVE_ENABLE && TCFG_USB_SLAVE_MSD_ENABLE
if (usb_device != NULL) {
USB_MassStorage(usb_device);
}
#endif
ret = sys_event_get(&event);
if (ret == FALSE) {
___idle;//无事件响应,系统进入idle
continue;
}
switch (event.type) {
case SYS_KEY_EVENT:
app_key_event_handler(&event);
break;
case SYS_MSG_EVENT:
app_msg_event_handler(&event);
break;
case SYS_DEVICE_EVENT:
app_device_event_handler(&event);
break;
}
}
}
Source: app_main.c
分步解读与设计意图:
- 打印启动标记
log_info("app_main!\n"):LOG_TAG被定义为"[APP]"(app_main.c#L14-L15),配合debug.h的 tag 机制,应用层日志统一带[APP]前缀,便于在混合日志中过滤。 onchip_device_init()(app_main.c#L104-L109)依次调用adc_init()(GPADC 模块初始化,AD 按键与电池检测依赖)与devices_init()(注册设备表,供dev_open/dev_read等设备接口使用)。片上设备必须先于按键与 USB 初始化,因为后者会引用 AD 通道与设备节点。key_driver_init():启动按键驱动,之后按键扫描/去抖/长短按判定由驱动在中断或低功耗唤醒上下文中完成,最终以SYS_KEY_EVENT上报应用。user_usb_otg_init():配置 OTG 口,之后 USB 插拔以SYS_DEVICE_EVENT(DEVICE_EVENT_FROM_OTG)上报。- RTC 初始化时序注释:代码刻意将
rtc_init(...)注释保留为占位,并写明两个约束——(a) RTC 必须在唤醒 IO 初始化之后,避免唤醒 IO 未配置时 RTC 溢出关机导致唤醒异常;(b) 显示相关初始化放到rtc_init之后,避免 RTC 溢出唤醒瞬间屏幕亮起。这是"初始化顺序由硬件时序约束决定"的典型例子,直接引用线上 issue 作为依据。 - 主循环(详见下一节)。
2. 主事件循环:喂狗、MSD 轮询与空转
主循环是应用唯一的常驻执行体,每个周期的顺序固定:wdt_clear() → (条件编译)USB_MassStorage() → sys_event_get() → 空转或分发。该顺序的设计意图:
wdt_clear()必须位于循环最前:任何事件处理耗时再长,回到循环顶部都会先喂狗,避免在长时间 USB 拷贝或解码处理中触发看门狗复位。这是裸机事件循环的稳定性基石。USB_MassStorage()的条件编译:仅当TCFG_USB_SLAVE_ENABLE && TCFG_USB_SLAVE_MSD_ENABLE且usb_device != NULL(USB 设备在线,由事件处理器维护)时执行。usb_device是文件级static变量(app_main.c#L18),在线时通过usb_id2device(0)获得、离线时置NULL,这种"事件置标志、主循环轮询"的模式避免在中断/事件上下文直接做耗时操作。sys_event_get()返回FALSE即___idle:无事件时应用主动让出 CPU 进入低功耗休眠,由外设中断唤醒;唤醒后continue回到循环顶部重新喂狗。这一行代码是"事件驱动 + 低功耗"的关键:没有事件时系统几乎不耗电。
3. 事件分发:sys_event 的三路路由
所有处理器接收统一结构 struct sys_event,分发的唯一依据是 event.type:
event.type | 处理器 | 触发来源示例 |
|---|---|---|
SYS_KEY_EVENT | app_key_event_handler | 按键按下/抬起/长按(IO 键、AD 键、IR 键、触摸键) |
SYS_MSG_EVENT | app_msg_event_handler | 解码结束等应用消息(MSG_EVENT_DECODE_END) |
SYS_DEVICE_EVENT | app_device_event_handler | USB OTG 插拔、USB Host 事件 |
4. 按键事件处理器 app_key_event_handler()
app_main.c#L66-L77 的实现:
void app_key_event_handler(struct sys_event *event)
{
struct key_event *key = &event->u.key;
log_info("key_type: %d, key_event: %d, key_value: %d\n", key->type, key->event, key->value);
#if 0
//adkey 0 长按抬起关机测试
if (key->type == KEY_DRIVER_TYPE_AD && key->event == KEY_EVENT_UP && key->value == 0) {
sys_softoff();
}
#endif
}
Source: app_main.c
设计意图:当前版本只做日志记录,业务按键逻辑以 #if 0 关闭的参考代码形式保留。被注释的示例展示了标准的按键业务写法——判断 key->type(KEY_DRIVER_TYPE_AD 表示 AD 按键)、key->event(KEY_EVENT_UP 抬起)、key->value(键值 0),然后调用 sys_softoff() 软关机。新需求(如"AD 键 0 长按抬起关机")只需放开此段并替换为自己的业务分支。这种"默认空实现 + 参考样例"的写法既保证开箱即用,又为开发者提供了准确的 API 用法模板。
5. 消息事件处理器 app_msg_event_handler()
app_main.c#L79-L90 的实现:
void app_msg_event_handler(struct sys_event *event)
{
struct msg_event *msg = &event->u.msg;
log_info("msg_event: %d, msg_value: %d\n", msg->event, msg->value);
switch (msg->event) {
case MSG_EVENT_DECODE_END:
if (!app_play_file_loop()) {
a_player_stop(1);
}
break;
}
}
Source: app_main.c
设计意图:应用监听 MSG_EVENT_DECODE_END(解码结束)来实现循环播放。app_play_file_loop() 尝试播放下一首;若返回 FALSE(没有下一首可播),则调用 a_player_stop(1) 停止播放器。这一"循环播放/停止"的判定逻辑集中在解码结束消息上,是音频类应用最常见的业务模式——把"播完一首后干什么"的决定权交给应用层,而不是解码器内部。
6. 设备事件处理器 app_device_event_handler()
app_main.c#L92-L102 是 USB 设备事件的"二级路由":
void app_device_event_handler(struct sys_event *event)
{
struct device_event *dev = &event->u.dev;
log_info("dev_arg: 0x%x, dev_event: %d, dev_value: %d\n", (u32)event->arg, dev->event, dev->value);
if ((int)event->arg == DEVICE_EVENT_FROM_OTG) {
app_usb_event_handler(event);
} else if ((int)event->arg == DEVICE_EVENT_FROM_USB_HOST) {
app_usb_event_handler(event);
} else {
}
}
Source: app_main.c
设计意图:设备事件的来源标识在 event->arg 中(而非 event.type),因此处理器先按 arg 判断事件来自 OTG 还是 USB Host,再统一转交 app_usb_event_handler()。目前两个来源都走同一处理器,未来若需要为不同来源定制行为,在此处扩展分支即可,无需改动分发框架。
7. USB 事件处理器 app_usb_event_handler()
app_main.c#L20-L64 是功能最复杂的处理器,核心逻辑如下:
static struct usb_device_t *usb_device;
void app_usb_event_handler(struct sys_event *event)
{
const char *usb_msg = (const char *)event->u.dev.value;
const u8 u_dev_event = event->u.dev.event;
const int u_dev_value = event->u.dev.value;
const usb_dev usb_id = usb_msg[2] - '0';
switch ((int)event->arg) {
case DEVICE_EVENT_FROM_OTG:
#if TCFG_USB_SLAVE_ENABLE
if (usb_msg[0] == 's') {
if (u_dev_event == DEVICE_EVENT_IN) {
log_info("usb_slave online\n");
usb_start(usb_id);
usb_device = usb_id2device(0);
} else if (u_dev_event == DEVICE_EVENT_OUT) {
log_info("usb_slave offline\n");
usb_device = NULL;
usb_stop(usb_id);
}
}
#endif
#if USB_HOST_ENABLE
if (usb_msg[0] == 'h') {
if (u_dev_event == DEVICE_EVENT_IN) {
log_info("usb_host online\n");
usb_host_mount(usb_id, MOUNT_RETRY, MOUNT_RESET, MOUNT_TIMEOUT);
} else if (u_dev_event == DEVICE_EVENT_OUT) {
log_info("usb_host offline\n");
usb_host_unmount(usb_id);
}
}
#endif
break;
case DEVICE_EVENT_FROM_USB_HOST:
if (u_dev_event == DEVICE_EVENT_IN) {
/* dev_open("name", &arg); */
} else if (u_dev_event == DEVICE_EVENT_OUT) {
/* dev_close("name", &arg); */
} else if (u_dev_event == DEVICE_EVENT_CHANGE) {
}
break;
default:
break;
}
}
Source: app_main.c
关键机制解读:
- 消息格式约定:设备事件负载
usb_msg是字符串,usb_msg[0]为's'(slave,设备模式)或'h'(host,主机模式),usb_msg[2]为端口号字符,usb_id = usb_msg[2] - '0'转换为数字端口号。应用层通过解析字符串来区分角色与端口,这是 SDK 事件负载的既定约定。 - USB 设备(slave)在线/离线:
DEVICE_EVENT_IN时usb_start(usb_id)启动设备栈,并通过usb_id2device(0)缓存设备句柄到static usb_device,供主循环的USB_MassStorage()使用;DEVICE_EVENT_OUT时置NULL并usb_stop(usb_id)。句柄的置空/赋值与主循环的判空严格对应,保证不会对已离线的设备做 MSD 操作。 - USB 主机(host)在线/离线:在线时
usb_host_mount(usb_id, MOUNT_RETRY, MOUNT_RESET, MOUNT_TIMEOUT)挂载 U 盘(带重试、复位与超时参数),离线时usb_host_unmount(usb_id)卸载。 DEVICE_EVENT_FROM_USB_HOST分支:预留给 Host 模式下具体设备节点(如读卡器)的dev_open/dev_close,当前以注释占位——展示"驱动事件 → 应用打开设备节点"的标准扩展路径。- 两个分支分别被
TCFG_USB_SLAVE_ENABLE与USB_HOST_ENABLE条件编译包裹,未使能的功能代码不会进入固件。
8. 片上设备初始化 onchip_device_init()
void onchip_device_init()
{
adc_init();
devices_init();
}
Source: app_main.c
adc_init() 初始化 GPADC 模块(AD 按键 TCFG_ADKEY_ENABLE、电池检测 TCFG_BAT_AD_CHANNEL 均依赖它);devices_init() 完成设备表注册,是后续 dev_open/dev_read 等设备操作的前提。此函数虽短,却是所有外设功能的地基,且必须先于按键与 USB 初始化执行。
核心流程:事件处理时序
下图展示一次 USB 设备插入到 U 盘功能生效的完整时序,以及按键事件从驱动到应用的传递路径:
sequenceDiagram
participant HW as 硬件/中断
participant DRV as 按键/USB驱动
participant APP as app_main() 主循环
participant HD as app_usb_event_handler
participant MSD as USB_MassStorage()
Note over HW,DRV: 场景A: USB设备(PC)插入 OTG 口
HW->>DRV: 插拔检测中断
DRV->>APP: 投递 SYS_DEVICE_EVENT (arg=DEVICE_EVENT_FROM_OTG, msg="s0...")
APP->>APP: sys_event_get() 取到事件
APP->>HD: app_device_event_handler → app_usb_event_handler
HD->>HD: usb_msg[0]=='s' → 设备模式; usb_id=0
HD->>HD: usb_start(0); usb_device = usb_id2device(0)
APP->>APP: 回到循环顶部 wdt_clear()
APP->>MSD: usb_device != NULL → USB_MassStorage(usb_device)
MSD-->>APP: 维持U盘/MSC枚举与读写
Note over HW,DRV: 场景B: 按键按下
HW->>DRV: AD按键扫描中断
DRV->>APP: 投递 SYS_KEY_EVENT (key_event)
APP->>APP: sys_event_get() 取到事件
APP->>APP: app_key_event_handler() 打印 key_type/key_event/key_value
APP->>APP: 继续等待下一个事件 (无事件则 ___idle)
时序要点:
- USB 插拔由驱动以
SYS_DEVICE_EVENT异步上报,应用在下一次主循环迭代中才处理——事件处理与 MSD 轮询天然串行,无需加锁; - 设备句柄
usb_device的赋值发生在事件处理器中,而消费发生在同一线程的主循环顶部,两者同上下文、无竞争; - 无任何事件时
___idle使 CPU 休眠,中断唤醒后重新喂狗,形成"事件驱动 + 低功耗"闭环。
配置选项
主应用行为完全由 app_config.h 中的宏定义在编译期决定。与主入口直接相关的配置如下(完整文件见 app_config.h):
基础开关与系统配置
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
ENABLE_THIS_MOUDLE / DISABLE_THIS_MOUDLE | int | 1 / 0 | 模块使能/禁用的统一开关常量 |
ENABLE / DISABLE | int | 1 / 0 | 通用开关常量 |
NO_CONFIG_PORT | int | -1 | 表示"未配置 IO 端口"的哨兵值 |
CONFIG_DEBUG_ENABLE | 宏 | 定义 | 系统打印总开关(未定义则关闭全部打印) |
串口(打印)配置
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
TCFG_DB_UART_ENABLE | int | ENABLE | 打印串口使能 |
TCFG_DB_UART_RX_PORT | IO | NO_CONFIG_PORT | 串口接收脚(打印场景可不接) |
TCFG_DB_UART_TX_PORT | IO | IO_PORTA_04 | 串口发送脚 |
TCFG_DB_UART_BAUDRATE | int | 1000000 | 打印波特率(1M) |
时钟配置(决定主循环与外设节奏)
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
TCFG_CLOCK_PLL_HZ | long | 480MHz | PLL 主频(240M 或 480M) |
TCFG_CLOCK_SYS_HZ | long | 96MHz | CPU 运行时钟 |
TCFG_CLOCK_LSB_HZ | long | 48MHz | 外设 LSB 时钟 |
按键配置(决定 app_key_event_handler 收到的键源)
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
KEY_IO_NUM_MAX | int | 3 | IO 按键最大数量 |
KEY_AD_NUM_MAX | int | 10 | AD 按键最大数量 |
KEY_IR_NUM_MAX | int | 21 | IR 按键最大数量 |
KEY_TOUCH_NUM_MAX | int | 3 | 触摸按键最大数量 |
TCFG_IOKEY_ENABLE | int | DISABLE | IO 按键使能(端口 PA3/PA6/PA7) |
TCFG_ADKEY_ENABLE | int | ENABLE | AD 按键使能(PA0 / AD_CH_PA0,外部上拉 R_UP=220) |
TCFG_IRKEY_ENABLE | int | DISABLE | IR 按键使能(PA2) |
TCFG_TOUCH_KEY_ENABLE | int | DISABLE | 触摸按键使能(key0=PA8/0x12,key1=PA9/0x34) |
USB 配置(决定 app_usb_event_handler 与 MSD 轮询行为)
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
TCFG_PC_ENABLE | int | ENABLE | PC 模块使能 |
TCFG_UDISK_ENABLE | int | DISABLE | U 盘模块使能 |
TCFG_OTG_USB_DEV_EN | 位域 | BIT(0) | 使能 USB0(BIT(0))或 USB1(BIT(1)) |
USB_DEVICE_CLASS_CONFIG | 宏 | SPEAKER_CLASS|HID_CLASS|CDC_CLASS | 设备类组合(#undef 后重定义) |
USB_MALLOC_ENABLE | int | ENABLE | USB 动态内存使能 |
TCFG_USB_SLAVE_ENABLE | int | (由 USB 模块) | 设备模式使能,控制 app_usb_event_handler 的 slave 分支与 MSD 条件编译 |
TCFG_USB_SLAVE_MSD_ENABLE | int | (由 USB 模块) | 设备模式 MSD 使能,与 usb_device != NULL 共同决定主循环是否调用 USB_MassStorage() |
电源、复位与低功耗配置(影响主循环空转行为)
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
TCFG_LOWPOWER_LOWPOWER_SEL | int | SLEEP_EN | 低功耗模式选择 |
TCFG_VDDIOM_LEVEL | int | VDDIOM_VOL_33V | VDDIO 电压档 |
TCFG_LVD_USE_WKUP_MODE_ENABLE | int | DISABLE | LVD 中断模式(防写 VM 时掉电损坏) |
TCFG_SFC_FAST_BOOT_ENABLE | int | DISABLE | 快速启动开关 |
PINR_RESET_IO | IO | IO_PORTA_00 | 长按复位 IO |
PINR_RESET_LVL | int | 0 | 复位触发沿(0 下降沿 / 1 上升沿) |
PINR_PRESS_TIME | int | 8 | 长按时长(0/1/2/4/8/16 秒) |
PINR_RESET_RELASE | int | 1 | 0 释放后复位 / 1 立即复位 |
PINR_PROTECT_EN | int | 0 | 写保护使能 |
TCFG_BATTERY_TYPE_SEL | int | BATTERY_TYPE_3_DRY_DELL | 供电类型(2/3/4 节干电池或锂电池) |
TCFG_BAT_DET_IO | IO | NO_CONFIG_PORT | 电池检测 IO(未配置则走 PMU 通道) |
TCFG_BAT_AD_CHANNEL | AD | AD_ANA_PMU_CH_VPWR_DIV_4 | 电池检测 AD 通道 |
TCFG_APP_RTC_EN | int | 0 | RTC 功能使能(对应 app_main 中被注释的 rtc_init 占位) |
TCFG_APP_RTC_CLK_SEL | int | CLK_SEL_LRC | RTC 时钟源 |
存储与外设配置
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
TCFG_VM_SIZE | int | 16 | VM 空间大小(Kbyte) |
TCFG_VM_INIT_FAST | int | 0 | VM 快速初始化 |
TCFG_SD0_ENABLE / TCFG_SD1_ENABLE | int | 0 / 0 | SD 卡使能 |
TCFG_MSFC_DEV_ENABLE | int | DISABLE | 内置 Flash 枚举为 MSD |
TCFG_HW_SPI1_* | IO/数值 | PC4/PC5, 24MHz | LCD SPI1 端口与速率 |
TCFG_HW_SPI2_* | IO/数值 | PA10/PA9/PA11/PA12, 1MHz | NOR FLASH SPI2(当前 DISABLE) |
TCFG_AT_CHAR_UART_* | IO/数值 | PC4/PC3, 115200 | AT 蓝牙串口 |
TCFG_DUAL_BANK_ENABLE | int | DISABLE | 双备份升级使能 |
TCFG_DB_UPDATE_PASSIVE_DEMO_EN | int | DISABLE | 升级参考 demo |
配置值均摘自 app_config.h#L7-L213。修改配置后需重新编译固件,部分配置(如
TCFG_CLOCK_SYS_HZ、USB_DEVICE_CLASS_CONFIG)会改变链接布局或驱动行为。
编译期 Log 分级配置(app_config.c)
app_config.c 用 log_tag_const_* 系列常量按模块与级别控制编译期日志开关,其中 APP 模块全部级别默认开启(LOG_DEBUG_EN(TRUE)),主入口的 log_info 即受此控制。其关键机制:
#ifdef CONFIG_DEBUG_ENABLE
#define LOG_DEBUG_EN 1
#else
#define LOG_DEBUG_EN 0
#endif
#define CONFIG_DEBUG_EN(x) (x & LOG_DEBUG_EN)
const char log_tag_const_v_APP AT(.LOG_TAG_CONST) = CONFIG_DEBUG_EN(TRUE);
const char log_tag_const_d_APP AT(.LOG_TAG_CONST) = CONFIG_DEBUG_EN(TRUE);
const char log_tag_const_c_APP AT(.LOG_TAG_CONST) = CONFIG_DEBUG_EN(TRUE);
const char log_tag_const_i_APP AT(.LOG_TAG_CONST) = CONFIG_DEBUG_EN(TRUE);
const char log_tag_const_w_APP AT(.LOG_TAG_CONST) = CONFIG_DEBUG_EN(TRUE);
const char log_tag_const_e_APP AT(.LOG_TAG_CONST) = CONFIG_DEBUG_EN(TRUE);
Source: app_config.c
设计意图:通过 AT(.LOG_TAG_CONST) 将开关常量放入专用段,配合链接脚本实现"可裁剪日志"——发布版(CONFIG_RELEASE_ENABLE)可将 config_asser 置 0 关闭断言;const_config_sfc_freq = 64000000 固定 SFC 频率 64MHz(注释说明 IOVDD 电压等级对 SFC 频率上限的约束);config_xosc_wait_time = 1000 控制上电晶振起振等待时间(存在校准记录后上电不再确认起振,>0 则等待指定毫秒后重新校准)。这些"普通变量 + 链接段属性"的技巧使得 SDK 可以在不改驱动源码的前提下,按产品需求裁剪代码与 RAM 占用。
故障模式、边界情况与并发
事件丢失与空转唤醒
sys_event_get() 返回 FALSE 时应用直接 ___idle 空转。若驱动投递事件与空转之间存在竞争(事件在 sys_event_get() 返回 FALSE 之后、___idle 执行之前到达),依赖 ___idle 自身的唤醒机制保证事件不丢失——___idle 是 SDK 提供的"进入休眠前二次确认"原语,中断会唤醒 CPU 并重新进入循环取事件。应用层无需自行处理该竞态,但不要在 FALSE 分支中加入耗时逻辑,否则会拉长事件响应延迟。
USB 设备句柄与主循环的耦合
usb_device 是 static 全局,赋值(app_usb_event_handler)与消费(主循环 USB_MassStorage)都在同一执行上下文(主循环线程)中顺序进行,因此无并发竞争。但存在边界:若 DEVICE_EVENT_OUT(离线)事件尚未被主循环处理,主循环仍会调用 USB_MassStorage() 操作正在拔出中的设备。SDK 的 USB_MassStorage() 内部具备超时/错误容错(与 MOUNT_RETRY/MOUNT_TIMEOUT 同类的机制),不会因拔线导致系统崩溃,但开发者不应假设该调用瞬时完成——这正是主循环每轮先喂狗的原因。
看门狗与耗时操作
主循环每轮 wdt_clear() 保证即使事件处理器耗时较长(如大文件解码、U 盘读写)也不会误触发看门狗复位。风险点在于:若开发者向某个事件处理器中加入长时间阻塞逻辑(如等待外设应答),可能使单轮循环超过看门狗超时上限。扩展业务时优先使用 SDK 提供的异步/状态机模式,或将耗时操作拆分为多轮事件驱动处理。
初始化顺序约束(RTC/唤醒 IO)
代码注释明确记录了踩坑经验:RTC 初始化若早于唤醒 IO(按键)配置,唤醒 IO 状态未配置正确时 RTC 内部关机(RTC 溢出)会导致后续唤醒异常;显示初始化若早于 RTC,RTC 溢出唤醒瞬间屏幕会亮起。任何新增的"需唤醒 IO"外设初始化,都必须放在 rtc_init 之前,这是 GP MCU 应用扩展时最重要的时序纪律。
条件编译分支的对称性
app_usb_event_handler 中 slave 分支受 TCFG_USB_SLAVE_ENABLE 控制、host 分支受 USB_HOST_ENABLE 控制,主循环 MSD 轮询受 TCFG_USB_SLAVE_ENABLE && TCFG_USB_SLAVE_MSD_ENABLE 双重控制。修改这些配置时需保持"事件处理分支"与"主循环轮询条件"一致:例如仅使能 slave 而关闭 MSD,则 usb_device 仍会被赋值但主循环不会调用 USB_MassStorage,功能静默失效——排查此类问题时应先核对三处条件编译。
性能与运维
- 空转功耗:无事件时
___idle进入休眠,主循环不占用 CPU,是低功耗产品的核心省电点;配合TCFG_LOWPOWER_LOWPOWER_SEL = SLEEP_EN生效。 - 日志开销:
app_key_event_handler、app_msg_event_handler、app_device_event_handler均无条件log_info。调试串口波特率 1Mbps 下开销较小,但量产固件可通过app_config.c中对应模块的log_tag_const_*开关或移除CONFIG_DEBUG_ENABLE裁剪打印,降低中断与串口负载。 - 时钟档位:
TCFG_CLOCK_SYS_HZ = 96MHz、TCFG_CLOCK_LSB_HZ = 48MHz、PLL 480MHz 是功耗与性能的平衡点;需要更强算力(如更复杂解码)时可在 SDK 支持范围内上调系统时钟。 - SFC 频率约束:
const_config_sfc_freq = 64000000(64MHz),注释给出电压-频率对照(IOVDD>1.65V 不超过 64MHz,>2.3V 不超过 80MHz,>2.7V 不超过 96MHz),改动供电方案时必须同步核对。
扩展点
- 按键业务:在
app_key_event_handler中按key->type/key->event/key->value分发业务(参考#if 0中 AD 键长按关机示例),并配合app_config.h的TCFG_*KEY_ENABLE开关选择键源。 - 消息业务:在
app_msg_event_handler的switch (msg->event)中追加新消息类型分支(如解码失败、录音结束等),消息由 SDK 各模块通过系统事件队列投递。 - USB Host 设备节点:
DEVICE_EVENT_FROM_USB_HOST分支的dev_open("name", &arg)/dev_close(...)注释即标准模板——Host 检测到存储设备后,在此打开设备节点进行读写。 - 新增外设初始化:在
onchip_device_init()中追加(须遵守 RTC 时序纪律);需要在rtc_init之后初始化的显示类外设,按注释建议放在 RTC 之后。 - 周期任务:直接在主循环
while (1)内、sys_event_get()之前插入周期性逻辑(如状态机步进),但需保证单次执行时间可控,避免影响事件响应与喂狗节奏。
相关链接
- app_main.c(主入口实现)
- app_main.h(入口声明)
- app_config.h(编译期配置)
- app_config.c(Log 分级与系统常量)
- device_config.c(设备配置)
- sdk_version.z.S(SDK 版本定义)
提示:按键驱动、USB 协议栈、解码器等子系统分别属于本目录树的其他页面;本文仅在主入口调用它们时给出上下文说明。