杰理 SDK 文档中心
首页
首页
  • 概述与入门

    • 项目概览
    • 快速开始与开发环境
  • 应用与运行时

    • 应用入口与主循环
    • 按键驱动与用户消息处理
    • 消息系统
  • 固件升级

    • 双备份升级机制与状态机
    • UART 升级传输
    • 升级校验、启动信息与复位流程
  • 芯片与硬件支持

    • AC63 系列芯片 BSP 结构
    • 外设接口与驱动
    • 低功耗、RTC 与时基唤醒
  • 构建与工具

    • 构建系统与工作区
    • 烧写与量产工具
  • 参考资源

    • 数据手册与原理图
    • 双备份升级文档

消息系统

AC63 系列 MCU 固件中的轻量级事件消息队列,基于静态环形缓冲区和关中断临界区实现,为按键驱动、双 Bank 升级流程与应用主循环之间提供解耦的异步通信机制,支持 FIFO(先进先出)与 LIFO(后进先出)两种入队策略。

目的与范围

本页完整覆盖 BSP 消息系统的实现与集成方式,包括:

  • 消息表与哨兵值 NO_MSG 的定义(sdk/bsp/include/msg.h)
  • 环形消息池 msg_pool[]、读写游标与剩余容量管理(sdk/bsp/msg.c)
  • 临界区保护(local_irq_disable() / local_irq_enable())与并发语义
  • FIFO / LIFO 入队、出队、清空、初始化的完整控制流
  • 消息生产者/消费者的集成点(按键驱动、升级流程、应用主循环)

本页不涉及上层具体业务逻辑,例如按键动作如何映射到具体功能、双 Bank 升级协议本身、应用层的消息分发处理细节——这些属于各自能力的独立目录页(如按键系统、双 Bank 升级、应用框架)。

概述

在裸机/轻量 RTOS 风格的 MCU 固件中,硬件事件(按键按下/长按、升级流程状态变化)通常产生于中断、定时器扫描或驱动上下文,而业务处理集中在主循环中。若让驱动直接调用业务代码,会造成强耦合且难以在中断上下文中安全执行。消息系统正是为这种场景设计的异步事件通道:

  • 生产者(按键扫描、升级流程、应用事件)调用 msg_put_fifo() / msg_put_lifo() 将事件 ID 写入消息池;
  • 消费者(应用主循环)调用 get_msg() 取出消息并按 ID 分发处理;
  • 生产者与消费者之间通过环形缓冲解耦,任意一方阻塞或抖动都不会直接阻塞另一方。

设计上有几个关键决策:

  1. 静态内存、零动态分配:消息池是编译期固定大小的 u16 数组(默认 10 条),适合内存受限的嵌入式环境,也避免了 malloc 带来的碎片与不确定性。
  2. 临界区采用关中断:所有 API 内部用 local_irq_disable() / local_irq_enable() 包裹,保证 ISR 与主循环之间的原子性;临界区只包含几条赋值指令,关中断时间极短。
  3. 哨兵值 NO_MSG = 0xffff:消息 ID 从 0 递增且 MSG_MAX 被显式赋值为 NO_MSG,因此 0xffff 永远不会是合法消息,天然可作为"无消息"返回值。
  4. FIFO 与 LIFO 双策略:普通事件按顺序处理(FIFO),紧急事件可用 LIFO"插队"到队头优先被消费。

架构

flowchart TD
    subgraph sg_Producer["消息生产者(#include \"msg.h\" 的模块)"]
        KeyDriver["key_driver.c 按键扫描"]
        Boot["boot.c / dual_bank_update.c 升级流程"]
        AppProducer["main.c 应用事件"]
        TestBoard["board/test.c 板级测试"]
    end

    subgraph sg_API["消息系统 API(sdk/bsp/msg.c)"]
        Init["msg_init()"]
        Flush["flush_all_msg()"]
        PutFIFO["msg_put_fifo(u16 msg)"]
        PutLIFO["msg_put_lifo(u16 msg)"]
        GetMsg["get_msg()"]
    end

    subgraph sg_Pool["消息池(临界区保护)"]
        Pool[("msg_pool[10] 环形缓冲区<br/>msg_read / msg_write / msg_pool_residue")]
    end

    subgraph sg_Consumer["消息消费者"]
        MainLoop["应用主循环 get_msg() 分发"]
    end

    Init --> Flush
    Flush --> Pool
    KeyDriver -->|"按键事件"| PutFIFO
    AppProducer --> PutFIFO
    TestBoard --> PutFIFO
    Boot -->|"紧急状态"| PutLIFO
    PutFIFO -->|"写尾指针"| Pool
    PutLIFO -->|"写头指针(插队)"| Pool
    Pool -->|"读头指针"| GetMsg
    GetMsg --> MainLoop

架构说明:

  • 消息池是唯一共享状态,由 msg_pool[MAX_POOL] 数组加三个游标构成:msg_read(读位置)、msg_write(写位置)、msg_pool_residue(剩余空位)。msg_pool_residue 的存在避免了传统环形缓冲区在"空"与"满"两种状态下读写指针重合的歧义:池空时 residue == MAX_POOL,池满时 residue == 0。
  • 所有 API 都自包含临界区,调用方无需自行加锁;这是嵌入式消息队列的常见设计——把同步开销收敛到队列内部,简化调用方。
  • 生产者多于消费者:按键驱动、升级流程、板级测试都可投递消息;消费者语义上是单消费者(get_msg() 每调用消费一条),多消费者场景需要外部额外互斥(源码未提供,见「故障模式、边界情况与并发」)。

核心实现

消息定义与哨兵值

msg.h 定义了空消息哨兵和消息表。消息表是一个匿名 enum,所有消息 ID 从 0 开始连续递增,最后一个成员 MSG_MAX 被显式赋值为 NO_MSG(即 0xffff):

//定义空消息
#define   NO_MSG       0xffff


//消息表
enum {

    MSG_TEST_IO_KEY1_SHORT,
    MSG_TEST_IO_KEY1_LONG,
    MSG_TEST_IO_KEY1_HOLD,
    MSG_TEST_IO_KEY1_LONG_HOLD_UP,

    MSG_TEST_IO_KEY2_SHORT,
    MSG_TEST_IO_KEY2_LONG,
    MSG_TEST_IO_KEY2_HOLD,
    MSG_TEST_IO_KEY2_LONG_HOLD_UP,


    MSG_MAX = NO_MSG,
};

Source: msg.h

设计意图:MSG_MAX = NO_MSG 这一赋值是编译期保证——所有合法消息 ID 必然小于 0xffff,因此 NO_MSG 可以安全地作为 get_msg() 的"无消息"哨兵返回值,无需额外标志位。当前示例消息表对应测试 IO 按键的短按/长按/持续按住/长按抬起事件;新增业务消息时,在 MSG_MAX 之前插入枚举成员即可,编译器会自动分配递增 ID。

消息池数据结构与临界区

msg.c 顶部定义了消息池容量、临界区宏和全局状态:

//消息池大小
#define   MAX_POOL   10

//临界区管理
#define   MSG_ENTER_CRITICAL()    local_irq_disable()
#define   MSG_EXIT_CRITICAL()     local_irq_enable()

u16 msg_pool[MAX_POOL];             //消息池
u16 msg_read = 0;                   //消息读位置
u16 msg_write = 0;                  //消息写位置
u16 msg_pool_residue = MAX_POOL;    //消息池剩余大小

Source: msg.c

要点分析:

  • MAX_POOL 是编译期常量(默认 10),消息池共占用 10 × 2 = 20 字节 RAM,零动态内存。
  • 临界区宏可替换:MSG_ENTER_CRITICAL() / MSG_EXIT_CRITICAL() 封装了 BSP 的关中断/开中断 API。若移植到不同平台或改用 RTOS 的调度器锁,只需修改这两个宏,消息系统内部逻辑无需改动——这是本文件最重要的扩展点。
  • msg_pool_residue 从 MAX_POOL 递减:入队减一、出队加一,通过它与 0 或 MAX_POOL 的比较即可判定池满/池空,避免读写指针相等时的二义性。

msg_put_fifo() —— 先进先出入队

u8 msg_put_fifo(u16 msg)
{
    MSG_ENTER_CRITICAL();
    if (msg_pool_residue == 0) {
        MSG_EXIT_CRITICAL();
        puts("err:msg pool is full!\n");
        return -1;
    }
    msg_pool[msg_write] = msg;
    msg_write++;
    if (msg_write == MAX_POOL) {
        msg_write = 0;
    }
    msg_pool_residue--;
    MSG_EXIT_CRITICAL();
    return 0;
}

Source: msg.c

控制流:

  1. 进入临界区(关中断);
  2. 若 msg_pool_residue == 0(池满),先退出临界区、打印 err:msg pool is full! 并返回 -1;
  3. 否则将消息写入 msg_pool[msg_write](队尾),msg_write 自增;
  4. 写指针到达 MAX_POOL 时回绕到 0——环形缓冲区的经典回绕处理;
  5. msg_pool_residue--,退出临界区,返回 0。

注意满池时先退出临界区再打印,避免在关中断状态下执行耗时的串口输出,这是对中断实时性的细心处理。

msg_put_lifo() —— 后进先出"插队"入队

u8 msg_put_lifo(u16 msg)
{
    MSG_ENTER_CRITICAL();
    if (msg_pool_residue < 2) {
        MSG_EXIT_CRITICAL();
        puts("err:msg pool less than two\n");
        return -1;
    }
    msg_read--;
    if (msg_read == 0xffff) {
        msg_read = MAX_POOL - 1;
    }
    msg_pool[msg_read] = msg;
    msg_pool_residue--;
    MSG_EXIT_CRITICAL();
    return 0;
}

Source: msg.c

与 FIFO 的关键区别:

  • 入队位置不同:FIFO 写在 msg_write(尾),LIFO 写在 msg_read - 1(头),即新消息插入到队头,下一次 get_msg() 会优先读到这条消息——这正是"插队"语义,适合紧急事件(例如升级流程的状态切换)抢占普通按键事件。
  • 下溢回绕:msg_read-- 后若等于 0xffff,说明跨越了数组下界,回绕到 MAX_POOL - 1。
  • 容量门槛更严:要求 msg_pool_residue >= 2 才允许插入,比 FIFO 的 residue >= 1 多留一个空位。实现注释未说明原因,合理的推断是:LIFO 消息会抢占队头,若把池彻底占满(residue == 0),后续 FIFO 普通消息将完全无法入队,导致普通事件饿死;预留一个空位保证系统仍有接收普通事件的余地。

get_msg() —— 出队

u16 get_msg(void)
{
    u16 msg = NO_MSG;

    MSG_ENTER_CRITICAL();
    if (msg_pool_residue < MAX_POOL) {
        msg = msg_pool[msg_read];
        msg_read++;
        if (msg_read == MAX_POOL) {
            msg_read = 0;
        }
        msg_pool_residue++;
    }
    MSG_EXIT_CRITICAL();
    return msg;
}

Source: msg.c

控制流:

  1. 局部变量 msg 初始化为 NO_MSG 哨兵;
  2. 进入临界区;
  3. 若 msg_pool_residue < MAX_POOL 说明池非空(有消息),从 msg_pool[msg_read] 读取队头消息,msg_read 自增并回绕,msg_pool_residue++;
  4. 若池空则保持 NO_MSG;
  5. 退出临界区,返回消息值。

空池不报错,而是返回 NO_MSG,因此主循环可以无阻塞地轮询:msg = get_msg(); if (msg != NO_MSG) { ... }。由于 LIFO 会把消息写到 msg_read 之前,get_msg() 无需区分消息类型,天然先消费 LIFO 插队的消息。

flush_all_msg() 与 msg_init() —— 清空与初始化

void flush_all_msg(void)
{
    MSG_ENTER_CRITICAL();
    msg_read = 0;
    msg_write = 0;
    msg_pool_residue = MAX_POOL;
    memset(msg_pool, 0xff, sizeof(msg_pool));
    MSG_EXIT_CRITICAL();
}


void msg_init(void)
{
    if (MAX_POOL < 3) {
        while (1) {
            puts("err:max pool less than 3\n");
        }
    }
    flush_all_msg();
}

Source: msg.c

  • flush_all_msg() 将三个游标复位并把整个池填充为 0xff(即 NO_MSG 哨兵值),使池中残留数据不会在调试时被误读为合法消息。由于复位操作在临界区内完成,即使清空过程中发生中断,也不会观察到中间状态。
  • msg_init() 首先做编译期配置校验:若 MAX_POOL < 3 则进入死循环并反复打印 err:max pool less than 3。这是嵌入式固件常见的"配置错误就地暴露"手法——MAX_POOL 是编译期常量,若配置过小(连 FIFO 入队 + LIFO 插队 + 空位预留都满足不了)说明工程配置有误,与其带着隐患运行,不如在启动时死循环提示。初始化应在系统启动早期调用(boot.c 已包含 msg.h,属于消息系统的初始化入口之一)。

核心流程

典型 FIFO 消息生命周期

sequenceDiagram
    participant P as 生产者(按键/升级/应用)
    participant C as 消息系统 msg.c
    participant Pool as msg_pool[10]
    participant M as 主循环消费者

    P->>C: msg_put_fifo(msg)
    activate C
    C->>C: local_irq_disable() 进入临界区
    C->>C: msg_pool_residue == 0 ?
    alt 池满
        C-->>P: 返回 -1,打印 "err:msg pool is full!"
    else 池未满
        C->>Pool: msg_pool[msg_write] = msg
        C->>C: msg_write++,== MAX_POOL 时回绕为 0
        C->>C: msg_pool_residue--
        C-->>P: 返回 0
    end
    C->>C: local_irq_enable() 退出临界区
    deactivate C

    M->>C: get_msg()
    activate C
    C->>C: local_irq_disable()
    alt 池非空(msg_pool_residue < MAX_POOL)
        C->>Pool: 读取 msg_pool[msg_read]
        C->>C: msg_read++,== MAX_POOL 时回绕为 0
        C->>C: msg_pool_residue++
        C-->>M: 返回消息值
    else 池空
        C-->>M: 返回 NO_MSG(0xffff)
    end
    C->>C: local_irq_enable()
    deactivate C

LIFO 插队判定流程

flowchart TD
    Start(["msg_put_lifo(msg) 被调用"]) --> Enter["进入临界区<br/>local_irq_disable()"]
    Enter --> Check{"msg_pool_residue < 2 ?"}
    Check -->|"是(空位不足)"| Err["退出临界区<br/>返回 -1<br/>打印 err:msg pool less than two"]
    Check -->|"否"| Dec["msg_read--"]
    Dec --> Wrap{"msg_read == 0xffff ?"}
    Wrap -->|"是(跨过数组下界)"| WrapBack["msg_read = MAX_POOL - 1"]
    Wrap -->|"否"| Store["msg_pool[msg_read] = msg"]
    WrapBack --> Store
    Store --> Dec2["msg_pool_residue--"]
    Dec2 --> Exit["退出临界区 local_irq_enable()<br/>返回 0"]
    Err --> End([结束])
    Exit --> End

系统集成点

通过 #include "msg.h" 的模块可以确认消息系统的实际参与者(本页基于源文件中的引用关系整理):

模块角色说明
sdk/apps/main.c生产者/消费者应用入口,主循环中轮询 get_msg() 分发消息
sdk/bsp/AC632N/src/key_driver.c(及 AC635N/AC636N 对应文件)生产者按键扫描产生 MSG_TEST_IO_KEY* 系列消息
sdk/bsp/AC632N/src/boot.c(及对应系列文件)初始化启动阶段初始化消息系统
sdk/bsp/dual_bank_update.c生产者双 Bank 升级流程状态上报
sdk/bsp/AC632N/board/test.c(及对应系列文件)生产者板级测试程序验证消息通路

典型运行时序:系统上电 → boot.c 完成 msg_init() → 应用主循环启动 → 按键驱动在扫描上下文调用 msg_put_fifo() 投递按键事件 → 主循环 get_msg() 取出消息,若为 NO_MSG 则继续轮询,否则按消息 ID 分发处理。升级流程的紧急状态则通过 msg_put_lifo() 插队,确保被优先消费。

使用示例

示例 1:新增业务消息

在消息表中、MSG_MAX = NO_MSG 之前追加枚举成员,编译器自动分配 ID(始终小于 0xffff):

enum {

    MSG_TEST_IO_KEY1_SHORT,
    MSG_TEST_IO_KEY1_LONG,
    MSG_TEST_IO_KEY1_HOLD,
    MSG_TEST_IO_KEY1_LONG_HOLD_UP,

    MSG_TEST_IO_KEY2_SHORT,
    MSG_TEST_IO_KEY2_LONG,
    MSG_TEST_IO_KEY2_HOLD,
    MSG_TEST_IO_KEY2_LONG_HOLD_UP,

    // 在此处新增业务消息,例如:
    // MSG_UPGRADE_START,
    // MSG_UPGRADE_DONE,

    MSG_MAX = NO_MSG,
};

Source: msg.h

示例 2:生产者投递普通事件(FIFO)

按键驱动或应用模块在事件发生时调用 msg_put_fifo()。返回 0 表示投递成功,-1 表示消息池已满(此时事件被丢弃,调用方应自行决定是否重试或忽略):

u8 msg_put_fifo(u16 msg)
{
    MSG_ENTER_CRITICAL();
    if (msg_pool_residue == 0) {
        MSG_EXIT_CRITICAL();
        puts("err:msg pool is full!\n");
        return -1;
    }
    msg_pool[msg_write] = msg;
    msg_write++;
    if (msg_write == MAX_POOL) {
        msg_write = 0;
    }
    msg_pool_residue--;
    MSG_EXIT_CRITICAL();
    return 0;
}

Source: msg.c

示例 3:消费者主循环轮询消息

主循环以非阻塞方式轮询 get_msg():返回 NO_MSG 表示当前无消息,继续执行其他任务;否则按消息 ID 分发:

u16 get_msg(void)
{
    u16 msg = NO_MSG;

    MSG_ENTER_CRITICAL();
    if (msg_pool_residue < MAX_POOL) {
        msg = msg_pool[msg_read];
        msg_read++;
        if (msg_read == MAX_POOL) {
            msg_read = 0;
        }
        msg_pool_residue++;
    }
    MSG_EXIT_CRITICAL();
    return msg;
}

Source: msg.c

典型消费模式(伪代码,基于上述 API 语义):

for (;;) {
    u16 msg = get_msg();
    if (msg != NO_MSG) {
        switch (msg) {
        case MSG_TEST_IO_KEY1_SHORT: /* 处理按键1短按 */ break;
        case MSG_TEST_IO_KEY2_LONG:  /* 处理按键2长按 */ break;
        default: break;
        }
    }
    /* 其他主循环任务 */
}

示例 4:紧急事件插队(LIFO)

升级流程等需要抢占队头的场景使用 msg_put_lifo();注意其容量门槛为 msg_pool_residue >= 2,比 FIFO 更严格:

u8 msg_put_lifo(u16 msg)
{
    MSG_ENTER_CRITICAL();
    if (msg_pool_residue < 2) {
        MSG_EXIT_CRITICAL();
        puts("err:msg pool less than two\n");
        return -1;
    }
    msg_read--;
    if (msg_read == 0xffff) {
        msg_read = MAX_POOL - 1;
    }
    msg_pool[msg_read] = msg;
    msg_pool_residue--;
    MSG_EXIT_CRITICAL();
    return 0;
}

Source: msg.c

示例 5:系统启动初始化

启动阶段调用 msg_init() 完成游标复位与池内容填充(0xff),若 MAX_POOL < 3 则死循环报错:

void msg_init(void)
{
    if (MAX_POOL < 3) {
        while (1) {
            puts("err:max pool less than 3\n");
        }
    }
    flush_all_msg();
}

Source: msg.c

配置选项

消息系统没有运行时配置,全部为编译期宏/常量,定义于 sdk/bsp/msg.c 与 sdk/bsp/include/msg.h:

选项类型默认值说明
MAX_POOL宏(msg.c)10消息池容量,即环形缓冲区最多容纳的消息条数;要求 ≥ 3,否则 msg_init() 死循环报错
NO_MSG宏(msg.h)0xffff空消息哨兵,既是 MSG_MAX 的取值,也是 get_msg() 空池返回值
MSG_ENTER_CRITICAL()宏(msg.c)local_irq_disable()进入临界区操作,可替换为平台相关的关中断/调度器锁实现
MSG_EXIT_CRITICAL()宏(msg.c)local_irq_enable()退出临界区操作,与 MSG_ENTER_CRITICAL() 成对替换

调整容量时只需修改 MAX_POOL,消息池数组 msg_pool[MAX_POOL] 会自动随之伸缩(RAM 占用为 MAX_POOL × 2 字节)。注意:增大容量会延长极端情况下满池打印前可缓存的消息数,但不会影响各 API 的 O(1) 时间复杂度。

API 参考

u8 msg_put_fifo(u16 msg)

以先进先出方式将消息放入队尾。

  • 参数:msg(u16)——消息 ID,应来自消息表枚举(必须小于 NO_MSG)。
  • 返回:0 表示入队成功;-1 表示消息池已满(msg_pool_residue == 0),消息被丢弃并打印 err:msg pool is full!。
  • 并发:内部自含临界区,可在中断上下文调用。

u8 msg_put_lifo(u16 msg)

以后进先出方式将消息插入队头(插队),使下一次 get_msg() 优先返回该消息。

  • 参数:msg(u16)——消息 ID。
  • 返回:0 表示入队成功;-1 表示剩余空位不足 2(msg_pool_residue < 2),打印 err:msg pool less than two。
  • 说明:容量门槛比 FIFO 更严(要求至少 2 个空位),为普通 FIFO 消息保留入队空间。

u16 get_msg(void)

从队头取出一个消息。

  • 返回:消息值;若消息池为空则返回 NO_MSG(0xffff)。每调用一次消费一条消息。
  • 并发:内部自含临界区;单消费者语义,多个消费者并发调用需要外部额外互斥。

void flush_all_msg(void)

清空消息池:复位 msg_read、msg_write、msg_pool_residue,并将整个池填充为 0xff(NO_MSG)。

  • 参数/返回:无。
  • 用途:系统复位、模式切换(如进入低功耗/升级模式前丢弃积压事件)。

void msg_init(void)

初始化消息系统:校验 MAX_POOL >= 3(否则死循环打印 err:max pool less than 3),随后调用 flush_all_msg()。

  • 参数/返回:无。
  • 调用时机:系统启动早期,boot.c 等启动流程中。

故障模式、边界情况与并发

消息池满(FIFO)

当 msg_pool_residue == 0 时 msg_put_fifo() 返回 -1 并打印 err:msg pool is full!。消息被静默丢弃,调用方只能根据返回值感知。对于不可丢失的关键事件(如升级完成通知),调用方应实现重试或等待策略;对于高频按键事件,丢弃通常是可接受的(用户会再次按键)。

空位不足(LIFO)

msg_put_lifo() 要求 msg_pool_residue >= 2,否则返回 -1 并打印 err:msg pool less than two。该设计预留一个空位,避免 LIFO 插队把池占满导致普通 FIFO 事件完全无法入队。

环形回绕与下溢

  • FIFO 写指针 msg_write 与出队读指针 msg_read 在递增到 MAX_POOL 时回绕为 0;
  • LIFO 将 msg_read 递减,当跨过数组下界(msg_read == 0xffff)时回绕到 MAX_POOL - 1。
  • 回绕逻辑均位于临界区内,配合 msg_pool_residue 判断,读写指针相遇不会引发数据损坏。

空池读取

get_msg() 在池空时返回 NO_MSG 而非错误码,使主循环可以无阻塞轮询;哨兵值由 MSG_MAX = NO_MSG 编译期保证不会与合法消息冲突。

中断与主循环并发

所有 API 通过 local_irq_disable() / local_irq_enable() 实现原子性,因此可在中断上下文安全投递消息(典型场景:按键扫描在定时器中断中入队,主循环出队)。代价是临界区内会短暂关闭全局中断;由于临界区仅包含数组读写与游标自增等数条指令,对系统实时性影响极小。若系统中存在高实时性外设(如音频 DMA),应评估关中断时间是否可接受,或考虑将临界区宏替换为基于优先级的中断屏蔽方案。

单消费者语义

get_msg() 每调用消费一条,且没有任务级互斥。若多个任务/上下文同时调用 get_msg(),可能出现消息竞争(例如 A 任务读到 NO_MSG 的同时 B 任务消费了实际消息)。源码假定单消费者(应用主循环);引入多消费者时需要外部加锁或在消息分发层串行化。

初始化配置错误

msg_init() 在 MAX_POOL < 3 时进入死循环并打印错误。这是刻意的"失败即停"策略:MAX_POOL 是编译期常量,若配置过小说明工程配置有误,继续运行会在运行时反复丢消息,死循环能第一时间暴露问题。

内存占用与溢出风险

消息池是静态数组,不存在堆溢出风险;但 msg_pool_residue、msg_read、msg_write 均为 u16,在 MAX_POOL 不超过 65535 的前提下不会溢出。当前默认值 10 远小于该上限。

性能与运维

  • 时间复杂度:msg_put_fifo、msg_put_lifo、get_msg、flush_all_msg 均为 O(1);msg_init 额外执行一次 O(MAX_POOL) 的 memset(仅启动时)。
  • 内存占用:消息池固定 MAX_POOL × 2 字节(默认 20 字节),加 3 个 u16 游标,总计约 26 字节 RAM,无动态内存、无碎片。
  • 临界区开销:每次 API 调用执行一次关中断/开中断,临界区仅数条指令。投递/消费频率受关中断时长限制,对典型按键事件(毫秒级)完全够用;若消息频率极高(如每微秒级),应评估关中断累积开销。
  • 可观测性:满池与容量不足时会通过 puts() 输出错误串(err:msg pool is full! / err:msg pool less than two),配合串口即可定位"事件丢失"类问题。调试时可用 flush_all_msg() 丢弃积压消息后重新观察。
  • 复位语义:msg_init() 应在任何生产者开始投递之前完成;模式切换(升级、低功耗)前调用 flush_all_msg() 可避免旧事件在新模式下被误处理。

扩展点

  1. 新增消息类型:在 msg.h 的枚举中、MSG_MAX = NO_MSG 之前插入新成员,编译器自动分配 ID。消息系统对消息内容零感知——消息就是 u16 ID,语义完全由生产者和消费者约定,因此新增消息不需要改动 msg.c。
  2. 调整消息池容量:修改 MAX_POOL 即可(RAM 按 MAX_POOL × 2 增长);注意保持 ≥ 3,否则 msg_init() 死循环。
  3. 新增生产者:任何模块 #include "msg.h" 后调用 msg_put_fifo() / msg_put_lifo() 即可,无需注册、无需修改消息系统。当前已知生产者包括按键驱动(key_driver.c)、双 Bank 升级(dual_bank_update.c)、板级测试(board/test.c)与应用(main.c)。
  4. 替换临界区实现:修改 MSG_ENTER_CRITICAL() / MSG_EXIT_CRITICAL() 宏,例如适配 RTOS 的调度器锁或不同架构的关中断指令。这是将消息系统移植到新平台的主要改动点。
  5. 扩展消费者:get_msg() 语义固定为单消费者;若需多消费者/优先级消费,可在主循环分发层之上包装(例如按消息 ID 路由到不同任务),而不修改队列本身。

测试

仓库中未发现针对 msg.c 的独立单元测试文件。消息系统的验证主要依赖两类途径:

  • 板级测试程序:sdk/bsp/AC632N/board/test.c、sdk/bsp/AC635N/board/test.c、sdk/bsp/AC636N/board/test.c 均包含 msg.h,可在生产板上通过按键/串口交互验证消息投递与消费通路。
  • 集成验证:消息满池/空位不足时的 puts() 错误输出,可在系统测试中作为"事件丢失"的判定依据。

如需补充自动化测试,建议针对四个关键边界编写用例:FIFO 满池返回 -1、LIFO 空位不足返回 -1、读写指针回绕(MAX_POOL 处与 0xffff 下溢)、空池 get_msg() 返回 NO_MSG。

相关链接

  • 消息系统实现 msg.c
  • 消息系统头文件 msg.h
  • 应用入口 main.c(消息消费者/生产者)
  • 按键驱动 key_driver.c(AC632N,消息生产者)
  • 启动流程 boot.c(AC632N,消息初始化)
  • 双 Bank 升级 dual_bank_update.c(消息生产者)
  • 板级测试 test.c(AC632N)
Prev
按键驱动与用户消息处理