Pipes

Concepts

The microkernel’s pipe object type is an implementation of a traditional anonymous pipe.

A pipe allows a task to send a byte stream to another task. The pipe can be configured with a ring buffer which holds data that has been sent but not yet received; alternatively, the pipe may have no ring buffer. Pipes can be used to transfer chunks of data in whole or in part, and either synchronously or asynchronously.

Any number of pipes can be defined in a microkernel system. Each pipe needs

  • A name to uniquely identify it.
  • A size, in bytes, of the ring buffer. Note that a size of zero defines a pipe with no ring buffer.

Sending Data

A task sends data to a pipe by specifying a pointer to the data bytes to be sent. It also specifies both the number of data bytes available and a pipe option that indicates the minimum number of data bytes the pipe must accept. The following pipe option values are supported:

_ALL_N

Specifies that all available data bytes must be accepted by the pipe. When this requirement is not fulfilled, the send request either fails or performs a partial send.

_1_TO_N

Specifies that at least one data byte must be accepted by the pipe. When this requirement is not fulfilled, the send request fails.

_0_TO_N

Specifies that any number of data bytes, including zero, may be accepted by the pipe; the send request never fails.

The pipe accepts data bytes from the sending task if they can be delivered by copying them directly to the receiving task. If the sending task is unable to wait, or has waited as long as it can, the pipe can also accept data bytes by copying them to its ring buffer for later delivery. The ring buffer is used only when necessary to minimize copying of data bytes.

Upon the completion of a send operation, a return code is provided to indicate whether the send request was satisfied. The sending task can also read the bytes written argument attribute to determine how many data bytes were accepted by the pipe, and subsequently allowing it to deal with any unsent data bytes.

Data sent to a pipe that does not have a ring buffer is sent synchronously; that is, when the send operation is complete, the sending task knows that the receiving task has received the data that was sent. Data sent to a pipe that has a ring buffer is sent asynchronously; that is, when the send operation is complete, some or all of the data that was sent may still be in the pipe waiting for the receiving task to receive it.

Incomplete Send Requests

Although a pipe endeavors to accept all available data bytes when the _ALL_N pipe option is specified, it does not guarantee that the data bytes will be accepted in an “all or nothing” manner. When the pipe is able to accept at least one data byte, it returns RC_INCOMPLETE to notify the sending task that its request was not fully satisfied. When the pipe is unable to accept any data bytes, it returns RC_FAIL.

One example of a situation that can result in an incomplete send is a time-limited send request through an unbuffered pipe. If the receiving task chooses to receive only a subset of the sender’s data bytes, and the send operation times out before the receiving task attempts to receive the remainder, an incomplete send occurs.

Sending using a Memory Pool Block

A task that sends large chunks of data through a pipe may be able to improve its efficiency by placing the data into a memory pool block and sending the block. The pipe treats the memory block as a temporary addition to its ring buffer, allowing it to immediately accept the data bytes without copying them. Once all of the data bytes in the block have been delivered to the receiving task, the pipe automatically frees the block back to the memory pool.

Data sent using a memory pool block is always sent asynchronously, even for a pipe with no ring buffer of its own. Likewise, the pipe always accepts all of the available data in the block – a partial transfer never occurs.

Receiving Data

A task receives from a pipe by specifying a pointer to an area to receive the data bytes that were sent. It also specifies both the desired number of data bytes and a pipe option that indicates the minimum number of data bytes the pipe must deliver. The following pipe option values are supported:

_ALL_N

Specifies that all desired number of data bytes must be received. When this requirement is not fulfilled, the receive request either fails or performs a partial receive.

_1_TO_N

Specifies that at least one data byte must be received. When this requirement is not fulfilled, the receive request fails.

_0_TO_N

Specifies that any number of data bytes (including zero) may be received; the receive request never fails.

The pipe delivers data bytes by copying them directly from the sending task or from the pipe’s ring buffer. Data bytes taken from the ring buffer are delivered in a first in, first out manner.

When a pipe is unable to deliver the specified minimum number of data bytes, the receiving task may choose to wait until they can be delivered.

Upon completion of a receive operation, a return code is provided to indicate whether the receive request was satisfied. The receiving task also can read the bytes read argument attribute to determine how many data bytes were delivered by the pipe.

Incomplete Receive Requests

Although a pipe endeavors to deliver all desired data bytes when the _ALL_N pipe option is specified, it does not guarantee that the data bytes will be delivered in an “all or nothing” manner. When the pipe is able to deliver at least one data byte, it returns RC_INCOMPLETE to notify the receiving task that its request was not fully satisfied. When the pipe is unable to deliver any data bytes, it returns RC_FAIL.

An example of a situation that can result in an incomplete receive is a time-limited receive request through an unbuffered pipe. If the sending task sends fewer than the desired number of data bytes, and the receive operation times out before the sending task attempts to send the remainder, an incomplete receive occurs.

Receiving using a Memory Pool Block

A task can achieve the effect of receiving data from a pipe into a memory pool block by pre-allocating a block and then receiving the data into it.

Sharing a Pipe

A pipe is typically used by a single sending task and a single receiving task; however, it is possible for a pipe to be shared by multiple sending tasks or multiple receiving tasks.

Care must be taken when a pipe is shared by multiple sending tasks to ensure the data bytes they send do not become interleaved unexpectedly; using the _ALL_N pipe option helps to ensure that each data chunk is transferred in a single operation. The same is true when multiple receiving tasks are reading from the same pipe.

Purpose

Use a pipe to transfer data when the receiving task needs the ability to split or merge the data items generated by the sending task.

Usage

Defining a Pipe

The following parameters must be defined:

name
This specifies a unique name for the pipe.
buffer_size
This specifies the size in bytes of the pipe’s ring buffer. If no ring buffer is to be used specify zero.

Public Pipe

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

PIPE name buffer_size

For example, the file projName.mdef defines a pipe with a 1 KB ring buffer as follows:

% PIPE   NAME          BUFFERSIZE
% ===============================
  PIPE   DATA_PIPE        1024

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

Private Pipe

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

DEFINE_PIPE(name, size);

For example, the following code defines a private pipe named PRIV_PIPE.

DEFINE_PIPE(PRIV_PIPE, 1024);

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

extern const kpipe_t PRIV_PIPE;

Example: Writing Fixed-Size Data Items to a Pipe

This code uses a pipe to send a series of fixed-size data items to a consuming task.

void producer_task(void)
{
    struct item_type data_item;
    int amount_written;

    while (1) {
        /* generate a data item to send */
        data_item = ... ;

        /* write the entire data item to the pipe */
        task_pipe_put(DATA_PIPE, &data_item, sizeof(data_item),
                           &amount_written, _ALL_N, TICKS_UNLIMITED);

    }
}

Example: Reading Fixed-Size Data Items from a Pipe

This code uses a pipe to receive a series of fixed-size data items from a producing task. To improve performance, the consuming task waits until 20 data items are available then reads them as a group, rather than reading them individually.

void consumer_task(void)
{
    struct item_type data_items[20];
    int amount_read;
    int i;

    while (1) {
        /* read 20 complete data items at once */
        task_pipe_get(DATA_PIPE, &data_items, sizeof(data_items),
                      &amount_read, _ALL_N, TICKS_UNLIMITED);

        /* process the data items one at a time */
        for (i = 0; i < 20; i++) {
            ... = data_items[i];
            ...
        }
    }
}

Example: Reading a Stream of Data Bytes from a Pipe

This code uses a pipe to process a stream of data bytes from a producing task. The pipe is read in a non-blocking manner to allow the consuming task to perform other work when there are no unprocessed data bytes in the pipe.

void consumer_task(void)
{
    char data_area[20];
    int amount_read;
    int i;

    while (1) {
        /* consume any data bytes currently in the pipe */
        while (task_pipe_get(DATA_PIPE, &data_area, sizeof(data_area),
                             &amount_read, _1_TO_N, TICKS_NONE) == RC_OK) {
            /* now have from 1 to 20 data bytes */
            for (i = 0; i < amount_read; i++) {
                ... = data_area[i];
                ...
            }
        }

        /* do other processing */
        ...
    }
}

APIs

Pipe APIs provided by microkernel.h

task_pipe_put()
Write data to a pipe, with time limited waiting.
task_pipe_block_put()
Write data to a pipe from a memory pool block.
task_pipe_get()
Read data from a pipe, or fails and continues if data isn’t there.