mbox_flash 应用框架
mbox_flash 是 AW30N BLE SDK 中基于 Flash 运行(XIP,Execute-in-Place) 的轻量级应用框架,由 app.c 提供入口 mbox_flash_main(),负责系统初始化、工作模式装载(音乐/录音/Linein/MIDI/对箱/RC/RF Radio 等)、外设轮询(按键/ADC/充电/USB 热插拔)以及配套的链接脚本与内存布局管理。该框架专为从 SPI Flash 直接取指执行的低成本音箱(soundbox/mbox)类产品设计,是 BD49 平台应用侧的默认启动框架。
Purpose and Scope
本文档完整说明 mbox_flash 应用框架的实现机制,包括:
- 应用入口
mbox_flash_main()的启动序列与"设备更新完成"特殊流程 work_mode工作模式系统的装载、记忆(SYSMEM_INDEX_SYSMODE)与切换语义tick_timer_ram_loop()/app_timer_loop()定时轮询循环及各类外设挂接点- 链接脚本
app_ld.c的内存布局(app_code/ram0/cache_ram/boot_ram)与 overlay 条件编译 - 工程构建产物(
AW30N_mbox_flash.cbp、download_bat.c)与烧录流程
以下主题属于其他页面,不在本文展开:具体音乐解码(music_play)、USB 从机协议栈(usb_slave_mode)、录音算法(record_mode)、蓝牙协议栈(bt_ble)、VM 存储格式(vm_sfc)等具体模块的内部实现——本文只说明 mbox_flash 框架如何组织、装载和调度这些模块。
Overview
mbox_flash 应用框架解决的是"音箱产品固件如何组织"这一根本问题。与传统 MCU 上把全部代码烧进 ROM/内部 Flash 不同,AW30N 的 mbox_flash 方案把应用代码存放在 外部 SPI Flash 中,通过 #pragma code_seg(".app.text") 等段指令让代码直接在 Flash 上执行(XIP),从而把 RAM 留给数据、栈与蓝牙协议栈。
框架的核心设计可以概括为三点:
单一入口、循环调度:
mbox_flash_main()是整个应用的主入口,内部是一个while(1)大循环——每次循环先clear_all_message()清空消息队列,再读取/预擦除系统存储,最后按work_mode分发到对应模式的事件循环。模式切换通过写回SYSMEM_INDEX_SYSMODE并重启循环完成。模式即模块:每个工作模式(音乐、录音、Linein、MIDI、对箱、RC、RF Radio、RTC 等)对应一个独立模块,通过
app_modules.h统一头文件接入。框架本身不关心模式内部细节,只负责"装载哪个模式、在什么时机装载"。Tick 驱动的外设轮询:
app_timer_loop()以系统 tick 为节拍,按固定周期分派 ADC 扫描、USB 热插拔检测、SD 卡检测、充电管理、UAC 同步等任务,避免为每个外设单独建任务带来的 RAM 开销。
flowchart TD
subgraph sg_Boot["启动链 (ROM/Boot)"]
PowerOn["上电复位"] --> MaskROM["MaskROM 引导"]
MaskROM --> AppEntry["_start (app_ld.c ENTRY)"]
end
subgraph sg_App["mbox_flash 应用框架 (app.c)"]
AppEntry --> SysInit["app_system_init()"]
SysInit --> Main["mbox_flash_main()"]
Main -->|"get_up_suc_flag() == true"| UpdateEnd["wdt_close() + while(1)<br/>(升级完成等待断电)"]
Main -->|"读取音量"| VolRestore["sysmem_read_api(SYSMEM_INDEX_VOL)<br/>dac_vol() 恢复音量"]
Main -->|"读取模式"| ModeLoad["sysmem_read_api(SYSMEM_INDEX_SYSMODE)<br/>装载 work_mode"]
ModeLoad --> Loop["while(1) 主循环<br/>clear_all_message() + sysmem_pre_erase_api()"]
Loop --> ModeDispatch["按 work_mode 分发到模式模块"]
end
subgraph sg_Modules["工作模式模块 (app_modules.h)"]
ModeDispatch --> Music["music_play<br/>(音乐)"]
ModeDispatch --> Record["record_mode<br/>(录音)"]
ModeDispatch --> Linein["linein_mode<br/>(AUX)"]
ModeDispatch --> Midi["midi_dec_mode<br/>(MIDI)"]
ModeDispatch --> Loudspk["loudspk_mode<br/>(对箱)"]
ModeDispatch --> Rc["rc_app<br/>(遥控)"]
ModeDispatch --> Radio["rf_radio_app<br/>(RF 收音)"]
end
subgraph sg_Tick["Tick 轮询层 (app.c)"]
Loop --> TickLoop["tick_timer_ram_loop()<br/>LED5X7 扫描 (L2 cache)"]
Loop --> AppTimer["app_timer_loop()<br/>ADC/USB/SD/充电/UAC"]
end
subgraph sg_Layout["链接布局 (app_ld.c)"]
AppEntry -.->|"段指令 .app.text / .app.data"| Flash["app_code (SPI Flash XIP, 32M)"]
TickLoop -.->|"AT(.tick_timer.text.cache.L2)"| CacheRam["cache_ram / ram0 (L1)"]
end
架构说明:上电后由 MaskROM 引导到链接脚本定义的
_start,随后app_system_init()完成系统基础初始化,mbox_flash_main()接管应用生命周期。模式分发后各模式模块通过app_modules.h统一暴露事件处理接口;tick 层与链接布局层分别保证外设轮询的实时性与 XIP 代码的热路径缓存命中。
入口与初始化流程
段指令与 XIP 执行模型
app.c 文件顶部用 5 条 #pragma 把整个应用的代码/数据/常量段重定向到专用段:
#pragma bss_seg(".app.data.bss")
#pragma data_seg(".app.data")
#pragma const_seg(".app.text.const")
#pragma code_seg(".app.text")
#pragma str_literal_override(".app.text.const")
Source: app.c
这些段与链接脚本 app_ld.c 中 app_code 存储区(ORIGIN = _SFC_MEMORY_START_ADDR + 0x100, LENGTH = 32M-0x100)对应,即代码、只读常量直接落在 SPI Flash 地址空间,CPU 取指时由 SFC(SPI Flash Controller)映射访问——这就是"mbox_flash(mbox 产品 + Flash 执行)"名称的由来。bss/data 段则落在 ram0(L1 SRAM,起始 0x3f00000 + MAX_IRQ_ENTRY_NUM * 4 + 0x200)。把只读数据与代码放在 Flash、把可写数据放在 RAM,是 XIP 方案平衡容量(Flash 大)与速度(RAM 快)的基本策略。
mbox_flash_main() 启动序列
mbox_flash_main() 是应用级入口,调用点位于 app_system_init() 之后(app.c 第 278 行 mbox_flash_main();):
void mbox_flash_main(void)
{
log_info("Mbox-Flash App\n");
if (get_up_suc_flag()) {
log_info("----device update end----\n");
wdt_close();
while (1);
}
vm_isr_response_index_register(IRQ_TICKTMR_IDX);//vm擦写flash时响应tick_timer_ram_loop()函数
delay_10ms(50);//等待系统稳定
pa_mute(0);
#if 1//volume memory
u8 vol = 0;
u32 res = sysmem_read_api(SYSMEM_INDEX_VOL, &vol, sizeof(vol));
if ((vol <= 31) && (res == sizeof(vol))) {
dac_vol(0, vol);
log_info("powerup set vol : %d\n", vol);
}
#endif
sysmem_read_api(SYSMEM_INDEX_SYSMODE, &work_mode, sizeof(work_mode));
/* work_mode = MUSIC_MODE; */
/* work_mode = RECORD_MODE; */
...
while (1) {
clear_all_message();
//切换模式前做预擦除动作
sysmem_pre_erase_api();
Source: app.c
启动序列分五个阶段,设计意图如下:
- 升级完成守卫:
get_up_sup_flag()检测到设备升级(OTA/量产烧录)刚结束时,直接wdt_close()并死循环等待断电。此时应用尚未运行,避免在固件更新后立即进入业务逻辑造成数据竞争或异常播放;用户重新上电后标志位已被升级流程清除,才进入正常启动。 - VM 擦写中断响应注册:
vm_isr_response_index_register(IRQ_TICKTMR_IDX)让 VM(虚拟存储)擦写 Flash 期间仍能响应 tick 定时器中断,保证tick_timer_ram_loop()中的 LED 扫描等短任务不因 Flash 忙而丢拍。这是 XIP + VM 同占 Flash 时典型的协调手段。 - 稳定等待与功放静音:
delay_10ms(50)(约 500ms)等待电源/时钟稳定;pa_mute(0)立即静音功放,防止上电爆音(pop noise)。 - 音量记忆恢复:从
SYSMEM_INDEX_VOL读取上次音量(0~31 有效),调用dac_vol(0, vol)恢复 DAC 音量——注意先静音再设音量、最后才取消静音的顺序,避免音量突变。 - 工作模式装载:从
SYSMEM_INDEX_SYSMODE读出work_mode;注释中保留了各模式的手动强制赋值方式,便于调试时固定进入某一模式(如强制RECORD_MODE验证录音链路)。
主循环与模式预擦除
while(1) 是框架的事件分发核心:每次迭代先 clear_all_message() 清空所有待处理消息(防止旧消息污染新模式的初始状态),再调用 sysmem_pre_erase_api() 做 VM 擦写预擦除——把后续模式切换中大概率要用的 Flash 擦除动作提前完成,从而缩短模式切换的阻塞时间。循环体剩余部分(本次读取范围内未展开)负责向当前模式模块投递事件并执行其循环体,模式模块内部自行完成消息消费。
工作模式系统
work_mode 的装载与记忆
work_mode 是文件级全局变量(u8 work_mode;),由 SYSMEM_INDEX_SYSMODE 持久化。框架支持的模式在头文件 app_modules.h/app_config.h 中定义,包括:
| 模式宏 | 含义 | 对应模块 |
|---|---|---|
MUSIC_MODE | 音乐播放(本地/SD) | music_play |
RECORD_MODE | 录音 | record_mode |
AUX_MODE | Linein 音频输入 | linein_mode |
MIDI_DEC_MODE | MIDI 解码播放 | midi_dec_mode |
MIDI_KEYBOARD_MODE | MIDI 键盘模式 | midi_keyboard_mode |
SIMPLE_DEC_MODE | 简单解码模式 | simple_decode |
REMOTECONTROL_MODE | 遥控器模式 | rc_app |
LOUDSPEAKER_MODE | 对箱(TWS 音箱对)模式 | loudspk_mode |
RTC_MODE | 时钟模式 | rtc_mode |
RF_RADIO_MODE | RF 收音模式(RF_RADIO_EN 使能时) | rf_radio_app |
模式模块通过 app_modules.h 统一暴露给框架,每个模块实现自己的消息循环、按键处理和状态机。框架对模式的约束只有两条:入口函数签名一致(可被 work_mode 分发调用)与 退出时写回 SYSMEM_INDEX_SYSMODE(保证下次上电恢复同一模式)。
模式的切换语义
模式切换并非函数调用式跳转,而是"写回存储 + 重启主循环":切换请求被编码为一条系统消息,模式模块处理后更新 work_mode 并写 SYSMEM_INDEX_SYSMODE,随后主循环继续按新 work_mode 分发。clear_all_message() 保证切换瞬间不会有半途消息残留在队列中。这种设计避免了深层次函数调用栈,使每个模式的状态机保持独立、可静态分析,也便于在 Flash 上做 overlay(同一段 Flash 代码区域按模式覆盖加载)。
定时轮询循环
框架维护两个 tick 级循环,均无独立任务、直接在中断/主循环上下文中执行,从而节省任务栈 RAM。
tick_timer_ram_loop() —— LED 扫描
AT(.tick_timer.text.cache.L2)
void tick_timer_ram_loop(void)
{
#if LED_5X7
LED5X7_scan();
#endif
}
Source: app.c
该函数被显式放入 L2 缓存段(AT(.tick_timer.text.cache.L2)),与链接脚本中 *(.*.text.cache.L2) 收集规则对应——LED 5×7 点阵扫描需要严格周期性,放在缓存段保证其代码在 IRAM/缓存中稳定命中,不受 XIP Flash 总线竞争影响。该函数同时被注册为 VM 擦写期间的 tick 响应回调(见上文启动序列第 2 步)。
app_timer_loop() —— 外设分时轮询
void app_timer_loop(void)
{
if ((0 == (tick_cnt % 2))) {
adc_scan();
}
#if (TCFG_PC_ENABLE || TCFG_UDISK_ENABLE)
#if defined (BLE_EN) && (BLE_EN)
#if (0 == (RF_REMOTECONTROL_MODE_EN & TRANS_DATA_SPPLE_EN))
if (0 == (tick_cnt % 25)) {
void usb_hotplug_detect(void *argv);
usb_hotplug_detect(NULL);
}
#endif
#endif
#endif
#if SYS_TIMER_EN
timer_task_scan();
#endif
#if (FLASH_CACHE_ENABLE == 1)
#if TFG_EXT_FLASH_EN
if (get_flash_cache_timer()) {
if (0 == (tick_cnt % 50)) {
bsp_post_event(B_EVENT_100MS);
}
}
#endif
#endif
#if TCFG_PC_ENABLE
static u16 cnt = 0;
cnt++;
if (0 == (cnt % 10)) {
uac_inc_sync();
}
if (cnt >= 500) {
uac_1s_sync();
cnt = 0;
}
#endif
#if TFG_SD_EN
if (0 == (tick_cnt % 100)) {
extern void sd0_dev_detect(void *p);
sd0_dev_detect(NULL);
}
#endif
#if TCFG_CHARGE_ENABLE
charge_timer_handle();
#endif
}
Source: app.c
这是一个典型的分时复用(time-division multiplexing)轮询器,节拍由 tick_cnt 驱动,各外设按不同周期挂接:
| 周期(tick) | 任务 | 使能宏 |
|---|---|---|
| 每 2 tick | adc_scan() ADC 扫描(按键/电池检测) | 恒开 |
| 每 25 tick | usb_hotplug_detect() USB 热插拔 | TCFG_PC_ENABLE || TCFG_UDISK_ENABLE 且 BLE 使能且非 RF 遥控/透传组合 |
| 每 tick | timer_task_scan() 软件定时器 | SYS_TIMER_EN |
| 每 50 tick(缓存定时器触发时) | bsp_post_event(B_EVENT_100MS) 100ms 事件 | FLASH_CACHE_ENABLE + TFG_EXT_FLASH_EN |
| 每 10 次调用 | uac_inc_sync() UAC 增量同步 | TCFG_PC_ENABLE |
| 每 500 次调用 | uac_1s_sync() UAC 1 秒同步 | TCFG_PC_ENABLE |
| 每 100 tick | sd0_dev_detect() SD 卡检测 | TFG_SD_EN |
| 每 tick | charge_timer_handle() 充电状态机 | TCFG_CHARGE_ENABLE |
设计意图:把周期差异大的外设轮询合并进同一个 tick 回调,避免为每个外设创建独立线程。tick_cnt % N 的取模方式天然抗抖(无需额外计时变量),同时保证最紧急的 ADC 采样以最高频率执行。各任务通过编译期宏裁剪,未使能的外设完全不产生代码与执行开销。
核心流程
启动 → 模式分发 → 外设轮询(端到端时序)
sequenceDiagram
participant ROM as MaskROM/Boot
participant LD as 链接脚本 (app_ld.c)
participant APP as app.c
participant SYS as 系统服务 (sysmem/vm/wdt)
participant MOD as 模式模块 (music/record/...)
participant TICK as tick 中断层
ROM->>LD: 跳转 _start (ENTRY)
LD->>APP: 初始化 .data/.bss,进入 app_system_init()
APP->>APP: app_system_init() 系统基础初始化
APP->>APP: mbox_flash_main() 入口
APP->>SYS: get_up_suc_flag() 查询升级标志
alt 升级刚完成
APP->>SYS: wdt_close() + while(1) 等待断电
else 正常启动
APP->>SYS: vm_isr_response_index_register(IRQ_TICKTMR_IDX)
APP->>SYS: delay_10ms(50) 等待稳定
APP->>APP: pa_mute(0) 静音防爆音
APP->>SYS: sysmem_read_api(SYSMEM_INDEX_VOL) 恢复音量
APP->>SYS: sysmem_read_api(SYSMEM_INDEX_SYSMODE) 读取 work_mode
loop 主循环 while(1)
APP->>APP: clear_all_message() 清空消息
APP->>SYS: sysmem_pre_erase_api() 模式切换预擦除
APP->>MOD: 按 work_mode 分发事件
MOD-->>APP: 模式消息处理/状态机推进
TICK->>APP: tick_timer_ram_loop() LED5X7 扫描 (L2 缓存段)
TICK->>APP: app_timer_loop() ADC/USB/SD/充电/UAC 分时轮询
end
end
时序要点:整个应用生命周期只有一个主循环 + 一个 tick 中断回调集合。模式模块不是并行任务,而是被主循环"轮流调用"的协程式状态机;外设轮询则被压缩进 tick 回调,保证 RAM 占用可预测。升级完成场景被单独优先处理,是框架对"烧录后首次上电"这一高风险窗口的显式保护。
模式切换时序
sequenceDiagram
participant KEY as 按键/遥控事件
participant MOD as 当前模式模块
participant APP as mbox_flash 主循环
participant SYS as sysmem (VM 存储)
KEY->>MOD: 模式切换按键事件
MOD->>SYS: 写 SYSMEM_INDEX_SYSMODE = 新模式
MOD-->>APP: 返回/请求退出
APP->>APP: clear_all_message()
APP->>SYS: sysmem_pre_erase_api() 预擦除
APP->>APP: work_mode 已更新,重新分发
APP->>MOD: 进入新模式事件循环
链接脚本与内存布局
app_ld.c 的存储区定义
post_build/bd49/mbox_flash/app_ld.c 定义了 mbox_flash 方案的完整内存地图:
#define _ADDR_RAM0_END 0x3f10000
#define _ADDR_RAM0_START (0x3f00000 + MAX_IRQ_ENTRY_NUM * 4 + 0x200)
#define _ADDR_CACHE_RAM_START 0x3f30000
UPDATA_SIZE = 0x80;
UPDATA_BEG = _ADDR_RAM0_END - UPDATA_SIZE;
ICACHE_RAM_SIZE = 0x4000;
MEMORY
{
#if ICACHE_RAM_TO_RAM_ENABLE
cache_ram : ORIGIN = _ADDR_CACHE_RAM_START + ICACHE_RAM_SIZE - ICACHE_RAM_TO_RAM, LENGTH = ICACHE_RAM_TO_RAM
#endif
app_code(rx) : ORIGIN = _SFC_MEMORY_START_ADDR + 0x100, LENGTH = 32M-0x100
irq_vec(rx) : ORIGIN = _IRQ_MEM_ADDR, LENGTH = MAX_IRQ_ENTRY_NUM * 4
ram0(rw) : ORIGIN = _ADDR_RAM0_START, LENGTH = _ADDR_RAM0_END - _ADDR_RAM0_START - 0x24
boot_ram(rw) : ORIGIN = _ADDR_RAM0_END - 0x24, LENGTH = 0x24
}
ENTRY(_start)
Source: app_ld.c
各存储区的作用与设计意图:
| 存储区 | 属性 | 位置/大小 | 用途 |
|---|---|---|---|
cache_ram | rw | 0x3f30000 起,长度 ICACHE_RAM_TO_RAM(受 ICACHE_RAM_TO_RAM_ENABLE 控制) | 用户数据镜像到缓存 RAM,存放 .usr_data |
app_code | rx | _SFC_MEMORY_START_ADDR + 0x100,长度 32M-0x100 | XIP 应用代码与只读常量;+0x100 偏移避开头部信息,超过 32M 需长跳转(注释注明) |
irq_vec | rx | _IRQ_MEM_ADDR,长度 MAX_IRQ_ENTRY_NUM * 4 | 中断向量表 |
ram0 | rw | 0x3f00000 + MAX_IRQ_ENTRY_NUM*4 + 0x200 至 0x3f10000-0x24 | L1 SRAM:数据、bss、栈、蓝牙协议栈数据 |
boot_ram | rw | 0x3f10000 - 0x24,长度 0x24(36 字节) | 启动信息区 .boot_info,与 Boot 交换参数 |
app_code 长达 32M 的设计说明 SDK 面向大容量 SPI Flash;+0x100 保留给 Flash 头部(升级信息/密钥等)。UPDATA_BEG = _ADDR_RAM0_END - 0x80 在 RAM 顶部预留 128 字节更新参数区,供升级流程在 RAM 中传递数据。
关键 Section 布局
.data(ram0):data_buf_start起,依次收集.data_magic、.data、.common、.ble_app_data,中间内联cache_Lx_code_text_begin/end区(收集*.text.cache.L1/L2/L3与audio_isr_text、log_ut_text),随后 include 蓝牙协议栈数据段(btstack_lib_data.ld、btctler_lib_data.ld)。.ans_data_sec(ram0):集中放置 AEC/NS/NLP/FFT/noisegate 音频算法数据,便于统一管理算法内存。.bss(NOLOAD):cpu0_sstack_begin/end界定 CPU0 栈区(DONGLE 变体额外加 0xc00 栈深度),栈区前后布置.stack_magic/.stack_magic0哨兵用于栈溢出检测;之后是普通 bss 与蓝牙协议栈 bss。
Overlay 条件编译
app_ld.c 按应用变体动态选择 overlay 链接文件:
#if FULL_DUPLEX_RADIO
#include "../post_build/bd49/mbox_flash/app_ld_overlay_fullduplex_radio.c"
#elif RUN_APP_RC
#include "../post_build/bd49/mbox_flash/app_ld_overlay_rc.c"
#elif RUN_APP_DONGLE
#include "../post_build/bd49/mbox_flash/app_ld_overlay_dongle.c"
#else
#include "../post_build/bd49/mbox_flash/app_ld_overlay_custom.c"
#endif
Source: app_ld.c
设计意图:同一套应用代码,通过 overlay 覆盖 Flash 中同一段地址区域来切换不同产品形态(全双工收音、遥控器、Dongle、自定义)。overlay 机制让"一份框架、多种变体"成为可能,变体间共享的代码只保留一份,节省 Flash 空间。
Usage Examples
示例 1:向框架中接入一个新的工作模式
mbox_flash 框架通过 app_modules.h 统一接入模式模块。在 mbox_flash_main() 的模式装载阶段(app.c 第 166 行),框架只读取 SYSMEM_INDEX_SYSMODE 得到 work_mode;模式的具体分发发生在主循环体内。接入新模式需要:在 app_modules.h 声明模块入口、在 app.c 顶部 include 模块头文件(如 #include "loudspk_mode.h")、在模式枚举中定义新模式宏。框架侧无需修改启动序列——这正是"模式即模块"的解耦设计。
示例 2:调试时强制进入指定模式
sysmem_read_api(SYSMEM_INDEX_SYSMODE, &work_mode, sizeof(work_mode));
/* work_mode = MUSIC_MODE; */
/* work_mode = RECORD_MODE; */
/* work_mode = AUX_MODE; */
/* work_mode = MIDI_DEC_MODE; */
/* work_mode = MIDI_KEYBOARD_MODE; */
/* work_mode = SIMPLE_DEC_MODE; */
/* work_mode = REMOTECONTROL_MODE; */
/* work_mode = LOUDSPEAKER_MODE; */
/* work_mode = RTC_MODE; */
/* work_mode = RF_RADIO_MODE; */
Source: app.c
这些注释是框架作者留给调试者的"后门":取消某一行注释即可绕过存储中的模式记忆、强制进入目标模式,用于快速验证单一模式的完整链路(例如验证 MIDI 键盘模式时固定 MIDI_KEYBOARD_MODE),无需反复修改存储或按键操作。
示例 3:在 tick 循环中挂接新的周期任务
#if TFG_SD_EN
if (0 == (tick_cnt % 100)) {
extern void sd0_dev_detect(void *p);
sd0_dev_detect(NULL);
}
#endif
Source: app.c
这是框架外设轮询的标准挂接模板:外部模块以 extern 声明暴露函数,框架在 app_timer_loop() 中以 tick_cnt % N 控制周期。新增外设时照此模式添加一个 #if 使能宏 + 取模判断即可,代价仅是一两条比较指令。
示例 4:将热路径代码放入缓存段
AT(.tick_timer.text.cache.L2)
void tick_timer_ram_loop(void)
{
#if LED_5X7
LED5X7_scan();
#endif
}
Source: app.c
AT(...) 属性把需要确定性执行周期的函数放进 L2 缓存区,配合链接脚本 *(.*.text.cache.L2) 收集规则(app_ld.c),保证 LED 扫描等中断级任务不因 XIP 总线忙而抖动。这是 XIP 方案下"以缓存换实时性"的标准做法。
Configuration Options
框架行为由编译期宏控制,以下为主要配置项(定义于 app_config.h 及模块头文件):
| 宏 | 类型 | 默认/取值 | 说明 |
|---|---|---|---|
work_mode | u8 | 运行时从 SYSMEM_INDEX_SYSMODE 读取 | 当前工作模式:MUSIC_MODE/RECORD_MODE/AUX_MODE/MIDI_DEC_MODE/MIDI_KEYBOARD_MODE/SIMPLE_DEC_MODE/REMOTECONTROL_MODE/LOUDSPEAKER_MODE/RTC_MODE/RF_RADIO_MODE |
LED_5X7 | bool | 0 | 使能 5×7 LED 点阵扫描(tick_timer_ram_loop) |
RF_RADIO_EN | bool | 0 | 使能 RF 收音模块 rf_radio_app |
BLE_EN | bool | 1 | 使能 BLE 协议栈(影响 USB 热插拔检测周期分支) |
TCFG_PC_ENABLE | bool | 0 | 使能 PC(UAC)模式,开启 uac_inc_sync/uac_1s_sync 周期同步 |
TCFG_UDISK_ENABLE | bool | 0 | 使能 U 盘模式,加入 USB 热插拔检测 |
TFG_SD_EN | bool | 0 | 使能 SD 卡检测(每 100 tick) |
TFG_EXT_FLASH_EN | bool | 0 | 使能外部 Flash 缓存定时器(每 50 tick 发 B_EVENT_100MS) |
FLASH_CACHE_ENABLE | bool | 1 | 使能 Flash 缓存机制 |
SYS_TIMER_EN | bool | 0 | 使能软件定时器 timer_task_scan |
TCFG_CHARGE_ENABLE | bool | 0 | 使能充电管理 charge_timer_handle |
KEY_IR_EN | bool | 0 | 使能红外按键(定义 Sys_IRInput/Input_Number) |
FULL_DUPLEX_RADIO | bool | 0 | 链接 overlay:全双工收音变体 |
RUN_APP_RC | bool | 0 | 链接 overlay:遥控器变体 |
RUN_APP_DONGLE | bool | 0 | 链接 overlay:Dongle 变体(栈区 +0xc00) |
ICACHE_RAM_TO_RAM_ENABLE | bool | 0 | 使能 cache_ram 区(.usr_data 镜像) |
MAX_IRQ_ENTRY_NUM | int | 平台默认 | 中断向量条目数,影响 ram0 起始地址 |
注意:
work_mode是持久化配置(存储于 VM 的SYSMEM_INDEX_SYSMODE),其余均为编译期宏。宏之间有关联约束,例如 USB 热插拔检测仅在(TCFG_PC_ENABLE \|\| TCFG_UDISK_ENABLE)且 BLE 使能且(RF_REMOTECONTROL_MODE_EN & TRANS_DATA_SPPLE_EN) == 0时生效,配置组合需与链接 overlay 变体一致。
API Reference
void mbox_flash_main(void)
应用主入口。在 app_system_init() 之后被调用(app.c 第 278 行),完成升级守卫、VM 中断注册、音量/模式恢复后进入主循环。
- 参数:无
- 返回:无(正常路径永不返回,进入
while(1)) - 行为:升级完成标志置位时
wdt_close()并死循环;否则恢复音量与work_mode,在主循环中分发模式事件并驱动外设轮询。
void app_timer_loop(void)
tick 级外设分时轮询回调。以 tick_cnt 为节拍,按周期分派 ADC 扫描、USB 热插拔、软件定时器、Flash 缓存事件、UAC 同步、SD 检测与充电处理。
- 参数:无
- 返回:无
- 调用上下文:tick 中断或主循环低优先级位置;内部使用
static计数变量(UAC),可重入性由调用方保证。
void tick_timer_ram_loop(void)
LED 5×7 点阵扫描回调,被 AT(.tick_timer.text.cache.L2) 放入 L2 缓存段;同时注册为 VM 擦写 Flash 期间的 tick 响应函数(vm_isr_response_index_register(IRQ_TICKTMR_IDX))。
- 参数:无
- 返回:无
系统服务 API(框架调用,非框架定义)
| 函数 | 用途 |
|---|---|
sysmem_read_api(SYSMEM_INDEX_VOL, &vol, sizeof(vol)) | 从 VM 系统存储读取音量(0~31 有效) |
sysmem_read_api(SYSMEM_INDEX_SYSMODE, &work_mode, sizeof(work_mode)) | 读取持久化工作模式 |
sysmem_pre_erase_api() | 模式切换前的 VM 预擦除,缩短切换阻塞时间 |
vm_isr_response_index_register(IRQ_TICKTMR_IDX) | 注册 VM 擦写期间响应的中断索引 |
get_up_suc_flag() | 查询设备升级完成标志 |
dac_vol(0, vol) | 设置 DAC 音量 |
pa_mute(0) | 功放静音控制 |
bsp_post_event(B_EVENT_100MS) | 投递 100ms 周期事件 |
这些 API 的具体实现在系统库/其他模块中,mbox_flash 框架仅以声明方式调用,保持应用层与系统层的接口边界清晰。
故障模式、边界情况与并发
升级完成后的死循环守卫
get_up_suc_flag() 为真时框架进入 wdt_close(); while(1);。这是有意的"停机"而非缺陷:固件升级完成后,Flash 内容刚被改写,立即执行业务代码可能读到不一致的代码/数据;同时升级工具可能仍占用 Flash 总线。该守卫确保升级流程以干净状态结束,用户重新上电后(标志位已被升级流程清除)才正常启动。风险点:若升级标志未正确清除,设备将表现为"上电后无反应"——排查时应优先检查升级流程对标志位的清理逻辑。
VM 擦写与 XIP 的 Flash 总线竞争
mbox_flash 方案中,应用代码与 VM 数据同处 SPI Flash。VM 擦写期间 SFC 总线被独占,XIP 取指会被阻塞。框架的应对有三层:
vm_isr_response_index_register(IRQ_TICKTMR_IDX)——擦写期间仍响应 tick 中断,执行tick_timer_ram_loop()(LED 扫描)这类短任务;sysmem_pre_erase_api()——在模式切换前预擦除,把擦除时间移出对实时性敏感的阶段;- 热路径代码(
AT(.tick_timer.text.cache.L2))放入缓存段,即使 Flash 忙也能从缓存取指。
若产品在 VM 擦写期间出现 LED 闪烁异常或音频卡顿,应检查以上三层的配置是否完整。
并发模型:单循环 + tick 回调
框架没有多线程,并发维度只有两个:主循环上下文与 tick 中断上下文。共享状态(work_mode、tick_cnt、Sys_IRInput/Input_Number)的读写需要遵循"主循环写、tick 读"或反之的单向约定,避免中断与主循环同时修改同一变量。tick_cnt % N 的轮询判断天然原子(单字节读),但若在轮询回调中访问多字节共享结构,仍需自行保证原子性。UAC 同步使用函数内 static 计数器,说明框架允许轮询回调持有自己的私有状态——这是"无任务"模型的 RAM 节省代价:每个回调都要自行管理状态生命周期。
边界条件
- 音量越界:
sysmem_read_api读回的音量只有在vol <= 31 && res == sizeof(vol)时才应用,损坏/越界的存储值被安全忽略(不写入 DAC),防止音量寄存器越界。 - 栈溢出哨兵:链接脚本在 CPU0 栈区前后布置
.stack_magic/.stack_magic0(app_ld.c),栈越界会先破坏哨兵,便于运行时检测。Dongle 变体额外 +0xc00 栈深度,说明不同变体对栈的需求差异由链接脚本显式管理。 - 代码超过 32M:链接脚本注释明确"超过 32M 运行代码需要使用长跳转",
app_code容量上限 32M-0x100 是 XIP 线性寻址边界。
性能与运维
性能特征
- XIP 取指 vs IRAM:绝大多数代码从 SPI Flash 取指,速度低于 IRAM;框架用两级手段缓解——
L1/L2/L3缓存段收集热路径函数(.text.cache.L1/L2/L3、audio_isr_text),以及将音频算法数据集中到.ans_data_sec便于 cache 友好访问。 - 轮询开销:
app_timer_loop()每个 tick 的固定开销为几次取模比较(<1μs 级),即使所有外设使能也可忽略;未使能的模块完全不参与编译(#if裁剪)。 - 启动时间:
delay_10ms(50)固定 500ms 稳定等待,属于有意取舍(防止上电抖动)而非可优化项;模式切换的耗时大头在 VM 预擦除,已通过sysmem_pre_erase_api()前置化。
运维与调试
- 强制模式:临时取消
work_mode赋值注释(app.c)可固定进入目标模式,是定位"某模式下才出现"问题的首选手段。 - 日志:
LOG_TAG "[mbox_app]"(app.c)标识框架日志;启动关键路径有Mbox-Flash App、----device update end----、powerup set vol : %d三条日志,可用于判断启动阶段。 - 构建/烧录:工程文件
sdk/AW30N_mbox_flash.cbp(Code::Blocks 工程)与烧录脚本post_build/bd49/mbox_flash/download_bat.c(输出 "BD49 mbox_flash" 烧录信息)构成该方案的构建-下载链路。
Extension Points
- 新增工作模式:在模式枚举中定义新模式宏 → 在
app_modules.h声明模块接口 → 在app.cinclude 模块头文件 → 在主循环分发处添加分支。框架对模式的唯一契约是"事件循环 + 退出时写回SYSMEM_INDEX_SYSMODE"。 - 新增周期外设:照
TFG_SD_EN模板在app_timer_loop()中增加#if宏 +tick_cnt % N分支即可;周期选择需考虑与现有任务错峰(如 100ms 事件与 SD 检测的相位)。 - 新增产品变体:通过
app_ld_overlay_*.c链接 overlay 机制(app_ld.c),在app_ld.c中增加#elif分支并编写对应 overlay 段文件,即可复用同一应用代码生成不同 Flash 布局的固件。 - 热路径优化:对需要确定执行周期的新中断回调,使用
AT(.xxx.text.cache.L2)属性放入缓存段,并同步在app_ld.c的 cache 收集区添加对应通配符。
Tests
本次读取的源文件(app.c、app_ld.c)未包含单元测试代码;mbox_flash 框架的验证主要依赖硬件在环测试:模式切换回归(各 work_mode 装载/退出)、升级流程(烧录后首启守卫)、VM 擦写期间的 tick 响应、以及各外设使能组合下的轮询稳定性。测试用例一般通过"强制模式 + 日志断言"(Mbox-Flash App/powerup set vol)在真机上进行。SDK 根目录 补丁包/ 目录下的裁剪/升级补丁包(如 AW30N_v1.2.0_SDK裁剪到256KB以内的补丁包)可视为对该框架的配置级回归验证。
Related Links
- app.c(mbox_flash 应用入口) —— 本文档核心源文件:主入口、模式装载、tick 轮询
- app_ld.c(mbox_flash 链接脚本) —— 内存布局、overlay 变体选择
- AW30N_mbox_flash.cbp(Code::Blocks 工程) —— 该方案的构建工程
- download_bat.c(烧录脚本) —— "BD49 mbox_flash" 烧录流程
- 相关系统能力(独立页面):VM 存储与
vm_sfc、sysmem系统存储、BLE 协议栈(bt_ble)、USB 协议栈、音乐播放(music_play)、录音(record_mode)