杰理 SDK 文档中心
首页
首页
  • 概述与入门

    • 项目概述与芯片平台
    • 环境搭建与工具链安装
    • 编译与烧录指南
    • 工程结构总览
  • 应用层与公共模块

    • GP MCU 主应用入口
    • AT 指令与调试模块
    • 电池检测与电源管理
    • EEPROM 与参数存储
    • 按键与 USB 设备驱动
    • 音频解码与 APA 语音播报
  • 外设驱动与示例

    • 高精度 ADC(HADC)
    • 通用 ADC 与定时器
    • UART / SPI / IIC 通信外设
    • MCPWM 与电机控制
    • RTC 与低功耗唤醒
    • 段码 LCD 驱动
    • NOR Flash 与红外编解码组件
  • 显示与 UI 系统

    • LCD 驱动与字库引擎
    • UI 平台与控件绘制
    • UI 工程与资源生成工具
  • 系统底层与芯片平台

    • cd09 芯片平台与预编译库
    • GPIO 与 IIC 底层驱动
    • 系统文件系统与设备模型
  • 启动引导与固件升级

    • UBOOT 引导工程
    • 固件升级机制
  • 开发工具与资源

    • 编译脚本与命令行工具
    • 音频文件转换工具
    • 硬件资料与文档资源

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

分步解读与设计意图:

  1. 打印启动标记 log_info("app_main!\n"):LOG_TAG 被定义为 "[APP]"(app_main.c#L14-L15),配合 debug.h 的 tag 机制,应用层日志统一带 [APP] 前缀,便于在混合日志中过滤。
  2. onchip_device_init()(app_main.c#L104-L109)依次调用 adc_init()(GPADC 模块初始化,AD 按键与电池检测依赖)与 devices_init()(注册设备表,供 dev_open/dev_read 等设备接口使用)。片上设备必须先于按键与 USB 初始化,因为后者会引用 AD 通道与设备节点。
  3. key_driver_init():启动按键驱动,之后按键扫描/去抖/长短按判定由驱动在中断或低功耗唤醒上下文中完成,最终以 SYS_KEY_EVENT 上报应用。
  4. user_usb_otg_init():配置 OTG 口,之后 USB 插拔以 SYS_DEVICE_EVENT(DEVICE_EVENT_FROM_OTG)上报。
  5. RTC 初始化时序注释:代码刻意将 rtc_init(...) 注释保留为占位,并写明两个约束——(a) RTC 必须在唤醒 IO 初始化之后,避免唤醒 IO 未配置时 RTC 溢出关机导致唤醒异常;(b) 显示相关初始化放到 rtc_init 之后,避免 RTC 溢出唤醒瞬间屏幕亮起。这是"初始化顺序由硬件时序约束决定"的典型例子,直接引用线上 issue 作为依据。
  6. 主循环(详见下一节)。

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_EVENTapp_key_event_handler按键按下/抬起/长按(IO 键、AD 键、IR 键、触摸键)
SYS_MSG_EVENTapp_msg_event_handler解码结束等应用消息(MSG_EVENT_DECODE_END)
SYS_DEVICE_EVENTapp_device_event_handlerUSB 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()

app_main.c#L104-L109:

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)

时序要点:

  1. USB 插拔由驱动以 SYS_DEVICE_EVENT 异步上报,应用在下一次主循环迭代中才处理——事件处理与 MSD 轮询天然串行,无需加锁;
  2. 设备句柄 usb_device 的赋值发生在事件处理器中,而消费发生在同一线程的主循环顶部,两者同上下文、无竞争;
  3. 无任何事件时 ___idle 使 CPU 休眠,中断唤醒后重新喂狗,形成"事件驱动 + 低功耗"闭环。

配置选项

主应用行为完全由 app_config.h 中的宏定义在编译期决定。与主入口直接相关的配置如下(完整文件见 app_config.h):

基础开关与系统配置

配置项类型默认值说明
ENABLE_THIS_MOUDLE / DISABLE_THIS_MOUDLEint1 / 0模块使能/禁用的统一开关常量
ENABLE / DISABLEint1 / 0通用开关常量
NO_CONFIG_PORTint-1表示"未配置 IO 端口"的哨兵值
CONFIG_DEBUG_ENABLE宏定义系统打印总开关(未定义则关闭全部打印)

串口(打印)配置

配置项类型默认值说明
TCFG_DB_UART_ENABLEintENABLE打印串口使能
TCFG_DB_UART_RX_PORTIONO_CONFIG_PORT串口接收脚(打印场景可不接)
TCFG_DB_UART_TX_PORTIOIO_PORTA_04串口发送脚
TCFG_DB_UART_BAUDRATEint1000000打印波特率(1M)

时钟配置(决定主循环与外设节奏)

配置项类型默认值说明
TCFG_CLOCK_PLL_HZlong480MHzPLL 主频(240M 或 480M)
TCFG_CLOCK_SYS_HZlong96MHzCPU 运行时钟
TCFG_CLOCK_LSB_HZlong48MHz外设 LSB 时钟

按键配置(决定 app_key_event_handler 收到的键源)

配置项类型默认值说明
KEY_IO_NUM_MAXint3IO 按键最大数量
KEY_AD_NUM_MAXint10AD 按键最大数量
KEY_IR_NUM_MAXint21IR 按键最大数量
KEY_TOUCH_NUM_MAXint3触摸按键最大数量
TCFG_IOKEY_ENABLEintDISABLEIO 按键使能(端口 PA3/PA6/PA7)
TCFG_ADKEY_ENABLEintENABLEAD 按键使能(PA0 / AD_CH_PA0,外部上拉 R_UP=220)
TCFG_IRKEY_ENABLEintDISABLEIR 按键使能(PA2)
TCFG_TOUCH_KEY_ENABLEintDISABLE触摸按键使能(key0=PA8/0x12,key1=PA9/0x34)

USB 配置(决定 app_usb_event_handler 与 MSD 轮询行为)

配置项类型默认值说明
TCFG_PC_ENABLEintENABLEPC 模块使能
TCFG_UDISK_ENABLEintDISABLEU 盘模块使能
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_ENABLEintENABLEUSB 动态内存使能
TCFG_USB_SLAVE_ENABLEint(由 USB 模块)设备模式使能,控制 app_usb_event_handler 的 slave 分支与 MSD 条件编译
TCFG_USB_SLAVE_MSD_ENABLEint(由 USB 模块)设备模式 MSD 使能,与 usb_device != NULL 共同决定主循环是否调用 USB_MassStorage()

电源、复位与低功耗配置(影响主循环空转行为)

配置项类型默认值说明
TCFG_LOWPOWER_LOWPOWER_SELintSLEEP_EN低功耗模式选择
TCFG_VDDIOM_LEVELintVDDIOM_VOL_33VVDDIO 电压档
TCFG_LVD_USE_WKUP_MODE_ENABLEintDISABLELVD 中断模式(防写 VM 时掉电损坏)
TCFG_SFC_FAST_BOOT_ENABLEintDISABLE快速启动开关
PINR_RESET_IOIOIO_PORTA_00长按复位 IO
PINR_RESET_LVLint0复位触发沿(0 下降沿 / 1 上升沿)
PINR_PRESS_TIMEint8长按时长(0/1/2/4/8/16 秒)
PINR_RESET_RELASEint10 释放后复位 / 1 立即复位
PINR_PROTECT_ENint0写保护使能
TCFG_BATTERY_TYPE_SELintBATTERY_TYPE_3_DRY_DELL供电类型(2/3/4 节干电池或锂电池)
TCFG_BAT_DET_IOIONO_CONFIG_PORT电池检测 IO(未配置则走 PMU 通道)
TCFG_BAT_AD_CHANNELADAD_ANA_PMU_CH_VPWR_DIV_4电池检测 AD 通道
TCFG_APP_RTC_ENint0RTC 功能使能(对应 app_main 中被注释的 rtc_init 占位)
TCFG_APP_RTC_CLK_SELintCLK_SEL_LRCRTC 时钟源

存储与外设配置

配置项类型默认值说明
TCFG_VM_SIZEint16VM 空间大小(Kbyte)
TCFG_VM_INIT_FASTint0VM 快速初始化
TCFG_SD0_ENABLE / TCFG_SD1_ENABLEint0 / 0SD 卡使能
TCFG_MSFC_DEV_ENABLEintDISABLE内置 Flash 枚举为 MSD
TCFG_HW_SPI1_*IO/数值PC4/PC5, 24MHzLCD SPI1 端口与速率
TCFG_HW_SPI2_*IO/数值PA10/PA9/PA11/PA12, 1MHzNOR FLASH SPI2(当前 DISABLE)
TCFG_AT_CHAR_UART_*IO/数值PC4/PC3, 115200AT 蓝牙串口
TCFG_DUAL_BANK_ENABLEintDISABLE双备份升级使能
TCFG_DB_UPDATE_PASSIVE_DEMO_ENintDISABLE升级参考 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),改动供电方案时必须同步核对。

扩展点

  1. 按键业务:在 app_key_event_handler 中按 key->type/key->event/key->value 分发业务(参考 #if 0 中 AD 键长按关机示例),并配合 app_config.h 的 TCFG_*KEY_ENABLE 开关选择键源。
  2. 消息业务:在 app_msg_event_handler 的 switch (msg->event) 中追加新消息类型分支(如解码失败、录音结束等),消息由 SDK 各模块通过系统事件队列投递。
  3. USB Host 设备节点:DEVICE_EVENT_FROM_USB_HOST 分支的 dev_open("name", &arg)/dev_close(...) 注释即标准模板——Host 检测到存储设备后,在此打开设备节点进行读写。
  4. 新增外设初始化:在 onchip_device_init() 中追加(须遵守 RTC 时序纪律);需要在 rtc_init 之后初始化的显示类外设,按注释建议放在 RTC 之后。
  5. 周期任务:直接在主循环 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 协议栈、解码器等子系统分别属于本目录树的其他页面;本文仅在主入口调用它们时给出上下文说明。

Next
AT 指令与调试模块