Understanding the Differences Between RTOS Inter-Task Communication and Global Variables

Follow+Star Public Number, do not miss wonderful content

Understanding the Differences Between RTOS Inter-Task Communication and Global Variables

Source | Mculover666

1. Review of Knowledge Points

A queue is a data structure that allows elements to be inserted at one end and removed from the other, following the First-In-First-Out (FIFO) principle.

A circular queue can conveniently reuse this memory space while also adhering to the FIFO principle.

A priority queue does not follow FIFO but instead dequeues elements based on their priority, with the highest priority being dequeued first.

All content in this article is based on these two data structures. For implementations and usage examples of circular queues and priority queues in TencentOS-tiny, please read the article:

  • Data Structure | Implementation and Usage of Queues, Circular Queues, and Priority Queues in TencentOS-tiny

2. Message Queue

2.1. What is a Message Queue?

A message queue, as the name suggests, consists of two parts: message + queue, or it can be understood as a queue of messages.

① What is a message?

When data is transmitted between two different tasks, this data is referred to as a message. This message can be an integer value, a floating-point value, or even a structure, a pointer… Therefore, when using message queues in different RTOS, it is important to note whether you are passing a value or the address of that value.

The disadvantage of passing a value is that the length of the value can vary, leading to variable lengths of the entire message queue.

The length of a pointer is fixed at 4 bytes, so when passing a value, regardless of its type, you only pass the address of that value.

Passing an address also has its drawbacks. If a local variable is defined in a dynamic task task1 and its address is passed to task2, and then task1 is destroyed for some reason, causing memory recovery, the pointer pointing to that local variable becomes a dangling pointer, which is very dangerous. However, do not panic, it is a minor issue that can be avoided during programming.

In TencentOS-tiny, the message passed in the message queue refers to the address, while the message passed in the mailbox queue refers to the value.

② What is a queue?

If the underlying message queue uses a circular queue to store messages, it becomes a message queue, following the principle that messages sent first are received first.

If the underlying message queue uses a priority queue to store messages, it becomes a priority message queue, following the principle that the highest priority message is received first.

In TencentOS-tiny, both types of message queues exist, and will be described one by one below.

③ Pend-Post Mechanism

Regardless of the type of queue, there are two situations: when the queue is full, adding elements will result in an error; when the queue is empty, removing elements will also result in an error.

This issue can be cleverly resolved using the pend-post mechanism based on the queue, which is a wait-release mechanism.

When the queue is full, the incoming task1 can choose to pend for a while or wait indefinitely. Once an element is dequeued by task2, it calls post to release a signal, waking up the waiting task1.

Similarly, when the queue is empty, the incoming task1 can choose to pend for a while or wait indefinitely. Once an element is enqueued by task2, it calls post to release a signal, waking up the waiting task1.

Isn’t it clever?

Next, let’s look at the source code! Let’s see the demo! It’s clear at a glance~

2.2. Implementation of Message Queue

The implementation of the message queue in TencentOS-tiny is in tos_message_queue.h and tos_message_queue.c.

typedef struct k_message_queue_st {
    knl_obj_t   knl_obj;

    pend_obj_t  pend_obj;
    k_ring_q_t  ring_q;
} k_msg_q_t;

A pend_obj object is used to implement the pend-post mechanism, and a ring_q circular queue is used to store messages.

Isn’t it as I described? Once you understand it thoroughly, everything is not that mysterious~

Now let’s take a look at the API implementation for obtaining messages from the message queue:

__API__ k_err_t tos_msg_q_pend(k_msg_q_t *msg_q, void **msg_ptr, k_tick_t timeout)
{
    // Some source code omitted
    TOS_CPU_INT_DISABLE();

    if (tos_ring_q_dequeue(&msg_q->ring_q, msg_ptr, K_NULL) == K_ERR_NONE) {
        TOS_CPU_INT_ENABLE();
        return K_ERR_NONE;
    }
    
    pend_task_block(k_curr_task, &msg_q->pend_obj, timeout);

    TOS_CPU_INT_ENABLE();
    knl_sched();

    return err;
}

The API implementation for storing messages in the message queue is as follows:

__STATIC__ k_err_t msg_q_do_post(k_msg_q_t *msg_q, void *msg_ptr, opt_post_t opt)
{
    // Some source code omitted
    TOS_CPU_INT_DISABLE();

    if (pend_is_nopending(&msg_q->pend_obj)) {
        err = tos_ring_q_enqueue(&msg_q->ring_q, &msg_ptr, sizeof(void*));
        if (err != K_ERR_NONE) {
            TOS_CPU_INT_ENABLE();
            return err;
        }
        TOS_CPU_INT_ENABLE();
        return K_ERR_NONE;
    }

    if (opt == OPT_POST_ONE) {
        msg_q_task_recv(TOS_LIST_FIRST_ENTRY(&msg_q->pend_obj.list, k_task_t, pend_list), msg_ptr);
    } else { // OPT_POST_ALL
        TOS_LIST_FOR_EACH_ENTRY_SAFE(task, tmp, k_task_t, pend_list, &msg_q->pend_obj.list) {
            msg_q_task_recv(task, msg_ptr);
        }
    }

    TOS_CPU_INT_ENABLE();
    knl_sched();

    return K_ERR_NONE;
}

From the source code, we can see that if the opt flag is OPT_POST_ONE, it indicates waking one task, specifically waking the task with the highest priority on the waiting list of that message queue; if the opt flag is OPT_POST_ALL, all tasks are woken up.

2.3. Example of Using Message Queue

#define MESSAGE_MAX     10

uint8_t msg_pool[MESSAGE_MAX * sizeof(void *)];

k_msg_q_t msg_q;

void entry_task_receiver(void *arg)
{
    k_err_t err;
    void *msg_received;

    while (K_TRUE) {
        err = tos_msg_q_pend(&msg_q, &msg_received, TOS_TIME_FOREVER);
        if (err == K_ERR_NONE) {
            printf("receiver: msg incoming[%s]\n", (char *)msg_received);
        }
    }
}

void entry_task_sender(void *arg)
{
    char *msg_prio_0 = "msg 0 without priority";
    char *msg_prio_1 = "msg 1 without priority";
    char *msg_prio_2 = "msg 2 without priority";

    printf("sender: post a message 2 without priority\n");
    tos_msg_q_post(&msg_q, msg_prio_2);

    printf("sender: post a message 1 without priority\n");
    tos_msg_q_post(&msg_q, msg_prio_1);

    printf("sender: post a message 0 without priority\n");
    tos_msg_q_post(&msg_q, msg_prio_0);
}

The execution result is as follows:

TencentOS-tiny Port on STM32L431RCT6 By Mculover666
sender: post a message 2 without priority
sender: post a message 1 without priority
sender: post a message 0 without priority
receiver: msg incoming[msg 2 without priority]
receiver: msg incoming[msg 1 without priority]
receiver: msg incoming[msg 0 without priority]

3. Priority Message Queue

3.1. Implementation of Priority Message Queue

The implementation is similar to the message queue, achieved by adding the pend-post mechanism on top of the priority queue.

The implementation of the priority message queue in TencentOS-tiny is in tos_priority_message_queue.h and tos_priority_message_queue.c.

typedef struct k_priority_message_queue_st {
    knl_obj_t   knl_obj;

    pend_obj_t  pend_obj;

    void       *prio_q_mgr_array;
    k_prio_q_t  prio_q;
} k_prio_msg_q_t;

Here, pend_obj is used to mount tasks waiting for that priority message queue, while prio_q and prio_q_mgr_array together implement the priority queue.

The APIs for message enqueuing and dequeuing are conceptually identical to those of the message queue, so they will not be explained further.

3.2. Example of Using Priority Message Queue

#define MESSAGE_MAX     10

uint8_t msg_pool[MESSAGE_MAX * sizeof(void *)];

k_prio_msg_q_t prio_msg_q;

void entry_task_receiver(void *arg)
{
    k_err_t err;
    void *msg_received;

    while (K_TRUE) {
        err = tos_prio_msg_q_pend(&prio_msg_q, &msg_received, TOS_TIME_FOREVER);
        if (err == K_ERR_NONE) {
            printf("receiver: msg incoming[%s]\n", (char *)msg_received);
        }
    }
}

void entry_task_sender(void *arg)
{
    char *msg_prio_0 = "msg with priority 0";
    char *msg_prio_1 = "msg with priority 1";
    char *msg_prio_2 = "msg with priority 2";

    printf("sender: post a message with priority 2\n");
    tos_prio_msg_q_post(&prio_msg_q, msg_prio_2, 2);

    printf("sender: post a message with priority 1\n");
    tos_prio_msg_q_post(&prio_msg_q, msg_prio_1, 1);

    printf("sender: post a message with priority 0\n");
    tos_prio_msg_q_post(&prio_msg_q, msg_prio_0, 0);
}

The execution result is as follows:

TencentOS-tiny Port on STM32L431RCT6 By Mculover666
sender: post a message with priority 2
sender: post a message with priority 1
sender: post a message with priority 0
receiver: msg incoming[msg with priority 0]
receiver: msg incoming[msg with priority 1]
receiver: msg incoming[msg with priority 2]

By comparing the results from section 2 and section 3, you will find that despite the same message sending order, the order in which tasks receive messages is completely different due to the use of different message queues.

4. Mail Queue

4.1. Differences

The difference between a message queue and a mail queue lies in the type of each element in the underlying queue, which can be seen by looking at the source code.

A message queue passes messages by address, so when initializing the message queue, each element in the circular queue is a null pointer type:

__API__ k_err_t tos_msg_q_create(k_msg_q_t *msg_q, void *pool, size_t msg_cnt)
{
 // Some source code omitted

 // Key point: each element type size is sizeof(void*)
    err = tos_ring_q_create(&msg_q->ring_q, pool, msg_cnt, sizeof(void *));
    if (err != K_ERR_NONE) {
        return err;
    }

    return K_ERR_NONE;
}

Whereas a mail queue passes values, so when initializing the underlying circular queue, the size of each element is specified by the user:

__API__ k_err_t tos_mail_q_create(k_mail_q_t *mail_q, void *pool, size_t mail_cnt, size_t mail_size)
{
 // Some source code omitted
 
 // Key point: each element size is mail_size, specified by user input
    err = tos_ring_q_create(&mail_q->ring_q, pool, mail_cnt, mail_size);
    if (err != K_ERR_NONE) {
        return err;
    }

    return K_ERR_NONE;
}

4.2. Implementation of Mail Queue

There is nothing complicated about this~ Just a circular queue + pend-post object is sufficient.

The implementation of the mail queue in TencentOS-tiny is in tos_mail_queue.h and tos_mail_queue.c.

typedef struct k_mail_queue_st {
    knl_obj_t   knl_obj;

    pend_obj_t  pend_obj;
    k_ring_q_t  ring_q;
} k_mail_q_t;

Isn’t there much difference~ As for the operation APIs, there is even less difference, so I won’t write them, just skimming through.

4.3. Example of Using Mail Queue

#define MAIL_MAX    10

typedef struct mail_st {
    char   *message;
    int     payload;
} mail_t;

uint8_t mail_pool[MAIL_MAX * sizeof(mail_t)];

k_mail_q_t mail_q;

void entry_task_receiver_higher_prio(void *arg)
{
    k_err_t err;
    mail_t mail;
    size_t mail_size;

    while (K_TRUE) {
        err = tos_mail_q_pend(&mail_q, &mail, &mail_size, TOS_TIME_FOREVER);
        if (err == K_ERR_NONE) {
            TOS_ASSERT(mail_size == sizeof(mail_t));
            printf("higher: msg incoming[%s], payload[%d]\n", mail.message, mail.payload);
        }
    }
}

void entry_task_receiver_lower_prio(void *arg)
{
    k_err_t err;
    mail_t mail;
    size_t mail_size;

    while (K_TRUE) {
        err = tos_mail_q_pend(&mail_q, &mail, &mail_size, TOS_TIME_FOREVER);
        if (err == K_ERR_NONE) {
            TOS_ASSERT(mail_size == sizeof(mail_t));
            printf("lower: msg incoming[%s], payload[%d]\n", mail.message, mail.payload);
        }
    }
}

void entry_task_sender(void *arg)
{
    int i = 1;
    mail_t mail;

    while (K_TRUE) {
        if (i == 2) {
            printf("sender: send a mail to one receiver, and should be the highest priority one\n");
            mail.message = "1st time post";
            mail.payload = 1;
            tos_mail_q_post(&mail_q, &mail, sizeof(mail_t));
        }
        if (i == 3) {
            printf("sender: send a message to all receivers\n");
            mail.message = "2nd time post";
            mail.payload = 2;
            tos_mail_q_post_all(&mail_q, &mail, sizeof(mail_t));
        }
        if (i == 4) {
            printf("sender: send a mail to one receiver, and should be the highest priority one\n");
            mail.message = "3rd time post";
            mail.payload = 3;
            tos_mail_q_post(&mail_q, &mail, sizeof(mail_t));
        }
        if (i == 5) {
            printf("sender: send a message to all receivers\n");
            mail.message = "4th time post";
            mail.payload = 4;
            tos_mail_q_post_all(&mail_q, &mail, sizeof(mail_t));
        }
        tos_task_delay(1000);
        ++i;
    }
}

The execution result is:

TencentOS-tiny Port on STM32L431RCT6 By Mculover666

sender: send a mail to one receiver, and should be the highest priority one
higher: msg incoming[1st time post], payload[1]

sender: send a message to all receivers
higher: msg incoming[2nd time post], payload[2]
lower: msg incoming[2nd time post], payload[2]

sender: send a mail to one receiver, and should be the highest priority one
higher: msg incoming[3rd time post], payload[3]

sender: send a message to all receivers
higher: msg incoming[4th time post], payload[4]
lower: msg incoming[4th time post], payload[4]

This example mainly demonstrates two points: 1. How to use the mailbox queue to directly pass values; 2. The difference between waking a waiting task and waking all waiting tasks.

5. Priority Mail Queue

At this point, there’s really nothing more to say~

The implementation in TencentOS-tiny is in tos_priority_mail_queue.c and tos_priority_mail_queue.h.

You can try to write a demo using the priority mailbox queue based on the previous demos to test whether high-priority mails are received first, and then compare the results with the experiments in section 4.

The further we go into the article, the lazier I get, and skimming is no longer enough; the author needs to go fish~

6. Summary

As usual, let’s summarize the content discussed in this article.

This article mainly discusses some kernel objects used for inter-task communication, primarily four: message queues and priority message queues, mailbox queues, and priority mailbox queues.

Next, here are some important points:

When using certain inter-task communication mechanisms in RTOS, be aware of whether you are passing values or addresses. In TencentOS-tiny, the message queue transmits addresses, while the mailbox queue transmits values.

Message queues and mailbox queues are based on circular queues and follow the FIFO principle; whereas priority message queues and priority mailbox queues are based on priority queues and follow the principle of retrieving elements based on priority.

Finally, let’s answer the question posed in the title: why not use global variables for inter-task communication?

① Both message queues and mailbox queues leverage the characteristic of global variables being freely accessible, so they are defined as global variables.

② Ordinary global variables can be used in some simple inter-task communication scenarios.

③ Compared to ordinary global variables, adding a queuing mechanism can store multiple messages, and incorporating a pend-post mechanism allows for task waiting and waking mechanisms, solving issues of full or empty queues.

———— END ————
Recommended Reading:
Selected Tutorials from the Embedded Column
Summary | STM32, Microcontroller
Summary | RTOS, Operating System
Welcome to follow my public accountReply “Join Group” to join the technical exchange group according to the rules, and reply “1024” to see more content.
Welcome to follow my video account:

Understanding the Differences Between RTOS Inter-Task Communication and Global Variables

Click “Read Original” for more sharing, welcome to share, collect, like, and view.

Leave a Comment

×