Mailboxes

Concepts

The microkernel’s mailbox object type is an implementation of a traditional message queue.

A mailbox allows tasks to exchange messages. A task that sends a message is known as the sending task, while a task that receives the message is known as the receiving task. Messages may not be sent or received by fibers or ISRs, nor may a given message be received by more than one task; point-to-multipoint messaging is not supported.

A mailbox has a queue of messages that have been sent, but not yet received. The messages in the queue are sorted by priority, allowing a higher priority message to be received before a lower priority message that was sent earlier. Messages of equal priority are handled in a first in, first out manner.

Any number of mailboxes can be defined in a microkernel system. Each mailbox needs a name that uniquely identifies it. A mailbox does not limit the number of messages it can queue, nor does it place limits on the size of the message it handles.

The content of a message is stored in an array of bytes, called the message data. The size and format of the message data is application-defined, and can vary from one message to the next. Message data may be stored in a buffer provided by the task that sends or receives the message, or in a memory pool block. The message data portion of a message is optional; a message without any message data is called an empty message.

The life cycle of a message is fairly simple. A message is created when it is given to a mailbox by the sending task. The message is then owned by the mailbox until it is given to a receiving task. The receiving task may retrieve the message data when it receives the message from the mailbox, or it may perform data retrieval during a second, subsequent mailbox operation. Only when data retrieval has been performed is the message deleted by the mailbox.

A message can be exchanged non-anonymously or anonymously between a sending and receiving task. A sending task can specify the name of the task to which the message is being sent, or it can specify that any task may receive the message. Likewise, a receiving task can specify the name of the task from which it wishes to receive a message, or it can specify that it is willing to receive a message from any task. A message is exchanged only when the requirements for both the sending task and receiving task are satisfied; such tasks are said to be compatible.

For example, task A sends a message to task B, but it will be received by task B only if the latter tries to receive a message from task A (or from any task). The exchange will not occur if task B tries to receive a message from task C. The message can never be received by task C, even if it is trying to receive a message from task A (or from any task).

Messages can be exchanged synchronously or asynchronously. In a synchronous exchange, the sending task blocks until the message has been fully processed by the receiving task. In an asynchronous exchange, the sending task does not wait until the message has been received by another task before continuing; this allows the task to do other work (such as gather data that will be used in the next message) before the message is given to a receiving task and fully processed. The technique used for a given message exchange is determined by the sending task.

The synchronous exchange technique provides an inherent form of flow control, preventing a sending task from generating messages faster than they can be consumed by receiving tasks. The asynchronous exchange technique provides an optional form of flow control, which allows a sending task to determine if a previously sent message still exists before sending a subsequent message.

Message Descriptor

A message descriptor is a data structure that specifies where a message’s data is located, and how the message is to be handled by the mailbox. Both the sending task and the receiving task pass a message descriptor to the mailbox when accessing a mailbox. The mailbox uses both message descriptors to perform a message exchange between compatible sending and receiving tasks. The mailbox also updates some fields of the descriptors during the exchange, allowing both tasks to know what occurred.

A message descriptor is a structure of type struct k_msg. The fields listed below are available for application use; all other fields are for kernel use only.

info

A 32-bit value that is exchanged by the message sender and receiver, and whose meaning is defined by the application. This exchange is bi-directional, allowing the sender to pass a value to the receiver during any message exchange, and allowing the receiver to pass a value to the sender during a synchronous message exchange.
size
The message data size, in bytes. Set it to zero when sending an empty message, or when discarding the message data of a received message. The mailbox updates this field with the actual number of data bytes exchanged once the message is received.
tx_data
A pointer to the sending task’s message data buffer. Set it to NULL when sending a memory pool block, or when sending an empty message. (Not used when receiving a message.)
tx_block
The descriptor for the memory pool block containing the sending task’s message data. (Not used when sending a message data buffer, or when sending an empty message. Not used when receiving a message.)
rx_data
A pointer to the receiving task’s message data buffer. Set it to NULL when the message’s data is not wanted, or when it will be retrieved by a subsequent mailbox operation. (Not used when sending a message.)
tx_task
The name of the sending task. Set it to ANYTASK to receive a message sent by any task. The mailbox updates this field with the actual sender’s name once the message is received. (Not used when sending a message.)
rx_task
The name of the receiving task. Set it to ANYTASK to allow any task to receive the message. The mailbox updates this field with the actual receiver’s name once the message is received, but only if the message is sent synchronously. (Not used when receiving a message.)

Sending a Message

A task sends a message by first creating the message data to be sent (if any). The data may be placed in a message buffer – such as an array or structure variable – whose contents are copied to an area supplied by the receiving task during the message exchange. Alternatively, the data may be placed in a block allocated from a memory pool, which gets handed off to the receiving task during the exchange. A message buffer is typically used when the data volume flowing through is small, and the cost of copying the data is less than the cost of allocating and freeing a memory pool block. A memory pool block must be used when a non-empty message is sent asynchronously.

Next, the task creates a message descriptor that characterizes the message to be sent, as described in the previous section.

Finally, the task calls one of the mailbox send APIs to initiate the message exchange. The message is immediately given to a compatible receiving task, if one is currently waiting for a message. Otherwise, the message is added to the mailbox’s queue of messages, according to the priority specified by the sending task. Typically, a sending task sets the message priority to its own task priority level, allowing messages sent by higher priority tasks to take precedence over those sent by lower priority tasks.

For a synchronous send operation, the operation normally completes when a receiving task has both received the message and retrieved the message data. If the message is not received before the waiting period specified by the sending task is reached, the message is removed from the mailbox’s queue and the sending task continues processing. When a send operation completes successfully the sending task can examine the message descriptor to determine which task received the message and how much data was exchanged, as well as the application-defined info value supplied by the receiving task.

Note

A synchronous send operation may block the sending task indefinitely – even when the task specifies a maximum waiting period – since the waiting period only limits how long the mailbox waits before the message is received by another task. Once a message is received there is no limit to the time the receiving task may take to retrieve the message data and unblock the sending task.

For an asynchronous send operation, the operation always completes immediately. This allows the sending task to continue processing regardless of whether the message is immediately given to a receiving task or is queued by the mailbox. The sending task may optionally specify a semaphore that the mailbox gives when the message is deleted by the mailbox (for example, when the message has been received and its data retrieved by a receiving task). The use of a semaphore allows the sending task to easily implement a flow control mechanism that ensures that the mailbox holds no more than an application-specified number of messages from a sending task (or set of sending tasks) at any point in time.

Receiving a Message

A task receives a message by first creating a message descriptor that characterizes the message it wants to receive. It then calls one of the mailbox receive APIs. The mailbox searches its queue of messages and takes the first one it finds that satisfies both the sending and receiving tasks’ message descriptor criteria. If no compatible message exists, the receiving task may choose to wait for one to be sent. If no compatible message appears before the waiting period specified by the receiving task is reached, the receive operation fails and the receiving task continues processing. Once a receive operation completes successfully the receiving task can examine the message descriptor to determine which task sent the message, how much data was exchanged, and the application-defined info value supplied by the sending task.

The receiving task controls both the quantity of data it retrieves from an incoming message and where the data ends up. The task may choose to take all of the data in the message, to take only the initial part of the data, or to take no data at all. Similarly, the task may choose to have the data copied into a buffer area of its choice or to have it placed in a memory pool block. A message buffer is typically used when the volume of data involved is small, and the cost of copying the data is less than the cost of allocating and freeing a memory pool block.

The following sections outline various approaches a receiving task may use when retrieving message data.

Retrieving Data Immediately into a Buffer

The most straightforward way for a task to retrieve message data is to specify a buffer when the message is received. The task indicates both the location of the buffer (which must not be NULL) and its size (which must be greater than zero).

The mailbox copies the message’s data to the buffer as part of the receive operation. If the buffer is not big enough to contain all of the message’s data, any uncopied data is lost. If the message is not big enough to fill all of the buffer with data, the unused portion of the buffer is left unchanged. In all cases the mailbox updates the receiving task’s message descriptor to indicate how many data bytes were copied (if any).

The immediate data retrieval technique is best suited for applications involving small messages where the maximum size of a message is known in advance.

Note

This technique can be used when the message data is actually located in a memory pool block supplied by the sending task. The mailbox copies the data into the buffer specified by the receiving task, then automatically frees the block back to its memory pool. This allows a receiving task to retrieve message data without having to know whether the data was sent using a buffer or a block.

Retrieving Data Subsequently into a Buffer

A receiving task may choose to retrieve no message data at the time the message is received, so that it can retrieve the data into a buffer at a later time. The task does this by specifying a buffer location of NULL and a size indicating the maximum amount of data it is willing to retrieve later (which must be greater than or equal to zero).

The mailbox does not copy any message data as part of the receive operation. However, the mailbox still updates the receiving task’s message descriptor to indicate how many data bytes are available for retrieval.

The receiving task must then respond as follows:

  • If the message descriptor size is zero, then either the received message is an empty message or the receiving task did not want to receive any message data. The receiving task does not need to take any further action since the mailbox has already completed data retrieval and deleted the message.
  • If the message descriptor size is non-zero and the receiving task still wants to retrieve the data, the task must supply a buffer large enough to hold the data. The task first sets the message descriptor’s rx_data field to the address of the buffer, then calls task_mbox_data_get(). This instructs the mailbox to copy the data and delete the message.
  • If the message descriptor size is non-zero and the receiving task does not want to retrieve the data, the task sets the message descriptor’s size field to zero and calls task_mbox_data_get(). This instructs the mailbox to delete the message without copying the data.

The subsequent data retrieval technique is suitable for applications where immediate retrieval of message data is undesirable. For example, it can be used when memory limitations make it impractical for the receiving task to always supply a buffer capable of holding the largest possible incoming message.

Note

This technique can be used when the message data is actually located in a memory pool block supplied by the sending task. The mailbox copies the data into the buffer specified by the receiving task, then automatically frees the block back to its memory pool. This allows a receiving task to retrieve message data without having to know whether the data was sent using a buffer or a block.

Retrieving Data Subsequently into a Block

A receiving task may choose to retrieve message data into a memory pool block, rather than a buffer area of its choice. This is done in much the same way as retrieving data subsequently into a buffer—the receiving task first receives the message without its data, then retrieves the data by calling task_mbox_data_block_get(). The latter call fills in the block descriptor supplied by the receiving task, allowing the task to access the data. This call also causes the mailbox to delete the received message, since data retrieval has been completed. The receiving task is then responsible for freeing the block back to the memory pool when the data is no longer needed.

This technique is best suited for applications where the message data has been sent using a memory pool block, either because a large amount of data is involved or because the message was sent asynchronously.

Note

This technique can be used when the message data is located in a buffer supplied by the sending task. The mailbox automatically allocates a memory pool block and copies the message data into it. However, this is much less efficient than simply retrieving the data into a buffer supplied by the receiving task. In addition, the receiving task must be designed to handle cases where the data retrieval operation fails because the mailbox cannot allocate a suitable block from the memory pool. If such cases are possible, the receiving task can call task_mbox_data_block_get_wait() or task_mbox_data_block_get_wait_timeout() to permit the task to wait until a suitable block can be allocated. Alternatively, the task can use task_mbox_data_get() to inform the mailbox that it no longer wishes to receive the data at all, allowing the mailbox to release the message.

Purpose

Use a mailbox to transfer data items between tasks whenever the capabilities of a FIFO are insufficient.

Usage

Defining a Mailbox

The following parameters must be defined:

name
This specifies a unique name for the mailbox.

Public Mailbox

Define the mailbox in the application’s MDEF using the following syntax:

MAILBOX name

For example, the file projName.mdef defines a mailbox as follows:

% MAILBOX   NAME
% ==========================
  MAILBOX   REQUEST_BOX

A public mailbox can be referenced by name from any source file that includes the file zephyr.h.

Private Mailbox

Define the mailbox in a source file using the following syntax:

DEFINE_MAILBOX(name);

For example, the following code defines a private mailbox named PRIV_MBX.

DEFINE_MAILBOX(PRIV_MBX);

The mailbox PRIV_MBX can be used in the same style as those defined in the MDEF.

To use this mailbox from a different source file use the following syntax:

extern const kmbox_t PRIV_MBX;

Example: Sending a Variable-Sized Mailbox Message

This code uses a mailbox to synchronously pass variable-sized requests from a producing task to any consuming task that wants it. The message “info” field is used to exchange information about the maximum size buffer that each task can handle.

void producer_task(void)
{
    char buffer[100];
    int buffer_bytes_used;

    struct k_msg send_msg;
    k_priority_t send_priority = task_priority_get();

    while (1) {

        /* generate data to send */
        ...
        buffer_bytes_used = ... ;
        memcpy(buffer, source, buffer_bytes_used);

        /* prepare to send message */
        send_msg.info = buffer_bytes_used;
        send_msg.size = buffer_bytes_used;
        send_msg.tx_data = buffer;
        send_msg.rx_task = ANYTASK;

        /* send message and wait until a consumer receives it */
        task_mbox_put(REQUEST_BOX, send_priority,
                      &send_msg,TICKS_UNLIMITED);

        /* info, size, and rx_task fields have been updated */

        /* verify that message data was fully received */
        if (send_msg.size < buffer_bytes_used) {
            printf("some message data dropped during transfer!");
            printf("receiver only had room for %d bytes", send_msg.info);
        }
    }
}

Example: Receiving a Variable-Sized Mailbox Message

This code uses a mailbox to process variable-sized requests from any producing task, using the immediate data retrieval technique. The message “info” field is used to exchange information about the maximum size buffer that each task can handle.

void consumer_task(void)
{
    struct k_msg recv_msg;
    char buffer[100];

    int i;
    int total;

    while (1) {
        /* prepare to receive message */
        recv_msg.info = 100;
        recv_msg.size = 100;
        recv_msg.rx_data = buffer;
        recv_msg.rx_task = ANYTASK;

        /* get a data item, waiting as long as needed */
        task_mbox_get(REQUEST_BOX, &recv_msg, TICKS_UNLIMITED);

        /* info, size, and tx_task fields have been updated */

        /* verify that message data was fully received */
        if (recv_msg.info != recv_msg.size) {
            printf("some message data dropped during transfer!");
            printf("sender tried to send %d bytes", recv_msg.info);
        }

        /* compute sum of all message bytes (from 0 to 100 of them) */
        total = 0;
        for (i = 0; i < recv_msg.size; i++) {
            total += buffer[i];
        }
    }
}

Example: Sending an Empty Mailbox Message

This code uses a mailbox to synchronously pass 4 byte random values to any consuming task that wants one. The message “info” field is large enough to carry the information being exchanged, so the data buffer portion of the message isn’t used.

void producer_task(void)
{
    struct k_msg send_msg;
    k_priority_t send_priority = task_priority_get();

    while (1) {

        /* generate random value to send */
        uint32_t random_value = sys_rand32_get();

        /* prepare to send empty message */
        send_msg.info = random_value;
        send_msg.size = 0;
        send_msg.tx_data = NULL;
        send_msg.rx_task = ANYTASK;

        /* send message and wait until a consumer receives it */
        task_mbox_put(REQUEST_BOX, send_priority,
                      &send_msg, TICKS_UNLIMITED);

        /* no need to examine the receiver's "info" value */
    }
}

Example: Deferring the Retrieval of Message Data

This code uses a mailbox’s subsequent data retrieval mechanism to get message data from a producing task only if the message meets certain criteria, thereby eliminating unneeded data copying. The message “info” field supplied by the sender is used to classify the message.

void consumer_task(void)
{
    struct k_msg recv_msg;
    char buffer[10000];

    while (1) {
        /* prepare to receive message */
        recv_msg.size = 10000;
        recv_msg.rx_data = NULL;
        recv_msg.rx_task = ANYTASK;

        /* get message, but not its data */
        task_mbox_get(REQUEST_BOX, &recv_msg, TICKS_UNLIMITED);

        /* get message data for only certain types of messages */
        if (is_message_type_ok(recv_msg.info)) {
            /* retrieve message data and delete the message */
            recv_msg.rx_data = buffer;
            task_mbox_data_get(&recv_msg);

            /* process data in "buffer" */
            ...
        } else {
            /* ignore message data and delete the message */
            recv_msg.size = 0;
            task_mbox_data_get(&recv_msg);
        }
    }
}

Example: Sending an Asynchronous Mailbox Message

This code uses a mailbox to send asynchronous messages using memory blocks obtained from TXPOOL, thereby eliminating unneeded data copying when exchanging large messages. The optional semaphore capability is used to hold off the sending of a new message until the previous message has been consumed, so that a backlog of messages doesn’t build up when the consuming task is unable to keep up.

void producer_task(void)
{
    struct k_msg send_msg;
    kpriority_t send_priority = task_priority_get();

    volatile char *hw_buffer;

    /* indicate that all previous messages have been processed */
    task_sem_give(MY_SEMA);

    while (1) {
        /* allocate memory block that will hold message data */
        task_mem_pool_alloc(&send_msg.tx_block, TXPOOL,
                            4096, TICKS_UNLIMITED);

        /* keep saving hardware-generated data in the memory block      */
        /* until the previous message has been received by the consumer */
        do {
            memcpy(send_msg.tx_block.pointer_to_data, hw_buffer, 4096);
        } while (task_sem_take(MY_SEMA, TICKS_NONE) != RC_OK);

        /* finish preparing to send message */
        send_msg.size = 4096;
        send_msg.rx_task = ANYTASK;

        /* send message containing most current data and loop around */
        task_mbox_block_put(REQUEST_BOX, send_priority, &send_msg, MY_SEMA);
    }
}

Example: Receiving an Asynchronous Mailbox Message

This code uses a mailbox to receive messages sent asynchronously using a memory block, thereby eliminating unneeded data copying when processing a large message.

void consumer_task(void)
{
    struct k_msg recv_msg;
    struct k_block recv_block;

    int total;
    char *data_ptr;
    int i;

    while (1) {
        /* prepare to receive message */
        recv_msg.size = 10000;
        recv_msg.rx_data = NULL;
        recv_msg.rx_task = ANYTASK;

        /* get message, but not its data */
        task_mbox_get(REQUEST_BOX, &recv_msg, TICKS_UNLIMITED);

        /* get message data as a memory block and discard message */
        task_mbox_data_block_get(&recv_msg, &recv_block, RXPOOL,
                                 TICKS_UNLIMITED);

        /* compute sum of all message bytes in memory block */
        total = 0;
        data_ptr = (char *)(recv_block.pointer_to_data);
        for (i = 0; i < recv_msg.size; i++) {
            total += data_ptr++;
        }

        /* release memory block containing data */
        task_mem_pool_free(&recv_block);
    }
}

Note

An incoming message that was sent synchronously is also processed correctly by this algorithm, since the mailbox automatically creates a memory block containing the message data using RXPOOL. However, the performance benefit of using the asynchronous approach is lost.

APIs

The following APIs for mailbox operations are provided by the kernel:

task_mbox_put()
Send synchronous message to a receiving task, with time limited waiting.
task_mbox_block_put()
Send asynchronous message to a receiving task, or to a mailbox queue.
task_mbox_get()
Get message from a mailbox, with time limited waiting.
task_mbox_data_get()
Retrieve message data into a buffer.
task_mbox_data_block_get()
Retrieve message data into a block, with time limited waiting.