本帖最后由 DKENNY 于 2024-7-6 16:26 编辑
#申请原创# @21小跑堂
在嵌入式系统开发中,实时操作系统(RTOS)如uC/OS-II在任务调度、资源管理和通信等方面提供了强大的功能。消息队列(Message Queue)作为其中一种重要的通信机制,允许不同任务间进行安全高效的数据传输与同步。本文将深入探讨uC/OS中的消息队列功能,包括其工作原理、实现方法以及使用实例。
1、消息队列的基本概念 消息队列是一种常用于不同任务间通信的工具,也可以用于任务与中断之间的信息传递。通过消息队列,任务可以接收来自其他任务或中断的各种长度的消息,并在需要时从队列中读取这些消息。当消息队列为空时,尝试读取消息的任务会被阻塞,可以设置一个超时时间,在这段时间内如果没有新消息,任务会自动切换状态以保持效率。一旦队列中有新消息,阻塞的任务会被唤醒来处理这些消息。消息队列的方式是异步的,使得任务可以在不需要等待对方直接响应的情况下,继续执行自己的任务。 在消息队列服务中,任务或中断服务程序可以把消息放入队列,而一个或多个任务可以从中获取消息。消息按照先进先出(FIFO)原则一般传递给任务,但也可以根据需要支持后进先出(LIFO)方式。除此之外,消息队列也支持超时机制,允许传递不同长度和类型的消息,因为实际上只传递了指针。一个任务可以与任意消息队列进行通信,同时多个任务也可以共享同一个消息队列。当消息队列使用完毕后,可以通过删除队列函数对其进行清理。消息队列因其灵活性和效率常被用于实现任务间的异步通信。
2、消息队列的工作过程 在 μCOS-III 中,定义了一个名为 OSCfg_MsgPool 的数组,大小为 OS_CFG_MSG_POOL_SIZE。这个数组在系统初始化时被串成一个单向链表,形成了所谓的消息池,其中每个元素都代表一个消息。这样的设计之所以选择单向链表而不是常见的双向链表,是因为在这里消息的存取操作只需要在链表的首尾进行,不需要在中间进行操作,因此单向链表足以满足需求,而双向链表则会增加不必要的复杂性。 假设我们定义了一个大小为 10 的消息池(OS_CFG_MSG_POOL_SIZE = 10),其中每个消息元素代表一条消息。当系统中需要使用消息队列时,可以从消息池中取出消息,挂载到相应的队列上,表示该队列拥有这条消息。当这条消息被使用完毕后,会被释放回到消息池中,其他队列也可以再次从消息池中取出并使用这条消息。这样,系统中所有被创建的队列都可以共享消息池中的消息资源,实现了消息资源的高效共享和重复利用。 举个例子,假设有两个消息队列 A 和 B,它们都需要使用消息池中的消息资源。当队列 A从消息池中取出一条消息后,使用完毕后释放回消息池,队列B随后又可以取出同一条消息并使用,实现了消息资源的共享和重复利用。这种设计使得系统中的消息队列能够快速高效地进行消息存取操作,提高了系统的性能和资源利用率。
2.1 消息池初始化 在系统初始化的过程中(通过 OSInit() 函数),系统会对消息池进行初始化操作。这个初始化过程包括调用 OS_MsgPoolInit() 函数来初始化消息池,该函数的定义位于 os_msg.c 文件中。通过 OS_MsgPoolInit() 函数,系统会对消息池中的消息资源进行适当的设置和准备工作,以确保消息池能够正常运作并提供给系统中的消息队列使用。这个步骤是系统启动时必要的一部分,以确保消息资源的有效管理和共享。
OS_MsgPoolInit()源码 void OS_MsgPoolInit (OS_ERR *p_err)
{
OS_MSG *p_msg1; // 定义指向 OS_MSG 类型的指针 p_msg1
OS_MSG *p_msg2; // 定义指向 OS_MSG 类型的指针 p_msg2
OS_MSG_QTY i; // 定义一个用于循环计数的变量 i
OS_MSG_QTY loops; // 定义一个变量 loops,用于表示循环次数
#if (OS_CFG_ARG_CHK_EN > 0u)
// 检查消息池基地址是否为空,若为空,则返回错误码 OS_ERR_MSG_POOL_NULL_PTR
if (OSCfg_MsgPoolBasePtr == (OS_MSG *)0) {
*p_err = OS_ERR_MSG_POOL_NULL_PTR;
return;
}
// 检查消息池大小是否为0,若为0,则返回错误码 OS_ERR_MSG_POOL_EMPTY
if (OSCfg_MsgPoolSize == 0u) {
*p_err = OS_ERR_MSG_POOL_EMPTY;
return;
}
#endif
p_msg1 = OSCfg_MsgPoolBasePtr; // 初始化指针 p_msg1 指向消息池的基地址
p_msg2 = OSCfg_MsgPoolBasePtr; // 初始化指针 p_msg2 指向消息池的基地址的下一个位置
p_msg2++; // p_msg2 后移一位,指向消息池中的第二个元素
loops = OSCfg_MsgPoolSize - 1u; // 计算循环次数,消息池大小减1
for (i = 0u; i < loops; i++) { // 循环初始化消息池中的每个消息元素
p_msg1->NextPtr = p_msg2; // 将 p_msg1 的下一个指针指向 p_msg2
p_msg1->MsgPtr = (void *)0; // 消息指针置为空
p_msg1->MsgSize = 0u; // 消息大小置为 0
#if (OS_CFG_TS_EN > 0u)
p_msg1->MsgTS = 0u; // 如果支持时间戳功能,消息时间戳置为 0
#endif
p_msg1++; // p_msg1 后移
p_msg2++; // p_msg2 后移
}
p_msg1->NextPtr = (OS_MSG *)0; // 最后一个消息元素的 NextPtr 设置为 NULL
p_msg1->MsgPtr = (void *)0; // 最后一个消息元素的消息指针置为空
p_msg1->MsgSize = 0u; // 最后一个消息元素的消息大小置为 0
#if (OS_CFG_TS_EN > 0u)
p_msg1->MsgTS = 0u; // 如果支持时间戳功能,最后一个消息元素的时间戳置为 0
#endif
OSMsgPool.NextPtr = OSCfg_MsgPoolBasePtr; // 消息池链表的下一个指针指向消息池的基地址
OSMsgPool.NbrFree = OSCfg_MsgPoolSize; // 可用消息数量等于消息池大小
OSMsgPool.NbrUsed = 0u; // 已用消息数量初始化为0
#if (OS_CFG_DBG_EN > 0u)
OSMsgPool.NbrUsedMax = 0u; // 最大已用消息数量初始化为0
#endif
*p_err = OS_ERR_NONE; // 返回无错
}
如下是其初始化完成的消息池示意图。
2.2 消息队列的运作机制 在 μCOS 操作系统中,消息队列控制块是由多个元素构成的结构体。当一个消息队列被创建时,编译器会静态地为这个消息队列分配内存空间。这是因为我们需要定义一个消息队列控制块,它包含了消息队列的各种信息,例如队列的名称、队列可以容纳的最大消息数量、入队和出队的指针等。一旦消息队列创建成功,这些内存空间就会被占用。在创建队列时,用户会指定队列的最大消息数量,这个数量一旦设置就无法更改。每个消息空间都可以存储任意类型的数据。 任务或中断服务程序都可以向消息队列发送消息。当有消息发送时,如果队列未满,μCOS 会从消息池中取出一个消息元素,并将其添加到队列的尾部。消息元素中的 MsgPtr 成员变量会指向要发送的数据。如果队列已满,μCOS 会返回错误代码,表示消息入队失败。 μCOS 还支持发送紧急消息,这种消息遵循后进先出(LIFO)的原则。发送紧急消息的过程与普通消息类似,不同之处在于紧急消息会被添加到队列的头部而不是尾部。这样做可以确保接收者能够优先接收到紧急消息,并及时处理。 当任务尝试从队列中读取消息时,可以指定一个阻塞超时时间。如果在这段时间内队列为空,任务会保持阻塞状态,等待队列中有有效数据。如果在等待期间其他任务或中断服务程序向队列中写入数据,阻塞的任务会自动转为就绪状态。如果等待时间超过了指定的阻塞时间,即使队列中没有有效数据,任务也会从阻塞状态转为就绪状态。 当消息队列不再需要时,可以将其删除。一旦删除操作完成,消息队列会被永久删除,所有关于该队列的信息都会被清除,直到它被再次创建才能使用。
消息队列运作过程如下:
3、消息队列的阻塞机制 在μCOS操作系统中,消息队列是一个公共资源,可以被多个任务共同访问和操作。为了确保任务在读取消息时的顺序性和安全性,μCOS提供了阻塞机制。这个机制允许任务在尝试读取(出队)操作时,如果发现队列中没有消息,可以选择如何处理这种情况: 1. 非阻塞:任务A发现队列中没有消息,可以选择立即离开,去执行其他操作。这种情况下,任务A不会进入阻塞状态。 2. 有限时间阻塞:任务A可以选择等待一段时间,比如1000个系统时钟节拍(ticks)。在这段时间内,如果队列接收到消息,任务A会从阻塞状态变为就绪状态,并处理消息。如果在1000个ticks结束后仍没有消息,任务A会被唤醒,返回一个错误代码,并继续执行其他代码。 3. 无限时间阻塞:任务A可以选择无限期等待,直到队列中有消息为止。这种情况下,任务A会一直保持阻塞状态,直到能够读取到消息。
如果有多个任务同时阻塞在同一个消息队列上,μCOS会根据任务的优先级来决定哪个任务先获得消息。优先级高的任务会先获得队列的访问权。 此外,如果在发送消息时选择了广播方式,那么所有等待中的任务都会收到相同的消息。这样,每个任务都可以得到通知,而不是只有一个任务接收到消息。 通过这种方式,μCOS确保了消息队列的有效管理和任务间的公平性,同时也保护了任务在读取消息时的顺序性和完整性。这是一个非常重要的特性,因为它允许多个任务能够高效且安全地共享信息。
4、消息队列的结构 在μCOS操作系统中,消息队列是通过一个特殊的数据结构来管理的,这个结构被称为 OS_Q 。当我们创建一个消息队列时,实际上是在定义一个 OS_Q类型的变量 ,这个变量可以被视为消息队列的句柄或者引用。这个句柄包含了消息队列的所有必要信息,使得系统能够有效地管理消息队列。
OS_Q数据结构包含以下几个关键元素: - 基本信息:如队列的名称、最大消息数量、当前消息数量、入队和出队指针等。 - PendList:这是一个链表,用于管理那些正在等待(阻塞)消息队列变得可用的任务。当消息队列为空时,尝试读取队列的任务会被添加到这个链表中,并根据它们的优先级进行排序。 - MsgQ:这是一个指向实际存储消息的内存区域的指针。消息队列中的每个消息都会在这个区域中有一个对应的位置。 通过这种方式,μCOS能够确保即使在多任务环境中,消息队列的访问也是有序和安全的。PendList链表使得系统能够追踪哪些任务正在等待消息,并根据优先级决定哪个任务应该首先接收消息。这样,即使在高度竞争的环境中,也能保证消息的公平分配和任务的同步。 总的来说,OS_Q结构是μCOS中实现消息队列功能的核心,它不仅包含了队列的基本信息,还提供了系统管理和任务同步所需的所有机制。这使得μCOS成为一个强大且灵活的实时操作系统,能够满足各种复杂场景下的需求。
os_q结构体 struct os_q { /* Message Queue */
/* ------------------ GENERIC MEMBERS ------------------ */
#if (OS_OBJ_TYPE_REQ > 0u)
OS_OBJ_TYPE Type; /* Should be set to OS_OBJ_TYPE_Q */
#endif
#if (OS_CFG_DBG_EN > 0u)
CPU_CHAR *NamePtr; /* Pointer to Message Queue Name (NUL terminated ASCII) */
#endif
OS_PEND_LIST PendList; /* List of tasks waiting on message queue */
#if (OS_CFG_DBG_EN > 0u)
OS_Q *DbgPrevPtr;
OS_Q *DbgNextPtr;
CPU_CHAR *DbgNamePtr;
#endif
/* ------------------ SPECIFIC MEMBERS ------------------ */
OS_MSG_Q MsgQ; /* List of messages */
};
os_msg_q结构体 struct os_msg_q { /* OS_MSG_Q */
OS_MSG *InPtr; /* Pointer to next OS_MSG to be inserted in the queue */
OS_MSG *OutPtr; /* Pointer to next OS_MSG to be extracted from the queue */
OS_MSG_QTY NbrEntriesSize; /* Maximum allowable number of entries in the queue */
OS_MSG_QTY NbrEntries; /* Current number of entries in the queue */
#if (OS_CFG_DBG_EN > 0u)
OS_MSG_QTY NbrEntriesMax; /* Peak number of entries in the queue */
#endif
#if (defined(OS_CFG_TRACE_EN) && (OS_CFG_TRACE_EN > 0u))
CPU_INT16U MsgQID; /* Unique ID for third-party debuggers and tracers. */
#endif
};
注意:在μCOS操作系统中,消息队列中的消息通常采用单向链表进行连接。不同于消息池,消息队列中的消息可以通过两种方式来存储和访问。 - FIFO模式(先进先出):在FIFO模式下,消息按照到达的顺序存储并被读取,就像排队一样。消息的存取位置分别位于链表的头部和尾部,这就导致了需要两个指针来指示消息的入队和出队位置。新的消息被添加到链表的尾部,而读取消息则从链表的头部开始。 - LIFO模式(后进先出):在LIFO模式下,消息被以后进先出的次序存储和读取,就像操作堆栈一样。所有的存取都发生在链表的同一端,这时只需要一个指针来指示消息的出队位置。 如果队列中已经存在许多未处理的消息,并且有一条紧急消息需要尽快传递给其他任务时,可以选择使用LIFO模式来发布消息。在LIFO模式下,最新的消息会被最先处理,而在FIFO模式下,则按照消息到达的先后顺序进行处理。 通过选择合适的消息存取模式,开发人员可以根据实际情况来优化消息处理的速度和效率。μCOS操作系统提供了灵活的消息队列管理方式,使得能够在不同场景下高效地存储、传递和处理消息。
FIFO模式(先进先出)
LIFO模式(后进先出)
这篇文章主要讲了消息队列的原理以及相关工作模式,下篇文章讲讲 ucos 中消息队列实现的各个函数,以及一个实例的搭建。
|
详细的介绍了实时操作系统中消息队列的概念及工作过程,图文结合并举例,易消化理解,实例操作中代码解析清晰。整体较佳