µCOS-III从入门到精通 第十章(µC/OS-III消息队列)
参考教程:【正点原子】手把手教你学UCOS-III实时操作系统_哔哩哔哩_bilibili
一、队列介绍
1、概述
(1)队列是任务到任务、中断到任务数据交流的一种机制,它不同于全局变量。假设有一个全局变量a,现有两个任务都在写这个变量a,如下所示,变量自增分为三个步骤,如果在任务1读数据以后、修改数据以前发生任务切换,这将导致任务2和任务1读取相同的数据,并且基于相同的数据做相同的修改,这显然是有问题的,而使用队列可以避免这种问题(指访问冲突)。
(2)µC/OS-III基于队列实现了多种功能,并且读写队列做好了保护(主要依靠临界区),防止多任务同时访问冲突,用户只需要直接调用API函数即可。(中断不可以调用队列接收函数,但是可以调用队列发送函数)
2、µC/OS-III队列的特点
(1)数据入队出队方式:队列通常采用“先进先出”(FIFO)的数据存储缓冲机制,即先入队的数据会先从队列中被读取;µC/OS-III中也可以配置为“后进先出”(LIFO)方式。
(2)数据传递方式:µC/OS-III中的队列数据是一个“万能指针”(void*类型的指针),可以指向任何数据甚至是函数,所以发送方和接收方必须按照约定好的方式去发送和接收消息,这样才能正常解析接收到的消息。
(3)队列不属于某个任务,任何任务和中断都可以向队列发送消息,但是读取消息只能在任务中,不支持中断读取消息。
(4)当任务向一个队列读取消息时,可以指定一个阻塞时间。
假设此时队列已空无法出队(出队阻塞):
若阻塞时间为0,其它任务打算让数据出队时会死等(期间任务将会进入阻塞态),一直等到队列中有数据可以出队为止。
若阻塞时间不为0,其它任务打算让数据出队时会等待设定的阻塞时间(期间任务将会进入阻塞态),若在该时间内还无数据可出队,超时后直接返回,不再等待。
(5)当任务向一个队列写入消息时,无论如何都不会引起阻塞。
二、队列相关API函数介绍
1、概述
(1)使用队列的主要流程:创建队列→写队列(发送消息到队列)→读队列(获取消息队列的数据)。
(2)队列相关API函数概览:
函数 | 描述 |
OSQCreate | 创建一个消息队列 |
OSQDel | 删除一个消息队列 |
OSQFlush | 清空消息队列中的所有消息 |
OSQPend | 获取消息队列中的消息 |
OSQPendAbort | 终止任务挂起等待消息队列 |
OSQPost | 发送消息到消息队列 |
2、函数定义概览
(1)OSQCreate函数定义:
void OSQCreate
(
OS_Q* p_q,
CPU_CHAR* p_name,
OS_MSG_QTY max_qty,
OS_ERR* p_err
)
形参 | 描述 |
p_q | 指向消息队列结构体的指针 |
p_name | 指向作为消息队列名的 ASCII 字符串的指针 |
max_qty | 消息队列的大小 |
p_err | 指向接收错误代码变量的指针 |
(2)OSQPost函数定义:
void OSQPost
(
OS_Q* p_q,
void* p_void,
OS_MSG_SIZE msg_size,
OS_OPT opt,
OS_ERR* p_err
)
形参 | 描述 |
p_q | 指向消息队列结构体的指针 |
p_void | 指向消息的指针 |
msg_size | 消息的大小,单位: 字节 |
opt | OS_OPT_POST_FIFO :将发送的消息保存在队列的末尾 |
p_err | 指向接收错误代码变量的指针 |
(3)OSQPend函数定义:
void OSQPost
(
OS_Q* p_q,
void* p_void,
OS_MSG_SIZE msg_size,
OS_OPT opt,
OS_ERR* p_err
)
形参 | 描述 |
p_q | 指向消息队列结构体的指针 |
timeout | 任务挂起等待消息队列的最大允许时间,当为0时,表示将一直等待,直到接收到消息 |
opt | OS_OPT_PEND_BLOCKING:如无任何消息存在就阻塞任务OS_OPT_PEND_NON_BLOCKING:如无任何消息就直接返回 |
p_msg_size | 指向一个变量用来表示接收到的消息长度(字节数) |
p_ts | 指向接收消息队列接收时的时间戳的变量的指针,为NULL,说明用户没有要求时间戳 |
p_err | 指向接收错误代码变量的指针 |
三、队列操作实验
1、原理图与实验目标
(1)原理图(未画出OLED屏,接法与stm32教程中的一致):
(2)实验目标:
①设计4个任务——start_task、task1、task2、task3:
[1]start_task:用于创建其它三个任务,然后删除自身。
[2]task1:当按键key0按下,将当前存储按键次数变量的地址拷贝到队列key_queue中;当按键key1按下,将传输大容量数据,拷贝大容量数据的地址到队列big_date_queue中。(入队)
[3]task2:读取队列key_queue中的消息,在OLED屏上打印出按键次数。(出队)
[4]task3:从队列big_date_queue读取大容量数据地址,通过地址访问大容量数据,并通过OLED显示。(出队)
②预期实验现象:
[1]程序下载到板子上后,暂时没有任何现象。
[2]按下相关按键,OLED屏会有相应变化。
2、实验步骤
(1)将“任务创建和删除实验”的工程文件夹复制一份,在拷贝版中进行实验。
(2)在UCOS_experiment.c文件中添加头文件OLED.h,并定义一个整型数组(全局变量,作为大容量数据)以及两个队列(分别为按键次数队列和大容量数据队列)。
#include "OLED.h"
int buffer[5] = {13, 32, 26, 114, 51};
OS_Q key_queue; //按键次数队列
OS_Q big_data_queue; //大容量数据队列
(3)在UCOS_Test函数中需要创建记录按键次数的队列和大容量数据队列,与它们的句柄一一对应。
void UCOS_Test(void)
{
OS_ERR err;
OSInit(&err); //初始化μC/OS-III
//创建Start Task
OSTaskCreate (&start_task_tcb,
"start_task",
(OS_TASK_PTR)start_task,
NULL,
START_TASK_PRIO,
start_task_stack,
START_TASK_STACK_SIZE / 10,
START_TASK_STACK_SIZE,
0,
0,
0,
(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),
&err);
OSQCreate(&key_queue, "key_queue", 1, &err); //创建队列key_queue,长度为1
OSQCreate(&big_data_queue, "big_data_queue", 1, &err); //创建队列big_data_queue,长度为1
OSStart(&err);
}
(4)更改task1、task2和task3函数的实现。
void task1(void)
{
OS_ERR err;
uint8_t key = 0;
unsigned int num = 0; //记录按键1按下次数
int* buf = buffer; //OSQPost中不能直接传&buffer
while(1)
{
key = Key_GetNum(); //读取按键键值
if(key == 1)
{
num++; //修改计数值
OSQPost(&key_queue, &num, sizeof(num), OS_OPT_POST_FIFO, &err);
}
else if(key == 2)
{
buffer[1]++; //修改大容量数据本身的值
OSQPost(&big_data_queue, &buf, sizeof(buffer), OS_OPT_POST_FIFO, &err);
}
OSTimeDly(10, OS_OPT_TIME_DLY, &err); //自我阻塞10ms
}
}
void task2(void)
{
OS_ERR err;
CPU_SR_ALLOC();
OS_MSG_SIZE num_length = sizeof(unsigned int);
int* buffer1;
while(1)
{
buffer1 = OSQPend(&key_queue, 0, OS_OPT_PEND_BLOCKING, &num_length, NULL, &err); //读不出来就死等
CPU_CRITICAL_ENTER(); //要屏蔽中断,防止与OLED通信时产生差错
OLED_ShowNum(1, 1, *buffer1, 5);
CPU_CRITICAL_EXIT();
OSTimeDly(500, OS_OPT_TIME_DLY, &err); //自我阻塞500ms
}
}
void task3(void)
{
OS_ERR err;
OS_MSG_SIZE big_data_length = sizeof(int*);
int** buffer2;
while(1)
{
buffer2 = OSQPend(&big_data_queue, 0, OS_OPT_PEND_BLOCKING, &big_data_length, NULL, &err); //读不出来就死等
OLED_ShowNum(2, 1, buffer[1], 5);
OLED_ShowNum(3, 1, (*buffer2)[1], 5);
OSTimeDly(500, OS_OPT_TIME_DLY, &err); //自我阻塞500ms
}
}
(5)在main.c文件中初始化OLED屏模块和按键模块。
int main(void)
{
/*模块初始化*/
OLED_Init(); //OLED初始化
Key_Init(); //按键初始化
UCOS_Test();
while (1)
{
}
}
(6)程序完善好后点击“编译”,然后将程序下载到开发板上,根据程序注释进行调试。
3、程序执行流程
(1)main函数全流程:
①初始化OLED模块、按键模块。
②调用UCOS_Test函数。
(2)测试函数全流程:
①创建记录按键次数的队列和大容量数据队列(下图未示出)。
②创建任务start_task。
(3)多任务调度执行阶段(发生在开启任务调度器以后):
①在前面的实验中已对任务调度方面做了多次详细解释,从本实验开始简单的任务调度将不再详解。
②按下按键1,task1函数中记录的按键1按下次数自增,并将存储按键次数变量的地址写进队列key_queue中;紧接着,原本task2函数中的OSQPend函数因为key_queue为空读不到数据而进入无限阻塞,现在key_queue有数据,OSQPend函数则将key_queue中的数据,也就是存储按键次数变量的地址取出,并解引用获取最新的按键次数,显示在OLED屏上;以此往复,每次按下按键1,task1就会往key_queue中写一次存储按键次数变量的地址,而task2会将它马上取出并解引用获取最新的按键次数。
③按下按键2,task1函数先修改大容量数据本身的值,然后将大容量数据的地址写进队列big_data_queue中;紧接着,原本task3函数中的OSQPend函数因为big_data_queue为空读不到数据而进入无限阻塞,现在big_data_queue有数据,OSQPend函数则将big_data_queue中的数据,也就是大容量数据的地址取出,并通过这个地址访问大容量数据,显示一部分在OLED屏上;以此往复,每次按下按键2,task1就会往big_data_queue中写一个数据的地址,而task3会将它马上取出,然后可以通过地址访问数据。