Feedback
Feedback

If you are having issues with the exercises, please create a ticket on DevZone: devzone.nordicsemi.com
Click or drag files to this area to upload. You can upload up to 2 files.

Data passing

Now that you understand how ISRs and the different types of threads act as the logical building blocks for an nRF Connect SDK application, it’s time to learn how you can safely exchange data between these blocks. There are several data passing mechanics, each with their own use cases. In this topic, we will cover message queues and FIFOs, which are commonly used and cover a variety of use cases.

Message queue

A message queue is a thread-safe data container that holds a fixed amount of data (messages), which can be accessed safely by multiple threads at the same time. The queue can store different types of data, such as variables, structs, pointers, or any other type of your preference. The maximum number of objects the queue can hold is limited only by the amount of available RAM in your system.

The kernel takes care of all the necessary operations and safeguards for adding new data to the queue and removing data from it. In other words, the message queue is a kernel object. This means you don’t have to worry about manually managing the queue’s internal mechanisms; the kernel manages it.

In addition, the queue also supports timeouts. You can use a timeout to:

  • Make a thread that puts data to the queue go to sleep if the queue is full.
  • Make a thread that gets data from the queue go to sleep if the queue is empty.

If the message queue is empty, any number of receiving threads may wait simultaneously, and when a data item becomes available, it is given to the highest-priority receiving thread. The same concept applies to sending threads when the message queue is full.

The message queue size (N in the above figure) is implemented internally by the kernel as a ring buffer and is statically defined. The data size (message size) must be a multiple of the data alignment. If you have an odd data size, you can either pad the data or use a compiler attribute (such as __aligned(4)) to specify minimum alignment.

While you can use a message queue with interrupts, be careful not to trigger a long or blocking operation, and never use the timeout options (for example, K_NO_WAIT) with interrupts.

How to use:

1. Decide on what data you want to put in a message.

This depends on the use-case for the application. Be mindful that the data type of a message is set statically, so you can’t change it dynamically later. You can define the message as a simple integer, a string, a stuct, or a union within a struct. By using a union within a struct, you can save memory compared to allocating separate memory space for each data type, especially when those data types are not required simultaneously. Here’s an example to illustrate the usage of a union nested within a struct:

struct MyStruct {
  int dataType; // Indicates the active data type
  union {
    int intValue;
    float floatValue;
    char stringValue[24];
  } data;
};

In the above example, the struct MyStruct contains a member dataType to indicate the active data type within the union data. Depending on the value of dataType, you can access the appropriate data member (intValue, floatValue, or stringValue) to retrieve or modify the corresponding value.

Another example that uses a message queue is the Bluetooth: Central and Peripheral HRS sample, where you can see that the messages are defined as struct bt_hrs_client_measurement structs.

2. Define the message queue and initialize it.

You can do both simultaneously with K_MSGQ_DEFINE(), which takes four parameters as shown below:

The below simple snippet shows how to define and initialize a message queue (device_message_queue) with 16 messages. Each element is 4 bytes in size (uint32_t). For the alignment, the snippet specifies sizeof(uint32_t), but you could simply pass 4.

K_MSGQ_DEFINE(device_message_queue, sizeof(uint32_t), 16, 4);

3. Write a message to the message queue.

Write the message using the k_msgq_put() function, which expects three parameters: the message queue defined in step 2, a pointer to a message of the type defined in step 1, and a timeout option. With the timeout option, you can decide what happens if the message queue is full:

  • K_FOREVER – The thread putting the data waits indefinitely until there is space in the message queue, meaning a receiving thread has consumed a message.
  • K_MSEC() – The thread putting the data waits for the specified time when using this option.
  • K_NO_WAIT – The thread putting the data does nothing. This means that the new data will not be added, and is lost. Alternatively, you can use this option to discard all messages in the queue and insert the new data. This is done by also checking the return value of the function, and manually calling k_msgq_purge().

4. Read a message from the message queue.

To read a message, use the k_msgq_get() function. Note that calling this function will remove the message from the message queue (known as popping the message). This is the recommended way as you don’t want your message queue to fill up. The alternative is use the k_msgq_peek() function, which reads a message without removing it from the message queue.

In both cases, messages are read in a First in, First out fashion. In other words, the first message pushed by a sender thread will be read first. The k_msgq_get() function takes three parameters: the message queue defined in step 2, a pointer to a local variable to hold incoming messages (of the type defined in step 2), and a timeout option. The timeout option allows you to decide what happens if the message queue is empty.

Suggested use: Use a message queue to transfer data items of known size and number between threads in an asynchronous manner. You can also use message queues with interrupts, but do so with care as discussed before.

FIFO

A FIFO is a kernel object that facilitates a traditional first-in, first-out (FIFO) queue structure. It enables threads and Interrupts Service Routines (ISRs) to add or remove any number of data items of varying sizes from the queue structure.

Unlike a message queue, you don’t need to specify the number of items in a FIFO or their size statically. Instead, you typically need to use the heap memory (k_malloc() and k_free()) to allocate space for the items on the fly. The FIFO will only hold the addresses of the data items, so the number of items in a FIFO can dynamically change and is only limited by memory, more precisely, the area allocated to the heap.

How to use

1. Specify the size of the heap memory pool to store the items.

The value should be set based on your application requirements. By default, CONFIG_HEAP_MEM_POOL_SIZE is set to zero. Therefore you need to set it to a value that represents the maximum number of elements you can have in the FIFO at a given time.

Note that using heap and dynamic memory allocation in embedded systems firmware must be handled with extra care. It’s the responsibility of the firmware developer to make sure to free the popped items from the heap memory safely.

2. Define the FIFO.

You can use the K_FIFO_DEFINE() macro to statically define a FIFO.

K_FIFO_DEFINE(my_fifo);

Note that, unlike the message queue, you don’t need to specify the data type and size of the FIFO in the definition.

3. Specify the data type of the items.

The important point here is to define the data item as a struct where the first member is always a reserved void pointer. This is needed because the FIFO is implemented internally by the kernel as a simple linked list and the first word of an item should be reserved for use as a pointer to the next data item in the FIFO. The first example below shows a data item struct where the data has a defined size.

struct data_item_t {
	void *fifo_reserved;
	uint8_t  data[256];
	uint16_t len;
};

This second example shows a data item struct that contains a pointer to a variable-size memory allocated on the heap.

struct data_item_var_t {
	void *fifo_reserved;
	void *data;
	uint16_t len;
};

4. Add a data item to the FIFO.

Add the item with the k_fifo_put() function. The API is shown below:

/* create data item to send */
struct data_item_t *buf = k_malloc(sizeof(struct data_item_t));
if (buf == NULL){
/* Unable to locate memory from the heap */
  return ;
}
/* Populate the data item. This is usually done using memcpy() */

/* send data to consumers */
k_fifo_put(&my_fifo,buf);

A data item may be added to a FIFO by a thread or an ISR. The item is given directly to a waiting thread if one exists, or added to the FIFO’s queue if one does not exist. The only limit to the number of items that may be queued is the heap size memory allocated. This also means there is no timeout option available for the k_fifo_put() call, and the size of the heap must be set carefully to accommodate the use case.

5. Read a data item from a FIFO.

Read the item with the k_fifo_get() function. The API is shown below:

Calling k_fifo_get() removes the data from the FIFO. However, the developer must handle removing allocated memory from the heap memory by calling k_free() afterwards. Failing to properly free the memory allocated to consumed data items will result in a run-time error (heap overflow).

Using a timeout parameter allows a thread to wait for a data item to be given in case the FIFO’s queue is empty. Any number of threads may wait on an empty FIFO simultaneously. When a data item is added, it is given to the highest priority thread that has waited the longest.

The complete list of APIs for FIFO is available here.

struct data_item_t *rec_item = k_fifo_get(&my_fifo, K_FOREVER);
/* process FIFO data item */
/*Free */
k_free(rec_item);

Suggested Use: Use a FIFO to asynchronously transfer data items of arbitrary number and size between threads in a First in, First out manner. You can also use FIFO with interrupts, but do so with care as discussed before.

Important

You don’t have to use the heap with FIFO if you have reservations about using dynamic memory allocation in your code. You can choose to allocate the memory needed for the items statically.
Also, it’s important to remember that you can NOT add the same data item twice in a FIFO. This is likely to break the linked list used by the FIFO internally, and results in undefined behavior.

Register an account
Already have an account? Log in
(All fields are required unless specified optional)

  • 8 or more characters
  • Upper and lower case letters
  • At least one number or special character

Forgot your password?
Enter the email associated with your account, and we will send you a link to reset your password.