消息与事件机制
AD24N SDK 的进程内通信核心:基于「事件位图 + 环形消息队列」双通道的解耦机制,供中断、外设驱动、解码器与上层应用模块之间异步传递控制指令与状态通知。
Purpose and Scope
本文档介绍 AD24N(GP-MCU SDK)中消息与事件机制(Message & Event)的完整实现,涵盖:
- 事件(Event)位图通道与消息(Message)环形队列通道的设计与编码格式;
- 核心引擎
msg.c的初始化、投递、获取与优先级调度逻辑; - 消息/事件 API 的完整签名与返回码语义;
- 上层消费示例(
common_msg.c)展示的典型用法:定时任务、按键、音量、设备插拔与固件升级联动。
不属于本文档范围(属于兄弟页面)的内容包括:按键扫描与键值映射(key)、电源管理(app_power_mg)、音频解码器(decoder)以及各工作模式(work mode)自身的状态机。本文档只解释它们之间传递消息的通道本身。
概述
在单 MCU 嵌入式系统中,不同模块(中断服务程序、外设驱动、解码器库、按键扫描、上层应用)运行在不同的执行上下文,且不能直接相互调用(会造成耦合与重入问题)。AD24N 的消息与事件机制提供了一种异步、无阻塞、单消费者的通信模型:
- 事件(Event):一个按位存储的位图,每个事件占 1 bit,只能表达「发生/未发生」,无参数。适合通知型信号(解码结束、设备插入、U 盘拔出等)。事件优先级高于消息,且事件号越小优先级越高。
- 消息(Message):写入环形缓冲区的数据帧,消息头携带 12 bit 消息 ID 与 4 bit 参数个数,可携带最多 15 个
int参数。适合需要携带数据的控制指令(如MSG_CHANGE_WORK_MODE、MSG_VOL_UP)。
该机制的设计意图:
- 解耦:生产者只需调用
post_event()/post_msg(),无需知道消费者是谁; - 中断安全:所有入队操作均处于
OS_ENTER_CRITICAL()临界区内,可安全地从中断上下文调用; - 确定性:固定内存池(无动态分配)、O(1) 位图操作、环形缓冲无碎片;
- 低功耗:消费者在队列为空时通过
__builtin_pi32_idle()让 CPU 进入 idle 等待唤醒。
架构
flowchart TD
subgraph sg_Producers["生产者(Producer)"]
Decoder["解码器库<br/>F1A1/MP3/WAV/A 解码"]
Keys["按键/IR 输入"]
Devices["外设驱动<br/>USB/SD/AUX/PC"]
Timers["系统定时器"]
end
subgraph sg_Core["消息引擎(msg.c / msg.h)"]
PostEvent["post_event()<br/>置位 event_buf 位图"]
PostMsg["post_msg()<br/>写入 msg_cbuf 环形缓冲"]
EventBuf["event_buf[]<br/>事件位图(每事件 1 bit)"]
MsgCBuf["msg_cbuf<br/>环形缓冲 + msg_pool[128]"]
Map["event2msg[]<br/>事件号→消息 ID 映射表"]
end
subgraph sg_Consumer["消费者(Consumer)"]
GetMsg["get_msg()<br/>主循环取消息"]
CommonDeal["common_msg_deal()<br/>公共消息处理"]
ModeDeal["各 WorkMode 消息处理<br/>toy_main / app_*"]
end
Decoder --> PostEvent
Devices --> PostEvent
Keys --> PostMsg
Timers --> PostMsg
PostEvent --> EventBuf
PostMsg --> MsgCBuf
EventBuf --> Map
Map --> GetMsg
MsgCBuf --> GetMsg
GetMsg --> CommonDeal
GetMsg --> ModeDeal
CommonDeal -->|"post_event/post_msg 联动"| PostEvent
架构要点
- 双通道合并消费:
get_msg()是唯一消费入口,它先扫描事件位图(事件优先级更高),再读取环形缓冲中的消息帧;两者都在同一临界区内完成,保证原子性。 - 事件到消息的映射:事件并不直接携带消息 ID,而是通过
event2msg[]表把「事件号」翻译成「消息 ID」(见 msg.c)。表中某些槽位填NO_MSG(如事件 8、33),表示该事件号保留未使用。 - 单消费者模型:系统只有一个主循环调用
get_msg()并分发,因此无需多消费者竞争锁,临界区只需防中断/防生产者在取消息中途插入数据。 - 与工作模式状态机协作:上层
common_msg_deal()收到设备类消息后,会改变work_mode并通过post_msg(1, MSG_CHANGE_WORK_MODE)通知模式状态机切换,形成「消息驱动状态机」的闭环。
核心机制
事件位图通道(Event)
事件存储在静态数组 event_buf[EVENT_TOTAL] 中,其中:
#define EVENT_TOTAL (1+(sizeof(event2msg)/2 -1)/32)
即根据 event2msg[] 表的元素个数向上取整到 32 的倍数(每 1 个 u32 承载 32 个事件位)。当前表中有 34 个槽位,因此 EVENT_TOTAL = 2,共 64 个事件位。事件号直接作为位索引使用:事件 n 落在 event_buf[n/32] 的第 n%32 位。
投递:post_event(event) 在临界区内执行 event_buf[event / 32] |= BIT(event % 32),事件被合并(同一事件多次投递只置 1 次位),并在事件号越界时返回 MSG_EVENT_PARAM_ERROR(见 msg.c)。
提取:get_event() 从第 0 个字开始,用 clz(count leading zeros)指令找出每个字中最高置位的位置,从而总是返回**事件号最小(优先级最高)**的未处理事件;若全部为 0 则返回 NO_EVENT(见 msg.c)。这保证了「先到先处理 + 小号优先」的确定性调度。
查询:get_event_status(event) 供消费者在 get_msg() 之外主动轮询某个事件是否发生(见 msg.c)。
清除:clear_one_event(event) 在 get_msg() 取走事件后立刻清位,防止重复处理(见 msg.c)。
事件→消息映射表
event2msg[] 把事件号翻译为消息 ID(见 msg.c),表项与 msg.h 中的 EVENT_* 宏一一对应:
| 事件号 | EVENT_* 宏 | 映射消息 | 语义 |
|---|---|---|---|
| 0 | EVENT_F1A1_END | MSG_F1A1_FILE_END | F1A1 解码文件结束 |
| 1 | EVENT_F1A1_ERR | MSG_F1A1_FILE_ERR | F1A1 解码错误 |
| 6 | EVENT_MIDI_END | MSG_MIDI_FILE_END | MIDI 解码结束 |
| 9 | EVENT_A_END | MSG_A_FILE_END | A 格式解码结束 |
| 12 | EVENT_MP3_END | MSG_MP3_FILE_END | MP3 解码结束 |
| 15 | EVENT_WAV_END | MSG_WAV_FILE_END | WAV 解码结束 |
| 18 | EVENT_APP_SW_ACTIVE | MSG_APP_SWITCH_ACTIVE | App 切换激活 |
| 19 | EVENT_WFILE_FULL | MSG_WFILE_FULL | 写文件满 |
| 20 | EVENT_OTG_IN | MSG_OTG_IN | OTG 插入 |
| 21 | EVENT_OTG_OUT | MSG_OTG_OUT | OTG 拔出 |
| 22 | EVENT_UDISK_IN | MSG_USB_DISK_IN | U 盘插入 |
| 23 | EVENT_UDISK_OUT | MSG_USB_DISK_OUT | U 盘拔出 |
| 24 | EVENT_PC_IN | MSG_PC_IN | PC 连接 |
| 25 | EVENT_PC_OUT | MSG_PC_OUT | PC 断开 |
| 28 | EVENT_SDMMCA_IN | MSG_SDMMCA_IN | SD 卡插入 |
| 29 | EVENT_SDMMCA_OUT | MSG_SDMMCA_OUT | SD 卡拔出 |
| 32 | EVENT_EXTFLSH_IN | MSG_EXTFLSH_IN | 外部 Flash 插入 |
设计意图:解码器、设备驱动等「库侧」代码只认识中断/回调语境下的事件号,而「应用侧」只处理消息。映射表把两者解耦,使库无需包含应用层消息定义,也允许同一事件在不同产品上映射到不同的处理策略。
消息队列通道(Message)
存储结构
消息写入静态环形缓冲 msg_cbuf,其底层存储为固定数组 msg_pool[MAX_POOL](MAX_POOL = 128 个 u32,共 512 字节,见 msg.h):
static cbuffer_t msg_cbuf;
static u32 msg_pool[MAX_POOL];
环形缓冲来自 SDK 通用组件 circular_buf.h(cbuf_init / cbuf_read / cbuf_write / cbuf_is_write_able / cbuf_clear),支持单生产者单消费者的无锁读写模式,配合临界区保证多生产者场景安全。
消息帧编码格式
每条消息以 2 字节(u16)头部 开始,头部位域定义如下(见 msg.h):
#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)
- bit[11:0]:消息 ID(
MSG_TYPE_BIT_LEN = 12,取值 0x000 ~ 0xFFF,NO_MSG = 0x0FFF保留为空消息); - bit[15:12]:参数个数(
MSG_PARAM_BIT_LEN = 4,最多 15 个参数,参数以int(4 字节)为单位)。
因此一条消息占用的字节数为 2 + 参数个数 * 4。头部之后依次存放各参数(小端、原生 int 宽度)。
入队:post_msg()
post_msg(int argc, ...) 是变参函数,argc 表示总参数个数(含消息 ID 本身),第一个变参必须是消息 ID,其余是随消息携带的参数(见 msg.c):
int post_msg(int argc, ...)
{
u32 param;
u16 *t_msg = (u16 *)¶m;
CPU_SR_ALLOC();
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;
}
关键点:
- 先检查剩余空间,不足时直接丢弃并返回
MSG_BUF_NOT_ENOUGH(生产者不阻塞,这是嵌入式实时系统的常见取舍); - 头部编码:
(argc - 1) << 12 | (msg_id & 0x0FFF),即低 12 位消息 ID、高 4 位参数个数; - 头部和参数分别写入环形缓冲,整个操作在
OS_ENTER_CRITICAL()内完成,保证多生产者(如中断 + 主循环)并发投递不互相撕裂。
出队:get_msg()
get_msg(int len, int *msg) 是消费者唯一入口,len 是调用方提供的 msg 数组容量(int 个数),msg[0] 返回消息 ID,msg[1..] 返回参数(见 msg.c):
int get_msg(int len, int *msg)
{
u32 param = 0;
u16 *t_msg = (u16 *)¶m;
//get_msg
CPU_SR_ALLOC();
/* OS_ENTER_CRITICAL(); */
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();
/*get no msg,cpu enter idle.why do this? TODO*/
__builtin_pi32_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;
}
出队算法按以下顺序执行:
- 先查事件:
get_event()取最高优先级事件并清位,映射为消息 ID 返回(事件通道优先于消息通道); - 再读消息头:从环形缓冲读取 2 字节头部;若读不到完整头部说明队列为空,调用
__builtin_pi32_idle()让 CPU 进入 idle(等待中断唤醒,实现低功耗),并返回MSG_NO_MSG; - 校验容量:从头部高 4 位解析参数个数,若超过调用方
msg数组容量返回MSG_BUF_NOT_ENOUGH(消息保留在缓冲中,等待下次更大缓冲区读取); - 读参数:按参数个数读取参数区,若环形缓冲读数不符返回
MSG_CBUF_ERROR(理论上仅当缓冲损坏时发生)。
设计意图:事件通道优先于消息通道,保证解码结束、设备插拔等时序敏感信号不被堆积的消息延迟;同时
get_msg()内不进行任何可能阻塞的操作,确保主循环的实时性。
核心流程
下图展示一次典型的「设备插入 → 事件 → 消息 → 工作模式切换」完整链路(以 common_msg.c 的 U 盘升级流程为例):
sequenceDiagram
participant ISR as 中断/设备回调
participant Eng as 消息引擎 msg.c
participant Main as 主循环
participant CDeal as common_msg_deal()
participant Mode as 工作模式状态机
ISR->>Eng: post_event(EVENT_UDISK_IN)
activate Eng
Eng->>Eng: event_buf 置位(临界区)
deactivate Eng
loop 主循环轮询
Main->>Eng: get_msg(len, msg)
activate Eng
Eng->>Eng: get_event() 取到事件 22
Eng->>Eng: 映射 MSG_USB_DISK_IN 并清事件位
Eng-->>Main: MSG_NO_ERROR, msg[0]=MSG_USB_DISK_IN
deactivate Eng
Main->>CDeal: 分发 msg
activate CDeal
CDeal->>CDeal: device_update(udisk, 1) 返回 UPDATA_READY
CDeal->>Mode: work_mode = TOY_UPDATE
CDeal->>Eng: post_msg(1, MSG_CHANGE_WORK_MODE)
deactivate CDeal
Main->>Eng: get_msg(len, msg)
activate Eng
Eng->>Eng: 从环形缓冲读头部(ID=0x600, 0 参数)
Eng-->>Main: MSG_NO_ERROR, msg[0]=MSG_CHANGE_WORK_MODE
deactivate Eng
Main->>Mode: 切换到 TOY_UPDATE 模式
end
Note over Main,Eng: 队列与事件均空时<br/>get_msg() 调用 __builtin_pi32_idle() 进入低功耗
关键流程说明
- 事件链路:外设驱动在中断/回调中调用
post_event()(如 common_msg.c 中MSG_OTG_IN → usb_host_mount → post_event(EVENT_UDISK_IN)),主循环在下一次get_msg()时将其映射为消息处理; - 消息链路:任何上下文调用
post_msg()(如post_msg(1, MSG_CHANGE_WORK_MODE)),主循环取出后交给common_msg_deal()或对应工作模式处理器; - 模式切换联动:消息处理函数通过修改
work_mode全局变量 + 投递MSG_CHANGE_WORK_MODE消息,把「设备事件」转化为「状态机迁移」,实现模式之间的平滑切换(见 common_msg.c); - 空转低功耗:当事件位图全零且环形缓冲为空时,
get_msg()执行__builtin_pi32_idle(),CPU 停止取指直到下一个中断到来——这是 AD24N 待机功耗优化的关键路径。
使用示例
示例 1:设备插拔事件联动(生产者视角)
common_msg.c 在收到 OTG 设备消息后挂载文件系统,并向事件通道投递 EVENT_UDISK_IN/OUT,供上层解码模块感知设备变化:
case MSG_OTG_IN:
if (0 != usb_host_mount(0, 3, 20, 200)) {
log_info("mount err!\n");
break;
}
post_event(EVENT_UDISK_IN);
break;
case MSG_OTG_OUT:
usb_host_unmount(0);
post_event(EVENT_UDISK_OUT);
break;
Source: common_msg.c
设计意图:
post_event只表达「U 盘已就绪」这一事实,不携带路径等数据;真正需要路径的模块可通过全局设备状态自行查询,避免事件带参带来的缓冲压力。
示例 2:消息驱动工作模式切换
在设备升级成功后,通过消息队列通知模式状态机切换到升级模式:
u32 err = device_update(t_up_device, 1);
/* log_info("msg 0x%x\n", *msg); */
if (err == UPDATA_READY) {
work_mode = TOY_UPDATE;
post_msg(1, MSG_CHANGE_WORK_MODE);
/* post_msg(2, MSG_CHANGE_WORK_MODE, *msg); */
}
Source: common_msg.c
post_msg(1, MSG_CHANGE_WORK_MODE)表示 argc=1,仅携带消息 ID 而无附加参数;被注释的post_msg(2, ...)展示了带 1 个参数的写法。消息 ID 保存在低 12 位,因此MSG_CHANGE_WORK_MODE (0x600)这类系统消息与普通消息共用同一队列。
示例 3:变参消息入队实现(引擎内部)
post_msg 使用 C 变参机制把消息 ID 与参数逐个写入环形缓冲,是 SDK 中「任意个数 int 参数」消息的通用实现:
int post_msg(int argc, ...)
{
u32 param;
u16 *t_msg = (u16 *)¶m;
CPU_SR_ALLOC();
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
示例 4:消费者消息分发(common_msg_deal)
主循环取出消息后进入 common_msg_deal(int *msg) 的 switch 分发。该函数展示了定时消息、按键消息、设备消息、模式切换消息的典型处理模式:
int common_msg_deal(int *msg)
{
char *t_up_device = NULL;
switch (*msg) {
case MSG_500MS:
wdt_clear();
music_vol_update();
adc_sample_vbg(1);
app_power_scan();
audio_lookup();
break;
case MSG_VOL_UP:
dac_vol('+', 255);
log_info("vol:%d \n", dac_vol('r', 0));
break;
case MSG_NEXT_WORKMODE:
log_info("MSG_NEXT_WORKMODE\n");
app_next_mode();
post_msg(1, MSG_CHANGE_WORK_MODE);
break;
case MSG_LOW_POWER:
case MSG_POWER_OFF:
log_info("MSG_POWER_OFF\n");
work_mode = TOY_SOFTOFF;
post_msg(1, MSG_CHANGE_WORK_MODE);
break;
...
default:
break;
}
return -1;
}
Source: common_msg.c
观察:
MSG_500MS是周期定时消息,集中执行看门狗喂狗、音量回写 VM、ADC 采样、电源扫描与音频查找等周期维护任务;多个消息 ID 可以共用一个 case 分支(如MSG_LOW_POWER/MSG_POWER_OFF都进入软关机流程),且处理函数内部可以继续投递新消息形成消息链。返回-1表示该消息已被公共层处理或无需进一步处理。
配置选项
消息机制的可配置项集中在 msg.h 与 msg.c 的编译期宏中:
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
MAX_POOL | 宏(u32 单元数) | 128 | 消息环形缓冲池大小,单位是 u32(4 字节),实际容量 512 字节;增大可容纳更多/更大消息 |
MSG_HEADER_BYTE_LEN | 宏 | 2 | 消息头字节数(u16) |
MSG_TYPE_BIT_LEN | 宏 | 12 | 消息头中消息 ID 占用的位数(bit[11:0]) |
MSG_PARAM_BIT_LEN | 宏 | 4 | 消息头中参数个数占用的位数(bit[15:12]),最多 15 个 int 参数 |
EVENT_TOTAL | 宏(自动推导) | 2 | 事件位图字数,由 event2msg[] 元素个数计算,每字 32 事件 |
NO_EVENT | 宏 | 0xffff | get_event() 无事件时的返回值 |
NO_MSG | 枚举 | 0x0fff | 空消息 ID,队列空时 get_msg 返回该值 |
TCFG_UDISK_ENABLE / TFG_SD_EN / TCFG_PC_ENABLE / KEY_IR_EN | 编译开关 | 按产品配置 | 决定 common_msg_deal 中设备/按键消息分支是否编译,影响可处理的消息集合 |
注意:
MSG_TYPE_BIT_LEN = 12决定了NO_MSG = 0x0FFF是消息 ID 的上界,新增消息 ID 不能超过 0xFFF;MSG_COMMON_MAX枚举值标注了公共消息的上限。
API 参考
void message_init(void)
初始化消息与事件通道:清零事件位图、初始化并清空环形缓冲。系统启动早期必须调用一次(见 msg.c)。
int post_event(int event)
投递一个事件(置位事件位图)。
参数: event(int):事件号,取值 0 ~ 33(EVENT_* 宏)。
返回:
MSG_NO_ERROR (0):成功;MSG_EVENT_PARAM_ERROR (-3):事件号越界(event >= ARRAY_SIZE(event2msg))。
线程安全: 可在中断上下文调用;内部使用 OS_ENTER_CRITICAL() 保护。
int post_msg(int argc, ...)
投递一条带参消息到环形缓冲。
参数:
argc(int):变参总数,含消息 ID 本身(argc = 1 + 参数个数);...:第一个变参为消息 ID(MSG_*),其余为随消息携带的int参数。
返回:
MSG_NO_ERROR (0):入队成功;MSG_BUF_NOT_ENOUGH (-4):环形缓冲剩余空间不足,消息被丢弃。
限制: 参数个数最多 15 个(4 bit 编码);消息 ID 须 < 0xFFF。
int get_msg(int len, int *msg)
从事件通道或消息队列取出一个消息。
参数:
len(int):调用方msg数组的容量(int 个数,须 ≥ 1 + 参数个数);msg(int *):输出缓冲区,msg[0]为消息 ID,msg[1..]为参数。
返回:
MSG_NO_ERROR (0):成功取出(可能来自事件映射或消息队列);MSG_NO_MSG (0):事件与消息均为空(此时msg[0] = NO_MSG,CPU 已进入 idle);MSG_BUF_NOT_ENOUGH (-4):调用方缓冲区容量小于消息参数个数,消息保留在队列中;MSG_CBUF_ERROR (-5):环形缓冲读取长度异常(数据损坏)。
bool get_event_status(u32 event)
查询某事件是否已发生(不消费)。
参数: event(u32):事件号。
返回: TRUE 表示事件位已置位,FALSE 表示未置位。
void clear_all_message(void)
清空消息队列(保留事件位图),用于模式切换等需要丢弃积压消息的场景(见 msg.c)。
返回码汇总(msg.h)
| 宏 | 值 | 含义 |
|---|---|---|
MSG_NO_ERROR | 0 | 操作成功 |
MSG_NO_MSG | 0 | 无消息(与成功共用 0,需结合 msg[0] == NO_MSG 判断) |
MSG_EVENT_EXIST | -1 | 事件已存在(预留) |
MSG_NOT_EVENT | -2 | 非事件(预留) |
MSG_EVENT_PARAM_ERROR | -3 | 事件号参数错误 |
MSG_BUF_NOT_ENOUGH | -4 | 缓冲区空间/容量不足 |
MSG_CBUF_ERROR | -5 | 环形缓冲错误 |
故障模式、边界情况与并发
事件通道
- 事件号越界:
post_event()对event >= ARRAY_SIZE(event2msg)直接返回MSG_EVENT_PARAM_ERROR,不会越界写位图;clear_one_event()同样有越界保护。 - 事件合并:同一事件在消费前多次投递只会置位一次,消费时也只处理一次——事件语义是「状态位」而非「计数」。若需要计数语义(如统计按键次数),应改用带参消息。
- 事件优先级:
get_event()用clz从字的高位向低位查找,实际返回的是事件号最小的置位事件。因此事件号的分配顺序隐含优先级:0 号事件(EVENT_F1A1_END)优先于 32 号(EVENT_EXTFLSH_IN)。新增事件时需考虑优先级编排。
消息通道
- 队列满即丢:
post_msg()在空间不足时返回MSG_BUF_NOT_ENOUGH并丢弃消息,不阻塞、不覆盖旧消息。生产方必须容忍消息丢失(例如对丢失敏感的操作应做重投/轮询兜底)。 - 消费者缓冲区过小:
get_msg()发现param_len > len - 1时返回MSG_BUF_NOT_ENOUGH,消息帧保留在环形缓冲中,等待下次以更大缓冲区读取;调用方应使用足够大的msg数组(典型为 8~16 个 int)。 - 空队列行为:事件为空且读不到完整头部时,
get_msg()调用__builtin_pi32_idle()使 CPU 进入 idle 并返回MSG_NO_MSG。MSG_NO_MSG与MSG_NO_ERROR同为 0,调用方必须通过msg[0] == NO_MSG区分「有消息」与「空」。 - 临界区与中断安全:
post_event/post_msg/get_msg均以OS_ENTER_CRITICAL()/OS_EXIT_CRITICAL()包裹(MSG_ENTER_CRITICAL宏),可安全地从 ISR 上下文投递;但消费(get_msg)只能在主循环上下文调用,因为 idle 指令与消息分发不具可重入性。 - 单消费者约束:机制设计为单消费者模型,若多个任务同时调用
get_msg()会破坏消息顺序与事件消费的原子性,SDK 中由主循环唯一持有消费权。
数据损坏防护
get_msg()对参数区读取长度做严格校验(rlen != param_len * sizeof(int)时返回MSG_CBUF_ERROR),用于尽早暴露环形缓冲被破坏或写入端溢出等问题;- 头部 12/4 位域划分使得消息 ID 与参数个数互不干扰,错误编码会在解析阶段被长度校验拦截。
性能与运维
- 时间复杂度:
post_event为 O(1) 置位;get_event用硬件clz指令,最多扫描EVENT_TOTAL(当前 2)个字;post_msg/get_msg为 O(参数个数) 内存拷贝。整体满足高频主循环轮询要求。 - 内存占用:消息池固定 512 字节(
MAX_POOL=128u32),事件位图 2 个字,无堆分配、无碎片,适合 MCU 的静态内存布局。 - 功耗:空队列时 CPU 自动进入 idle,是待机电流优化的关键;任何
post_event/post_msg都来自中断上下文(解码器回调、设备驱动、定时器),天然具备唤醒 CPU 的能力。 - 调试:
msg.c中保留了log_info("event 0x%x\n", event)、log_info("msg 0x%x\n", t_msg[0])等被注释的日志点,排查消息丢失时可按需打开;common_msg_deal内大量使用log_info输出音量、模式切换、设备插拔等关键状态。 - 容量调优:若出现频繁
MSG_BUF_NOT_ENOUGH丢消息,可增大MAX_POOL;若事件超过 64 个,需同步扩展event2msg[]表(EVENT_TOTAL会自动推导)。
扩展点
- 新增普通消息:在
msg.h的MSG_*枚举中追加(保持 < 0xFFF),消费端在common_msg_deal或对应工作模式处理函数中新增 case 分支即可。注意MSG_COMMON_MAX之后、NO_MSG之前的区间属于系统保留,MSG_CHANGE_WORK_MODE = 0x600与MSG_F1A1_FILE_END = 0x800之后为库使用段,勿改动。 - 新增事件:三步完成——在
msg.h中定义EVENT_XXX宏(按优先级编排事件号);在msg.c的event2msg[]表中对应下标填入目标消息 ID(无对应消息填NO_MSG);生产者调用post_event(EVENT_XXX)。EVENT_TOTAL与越界检查自动适配。 - 新增消费处理器:消息分发入口是
common_msg_deal()(公共/设备/按键消息)与各工作模式的处理函数;可仿照MSG_NEXT_WORKMODE分支,在处理后通过post_msg(1, MSG_CHANGE_WORK_MODE)触发模式迁移,形成可组合的消息链。 - 带参消息:任何需要携带数据的通信(如
MSG_MUSIC_PLAY_NEW_FILE携带文件号)都通过post_msg(argc, MSG_XXX, p0, p1, ...)传递,消费端从msg[1..]读取。
测试与验证
仓库未提供针对消息引擎的独立单元测试(嵌入式 SDK 通常以板级联调验证),但可通过以下既有证据验证机制行为:
common_msg.c的common_msg_deal覆盖了定时(MSG_500MS)、按键(MSG_0~MSG_9)、音量、设备插拔、升级、软关机等多类消息分支,是最完整的消费端回归用例;decoder_msg_tab.c(sdk/app/bsp/common/decoder/)展示了解码器侧如何按格式注册/处理解码相关消息,可交叉验证事件映射表(EVENT_*_END/ERR/LOOP)与解码器消息的一致性;- 边界行为(队列满、事件越界、空队列 idle、
NO_MSG返回)在msg.c源码中有明确的返回码路径,可通过在post_msg满池后观察返回-4、在空闲时观察msg[0] == NO_MSG来验证。
相关链接
- 核心引擎实现:msg.c(事件位图、消息队列、
post_event/post_msg/get_msg) - API 与常量定义:msg.h(
MSG_*枚举、EVENT_*宏、位域与返回码) - 消费端示例:common_msg.c(
common_msg_deal消息分发与事件联动) - 解码器消息表:decoder_msg_tab.c(解码事件/消息的注册与映射)
- 相关机制(兄弟页面):按键键值映射(key)、电源管理(app_power_mg)、工作模式状态机(toy_main / app_*)