You now have a good grasp of interrupt and thread contexts, as well as how to pass data between threads safely using a message queue. This time, let’s practice passing data between threads using FIFO and also explore the kernel options available in nRF Connect SDK and Zephyr.
A FIFO is used to pass data items of variable size or a variable number between threads, or from an ISR to a thread. For example, if you receive data from a peripheral (UART) and don’t know the data size, you can use the FIFO with the heap to allocate memory to the received data and pass it to other software components in the system.
In this exercise, you will develop an application with the same software components as the previous exercise: five threads (producer, consumer, main, logging, idle), and a function that runs periodically every 500 milliseconds (timer) in an interrupt context. The difference is that the producer thread this time will generate a random number of data items. This emulates a situation where data is run-time dependent and the number or size of data items can’t be predicted at compile time.
Same as in the previous exercise, the main thread will setup the GPIO pins for LED0 and LED1, start a timer and then terminate normally by returning 0
.
When the main thread terminates, the producer thread is the highest priority thread in the ready queue, so it will execute. In the producer thread, you will generate a random number of data items (between 4 and 14), and put them in the FIFO and the thread will go to sleep for PRODUCER_SLEEP_TIME_MS
, which is 2200 ms.
The consumer thread will execute after the producer thread. It gets all data items submitted by the producer thread, and then becomes Unready when all data items in the FIFO are consumed.
The logger module will execute next and send all logs to the logging backend (UART0), and finally the idle thread is executed to put the system in a power-saving mode. Once the time defined in PRODUCER_SLEEP_TIME_MS
has elapsed, the cycle repeats itself.
As covered in the previous exercise, you need to open the code base of the exercise. Open the nRF Connect For VS Code extension, navigate to Create a new application, select Copy a sample, and search for Lesson 1 – Exercise 2. Make sure to select the right version directory when copying as explained in the previous exercise.
Alternatively, in the GitHub repository for this course, go to the base code for this exercise, found in l1/l1_e2
of whichever version directory you are using.
1. Enable random number generation.
This is needed for emulation purposes only, to generate a random number of data items in the producer thread. Add the following Kconfig Symbol in the application configuration file (prj.conf
):
CONFIG_ENTROPY_GENERATOR=y
KconfigThis Kconfig enables the entropy driver to generate a random number based on the cryptographic hardware available in the chip. The driver is selected automatically by the build system based on the default value available in your board Kconfig files (for example, with the nRF52840 DK, the CryptoCell 310
driver will be enabled). Alternatively, you can use the Kconfig option CONFIG_TEST_RANDOM_GENERATOR
, which is intended for testing purposes only and never for production code.
In the producer thread, you will use the random number generator function sys_rand32_get() to get a random value between 4 and 14 to emulate a random number of data items at run-time. You will also use it to generate a Return, a 32-bit value to be passed as part of the data item between the producer thread and the consumer thread.
2. Allocate proper heap size for FIFO usage.
You need to predict the maximum number of data items to calculate the heap size allocated for FIFO usage. In this sample, the expected maximum number no more than 14 data items, each of which is 40 bytes. Since 14×40 = 560, the heap size needs to be higher than that (1024 bytes).
CONFIG_HEAP_MEM_POOL_SIZE=1024
Kconfig3. Define the FIFO.
Add the following line in main.c
:
K_FIFO_DEFINE(my_fifo);
C4. Define the data type of the FIFO items.
In the data item, we want to store a string with the following format:
Data Seq. <Unsigned Integer> <Random Unsigned Integer>
Where Unsigned Integer
is incremented by 1 every time the producer thread pushes a data item into the FIFO, and Random Unsigned Integer
is a value returned by calling sys_rand32_get(). Therefore we will define the data item as:
struct data_item_t {
void *fifo_reserved;
uint8_t data[MAX_DATA_SIZE];
uint16_t len;
};
CThe first member of the struct, fifo_reserved
, is mandatory for all FIFOs and is used internally by the kernel. The second member, data
, is an array of uint8_t
of size MAX_DATA_SIZE
, and the last member, len
, will hold the actual data written into the array.
5. Add data items into the FIFO.
Add the following code inside the producer_func()
function:
while (1) {
int bytes_written;
/* Generate a random number between MIN_DATA_ITEMS & MAX_DATA_ITEMS to represent the number
of data items to send every time the producer thread is scheduled */
uint32_t data_number =
MIN_DATA_ITEMS + sys_rand32_get() % (MAX_DATA_ITEMS - MIN_DATA_ITEMS + 1);
for (int i = 0; i < data_number; i++) {
/* Create a 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 */
LOG_ERR("Unable to allocate memory");
return;
}
bytes_written = snprintf(buf->data, MAX_DATA_SIZE, "Data Seq. %u:\t%u",
dataitem_count, sys_rand32_get());
buf->len = bytes_written;
dataitem_count++;
k_fifo_put(&my_fifo, buf);
}
LOG_INF("Producer: Data Items Generated: %u", data_number);
k_msleep(PRODUCER_SLEEP_TIME_MS);
}
CEvery time the producer thread is scheduled for execution, it will add data_number
of data items in the FIFO where data_number
is a random value between 4 and 14.
Notice that, to add a data item in a FIFO, you need to allocate memory space for the item (k_malloc()
). It is essential to make sure that the memory allocation is successful by checking the value returned by k_malloc()
.
In the data item, you will write a string with the sequence number of the data item, and a random 32-bit unsigned integer inside buf->data
. An example of a string generated inside the producer thread is shown below:
Data Seq. 1: 1733782513
The length of the string will be variable since it dependents on the random value generated and the sequence number. You will store the size of the string in buf->len
. The data item is added to the FIFO using the function k_fifo_put(&my_fifo,buf);
6. Read data items from the FIFO.
Add the following code inside the consumer_func()
function:
while (1) {
struct data_item_t *rec_item;
rec_item = k_fifo_get(&my_fifo, K_FOREVER);
LOG_INF("Consumer: %s\tSize: %u",rec_item->data,rec_item->len);
k_free(rec_item);
}
CThe consumer thread will get all the data items submitted by the producer thread by calling the function k_fifo_get(&my_fifo, K_FOREVER);
inside an infinite loop. It will be put to sleep once all data items are consumed from the FIFO because the parameter K_FOREVER
is used as a timeout option.
The consumer thread will submit the received data to the logger module in the following format:
[00:13:22.112,609] <inf> Less1_Exer2: Consumer: Data Seq. 1: 1733782513 Size: 23
TerminalIt’s critical that the application code frees the memory allocated to the data item after it has been consumed. This is done by calling k_free()
. Failing to free the memory allocated to consumed data items will result in a run-time error (heap overflow).
7. Build and flash your application to the board.
8. Connect to the serial terminal and examine the output.
You should see a random number of data items passed between the producer and consumer thread. Below is a sample output.
[00:02:41.204,315] <inf> Less1_Exer2: Producer: Data Items Generated: 4
[00:02:41.204,376] <inf> Less1_Exer2: Consumer: Data Seq. 742: 1266499320 Size: 25
[00:02:41.204,406] <inf> Less1_Exer2: Consumer: Data Seq. 743: 4061392639 Size: 25
[00:02:41.204,467] <inf> Less1_Exer2: Consumer: Data Seq. 744: 1452774199 Size: 25
[00:02:41.204,498] <inf> Less1_Exer2: Consumer: Data Seq. 745: 721881686 Size: 24
[00:02:41.204,559] <inf> Less1_Exer2: Consumer: Data Seq. 746: 1090030288 Size: 25
[00:02:43.407,135] <inf> Less1_Exer2: Producer: Data Items Generated: 11
[00:02:43.407,196] <inf> Less1_Exer2: Consumer: Data Seq. 747: 2517510613 Size: 25
[00:02:43.407,257] <inf> Less1_Exer2: Consumer: Data Seq. 748: 3661502652 Size: 25
[00:02:43.407,287] <inf> Less1_Exer2: Consumer: Data Seq. 749: 2479144377 Size: 25
[00:02:43.407,348] <inf> Less1_Exer2: Consumer: Data Seq. 750: 2723040850 Size: 25
[00:02:43.407,379] <inf> Less1_Exer2: Consumer: Data Seq. 751: 3377936138 Size: 25
[00:02:43.407,440] <inf> Less1_Exer2: Consumer: Data Seq. 752: 1772986057 Size: 25
[00:02:43.407,470] <inf> Less1_Exer2: Consumer: Data Seq. 753: 3285953890 Size: 25
[00:02:43.407,531] <inf> Less1_Exer2: Consumer: Data Seq. 754: 331749191 Size: 24
[00:02:43.407,562] <inf> Less1_Exer2: Consumer: Data Seq. 755: 3144611545 Size: 25
[00:02:43.407,623] <inf> Less1_Exer2: Consumer: Data Seq. 756: 1973893503 Size: 25
[00:02:43.407,653] <inf> Less1_Exer2: Consumer: Data Seq. 757: 91793513 Size: 23
[00:02:43.407,714] <inf> Less1_Exer2: Consumer: Data Seq. 758: 3285217816 Size: 25
TerminalYou can also do a debugging session like in the previous exercise to examine the content of the local variables and FIFO in run-time.
Now that you have practiced using FIFO using, the next step is exploring the Kernel options for an nRF Connect SDK application.
In this part, you will dive into the Kernel options discussed in the Scheduler in-depth topic.
9. Open the nRF Kconfig GUI and General Kernel Options.
Starting from nRF Connect SDK v2.8.0, Sysbuild is the default build system. nRF Kconfig GUI allows you to display the Kconfig configurations for Sysbuild itself and any of its images in the project. This is done by first selecting the image of interest from the APPLICATIONS view and then clicking on the nRF Kconfig GUI in the ACTIONS view, as shown below where the application image is selected.
You can see where these are located in the screenshot below:
The nRF Kconfig GUI is a graphical representation of the complete application Kconfig options. This includes the prj.conf
file, board default Kconfigs, SDK Kconfigs, and fragments. The GUI lists these options in groups, and lets you control them directly from VS Code. It parses the generated .config
file and presents it into groups (menus). Since they are created from the generated file, it is only possible to use the GUI once an application is built.
The kernel options are sourced when Zephyr RTOS is built, which is triggered as one of the early stages of building an nRF Connect SDK application. The default out-of-the-box kernel options are located in <nRF Connect SDK Installation Path>\zephyr\kernel\Kconfig
.
9.1. Examine the options at the top of the menu.
Multithreading (CONFIG_MULTITHREADING
) is enabled by default in all nRF Connect SDK applications. This is because all the libraries and modules in the nRF Connect SDK rely on the multithreading features offered by the Zephyr RTOS. You will not be able to use any of the connectivity options (such as Bluetooth Low Energy, Wi-Fi, or cellular) if you disable this option. This option is enabled by default in <nRF Connect SDK Installation Path>\zephyr\kernel\Kconfig
.
In the nRF Kconfig GUI, you can always click on the information symbol next to each entry to learn more details about that Kconfig configuration. Other options that exist on the top of the General Kernel Options:
CONFIG_MAIN_STACK_SIZE
). This is an important parameter, especially if your main thread is going to perform a lot of tasks.9.2. Scroll down for more options.
Find the Scheduler priority queue algorithm and Wait queue priority algorithm menus.
In the Scheduler priority queue algorithm menu, you can control the internal implementation of the ready queue. There are three options that affect factors such as code size, constant factor runtime overhead and performance scaling when many threads are added. The default option selected (Simple linked-list ready queue) is ideal for systems with few runnable threads at any given time. It has fast, constant time performance and very low code size. You can find more details on the other options in the scheduling documentation for Zephyr. It’s recommended you don’t change this option.
In the wait queue priority algorithm menu, you can control the internal implementation of wait_q. It is a core internal data structure used by the kernel intensively to allow IPC primitives (such as the message queue and FIFOs) to pend threads for later wakeup based on data availability. Two options are available. The default one (Simple linked-list wait_q
) is ideal for when you only have a few threads blocked on any single IPC primitive. You can find more details on the other options in the scheduling documentation for Zephyr. It’s recommended you don’t change this option.
9.3. Scroll down for more options.
Find the Kernel Debugging and Metrics and the Work Queue Options menus.
The first Kconfig option in the Kernel Debugging and Metrics menu is the Initialize stack areas (CONFIG_INIT_STACKS
) option. This option instructs the kernel to initialize stack areas with a known value (0xaa
) before they are first used, so that the high water mark can be easily determined using the Memory Viewer during a debugging session. This is similar to the functionality available in the nRF Debug, except here it’s done manually. This means that the developer needs to inspect the memory locations manually. This applies to the stack areas of both threads, as well as to the interrupt stack.
CONFIG_BOOT_BANNER
), which is enabled by default, prints the boot banner in the terminal on boot-up:*** Booting nRF Connect SDK v2.x.x ***
CONFIG_BOOT_DELAY
) delays boot-up for the specified amount of milliseconds. This delay is introduced between the POST_KERNEL
and APPLICATION
levels discussed in the Boot-up Sequence & Execution context topic.CONFIG_THREAD_MONITOR
) instructs the kernel to maintain a list of all threads (excluding those that have not yet started or have already terminated). This is needed for nRF Debug to visualize the threads in the Ready, Unready, and Running states.CONFIG_THREAD_NAME
) allows you to set a name for a thread.Note that both the thread monitoring and thread name options
are selected in the sample application, in the prj.conf
file. This is done with the CONFIG_DEBUG_THREAD_INFO
option, which selects both when it is enabled.
The Thread runtime statistic option (corresponding to CONFIG_THREAD_RUNTIME_STATS
) is used to gather thread runtime statistics. This can be used in situations where you want your firmware to be able to send this information to, for example, a shell, a Bluetooth LE connection, a wifi connection or cellular. This option is not used in the exercise. Instead, you will use nRF Debug.
In the Work Queue Options menu, you can control the behavior of the System Work Queue. The System Work Queue is a cooperative thread that goes over the submitted work items one by one.
SYSTEM_WORKQUEUE_STACK_SIZE
) controls the stack size of the System Work Queue. Setting this value based on your system work queue usage is very important.CONFIG_SYSTEM_WORKQUEUE_PRIORITY
) can be used to select which priority the System Work Queue runs in.CONFIG_SYSTEM_WORKQUEUE_NO_YIELD
) allows you to disable yielding. By default, the System Work Queue yields between each work item to prevent other threads from being starved. Selecting this option removes this yield. This means that you should only enable this option if there is a strong need for a sequence of work items to complete without yielding.The Atomic operations is managed by the architecture port.
9.3. Scroll down for more options.
Find the Timer API Options and Other Kernel Object Options menus.
The Timer API Options menu controls timer settings.
CONFIG_TIMESLICING
), and the options under it are related to time slicing, which is covered in depth in Lesson 7 – Exercise 2 of the nRF Connect SDK Fundamentals Course.k_poll()
and k_poll_signal_raise()
APIs. The former can wait on multiple events concurrently, which can be either directly triggered or triggered by the availability of some kernel objects (semaphores and FIFOs). An example that uses the Polling API is the Bluetooth LE Bluetooth NFC pairing sample.In the Other Kernel Object Options menu, you can configure and enable different kernel objects, such as the Events objects (CONFIG_EVENTS
), which is used to pass small amounts of data to multiple threads at once and also to indicate that a set of conditions have occurred. An example that uses the Events objects is the nRF Cloud multi-service sample.
The heap allocation configuration option (CONFIG_HEAP_MEM_POOL_SIZE
) is also grouped under the Other Kernel Object Options.
9.4. Scroll down for more options.
At the end of the General Kernel Options, you can find the menus related to key Kconfig options to the kernel.
The System tick frequency (in ticks/second) (CONFIG_SYS_CLOCK_TICKS_PER_SEC
) and the System clock’s h/w timer frequency (CONFIG_SYS_CLOCK_HW_CYCLES_PER_SEC
) are obtained from the default value for the architecture port (SoC level) as shown in the screenshot below.
The Security menu has the Compiler stack canaries (CONFIG_STACK_CANARIES
). This option enables compiler stack canaries, if it is supported by the compiler used. When enabled, it will emit extra code that inserts a canary value into the stack frame when a function is entered and validates this value upon exit. Stack corruption (such as that caused by buffer overflow) results in a fatal error condition for the running entity (for example, a thread will be terminated). Enabling this option can result in a significant increase in footprint and an associated decrease in performance. Therefore, it’s only recommended during the development and debugging phase.
At the bottom is the tickless kernel option (CONFIG_TICKLESS_KERNEL)
, which is enabled by default in all nRF Connect SDK applications as we discussed previously.
With this, you have a good understanding of the available Kernel Options.