杰理 SDK 文档中心
首页
首页
  • 项目概览与快速开始

    • 项目概述与芯片支持
    • 环境搭建与工具链
    • 工程与构建系统
    • 烧录与升级工具
    • 文档与硬件资料
  • 系统架构与芯片平台

    • 芯片平台与启动流程
    • 预编译库与头文件体系
    • 消息、定时器与中断服务
    • 通用外设驱动
  • 存储与文件系统

    • 文件系统实现
    • 存储设备驱动
    • VM 参数存储系统
  • 音频处理

    • 音频解码器
    • 音频编码器
    • MIDI 合成与播放
    • 音效、变速变调与降噪
  • 语音玩具应用

    • 应用框架与状态机
    • 音乐播放与外部音源
    • MIDI 乐器模式
    • 录音应用
    • 待机、电源管理与 USB 从机
  • 小音箱应用

    • 应用框架与模式管理
    • 播放源:音乐、FM、录音与 LineIn
  • 应用层与示例工程

    • 通用 MCU 应用
  • 固件更新与补丁

    • 固件升级机制
    • AD14N 主动降噪补丁

音乐播放与外部音源

本文档介绍 AD15N voice-toy 应用中「音乐播放」(TOY_MUSIC / toy_music_app)与「外部音源」(TOY_LINEIN / toy_linein_app)两个工作模式:它们如何通过统一的工作模式状态机被调度、如何接入系统级的音频解码与 Line-in 检测服务,以及相关的编译开关、音量持久化和错误处理机制。

Purpose and Scope

本页面聚焦 voice-toy 应用中与「音乐播放与外部音源」直接相关的能力:

  • 工作模式状态机:toy_main.c 中的 work_mode 切换与主循环调度;
  • 音乐播放模式:TOY_MUSIC(由 SIMPLE_DEC_EN 控制)及其依赖的 music/music_play.h 解码播放服务;
  • 外部音源模式:TOY_LINEIN(由 LINEIN_MODE_EN 控制)及其依赖的 line_in/line_in_mode.h 音源检测/切换服务;
  • 系统级集成:mbox_main.c 中的 50ms Line-in 插入检测轮询、SDX_SUSPEND_EVENT_LINEIN 挂起事件,以及 fm_radio.c 中与收音机模块共用的 Line-in 初始化。

以下主题属于同目录下的兄弟页面,不在本页展开:MIDI 播放(TOY_MIDI / TOY_MIDI_KEYBOARD)、扬声器(TOY_SPEAKER / toy_speaker_app)、录音(TOY_RECORD / toy_record_app)、USB 从机(TOY_USB_SLAVE)与待机(TOY_IDLE / TOY_SOFTOFF)。

Overview

在 voice-toy(语音玩具)应用中,固件启动后进入一个永不退出的主循环:每个循环周期先清空消息队列、预擦除 VM 存储,然后根据全局变量 work_mode 分发到对应的模式应用函数。TOY_MUSIC 与 TOY_LINEIN 就是其中两个可编译选择的工作模式:

  • TOY_MUSIC(音乐播放):玩具以本地解码方式播放音频,依赖 music/music_play.h 提供的播放服务,是默认工作模式(work_mode = TOY_MUSIC;)。
  • TOY_LINEIN(外部音源):玩具将 AUX/Line-in 输入作为音源,依赖 line_in/line_in_mode.h 进行音源检测与模式切换;在系统层(mbox)还由 line_in_det() 以约 50ms 周期轮询外部音源插入状态。

这两个模式并非孤立存在,而是通过编译宏(SIMPLE_DEC_EN、LINEIN_MODE_EN、LINEIN_EN)条件编译,与系统层音频服务(music_play、line_in_mode)以及硬件外设(DAC 输出、ADC 采样)协同工作。设计意图是:用一个可扩展的模式状态机把"玩具的多种玩法"(音乐、外音、录音、MIDI 等)解耦为独立的应用函数,每个模式由编译宏按产品配置裁剪,主循环只负责调度,从而兼顾代码复用与固件体积控制。

Architecture

下图展示了「音乐播放与外部音源」在 voice-toy 应用中的分层架构与依赖关系:

flowchart TD
    subgraph sg_App["应用层 voice_toy"]
        ToyMain["toy_main.c 主循环<br/>work_mode 状态机"]
        ToyMusic["toy_music_app() 音乐播放"]
        ToyLinein["toy_linein_app() 外部音源"]
    end

    subgraph sg_Svc["系统服务层"]
        MusicPlay["music/music_play.h<br/>解码播放服务"]
        LineInMode["line_in/line_in_mode.h<br/>音源模式切换"]
        LineDet["mbox_main.c<br/>line_in_det() 50ms 轮询"]
    end

    subgraph sg_HW["硬件层"]
        DAC["DAC 音频输出"]
        ADC["AUX/Line-in 采样输入"]
    end

    ToyMain -->|"case TOY_MUSIC<br/>SIMPLE_DEC_EN"| ToyMusic
    ToyMain -->|"case TOY_LINEIN<br/>LINEIN_MODE_EN"| ToyLinein
    ToyMusic --> MusicPlay
    ToyLinein --> LineInMode
    LineDet -->|"LINEIN_EN 保护"| LineInMode
    MusicPlay --> DAC
    LineInMode --> ADC

架构说明:

  • 应用层:toy_main.c 是唯一入口,通过 switch (work_mode) 把控制权交给 toy_music_app() 或 toy_linein_app()。模式函数返回后主循环重新调度,因此每个模式函数内部通常是"事件循环 + 消息处理"结构。
  • 系统服务层:music/music_play.h 是跨应用复用的播放服务接口(mbox_main.c、fm_radio.c 也包含它);line_in/line_in_mode.h 提供外部音源模式支持,且受 LINEIN_EN 宏保护。mbox_main.c 的系统任务中周期性调用 line_in_det() 完成音源插入检测,并通过挂起事件 SDX_SUSPEND_EVENT_LINEIN 与低功耗流程联动。
  • 硬件层:播放路径最终驱动 DAC 输出,外部音源路径通过 ADC 采样 AUX 输入;fm_radio.c 的 fm_linein_init() 表明收音机与 Line-in 共享部分初始化资源。

说明:toy_music_app() / toy_linein_app() 的具体函数体(消息处理细节)位于 voice_toy 目录下的对应实现文件中;在本次源码调研范围内未读取到其函数体实现,下文将以已读到的调度代码、编译宏与系统级集成点为准进行说明。

主内容:工作模式状态机与调度机制

工作模式定义与编译裁剪

toy_main.c 通过一组编译宏决定哪些模式被编译进固件。模式常量(TOY_MUSIC、TOY_LINEIN 等)与模式应用函数一一对应,work_mode 变量在启动时被显式赋值为 TOY_MUSIC,其余模式以注释形式保留作为可选配置:

    work_mode = TOY_MUSIC;
    /* work_mode = TOY_MIDI; */
    /* work_mode = TOY_MIDI_KEYBOARD; */
    /* work_mode = TOY_SPEAKER; */
    /* work_mode = TOY_LINEIN; */
    /* work_mode = TOY_RECORD; */
    /* work_mode = TOY_IDLE; */

Source: toy_main.c

设计意图:把"产品玩法"声明为编译期配置而不是运行时配置。玩具厂商可以在不修改调度逻辑的前提下,通过注释/宏选择固件内置的玩法组合,同时借助条件编译让未启用的模式完全不占用代码空间。

主循环与模式分发

主循环是 while (1) 形式的超级循环(super-loop),每个周期做三件事:清空消息、预擦除 VM、分发模式。分发逻辑如下(节选):

    while (1) {
        clear_all_message();
        vm_pre_erase();
        switch (work_mode) {
#if SIMPLE_DEC_EN
        case TOY_MUSIC:
            toy_music_app();
            break;
#endif
...
#if defined(LINEIN_MODE_EN) && (LINEIN_MODE_EN)
        case TOY_LINEIN:
            toy_linein_app();
            break;
#endif
...
        case TOY_IDLE:
            toy_idle_app();
            break;
        case TOY_SOFTOFF:
            toy_softoff();
            break;
        default:
            work_mode++;
            if (work_mode >= MAX_MODE) {
                work_mode = TOY_MUSIC;
            }
            break;
        }
    }

Source: toy_main.c

这段代码揭示了三个关键机制:

  1. 条件编译保护:TOY_MUSIC 分支由 SIMPLE_DEC_EN 宏保护,TOY_LINEIN 分支由 defined(LINEIN_MODE_EN) && (LINEIN_MODE_EN) 双条件保护(先检查宏是否定义,再检查其值是否为真)。
  2. 默认回退:default 分支执行 work_mode++,超出 MAX_MODE 后回绕到 TOY_MUSIC。这意味着即使某个模式宏被裁剪掉,状态机也能自动跳到下一个合法模式,不会死循环在无效值上——这是嵌入式状态机常见的"兜底"设计。
  3. 模式函数阻塞语义:toy_music_app() / toy_linein_app() 返回后才会进入下一轮循环,因此每个模式函数内部必须自行处理事件驱动(消息循环),主循环本身不提供抢占式调度。

音量持久化与启动恢复

启动阶段(主循环之前)从 VM(虚拟存储/掉电保存存储)读取用户音量并写入 DAC:

    u8 vol = 0;
    u32 res = vm_read(VM_INDEX_VOL, &vol, sizeof(vol));
    if ((vol <= 31) && (res == sizeof(vol))) {
        dac_vol(0, vol);
        log_info("powerup set vol : %d\n", vol);
    }

Source: toy_main.c

要点:

  • VM_INDEX_VOL 是 VM 存储中的音量索引;读取结果 res 必须等于 sizeof(vol) 才认为有效,这是 VM 读取的常规校验方式。
  • 校验 vol <= 31 限制了音量合法范围(0~31),防止越界值写入 DAC。
  • dac_vol(0, vol) 将音量应用到 DAC 通道 0,log_info 输出上电音量日志便于调试。

外部音源:系统级检测与集成

音乐/外音模式在系统层(mbox)还有独立的集成点。mbox_main.c 的系统任务以 50ms 为周期调用 line_in_det() 检测外部音源:

    if (0 == (cnt % 25)) { //50ms
#ifdef LINEIN_EN
        line_in_det();
#endif

Source: mbox_main.c

这里的 cnt % 25 配合系统节拍(注释标注 50ms,即每 25 个 2ms 节拍一次)形成软件定时器;LINEIN_EN 宏决定是否编译 Line-in 检测逻辑。另外,mbox_main.c 还处理与 Line-in 相关的低功耗挂起事件:

    if (event & BIT(SDX_SUSPEND_EVENT_LINEIN)) {
        void line_in_det1(void);

Source: mbox_main.c

这表示 Line-in 检测还参与 SDX(掉电/挂起)事件仲裁:当 SDX_SUSPEND_EVENT_LINEIN 位置位时,会调用 line_in_det1() 做挂起前的补充检测,避免在外部音源仍在播放时误入低功耗。此外 mbox_main.c 在 LINEIN_EN 保护下支持 AUX_MODE 模式分支(见 mbox_main.c),说明系统消息层对 AUX/Line-in 音源也有模式化处理。

与收音机模块的共用初始化

fm_radio.c 同时包含 music/music_play.h 与 line_in/line_in_mode.h,并提供 fm_linein_init():

#include "music/music_play.h"
...
#ifdef LINEIN_EN
#include "line_in/line_in_mode.h"
...
int fm_linein_init(void)

Source: fm_radio.c

设计意图:收音机(FM)与外部音源(Line-in)在硬件上共享模拟音频通路(输入选择、增益配置),因此 fm_linein_init() 把两者的初始化合并,避免重复配置 ADC/增益寄存器;LINEIN_EN 宏再次作为裁剪开关,未启用外音的产品不会引入 line_in_mode.h 的依赖。

Core Flow:从启动到外部音源播放的完整流程

sequenceDiagram
    participant Boot as 启动代码
    participant Main as toy_main.c 主循环
    participant App as 模式应用函数
    participant Svc as 系统服务层
    participant HW as 硬件外设

    Boot->>Boot: 读取 VM_INDEX_VOL 恢复音量
    Boot->>Main: work_mode = TOY_MUSIC (默认)
    loop 主循环 while(1)
        Main->>Main: clear_all_message() / vm_pre_erase()
        Main->>App: switch(work_mode) 分发
        alt TOY_MUSIC (SIMPLE_DEC_EN)
            App->>Svc: toy_music_app() → music_play 服务
            Svc->>HW: 解码数据 → DAC 输出
        else TOY_LINEIN (LINEIN_MODE_EN)
            App->>Svc: toy_linein_app() → line_in_mode
            Svc->>HW: 采样 AUX 输入 → 播放
            Svc-->>App: 音源插入状态变化
        end
        App-->>Main: 模式函数返回,进入下一轮
    end

执行步骤分解:

  1. 启动音量恢复:toy_main.c 从 VM 读取 VM_INDEX_VOL,校验后调用 dac_vol(0, vol) 设置 DAC 音量(toy_main.c#L80-L86)。
  2. 设置初始模式:work_mode = TOY_MUSIC,默认进入音乐播放。
  3. 主循环调度:每轮先 clear_all_message() 清空遗留消息、vm_pre_erase() 预擦除 VM 页,然后 switch(work_mode) 分发(toy_main.c#L96-L152)。
  4. 模式执行:toy_music_app()(受 SIMPLE_DEC_EN 保护)走本地解码路径;toy_linein_app()(受 LINEIN_MODE_EN 保护)走外部音源路径。两者都依赖系统服务层,但硬件通路不同:DAC 输出 vs. ADC 采样 AUX。
  5. 外部音源并行检测:无论当前处于哪个模式,mbox 系统任务每 50ms 调用一次 line_in_det()(受 LINEIN_EN 保护)感知音源插入(mbox_main.c#L66-L68)。
  6. 低功耗联动:挂起事件 SDX_SUSPEND_EVENT_LINEIN 置位时调用 line_in_det1() 做挂起前检测,避免外音播放中被误挂起(mbox_main.c#L93-L95)。
  7. 模式回绕:若 work_mode 指向未编译的模式,default 分支自增并回绕到 TOY_MUSIC,保证状态机始终收敛到合法模式。

Configuration Options

「音乐播放与外部音源」的行为完全由编译期宏控制,没有运行时配置文件。产品工程师通过修改 toy_main.c 顶部的模式赋值与工程配置中的宏定义来裁剪功能:

宏类型生效范围说明
SIMPLE_DEC_EN编译宏TOY_MUSIC 分支启用简单解码播放(音乐模式);未定义时 case TOY_MUSIC 不编译
LINEIN_MODE_EN编译宏(值判断)TOY_LINEIN 分支启用外部音源模式;需 defined(LINEIN_MODE_EN) && (LINEIN_MODE_EN) 双条件满足
LINEIN_EN编译宏系统层 Line-in 功能启用 line_in_det() 检测、line_in_mode.h 依赖与 AUX_MODE 分支
MAX_MODE常量主循环回绕边界模式枚举上限;work_mode 超过后回绕到 TOY_MUSIC
VM_INDEX_VOLVM 索引音量持久化音量存储索引;有效值范围校验为 vol <= 31
SDX_SUSPEND_EVENT_LINEIN事件位低功耗仲裁Line-in 挂起事件位,置位时执行挂起前音源检测

配置要点:

  • 三个宏的关系是"应用层模式开关 + 系统层功能开关":即使 LINEIN_MODE_EN 打开,若 LINEIN_EN 未开,toy_linein_app() 虽然编译,但系统层不会执行 line_in_det() 检测,音源切换能力受限。
  • TOY_MUSIC 是默认且兜底模式:work_mode 初始化与回绕目标都指向它,产品若只需外音功能,也应保留 SIMPLE_DEC_EN 或确保回绕路径可用,否则 default 分支可能反复自增。

API Reference

以下接口/函数均从已读取的源码中提取,属于本页主题的直接相关入口。toy_music_app() 与 toy_linein_app() 的声明位于 toy_linein.h(被 toy_main.c 包含),其函数体实现在本次调研范围外,故仅列出由调用点确认的签名。

toy_music_app(void)

音乐播放模式应用入口。由主循环在 case TOY_MUSIC 下调用,仅在 SIMPLE_DEC_EN 编译时存在。

  • 返回:void;返回后主循环进入下一轮调度。
  • 调用点:toy_main.c#L101-L103

toy_linein_app(void)

外部音源模式应用入口。由主循环在 case TOY_LINEIN 下调用,仅在 defined(LINEIN_MODE_EN) && (LINEIN_MODE_EN) 编译时存在。

  • 返回:void;返回后主循环进入下一轮调度。
  • 调用点:toy_main.c#L120-L123

line_in_det(void)

外部音源插入检测函数(mbox 系统层)。由系统任务以 50ms 周期调用,仅在 LINEIN_EN 编译时存在。

  • 返回:void
  • 调用点:mbox_main.c#L67-L68

line_in_det1(void)

挂起前的 Line-in 补充检测函数。在 SDX_SUSPEND_EVENT_LINEIN 事件置位时调用,用于低功耗仲裁。

  • 返回:void(前向声明 void line_in_det1(void);)
  • 调用点:mbox_main.c#L94-L95

fm_linein_init(void)

收音机模块的 Line-in 共用初始化函数,合并 FM 与外部音源的模拟通路初始化。

  • 返回:int
  • 定义位置:fm_radio.c#L81

dac_vol(u8 ch, u8 vol)

DAC 音量设置函数(硬件驱动层)。启动时用于恢复用户音量。

  • 参数:ch — DAC 通道(启动流程使用 0);vol — 音量值(合法范围 0~31)。
  • 调用点:toy_main.c#L84

vm_read(u16 index, void *buf, u32 len)

VM 存储读取函数,用于读取 VM_INDEX_VOL 持久化的音量。

  • 返回:实际读取字节数;调用方用返回值与 sizeof(vol) 比较判断有效性。
  • 调用点:toy_main.c#L82-L83

失败模式、边界情况与并发

音量数据损坏

若 vm_read(VM_INDEX_VOL, ...) 返回值不等于 sizeof(vol)(首次上电、VM 页损坏、长度不匹配),启动代码跳过 dac_vol 调用,音量保持硬件默认值。校验 vol <= 31 进一步防止非法值进入 DAC。失败路径为静默降级而非报错,符合嵌入式产品的容错预期(toy_main.c#L82-L86)。

模式宏不一致导致的状态机空转

若 work_mode 指向被裁剪的模式(例如 LINEIN_MODE_EN 关闭但运行时仍赋值 TOY_LINEIN),default 分支执行 work_mode++ 并回绕到 TOY_MUSIC。风险在于:若所有模式宏均关闭,状态机会在 default 分支空转并反复擦写 VM(每轮 vm_pre_erase()),因此产品配置必须至少保留一个可用模式。

事件/检测的并发时序

  • line_in_det() 由系统任务周期性调用,而 toy_linein_app() 运行在应用主循环中,两者属于不同执行上下文。音源插入状态通过共享标志/事件传递,存在经典的生产者-消费者时序:检测到插入到应用层响应之间可能有最多 50ms 延迟。
  • SDX_SUSPEND_EVENT_LINEIN 事件位在挂起仲裁中被检查,若外音正在播放,挂起流程会先执行 line_in_det1() 确认状态,避免"边播放边挂起"导致音频通路异常。

低功耗与外音冲突

外部音源通路(ADC 采样)与系统低功耗(SDX)天然冲突:进入挂起会切断模拟通路。因此 SDX_SUSPEND_EVENT_LINEIN 仲裁是保证"外音播放不被静默打断"的关键边界,产品若支持外音必须保留该事件处理。

性能与运维

  • 检测周期:line_in_det() 每 50ms 一次(cnt % 25),是固定的软件定时器节拍;检测开销小(多为 GPIO/ADC 状态读取),不会显著占用 CPU。若产品需要更快的音源响应,可调整节拍计数,但需同步评估 ADC 采样与低功耗时序。
  • 启动路径:音量恢复(一次 vm_read + 一次 dac_vol)在进入主循环前完成,开销可忽略;主循环每轮 vm_pre_erase() 是 VM 磨损均衡的一部分,频繁模式切换会加速 VM 页擦写,应避免在循环内高频切换 work_mode。
  • 日志:log_info("powerup set vol : %d\n", vol) 提供上电音量日志,便于产测与现场排查音量异常问题(toy_main.c#L85)。

扩展点

  • 新增玩法模式:在 toy_main.c 的 switch (work_mode) 中仿照 TOY_MUSIC / TOY_LINEIN 添加 case TOY_XXX,配套一个 #if defined(XXX_MODE_EN) 编译宏与一个 toy_xxx_app() 模式函数,并确保 MAX_MODE 覆盖新枚举值。该模式即自动获得主循环调度、消息清空与 VM 预擦写服务。
  • 外音源检测策略:替换/扩展 line_in_det() 的检测算法(电平阈值、消抖窗口)时,保持其"50ms 周期 + 事件置位"的对外契约,应用层无需改动。
  • 共用初始化:新增与 Line-in 共用模拟通路的模块(如录音输入),可参照 fm_linein_init() 的模式把初始化集中到一处,用宏裁剪避免重复配置 ADC/增益寄存器。

Related Links

  • toy_main.c(工作模式状态机与主循环)
  • mbox_main.c(Line-in 50ms 检测与挂起事件)
  • fm_radio.c(fm_linein_init 共用初始化)
  • 兄弟页面:MIDI 播放(TOY_MIDI)、扬声器模式(TOY_SPEAKER)、录音模式(TOY_RECORD)、USB 从机(TOY_USB_SLAVE)、待机与软关机(TOY_IDLE / TOY_SOFTOFF)

Usage Examples

以下示例均直接提取自仓库源码,展示如何启用与集成「音乐播放与外部音源」能力。

示例 1:选择工作模式(产品玩法配置)

产品工程师通过修改 toy_main.c 启动处的赋值选择默认玩法。取消 TOY_LINEIN 注释即把玩具默认切到外部音源模式:

    work_mode = TOY_MUSIC;
    /* work_mode = TOY_MIDI; */
    /* work_mode = TOY_MIDI_KEYBOARD; */
    /* work_mode = TOY_SPEAKER; */
    /* work_mode = TOY_LINEIN; */
    /* work_mode = TOY_RECORD; */
    /* work_mode = TOY_IDLE; */

Source: toy_main.c

示例 2:主循环内模式分发(调度契约)

所有模式应用函数都必须遵循"返回后由主循环重新调度"的契约;default 分支保证模式回绕不会死锁:

        switch (work_mode) {
#if SIMPLE_DEC_EN
        case TOY_MUSIC:
            toy_music_app();
            break;
#endif
...
#if defined(LINEIN_MODE_EN) && (LINEIN_MODE_EN)
        case TOY_LINEIN:
            toy_linein_app();
            break;
#endif
        default:
            work_mode++;
            if (work_mode >= MAX_MODE) {
                work_mode = TOY_MUSIC;
            }
            break;
        }

Source: toy_main.c

示例 3:系统层周期检测外部音源

在 mbox 系统任务中加入 50ms 周期检测,并受 LINEIN_EN 宏保护——这是外部音源插入感知的底层入口:

    if (0 == (cnt % 25)) { //50ms
#ifdef LINEIN_EN
        line_in_det();
#endif

Source: mbox_main.c

示例 4:挂起前音源检测(低功耗联动)

在 SDX 挂起仲裁中响应 Line-in 事件,防止外音播放期间进入低功耗:

    if (event & BIT(SDX_SUSPEND_EVENT_LINEIN)) {
        void line_in_det1(void);

Source: mbox_main.c

示例 5:收音机与 Line-in 共用初始化

新增与外部音源共用模拟通路的模块时,参照 fm_linein_init() 集中初始化:

#include "music/music_play.h"
...
#ifdef LINEIN_EN
#include "line_in/line_in_mode.h"
...
int fm_linein_init(void)

Source: fm_radio.c


文档小结:本文档基于 toy_main.c、mbox_main.c、fm_radio.c 的已读源码,系统梳理了 voice-toy 应用中「音乐播放」(TOY_MUSIC)与「外部音源」(TOY_LINEIN)的模式状态机调度、系统级 Line-in 检测与低功耗联动、编译宏配置、关键 API、失败模式与扩展方式。toy_music_app() / toy_linein_app() 的函数体实现位于 voice_toy 目录对应文件中,本次调研未读取到,已如实标注。

Prev
应用框架与状态机
Next
MIDI 乐器模式