跳转至

消息队列

ISR 到任务的数据传递 — OSAL 消息队列、GPIO 驱动

前置阅读:多任务并发ISR 三级分发 — 消息队列是 ISR→任务通信的核心工具

学习目标

  • 理解消息队列在 ISR 与任务之间传递数据的核心作用——ISR 不能阻塞,但队列写是非阻塞的
  • 掌握 osal_msg_queue_create()write_copy()read_copy() 的标准调用链
  • 理解 ISR 侧必须用 OSAL_MSGQ_NO_WAIT、任务侧可用 OSAL_MSGQ_WAIT_FOREVER 的原因
  • 能够用消息队列实现按键中断 → 任务的完整数据传递链路

基本概念

消息队列解决什么问题

信号量只能传"有没有"(通知),消息队列能传"是什么"(数据)。当 ISR 需要把具体数据(时间戳、字节、采样值)传给任务时,消息队列是正确的选择。

sequenceDiagram
    participant H as 硬件
    participant I as ISR
    participant Q as 消息队列
    participant T as 处理任务

    H->>I: 按键中断
    Note over I: 1. 清中断<br/>2. 记录时间戳<br/>3. write_copy(NO_WAIT)
    I->>Q: 写入 key_event_t
    Note over I: ISR 返回,< 5μs

    T->>Q: read_copy(WAIT_FOREVER)
    Q-->>T: 返回 key_event_t
    T->>T: printf key: count
    T->>Q: read_copy(WAIT_FOREVER) — 继续等

ISR 到任务的经典数据管道

消息队列是嵌入式系统中最常用的 IPC 之一。典型应用:

场景 ISR 做什么 任务做什么
UART 接收 ISR 读 FIFO,写字节到队列 任务读队列,做协议解析
按键中断 ISR 记时间戳,写队列 任务读队列,消抖+业务
ADC 完成 ISR 写采样值到队列 任务读队列,滤波+上报
DMA 完成 ISR 写传输状态到队列 任务读队列,切换缓冲区

队列满/空的处理策略

操作 可用标志 行为 适用场景
写队列 OSAL_MSGQ_NO_WAIT 队列满立即返回失败 ISR 中必须用这个
写队列 OSAL_MSGQ_WAIT_FOREVER 队列满阻塞等 任务中、不能丢数据的场景
写队列 N(超时 ms) 队列满等 N ms 后返回 任务中、可接受丢数据的场景
读队列 OSAL_MSGQ_WAIT_FOREVER 队列空阻塞等 任务处理、没数据就休眠
读队列 OSAL_MSGQ_NO_WAIT 队列空立即返回 轮询模式

ISR 中绝不能阻塞——用 OSAL_MSGQ_NO_WAIT 是硬约束。如果队列满,说明任务处理太慢需要优化(加大队列或提速任务)。

消息队列 vs 信号量

对比项 消息队列 信号量
传递什么 数据(时间戳、字节、结构体) 通知("有了"/"没了")
数据拷贝 有(write_copy 把数据拷进队列)
ISR 可用 write_copy 可以(非阻塞) sem_up 可以
队列深度 可配置(16/32/64...) 计数值(0~N)
选择场景 "发的是什么" "发了没有"

涉及 API

API 谁调用 用途 头文件
osal_msg_queue_create(name, queue_len, &queue_id, flags, msg_size) 入口任务 创建消息队列,返回 queue_id osal_msgqueue.h
osal_msg_queue_write_copy(queue_id, buffer_addr, buffer_size, timeout) ISR 写消息(拷贝模式),ISR 中用 NO_WAIT osal_msgqueue.h
osal_msg_queue_read_copy(queue_id, buffer_addr, &buffer_size, timeout) 任务 读消息(拷贝模式),可阻塞等待 osal_msgqueue.h

queue_idunsigned long 类型——不是指针,由 create 时内核分配。后续读写都用这个 id。

案例说明

做什么

用消息队列作为 ISR 和任务之间的数据通道——按键中断触发 ISR,ISR 把时间戳写入队列,任务从队列读出并打印。演示从硬件到应用代码的完整数据传递链路。

与「中断处理」篇的区别:那篇演示完整的三级分发(ISR→tasklet→任务),本篇聚焦于"队列"这一种 IPC 机制的独立使用——不依赖 tasklet 和信号量。

规格与功能

规格项 说明
触发源 GPIO 按键,下降沿触发
ISR 动作 记录时间戳 + write_copy(NO_WAIT) 写队列
任务动作 read_copy(WAIT_FOREVER) 阻塞等待 → 打印
队列深度 16
消息大小 sizeof(key_event_t)(时间戳 + 电平状态)
ISR→任务 纯消息队列——任务直接阻塞在 read_copy

程序运行流程:

  1. 入口创建消息队列 → 注册 GPIO 中断 → 创建处理任务
  2. 处理任务阻塞在 read_copy 上,CPU 休眠
  3. 按键按下 → ISR 写时间戳到队列 → 任务被唤醒 → 打印按键序号
  4. 任务继续 read_copy → 等待下一次按键

数据流

sequenceDiagram
    participant H as 按键 GPIO
    participant I as key_isr
    participant Q as g_key_queue 深度16
    participant T as 处理任务

    H->>I: 下降沿中断
    Note over I: event.timestamp = tick<br/>event.level = 0
    I->>Q: write_copy — NO_WAIT
    Note over I: ISR 返回

    loop 每次按键
        T->>Q: read_copy — WAIT_FOREVER
        Q-->>T: 返回 key_event_t
        T->>T: printf key: count
    end

案例操作指导

第一步:编译

fbb build ws63-liteos-app

第二步:烧录

将固件烧录到 WS63 开发板,按键接指定 GPIO 引脚。

第三步:验证

按一下按键:

key: 1

快速连按 10 次:

key: 1
key: 2
...
key: 10

队列深度 16,不丢不重。

关键配置

参数 说明 调大影响 调小影响
队列深度 16 缓冲快速连按事件 浪费内存 快速连按丢事件
消息大小 sizeof(key_event_t) 时间戳 + 电平 浪费内存 信息不全
ISR 写超时 OSAL_MSGQ_NO_WAIT ISR 绝不能阻塞 队列满→ISR 卡住→系统崩溃
任务读超时 OSAL_MSGQ_WAIT_FOREVER 没数据就休眠 需处理超时分支
队列名 "KeyQueue" 调试时在系统信息中可见

ISR 中用 NO_WAIT 是硬约束——如果队列满时阻塞,ISR 永远无法返回,整个系统卡死。

代码详解

数据结构与全局变量

#include "osal_msgqueue.h"
#include "osal_interrupt.h"
#include "driver/gpio.h"

/* ISR 通过队列传给任务的数据 */
typedef struct {
    unsigned int timestamp;   // 按键时刻(系统 tick)
    int level;               // 当前电平(0=按下)
} key_event_t;

static unsigned long g_key_queue_id;  // 队列 ID(内核分配)

消息队列创建

static void app_entry(void)
{
    int ret;

    /* 创建消息队列
       "KeyQueue": 名称(调试用)
       16:        队列深度
       &g_key_queue_id: 返回的队列 ID
       0:         flags(保留)
       sizeof(key_event_t): 每条消息的大小 */
    ret = osal_msg_queue_create("KeyQueue", 16,
                                &g_key_queue_id, 0,
                                sizeof(key_event_t));
    if (ret != 0) {
        printf("queue create failed!\n");
        return;
    }

    /* 初始化 GPIO,注册中断 */
    uapi_gpio_init();
    uapi_gpio_set_dir(KEY_PIN, GPIO_DIRECTION_INPUT);
    uapi_gpio_register_isr_func(KEY_PIN, GPIO_TRIGGER_FALLING, key_isr);

    /* 创建处理任务 */
    osal_task *task = osal_kthread_create(
        (osal_kthread_handler)key_process_task, NULL,
        "KeyProcess", 4096);
    osal_kthread_set_priority(task, OSAL_TASK_PRIORITY_LOW);
}
app_run(app_entry);

ISR 中写队列——非阻塞

static int key_isr(int irq, void *dev)
{
    (void)irq;
    (void)dev;
    int ret;

    key_event_t event;
    event.timestamp = osal_get_tick();  // 记录时间戳
    event.level = 0;                     // 按下

    /* ISR 中必须用 OSAL_MSGQ_NO_WAIT——绝不阻塞
       队列满时返回非 0,消息被丢弃 */
    ret = osal_msg_queue_write_copy(g_key_queue_id,
                                     (void *)&event,
                                     sizeof(key_event_t),
                                     OSAL_MSGQ_NO_WAIT);
    if (ret != 0) {
        /* 队列满了——任务处理不过来
           产品代码中可以记录丢事件次数用于诊断 */
    }

    return OSAL_IRQ_HANDLED;
}

任务中读队列——阻塞等待

static int key_process_task(void *data)
{
    (void)data;
    key_event_t event;
    unsigned int msg_size;
    int count = 0;

    while (1) {
        /* 阻塞等待——没按键时休眠,CPU 可进低功耗 */
        msg_size = sizeof(key_event_t);
        int ret = osal_msg_queue_read_copy(g_key_queue_id,
                                            (void *)&event,
                                            &msg_size,
                                            OSAL_MSGQ_WAIT_FOREVER);
        if (ret != 0) continue;  // 读失败,重试

        count++;
        printf("key: %d (tick=%u)\n", count, event.timestamp);

        /* 继续循环——再次阻塞在 read_copy 上 */
    }
    return 0;
}

对比 ISR 和任务中对队列的调用:ISR 用 NO_WAIT(非阻塞),任务用 WAIT_FOREVER(阻塞等)。这个区别是 ISR 编程的第一法则——ISR 中永远不阻塞。

更健壮的写法——超时 + 错误处理

/* 任务中读队列——带超时版本 */
msg_size = sizeof(key_event_t);
ret = osal_msg_queue_read_copy(g_key_queue_id,
                                (void *)&event,
                                &msg_size,
                                1000);  // 1 秒超时
if (ret == 0) {
    /* 成功读到数据 */
    count++;
    printf("key: %d\n", count);
} else {
    /* 超时——1 秒内没按键,可以做其他事(如喂狗、心跳) */
    printf("idle...\n");
}