消息调度与通信外设
消息调度与通信外设是 AW31N BLE SDK 中负责系统级消息(Message)与事件(Event)投递、排队、调度和消费的公共基础模块,核心实现在 msg.c / msg.h,并通过事件池(sys_event.c)与低功耗框架(REGISTER_LP_TARGET / DEEPSLEEP_TARGET_REGISTER)深度集成。
Purpose and Scope(目的与范围)
本文档完整覆盖该模块的端到端机制:
- 消息编码格式:16 位消息头(12 位类型 + 4 位参数个数)与消息 ID 分区设计;
- 消息队列:基于环形缓冲区(
msg_cbuf)的消息池,生产者/消费者模型; - 事件位图调度:
event_buf位图 + CLZ 指令实现的静态优先级事件调度; - 投递与消费 API:
post_msg/post_event/get_msg/get_msg_phy的完整实现与错误码语义; - 特殊消息路径:
MSG_TYPE_EVENT携带sys_event*与dev->ops指针的双指针扩展; - 生命周期与低功耗集成:
message_init初始化、低功耗 idle 查询、深睡唤醒复位; - 与通信外设的消息通路:UART 测试盒升级(
MSG_UART_TESTBOX_UPDATE_START)、BLE 连接/断开事件(0x900段)等外设事件如何进入统一调度。
以下内容属于兄弟页面,不在本文展开:UART/SPI/I2C 底层驱动的寄存器级实现、BLE 协议栈内部状态机、具体 APP 业务逻辑(如音乐播放器的 MSG_PP 处理细节)。本文只覆盖"消息调度"这一公共机制及其与外设事件接入的边界。
Overview(概述)
在嵌入式 BLE SoC 固件中,按键任务、解码器、设备热插拔检测、BLE 协议栈、升级模块等众多异步生产者(可运行在中断上下文或不同任务中)都需要向 APP 主逻辑投递事件。该模块提供了一种单消费者、无锁(关中断临界区)、静态优先级的消息/事件调度机制:
- 普通消息(
post_msg)走环形缓冲区,按 FIFO 顺序消费,支持最多 15 个int参数; - 系统事件(
post_event)走位图,用 CLZ(Count Leading Zeros)指令在常数时间内取出优先级最高的挂起事件,再通过event2msg[]静态表映射为消息 ID; - 两种路径共享同一个忙碌状态字
msg_busy_state,它同时是低功耗框架判断"消息子系统是否可休眠"的依据。
设计意图(WHY):
- 中断安全:所有队列/位图操作都包在
OS_ENTER_CRITICAL()/OS_EXIT_CRITICAL()中,中断服务程序可以直接调用post_msg/post_event,无需等待互斥锁; - 优先级确定性:事件位图索引即优先级(索引越小优先级越高),CLZ 保证
get_msg每次都先取到最高优先级事件,避免事件饿死; - 零动态内存:消息池是编译期分配的
u32 msg_pool[MAX_POOL],事件位图是静态u32 event_buf[],杜绝堆碎片; - 功耗门控:
msg_busy_state非零表示还有待处理消息,低功耗控制器据此阻止进入睡眠;深睡唤醒时调用message_init()丢弃陈旧消息,保证唤醒后状态一致。
Architecture(架构)
flowchart TD
subgraph sg_Producer["消息生产者(多上下文)"]
KEY["按键/编码器任务"]
BLE["BLE 协议栈"]
DEC["解码器 F1A1/MP3/WAV/OPUS"]
DEV["设备检测 USB/SD/AUX/OTG"]
UART["UART 测试盒升级"]
end
subgraph sg_Core["消息调度核心 msg.c"]
POST["post_msg / post_event"]
CBUF["环形缓冲区 msg_cbuf<br/>msg_pool[32]"]
EVBUF["事件位图 event_buf[]"]
BUSY["忙碌状态 msg_busy_state<br/>BIT0=缓冲忙 BIT1=事件忙"]
EVMAP["event2msg[] 静态映射表"]
end
subgraph sg_Consumer["消息消费者"]
GET["get_msg / get_msg_sub / get_msg_phy"]
APP["APP 主循环/任务"]
end
subgraph sg_Power["低功耗框架"]
LP["REGISTER_LP_TARGET 查询 is_idle"]
DS["DEEPSLEEP_TARGET_REGISTER 唤醒复位"]
end
KEY --> POST
BLE --> POST
DEC --> POST
DEV --> POST
UART --> POST
POST --> CBUF
POST --> EVBUF
POST --> BUSY
EVBUF --> EVMAP
CBUF --> GET
EVMAP --> GET
GET --> APP
BUSY --> LP
DS --> POST
DS --> EVBUF
架构说明:左侧是各类异步生产者,它们只依赖 post_msg / post_event 两个入口,因此与调度核心解耦;中间是核心,普通消息与事件分别进入环形缓冲和位图,msg_busy_state 统一反映两条路径的待处理状态;右侧 get_msg 是唯一的消费出口(单消费者模型),APP 主循环轮询调用;底部低功耗框架通过 is_idle 回调读取忙碌状态门控睡眠,深睡唤醒通过 .exit 回调调用 message_init() 复位整个子系统。
核心实现分析
消息 ID 定义与分区
所有消息 ID 在 msg.h 中以枚举定义,按功能分区,数值区间承载了语义:
| 数值区间 | 用途 | 典型消息 |
|---|---|---|
| 0x0000–0x000F | 系统通用 | MSG_0~MSG_9、MSG_PP_2、MSG_RECODE_START/END |
| 0x0010 起 | APP 级 | MSG_500MS、MSG_APP_SWITCH_ACTIVE、MSG_LOW_POWER、MSG_ENTER_IDLE、MSG_POWER_OFF |
| — | 音乐/录音/MIDI/RF | MSG_PP、MSG_NEXT_FILE、MSG_REC_MODE_SWITCH、MSG_MIDICTRL_*、MSG_SENDER_START 等 |
| 0x0600 起 | mbox 音乐控制 | MSG_CHANGE_WORK_MODE、MSG_MUSIC_PLAY_NEW_FILE、MSG_NEXT_WORKMODE |
| 0x0610 起 | 均衡器/播放模式 | MSG_MUSIC_PREV_EQ、MSG_NEXT_PLAYMODE |
| 0x0620 起 | FM 收音机 | MSG_FM_NEXT_STEP、MSG_FM_SCAN_ALL_UP、MSG_CH_SET |
| 0x0630 起 | 系统/时钟 | MSG_REQUEST_Y/N、MSG_INPUT_TIMEOUT、MSG_READ_CLOCK、MSG_ALARM |
| 0x0640 起 | 设备热插拔 | MSG_USB_DISK_IN/OUT、MSG_SDMMCA_IN/OUT、MSG_AUX_IN/OUT、MSG_PC_MUTE |
| 0x0800 起 | 解码器回执 | MSG_F1A1_FILE_END/ERR/LOOP、MSG_MP3_FILE_END、MSG_WAV_LOOP、MSG_OPUS_FILE_ERR |
| 0x0900 起 | BLE/升级 | MSG_BLE_CONNECT_COMPLETE、MSG_BLE_APP_UPDATE_START、MSG_UART_TESTBOX_UPDATE_START |
MSG_TYPE_EVENT | 事件消息 | 携带 sys_event* 与 dev->ops 两个指针 |
NO_MSG = 0x0fff | 空哨兵 | 无消息时的填充值 |
关键设计:解码器回执(0x800 段)和 BLE 事件(0x900 段)的注释明确标注"库会使用到,不能更改",说明数值稳定性是 ABI 约束;设备插入/拔出段(0x640 起)注释"以下顺序不可随意调整",因为事件位图索引 EVENT_* 宏与其顺序一一对应。
消息头编码与缓冲池布局
消息头为 16 位定长,位域由 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)
#define MAX_POOL 32//128
#define NO_EVENT 0xffff
来源:msg.h
头布局为:bit[15:4] 消息类型(12 位),bit[3:0] 参数个数(4 位,最多 15 个参数)。MAX_POOL 当前为 32(注释保留历史值 128),即消息池为 32 个 u32(128 字节)。
存储结构在 msg.c 中静态分配:
NOT_KEEP_RAM
static cbuffer_t msg_cbuf;
NOT_KEEP_RAM
static u32 event_buf[EVENT_TOTAL];
NOT_KEEP_RAM
static u32 msg_pool[MAX_POOL];
#define MSG_BUF_BUSY_BIT BIT(0)
#define MSG_EVENT_BUSY_BIT BIT(1)
static u8 msg_busy_state; //记录msg的状态
来源:msg.c
msg_cbuf:cbuffer_t环形缓冲区,底层存储为msg_pool,承载普通消息的头部与参数;event_buf[EVENT_TOTAL]:事件位图,EVENT_TOTAL = (1+(sizeof(event2msg)/2 -1)/32),即按event2msg[]表项数向上取整到 32 位字的个数;msg_busy_state:两个忙位——MSG_BUF_BUSY_BIT表示缓冲中还有未消费消息,MSG_EVENT_BUSY_BIT表示还有挂起事件;两者都清零时子系统才允许休眠。
文件顶部通过 #pragma bss_seg(".msg.data.bss")、#pragma code_seg(".msg.text") 等指令把消息模块的变量与代码放入专用内存段,便于链接器布局与低功耗管理(NOT_KEEP_RAM 表示该段在深睡时可被丢弃/复位)。
事件位图调度算法
event2msg[] 静态表(msg.c)把"事件索引"映射为"消息 ID",例如索引 0→MSG_F1A1_FILE_END、36→MSG_BLE_APP_UPDATE_START。取出事件时用 CLZ 指令直接定位最高优先级的置位比特(索引最小即优先级最高):
_NOINLINE_
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();
/* log_info(" has event 0x%x\n",i*32 + (31 - event_cls)); */
return i * 32 + (31 - event_cls);
}
}
OS_EXIT_CRITICAL();
return NO_EVENT;
}
来源:msg.c
该实现是典型的静态优先级调度:get_event 从低到高扫描位图字,CLZ 返回最高位位置(31 - clz 即事件索引),因此 event2msg[] 中排在前面的事件(如解码器文件结束)永远优先于排在后面的事件被消费。整个查找在最坏情况下只需扫描 EVENT_TOTAL 个字,且单字查找是常数时间。
Core Flow(核心流程)
sequenceDiagram
participant P as 生产者(中断/任务)
participant POST as post_msg / post_event
participant Q as 环形缓冲 msg_cbuf
participant E as 事件位图 event_buf
participant G as get_msg / get_msg_sub
participant A as APP 主循环
P->>POST: post_msg(2, MSG_PP, param)
POST->>POST: 关中断,组装 16bit 头
POST->>Q: 写入头部 + 参数
POST->>POST: 置 MSG_BUF_BUSY_BIT
POST-->>P: MSG_NO_ERROR
P->>POST: post_event(EVENT_MP3_END)
POST->>E: event_buf[event/32] |= BIT(event%32)
POST->>POST: 置 MSG_EVENT_BUSY_BIT
POST-->>P: MSG_NO_ERROR
A->>G: get_msg(len, msg)
G->>E: get_event() 用 CLZ 找最高优先级置位
E-->>G: 事件索引
G->>G: event2msg[索引] -> msg[0],清位
G->>Q: 读 2 字节头 + 参数
Q-->>G: 消息内容
G-->>A: MSG_NO_ERROR
消费侧两条路径的汇合点在 get_msg_sub(msg.c):事件优先于普通消息——先查 get_event(),有挂起事件则立即清位并映射返回;无事件时才调用 get_msg_phy 从环形缓冲取普通消息。同时 get_msg 入口(msg.c)先检查 msg_busy_state,若两条路径都空闲则直接返回 MSG_NO_MSG,避免无谓的临界区开销——这是"忙碌状态字同时充当快速路径开关"的设计。
投递路径实现
post_msg:变参消息写入
post_msg 是变参函数,签名 int post_msg(int argc, ...):第一个参数是"参数个数 + 1"(含消息 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);
int msg = va_arg(argptr, int);
size_t additional_size = 0;
if (msg == MSG_TYPE_EVENT) {
additional_size += 2 * sizeof(void *); // for sys_event* and dev->ops
}
if (!cbuf_is_write_able(&msg_cbuf, (argc - 1) * sizeof(int) + additional_size + MSG_HEADER_BYTE_LEN)) {
OS_EXIT_CRITICAL();
va_end(argptr);
putchar('f');
return MSG_BUF_NOT_ENOUGH;
}
t_msg[0] = (MSG_HEADER_ALL_BIT >> MSG_PARAM_BIT_LEN) & msg;
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));
}
if (msg == MSG_TYPE_EVENT) {
void *event_ptr = va_arg(argptr, void *);
void *ops_ptr = va_arg(argptr, void *);
cbuf_write(&msg_cbuf, &event_ptr, sizeof(void *));
cbuf_write(&msg_cbuf, &ops_ptr, sizeof(void *));
}
va_end(argptr);
#ifdef CONFIG_DEBUG_ENABLE
if (msg_remain_min > msg_cbuf_remain()) {
msg_remain_min = msg_cbuf_remain();
}
#endif
msg_busy_state |= MSG_BUF_BUSY_BIT;
OS_EXIT_CRITICAL();
return MSG_NO_ERROR;
}
来源:msg.c
要点:
- 头编码:
t_msg[0] = (MSG_HEADER_ALL_BIT >> MSG_PARAM_BIT_LEN) & msg;取消息 ID 的低 12 位作为类型,再((argc-1) << MSG_TYPE_BIT_LEN)把参数个数放进高 4 位(注意端序上参数个数落在bit[15:12]语义位); - 容量预检:
cbuf_is_write_able提前计算"参数 + 可能的双指针 + 2 字节头"是否放得下,放不下时打印字符'f'(fail)并返回MSG_BUF_NOT_ENOUGH,全程在临界区内,保证不出现半写状态; - MSG_TYPE_EVENT 扩展:当消息 ID 为
MSG_TYPE_EVENT时,参数区之后还会写入sys_event*与dev->ops两个指针,这是事件消息与普通 int 参数消息的结构性区别; - 调试统计:
CONFIG_DEBUG_ENABLE下维护msg_remain_min记录缓冲剩余空间的历史最小值,用于运行时定位消息积压。
post_event:位图事件置位
int post_event(int event)
{
if (event >= ARRAY_SIZE(event2msg)) {
return MSG_EVENT_PARAM_ERROR;
}
CPU_SR_ALLOC();
OS_ENTER_CRITICAL();
event_buf[event / 32] |= BIT(event % 32);
msg_busy_state |= MSG_EVENT_BUSY_BIT;
OS_EXIT_CRITICAL();
return MSG_NO_ERROR;
}
来源:msg.c
post_event 的事件参数是 event2msg[] 的表索引(即 EVENT_* 宏),而非消息 ID。越界索引返回 MSG_EVENT_PARAM_ERROR。与 post_msg 不同,事件不占环形缓冲,天然去重(同一事件重复置位只保留一个位),适合"解码器文件结束"这类可能高频触发的信号。
MSG_TYPE_EVENT 双指针消费路径
get_msg_phy(msg.c)在读完头部和参数后,若 msg[0] == MSG_TYPE_EVENT,会继续从缓冲读出两个指针并分别放入 msg[param_len+1] 与 msg[param_len+2],把"消息 + 事件对象 + 设备操作表"打包成一次取出的完整上下文。这样 APP 侧拿到事件消息后无需再做二次查找,即可同时获得 sys_event 实例与对应设备的 ops 函数表——这是"通信外设"接入的核心通道:外设事件(如 UART 升级请求)以 sys_event 形式投递,dev->ops 指向该外设驱动的操作接口。
flowchart LR
subgraph sg_Header["消息头 16bit"]
T["bit[15:4] 消息类型 12bit"]
N["bit[3:0] 参数个数 4bit"]
end
subgraph sg_Event["MSG_TYPE_EVENT 扩展"]
P1["参数区 param[0..n-1]"]
P2["sys_event* 指针"]
P3["dev->ops* 指针"]
end
T --- N
N --- P1
P1 --> P2
P2 --> P3
生命周期与低功耗集成
stateDiagram-v2
[*] --> IDLE
IDLE --> BUF_BUSY: post_msg 写入缓冲
IDLE --> EVENT_BUSY: post_event 置位事件
BUF_BUSY --> IDLE: get_msg 消费完毕
EVENT_BUSY --> IDLE: get_msg 消费完毕
IDLE --> SLEEP: 低功耗查询 is_idle==true
SLEEP --> IDLE: 深睡唤醒 exit 回调<br/>message_init() 复位
- 初始化:
message_init()(msg.c)清零事件位图、以msg_pool为底初始化环形缓冲、复位忙碌状态字,全程关中断;系统启动与深睡唤醒都会调用它。 - 低功耗门控:
REGISTER_LP_TARGET(msg_lowpower_target)(msg.c)注册名为"msg_lowpwer_deal"的低功耗目标,is_idle回调返回!msg_busy_state——只要还有未消费消息或事件,低功耗控制器就不允许进入睡眠,保证待处理消息不会丢失。 - 深睡恢复:
DEEPSLEEP_TARGET_REGISTER(sys_msg_sleep)(msg.c)注册.exit = msg_exit_leep,唤醒时调用message_init()丢弃唤醒前的陈旧消息并复位状态,避免深睡期间残留的中断消息污染唤醒后的业务逻辑。注意msg_exit_leep的拼写是源码中的原样(leep),属历史命名。
使用示例
示例 1:消费侧主循环取消息
APP 主循环典型地循环调用 get_msg,收到 MSG_NO_MSG 即空闲(源码中 get_msg 的忙碌状态快速路径):
int get_msg(int len, int *msg)
{
if (!msg_busy_state) {
msg[0] = NO_MSG;
return MSG_NO_MSG;
}
return get_msg_sub(len, msg);
}
来源:msg.c
示例 2:投递普通消息
生产者(按键/解码器/外设驱动)投递"播放/暂停"类消息,第一个参数为消息 ID,其后携带业务参数:
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);
int msg = va_arg(argptr, int);
size_t additional_size = 0;
if (msg == MSG_TYPE_EVENT) {
additional_size += 2 * sizeof(void *); // for sys_event* and dev->ops
}
if (!cbuf_is_write_able(&msg_cbuf, (argc - 1) * sizeof(int) + additional_size + MSG_HEADER_BYTE_LEN)) {
OS_EXIT_CRITICAL();
va_end(argptr);
putchar('f');
return MSG_BUF_NOT_ENOUGH;
}
t_msg[0] = (MSG_HEADER_ALL_BIT >> MSG_PARAM_BIT_LEN) & msg;
t_msg[0] = ((argc - 1) << (MSG_TYPE_BIT_LEN)) | t_msg[0];
cbuf_write(&msg_cbuf, (void *)&t_msg[0], MSG_HEADER_BYTE_LEN);
...
}
来源:msg.c
调用形态如 post_msg(2, MSG_PP, 0):argc=2 表示"消息 ID + 1 个参数",MSG_PP 写入 16 位头,参数 0 紧随其后;中断服务程序中同样可以安全调用。
示例 3:消费侧解析消息头与参数
get_msg_phy 展示了完整解析:先读 2 字节头,拆出类型与参数个数,再按个数读参数,最后处理 MSG_TYPE_EVENT 的双指针:
u32 tlen = cbuf_read(&msg_cbuf, (void *)t_msg, MSG_HEADER_BYTE_LEN);
if (MSG_HEADER_BYTE_LEN != tlen) {
msg_busy_state &= (~MSG_BUF_BUSY_BIT);
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)) {
OS_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) {
OS_EXIT_CRITICAL();
return MSG_CBUF_ERROR;
}
来源:msg.c
注意:当缓冲为空时,get_msg_phy 会清除 MSG_BUF_BUSY_BIT 并返回 MSG_NO_MSG,msg[0] 置为 NO_MSG(0x0fff)——这是忙碌状态字自动复位的关键路径。
配置选项
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
MAX_POOL | 宏 | 32(注释保留历史值 128) | 消息缓冲池大小(u32 个数),决定环形缓冲容量与可排队的普通消息数 |
MSG_HEADER_BYTE_LEN | 宏 | 2 | 消息头字节数,头编码的基础 |
MSG_TYPE_BIT_LEN | 宏 | 12 | 消息类型位宽,MSG_PARAM_BIT_LEN 由 16-12=4 推导,决定最大参数个数 15 |
EVENT_TOTAL | 宏 | (1+(sizeof(event2msg)/2-1)/32) | 事件位图所需 u32 字数,随 event2msg[] 表自动推导 |
NO_EVENT | 宏 | 0xffff | get_event 无事件时的返回值哨兵 |
CONFIG_DEBUG_ENABLE | 编译宏 | 关闭 | 开启后维护 msg_remain_min 历史最低剩余容量,并启用 msg_debug_info() 输出 |
LOG_TAG_CONST | 宏 | OFF | 消息模块日志开关,置 NORM 可打开 [msg] 日志 |
API Reference
| 函数 | 签名 | 说明 |
|---|---|---|
post_msg | int post_msg(int argc, ...) | 投递普通消息;argc 为参数个数 +1(含消息 ID);返回 MSG_NO_ERROR 或 MSG_BUF_NOT_ENOUGH |
post_event | int post_event(int event) | 投递系统事件;event 为 event2msg[] 表索引(EVENT_* 宏);越界返回 MSG_EVENT_PARAM_ERROR |
get_msg | int get_msg(int len, int *msg) | 消费消息;空闲时直接返回 MSG_NO_MSG 且 msg[0]=NO_MSG |
get_msg_phy | int get_msg_phy(int len, int *msg, bool idle) | 物理读取环形缓冲;len 为调用方缓冲容量(int 个数),防止参数溢出 |
get_event_status | bool get_event_status(u32 event) | 查询指定事件是否挂起(关中断读位图) |
clear_one_event | void clear_one_event(u32 event) | 清除指定事件的挂起位 |
has_sys_event | bool has_sys_event(void) | 是否有任一事件挂起 |
clear_all_message | void clear_all_message(void) | 清空环形缓冲(不清事件位图) |
message_init | void message_init() | 复位整个消息子系统:清位图、初始化缓冲、清忙碌状态;启动与深睡唤醒调用 |
msg_debug_info | void msg_debug_info(void) | 打印 msg_remain_min(仅 CONFIG_DEBUG_ENABLE) |
event_pool_alloc | struct sys_event *event_pool_alloc() | 从事件池分配 sys_event(实现在 sys_event.c) |
event_pool_free | void event_pool_free(struct sys_event *event_ptr) | 归还事件对象到事件池 |
event_pool_init | void event_pool_init() | 初始化事件池 |
错误码
定义于 msg.h:
| 错误码 | 值 | 触发场景 |
|---|---|---|
MSG_NO_ERROR / MSG_NO_MSG | 0 | 成功 / 无消息可消费 |
MSG_EVENT_EXIST | -1 | 事件已存在(预留) |
MSG_NOT_EVENT | -2 | 非事件(预留) |
MSG_EVENT_PARAM_ERROR | -3 | post_event 索引越界 |
MSG_BUF_NOT_ENOUGH | -4 | 环形缓冲空间不足(post_msg)或调用方缓冲过小(get_msg_phy) |
MSG_CBUF_ERROR | -5 | 环形缓冲读取长度不一致(数据损坏) |
故障模式与边界情况
| 场景 | 行为 | 源码依据 |
|---|---|---|
环形缓冲满(post_msg) | 返回 MSG_BUF_NOT_ENOUGH 并打印 'f',消息丢弃,调用方需自行重试或忽略 | msg.c L277-L282 |
调用方缓冲过小(get_msg_phy) | 返回 MSG_BUF_NOT_ENOUGH,已读的头不退还,下一次 get_msg 会读到同一个头——要求调用方保证 len 足够大 | msg.c L180-L184 |
| 缓冲数据不一致 | cbuf_read 实际读长度与期望不符,返回 MSG_CBUF_ERROR(数据损坏或并发写穿) | msg.c L185-L189 |
| 事件索引越界 | post_event 返回 MSG_EVENT_PARAM_ERROR,位图不写入 | msg.c L251-L253 |
| 队列与事件都为空 | get_msg 快速路径返回 MSG_NO_MSG,msg[0]=NO_MSG | msg.c L242-L245 |
| 深睡唤醒 | .exit 回调 message_init() 清空全部缓冲与位图,唤醒后旧消息不再可见 | msg.c L344-L355 |
| 待处理消息时请求休眠 | is_idle = !msg_busy_state 返回 false,低功耗控制器推迟睡眠 | msg.c L334-L342 |
并发与一致性
- 原子性:
post_msg、post_event、get_msg*、clear_one_event、get_event_status、message_init全部以OS_ENTER_CRITICAL()/OS_EXIT_CRITICAL()包裹临界区,因此中断上下文与任务上下文都能安全调用,无需互斥锁或信号量; - 单消费者:
get_msg是唯一消费出口(设计上 APP 侧单任务轮询),天然规避多读者竞争;若未来引入多消费者,需要新增消费端互斥; - 事件去重:位图置位天然合并重复事件,高频信号(如文件结束)不会撑爆缓冲,但会丢失中间计数——语义上属于"最新状态"而非"事件计数";
- 事件优先级:低索引事件优先,属于静态优先级;普通消息之间是严格 FIFO,事件与消息之间事件优先(
get_msg_sub先查位图); - 无锁数据结构风险:临界区只保护单次操作,若调用方在
get_msg与下一次get_msg之间长期不消费,生产者的cbuf_is_write_able会持续失败——缓冲满时的消息丢弃是显式策略(打印'f'),上层需要据此实现重试或告警。
性能与运维
- 取事件 O(1):CLZ 单指令定位最高优先级事件,位图扫描最坏为
EVENT_TOTAL次循环(当前约 2 个u32),极低成本; - 临界区短:
post_msg的临界区仅含容量判断与cbuf_write批量拷贝,不触发任何回调,适合在中断服务程序中直接调用; - 零动态分配:缓冲池、位图、忙碌状态均为编译期静态内存,运行期无 malloc/free,无堆碎片风险;
- 调试手段:
CONFIG_DEBUG_ENABLE下msg_remain_min记录历史最低剩余容量,msg_debug_info()打印该值——若持续接近 0 说明消息积压,应排查生产者频率或增加MAX_POOL(注意MAX_POOL从 128 被调小到 32,属于内存与吞吐的权衡点); - 内存段约束:模块数据置于
.msg.data/.msg.data.bss专用段,NOT_KEEP_RAM变量在深睡时可不保留,链接时需保证该段与低功耗唤醒复位流程匹配。
扩展点
- 新增普通消息:在
msg.h枚举中追加 ID(避开已占用区间,且勿改动0x800/0x900段"库会使用"的既有值),即可被post_msg投递; - 新增系统事件:在
event2msg[]表尾追加"事件索引 → 消息 ID"映射,同时在msg.h定义对应的EVENT_XXX宏;EVENT_TOTAL自动推导,无需手工修改位图大小; - 接入新外设事件:复用
MSG_TYPE_EVENT路径——投递时附带sys_event*与dev->ops*,消费侧从msg[param_len+1]/msg[param_len+2]直接取得事件对象与设备操作表; - 事件对象生命周期:
event_pool_alloc/event_pool_free/event_pool_init由sys_event.c实现,分配与归还配对使用,避免MSG_TYPE_EVENT消息中的指针悬挂; - 低功耗协同:可通过
REGISTER_LP_TARGET注册其他模块的 idle 查询,与msg_busy_state一起参与睡眠裁决;深睡目标可通过DEEPSLEEP_TARGET_REGISTER注册各自的.enter/.exit回调。
测试情况
在本次探索预算内未发现针对 msg.c 的独立单元测试文件。模块的运行时正确性主要依赖:CONFIG_DEBUG_ENABLE 统计(msg_remain_min)、低功耗框架的 is_idle 断言(忙碌状态驱动睡眠裁决),以及 get_msg / get_msg_phy 对 MSG_CBUF_ERROR 等错误码的显式返回——这些错误码设计本身即为可测试契约。建议的验证方式:在 post_msg 满队列、事件高优先级抢占、深睡唤醒丢弃等边界场景下做压力与状态机验证。
相关链接
- msg.c(核心实现)
- msg.h(消息定义与 API 声明)
- sys_event.c(事件池实现,与本文档
event_pool_*API 对应) - 通信外设底层细节(UART/SPI 寄存器级驱动)与 BLE 协议栈状态机请参见对应外设/协议栈目录的独立页面;
- 低功耗框架的
REGISTER_LP_TARGET/DEEPSLEEP_TARGET_REGISTER机制请参见低功耗管理相关页面。