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 1

Thread creation and priorities

In this exercise, we will learn how to create and initialize two threads and learn how they can affect one another with their priorities. There are two ways to create a thread in Zephyr, the first one is dynamically (at run-time) through k_thread_create() and the other method, which is more frequently used, is statically (at compile time) by using the K_THREAD_DEFINE()macro. This is the macro for defining and initializing a thread and plugging its data structures into the RTOS kernel, it has the API shown below:

K_THREAD_DEFINE API

Exercise steps

1. In the GitHub repository for this course, open the base code for this exercise, found in l7/l7_e1 of whichever version directory you are using.

Creating and initializing threads

2. Define the stack size and scheduling priority of the two threads that we will use when defining them.

#define STACKSIZE 1024
#define THREAD0_PRIORITY 7
#define THREAD1_PRIORITY 7
C

Even though the threads are simple in this exercise, we are setting a stack size of 1024. Stack sizes should always be a power of two (512, 1024, 2048, etc.).

Note

In actual application development, you should choose the stack sizes more carefully, to avoid unnecessarily using the stack size. We do not need that here for a simple application.

We are giving the two threads the same priority. In this case, the actual number used isn’t important.

3. The thread entry functions for the two threads (thread0 and thread1) are provided for you, but they contain no code. For now, let’s make them do something very simple which is just print a string in a while-loop.

Add the following printk() statement inside the thread0 and thread1 entry function. Make sure to change the name of the second thread to thread1.

printk("Hello, I am thread0\n");
C

Since the threads have no dependency on each other and neither yield nor sleep, they will always be in the “Runnable” state competing for the CPU resource.

4. Now that we have defined the necessary parameters, we can define the two threads, thread0 and thread1, using K_THREAD_DEFINE(). Pass the thread’s name to be created, its stack size, thread entry function, optional arguments to pass to the thread up on starting(up to three), the thread’s priority, optional thread options, and finally, an optional scheduling delay.

For the optional arguments passed to the thread, we will pass NULL.

For the optional thread options, which can allow us to configure, for example, if the thread is to be treated as an essential thread K_ESSENTIAL. It instructs the kernel to treat the termination or aborting of the thread as a fatal system error. By default, the thread is not considered to be an essential thread. We will not set an option in this exercise.

For the optional scheduling delay, we will also set it to zero, meaning the thread will be created and put in the ready state right away.

K_THREAD_DEFINE(thread0_id, STACKSIZE, thread0, NULL, NULL, NULL,
		THREAD0_PRIORITY, 0, 0);
K_THREAD_DEFINE(thread1_id, STACKSIZE, thread1, NULL, NULL, NULL,
		THREAD1_PRIORITY, 0, 0);
C

The name can be anything but is used as the thread ID, so name wisely.

5. Build the application and flash it on your development kit. Using a serial terminal you should see the below output:

*** Booting nRF Connect SDK 2.6.1-3758bcbfa5cd ***
Hello, I am thread0
Hello, I am thread0
Hello, I am thread0
Hello, I am thread0
Hello, I am thread0
Hello, I am thread0
Hello, I am thread0
Hello, I am thread0
Hello, I am thread0
Hello, I am thread0
Hello, I am thread0
Hello, I am thread0
Hello, I am thread0
Hello, I am thread0
Hello, I am thread0
Hello, I am thread0
Terminal

Notice that we only see output from thread0, indicating that thread1 never gets to run even though both threads were created with the same parameters and priority. The second thread is being starved, meaning it’s constantly being blocked since thread0 never yields, or waits (sleeps), or does any other event that triggers a rescheduling point.

Let’s see how we can fix this.

Thread yielding

To avoid starving thread1, we will make thread0 voluntarily yield using k_yield().

k_yield() causes the current thread to give away execution (yield) to another thread of the same or higher priority. The thread will be in the “Runnable” state, just pushed to the end of the list of “Runnable” threads. If there are no other threads in that list of the same or higher priority, the threads runs again immediately.

Note

To give lower priority threads a chance to run, the current thread needs to be put to “Non-runnable”. This can be done using k_sleep(), which we will see further on in this exercise.

A thread normally yields when it either has nothing else to do or wants to give other equal or higher priority threads a chance to run. There is usually some logic behind when a thread wants to call k_yield(). To keep this exercise simple, let us make thread0 yield every time it completes the printk() message.

6. Change the entry function of thread0 so it yields after running printk().

k_yield();
C

7. Build the application and flash it on your development kit. Using a serial terminal you should now see the below output:

*** Booting nRF Connect SDK 2.6.1-3758bcbfa5cd ***
Hello, I am thread0
Hello, I am thread1
Hello, I am thread1
Hello, I am thread1
Hello, I am thread1
Hello, I am thread1
Hello, I am thread1
Hello, I am thread1
Hello, I am thread1
Hello, I am thread1
Hello, I am thread1
Hello, I am thread1
Hello, I am thread1
Hello, I am thread1
Hello, I am thread1
Hello, I am thread1
Terminal

Notice that thread0 is now able to print one message and then yields voluntarily to equal or higher priority threads. Since there is another equal priority thread in the “Runnable” state, this thread is now made active by the scheduler and will get the CPU time. Since thread1 never yields it will run forever once it becomes active, starving thread0.

8. Let’s make thread1 yield as well by adding k_yield() after it prints its message.

k_yield();
C

9. Build the application and flash it on your development kit. Using a serial terminal you should now see the below output:

*** Booting nRF Connect SDK 2.6.1-3758bcbfa5cd ***
Hello, I am thread0
Hello, I am thread1
Hello, I am thread0
Hello, I am thread1
Hello, I am thread0
Hello, I am thread1
Hello, I am thread0
Hello, I am thread1
Hello, I am thread0
Hello, I am thread1
Hello, I am thread0
Hello, I am thread1
Hello, I am thread0
Hello, I am thread1
Terminal

And the events on the CPU look similar to the ones below

Timeline when both threads yield

Since both threads now yield after printing a message, the scheduler comes into play between every thread to evaluate if there are any runnable threads in the queue. Since both threads have equal priority, the scheduler will always choose the other thread as the next running thread, resulting in the two threads alternating each time they’ve run.

The disadvantage of this is that yielding this often and thereby invoking the scheduler also takes up CPU time. The scheduler uses CPU time to do the book-keeping of the kernel resources every time k_yield() is called which in turn costs power. A system with good architecture entails designing your threads so the scheduler uses a minimal amount of CPU time, i.e designing threads that have correct priorities and are reasonably considerate (yielding/sleeping/waiting) to other threads.

Thread sleeping

Since printing is a noncrucial task, it is acceptable for our threads to print less frequently. Therefore, a better option can be for the threads to sleep rather than yield. When sleeping, threads are put in the “Non-runnable” state and do very little processing. This is done by using k_sleep() or some derivative, like k_msleep().

More on this

k_sleep() expect a parameter of type k_timeout_t which can be constructed using macros like K_MSEC(). For simplicity, k_sleep() has simpler-to-use derivatives like k_msleep() and k_usleep() that you can pass the time unit directly. The latter will be used extensively.

10. Let’s try this out by replacing the k_yield() function in both threads with k_msleep(5), i.e a sleep duration of 5 ms. Now the threads should look like below:

void thread0(void)
{
	while (1) {
           printk("Hello, I am thread0\n");
           k_msleep(5);
	}
}

void thread1(void)
{
	while (1) {
           printk("Hello, I am thread1\n");
	         k_msleep(5);
	}
}
C

11. Build the application and flash it on your development kit. Using a serial terminal you should now see the below output:

*** Booting nRF Connect SDK 2.6.1-3758bcbfa5cd ***
Hello, I am thread0
Hello, I am thread1
Hello, I am thread0
Hello, I am thread1
Hello, I am thread0
Hello, I am thread1
Hello, I am thread0
Hello, I am thread1
Hello, I am thread0
Hello, I am thread1
Hello, I am thread0
Hello, I am thread1
Hello, I am thread0
Hello, I am thread1
Terminal

Although the output looks identical to when the threads were using k_yield(), there is a significant difference here. The threads are printing less frequently and thereby calling the scheduler less frequently. Take a look at the time sequence graph below and look at the idle period during which the system can switch to low power states.

Timeline when both threads sleep

As you can see, when both threads sleep for a certain amount of time and there are no other ready threads that can be made active by the scheduler, it makes the idle thread (which is one of the system threads) active.

In conclusion:

  • k_yield() will change the thread state from “Running” to “Runnable”, which means that at the next rescheduling point, the thread that just yielded is still a candidate in the scheduler’s algorithm for making a thread active (“Running”). The overall result is that after the thread yields, there will be at least one item in the runnable thread queue for the scheduler to choose from at the next rescheduling point.
  • k_sleep() will change the thread state from “Running” to “Non-runnable” until the timeout has passed, and then change it to “Runnable”. This means that the thread will not be candidate in the scheduler’s algorithm until the timeout amount of time has passed. Hence thread sleeping is better choice for adding delays and not for yielding.

The solution for this exercise can be found in the GitHub repository, in l7/l7_e1_sol of whichever version directory you are using.

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.