1)实验平台:正点原子开拓者FPGA 开发板
2)摘自《开拓者 Nios II开发指南》关注官方微信号公众号,获取更多资料:正点原子
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/index.html
第十九章uC/OSII消息邮箱与消息队列
uCOSII系统任务间的通信除信号量之外,还有消息邮箱和消息队列。本章我们将学习任务
间其它同步与通信的方法,消息邮箱和消息队列。本章包括以下几个部分:
19.1 消息邮箱和消息队列简介
19.2 实验任务
19.3 硬件设计
19.4 软件设计
19.5 下载验证
消息邮箱和消息队列简介
一个任务有时候需要和另一个任务交流信息,这个传递消息的过程就叫做任务间通信,任
务间的消息传递可以通过2种途径:一是通过全局变量,二是通过发布消息。
使用全局变量的时候每个任务或者中断服务程序都必须保证其对全局变量的独占访问。除
此之外,消息也可以通过消息邮箱(也称为邮箱)或者消息队列作为中介发布给任务。
消息邮箱
在多任务操作系统中,常常需要在任务与任务之间通过传递一个数据(这种数据叫做“消
息”)的方式来进行通信。为了达到这个目的,可以在内存中创建一个存储空间作为该数据的
缓冲区。如果把这个缓冲区称之为消息缓冲区,这样在任务间传递数据(消息)的最简单办法
就是传递消息缓冲区的指针。我们把用来传递消息缓冲区指针的数据结构叫做邮箱(消息邮箱)。
在uCOSII系统中,我们通过事件控制块的OSEventPrt来传递消息缓冲区指针,同时使事件
控制块的成员OSEventType为常数OS_EVENT_TYPE_MBOX,则该事件控制块就叫做消息邮箱。
两个任务通过消息邮箱进行通讯的示意图如图 19.1.1所示:
图 19.1.1 任务间通过消息进行通信
由上图可知,任务1向消息邮箱发送消息,这个过程叫做发送消息。任务2从消息邮箱中读
取消息,读取的过程叫做请求消息。需要注意的是,这里所说的消息并不是真正想要获取的数
据,而是获取的指针。指针指向的区域才是消息缓冲区,我们实际上是通过指针,来找到真正
想要获取的数据的。
消息邮箱API函数如下表所示:
图 19.1.2 消息邮箱API函数
下面我们来详细介绍下消息邮箱常用的几个API函数。
1)创建邮箱函数(OSMboxCreat)
创建邮箱通过函数OSMboxCreate实现,该函数原型为:OS_EVENT *OSMboxCreate (void
*pmsg)。函数中的参数msg为消息的指针,函数的返回值为消息邮箱的指针。
调用函数OSMboxCreate需先定义pmsg的初始值。在一般的情况下,这个初始值为NULL;但
也可以事先定义一个邮箱,然后把这个邮箱的指针作为参数传递到函数OSMboxCreate 中,使
之一开始就指向一个邮箱。
2)等待/请求消息邮箱函数(OSMboxPend)
当一个任务请求邮箱时需要调用函数OSMboxPend,这个函数的主要作用就是查看邮箱指针
OSEventPtr是否为NULL,如果不是NULL就把邮箱中的消息指针返回给调用函数的任务,同时用
OS_NO_ERR通过函数的参数err通知任务获取消息成功;如果邮箱指针OSEventPtr是NULL,则使
任务进入等待状态,并引发一次任务调度。
函数OSMboxPend的原型为:void *OSMboxPend (OS_EVENT *pevent, INT16U timeout,
INT8U *perr)。其中pevent为请求邮箱指针,timeout为等待时限,perr为错误信息。
3)释放/发送消息函数(OSMboxPost)
任务可以通过调用函数OSMboxPost向消息邮箱发送消息,这个函数的原型为:INT8U
OSMboxPost (OS_EVENT *pevent,void *pmsg)。其中pevent为消息邮箱的指针,pmsg为消息指
针。
消息队列
消息邮箱一次只能传输单条消息,当希望一次性向某个任务发送多则消息时,邮箱就有点
力不从心了,因为一个邮箱只能传输一则消息。把多个邮箱集中到一起管理和使用就变成了消
息队列,所以消息队列的操作和邮箱很相似。可以简单地认为,消息队列是邮箱数组。
两个任务通过消息队列进行通讯的示意图如图 19.1.3所示:
图 19.1.3 任务间通过消息邮箱进行通信
由上图可知,通过消息队列发送和请求消息的过程和邮箱较为类似,不同的是,消息队列
的指针指向的是消息缓冲区指针数组,而邮箱的指针指向的是消息缓冲区。这里的消息缓冲区
指针数组类似于FIFO的结构,会被写满和读空的,当消息缓冲区指针数组写满后,任务1不可
以继续发送消息;而当消息缓冲区指针数组为空时,任务2不可以继续请求消息。
消息队列API函数如下表所示:
图 19.1.4 消息队列API函数
下面我们来详细介绍下消息队列常用的几个API函数。
1)创建消息队列函数(OSQCreat)
创建一个消息队列首先需要定义一指针数组,然后把各个消息数据缓冲区的首地址存入这
个数组中,接下来再调用函数OSQCreate来创建消息队列。创建消息队列函数OSQCreate的原型
为:OS_EVENT *OSQCreate(void **start,INT16U size)。其中,start为存放消息缓冲区指针
数组的地址,size为该数组大小。该函数的返回值为消息队列指针。
2)请求消息队列函数(OSQPend)
请求消息队列的目的是为了从消息队列中获取消息。任务请求消息队列需要调用函数
OSQPend,该函数原型为:void *OSQPend(OS_EVENT *pevent,INT16U timeout,INT8U *perr)。
其中,pevent为所请求的消息队列的指针,timeout为任务等待时限,err为错误信息。
3)向消息队列发送消息函数(OSQPost)
任务可以通过调用函数OSQPost或OSQPostFront两个函数来向消息队列发送消息。函数
OSQPost以FIFO(先进先出)的方式组织消息队列,函数OSQPostFront以LIFO(后进先出)的
方式组织消息队列。这两个函数的原型分别为:INT8U OSQPost(OS_EVENT *pevent,void *pmsg)
和INT8U OSQPostFront (OS_EVENT *pevent,void *pmsg)。
其中,pevent为消息队列的指针,msg为待发消息的指针。
消息队列还有其他一些函数,这里我们就不介绍了,感兴趣的朋友可以参考《嵌入式实时
操作系统uCOSII原理及应用》第五章,关于队列更详细的介绍,也请参考该书。
实验任务
本节通过一个实例,让大家学习和掌握uCOSII系统消息邮箱和消息队列的使用方法。
硬件设计
Qsys系统搭建的步骤以及使用的IP核和“创建第一个uC/OSII系统”章节中完全一致,顶层模块代码除了模块名不一致外,其余也完全一致。如果大家有不明白的地方,可以参考“创
建第一个uC/OSII系统”章节实验,这里不再赘述。
软件设计
uCOSII系统的创建可以参考“创建第一个uC/OSII系统”章节中的软件设计部分。创建完
uCOSII系统之后,只需修改生成的代码,代码修改如下:
1 #include <stdio.h>
2 #include <unistd.h>
3 #include "includes.h"
4
5 //定义任务堆栈大小
6 #define TASK_STACKSIZE 2048
7
8 //定义各任务堆栈
9 OS_STK start_task_stk[TASK_STACKSIZE];
10 OS_STK mail_sender_stk[TASK_STACKSIZE];
11 OS_STK mail_receiver_stk[TASK_STACKSIZE];
12 OS_STK msg_sender_stk[TASK_STACKSIZE];
13 OS_STK msg_receiver_stk[TASK_STACKSIZE];
14
15 //分配各任务优先级
16 #define START_TASK_PRIORITY 5
17 #define MAIL_SENDER_PRIORITY 6
18 #define MAIL_RECEIVER_PRIORITY 7
19 #define MSG_SENDER_PRIORITY 8
20 #define MSG_RECEIVER_PRIORITY 9
21
22 //定义1个邮箱
23 OS_EVENT *mailbox;
24
25 //定义消息队列 (msgqueue)
26 #define QUEUE_SIZE 10 //消息队列大小
27 OS_EVENT *msgqueue; //消息队列事件指针
28 void *msgqueueTbl[QUEUE_SIZE]; //队列缓冲区
29
30 //用于发送消息队列的数据
31 INT32U data_arr[QUEUE_SIZE] = {0,1,2,3,4,5,6,7,8,9};
32
33 //局部函数声明
34 int initOSDataStructs(void); //初始化邮箱和消息队列的数据结构
35 int initCreateTasks(void); //初始化任务
36
37 //邮箱部分
38 //邮件发送任务: 每隔2s将当前时间发送给mailbox
39 void MailSend(void* pdata)
40 {
41 INT32U time;
42 while (1)
43 {
44 OSTimeDlyHMSM(0, 0, 2, 0);
45 time = OSTimeGet();
46 //把消息发送到mailbox
47 OSMboxPost(mailbox,(void *)&time);
48 printf("Task send the message: %lu in mailbox",time);
49 }
50 }
51
52 //邮件接收任务
53 void MailReceiver(void* pdata)
54 {
55 INT8U return_code = OS_NO_ERR;
56 INT32U *mbox_contents; //指向邮件内容的指针
57 while (1)
58 {
59 //从mailbox接收邮件,如果邮箱为空,则一直等待
60 mbox_contents = (INT32U *)OSMboxPend(mailbox, 0, &return_code);
61 printf("Task get the message: %lu in mailbox",(*mbox_contents));
62 OSTimeDlyHMSM(0, 0, 1, 0);
63 }
64 }
65
66 //消息队列部分
67 //发送消息任务: 将消息通过消息队列发送给所有等待消息的任务,当队列满时,延时2s
68 void MsgSender(void* pdata)
69 {
70 INT8U send_cnt = 0; //定义发送计数器,用于循环发送数组中的数据
71 OS_Q_DATA queue_data; //存放消息队列的事件控制块中的信息
72 while (1)
73 {
74 OSQQuery(msgqueue, &queue_data); //查询消息队列的信息
75 if(queue_data.OSNMsgs < QUEUE_SIZE) //查询消息队列是否已满
76 {
77 //消息队列未满时,将消息发送到消息队列
78 OSQPost(msgqueue,(void *)&data_arr[send_cnt]);
79 if(send_cnt == QUEUE_SIZE-1)
80 send_cnt = 0;
81 else
82 send_cnt++;
83 }
84 else
85 {
86 //消息队列已满,延时2s
87 OSTimeDlyHMSM(0, 0, 2, 0);
88 }
89 }
90 }
91
92 //消息接收任务:每200ms接收一次消息队列的消息
93 void MsgReceiver(void* pdata)
94 {
95 INT8U return_code = OS_NO_ERR;
96 INT32U *msg_rec; //存储接收到的消息
97 while (1)
98 { //到msgqueue接收消息,如果消息队列为空,则一直等待
99 msg_rec = (INT32U *)OSQPend(msgqueue, 0, &return_code);
100 printf("Receive message: %lu ",*msg_rec);
101 OSTimeDlyHMSM(0, 0, 0, 20); //延时200ms
102 }
103 }
104
105 //初始化各子任务
106 void start_task(void* pdata)
107 {
108 //初始化消息队列和邮箱的数据结构
109 initOSDataStructs();
110 //创建子任务
111 initCreateTasks();
112 OSTaskDel(OS_PRIO_SELF);
113 while (1);
114 }
115
116 int initOSDataStructs(void)
117 { //初始化邮箱的数据结构
118 mailbox = OSMboxCreate((void *)NULL);
119 //初始化消息队列的数据结构
120 msgqueue = OSQCreate(&msgqueueTbl[0], QUEUE_SIZE);
121 return 0;
122 }
123
124 int initCreateTasks(void)
125 {
126
127 //创建MailSender任务
128 OSTaskCreateExt(MailSend,
129 NULL,
130 &mail_sender_stk[TASK_STACKSIZE-1],
131 MAIL_SENDER_PRIORITY,
132 MAIL_SENDER_PRIORITY,
133 mail_sender_stk,
134 TASK_STACKSIZE,
135 NULL,
136 0);
137
138 //创建MAILReceiver任务
139 OSTaskCreateExt(MailReceiver,
140 NULL,
141 &mail_receiver_stk[TASK_STACKSIZE-1],
142 MAIL_RECEIVER_PRIORITY,
143 MAIL_RECEIVER_PRIORITY,
144 mail_receiver_stk,
145 TASK_STACKSIZE,
146 NULL,
147 0);
148 //创建MsgSender任务
149 OSTaskCreateExt(MsgSender,
150 NULL,
151 &msg_sender_stk[TASK_STACKSIZE-1],
152 MSG_SENDER_PRIORITY,
153 MSG_SENDER_PRIORITY,
154 msg_sender_stk,
155 TASK_STACKSIZE,
156 NULL,
157 0);
158
159 //创建MsgReceiver任务
160 OSTaskCreateExt(MsgReceiver,
161 NULL,
162 &msg_receiver_stk[TASK_STACKSIZE-1],
163 MSG_RECEIVER_PRIORITY,
164 MSG_RECEIVER_PRIORITY,
165 msg_receiver_stk,
166 TASK_STACKSIZE,
167 NULL,
168 0);
169
170 return 0;
171 }
172
173 //main函数
174 int main (void)
175 {
176 //创建父任务
177 OSTaskCreateExt(start_task,
178 NULL,
179 &start_task_stk[TASK_STACKSIZE-1],
180 START_TASK_PRIORITY,
181 START_TASK_PRIORITY,
182 start_task_stk,
183 TASK_STACKSIZE,
184 NULL,
185 0);
186 OSStart(); //启动uCOSII系统
187 return 0;
188 }
程序中共定义了5个任务,分别是开始任务、邮箱发送任务、邮箱接收任务、消息队列发
送任务和消息队列接收任务。
代码中第44行定义了邮箱的指针(mailbox),消息队列的指针(msgqueue)和队列缓冲
区(msgqueueTbl),定义邮箱的指针和消息队列的指针同样使用OS_EVENT进行定义。另外,
程序中也定义了用于发送消息队列的数组,数据为0、1、2、……,9。
程序中第173行至第188行是main函数,创建了一个开始任务,并启动uCOSII系统。开始任
务中调用了初始化邮箱和消息队列函数和初始化任务函数,在这两个函数执行完成后,通过调
用删除任务函数(OSTaskDel)删除开始任务,如代码中第105至第114行所示。
初始化邮箱和消息队列函数创建了一个消息邮箱和一个消息队列,我们在简介部分向大家
介绍了创建邮箱和消息队列的方法,即通过函数OSMboxCreate创建邮箱,通过OSQCreate创建
消息队列。
初始化任务函数创建了4个任务,邮箱发送任务(MailSend)、邮箱接收任务(MailReceiver)、
消息队列发送任务(MsgSender)和消息队列接收任务(MsgReceiver)。
邮箱发送任务实现每隔两秒将当前时间发送到邮箱(mailbox),发送邮箱的函数为
OSMboxPost,并打印发送的时间。
邮箱接收任务实现请求邮箱的功能,请求邮箱的函数为OSMboxPend。如果邮箱为空,则一
直等待,直到请求到邮箱,并打印邮箱指针指向的数据。
消息队列发送任务将消息通过消息队列中,当队列满时,则等待两秒。在发送消息之前,
先通过OS_Q_DATA定义消息队列事件控制块中的信息,然后通过OSQQuery函数查询消息队列是
否已满,queue_data.OSNMsgs记录了当前消息队列中已发送的个数。如果消息队列已满,则等
待两秒,否则循环发送队列缓冲数组的数据,数据为0、1、2、……,9。
消息接收任务每200毫秒接收一次消息队列的消息,接收消息的函数为OSQPend,并打印消
息队列指针指向的数据。
需要说明的是,本次实验只是演示邮箱和消息队列的使用方法,实际上邮箱相关的任务和
消息队列相关的任务没有什么联系。
下载验证
接下来编译工程,稍等片刻后console界面会显示Build Finished,即编译成功,此时就
可以下载程序了。开发板连接电源线和下载器,并打开电源开关。
首先下载sof文件,然后下载elf文件。程序下载完成,就会看到Nios II Console界面如
下图所示:
图 19.5.1 Nios II Console界面