杰理 SDK 文档中心
首页
首页
  • 入门指南

    • SDK 概述与芯片平台
    • 环境搭建与开发工具链
    • 编译、烧录与快速开始
  • 应用层开发

    • 语音玩具应用 voice_toy
    • 扩音器应用 voice_enhanced
    • 语音功能状态机 voice_func
    • 应用公共框架与配置
  • 音频子系统

    • 音频解码器与 MIDI 播放
    • 音频编码与录音
    • 音效算法(ANS、变调、变声、混响)
    • 音频输出、功放与硬件重采样
  • 存储与文件系统

    • 文件系统层(FAT、NOR_FS、SYDF 等)
    • 存储设备与设备管理
    • 参数存储 VM 与保留区
  • 系统机制

    • 消息与事件机制
    • 电源管理与低功耗
    • 固件升级机制
    • 外设驱动(按键、红外、SPI、USB)
    • 实时时钟与定时器
  • 构建系统与工具

    • 构建系统(Makefile 与 Code::Blocks)
    • 编译后处理与语音资源打包
  • 硬件平台与文档

    • 芯片平台与启动流程
    • 硬件文档、规格书与原理图

消息与事件机制

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)。

该机制的设计意图:

  1. 解耦:生产者只需调用 post_event() / post_msg(),无需知道消费者是谁;
  2. 中断安全:所有入队操作均处于 OS_ENTER_CRITICAL() 临界区内,可安全地从中断上下文调用;
  3. 确定性:固定内存池(无动态分配)、O(1) 位图操作、环形缓冲无碎片;
  4. 低功耗:消费者在队列为空时通过 __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_* 宏映射消息语义
0EVENT_F1A1_ENDMSG_F1A1_FILE_ENDF1A1 解码文件结束
1EVENT_F1A1_ERRMSG_F1A1_FILE_ERRF1A1 解码错误
6EVENT_MIDI_ENDMSG_MIDI_FILE_ENDMIDI 解码结束
9EVENT_A_ENDMSG_A_FILE_ENDA 格式解码结束
12EVENT_MP3_ENDMSG_MP3_FILE_ENDMP3 解码结束
15EVENT_WAV_ENDMSG_WAV_FILE_ENDWAV 解码结束
18EVENT_APP_SW_ACTIVEMSG_APP_SWITCH_ACTIVEApp 切换激活
19EVENT_WFILE_FULLMSG_WFILE_FULL写文件满
20EVENT_OTG_INMSG_OTG_INOTG 插入
21EVENT_OTG_OUTMSG_OTG_OUTOTG 拔出
22EVENT_UDISK_INMSG_USB_DISK_INU 盘插入
23EVENT_UDISK_OUTMSG_USB_DISK_OUTU 盘拔出
24EVENT_PC_INMSG_PC_INPC 连接
25EVENT_PC_OUTMSG_PC_OUTPC 断开
28EVENT_SDMMCA_INMSG_SDMMCA_INSD 卡插入
29EVENT_SDMMCA_OUTMSG_SDMMCA_OUTSD 卡拔出
32EVENT_EXTFLSH_INMSG_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 *)&param;
    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 *)&param, sizeof(int));
    }
    va_end(argptr);
    OS_EXIT_CRITICAL();
    return MSG_NO_ERROR;
}

关键点:

  1. 先检查剩余空间,不足时直接丢弃并返回 MSG_BUF_NOT_ENOUGH(生产者不阻塞,这是嵌入式实时系统的常见取舍);
  2. 头部编码:(argc - 1) << 12 | (msg_id & 0x0FFF),即低 12 位消息 ID、高 4 位参数个数;
  3. 头部和参数分别写入环形缓冲,整个操作在 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 *)&param;
    //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;
}

出队算法按以下顺序执行:

  1. 先查事件:get_event() 取最高优先级事件并清位,映射为消息 ID 返回(事件通道优先于消息通道);
  2. 再读消息头:从环形缓冲读取 2 字节头部;若读不到完整头部说明队列为空,调用 __builtin_pi32_idle() 让 CPU 进入 idle(等待中断唤醒,实现低功耗),并返回 MSG_NO_MSG;
  3. 校验容量:从头部高 4 位解析参数个数,若超过调用方 msg 数组容量返回 MSG_BUF_NOT_ENOUGH(消息保留在缓冲中,等待下次更大缓冲区读取);
  4. 读参数:按参数个数读取参数区,若环形缓冲读数不符返回 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 *)&param;
    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 *)&param, 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宏0xffffget_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_ERROR0操作成功
MSG_NO_MSG0无消息(与成功共用 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=128 u32),事件位图 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 会自动推导)。

扩展点

  1. 新增普通消息:在 msg.h 的 MSG_* 枚举中追加(保持 < 0xFFF),消费端在 common_msg_deal 或对应工作模式处理函数中新增 case 分支即可。注意 MSG_COMMON_MAX 之后、NO_MSG 之前的区间属于系统保留,MSG_CHANGE_WORK_MODE = 0x600 与 MSG_F1A1_FILE_END = 0x800 之后为库使用段,勿改动。
  2. 新增事件:三步完成——在 msg.h 中定义 EVENT_XXX 宏(按优先级编排事件号);在 msg.c 的 event2msg[] 表中对应下标填入目标消息 ID(无对应消息填 NO_MSG);生产者调用 post_event(EVENT_XXX)。EVENT_TOTAL 与越界检查自动适配。
  3. 新增消费处理器:消息分发入口是 common_msg_deal()(公共/设备/按键消息)与各工作模式的处理函数;可仿照 MSG_NEXT_WORKMODE 分支,在处理后通过 post_msg(1, MSG_CHANGE_WORK_MODE) 触发模式迁移,形成可组合的消息链。
  4. 带参消息:任何需要携带数据的通信(如 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_*)
Next
电源管理与低功耗