消息队列
ISR 到任务的数据传递 — OSAL 消息队列、GPIO 驱动
学习目标
- 理解消息队列在 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_id是unsigned 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 上 |
程序运行流程:
- 入口创建消息队列 → 注册 GPIO 中断 → 创建处理任务
- 处理任务阻塞在
read_copy上,CPU 休眠 - 按键按下 → ISR 写时间戳到队列 → 任务被唤醒 → 打印按键序号
- 任务继续
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
案例操作指导
第一步:编译
第二步:烧录
将固件烧录到 WS63 开发板,按键接指定 GPIO 引脚。
第三步:验证
按一下按键:
快速连按 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");
}