消息、定时器与中断服务
消息、定时器与中断服务是 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/ 等芯片目录),其应用层采用典型的事件驱动 + 单任务循环模型,而不是多线程抢占模型。整个系统的运转建立在三个层次上:
- 硬件事件层:按键、USB 插拔、ADC 转换完成、IIC 传输结束、红外遥控信号等硬件事件触发芯片中断。
- 中断服务层:各驱动模块在初始化时调用
request_irq(IRQ_XXX_IDX, priority, isr, cpu_id)注册中断服务函数。ISR 中只做最轻量的处理(读状态寄存器、清 pending、记录事件),并把需要业务层处理的内容封装成消息。 - 消息/任务层:
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 | 录音模式切换 |
| MIDI | MSG_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_ALL | FM 调谐控制 |
注意 MSG_CHANGE_WORK_MODE = 0x600 显式指定了数值,说明 mbox 段是预留的地址空间:它可能与芯片/DSP 侧或其他核的消息编号保持对齐,避免枚举重排导致跨模块编号冲突。这是嵌入式系统里常见的"消息号即协议号"实践——消息 ID 不只是内部枚举,还是核间/模块间的通信协议字段。
消息队列实现(msg.c)
队列的实体实现位于 sdk/app/bsp/common/msg/msg.c,作为 BSP 公共组件被所有方案复用。其核心职责是:
- 缓冲:为中断上下文投递的消息提供有界缓冲区,避免 ISR 直接调用业务代码;
- 解耦:隔离"产生消息的模块"(驱动、定时器)与"消费消息的模块"(应用任务、业务状态机);
- 串行化:所有消息在单一任务循环中顺序处理,天然避免了业务数据的并发竞争。
典型消息体包含消息 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 起始正是为了给跨模块消息预留稳定地址空间。
并发模型:单任务串行化的收益与边界
本架构的核心并发保证是"所有业务消息由唯一任务循环串行处理",因此业务代码之间天然互斥。但有两个边界必须遵守:
- ISR 与任务共享的数据(如驱动状态标志、环形缓冲区读写指针)仍需保证原子性或临界区保护——消息队列只保护"投递/取出"本身,不保护消息所指向的缓冲区内容。
- 多核/多任务方案(如 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)暂停或降频周期消息,避免频繁唤醒影响休眠电流。
扩展点
- 新增业务消息:在
msg.h枚举尾部添加MSG_XXX→ 在事件源(按键解码、驱动 ISR、定时器回调)投递 → 在common_msg.c/hot_msg.c或方案自有处理函数中新增 case 分支。三步即可完成一次事件驱动扩展。 - 新增外设中断:在驱动初始化调用
request_irq(IRQ_XXX_IDX, priority, xxx_isr, cpu_id),ISR 中投递消息,即可无缝接入现有任务循环,无需改动系统调度。 - 新增周期任务:复用
MSG_500MS计数,或在定时器服务中注册自定义周期的定时器并向队列投递专用消息。 - 跨方案复用:
sdk/app/bsp/common/msg/msg.c位于 BSP 公共层,voice_toy 与 mbox_mg 方案共用同一套队列机制,仅应用层消息处理不同——新方案只需提供自己的消息处理函数即可复用整个事件框架。