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

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

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

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

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

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

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

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

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

消息、定时器与中断服务

消息、定时器与中断服务是 AD15N SDK 事件驱动架构的三大基础系统服务:硬件中断通过 request_irq 注册的中断服务函数(ISR)把底层事件转化为消息,投递到由 msg.c 实现的消息队列;定时器服务产生周期消息(如 MSG_500MS)驱动应用层轮询与状态机;应用任务循环统一出队并分发消息给各业务模块处理。

Purpose and Scope

本页面向系统服务层(architecture/system-services)的读者,完整说明 AD15N SDK 中"消息(Message)、定时器(Timer)、中断服务(Interrupt)"三者如何协作构成事件驱动内核:

  • 消息系统:消息枚举定义(sdk/include_lib/msg/msg.h)与消息队列实现(sdk/app/bsp/common/msg/msg.c),以及应用层消息处理入口(common_msg.c、hot_msg.c)。
  • 定时器服务:周期定时器如何以消息形式(如 MSG_500MS)注入应用任务,以及定时器与消息系统之间的耦合方式。
  • 中断服务:request_irq(irq_index, priority, isr, cpu_id) 中断注册模型,以及 USB、SARADC、外部 GPIO、IIC、红外等驱动中的实际注册示例。

以下主题属于兄弟页面,本页只做交叉引用、不展开:USB 设备/主机协议栈的完整实现(见 usb_config.c 所在驱动文档)、音频编解码与播放链路、电源管理(MSG_LOW_POWER / MSG_POWER_OFF 的消费方)。

Overview

AD15N 是一颗面向音频应用的 MCU(SDK 中可见 sdk/app/bsp/cpu/ch58/ 等芯片目录),其应用层采用典型的事件驱动 + 单任务循环模型,而不是多线程抢占模型。整个系统的运转建立在三个层次上:

  1. 硬件事件层:按键、USB 插拔、ADC 转换完成、IIC 传输结束、红外遥控信号等硬件事件触发芯片中断。
  2. 中断服务层:各驱动模块在初始化时调用 request_irq(IRQ_XXX_IDX, priority, isr, cpu_id) 注册中断服务函数。ISR 中只做最轻量的处理(读状态寄存器、清 pending、记录事件),并把需要业务层处理的内容封装成消息。
  3. 消息/任务层:msg.c 实现的消息队列作为中断上下文与任务上下文的解耦通道。应用主任务循环从队列取出消息,依据 msg.h 中定义的 MSG_* 枚举分发到对应处理函数。

这种"中断只投递、任务才处理"的设计意图非常明确:ISR 运行在不可阻塞的上下文,必须短小;而业务逻辑(状态机切换、文件操作、音频控制)可能耗时且需要上下文,必须放到任务循环中执行。消息队列正是这两个世界之间的安全缓冲区,同时也天然解决了中断与任务的并发互斥问题——中断只写队列、任务只读队列。

此外,系统还依赖一个周期定时器:它周期性(典型 500ms)向任务循环投递 MSG_500MS 消息,让应用层无需自建时钟即可完成超时检测、状态刷新等轮询工作,这是"定时器服务"与"消息服务"融合的关键点。

Architecture

下图展示三大服务在系统分层中的位置与数据流:

flowchart TD
    subgraph sg_HW["硬件层 (CPU/外设)"]
        HW_KEY["按键/GPIO 外部中断"]
        HW_USB["USB 控制器"]
        HW_ADC["SARADC"]
        HW_IIC["硬件 IIC"]
        HW_IR["红外遥控 IRTMR"]
    end

    subgraph sg_ISR["中断服务层 (request_irq 注册)"]
        ISR_EXTI["exti_irq (IRQ_PORT_IDX)"]
        ISR_USB["usb0_g_isr (IRQ_USB_CTRL_IDX)"]
        ISR_ADC["adc_isr (IRQ_SARADC_IDX)"]
        ISR_IIC["hw_iic_isr0/1 (IRQ_IIC0/1_IDX)"]
        ISR_IR["irtmr_ir_isr (IRQ_IRTMR)"]
    end

    subgraph sg_MSG["消息系统层"]
        MSG_DEF["消息定义 msg.h (MSG_* 枚举)"]
        QUEUE["消息队列 msg.c"]
    end

    subgraph sg_APP["应用任务层"]
        TIMER["定时器服务 (周期消息 MSG_500MS)"]
        TASK["App 任务循环"]
        HANDLER["消息处理 common_msg.c / hot_msg.c"]
    end

    HW_KEY --> ISR_EXTI
    HW_USB --> ISR_USB
    HW_ADC --> ISR_ADC
    HW_IIC --> ISR_IIC
    HW_IR --> ISR_IR

    ISR_EXTI -->|"投递消息"| QUEUE
    ISR_USB -->|"投递消息"| QUEUE
    ISR_ADC -->|"投递消息"| QUEUE
    ISR_IIC -->|"投递消息"| QUEUE
    ISR_IR -->|"投递消息"| QUEUE

    MSG_DEF --> QUEUE
    TIMER -->|"MSG_500MS"| QUEUE
    QUEUE -->|"出队"| TASK
    TASK -->|"分发"| HANDLER

分层说明:

  • 中断服务层是硬件与软件的分界:每个驱动通过 request_irq 把芯片的中断向量(IRQ_*_IDX)绑定到自己的 ISR。ISR 中不做业务处理,只做状态收集与消息投递,保证中断延迟可控。
  • 消息系统层是解耦核心:msg.h 用枚举集中定义全系统的消息 ID(从 MSG_0 到音乐、录音、MIDI、设备、FM 等数百个),msg.c 提供队列的初始化、投递、取出与分发机制。
  • 应用任务层是唯一允许运行"重逻辑"的地方:任务循环消费队列中的消息,common_msg.c(voice_toy 方案)与 hot_msg.c(mbox_mg 方案)分别展示了不同产品方案如何挂接自己的消息处理函数。
  • 定时器服务横跨系统层与应用层:它不直接调用业务代码,而是向队列投递周期消息,使业务层在统一的任务上下文中响应时间事件,避免了在 ISR/定时器回调里执行耗时操作的风险。

消息系统:事件驱动内核的枢纽

消息 ID 的集中定义(msg.h)

全系统所有消息 ID 在 sdk/include_lib/msg/msg.h 中以枚举集中定义。这是一个单文件消息字典——新增业务消息时在此追加枚举值即可,任务循环和调试工具都能从中获得全局一致的消息语义。以下节选展示消息的分类组织方式:

enum {
//SYS_MSG_START_LINE
    MSG_0 = 0,
    MSG_1,
    ...
    MSG_RECODE_START,
    MSG_RECODE_END,
    ///APP
    MSG_500MS,
    MSG_APP_SWITCH_ACTIVE,
    MSG_LOW_POWER,
    MSG_ENTER_IDLE,
    MSG_POWER_OFF,
    ...
    ///音乐操作相关消息
    MSG_PP,
    MSG_NEXT_FILE,
    MSG_PREV_FILE,
    MSG_NEXT_DIR,
    ...
    //mbox msg
    MSG_CHANGE_WORK_MODE = 0x600,
    ...
    MSG_KEY_CHANGE,
    ...

Source: msg.h

枚举的布局体现了设计意图:

分区典型消息说明
系统基础消息(MSG_0 起)MSG_RECODE_START / MSG_RECODE_END录音等系统级事件
APP 服务消息MSG_500MS、MSG_LOW_POWER、MSG_ENTER_IDLE、MSG_POWER_OFF定时器与电源管理等系统服务注入的周期/状态消息
音乐操作MSG_PP、MSG_NEXT_FILE、MSG_VOL_UP、MSG_A_PLAY播放控制,通常由按键消息经状态机转换而来
录音相关MSG_REC_MODE_SWITCH、MSG_REC_SPEED_EN录音模式切换
MIDIMSG_MIDI_MODE_SWITCH、MSG_MIDICTRL_NOTE_ON_*电子琴/MIDI 控制器按键语义
设备/工作模式MSG_NEXT_MODE、MSG_NEXT_DEV模式与设备切换
mbox 消息(0x600 起)MSG_CHANGE_WORK_MODE、MSG_MUSIC_PLAY_NEW_FILE多盒子/多任务协作场景(mbox_mg 方案)
FM 收音MSG_FM_NEXT_STATION、MSG_FM_SCAN_ALLFM 调谐控制

注意 MSG_CHANGE_WORK_MODE = 0x600 显式指定了数值,说明 mbox 段是预留的地址空间:它可能与芯片/DSP 侧或其他核的消息编号保持对齐,避免枚举重排导致跨模块编号冲突。这是嵌入式系统里常见的"消息号即协议号"实践——消息 ID 不只是内部枚举,还是核间/模块间的通信协议字段。

消息队列实现(msg.c)

队列的实体实现位于 sdk/app/bsp/common/msg/msg.c,作为 BSP 公共组件被所有方案复用。其核心职责是:

  1. 缓冲:为中断上下文投递的消息提供有界缓冲区,避免 ISR 直接调用业务代码;
  2. 解耦:隔离"产生消息的模块"(驱动、定时器)与"消费消息的模块"(应用任务、业务状态机);
  3. 串行化:所有消息在单一任务循环中顺序处理,天然避免了业务数据的并发竞争。

典型消息体包含消息 ID、参数与指针字段(id / arg / ptr 结构),驱动在 ISR 中只填必要字段即投递,应用层取出后按 ID 分发。由于队列消费端是单任务,业务代码无需加锁——这是该架构相比多线程方案最大的简化点。

应用层的消息消费模式

SDK 中不同产品方案展示了两种挂接方式:

  • voice_toy 方案:sdk/app/src/voice_toy/common/common_msg.c 提供公共消息处理,适合以语音玩具为典型形态的产品;
  • mbox_mg 方案:sdk/app/src/mbox_mg/common/hot_msg.c(及 hot_msg.h)处理热插拔/多设备场景下的设备与音乐消息,例如 MSG_MUSIC_NEW_DEVICE_IN、MSG_MUSIC_SELECT_NEW_DEVICE 等由 USB/SD 热插拔中断触发、最终驱动音乐播放状态机的消息。

应用层处理函数通过 switch/case 按 MSG_* 枚举分发,配合各自方案的状态机(如播放/暂停/切歌状态)完成业务动作。新的业务消息只需要:在 msg.h 增加枚举 → 在合适的事件源(按键、中断、定时器)投递 → 在 common_msg.c/hot_msg.c 或本方案的消息处理函数中增加 case 分支。

消息流转时序

sequenceDiagram
    participant HW as 硬件外设
    participant ISR as 中断服务 (request_irq 注册)
    participant Q as 消息队列 (msg.c)
    participant T as App 任务循环
    participant H as 消息处理函数 (common_msg/hot_msg)

    HW->>ISR: 触发中断
    ISR->>ISR: 读状态寄存器/清 pending
    ISR->>Q: 投递消息 (id/arg/ptr)
    Q->>T: 出队
    T->>H: 按 MSG_* 分发
    H-->>T: 业务处理完成/触发新消息
    T-->>Q: 循环取下一条

该时序揭示了一个关键设计点:ISR 到队列的箭头是"写"、任务到队列的箭头是"读",读写方向固定,配合队列自身的原子性,中断与任务之间无需互斥锁即可安全通信。若 ISR 需要给任务传大量数据,通常通过消息携带指针(ptr)指向驱动预先分配的缓冲区,任务在使用完后再释放,避免在 ISR 中做内存分配。

定时器服务:把时间变成消息

定时器与消息的融合

在 AD15N SDK 中,定时器服务并不是一个独立于消息系统的"调度器",而是以消息为载体的时间服务。最直接的证据就是 msg.h 中的系统级周期消息:

  • MSG_500MS —— 每 500ms 由系统定时器注入应用任务的消息。

应用层只要在消息处理函数中响应 MSG_500MS,就获得了一个稳定的"心跳",可用于:界面/指示灯刷新、超时判断、自动关机倒计时、录音时长统计等。其设计意图在于:应用层永远不需要自己读硬件定时器计数值,也不需要编写定时器回调——它只需要在任务循环里等待一个消息,与处理按键消息、USB 消息的方式完全一致。这保证了所有业务逻辑运行在同一上下文,不存在"回调上下文与任务上下文"的分裂。

定时器的分层工作方式

flowchart LR
    TICK["硬件定时器 tick (毫秒级)"]
    PERIOD["周期定时器 (500ms/自定义周期)"]
    ONESHOT["单次定时器 (超时通知)"]
    QUEUE["消息队列 msg.c"]
    TASK["App 任务循环"]

    TICK -->|"计数"| PERIOD
    TICK -->|"计数"| ONESHOT
    PERIOD -->|"周期到 → MSG_500MS"| QUEUE
    ONESHOT -->|"超时 → 事件消息"| QUEUE
    QUEUE --> TASK
  • 硬件 tick:芯片定时器产生固定周期的 tick 中断,作为所有软件定时器的时间基准。
  • 周期定时器:维护一个周期列表,到期时向队列投递对应周期消息(MSG_500MS 即此机制),而不是直接调用回调函数。
  • 单次定时器:用于超时检测(如等待设备就绪、等待串口应答),到期同样以消息形式通知任务层。

说明:本仓库中定时器服务(如 sys_timer/os_timer 相关实现)的具体源文件未在本次文档的源文件发现预算内枚举到,以上分层模型是根据 MSG_500MS 消息定义、任务循环架构与 SDK 通用设计推导出的行为模型;如需精确的定时器 API 签名与链表实现细节,请直接查阅 sdk/app/bsp/common/ 目录下 timer 相关源文件。

与电源/空闲管理的联动

msg.h 中与定时器强相关的还有一组 APP 服务消息:MSG_APP_SWITCH_ACTIVE(应用切换到活跃)、MSG_LOW_POWER(低功耗)、MSG_ENTER_IDLE(进入空闲)、MSG_POWER_OFF(关机)。这些消息的典型来源就是定时器驱动的状态监测:例如长时间无用户操作时,任务层依靠 MSG_500MS 计数触发空闲判定,进而投递 MSG_ENTER_IDLE/MSG_LOW_POWER 让系统进入低功耗;按键或外部中断唤醒后投递 MSG_APP_SWITCH_ACTIVE 恢复工作。

这一设计让功耗管理也统一在消息框架内:电源状态机只在任务上下文迁移,ISR 从不直接切电源状态,从而避免在中断里执行耗时的时钟/外设切换操作。

中断服务:事件的最初入口

中断注册模型:request_irq

SDK 提供统一的软中断注册接口,驱动模块在初始化时把自己的 ISR 绑定到芯片中断向量:

void usb_g_init(void)
{
    ...
    request_irq(IRQ_USB_CTRL_IDX, priority, usb0_g_isr, cpu_id);
    ...
}

Source: usb_config.c

request_irq 的四个参数构成了 SDK 的中断管理契约:

参数含义示例
irq_index芯片中断源编号(IRQ_*_IDX)IRQ_USB_CTRL_IDX、IRQ_SARADC_IDX
priority中断优先级(0~3,越小越高)IRQ_ADC_IP、EXTI_IRQ_PRIORITY
isr中断服务函数指针usb0_g_isr、adc_isr、exti_irq
cpu_id绑定到的 CPU/核单核为 0

各驱动的实际注册示例

同一注册模型被所有外设驱动复用,以下是从各驱动源文件提取的真实调用:

SARADC 模数转换中断(转换完成才读结果,避免轮询):

request_irq(IRQ_SARADC_IDX, IRQ_ADC_IP, adc_isr, 0);

Source: adc_drv.c

外部 GPIO/按键中断(端口中断统一入口 exti_irq,优先级 3 为最低,适合按键这种非实时事件):

request_irq(IRQ_PORT_IDX, EXTI_IRQ_PRIORITY, exti_irq, 0);//中断优先级3

Source: external_interrupt.c

硬件 IIC 传输中断(两个 IIC 控制器各自绑定 ISR,注释明确标注优先级 3):

if (iic == HW_IIC_1) {
    request_irq(IRQ_IIC1_IDX, HW_IIC1_IRQ_PRIORITY, hw_iic_isr1, 0);//3: 中断优先级
} else {
    request_irq(IRQ_IIC0_IDX, HW_IIC0_IRQ_PRIORITY, hw_iic_isr0, 0);//3: 中断优先级
}

Source: iic_hw.c

红外遥控接收中断(IRTMR 定时器/滤波单元产生中断,边沿触发):

request_irq(IRQ_IRTMR, IRQ_IRTMR_IP, irtmr_ir_isr, 0);

Source: irflt.c

USB 主机控制器中断(按 USB 控制器序号分别注册,支持双口):

if (usb_id == 0) {
    request_irq(IRQ_USB_CTRL_IDX, priority, usb0_h_isr, cpu_id);
#if USB_MAX_HW_NUM > 1
} else if (usb_id == 1) {
    request_irq(IRQ_USB1_CTRL_IDX, priority, usb1_h_isr, cpu_id);
#endif

Source: usb_host_config.c

这些示例共同印证了同一套规则:驱动只负责在初始化时注册 ISR,ISR 内部完成状态读取与消息投递。例如 USB 的插拔/传输完成事件最终会转化为 MSG_MUSIC_NEW_DEVICE_IN、MSG_MUSIC_PLAY_NEW_FILE 等 mbox 消息进入音乐播放状态机;红外按键解码后转化为 MSG_FM_*、MSG_PP 等控制消息。

中断优先级与实时性

从示例可见优先级通过宏(IRQ_ADC_IP、EXTI_IRQ_PRIORITY、HW_IIC1_IRQ_PRIORITY、IRQ_IRTMR_IP)配置:USB/SARADC 等需要及时响应的外设通常配置较高优先级,按键/GPIO 外部中断配置为最低优先级 3。这样即使按键在 ISR 里做了少量消抖/解码工作,也不会阻塞 ADC 采样或 USB 传输的实时性。中断服务函数命名统一为 xxx_isr,便于在中断向量表与调试器中识别。

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

ISR 内执行重操作(最典型的错误)

如果 ISR 中直接调用耗时业务(如文件 I/O、长时间循环、阻塞等待),会拉高中断延迟并可能造成数据丢失(USB FIFO 溢出、ADC 结果被覆盖)。架构约束为:ISR 只允许读寄存器、清 pending、投递消息,一切重逻辑放入任务循环。新增驱动时若发现 ISR 中需要"等一会儿"的逻辑,应改为投递消息或启动单次定时器。

消息队列溢出

中断速率高于任务消费速率时(例如高频 ADC/红外信号洪泛),消息队列可能写满。此时应有"丢弃策略"(丢弃新消息或丢弃旧消息)并记录计数,同时排查是否有 ISR 在短期内投递过多消息。业务上表现为消息丢失,可通过增加队列深度或降低投递频率缓解。

消息 ID 冲突

msg.h 是全局唯一字典,新增消息必须在枚举末尾(或在 mbox 段 0x600 之后的预留区内)追加,禁止在已有枚举中间插入——那会改变后续所有消息的数值,破坏与 DSP/其他核或已烧录固件的协议兼容性。mbox 段显式 = 0x600 起始正是为了给跨模块消息预留稳定地址空间。

并发模型:单任务串行化的收益与边界

本架构的核心并发保证是"所有业务消息由唯一任务循环串行处理",因此业务代码之间天然互斥。但有两个边界必须遵守:

  1. ISR 与任务共享的数据(如驱动状态标志、环形缓冲区读写指针)仍需保证原子性或临界区保护——消息队列只保护"投递/取出"本身,不保护消息所指向的缓冲区内容。
  2. 多核/多任务方案(如 mbox_mg 中跨核消息 MSG_CHANGE_WORK_MODE)属于跨模块协议,消息 ID 与数据布局必须按协议定义,不能依赖单核单任务的简化假设。

优先级反转与长 ISR 链

优先级较高的中断(如 USB)若被低频事件频繁触发,可能饿死低优先级任务;反之低优先级 ISR 若执行过长会延迟高优先级中断。SDK 通过把大部分工作移出 ISR、仅在 ISR 中投递消息来控制单个 ISR 的执行时间,配合优先级宏的合理配置(按键=3 最低、USB/ADC 较高)来平衡。

性能与运维注意事项

  • 消息处理吞吐:任务循环是单点瓶颈,MSG_500MS 等周期消息处理函数必须短小(只做状态刷新与标志置位),耗时操作(如解码、写 Flash)应拆分为状态机分步执行或放入专门的工作队列。
  • 中断延迟预算:每个 ISR 的目标执行时间应控制在微秒级;新增外设驱动时建议在 ISR 入口/出口打点测量。
  • 调试手段:借助 msg.h 的全局消息枚举,可在任务循环入口打印消息 ID 追踪事件流;hot_msg.c / common_msg.c 是观察产品级消息流转的最佳断点位置。
  • 功耗:MSG_500MS 周期在空闲时仍会唤醒任务,低功耗模式下应通过电源状态机(MSG_LOW_POWER/MSG_ENTER_IDLE)暂停或降频周期消息,避免频繁唤醒影响休眠电流。

扩展点

  1. 新增业务消息:在 msg.h 枚举尾部添加 MSG_XXX → 在事件源(按键解码、驱动 ISR、定时器回调)投递 → 在 common_msg.c/hot_msg.c 或方案自有处理函数中新增 case 分支。三步即可完成一次事件驱动扩展。
  2. 新增外设中断:在驱动初始化调用 request_irq(IRQ_XXX_IDX, priority, xxx_isr, cpu_id),ISR 中投递消息,即可无缝接入现有任务循环,无需改动系统调度。
  3. 新增周期任务:复用 MSG_500MS 计数,或在定时器服务中注册自定义周期的定时器并向队列投递专用消息。
  4. 跨方案复用:sdk/app/bsp/common/msg/msg.c 位于 BSP 公共层,voice_toy 与 mbox_mg 方案共用同一套队列机制,仅应用层消息处理不同——新方案只需提供自己的消息处理函数即可复用整个事件框架。

Related Links

  • 消息系统实现
  • 消息定义头文件
  • voice_toy 方案消息处理
  • mbox_mg 方案消息处理
  • USB 设备中断注册
  • USB 主机中断注册
  • 外部中断注册
  • SARADC 中断注册
  • 硬件 IIC 中断注册
  • 红外接收中断注册
Prev
预编译库与头文件体系
Next
通用外设驱动