消息事件机制
AC63 BT SDK 中应用任务(App Task)与各业务模块之间进行异步通信的消息/事件机制,涵盖用户自定义消息、按键消息的发送与获取接口,以及按键消息映射的配置存储。它基于底层 RTOS 消息队列,为上层应用提供解耦、事件驱动的通信模型。
Purpose and Scope
本页文档面向 AC63 BT SDK 的 app 级消息事件机制,即 include_lib/system/app_msg.h 所声明的整套接口及其背后的队列模型:
app_task_put_usr_msg()— 用户自定义消息发送(支持可变参数)app_task_get_msg()— 消息获取(支持阻塞/非阻塞两种模式)app_task_put_key_msg()/app_task_clear_key_msg()— 按键消息投递与清理- 按键消息映射的系统配置存储(
CFG_KEY_MSG_ID)与配置工具脚本(key_msg.lua)
以下内容属于其他能力,不在本页展开(可参考对应源文件):
- OS 内核消息队列(
os_msg_*系列):位于预编译库与include_lib/system/os/中,本页只描述其上层封装语义,不展开内核实现。 - SIG Mesh 子系统消息(
apps/common/third_party_profile/sig_mesh/msg.h、delayable_msg.c、proxy_msg.c):Mesh 协议自身的消息处理,属于独立子系统。 - RCSP 协议消息(
apps/common/third_party_profile/jieli/JL_rcsp/rcsp_msg.h):杰理私有通信协议的消息定义。
说明:
app_msg.h的实现在 SDK 中以预编译库形式提供,仓库中只有头文件声明与调用方。本文档以头文件声明的语义为准,凡涉及内部实现细节处均明确标注"实现细节未在源码中体现"。
Overview
为什么需要消息事件机制
在 AC63 这类单芯片蓝牙音频 SoC 上,系统运行在 RTOS 之上,存在多个任务(App Task、音频任务、协议栈任务等)以及大量中断回调。若各模块直接互相调用,会带来以下问题:
- 耦合:业务模块需要知道对方的存在与调用时机;
- 异步事件处理:按键扫描、外设状态变化等事件发生在不确定的上下文(中断或低优先级任务)中,无法直接驱动 UI/应用逻辑;
- 竞态:多任务直接读写共享状态容易产生竞争。
消息事件机制将"事件产生"与"事件处理"解耦:任何模块只需把消息投递到 App Task 的消息队列,由 App Task 主循环统一取出并分发。这是典型的生产者-消费者 + 事件驱动模型,也是嵌入式系统中最常见的任务间通信范式。
关键概念与术语
| 术语 | 含义 |
|---|---|
| App Task | 应用主任务,消息的唯一消费者,运行主循环并分发消息 |
| 消息队列 | App Task 内部持有的 FIFO 队列,消息先入先出 |
| 消息 ID(msg) | 标识消息类型的整数值,由应用/系统约定 |
| 参数(arg_num, ...) | 消息携带的附加数据,通过变参传递 |
| block | app_task_get_msg 的阻塞模式:0 = 内部 pend 挂起等待,1 = 非阻塞直接返回 |
| pend | RTOS 术语,任务因等待资源(此处为消息)而挂起休眠,直到条件满足被唤醒 |
接口一览
app_msg.h 声明的全部接口如下(完整代码见 app_msg.h):
//app自定义消息发送接口
int app_task_put_usr_msg(int msg, int arg_num, ...);
//app消息获取接口(block参数为0表示内部pend,1直接返回)
void app_task_get_msg(int *msg, int msg_size, int block);
//app按键消息发送接口
int app_task_put_key_msg(int msg, int value);
//app清理按键消息接口
void app_task_clear_key_msg();
Source: app_msg.h
Architecture
下图展示消息事件机制的整体架构:左侧是各类事件产生者(生产者),中间是 app 消息层与消息队列,右侧是唯一的消费端(App Task 主循环),下方是按键消息映射的配置来源。
flowchart TD
subgraph sg_Producers["事件产生者 (Producers)"]
KeyScan["按键扫描/解码模块"]
UserCode["业务模块/中断回调"]
SysSvc["系统服务"]
end
subgraph sg_AppMsg["app 消息层 (app_msg.h)"]
PutUsr["app_task_put_usr_msg"]
PutKey["app_task_put_key_msg"]
ClearKey["app_task_clear_key_msg"]
GetMsg["app_task_get_msg"]
Queue[("App Task 消息队列")]
end
subgraph sg_Consumer["消费端 (Consumer)"]
AppLoop["App Task 主循环"]
Dispatch["消息分发/处理"]
end
subgraph sg_Config["按键消息配置"]
CfgID["CFG_KEY_MSG_ID (606)"]
KeyLua["key_msg.lua 配置脚本"]
end
KeyScan -->|"按键事件"| PutKey
UserCode -->|"业务事件"| PutUsr
SysSvc -->|"系统事件"| PutUsr
PutUsr -->|"入队"| Queue
PutKey -->|"入队"| Queue
ClearKey -.->|"清除残留按键消息"| Queue
Queue -->|"出队"| GetMsg
GetMsg -->|"msg + 参数"| AppLoop
AppLoop --> Dispatch
KeyLua --> CfgID
CfgID -->|"持久化按键映射"| PutKey
架构说明
- 事件产生者(多):按键扫描模块、业务模块、系统服务都可以调用
app_task_put_usr_msg/app_task_put_key_msg向队列投递消息。它们之间互不依赖,这是消息机制解耦价值的体现。 - 消息层(中间):
app_msg.h提供 4 个接口,屏蔽了底层 OS 消息队列(os_msg_*,预编译库)的细节。app_task_clear_key_msg以虚线表示,因为它不是"入队"而是"出队/清理"操作。 - 消费端(唯一):App Task 主循环调用
app_task_get_msg取消息,再按消息 ID 分发到对应处理函数。单一消费者保证了队列访问无需加锁,天然避免竞态。 - 配置层(底部):按键消息的映射关系通过配置工具(
key_msg.lua)生成,并以CFG_KEY_MSG_ID = 606作为系统配置项持久化(见 syscfg_id.h),运行时按键消息的投递行为由该映射决定。
实现分析:消息队列模型与四大接口
应用任务消息队列模型
App Task 在系统启动时创建,并持有自己的消息队列。所有发往 App 的消息(无论来自哪个任务或中断)都进入该队列,由 App Task 主循环统一取出。其核心设计约束是:
- 单消费者:只有 App Task 调用
app_task_get_msg取消息。这意味着队列的"出队"端不存在竞争,入队端由 OS 消息队列原语保证原子性(该原语位于预编译库,具体机制未在源码中体现)。 - FIFO 顺序:消息按投递顺序处理,保证事件的时间有序性——这在按键处理中尤其重要,例如"按下→释放"必须按序执行。
用户自定义消息:app_task_put_usr_msg(int msg, int arg_num, ...)
//app自定义消息发送接口
int app_task_put_usr_msg(int msg, int arg_num, ...);
Source: app_msg.h
这是整个机制的通用入口,设计意图有三点:
- 消息 ID 与数据分离:
msg只表达"发生了什么",具体数据由变参携带,使消息类型定义保持精简; - 变参传递(
arg_num, ...):一个消息可携带 0 到 N 个附加参数,常见用法是传数值(如音量值、索引)或指针(如指向结构体的地址)。相比为每个场景定义独立消息,变参显著减少了消息 ID 的数量和分发分支; - 调用方无阻塞承诺:接口返回
int表示投递结果,调用方据此判断消息是否成功入队(具体返回值语义在头文件中未定义,需以预编译库行为为准——实现细节未在源码中体现)。
典型调用形式:app_task_put_usr_msg(MSG_ID, 0)(无参数)、app_task_put_usr_msg(MSG_ID, 2, arg1, arg2)(两个参数)。消息 ID 的取值由各业务模块自行约定并统一管理。
消息获取:app_task_get_msg(int *msg, int msg_size, int block)
//app消息获取接口(block参数为0表示内部pend,1直接返回)
void app_task_get_msg(int *msg, int msg_size, int block);
Source: app_msg.h
block 参数是本接口的设计核心,直接决定 App Task 主循环的两种运行模式:
block = 0(阻塞/pend):队列为空时,任务调用 OS 原语挂起休眠,直到有消息入队才被唤醒。这是低功耗的关键——主循环无事可做时不忙等,CPU 可进入休眠;同时它天然实现了"事件驱动":没有事件就不运行。block = 1(非阻塞):无论队列是否有消息都立即返回。适用于主循环还需要轮询其他条件(如定时器标志、非消息事件)的场景。
msg 是输出缓冲区,msg_size 是其容量(以 int 为单位)。设计为 int 数组而非结构体,是为了与"消息 ID + 参数"的扁平表示兼容:取出的消息连同参数一起填充到缓冲区,由调用方按约定的布局解析。参数实际如何随消息存储与拷贝,属于预编译库内部实现,未在源码中体现。
按键消息:app_task_put_key_msg(int msg, int value) 与 app_task_clear_key_msg()
//app按键消息发送接口
int app_task_put_key_msg(int msg, int value);
//app清理按键消息接口
void app_task_clear_key_msg();
Source: app_msg.h
按键是音频 SoC 上最频繁、最实时的事件源,因此 SDK 为它单独设计了两个接口:
app_task_put_key_msg(msg, value):按键扫描/解码模块在检测到按键事件时调用。msg是按键消息 ID(按键按下、释放、连击等),value携带按键的具体值(键号或事件码)。按键消息独立于用户消息,是因为它的产生频率高、处理优先级明确,需要与业务消息区分对待。app_task_clear_key_msg():清除队列中尚未处理的按键消息。它的存在解决了一个经典问题——状态切换时的残留按键:例如用户按住按键进入新界面/新状态,队列中残留的旧按键事件若在新状态下被取出执行,会触发预期外的行为。因此在状态切换点调用app_task_clear_key_msg()丢弃残留消息,保证"新状态只响应新按键"。
按键消息映射的配置存储
按键消息并非硬编码,而是可配置的:
#define CFG_KEY_MSG_ID \t\t\t606
Source: syscfg_id.h
CFG_KEY_MSG_ID(ID = 606)是按键消息配置在系统配置(syscfg)中的存储槽位。该配置由配置工具生成——每个 CPU 平台的配置工具目录下都有 board_common/key_msg.lua 脚本,例如:
cpu/bd19/tools/AC632N_config_tool/conf/source/board_common/key_msg.luacpu/bd29/tools/AC630N_config_tool/conf/source/board_common/key_msg.luacpu/br23/tools/AC695X_config_tool/conf/source/board_common/key_msg.luacpu/br25/tools/AC696X_config_tool/conf/source/board_common/key_msg.luacpu/br30/tools/AC897N_config_tool/conf/source/board_common/key_msg.luacpu/br34/tools/AC698N_config_tool/conf/source/board_common/key_msg.lua
key_msg.lua 定义按键(键号、组合键)到消息的映射关系,随固件烧录后由 syscfg 读取,运行时按键解码模块据此调用 app_task_put_key_msg。设计意图是板级定制与代码解耦:换板子改按键映射时只需改配置,无需改应用代码。各平台脚本内容的具体差异属于板级配置,本页不逐一展开。
与底层 OS 消息系统的关系
app_msg.h 是应用层的封装接口,其实现建立在 RTOS 消息队列原语之上(os_msg_* 系列,位于预编译库与 include_lib/system/os/ 中)。这种分层设计的好处是:上层代码只依赖 app_msg.h 这 4 个稳定接口,OS 层的实现细节(队列容量、内存分配、阻塞唤醒机制)可以随库升级而改变,不影响应用代码。仓库中无法看到 app_task_* 的内部实现,因此队列容量、满队列时的行为(丢弃还是阻塞)等细节未在源码中体现,需参考 SDK 库说明。
核心流程
用户消息投递与处理时序
下图展示一条用户自定义消息从投递到处理的完整生命周期:
sequenceDiagram
participant P as 业务模块 (Producer)
participant Q as App Task 消息队列
participant L as App Task 主循环
participant H as 消息处理函数
P->>Q: app_task_put_usr_msg(msg, arg_num, ...)
Note over P,Q: 消息与参数入队,调用方继续运行(异步)
L->>Q: app_task_get_msg(&msg, size, block=0)
Note over L,Q: 队列为空时内部 pend 挂起,CPU 可休眠
Q-->>L: 唤醒并返回消息与参数
L->>H: 按 msg ID 分发
H-->>L: 处理完成,回到主循环
各步骤说明:
- 业务模块调用
app_task_put_usr_msg投递消息后立即返回,不等待处理完成——这就是异步解耦; - 主循环调用
app_task_get_msg(..., 0)阻塞等待;队列为空时任务挂起(pend),不占用 CPU; - 消息入队后 OS 唤醒主循环,返回消息 ID 与参数;
- 主循环按消息 ID 分发到对应处理函数,处理完成后回到步骤 2,形成事件驱动循环。
按键消息生命周期
按键消息的路径与用户消息略有不同,还包含"清理残留"这一关键环节:
flowchart TD
Start([按键事件]) --> Decode["按键扫描/解码模块"]
Decode --> Map{"按键→消息映射<br/>(CFG_KEY_MSG_ID / key_msg.lua)"}
Map -->|"命中映射"| PutKey["app_task_put_key_msg(msg, value)"]
Map -->|"未配置"| Drop["忽略该事件"]
PutKey --> Queue[("消息队列")]
Queue --> Get["app_task_get_msg(block=1) 轮询/block=0 等待"]
Get --> Handle{"当前状态需要该按键?"}
Handle -->|"需要"| Do["执行按键逻辑"]
Handle -->|"不需要/发生状态切换"| Clear["app_task_clear_key_msg() 清理残留"]
Clear -.-> Queue
Drop --> End([结束])
Do --> End
说明:按键事件先经映射(配置来源为 key_msg.lua 生成的 CFG_KEY_MSG_ID 配置),命中后投递按键消息;消费端在状态切换点调用 app_task_clear_key_msg() 清理尚未处理的残留按键消息,防止旧事件污染新状态。具体映射规则与"未配置键"的处理行为由配置工具脚本决定。
使用示例
示例 1:消息接口声明(app_msg.h 全文)
以下代码是 SDK 中消息事件机制的唯一公开接口来源,所有上层用法都围绕这 4 个声明展开:
#ifndef SYS_APP_MSG_H
#define SYS_APP_MSG_H
//app自定义消息发送接口
int app_task_put_usr_msg(int msg, int arg_num, ...);
//app消息获取接口(block参数为0表示内部pend,1直接返回)
void app_task_get_msg(int *msg, int msg_size, int block);
//app按键消息发送接口
int app_task_put_key_msg(int msg, int value);
//app清理按键消息接口
void app_task_clear_key_msg();
#endif
Source: app_msg.h
要点解读:
- 头文件以
SYS_APP_MSG_H宏做 include guard,说明该接口隶属"系统(SYS)层"; - 无
extern "C"包裹(纯 C SDK),各接口均为全局函数; - 注释直接给出了关键语义:"block 参数为 0 表示内部 pend,1 直接返回"。
示例 2:按键消息配置存储 ID
按键消息映射作为系统配置项持久化,其存储 ID 在系统配置 ID 表中定义:
#define CFG_KEY_MSG_ID \t\t\t606
Source: syscfg_id.h
该宏位于系统配置 ID 的连续分配区(相邻 ID 如 CFG_UI_TONE_STATUS_ID 605、CFG_LRC_ID 607),说明按键消息配置与其他系统配置共用同一套 syscfg 存储体系。
示例 3:按键消息配置脚本(配置工具)
各 CPU 平台的配置工具在 board_common 下维护按键消息映射脚本,例如 br25 平台:
该 Lua 脚本在配置工具编译固件时参与按键映射生成,最终写入 CFG_KEY_MSG_ID 配置槽位;修改按键→消息映射时优先改这里,而不是改应用代码。
典型调用约定(基于声明的使用方式)
结合接口签名,上层代码的典型用法如下(消息 ID 由各模块自行定义,此处仅示意调用形态,非仓库现成代码):
- 发送无参消息:
app_task_put_usr_msg(MSG_ID, 0); - 发送带参消息:
app_task_put_usr_msg(MSG_ID, 2, value1, value2); - 主循环阻塞取消息:
app_task_get_msg(&msg_buf, MSG_BUF_SIZE, 0); - 主循环非阻塞轮询:
app_task_get_msg(&msg_buf, MSG_BUF_SIZE, 1); - 状态切换时清理按键:
app_task_clear_key_msg();
配置选项
| 配置项 | 类型 | 默认/取值 | 说明 |
|---|---|---|---|
block(app_task_get_msg) | 参数(int) | 0 / 1 | 0 = 内部 pend 阻塞等待;1 = 非阻塞直接返回 |
arg_num(app_task_put_usr_msg) | 参数(int) | 0..N | 变参个数,决定消息携带的附加数据量 |
msg_size(app_task_get_msg) | 参数(int) | 由调用方指定 | 消息缓冲区容量(以 int 计),须足够容纳消息 ID 与参数 |
CFG_KEY_MSG_ID | 宏(syscfg ID) | 606 | 按键消息映射在系统配置中的存储槽位 |
key_msg.lua | Lua 脚本(配置工具) | 板级默认 | 按键→消息映射定义,位于各平台配置工具 board_common 目录 |
API 参考
以下 API 均声明于 app_msg.h。嵌入式 C 环境无异常机制,所有错误通过返回值表达。
int app_task_put_usr_msg(int msg, int arg_num, ...)
App 自定义消息发送接口:将一条携带 arg_num 个附加参数的消息投递到 App Task 消息队列。
参数:
msg(int):消息 ID,由业务模块自行约定;arg_num(int):可变参数个数(0 表示无附加参数);...:附加参数,通常为u32数值或指针。
返回: int,投递结果。头文件未定义具体取值,调用方应判断返回值决定是否重试/降级(成功/失败的具体约定属于预编译库实现细节,未在源码中体现)。
注意: 该接口为异步投递,返回后消息仍在队列中,不保证已处理。
void app_task_get_msg(int *msg, int msg_size, int block)
App 消息获取接口:从队列取出一条消息到 msg 缓冲区。
参数:
msg(int*):输出缓冲区,接收消息 ID 与参数;msg_size(int):缓冲区容量(以int为单位);block(int):阻塞模式——0表示内部 pend(队列为空时挂起等待),1表示直接返回(非阻塞轮询)。
返回: 无。
设计意图: 主循环在"等待事件"时用 block=0 休眠省电,在"还需轮询其他条件"时用 block=1 快速轮询。
int app_task_put_key_msg(int msg, int value)
App 按键消息发送接口:由按键扫描/解码模块投递按键事件。
参数:
msg(int):按键消息 ID(按下/释放/连击等事件类别);value(int):按键值/事件码,标识具体按键与事件。
返回: int,投递结果(同 app_task_put_usr_msg 的约定)。
void app_task_clear_key_msg()
App 清理按键消息接口:清除队列中尚未处理的按键消息。
参数: 无。返回: 无。
典型场景: 界面/状态切换时调用,丢弃残留按键事件,防止旧事件在新状态下误触发。
故障模式、边界情况与并发
| 场景 | 行为/风险 | 依据与建议 |
|---|---|---|
| 队列满时投递 | 未在源码中体现(预编译库内部行为) | 调用方应检查 app_task_put_usr_msg 返回值;若库采用丢弃策略,高频消息可能丢失,需在业务层做节流或确认 |
多任务并发 get_msg | 设计上仅 App Task 消费,若其他任务也调用会破坏单消费者模型 | 仅由 App Task 主循环调用 app_task_get_msg |
| 中断上下文投递 | 中断中调用 app_task_put_* 的安全性取决于底层 OS 原语(预编译库) | 未在源码中验证,建议业务消息在任务上下文投递 |
| 状态切换残留按键 | 队列中旧按键事件在新状态被取出执行,产生误操作 | 状态切换点调用 app_task_clear_key_msg() |
| 缓冲区过小 | msg_size 小于消息实际占用时可能截断/溢出 | 按"最大消息 ID + 最大参数个数"规划 msg_size |
| 阻塞死等 | 若投递端逻辑错误导致某类消息永不产生,block=0 会长期挂起 | 关键路径考虑 block=1 + 超时轮询组合 |
并发模型总结: 本机制采用"多生产者、单消费者"模型。入队端天然并发(多个任务/模块可同时投递),由 OS 队列原语保证原子性;出队端唯一(App Task),因此消费侧无锁。这是该设计最核心的并发保证,上层代码应保持"只有 App Task 调 get_msg"这一约定。
性能与运维注意事项
- 低功耗设计:
block=0的 pend 等待使主循环在无事件时彻底休眠,不忙等,是电池供电音频设备功耗控制的重要一环。 - 拷贝开销:消息与参数通过队列传递存在内存拷贝(在预编译库内实现);参数较多时优先传指针而非大结构体。
- 消息频率:按键、触摸等高频事件建议在产生端做消抖/合并,避免队列被灌满。
- 调试建议:可在
app_task_put_usr_msg/app_task_put_key_msg返回值处加断言,快速发现队列异常(满/失败)。 - 平台一致性:6 个 CPU 平台(bd19/bd29/br23/br25/br30/br34)共享同一套
app_msg.h接口与board_common/key_msg.lua目录结构,跨平台迁移时消息机制无需改动。
扩展点
- 自定义消息 ID:业务模块自行定义消息 ID 与参数布局,通过
app_task_put_usr_msg投递,由主循环分发——新增业务事件不需要改消息机制本身。 - 按键映射定制:修改对应平台配置工具下的
key_msg.lua即可重定义按键→消息映射,实现板级差异化而无需改应用代码。 - 消息参数形态:变参既支持标量值也支持指针,可扩展出"传结构体指针 + 回调"等高级用法。
- 获取模式切换:通过
block参数可在纯事件驱动(阻塞)与事件+轮询混合(非阻塞)两种主循环形态间切换,适应不同应用对实时性的要求。
测试
仓库中未发现针对 app_msg.h 消息机制的独立测试用例——该接口以预编译库形式提供,其正确性由 SDK 库自身保证。上层应用对消息机制的验证通常通过板级功能测试完成(按键响应、UI 切换、状态机行为),建议在自定义消息 ID 的处理函数中补充日志/断言以辅助联调。
Related Links
- 接口声明:app_msg.h
- 系统配置 ID(
CFG_KEY_MSG_ID = 606):syscfg_id.h - 按键消息配置脚本(br25 平台示例):key_msg.lua
- 兄弟能力(独立消息子系统,不在本页范围):
- SIG Mesh 消息:msg.h / msg.c
- RCSP 协议消息:rcsp_msg.h
- 系统配置机制(syscfg 存储体系,
CFG_KEY_MSG_ID所属):见"系统运行时"目录下的系统配置相关页面