消息机制与事件分发
AD23N SDK 的轻量级消息系统:以「事件位图 + 环形缓冲队列」双通道承载系统消息,为应用层提供 post_msg / post_event 投递接口与 get_msg 统一取消息入口,是应用任务与底层驱动、解码器、外设热插拔之间解耦通信的核心基础设施。
Purpose and Scope
本文档介绍 AD23N SDK 中消息机制与事件分发的完整实现,涵盖:
- 消息队列的数据结构(环形缓冲区
msg_cbuf、事件位图event_buf、消息池msg_pool) - 事件到消息的映射表
event2msg[]及其设计意图 - 消息编码格式(2 字节头:12 bit 类型 + 4 bit 参数个数)
- 生产者接口
post_msg/post_event与消费者接口get_msg的完整控制流 - 临界区保护、错误码、边界条件与性能特征
相关但独立的话题留给各自的页面:解码器事件表(decoder_msg_tab.c)属于解码器子系统的消息编排;mbox_flash 的 hot_msg.c/h 是面向 mbox 场景的热消息通道;按键、外设热插拔等具体消息的生产逻辑属于对应驱动页面。
概述
在 AD23N 这种资源受限的 MCU 固件中,多个异步源(按键扫描、外设热插拔、解码器完成回调、定时器)都会产生事件,而应用主循环是一个串行消费者。消息机制的价值在于:
- 解耦:生产者只需调用
post_msg/post_event,不关心消费方是谁、何时处理; - 优先级:事件(
event_buf位图)比普通消息(msg_cbuf队列)优先被消费,保证解码器完成、外设插入等关键信号不被普通消息阻塞; - 零动态内存:队列使用静态数组
msg_pool[MAX_POOL],不依赖堆,避免碎片化; - 原子性:所有读写都包裹在
OS_ENTER_CRITICAL/OS_EXIT_CRITICAL临界区中,保证中断上下文与任务上下文并发安全。
消息 ID 统一在 sdk/include_lib/msg/msg.h 的枚举中声明,分为普通应用消息(MSG_0 ~ MSG_COMMON_MAX)、mbox 消息(0x600 起始)、系统/解码器消息(0x800 起始,库会使用,不可更改),以及哨兵值 NO_MSG = 0x0fff。
架构
flowchart TD
subgraph sg_Producer["生产者(异步源)"]
KEY["按键/IO 任务"]
DEV["外设热插拔<br/>USB/SD/AUX/OTG"]
DEC["解码器回调<br/>文件结束/出错/循环"]
TIMER["定时器<br/>10ms/100ms/500ms"]
end
subgraph sg_MsgLayer["消息层(msg.c)"]
PE["post_event(event)"]
PM["post_msg(argc, ...)"]
EB["event_buf[] 位图"]
CB["msg_cbuf 环形队列"]
GM["get_msg(len, msg)"]
EM["event2msg[] 映射表"]
end
subgraph sg_Consumer["消费者"]
APP["应用主循环/任务"]
HD["消息分发处理"]
end
KEY --> PM
DEV --> PE
DEC --> PE
TIMER --> PM
PE --> EB
PM --> CB
EB -->|"get_event() 查位图"| GM
CB -->|"cbuf_read 出队"| GM
GM --> EM
GM --> APP
APP --> HD
架构说明:左侧四类异步生产者通过两个入口投递——需要携带参数的消息走 post_msg 进入环形队列,仅表达"某事件发生"的信号走 post_event 置位事件位图。get_msg 作为唯一消费者入口,先查事件位图、后读环形队列(详见"核心流程"),取出的消息 ID 通过 event2msg[] 翻译后交给应用层分发处理。这一设计保证事件类信号具备高于普通消息的抢占式优先级。
核心实现
数据布局与静态资源
msg.c 顶部定义了三个全局静态对象,构成整个消息系统的全部存储:
msg_cbuf:cbuffer_t类型的环形缓冲区,存放普通消息(含参数);event_buf[]:按EVENT_TOTAL计算的 32 位字数组,每一位代表一个事件是否被置位;msg_pool[MAX_POOL]:环形缓冲区的底层字节池,MAX_POOL在msg.h中定义为 128。
#define EVENT_TOTAL (1+(sizeof(event2msg)/2 -1)/32)
static const u16 event2msg[] = {
MSG_F1A1_FILE_END, /* 0 */
MSG_F1A1_FILE_ERR, /* 1 */
MSG_F1A1_LOOP,
...
};
static cbuffer_t msg_cbuf;
static u32 event_buf[EVENT_TOTAL] = {0};
static u32 msg_pool[MAX_POOL];
Source: msg.c
EVENT_TOTAL 由 event2msg[] 的元素个数推导而来:每 32 个事件占用一个 u32 位图字。event2msg[] 的下标即"事件号",值即"消息 ID"——事件是抽象的位索引,消息是可被应用识别的枚举 ID,两者通过该表一一对应。表尾的 NO_MSG 占位符说明事件号允许存在空洞。
消息编码格式
每个普通消息在环形队列中按如下布局存储:
- 头部(2 字节):低 12 位为消息类型(
MSG_TYPE_BIT_LEN),高 4 位为参数个数argc-1(MSG_PARAM_BIT_LEN); - 参数区:每个参数占 4 字节(
int),数量由头部高 4 位决定。
#define MSG_HEADER_BYTE_LEN 2
#define MSG_HEADER_BIT_LEN (MSG_HEADER_BYTE_LEN*8)
#define MSG_HEADER_ALL_BIT ((1L<<MSG_HEADER_BIT_LEN) - 1)
#define MSG_TYPE_BIT_LEN 12
#define MSG_PARAM_BIT_LEN (MSG_HEADER_BYTE_LEN*8-MSG_TYPE_BIT_LEN)
#define MAX_POOL 128
Source: msg.h
该格式的关键约束:单条消息最多携带 15 个参数(4 bit 表示),且头部只有 12 bit 用于消息 ID,因此消息 ID 范围上限为 0xFFF,这也解释了 NO_MSG = 0x0fff 作为哨兵值的设计——它处于 ID 空间的最高位,不可能与真实消息冲突。
事件机制
事件位图的核心操作是置位、查询、取最高优先级事件、清位,全部在临界区内完成:
static u32 get_event(void)
{
CPU_SR_ALLOC();
OS_ENTER_CRITICAL();
u32 event_cls;
for (u32 i = 0; i < EVENT_TOTAL; i++) {
__asm__ volatile("%0 = clz(%1)":"=r"(event_cls):"r"(event_buf[i]));
if (event_cls != 32) {
OS_EXIT_CRITICAL();
return i * 32 + (31 - event_cls);
}
}
OS_EXIT_CRITICAL();
return NO_EVENT;
}
Source: msg.c
get_event() 使用 DSP 指令 clz(count leading zeros)逐字扫描位图:clz 返回第一个为 1 的位之前的 0 的个数,31 - event_cls 即最高有效置位位的位置。由于事件号越小代表越关键(解码器文件结束、错误等排在表头),该算法天然实现了按事件号升序的优先级调度,且是常数级指令开销,非常适合中断/低功耗场景。
事件与消息的翻译由 event2msg_api() 完成:return event2msg[event];,消费者拿到的永远是消息 ID。get_event_status() 提供轮询式查询能力,供应用在不消费消息的情况下检查某个事件是否发生。
核心流程
消息投递与取出的完整控制流
sequenceDiagram
participant P as 生产者<br/>(按键/驱动/解码器)
participant Q as 消息层 msg.c
participant C as 消费者<br/>(应用主循环)
P->>Q: post_msg(argc, ...)
activate Q
Q->>Q: 临界区:检查 cbuf 可写空间
alt 空间不足
Q-->>P: 返回 MSG_BUF_NOT_ENOUGH
else 空间充足
Q->>Q: 写 2 字节头(类型|参数个数)
Q->>Q: 逐个写 4 字节参数
Q-->>P: 返回 MSG_NO_ERROR
end
deactivate Q
P->>Q: post_event(event)
activate Q
Q->>Q: 校验 event 越界
Q->>Q: 临界区:event_buf[event/32] \|= BIT(event%32)
Q-->>P: 返回 MSG_NO_ERROR
deactivate Q
C->>Q: get_msg(len, msg)
activate Q
Q->>Q: 临界区:get_event() 扫位图
alt 有事件
Q->>Q: 清事件位,msg[0]=event2msg[event]
Q-->>C: 返回 MSG_NO_ERROR(事件消息)
else 无事件
Q->>Q: cbuf_read 读 2 字节头
alt 队列空
Q-->>C: 返回 MSG_NO_MSG(msg[0]=NO_MSG)
else 头部有效
Q->>Q: 校验参数个数 ≤ len-1
Q->>Q: cbuf_read 读参数区
Q-->>C: 返回 MSG_NO_ERROR(普通消息)
end
end
deactivate Q
get_msg 的决策逻辑
get_msg() 是消息系统唯一的出口,其决策顺序体现了设计优先级:
- 先查事件位图:
get_event()返回非NO_EVENT时,直接清除该事件位并以event2msg[event]作为msg[0]返回——事件消息零拷贝、零排队; - 再读环形队列:
cbuf_read读取 2 字节头。若读取长度不足,说明队列为空,此时执行__asm__ volatile("idle")进入 CPU 空闲指令(低功耗等待),返回MSG_NO_MSG; - 解析头部:
msg[0] = t_msg[0] & (MSG_HEADER_ALL_BIT >> MSG_PARAM_BIT_LEN)取出低 12 位消息类型;param_len = param >> MSG_TYPE_BIT_LEN取出参数个数; - 参数越界检查:若
param_len > len - 1(调用方缓冲区不够),返回MSG_BUF_NOT_ENOUGH,注意此时头部已被消费、参数仍留在队列中——这是调用方必须保证len足够大的原因; - 读取参数区:
cbuf_read读param_len * sizeof(int)字节,长度不匹配返回MSG_CBUF_ERROR。
int get_msg(int len, int *msg)
{
u32 param = 0;
u16 *t_msg = (u16 *)¶m;
MSG_ENTER_CRITICAL();
u32 event = get_event();
if (event != NO_EVENT) {
clear_one_event(event);
msg[0] = event2msg[event];
MSG_EXIT_CRITICAL();
return MSG_NO_ERROR;
}
u32 tlen = cbuf_read(&msg_cbuf, (void *)t_msg, MSG_HEADER_BYTE_LEN);
if (MSG_HEADER_BYTE_LEN != tlen) {
MSG_EXIT_CRITICAL();
__asm__ volatile("idle");
msg[0] = NO_MSG;
return MSG_NO_MSG;
}
msg[0] = t_msg[0] & (MSG_HEADER_ALL_BIT >> MSG_PARAM_BIT_LEN);
u32 param_len = param >> MSG_TYPE_BIT_LEN;
if (param_len > (len - 1)) {
MSG_EXIT_CRITICAL();
return MSG_BUF_NOT_ENOUGH;
}
u32 rlen = cbuf_read(&msg_cbuf, (void *)(msg + 1), param_len * sizeof(int));
if ((param_len * sizeof(int)) != rlen) {
MSG_EXIT_CRITICAL();
return MSG_CBUF_ERROR;
}
MSG_EXIT_CRITICAL();
return MSG_NO_ERROR;
}
Source: msg.c
生产者接口
post_msg 是变参函数,argc 表示"消息类型 + 参数个数"(第一个变参为消息 ID,其余为参数)。它先检查环形缓冲区剩余空间,再在临界区内依次写入头与参数:
int post_msg(int argc, ...)
{
u32 param;
u16 *t_msg = (u16 *)¶m;
va_list argptr;
OS_ENTER_CRITICAL();
va_start(argptr, argc);
if (!cbuf_is_write_able(&msg_cbuf, (argc - 1)*sizeof(int) + MSG_HEADER_BYTE_LEN)) {
OS_EXIT_CRITICAL();
return MSG_BUF_NOT_ENOUGH;
}
t_msg[0] = (MSG_HEADER_ALL_BIT >> MSG_PARAM_BIT_LEN) & va_arg(argptr, int);
t_msg[0] = ((argc - 1) << (MSG_TYPE_BIT_LEN)) | t_msg[0];
cbuf_write(&msg_cbuf, (void *)&t_msg[0], MSG_HEADER_BYTE_LEN);
for (u32 i = 0; i < argc - 1; i++) {
param = va_arg(argptr, int);
cbuf_write(&msg_cbuf, (void *)¶m, sizeof(int));
}
va_end(argptr);
OS_EXIT_CRITICAL();
return MSG_NO_ERROR;
}
Source: msg.c
注意 t_msg 复用了 param 的地址:先通过 16 位视角写入头部,再通过 32 位视角写入参数,体现了 MCU 固件中常见的"位域手写打包"手法,避免引入结构体对齐开销。post_event 则更简单——校验事件号不越界后在临界区内置位 event_buf[event/32] |= BIT(event % 32)。
使用示例
投递一条带参数的消息(生产者视角)
按键或定时器任务中,投递"播放指定文件"消息并携带文件号参数:
/* 从任务/中断上下文投递带参数消息 */
post_msg(MSG_PLAY_FILE1 + 1, MSG_PLAY_FILE1, file_index);
Source: msg.c(
post_msg变参接口)
投递事件信号(解码器完成场景)
解码器回调中投递"MP3 文件结束"事件。事件号 EVENT_MP3_END 在 msg.h 中定义,经 event2msg[] 映射为消息 MSG_MP3_FILE_END:
/* 事件号定义,与 event2msg[] 下标对应 */
#define EVENT_MP3_END 12
#define EVENT_MP3_ERR 13
#define EVENT_MP3_LOOP 14
int post_event(int event); /* 事件入口 */
Source: msg.h
消费消息(应用主循环视角)
应用任务周期性调用 get_msg,根据 msg[0] 分发处理:
int msg[8]; /* 8 个 int,足够容纳头部 + 最多参数 */
int ret = get_msg(ARRAY_SIZE(msg), msg);
if (ret == MSG_NO_ERROR) {
switch (msg[0]) {
case MSG_PLAY_FILE1:
play_file(msg[1]); /* 参数区第一个 int */
break;
case MSG_MP3_FILE_END:
play_next();
break;
default:
break;
}
} else if (ret == MSG_NO_MSG) {
/* 无消息,可进入低功耗 */
}
Source: msg.c(
get_msg返回值语义)
初始化与清空
系统启动阶段调用 message_init() 复位事件位图并初始化环形缓冲区;异常恢复场景可调用 clear_all_message() 丢弃所有排队中的普通消息(事件位图不受影响):
void message_init()
{
event_init();
cbuf_init(&msg_cbuf, msg_pool, sizeof(msg_pool));
cbuf_clear(&msg_cbuf);
}
void clear_all_message(void)
{
cbuf_clear(&msg_cbuf);
}
Source: msg.c
配置选项
消息机制的可调参数均以宏形式定义在 sdk/include_lib/msg/msg.h,属编译期常量,修改后需重新编译:
| 宏 | 类型 | 默认值 | 说明 |
|---|---|---|---|
MAX_POOL | int | 128 | 环形队列底层字节池大小,决定可排队消息的总字节数(含头部与参数) |
MSG_HEADER_BYTE_LEN | int | 2 | 消息头部字节数(类型 + 参数个数) |
MSG_TYPE_BIT_LEN | int | 12 | 消息类型占位数,限制消息 ID 上限 0xFFF |
MSG_PARAM_BIT_LEN | int | 4 | 参数个数占位数,限制单条消息最多 15 个参数 |
MSG_HEADER_ALL_BIT | long | 0xFFFF | 头部全位掩码((1<<16)-1) |
NO_MSG | enum | 0x0fff | 空消息哨兵值,位于 ID 空间顶端 |
NO_EVENT | int | 0xffff | get_event() 无事件时的返回值 |
EVENT_TOTAL | int | 由 event2msg[] 推导 | 事件位图字数,1+(sizeof(event2msg)/2-1)/32 |
Source: msg.h
API 参考
以下接口均声明于 msg.h:
int post_msg(int argc, ...)
投递一条带参数的消息到环形队列。
参数:
argc(int):变参总数 = 1(消息 ID)+ 参数个数。注意argc最小为 1(无参数消息),此时头部参数个数为 0;...:第一个为消息 ID(如MSG_NEXT_FILE),其后为最多 15 个int参数。
返回:
MSG_NO_ERROR(0):投递成功;MSG_BUF_NOT_ENOUGH(-4):队列剩余空间不足,消息未写入。
并发说明: 内部全程处于 OS_ENTER_CRITICAL 临界区,可从中断上下文调用。
int post_event(int event)
置位事件位图,表达"某事件已发生"。
参数:
event(int):事件号,必须小于ARRAY_SIZE(event2msg)(即小于event2msg[]元素个数)。
返回:
MSG_NO_ERROR(0):成功;MSG_EVENT_PARAM_ERROR(-3):事件号越界。
说明: 同一事件重复置位是幂等的(位图置位),不会累积;事件消费时经 event2msg[event] 翻译为消息 ID。
int get_msg(int len, int *msg)
取出一条消息,事件优先于普通消息。
参数:
len(int):msg缓冲区可容纳的int个数;msg(int*):输出缓冲区,msg[0]为消息 ID,msg[1..param_len]为参数。
返回:
MSG_NO_ERROR(0):取到事件消息或普通消息;MSG_NO_MSG(0):队列空且无事件,msg[0]被置为NO_MSG;MSG_BUF_NOT_ENOUGH(-4):参数个数超出len-1(头部已消费);MSG_CBUF_ERROR(-5):环形队列数据损坏(参数区读取长度不匹配)。
注意: 因 MSG_NO_ERROR 与 MSG_NO_MSG 数值同为 0,调用方必须通过 msg[0] == NO_MSG 判断空队列,或直接依赖返回值语义。
bool get_event_status(u32 event)
非破坏性查询事件是否置位,不消费事件。
参数: event (u32):事件号。 返回: TRUE / FALSE。
void clear_all_message(void)
清空环形队列中所有普通消息(保留事件位图)。
void message_init(void)
初始化消息系统:清空事件位图、初始化并清空环形缓冲区。应在系统启动早期、任何生产/消费行为之前调用一次。
失败模式、边界情况与并发
错误码一览
错误码定义于 msg.h:
| 错误码 | 值 | 触发场景 |
|---|---|---|
MSG_NO_ERROR | 0 | 成功 |
MSG_NO_MSG | 0 | 无消息(与成功同值,靠 msg[0]==NO_MSG 区分) |
MSG_EVENT_EXIST | -1 | 预留,当前实现未使用 |
MSG_NOT_EVENT | -2 | 预留,当前实现未使用 |
MSG_EVENT_PARAM_ERROR | -3 | post_event 事件号越界 |
MSG_BUF_NOT_ENOUGH | -4 | 队列满(post_msg)或参数区超出调用方缓冲区(get_msg) |
MSG_CBUF_ERROR | -5 | 队列数据异常,参数区读取长度不匹配 |
边界情况
- 队列满:
post_msg在写前调用cbuf_is_write_able预检,失败则整体拒绝写入,消息不会半写;生产者应避免在队列满时高频投递(如打印/丢弃策略由调用方决定); - 参数越界:
get_msg返回MSG_BUF_NOT_ENOUGH时头部已被消费,参数残留在队列中会导致后续消息错位——调用方必须保证len≥ 1 + 最大参数个数; - 事件空洞:
event2msg[]中允许NO_MSG占位(如索引 8),对应事件即使置位也会翻译为NO_MSG; - 事件号越界:
post_event与clear_one_event均有ARRAY_SIZE(event2msg)越界保护,越界调用被安全忽略/报错; get_msg空队列:执行__asm__ volatile("idle")进入 CPU 空闲,依赖中断唤醒(如定时器中断再次post_msg),这是低功耗设计的组成部分。
并发与一致性
- 所有对
event_buf、msg_cbuf的读写均包裹在OS_ENTER_CRITICAL/OS_EXIT_CRITICAL(MSG_ENTER_CRITICAL)中,保证单核 MCU 上中断与任务上下文互斥; get_msg整个"查事件 → 读头 → 读参数"过程在一个临界区内完成,保证与post_msg/post_event的原子性;- 生产者在中断上下文调用是安全的(临界区 + 无阻塞操作),但注意临界区会短暂屏蔽中断,高频投递会增大中断延迟。
性能与运维注意事项
- 事件路径零拷贝:事件消息不进入环形队列,
get_event()用clz指令常数时间找到最高优先级事件,是消息系统的最快路径,适合解码器回调等高频关键信号; - 普通消息路径:头部 2 字节 + 每参数 4 字节的顺序写/读,环形缓冲区无搬移开销;
MAX_POOL=128决定了典型场景(每消息 2~12 字节)可排队约 10~60 条消息; - 优先级语义:事件按事件号升序优先(
clz扫描),普通消息 FIFO;若需要严格全局优先级,应把高优先级信号设计为事件而非普通消息; - 调试手段:
msg.c中保留了被注释的log_info调试点(如"evenr post : 0x%x"、" gm a 0x%x"),排查消息丢失时可临时打开;LOG_TAG为"[msg]"。
扩展点
- 新增消息:在
msg.h枚举中追加 ID(避开0x600/0x800保留段与NO_MSG),应用分发处增加 case 即可; - 新增事件:在
event2msg[]表尾追加"事件号 → 消息 ID"映射,事件号自动由数组长度决定(EVENT_TOTAL随之更新); - 调整容量:修改
MAX_POOL以适配更大吞吐,或调低以节省 RAM(消息池为静态数组,常驻内存); - 自定义消费者:任意任务均可调用
get_msg消费消息,但整个系统建议保持单一消费者(应用主循环),避免多消费者竞争同一队列导致消息被抢占。
相关链接
- msg.h 接口声明与消息 ID 枚举
- msg.c 消息机制实现
- decoder_msg_tab.c 解码器消息编排(解码器子系统消息表,独立页面)
- hot_msg.c/h mbox 热消息通道(mbox_flash 场景专用消息通道)