在前面的章节中,讲解了 µC/OS-III 中的几种信号量,信号量一般用于任务之间的同步,但是在实际的项目开发当中,经常会遇到需要在任务之间进行通信,任务间的通信,指的是消息的传递,µC/OS-III 提供了消息队列的机制,通过这一机制,就能够很好地在任务间传递消息。本章就来学习 µC/OS-III
中的消息队列。
1 µC/OS-III 消息队列简介
任务与其他任务或任务与中断间的通讯一般可以通过全局变量或消息队列来完成,如果是使用全局变量的话,那么这个全局变量将被作为任务与任务之间或任务与中断之间的共享资源,因此开发者在设计时还需要考虑该共享资源的互斥访问问题,并且当全局变量被一个任务或中断访问更新后,通讯中全局变量的接收任务无法知道该全局变量是否已经被更新,从而无法实时地获取该全局变量的最新数据,正是由于使用全局变量进行任务与任务或任务与中断之间的通讯存在诸多的弊端,因此诞生了消息队列这一机制。
消息队列中包含了消息,这些消息可以通过消息队列进行传输,也可以以直接送到指定的任务中,因为每个任务都可以有独自的内嵌消息队列(这在后面的章节中会进行讲解)。通过消息队列传输的消息,可以发送给多个任务。
下面来看一下消息的数据类型结构体,消息的数据类型结构体定义在文件 os.h
中,具体的代码如下所示:
struct os_msg{
OS_MSG* NextPtr; /* 指向下一条消息的指针 */
void* MsgPtr; /* 指向消息内容的指针 */
OS_MSG_SIZE MsgSize; /* 消息内容的大小,单位:字节 */
#if (OS_CFG_TS_EN > 0u)
CPU_TS MsgTS; /* 消息发送时的时间戳 */
#endif
};
可以看到,消息的结构中包含了一个指向消息内容的指针 MsgPtr
,其数据类型为
void*
,这可以理解为是一个“万能指针”,MsgPtr
可以指向任何的数据,甚至可以指向一个函数,因此消息的发送方和接收方必须按照实现约定好的方式去发送和接收消息,只有这样,消息的接收方才能够正确地解析接收到的消息。
前面说到,消息是通过消息队列进行传输的,任务和中断都能够操作消息队列,但是中断只能往消息队列中发送消息,而不能从消息队列中接收消息,如下图所示:
消息队列中的消息是以 LIFO
的方式传输消息的,即以后进先出的方式传输消息。
LIFO
的传输机制在任务或中断需要向任务发送紧急的消息的情况下显得非常有用,在这种情况下,紧急的消息将比消息队列中已有的其他非紧急消息更早地被任务接收。
在上图中可以看到,在靠近接收任务的地方有一个代表超时“沙漏”,这表示任务从消息队列中接收消息是可以指定超时时间的,这个超时时间说明接收消息的任务愿意在消息队列中无消息的时候,等待一定的时间,如果超过了指定的这段时间,消息队列中还是没有消息,那么将接收任务将收到超时相应的错误代码,当然,也可以指定等待的时间为无限长,那么接收消息的任务将一直被挂起,直到消息队列中有消息。
消息队列中还包含了挂起等待任务链表,这意味着,可以有多个任务同时等待同一个消息队列中的消息,如下图所示:
在这种情况下,发送消息的任务或中断,可以指定发送消息到挂起等待任务链表中任务优先级最高的任务或者是发送到挂起等待任务链表中的所有任务。如果因接收到消息而被解除挂起状态的任务的任务优先级高于发送消息的任务的任务优先级,那么 µC/OS-III
将会立马转去执行任务优先级高的接收任务。
2 µC/OS-III 消息队列相关 API 函数
µC/OS-III 提供了一些列操作消息队列的
API
函数,这些操作函数如下表所示:
函数
|
描述
|
OSQCreate()
|
创建一个消息队列
|
OSQDel()
|
删除一个消息队列
|
OSQFlush()
|
清空消息队列中的所有消息
|
OSQPend()
| 获取消息队列中的消息 |
OSQPendAbort()
|
终止任务挂起等待消息队列
|
OSQPost()
|
发送消息到消息队列
|
3 µC/OS-III 消息队列实验
本实验的程序流程图,如下图所示:
(1) start_task
任务
start_task 任务的入口函数代码如下所示:
/**
* @brief start_task
* @param p_arg : 传入参数(未用到)
* @retval 无
*/
void start_task(void *p_arg)
{
OS_ERR err;
CPU_INT32U cnts;
/* 初始化 CPU 库 */
CPU_Init();
/* 根据配置的节拍频率配置 SysTick */
cnts = (CPU_INT32U)(HAL_RCC_GetSysClockFreq() / OSCfg_TickRate_Hz);
OS_CPU_SysTickInit(cnts);
/* 开启时间片调度,时间片设为默认值 */
OSSchedRoundRobinCfg(OS_TRUE, 0, &err);
/* 创建消息队列 */
OSQCreate( (OS_Q* )&q,
(CPU_CHAR* )"q",
(OS_MSG_QTY )1,
(OS_ERR* )&err);
/* 创建 Task1 */
Task1Task_STK = (CPU_STK *)mymalloc( SRAMIN,
TASK1_STK_SIZE * sizeof(CPU_STK));
OSTaskCreate( (OS_TCB* )&Task1Task_TCB,
(CPU_CHAR* )"task1",
(OS_TASK_PTR )task1,
(void* )0,
(OS_PRIO )TASK1_PRIO,
(CPU_STK *)Task1Task_STK,
(CPU_STK_SIZE )TASK1_STK_SIZE / 10,
(CPU_STK_SIZE )TASK1_STK_SIZE,
(OS_MSG_QTY )0,
(OS_TICK )0,
(void* )0,
(OS_OPT )(OS_OPT_TASK_STK_CHK|OS_OPT_TASK_STK_CLR),
(OS_ERR* )&err);
/* 创建 Task2 */
Task2Task_STK = (CPU_STK *)mymalloc( SRAMIN,
TASK2_STK_SIZE * sizeof(CPU_STK));
OSTaskCreate( (OS_TCB* )&Task2Task_TCB,
(CPU_CHAR* )"task2",
(OS_TASK_PTR )task2,
(void* )0,
(OS_PRIO )TASK2_PRIO,
(CPU_STK* )Task2Task_STK,
(CPU_STK_SIZE )TASK2_STK_SIZE / 10,
(CPU_STK_SIZE )TASK2_STK_SIZE,
(OS_MSG_QTY )0,
(OS_TICK )0,
(void* )0,
(OS_OPT )(OS_OPT_TASK_STK_CHK|OS_OPT_TASK_STK_CLR),
(OS_ERR* )&err);
/* 删除 Start Task */
OSTaskDel((OS_TCB *)0, &err);
}
从上面的代码中可以看出,start_task 任务使用函数
OSTaskCreate()
创建了
task1
任务和
task2任务,并且 task1
任务和
task2
任务的任务栈都是动态分配的,当然,也可以静态地定任务栈,就像定义一个数组一样。同时还创建一个消息队列。
(2) task1
任务
/**
* @brief task1
* @param p_arg : 传入参数(未用到)
* @retval 无
*/
void task1(void *p_arg)
{
uint8_t key;
OS_ERR err;
while (1)
{
key = key_scan(0);
if (key != 0)
{
OSQPost( (OS_Q* )&q,
(void* )&key,
(OS_MSG_SIZE )sizeof(key),
(OS_OPT )OS_OPT_POST_FIFO,
(OS_ERR* )&err);
}
OSTimeDly(10, OS_OPT_TIME_DLY, &err);
}
}
从上面的代码中可以看出,task1
任务还是比较简单的,就是扫描按键,然后将扫描到的按键键值作为消息发送到消息队列中。
(3) task2
任务
/**
* @brief task2
* @param p_arg : 传入参数(未用到)
* @retval 无
*/
void task2(void *p_arg)
{
uint32_t task2_num = 0;
uint8_t* key;
OS_MSG_SIZE size;
OS_ERR err;
while (1)
{
key = (uint8_t *)OSQPend( (OS_Q *)&q,
(OS_TICK)0,
(OS_OPT)OS_OPT_PEND_BLOCKING,
(OS_MSG_SIZE *)&size,
(CPU_TS *)0,
(OS_ERR *)&err);
switch (*key)
{
case KEY0_PRES:
{
lcd_fill(6, 131, 233, 313, lcd_discolor[++task2_num % 11]);
break;
}
case KEY1_PRES:
{
LED0_TOGGLE();
break;
}
default:
{
break;
}
}
}
}
从上面的代码中可以看出,task2
任务就是不断地挂起等待获取消息队列中的消息,获取到消息后,将消息作为按键键值进行解析,然后对解析出的不同按键键值结果进行不同的解释,当接收到按键键值 0
的消息时,对
LCD
区域进行刷新,当接收到按键键值
1
的消息时,则改变LED0 的状态。
按下按键 0
,使得
task1
任务往消息队列中发送按键键值为
0
的消息,然后
task2
任务就能够从消息队列中获取到并解析出内容为按键键值 0
的消息,接着执行刷新
LCD
的区域显示的操作。
如果多按几下按键 0
,那么
task2
任务就会多次接收到键值
0
的消息,从而多次地去刷新LCD。如果按键按键
1
,那么
task1
任务就会发送键值为
1
的消息,
task2
任务就会收到该消息,闭关翻转 LED0
。