应用入口与模式调度
本文档介绍 AD23N (sh59) 固件的应用入口(c_main 启动链路)与多模式应用调度架构,涵盖从复位上电到 app() 分发各业务模式(idle、linein、loudspeaker、midi_dec、midi_keyboard、record、softoff)的完整机制。
Purpose and Scope
本页面向应用层入口与模式调度这一能力边界,覆盖:
- 复位后的引导序列:
maskrom_init→ 时钟/电源初始化 →system_init→app(); - 应用模式的组织方式:
sdk/app/src/mbox_flash/下各模式目录的*_mode.c/*_mode_key.c双文件约定; - 看门狗、异常挂钩等与入口生命周期相关的运行机制。
以下主题属于兄弟页面范畴,本页不做展开:单个模式内部的业务逻辑(如 MIDI 解码、录音编码)与具体按键事件流,建议查阅对应的模式实现页面;底层驱动(时钟、电源、MMU)的细节可参考 BSP/驱动相关文档。
Overview
AD23N 固件采用"单一入口 + 多模式调度"的应用组织方式:所有业务能力(空闲待机、线路输入、外放、MIDI 解码、MIDI 键盘、录音、软关机)都被封装为相对独立的"模式"模块,统一挂在 sdk/app/src/mbox_flash/ 目录下,由应用层 app.c 在系统初始化完成后统一调度。
这种设计的核心意图是解耦与可裁剪:
- 每个模式是一个自包含单元,拥有自己的主逻辑文件(
*_mode.c)与按键处理文件(*_mode_key.c),新增模式无需改动既有模式实现; - 模式集合通过
app_config.c进行配置,编译期即可裁剪不需要的功能,减少固件体积; - 入口(
main.c)只负责把系统拉起,业务分发全部交给应用层,职责单一。
Architecture
下图展示了从引导 ROM 到模式模块的完整架构关系(节点均对应仓库中的真实文件/模块):
flowchart TD
subgraph sg_Boot["启动层 sdk/app/bsp/start/sh59"]
ROM["MaskROM 引导"]
CMain["c_main() main.c"]
SysInit["system_init()"]
end
subgraph sg_App["应用层 sdk/app/src/mbox_flash"]
AppEntry["app() app.c"]
AppCfg["app_config.c 模式配置"]
Scheduler["模式调度"]
end
subgraph sg_Modes["模式集合"]
Idle["idle_mode"]
LineIn["linein_mode"]
Loud["loudspeaker_mode"]
MidiDec["midi_dec_mode"]
MidiKb["midi_keyboard_mode"]
Record["record_mode"]
SoftOff["softoff_mode"]
end
subgraph sg_Impl["模式内部双文件结构"]
ModeC["*_mode.c 模式主逻辑"]
KeyC["*_mode_key.c 按键处理"]
end
ROM --> CMain
CMain --> SysInit
SysInit --> AppEntry
AppEntry --> Scheduler
AppCfg --> Scheduler
Scheduler --> Idle
Scheduler --> LineIn
Scheduler --> Loud
Scheduler --> MidiDec
Scheduler --> MidiKb
Scheduler --> Record
Scheduler --> SoftOff
Idle --> ModeC
LineIn --> ModeC
Loud --> ModeC
MidiDec --> ModeC
MidiKb --> ModeC
Record --> ModeC
SoftOff --> ModeC
ModeC --> KeyC
各层职责说明:
| 层次 | 模块 | 职责 |
|---|---|---|
| 启动层 | main.c 的 c_main() | 复位后的 C 入口:初始化掩膜 ROM 回调、看门狗、efuse、时钟/电压、日志、内存、电源,最后调用 system_init() 与 app() |
| 应用层 | app.c 的 app() | 应用主入口,由 system_init() 之后调用,负责拉起模式调度 |
| 应用层 | app_config.c | 应用/模式配置,决定编译期启用哪些模式(内容未在本页读取中逐行验证) |
| 模式集合 | 7 个模式目录 | 每个目录一个业务模式,包含主实现与按键处理 |
| 模式结构 | *_mode.c / *_mode_key.c | 双文件约定:主逻辑与按键事件解耦,便于独立维护 |
说明:
app()与app_config.c的内部实现细节未在本页的源文件读取预算内验证,上图中其与调度器的关系基于目录结构与main.c中的调用点推断;具体调度接口(模式注册表、切换函数)请以sdk/app/src/mbox_flash/app.c实际源码为准。
启动流程详解(c_main 引导序列)
固件的 C 语言入口是 sdk/app/bsp/start/sh59/main.c 中的 c_main(int cfg_addr)。cfg_addr 是 MaskROM 引导阶段传入的配置地址参数,用于定位启动配置。整个引导过程严格按照"最小依赖 → 基础服务 → 应用"的顺序展开:
int c_main(int cfg_addr)
{
maskrom_init();
wdt_init(WDT_8S);
efuse_init();
clk_voltage_init(CLOCK_MODE_ADAPTIVE, DVDD_VOL_123V);
clk_early_init(PLL_REF_LRC, 200000, 480000000);
log_init(1000000);
register_handle_printf_putchar(putchar);
log_info("--------sh59-apps-------------\n");
power_early_flowing();
board_power_init();
clock_dump();
efuse_dump();
mem_init();
/* mem_stats(); */
critical_hook_init();
early_system_init();
power_later_flowing();
xprintf("sh59 main!\n");
system_init();
app();
while (1) {
wdt_clear();
mdelay(500);
putchar('h');
}
return 0;
}
Source: main.c
引导各阶段说明
1. 基础运行环境(maskrom_init)
maskrom_init() 先向 MaskROM 注册回调:putchar(串口打印)、exception_analyze(异常分析挂钩)、udelay(微秒延时)以及中断开关函数;随后清零当前 DSP 核的 32 个中断配置寄存器(interrupt_init()),注册异常中断 IRQ_EXCEPTION_IDX(优先级 7),最后初始化调试输出。这一步确保后续任何代码在发生异常或需要打印时都有可用的基础设施:
void maskrom_init(void)
{
struct maskrom_argv argv;
memset((void *)&argv, 0, sizeof(struct maskrom_argv));
argv.pchar = (void (*)(char))putchar;
argv.exp_hook = exception_analyze;
argv.flt = NULL;
argv.udelay = udelay;
argv.local_irq_enable = local_irq_enable;
argv.local_irq_disable = local_irq_disable;
mask_init(&argv);
interrupt_init();
request_irq(IRQ_EXCEPTION_IDX, 7, exception_irq_handler, 0);
debug_init();
}
Source: main.c
2. 看门狗与基础外设
wdt_init(WDT_8S):启用 8 秒看门狗。这是系统保命底线——任何环节阻塞超过 8 秒未喂狗都会被复位(见下文"失败模式")。efuse_init():读取芯片 eFuse(一次性可编程存储),用于芯片标识/校准信息。clk_voltage_init(CLOCK_MODE_ADAPTIVE, DVDD_VOL_123V):以自适应模式初始化时钟-电压联动,内核电压档位设为 1.23V。clk_early_init(PLL_REF_LRC, 200000, 480000000):PLL 参考时钟取 LRC(低速时钟),参考频率 200kHz,目标主频 480MHz。
3. 日志与电源时序
log_init(1000000) 初始化日志系统(参数对应日志串口波特率配置),并通过 register_handle_printf_putchar 将标准 putchar 注册为打印句柄,使 log_info/xprintf 与裸 putchar 共用同一输出通道。电源初始化分为两段:
power_early_flowing():早期电源流程(先于内存管理可用之前);board_power_init():板级电源初始化;- 中间穿插
clock_dump()/efuse_dump()打印当前时钟与 eFuse 状态,便于开发期定位; power_later_flowing():在mem_init()、critical_hook_init()、early_system_init()之后执行的后期电源流程。
这种"早/晚两段式电源初始化"是嵌入式平台的常见手法:早期只做启动必需的电源动作,等内存、临界区挂钩、系统早期初始化就绪后再完成完整电源流程,降低启动早期的依赖风险。
4. 系统初始化与应用入口
system_init() 完成系统级初始化(任务/调度器等),随后立即调用 app() 进入应用层。应用层完成模式调度后,c_main 进入永真循环 while (1),每 500ms 喂一次看门狗并输出一个 'h' 心跳字符——这个循环是系统"活着"的最低证明,也保证看门狗不会在应用空闲时误复位。
模式集合与命名约定
app() 调度的各业务模式统一位于 sdk/app/src/mbox_flash/ 下,每个模式一个目录,遵循"主实现 + 按键处理"双文件命名约定:
| 模式目录 | 主实现文件 | 按键处理文件 | 业务含义(依据命名推断) |
|---|---|---|---|
idle | idle_mode.c | idle_mode_key.c | 空闲/待机模式 |
linein | linein_mode.c | linein_mode_key.c | 线路输入(LINE-IN)模式 |
loudspeaker | loudspk_mode.c | loudspk_mode_key.c | 外放/扬声器模式 |
midi_dec | midi_dec_mode.c | midi_dec_mode_key.c | MIDI 解码播放模式 |
midi_keyboard | midi_keyboard_mode.c | midi_keyboard_mode_key.c | MIDI 键盘模式 |
record | record_mode.c | record_mode_key.c | 录音模式 |
softoff_app | softoff_mode.c | (无独立按键文件) | 软关机模式 |
双文件约定的设计意图
把"模式主逻辑"与"按键处理"拆分为两个文件,而不是合并到一个巨型文件中,其收益在于:
- 关注点分离:
*_mode.c负责模式进入/退出、业务处理与资源管理;*_mode_key.c专注按键事件到模式动作的映射。硬件按键矩阵变化时,通常只需修改 key 文件; - 可维护性:每个文件职责单一、行数可控,便于审查与测试;
- 对称性:除
softoff_app外每个模式都有成对的实现,新模式的开发可以照搬现有模式的文件骨架,降低上手成本。
注意:各
*_mode.c/*_mode_key.c的内部接口(如模式注册函数、进入/退出回调、按键消息结构)未在本页读取预算内逐行验证;上述表格中"业务含义"依据目录命名推断,具体行为请查阅对应模式源文件。
核心调度流程
下图以时序图展示从复位到应用模式就绪的完整调用链:
sequenceDiagram
participant ROM as MaskROM
participant M as c_main (main.c)
participant S as system_init()
participant A as app() (app.c)
participant MODE as 模式模块 (idle/linein/...)
ROM->>M: 跳转 c_main(cfg_addr)
activate M
M->>M: maskrom_init() 异常/中断/调试回调
M->>M: wdt_init(WDT_8S) + efuse_init()
M->>M: clk_voltage_init + clk_early_init
M->>M: log_init + power_early_flowing
M->>M: mem_init + critical_hook_init
M->>M: early_system_init + power_later_flowing
M->>S: system_init()
S-->>M: 系统初始化完成
M->>A: app() 进入应用层
activate A
A->>A: 依据 app_config 确定模式集合
A->>MODE: 调度/切换各业务模式
MODE-->>A: 模式事件/切换请求
A-->>M: 应用退出(通常不返回)
deactivate A
M->>M: while(1): wdt_clear() + mdelay(500) + putchar('h')
deactivate M
流程要点:
- 串行初始化:引导阶段所有初始化都是严格串行的,前一步失败会直接阻塞后续步骤——这是嵌入式启动的典型特征,优点是时序确定、易排查,缺点是单点故障会卡死系统(由看门狗兜底复位);
- 入口收敛:
system_init()完成后,控制权一次性移交app(),main.c不再参与业务,只保留喂狗心跳循环; - 模式调度在上层:模式集合的启用以
app_config.c的配置为准(编译期裁剪),运行时由app()驱动的调度器进行模式切换;各模式通过自身事件(如按键、音频流状态)请求切换,具体切换接口见app.c实际实现; - 永不返回:
app()设计上不返回,c_main的while (1)是防御性兜底——即使应用层异常退出,系统仍保持心跳与喂狗,避免静默死机。
配置选项
引导阶段的可配置项(来自 main.c 中的实际调用参数):
| 配置项 | 取值(本工程默认) | 类型 | 说明 |
|---|---|---|---|
| 看门狗超时 | WDT_8S | 宏/枚举 | 8 秒看门狗;空闲循环每 500ms 喂一次 |
| 时钟-电压模式 | CLOCK_MODE_ADAPTIVE | 枚举 | 自适应时钟-电压联动 |
| 内核电压档位 | DVDD_VOL_123V | 枚举 | DVDD 目标电压 1.23V |
| PLL 参考源 | PLL_REF_LRC | 枚举 | 以 LRC 低速时钟作为 PLL 参考 |
| PLL 参考频率 | 200000 | int | 参考时钟 200kHz |
| 目标主频 | 480000000 | int | 系统主频 480MHz |
| 日志初始化参数 | 1000000 | int | log_init 参数(日志串口波特率) |
| 异常中断 | IRQ_EXCEPTION_IDX,优先级 7 | int | 异常处理中断注册 |
模式级配置(启用哪些模式、默认启动模式等)位于 sdk/app/src/mbox_flash/app_config.c,其内部字段未在本页读取预算内验证,请以实际文件为准。
API 参考
以下接口均出自 sdk/app/bsp/start/sh59/main.c:
int c_main(int cfg_addr)
固件 C 语言总入口,由 MaskROM/启动代码跳入。
参数:
cfg_addr(int):启动配置地址,由引导阶段传入,用于定位启动配置。
返回: int,正常路径下不会返回(陷入 while (1) 心跳循环);仅当循环被外部破坏时返回 0。
void maskrom_init(void)
向 MaskROM 注册运行时回调(打印、异常分析、延时、中断开关),清零中断配置并注册异常中断、初始化调试输出。必须在任何可能触发异常或打印的代码之前调用。
void interrupt_init(void)
关闭本地中断,将当前 DSP 核 ICFG00 起始的 32 个中断配置寄存器清零,再恢复中断。用于在引导早期建立干净的中断环境。
void critical_hook_init(void)
将 enter_critical_hook / exit_critical_hook 注册为临界区挂钩函数,使后续临界区操作获得统一入口。
失败模式、边界情况与并发
看门狗兜底复位
wdt_init(WDT_8S) 设置了 8 秒看门狗,唯一喂狗点在 c_main 的 while (1) 循环(wdt_clear() + mdelay(500))。由此推导出的失败语义:
- 应用层卡死:若
app()内部某模式进入死循环或长时间阻塞(超过 8 秒未回到心跳循环),系统将被看门狗复位。这是设计上"最坏情况保活"的兜底,代价是复位的原始上下文丢失,只能依靠日志(log_init输出的缓冲)事后分析; - 喂狗周期裕量:喂狗间隔 500ms 远小于 8 秒超时,正常路径有充足裕量;一旦出现接近超时的现象,说明应用层已有明显的调度停顿,是排查优先级反转或阻塞性等待的重要信号。
异常处理挂钩
maskrom_init() 通过 request_irq(IRQ_EXCEPTION_IDX, 7, exception_irq_handler, 0) 注册异常中断,并将 exception_analyze 挂入 MaskROM 的 exp_hook。这意味着:
- 非法指令、总线错误、未对齐访问等硬件异常会进入统一的
exception_irq_handler,配合debug_init()输出异常现场(PC、寄存器等),便于定位崩溃位置; - 该挂钩在引导最早期建立,保证任何阶段的异常都能被捕获,而非仅应用运行时。
引导阶段单点阻塞
启动序列为严格串行,任一环节(如 efuse_init、clk_early_init、mem_init)失败都会阻塞后续流程。由于看门狗在 maskrom_init() 之后、这些初始化之前已经启动,阻塞超过 8 秒会触发复位——因此引导期卡死表现为周期性复位(复位日志中可见重复的 --------sh59-apps------------- 打印)。排查时应优先检查 clock_dump() / efuse_dump() 的输出。
并发与中断安全
- 引导阶段全程在中断关闭/受控状态下进行(
interrupt_init()后局部开启),maskrom_init注册的local_irq_enable/local_irq_disable回调为后续临界区提供统一接口; critical_hook_init()统一了临界区入口,业务代码进入临界区时不再各自为政,降低了中断与任务并发访问共享资源(如模式状态、按键缓冲)时的不一致风险;- 各模式的按键处理(
*_mode_key.c)通常运行在按键事件上下文,与模式主逻辑(*_mode.c)之间通过事件/消息解耦,具体同步机制以各模式实现为准。
心跳输出
while (1) 中每 500ms 输出一个 'h' 字符。该心跳在量产/调试中可用于:判断系统是否存活、估算运行时长(每 'h' 约 0.5s)、通过串口抓取判断应用是否进入异常复位循环。
扩展点:新增一个业务模式
基于仓库中已验证的目录结构,新增模式的推荐步骤如下(调度器注册细节以 app.c/app_config.c 实际实现为准):
- 在
sdk/app/src/mbox_flash/下新建目录,例如my_mode/; - 按双文件约定创建
my_mode_mode.c(模式主逻辑)与my_mode_mode_key.c(按键处理),参考record/或idle/现有骨架; - 在
app_config.c的模式配置中登记新模式(启用与裁剪); - 在
app.c的调度逻辑中接入新模式的进入/退出/切换路径; - 若模式需要独立的按键文件之外的特殊输入(如 MIDI 通道数据),在
*_mode.c内自行管理对应外设资源。
模式间共享资源(音频通道、电源状态)的仲裁由调度器与各模式自身的进入/退出处理共同保证,新增模式时应确保进入时申请、退出时释放,避免跨模式资源泄漏。
测试与运维提示
- 仓库中各模式目录(
idle/、linein/、record/等)是验证模式调度的天然试验台:替换默认启动模式即可在开发板上单独验证单个模式; - 通过串口日志(
log_info("--------sh59-apps-------------")、clock_dump、efuse_dump、心跳'h')可观察:复位次数(定位引导卡死)、时钟/电压档位(定位低功耗或超频问题)、系统存活状态; - 修改
clk_early_init的目标主频(当前 480MHz)或电压档位(DVDD_VOL_123V)会影响功耗与稳定性,属于全局性改动,应回归验证所有模式。
相关链接
- 启动入口 main.c(
c_main完整引导序列) - 应用入口与配置 app.c / app_config.c(模式调度实现,未在本页逐行展开)
- 空闲模式 idle
- 线路输入模式 linein
- 外放模式 loudspeaker
- MIDI 解码模式 midi_dec
- MIDI 键盘模式 midi_keyboard
- 录音模式 record
- 软关机模式 softoff
单个模式的内部实现(音频通路、按键映射、状态机)属于各模式自身的文档范畴,请参见对应模式页面;本页聚焦应用入口与模式调度骨架。