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

    • 项目概览
    • 快速开始与开发环境
  • 应用与运行时

    • 应用入口与主循环
    • 按键驱动与用户消息处理
    • 消息系统
  • 固件升级

    • 双备份升级机制与状态机
    • UART 升级传输
    • 升级校验、启动信息与复位流程
  • 芯片与硬件支持

    • AC63 系列芯片 BSP 结构
    • 外设接口与驱动
    • 低功耗、RTC 与时基唤醒
  • 构建与工具

    • 构建系统与工作区
    • 烧写与量产工具
  • 参考资源

    • 数据手册与原理图
    • 双备份升级文档

应用入口与主循环

本页描述 AC63_GP_MCU 固件的应用层入口 user_main() 与常驻主循环(main loop)的完整实现:从 SDK 初始化完成后的应用启动、条件编译的初始化序列,到 while(1) 内的喂狗—消息处理—低功耗休眠循环,以及 user_msg_handler() 的消息分发机制。

Purpose and Scope

本页覆盖"应用入口与主循环"这一能力的全部实现细节,包括:

  • 应用入口函数 user_main() 的初始化序列与编译期裁剪开关
  • 主循环的三大动作:wdt_clear() 喂狗、user_msg_handler() 消息处理、__asm__ volatile("idle") 低功耗休眠
  • 用户消息处理 user_msg_handler() 的分发逻辑与按键消息
  • 相关配置宏(APP_VERSION_CHECK、USE_KEY_DRIVER、DUAL_BANK_UPDATE_BY_UFW)的作用

以下主题属于兄弟页面,本页不展开:

  • 按键驱动 key_driver_init / iokey_para 的底层实现 → 参见"按键驱动"页面
  • 双 bank UFW 升级流程 dual_bank_update_init / update_download_opt → 参见"固件升级"页面
  • SDK 启动早期代码(Reset 向量、sys_init 之前的引导)→ 参见"系统启动与时钟"页面

Overview

在嵌入式 MCU 固件中,user_main() 是 SDK 完成底层初始化(时钟、电源、外设、内存等)之后交给应用层的第一个入口,也是应用逻辑的"心脏":它负责在进入主循环之前完成必要的应用级初始化,然后永不返回地驻留在一个紧凑的循环中。

该设计的核心意图:

  1. 单一入口、永不返回:user_main() 末尾的 while(1) 没有退出路径,return 0 仅用于满足编译器对非 void 返回类型的约束,实际不可达。这是裸机(bare-metal)固件最常见的范式——所有应用行为都由这个循环驱动。
  2. 条件编译裁剪:三个宏(APP_VERSION_CHECK、USE_KEY_DRIVER、DUAL_BANK_UPDATE_BY_UFW)在编译期决定初始化内容,使同一份源码可以适配不同产品配置(是否需要按键、是否支持双 bank 升级),避免运行时分支开销。
  3. 低功耗优先的主循环:循环体在无消息时立即执行 idle 指令,让 CPU 进入休眠等待中断唤醒,这是电池供电 MCU 产品的关键省电手段。
  4. 集中式消息分发:user_msg_handler() 通过 get_msg() 从消息队列取消息并分发,使异步事件(按键、升级等)统一收敛到主循环上下文处理,避免在中断上下文执行耗时逻辑。

Architecture

下图展示了从系统上电到应用主循环的完整架构与数据流:

flowchart TD
    subgraph sg_Boot["启动阶段"]
        Start["系统上电 / Reset"] --> SDKInit["SDK 底层初始化<br/>(时钟/电源/外设)"]
        SDKInit --> UM["user_main() 应用入口"]
    end

    subgraph sg_Init["应用初始化序列 (条件编译)"]
        UM --> VC{"APP_VERSION_CHECK<br/>宏开关"}
        VC -->|"1 (默认启用)"| AV["app_version_check()<br/>版本校验"]
        VC -->|"0 (禁用)"| KD
        AV --> KD{"USE_KEY_DRIVER == 1"}
        KD -->|"1 (启用)"| KDI["key_driver_init(&iokey_para)<br/>按键驱动注册"]
        KD -->|"0 (禁用)"| DB
        KDI --> DB{"DUAL_BANK_UPDATE_BY_UFW"}
        DB -->|"1 (启用)"| DBI["dual_bank_update_init()<br/>双 bank 升级初始化"]
        DB -->|"0 (禁用)"| Loop
        DBI --> Loop
    end

    subgraph sg_Loop["主循环 while(1)"]
        Loop["wdt_clear() 喂狗"] --> Handler["user_msg_handler()<br/>消息处理"]
        Handler --> Idle["__asm__ volatile('idle')<br/>CPU 休眠等待中断"]
        Idle --> Loop
    end

    Handler -->|"get_msg() / NO_MSG"| MsgQ["消息队列 msg.h"]
    MsgQ -.->|"中断/外设投递消息"| HW["按键/升级等事件源"]

架构说明:

  • 启动阶段:系统上电后由 SDK 引导代码完成基础初始化,最终调用 user_main()。user_main() 的定义在 sdk/apps/main.c#L41-L64。
  • 应用初始化序列:三个条件编译宏依次决定版本校验、按键驱动、双 bank 升级是否初始化。这些初始化全部在主循环启动前完成,保证循环体内不依赖运行时初始化失败处理。
  • 主循环:wdt_clear() 定期喂狗防止看门狗复位;user_msg_handler() 消费消息队列;idle 指令让 CPU 进入休眠,等待中断(如按键定时扫描、升级 UART 中断)唤醒后回到循环顶部。
  • 消息队列:user_msg_handler() 通过 msg.h 提供的 get_msg() 获取消息,NO_MSG 表示队列为空,立即返回让循环进入休眠——这是"事件驱动 + 轮询"混合模型的典型形态。

实现剖析:user_main() 入口

user_main() 是整个应用层的入口函数,完整源码如下:

int user_main()
{
#if (APP_VERSION_CHECK)
    app_version_check();
#endif

#if (USE_KEY_DRIVER == 1)
    key_driver_init(&iokey_para);
#endif

#if (DUAL_BANK_UPDATE_BY_UFW)
    dual_bank_update_init();
#endif

    while (1) {
        wdt_clear();

        user_msg_handler();

        __asm__ volatile("idle");
    }

    return 0;
}

Source: main.c

初始化阶段(进入循环之前)

user_main() 在进入主循环前按顺序执行三段"条件编译初始化",设计意图是让不同产品配置共用同一入口源码:

  1. 版本校验 app_version_check()(宏 APP_VERSION_CHECK,默认定义为 1 启用)

    • 该函数声明为 extern int app_version_check();(见 main.c#L6),实现位于 SDK 其他模块,用于校验应用与固件版本匹配,防止烧录错误版本导致异常。
    • 放在最前面的原因:如果版本不匹配,后续初始化(按键、升级通道)就没有意义,尽早失败比在运行时暴露问题更安全。
  2. 按键驱动初始化 key_driver_init(&iokey_para)(宏 USE_KEY_DRIVER == 1 时编译)

    • 传入全局参数 iokey_para 完成 IO 按键驱动的注册,包括按键扫描周期、去抖参数、按键映射表等。
    • 之所以用编译期判断而非运行时判断,是因为在资源受限的 MCU 上,未使用的按键驱动代码可以直接被链接器裁剪,减小 Flash 占用。
  3. 双 bank 升级初始化 dual_bank_update_init()(宏 DUAL_BANK_UPDATE_BY_UFW 时编译)

    • 通过 UART 接收 UFW 升级包并写入双 bank 分区,实现在线固件升级。
    • 必须在主循环前完成,因为升级过程依赖主循环中的 update_download_opt() 轮询(见下文 user_msg_handler())。

主循环:喂狗 — 消息 — 休眠

    while (1) {
        wdt_clear();

        user_msg_handler();

        __asm__ volatile("idle");
    }

Source: main.c

循环体只有三行,却承担了全部运行时行为,逐行分析:

  • wdt_clear():清除看门狗计数。如果主循环因异常(死锁、长耗时阻塞)无法及时回到循环顶部,看门狗将超时复位整个芯片。这一行的存在意味着:任何阻塞在主循环上下文中的操作都不能超过看门狗超时时间,这是设计主循环内任务时的硬约束。放在循环体最顶部,保证每次唤醒后最先喂狗。
  • user_msg_handler():消费消息队列中的待处理消息(详见下一节)。队列为空时立即返回,不空转。
  • __asm__ volatile("idle"):执行 CPU 的 idle 指令进入休眠态,等待中断唤醒。这是低功耗设计的核心:没有事件时 CPU 不取指、不执行,仅在中断到来时被唤醒回到循环顶部。volatile 防止编译器优化掉这条汇编指令。

循环结构保证了:每次中断唤醒 → 喂狗 → 处理一条消息 → 再次休眠。事件驱动模式下 CPU 占用率极低。

实现剖析:user_msg_handler() 消息分发

static void user_msg_handler()
{
#if (DUAL_BANK_UPDATE_BY_UFW)
    update_download_opt();
#endif

#if (USE_KEY_DRIVER == 1)

    int msg = get_msg();
    if (msg == NO_MSG) {
        return;
    }

    switch (msg) {
    case MSG_TEST_IO_KEY1_SHORT:
        log_info("IO_KEY1_SHOURT");
        break;
    case MSG_TEST_IO_KEY1_LONG:
        log_info("IO_KEY1___LONG");
        break;
    case MSG_TEST_IO_KEY1_HOLD:
        log_info("IO_KEY1_HOLD");
        break;
    case MSG_TEST_IO_KEY1_LONG_HOLD_UP:
        log_info("IO_KEY1_LONG_HOLD_UP");
        break;
    default:
        break;
    }

#endif
}

Source: main.c

消息处理的执行流

user_msg_handler() 的逻辑按以下顺序执行:

  1. 升级轮询优先(DUAL_BANK_UPDATE_BY_UFW 时编译):无条件调用 update_download_opt()。升级下载过程需要高频轮询 UART 接收状态,因此放在消息处理的最前面、每次循环都被调用,保证升级包的接收不被其他消息阻塞。
  2. 取消息:int msg = get_msg(); 从消息队列取出一条消息。get_msg() 与 NO_MSG 常量由 msg.h 提供(本文件第 2 行 #include "msg.h")。
  3. 空队列短路返回:if (msg == NO_MSG) return; —— 无消息时立即返回,让主循环进入 idle 休眠。这是低功耗的关键路径,绝不在空队列时轮询空转。
  4. switch 分发:按消息类型分发处理。当前模板实现处理四类 IO 按键消息,均通过 log_info 输出日志:
    • MSG_TEST_IO_KEY1_SHORT → 短按,日志 "IO_KEY1_SHOURT"(注意:源码中该字符串为原始拼写)
    • MSG_TEST_IO_KEY1_LONG → 长按,日志 "IO_KEY1___LONG"
    • MSG_TEST_IO_KEY1_HOLD → 持续按住,日志 "IO_KEY1_HOLD"
    • MSG_TEST_IO_KEY1_LONG_HOLD_UP → 长按释放,日志 "IO_KEY1_LONG_HOLD_UP"
    • default → 其他消息忽略(break),保证队列中的未知消息被安全消费掉,不会造成队列积压。

设计意图

  • 消息机制解耦:按键扫描、升级等事件源在中断或定时器上下文中投递消息,应用层在主循环上下文统一处理,避免在中断里执行 log_info 等耗时操作。
  • 模板化扩展点:switch 中的 case 分支就是应用业务逻辑的扩展点——产品开发者在此处添加实际业务处理(如短按开机、长按配对)。当前模板仅打印日志,属于"最小可用"的参考实现。
  • 条件编译隔离:整个按键处理块被 #if (USE_KEY_DRIVER == 1) 包裹,无按键的产品编译时不生成任何按键相关代码。

核心流程:主循环的运行时序

下图展示一次完整的主循环迭代中,各组件在时间轴上的交互:

sequenceDiagram
    participant WDT as 看门狗定时器
    participant CPU as CPU 核心
    participant MSG as 消息队列 (msg.h)
    participant KEY as 按键驱动
    participant UFW as 双bank升级模块

    Note over CPU: 上电 → SDK 初始化 → user_main()
    CPU->>CPU: 条件编译初始化 (版本/按键/升级)
    loop 主循环 while(1)
        CPU->>WDT: wdt_clear() 喂狗
        alt DUAL_BANK_UPDATE_BY_UFW 启用
            CPU->>UFW: update_download_opt() 轮询升级
        end
        alt USE_KEY_DRIVER 启用
            CPU->>MSG: get_msg() 取消息
            MSG-->>CPU: msg / NO_MSG
            alt msg != NO_MSG
                CPU->>CPU: switch(msg) 分发并 log_info
            end
        end
        CPU->>CPU: __asm__ volatile("idle") 休眠
        KEY-->>CPU: 按键中断/定时唤醒
        UFW-->>CPU: UART 中断唤醒
        CPU->>CPU: 唤醒 → 回到循环顶部
    end

时序要点:

  1. 每次循环迭代严格按"喂狗 → 处理消息 → 休眠"顺序执行,喂狗永远最先,防止看门狗误复位。
  2. 休眠期间 CPU 不执行指令,仅由中断(按键驱动定时扫描、升级 UART 接收等)唤醒——唤醒源与消息投递源一一对应。
  3. 消息处理是"轮询式"的(get_msg() 主动取),而唤醒是"中断式"的,二者通过消息队列衔接,形成事件驱动主循环。

配置选项

应用入口与主循环的行为由三个编译期宏控制,全部在 main.c 中通过 #if 指令引用:

宏类型默认值作用
APP_VERSION_CHECK整数宏1(#define APP_VERSION_CHECK \t1,见 main.c#L5)为 1 时在入口调用 app_version_check() 校验应用/固件版本
USE_KEY_DRIVER整数宏由工程配置(非零即启用)为 1 时编译按键驱动初始化 key_driver_init(&iokey_para) 与按键消息分发代码
DUAL_BANK_UPDATE_BY_UFW布尔宏由工程配置启用时编译 dual_bank_update_init() 初始化与 update_download_opt() 升级轮询

配置说明:

  • APP_VERSION_CHECK 在 main.c 顶部显式定义,属于文件级开关;其余两个宏来自工程级配置头文件(经 asm/includes.h 间接引入)。
  • 三者都是编译期常量,改动后必须重新编译烧录;这是为了在资源受限 MCU 上让未使用代码被预处理器/链接器完全剔除,零运行时开销。
  • 依赖关系:USE_KEY_DRIVER 还决定 iokey_para 参数与消息常量(MSG_TEST_IO_KEY1_*)是否参与编译,这些常量定义在 msg.h 中。

API Reference

以下为本页涉及的关键接口,签名与行为均取自实际源码。

int user_main(void)

应用层入口函数,由 SDK 在完成底层初始化后调用,永不返回。

  • 位置:main.c#L41-L64
  • 返回:0——仅在理论上的循环退出后执行,实际不可达
  • 行为:依次执行条件编译初始化(app_version_check / key_driver_init / dual_bank_update_init),随后进入 while(1) 主循环
  • 注意:本函数被调用后不返回,任何位于其后的代码不会被执行

static void user_msg_handler(void)

主循环内的消息处理函数,静态(文件私有),每次循环迭代被调用一次。

  • 位置:main.c#L8-L39
  • 行为:启用升级时先调用 update_download_opt();启用按键驱动时调用 get_msg() 取消息,NO_MSG 直接返回,否则按 switch 分发按键消息并打印日志

extern int app_version_check(void)

版本校验函数,声明于 main.c#L6,实现在 SDK 其他模块。

  • 调用条件:APP_VERSION_CHECK 非零
  • 用途:校验应用版本与固件版本的一致性,失败时通常阻止继续启动

key_driver_init(&iokey_para)

按键驱动注册函数,参数为按键参数结构体(iokey_para 为全局定义)。

  • 调用条件:USE_KEY_DRIVER == 1
  • 用途:注册 IO 按键扫描与消息投递,按键事件经 msg.h 消息队列送达主循环

get_msg() / NO_MSG

消息队列取消息接口与空队列常量,由 msg.h 提供。

  • 返回:NO_MSG 表示队列为空;否则返回消息 ID(如 MSG_TEST_IO_KEY1_SHORT)
  • 使用模式:int msg = get_msg(); if (msg == NO_MSG) return;(见 main.c#L16-L19)

按键消息常量

常量含义模板处理
MSG_TEST_IO_KEY1_SHORTIO 按键短按log_info("IO_KEY1_SHOURT")
MSG_TEST_IO_KEY1_LONGIO 按键长按log_info("IO_KEY1___LONG")
MSG_TEST_IO_KEY1_HOLDIO 按键持续按住log_info("IO_KEY1_HOLD")
MSG_TEST_IO_KEY1_LONG_HOLD_UPIO 按键长按后释放log_info("IO_KEY1_LONG_HOLD_UP")

故障模式、边界情况与并发

看门狗超时(最关键的故障模式)

主循环中 wdt_clear() 是唯一的喂狗点(main.c#L56)。任何使循环迭代时间超过看门狗超时值的代码(如在 user_msg_handler() 的 case 分支中加入阻塞等待、长延时)都会导致芯片被看门狗复位。约束:消息处理分支必须是非阻塞、可快速完成的短任务;耗时操作应拆分为多步状态机或移交后台处理。

消息队列溢出与未知消息

  • get_msg() 取出的消息若不在 switch 的 case 列表中,会落入 default 分支被消费丢弃——这保证队列不会因未知消息积压,但也意味着新消息类型必须显式添加分支,否则静默丢失。
  • 消息队列容量由 msg.h/SDK 配置决定;若中断投递速率高于主循环消费速率,队列可能溢出丢消息。当前模板每循环只消费一条消息,升级期间 UFW 轮询会占用循环时间,需保证消费速率充足。

中断与主循环的并发

按键扫描、UART 接收等事件源在中断上下文中投递消息,主循环在普通上下文消费。二者通过消息队列解耦,队列访问需由 SDK 保证原子性。user_msg_handler() 本身运行在普通上下文,不会被同优先级任务打断,但可能被中断打断——因此消息处理分支中不应访问与中断共享的非原子数据。

空队列路径的热点

NO_MSG 短路返回(main.c#L17-L19)是每次循环的必经路径,也是低功耗的关键。若在此处误加延时或忙等,将直接拉高功耗并威胁看门狗。

性能与运维考虑

  • CPU 占用:得益于 idle 休眠,无事件时 CPU 占用率趋近于零;唤醒后单次迭代仅执行取消息 + 日志,开销为微秒级。
  • 日志开销:log_info 在按键事件时输出,若按键频繁(如 HOLD 消息周期性投递),串口日志可能成为主要耗时来源,开发调试时可临时关闭日志评估真实时序。
  • 版本校验:app_version_check() 仅在启动时执行一次,不占用运行时资源;量产时建议保持 APP_VERSION_CHECK 为 1,防止误烧版本。
  • 升级与按键的时序竞争:update_download_opt() 位于消息处理最前,每次循环无条件执行,确保升级数据不丢;但同时会推迟按键消息处理,升级期间按键响应会有延迟,属预期行为。

扩展点

  1. 业务消息分支:在 user_msg_handler() 的 switch 中添加新的 case,即可为按键事件绑定实际业务逻辑(如短按切换模式、长按进入配对)。这是本模板最主要的定制位置。
  2. 新消息源:其他外设(触摸、传感器)可通过 msg.h 的消息机制投递自定义消息,在主循环统一分发;需同步在 msg.h 定义消息常量。
  3. 新增初始化项:在 user_main() 的初始化序列中按同样模式追加 #if (xxx) 条件编译初始化调用,保持与现有三段初始化一致的裁剪风格。
  4. 低功耗策略调整:若产品需要更深睡眠(如 sleep/power down),可在 idle 指令位置替换为 SDK 提供的低功耗 API,循环结构本身无需改动。

相关链接

  • main.c(应用入口与主循环源码)
  • 按键驱动 key_driver_init / iokey_para 实现 → 参见"按键驱动"页面
  • 双 bank UFW 升级 dual_bank_update_init / update_download_opt → 参见"固件升级"页面
  • 消息队列 get_msg / NO_MSG / MSG_TEST_IO_KEY1_* 定义 → 参见"消息机制"页面
  • 系统启动引导(user_main 之前的 SDK 初始化)→ 参见"系统启动与时钟"页面
Next
按键驱动与用户消息处理