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.

Exercise 2 – Kernel Options

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 teams 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.

Exercise steps

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.

Building the application

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.

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):

This 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 to be higher than that (1024 bytes).

3. Define the FIFO.

Add the following line in main.c:

K_FIFO_DEFINE(my_fifo);

4. Define the data type of the FIFO items.

In the data item, we want to store a string with the following format:

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;
}; 

The 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("Enable 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);
    }

Every 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:

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);
    }

The 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:

It’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.

You 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.

Exploring Kernel Options

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.

You can see where these are located in the below screenshot:

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:

  • The number of priority levels in the application, both preemptible and cooperative.
  • Enable/Disable Earliest-deadline-first scheduling.
  • Enable/Disable METAIRQ Meta-IRQ threads.
  • The stack size of the main thread (CONFIG_MAIN_STACK_SIZE). This is an important parameter, especially if your main thread is going to perform a lot of tasks.
  • The stack size used by interrupt service routines (ISRs).
  • The stack size of the idle thread.

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.

  • The boot banner option (corresponding to 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 ***

  • The boot delay option (corresponding to 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.
  • The the thread monitoring option (corresponding to 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.
  • The thread name option (corresponding to 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.

  • The stack size option (corresponding to 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.
  • The System Work Queue goes over the submitted work individually. The priority option (corresponding to CONFIG_SYSTEM_WORKQUEUE_PRIORITY) can be used to select which priority the System Work Queue runs in.
  • The yielding option (corresponding to 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.

  • The thread time slicing option (corresponding to 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.
  • The Async I/O Framework option (corresponding to CONFIG_POOL) enables the Polling API. The polling API are the 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.

10. Using guiconfig.

There are three interactive Kconfig interfaces (nRF Kconfig GUI , guiconfig, menuconfig) supported by nRF Connect for VS code. All of them allow you to explore related configuration options and know their values. One advantage of guiconfig is that it lists the dependencies and where the symbol is set. A disadvantage shared by guiconfig and menuconfig is that configurations are only set temporarily and will be lost any time you do a clean build. On the other hand, nRF Kconfig GUI is the only graphical interface that can allow you to save the configurations permanently in prj.conf.

In this step, you will learn how to use guiconfig to trace the dependencies of a Kconfig Symbol and figure out where a certain Kconfig is enabled or disabled. You can open guiconfig by pressing on the More actions (...) menu next to the nRF Kconfig GUI as shown below:

See below how guiconfig is used to figure out how the CONFIG_TICKLESS_KERNEL option was enabled.

If you enable the Show all option and click on Jump to… and type TICKLESS_CAPABLE, you can see that it is set by the NRF_RTC_TIMER option.

Guiconfig can also give you the exact location where the Kconfig is located.

With this, you have a good understanding of the available Kernel Options.

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.