发新帖本帖赏金 80.00元(功能说明)我要提问
返回列表
打印
[APM32F4]

uc/os3-消息队列(上)

[复制链接]
926|4
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
DKENNY|  楼主 | 2024-7-6 15:20 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式
本帖最后由 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 中消息队列实现的各个函数,以及一个实例的搭建。

使用特权

评论回复

打赏榜单

21小跑堂 打赏了 80.00 元 2024-07-18
理由:恭喜通过原创审核!期待您更多的原创作品~

评论
21小跑堂 2024-7-18 14:57 回复TA
详细的介绍了实时操作系统中消息队列的概念及工作过程,图文结合并举例,易消化理解,实例操作中代码解析清晰。整体较佳 
沙发
chenjun89| | 2024-7-7 20:31 | 只看该作者
还在用ucOS-II,ucOS-III区别大吗?

使用特权

评论回复
板凳
DKENNY|  楼主 | 2024-7-7 20:47 | 只看该作者
chenjun89 发表于 2024-7-7 20:31
还在用ucOS-II,ucOS-III区别大吗?

特性
uCOS-II
uCOS-III
优先级管理
每个任务有唯一的优先级
允许多个任务共享同一优先级
内核结构
非抢占式,时间分片简单
抢占式,支持动态优先级变化
任务切换
简单,因为优先级唯一
复杂,支持同一优先级的任务调度
中断处理
简单直接,适合低复杂度中断处理
高级,支持更多嵌套和复杂操作
互斥机制
优先级反转避免
优先级继承机制,更有效处理优先级反转
内存管理
简单的内存管理功能
增强的内存管理,包括任务堆栈检查等高级功能
配置和灵活性
配置选项较少,适合单一应用场景
配置丰富,适合复杂嵌入式系统,具有更高的灵活性

uCOS-III 在多任务管理、内核复杂度、实时响应能力以及系统灵活性等方面相比 uCOS-II 有显著提升,适合复杂和多样的嵌入式系统应用。

使用特权

评论回复
地板
田舍郎| | 2024-7-7 22:47 | 只看该作者
队列,链表,互斥

使用特权

评论回复
发新帖 本帖赏金 80.00元(功能说明)我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

30

主题

53

帖子

6

粉丝