杰理 SDK 文档中心
首页
首页
  • 项目概览

    • AD23N SDK 概述与芯片平台
    • 工程结构与模块划分
  • 快速开始

    • 开发环境搭建与工具链
    • 编译构建指南
    • 烧录与固件升级工具
  • 应用框架与产品工作流

    • 应用入口与模式调度
    • 音乐播放应用
    • MIDI 解码与键盘演奏
    • 录音应用
    • LINEIN 与扩音应用
    • USB 从设备应用
    • 待机、软关机与空闲检测
    • 公共 UI 与 LED 显示
  • 音频子系统

    • 音频解码器框架
    • 音频编码器框架
    • 音效算法库
    • 音频管理与输出通路
  • 存储与文件系统

    • 文件系统层
    • NOR Flash 与虚拟机存储
    • 设备与设备管理
  • 系统服务与运行时

    • 消息机制与事件分发
    • 按键扫描与输入处理
    • 电源管理与低功耗控制
    • 定时器与系统任务
  • 外设驱动与平台

    • CPU 平台与启动流程
    • USB 协议栈与主机/设备驱动
    • SPI 与通用外设接口
  • 固件升级与构建工具

    • 固件升级机制
    • 编译后处理与镜像打包
    • 构建系统与命令行工具

消息机制与事件分发

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 固件中,多个异步源(按键扫描、外设热插拔、解码器完成回调、定时器)都会产生事件,而应用主循环是一个串行消费者。消息机制的价值在于:

  1. 解耦:生产者只需调用 post_msg / post_event,不关心消费方是谁、何时处理;
  2. 优先级:事件(event_buf 位图)比普通消息(msg_cbuf 队列)优先被消费,保证解码器完成、外设插入等关键信号不被普通消息阻塞;
  3. 零动态内存:队列使用静态数组 msg_pool[MAX_POOL],不依赖堆,避免碎片化;
  4. 原子性:所有读写都包裹在 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() 是消息系统唯一的出口,其决策顺序体现了设计优先级:

  1. 先查事件位图:get_event() 返回非 NO_EVENT 时,直接清除该事件位并以 event2msg[event] 作为 msg[0] 返回——事件消息零拷贝、零排队;
  2. 再读环形队列:cbuf_read 读取 2 字节头。若读取长度不足,说明队列为空,此时执行 __asm__ volatile("idle") 进入 CPU 空闲指令(低功耗等待),返回 MSG_NO_MSG;
  3. 解析头部:msg[0] = t_msg[0] & (MSG_HEADER_ALL_BIT >> MSG_PARAM_BIT_LEN) 取出低 12 位消息类型;param_len = param >> MSG_TYPE_BIT_LEN 取出参数个数;
  4. 参数越界检查:若 param_len > len - 1(调用方缓冲区不够),返回 MSG_BUF_NOT_ENOUGH,注意此时头部已被消费、参数仍留在队列中——这是调用方必须保证 len 足够大的原因;
  5. 读取参数区:cbuf_read 读 param_len * sizeof(int) 字节,长度不匹配返回 MSG_CBUF_ERROR。
int get_msg(int len, int *msg)
{
    u32 param = 0;
    u16 *t_msg = (u16 *)&param;
    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 *)&param;
    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 *)&param, 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_POOLint128环形队列底层字节池大小,决定可排队消息的总字节数(含头部与参数)
MSG_HEADER_BYTE_LENint2消息头部字节数(类型 + 参数个数)
MSG_TYPE_BIT_LENint12消息类型占位数,限制消息 ID 上限 0xFFF
MSG_PARAM_BIT_LENint4参数个数占位数,限制单条消息最多 15 个参数
MSG_HEADER_ALL_BITlong0xFFFF头部全位掩码((1<<16)-1)
NO_MSGenum0x0fff空消息哨兵值,位于 ID 空间顶端
NO_EVENTint0xffffget_event() 无事件时的返回值
EVENT_TOTALint由 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_ERROR0成功
MSG_NO_MSG0无消息(与成功同值,靠 msg[0]==NO_MSG 区分)
MSG_EVENT_EXIST-1预留,当前实现未使用
MSG_NOT_EVENT-2预留,当前实现未使用
MSG_EVENT_PARAM_ERROR-3post_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]"。

扩展点

  1. 新增消息:在 msg.h 枚举中追加 ID(避开 0x600/0x800 保留段与 NO_MSG),应用分发处增加 case 即可;
  2. 新增事件:在 event2msg[] 表尾追加"事件号 → 消息 ID"映射,事件号自动由数组长度决定(EVENT_TOTAL 随之更新);
  3. 调整容量:修改 MAX_POOL 以适配更大吞吐,或调低以节省 RAM(消息池为静态数组,常驻内存);
  4. 自定义消费者:任意任务均可调用 get_msg 消费消息,但整个系统建议保持单一消费者(应用主循环),避免多消费者竞争同一队列导致消息被抢占。

相关链接

  • msg.h 接口声明与消息 ID 枚举
  • msg.c 消息机制实现
  • decoder_msg_tab.c 解码器消息编排(解码器子系统消息表,独立页面)
  • hot_msg.c/h mbox 热消息通道(mbox_flash 场景专用消息通道)
Next
按键扫描与输入处理