Multi-tasking under Zephyr OS
Introduction
Multi-tasking Programs
In this codelab, we will learn basic principle for creating multi-tasking application using Zephyr RTOS, namely:
- How to define threads for specific tasks of a program.
- How to synchronize multiple threads and control access to shared resources.
- Study the influence of shared resources on task scheduling.
In the next codelab, we will transform the bike_computer program into a
data-driven application, in which events generate data that is instantaneously processed
with the help of a WorkQueue.
What you’ll build
In this codelab, you’re going to program simple programs using various multi-tasking primitives. Programs will be presented as exercices that run independently from each other.
What you’ll learn
- How to use threads for performing different tasks in a program.
- How to share resources among different threads.
- How to synchronize tasks based on resources or events, including interrupt-based events.
- What the influence of shared resources can have on task scheduling.
What you’ll need
- Zephyr Development Environment for developing and debugging your program in C++.
- The BikeComputer part 1 codelab and the BikeComputer part 2 codelab are prerequisites to this codelab.
Threads in Zephyr RTOS
Zephyr RTOS runs on single-microcontroller embedded systems. In this context, a thread is an independent segment of a program that executes within the single process running on the microcontroller. Threading enables multiple tasks to run concurrently using a scheduler. It is important to point out that, while threads provide developers with flexibility, they require resources for themselves and the scheduler.
Your application (main function) starts execution in the main thread, but it’s not the only thread running in Zephyr RTOS. When starting an Zephyr RTOS application, there are other threads running system services:
- Main: The default thread that executes the application’s main function,
after all RTOS initializations. The main thread has 1KiB of stack space by
default. The application can configure it in “
prj.conf” by defining theMAIN_STACK_SIZEparameter, as follows:
# increase the stack size of the main thread
CONFIG_MAIN_STACK_SIZE=2048
-
Idle: The scheduler runs this thread when there is no other activity in the system (for example, when all other threads are waiting for an event). This ensures that the program does not burn empty processor cycles and is put to sleep for as long as possible.
-
Logging: The thread that’s run to log all information. It is is created only when
CONFIG_LOGis enabled and when log mode is not immediate (CONFIG_LOG_MODE_IMMEDIATE=n).
Using the Utils Helper Class for Logging Thread Info
The Utils class is available in the
zpp_lib
repository. You may use this class in your BikeSystem implementation to log
statistics related to each thread running in your application.
If you add a call to the zpp_lib::Utils::log_threads_summary() method at the start of the
BikeComputer::start() method, you may observe the three different threads,
as shown below:
Thread statistics logging
[00:00:00.241,166] <inf> zpp_rtos: # | Thread ID | Name | State | Prio | Stack (used/total)
[00:00:00.252,166] <inf> zpp_rtos: ---+----------------+------------+-----------+------+-------------------
[00:00:00.252,502] <inf> zpp_rtos: 1 | 0x20000170 | logging | queued | 14 | 48 / 768
[00:00:00.252,593] <inf> zpp_rtos: 2 | 0x20000848 | idle | ready | 15 | 48 / 320
[00:00:00.253,082] <inf> zpp_rtos: 3 | 0x20000918 | main | queued | 11 | 704 / 4096
[00:00:00.253,082] <inf> zpp_rtos: ---+----------------+------------+-----------+------+-------------------
Zephyr RTOS Scheduling Principles
Thread Priorities
Threads are assigned an integer value to indicate their priority, which can be negative or non-negative. A lower numerical value indicates a higher priority.
Under Zephyr RTOS, threads are either cooperative or preemptible threads:
- Cooperative threads run until they either complete their task or perform an action that renders them unready. Under Zephyr RTOS, these threads have a negative priority value.
- Preemptible threads may become the running thread and may be preempted by a cooperative thread or another preemptible thread with a higher or equal priority. Under Zephyr RTOS, these threads have a non-negative priority value.
The diagram below illustrates the different thread priorities with Zephyr RTOS. Note that CONFIG_NUM_COOP_PRIORITIES
and CONFIG_NUM_PREEMPT_PRIORITIES are configurable parameters.

Scheduling Based on Priority
The scheduler is the part of the kernel responsible for dispatching tasks. At specific points in time, the scheduler will decide which task is to be run.
For the purposes of this lecture, we will only use preemptible threads. With
these threads, the scheduler selects the highest-priority ready thread to be the
current running thread. When there are multiple threads with the same priority
that are ready, the scheduler chooses the one that has been waiting the longest.
Thread priorities are usually established at creation time. A thread priority may
be modified using the zpp_lib::ThisThread::set_priority() function.
With Zephyr RTOS, the queue of ready threads can be implemented using
different mechanisms. The choice depends on various criteria. However, given the
very limited number of threads we will use, the simplest mechanism,
CONFIG_SCHED_SIMPLE, is appropriate.
Preemptive Time Slicing
Once a preemptive thread becomes the current thread, it remains so until a thread with a higher priority becomes ready or the current thread performs an action that renders it unready. Consequently, if a preemptive thread performs lengthy computations, it may cause unacceptable delays in scheduling other threads, including those with the same priority. This is the reason why most schedulers, including the Zephyr RTOS scheduler, implement round-robin or time slicing. This allows threads with the same priority to run concurrently.
In time slicing, the scheduler divides time into time slices. At the end of each time slice, the scheduler checks if there are other threads with the same priority as the current one. If so, it preempts the current preemptible thread and dispatches another one.
Under Zephyr RTOS, time slicing can be configured as follows:
-
CONFIG_TIMESLICINGenables or disables time slicing among preemptible threads of equal priority. Time slicing is enabled by default. -
CONFIG_TIMESLICE_SIZEspecifies the amount of time (in milliseconds) that a thread can execute before being preempted by a thread of equal priority. The default value is 20 milliseconds. -
CONFIG_TIMESLICE_PRIORITYspecifies the thread priority level at which time slicing takes effect. Time slicing is not applied on threads with a higher priority than this ceiling. The default ceiling value is 0.
Priorities Values
It is important to recall that with Zephyr RTOS, higher priority means a smaller priority value. This is made
explicit in zpp_lib, where priorities are defined as enum class PreemptableThreadPriority.
Main Thread Default Priority
The main thread is created by the system with a default priority that can be configured using CONFIG_MAIN_THREAD_PRIORITY.
The default value is 0, meaning that by default the main thread has the highest priority among preemptible threads.
Using zpp_lib, the default main thread priority must be configured because zpp_lib::PreemptableThreadPriority::PriorityRealtime
has a value that is higher than 0 in any case. The default value should be set to zpp_lib::PreemptableThreadPriority::PriorityNormal,
which means a value of CONFIG_NUM_PREEMPT_PRIORITIES - 4 (by default 11).
Rescheduling points
The scheduler needs to decide at specific points in time which task should run. The Zephyr RTOS is a tickless kernel by default. This means that it eliminates the periodic timer interrupts, or “ticks”, generated by the system tick hardware. This provides significant power-saving advantages over traditional kernels:
-
Tickful kernels use a periodic timer interrupt known as the ‘system tick’ that is generated at fixed intervals, regardless of the system’s workload. This ticker serves as a time reference for the scheduler and other kernel functions.
-
Tickless kernels do not rely on fixed intervals to determine which thread to run next. Instead, the scheduler dispatches tasks at rescheduling points. These points include:
- When a thread gives up its task by calling
k_yield()and changing state from Running to Ready. - When a thread goes to sleep by calling
k_sleep(). - When a synchronization object such as a semaphore or mutex is released/unblocked, causing the state of the waiting thread to change from Blocked to Ready.
- When time slicing is enabled and the running thread has been running for the time slice time duration.
- When a thread gives up its task by calling
Zephyr RTOS can be converted into to a tickful kernel by setting CONFIG_TICKLESS_KERNEL=n.
Code of the multi_tasking application
The code used for the different demos explained in the rest of this codelab is made available here. It is almost fully implemented except for some classes that you must complete yourself.
Using zpp_lib::WorkQueue and zpp_lib::Work to Defer Work
Deferring work is a common necessity for embedded programs. In particular, when servicing interrupts, if the interrupt routine must perform a computationally intensive task or a task that may block, it must be deferred to another execution unit. Note that performing any operation that may block, such as locking a mutex, is forbidden in an ISR context.
One way of deferring work under Zephyr RTOS is with the use of workqueue threads.
With zpp_lib, this functionality is encapsulated in the zpp_lib::WorkQueue
and zpp_lib::Work classes.
In the WorkOnButton demo, the ISR is implemented in two different modes:
- In the first mode (
CONFIG_WORK_ON_BUTTON_USE_WORK_QUEUE=n), the ISR attempts to access a critical section and to perform some processing directly. - In the second mode (
CONFIG_WORK_ON_BUTTON_USE_WORK_QUEUE=y), the ISR defers the processing job to another thread using azpp_lib::WorkQueue. The work to be executed by theWorkQueueis implemented using aWorkinstance. It is important to note that thisWorkinstance must exist until theWorkhas been executed by theWorkQueue. The implementation of thework_queuemechanism in Zephyr RTOS does not perform a copy of the work at submission time. For this reason, theWorkinstance is also created as an attribute of theWorkOnButtonclass.
The code is rather simple to understand. Analyze it and answer the questions below.
Question 1: Using a mutex in an ISR and Deferring Work
Depending on the configuration parameters used to build the application, several behaviors can be reproduced with
the provided code sample. Note that the WorkOnButton demo launches whenever you start the multi_tasking
program without pressing any button. Build the application using different parameter configurations and answer the
following questions. Note that you must describe precisely what happens in the kernel, not just the application
behavior. It is helpful to enable logging for the kernel with CONFIG_KERNEL_LOG_LEVEL_DBG=y for a better understanding
of kernel behavior.
-
With
CONFIG_WORK_ON_BUTTON_USE_WORK_QUEUE=n:- Build the application using the command
just build multi_tasking/ log+debug+stats+gpio yes prj_multi_tasking.conf. Flash your board and run the application. At any time, press on Button 1 and explain the observed behavior. - Make sure that
CONFIG_WORK_ON_BUTTON_SLEEP=nis selected in theprj_multi_tasking.conffile and build the application withjust build multi_tasking/ log+stats+gpio yes prj_multi_tasking.conf. At any time, press on Button 1 and explain the observed behavior. - Make sure that
CONFIG_WORK_ON_BUTTON_SLEEP=yis selected in theprj_multi_tasking.conffile and build the application withjust build multi_tasking/ log+stats+gpio yes prj_multi_tasking.conf. Press on Button 1 while the processing thread is performing processing (sleep) outside the CS and explain the observed behavior. - With the same application running, press on Button 1 while the processing thread is performing processing (sleep) inside the CS and explain the observed behavior.
- Build the application using the command
-
With
CONFIG_WORK_ON_BUTTON_USE_WORK_QUEUE=y, build the application with or withoutdebugenabled. Explain the observed behavior.
Using zpp_lib::Events For Waiting For an Event
It is very common in a multi-tasking environment to wait for a specific event to
happen before performing a given task. An obvious example is waiting for a user
to press for a button. For this purpose, Zephyr RTOS provides the Events
API, that is encapsulated in the
zpp_lib::Events class.
For demonstrating the use of this API, we create a WaitOnButton class defined as follows:
wait_on_button.hpp
// Copyright 2025 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/****************************************************************************
* @file wait_on_button.hpp
* @author Serge Ayer <serge.ayer@hefr.ch>
*
* @brief Declaration of the WaitOnButton class
*
* @date 2025-07-01
* @version 1.0.0
***************************************************************************/
#pragma once
// stl
#include <chrono>
// zpp_lib
#include "zpp_include/event.hpp"
#include "zpp_include/interrupt_in.hpp"
#include "zpp_include/thread.hpp"
namespace multi_tasking {
class WaitOnButton {
public:
explicit WaitOnButton(const char* threadName);
[[nodiscard]] zpp_lib::ZephyrResult start();
void wait_started();
void wait_exit();
private:
void wait_for_button_event();
void on_button_pressed();
static constexpr uint8_t kPressedEvent = BIT(0);
static constexpr uint8_t kStartedEvent = BIT(1);
zpp_lib::Thread _thread;
static constexpr zpp_lib::PreemptableThreadPriority kThreadPriority =
zpp_lib::prio_to_preemptable_thread_priority(CONFIG_WAIT_ON_BUTTON_THREAD_PRIORITY);
std::chrono::microseconds _pressed_time;
zpp_lib::Event _event;
zpp_lib::InterruptIn _push_button{zpp_lib::InterruptIn::PinName::BUTTON1};
zpp_lib::RegistrationToken _push_button_token;
};
} // namespace multi_tasking
wait_on_button.cpp
// Copyright 2025 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/****************************************************************************
* @file wait_on_button.cpp
* @author Serge Ayer <serge.ayer@hefr.ch>
*
* @brief Implementation of the WaitOnButton class
*
* @date 2025-07-01
* @version 1.0.0
***************************************************************************/
#include "wait_on_button.hpp"
// zephyr
// zpp_lib
#include "zpp_include/time.hpp"
#include "zpp_include/zpp_log.hpp"
ZPP_LOG_MODULE_DECLARE(multi_tasking, CONFIG_APP_LOG_LEVEL);
namespace multi_tasking {
WaitOnButton::WaitOnButton(const char* threadName)
: _thread(kThreadPriority, threadName), _pressed_time(std::chrono::microseconds::zero()),
_push_button_token(_push_button.add_callback([this]() { this->on_button_pressed(); })) {}
zpp_lib::ZephyrResult WaitOnButton::start() {
auto res = _thread.start([this]() { this->wait_for_button_event(); });
if (!res) {
ZPP_LOG_ERR("Failed to start thread: %d", static_cast<int>(res.error()));
return res;
}
printk("Thread started successfully\n");
return res;
}
void WaitOnButton::wait_started() {
_event.wait_any(kStartedEvent);
}
void WaitOnButton::wait_exit() {
auto res = _thread.join();
if (!res) {
ZPP_LOG_ERR("join() failed: %d", static_cast<int>(res.error()));
}
}
void WaitOnButton::wait_for_button_event() {
printk("Waiting for button press\n");
_event.set(kStartedEvent);
while (true) {
_event.wait_any(kPressedEvent);
std::chrono::microseconds time = zpp_lib::Time::get_uptime();
std::chrono::microseconds latency = time - _pressed_time;
printk("Button pressed with response time: %lld usecs\n", latency.count());
printk("Waiting for button press\n");
}
}
void WaitOnButton::on_button_pressed() {
_pressed_time = zpp_lib::Time::get_uptime();
_event.set(kPressedEvent);
}
} // namespace multi_tasking
In the WaitOnButton class, we create and start an additional thread in the
WaitOnButton::start() method. This thread then executes the
WaitOnButton::waitForButtonEvent() method, which waits in an infinite loop for
a specific event to occur. This event is triggered by a button press (Button 1)
in the WaitOnButton::buttonPressed() method.
main.cpp
// Copyright 2025 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/****************************************************************************
* @file main.cpp
* @author Serge Ayer <serge.ayer@hefr.ch>
*
* @brief Main function of the Multi-Tasking program
*
* @date 2025-07-01
* @version 1.0.0
***************************************************************************/
// zephyr
#include <zephyr/kernel.h>
#include <zephyr/random/random.h>
// std
#include <ostream>
// zpp_lib
#include "zpp_include/this_thread.hpp"
#include "zpp_include/utils.hpp"
#include "zpp_include/zpp_log.hpp"
// local
#include "buffer.hpp"
#include "clock.hpp"
#include "consumer.hpp"
#include "deadlock.hpp"
#include "producer.hpp"
#include "wait_on_button.hpp"
#include "work_on_button.hpp"
ZPP_LOG_MODULE_REGISTER(multi_tasking, CONFIG_APP_LOG_LEVEL);
class RandomIntGenerator {
public:
static constexpr uint8_t kMaxRandomValue = 20;
static uint32_t s_produce_next_value() {
return sys_rand32_get() % kMaxRandomValue;
}
};
class RandomDoubleGenerator {
public:
// kRandomValues is a static array, so using a c array is safe
// NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays,modernize-avoid-c-arrays)
static constexpr double kRandomValues[] = {1.1, 2.2, 3.3, 4.4, 5.5};
static double s_produce_next_value() {
// kRandomValues is a static array, so we are sure that the index is within bounds
// NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-constant-array-index)
return kRandomValues[sys_rand32_get() % (sizeof(kRandomValues) / sizeof(kRandomValues[0]))];
}
};
struct Rect {
int32_t x;
int32_t y;
};
std::ostream& operator<<(std::ostream& os, const Rect& rect) {
os << "(" << rect.y << ", " << rect.x << ")";
return os;
}
class RandomRectGenerator {
public:
// kRandomValues is a static array, so using a c array is safe
// NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays,modernize-avoid-c-arrays)
static constexpr Rect kRandomValues[] = {{.x = 1, .y = 1}, {.x = 2, .y = 2}, {.x = 3, .y = 3}, {.x = 4, .y = 4}, {.x = 5, .y = 5}};
static Rect s_produce_next_value() {
// kRandomValues is a static array, so we are sure that the index is within bounds
// NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-constant-array-index)
return kRandomValues[sys_rand32_get() % (sizeof(kRandomValues) / sizeof(kRandomValues[0]))];
}
};
int main() {
printk("Multi-tasking program started\n");
// check which button is pressed and execute the corresponding demo
zpp_lib::InterruptIn button1(zpp_lib::InterruptIn::PinName::BUTTON1);
zpp_lib::InterruptIn button2(zpp_lib::InterruptIn::PinName::BUTTON2);
zpp_lib::InterruptIn button3(zpp_lib::InterruptIn::PinName::BUTTON3);
zpp_lib::InterruptIn button4(zpp_lib::InterruptIn::PinName::BUTTON4);
if (button1.read() == zpp_lib::kPolarityPressed) {
// log thread statistics
zpp_lib::Utils::log_threads_summary();
printk("Starting WaitOnButton demo\n");
// create the WaitOnButton instance and start it
multi_tasking::WaitOnButton wait_on_button("ButtonThread");
auto res = wait_on_button.start();
if (!res) {
ZPP_LOG_ERR("Cannot start wait_on_button: %d", static_cast<int>(res.error()));
return -1;
}
printk("WaitOnButton demo started\n");
// wait that the WaitOnButton thread started
printk("Calling wait_started()");
wait_on_button.wait_started();
printk("wait_started() unblocked");
// log thread statistics
zpp_lib::Utils::log_threads_summary();
#if CONFIG_WAIT_ON_BUTTON_SLEEP
// wait for the thread to exit (will not because of infinite loop in WaitOnButton)
wait_on_button.wait_exit();
#else // CONFIG_WAIT_ON_BUTTON_SLEEP
// or do busy waiting
while (true) {
}
#endif // CONFIG_WAIT_ON_BUTTON_SLEEP
} else if (button2.read() == zpp_lib::kPolarityPressed) {
printk("Starting Clock demo\n");
// create and start a clock
multi_tasking::Clock clock;
auto res = clock.start();
if (!res) {
ZPP_LOG_ERR("Cannot start wclockait_on_button: %d", static_cast<int>(res.error()));
return -1;
}
} else if (button3.read() == zpp_lib::kPolarityPressed) {
printk("Starting Deadlock demo\n");
// create a first deadlock instance
multi_tasking::Deadlock deadlock0(0, "Thread0");
deadlock0.start();
// create a second deadlock instance
multi_tasking::Deadlock deadlock1(1, "Thread1");
deadlock1.start();
// wait for both threads to terminate (will not because of deadlock)
deadlock0.wait();
deadlock1.wait();
} else if (button4.read() == zpp_lib::kPolarityPressed) {
printk("Starting Consumer/Producer demo\n");
using BufferType = Rect;
using ValueGenerator = RandomRectGenerator;
multi_tasking::Buffer<BufferType> buffer;
multi_tasking::Producer<BufferType, ValueGenerator> producer(buffer);
multi_tasking::Consumer<BufferType> consumer(buffer);
producer.start();
consumer.start();
// wait for threads to terminate (will not)
producer.wait();
consumer.wait();
} else {
printk("Starting WorkOnButton demo\n");
multi_tasking::WorkOnButton work_on_button("ButtonThread");
auto res = work_on_button.start();
if (!res) {
ZPP_LOG_ERR("Cannot start work_on_button: %d", static_cast<int>(res.error()));
return -1;
}
// wait for the thread to exit
work_on_button.wait_exit();
}
return 0;
}
In this example, we can observe the following:
- The use of the
WaitOnButtoninstance in the main function is self-documented. Make sure that you understand it in detail. - After starting the
WaitOnButtoninstance, the main thread waits for the thread to exit. This will never happen, so the main thread will wait forever in thewaitOnButton.wait_exit()method. Recall that the main thread should not exit themain()function for a Zephyr RTOS program to behave properly. - Each time the button is pressed, the
kPressedEventevent is set on the_eventsinstance in theWaitOnButton::on_button_pressed()method. When this event is set, theButtonThreadthread transitions from theWaitingto theReadystate. ThekPressedEventevent is reset on the_eventsinstance and theButtonThreadthread starts waiting again. You can observe this behavior by pressing the button multiple times.
To better understand the program, compile and run it. When you start the program, you should see the following in the console:
- Before the
WaitOnButtoninstance is created and started, the application runs the threemain,idleandloggingthreads, as illustrated above. Note that if theCONFIG_LOG_IMMEDIATE=yoption is set, then theloggingthread is not created. - After the
WaitOnButtoninstance is started, the application creates and runs an additional thread namedButtonThread. - While the program is running, the
ButtonThreadthread will transition from theWaitingto theReadystate each time the button is pressed. Then, it will immediately return to theWaitingstate after printing its message on the console.
Note that it is possible to wait for multiple events at once, as documented in
the Events API.
This is a very useful mechanism in some situations where a task must be executed
when multiple events arise. It is also possible to wait for a specific event as
demonstrated in the WaitOnButton class.
Question Multi-tasking under Zephyr OS/2
In an embedded system, the interrupt response time is an important performance metric. The
WaitOnButton class implements the response time estimation by registering the button press
time in the ISR method, and by computing the time interval until the
waitForButtonEvent thread is signaled. With this mechanism, we can
measure the interrupt response time accurately.
You may notice that the interrupt latency varies depending on some multi-tasking behaviors. Test the following scenarios:
- For each scenario, build your application using the command
just build multi_tasking/ log_app+debug+stats+gpio yes prj_multi_tasking.conf. The different scenarios are created by modifying only the configuration parameters in theprj_multi_tasking.conffile. For this test, it is important not to enable logging for thezpp_liblibrary or for the kernel. - Scenario 1 / 2 / 3: The main thread is in a
Waitingstate, waiting for theWaitOnButtonthread to terminate using theThread::join()method. This is the default behavior with00CONFIG_WAIT_ON_BUTTON_SLEE=y. Change the priority of theWaitOnButtonthread by modifying theCONFIG_WAIT_ON_BUTTON_THREAD_PRIORITYvalue to8/9/10(scenario 1 / 2 / 3) in theprj_multi_tasking.conffile. - Scenario 4 / 5 / 6: Replace the call to
waitOnButton.wait_exit()in themainfunction with a busy infinite waitwhile (true) {}by setting theCONFIG_WAIT_ON_BUTTON_SLEEPconfiguration parameter tonin theprj_multi_tasking.conffile. Test the following scenarios:- Scenario 4: Test with the
WaitOnButtonthread priority set toPriorityNormal, corresponding toCONFIG_NUM_PREEMPT_PRIORITIES - 6or9in theprj_multi_tasking.conffile. - Scenario 5: Modify the priority of the
WaitOnButtonthread toPriorityAboveNormal, corresponding toCONFIG_NUM_PREEMPT_PRIORITIES - 7or8in theprj_multi_tasking.conffile. - Scenario 6: Modify the priority of the
WaitOnButtonthread toPriorityBelowNormal, corresponding toCONFIG_NUM_PREEMPT_PRIORITIES - 5or10in theprj_multi_tasking.conffile.
- Scenario 4: Test with the
Report the interrupt response times for each scenario and explain the reasons for the observed values.
Shared Resources and Mutual Exclusion
On a uniprocessor, multiple threads can run concurrently with interleaved or time-shared execution. On multiprocessor systems, multiple threads can also run simultaneously. In both cases, managing resources such as access to shared memory/variables by multiple threads is an issue. Multitasking or multithreaded programs must ensure that resources are accessed properly, i.e. in the correct sequence as if the program were being executed by a single thread. In other words, the program must sometimes enforce mutually exclusive access to shared resources for only one thread at a time. To this end, most kernels provide a mutex mechanism that can be implemented at either the hardware or software level.
We will first illustrate this problem using an example that simulates a clock keeping track of the current time. To update the clock, a ticker with an interrupt interval of 1 second is used. The structures for defining the time and the ISR to update it are given below:
clock.hpp
// Copyright 2025 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/****************************************************************************
* @file clock.hpp
* @author Serge Ayer <serge.ayer@hefr.ch>
*
* @brief Declaration of the Clock class
*
* @date 2025-07-01
* @version 1.0.0
***************************************************************************/
#pragma once
// stl
#include <chrono>
#include <functional>
// zpp_lib
#include "zpp_include/thread.hpp"
#include "zpp_include/ticker.hpp"
#include "zpp_include/work_queue.hpp"
namespace multi_tasking {
using std::literals::chrono_literals::operator""ms;
class Clock {
public:
struct DateTimeType {
uint32_t day;
uint32_t hour;
uint32_t minute;
uint32_t second;
};
Clock();
// method called for starting the clock demo
[[nodiscard]] zpp_lib::ZephyrResult start();
private:
void display_from_ticker();
void display_current_time();
void update_from_ticker();
void update_current_time();
// type definition used by tickers and queues
using TickerFunction = std::function<void()>;
using WorkQueueFunction = std::function<void()>;
// used for display the current time
zpp_lib::WorkQueue _display_queue;
zpp_lib::Ticker<TickerFunction> _display_ticker;
zpp_lib::Work<Clock> _display_work;
// used for updating _currentTime
zpp_lib::WorkQueue _update_queue;
zpp_lib::Thread _update_thread;
zpp_lib::Ticker<TickerFunction> _update_ticker;
zpp_lib::Work<Clock> _update_work;
#if CONFIG_CURRENT_TIME_MUTEX
zpp_lib::Mutex _mutex;
#endif // CONFIG_CURRENT_TIME_MUTEX
static constexpr auto kNbrOfSecondsInMinute = 60;
static constexpr auto kNbrOfMinutesInHour = 60;
static constexpr auto kNbrOfHoursInDay = 24;
static constexpr auto kInitialHour = 10;
DateTimeType _current_time{.day = 0, .hour = kInitialHour, .minute = kNbrOfMinutesInHour - 1, .second = kNbrOfSecondsInMinute - 2};
static constexpr std::chrono::milliseconds kClockUpdateTimeout = 1000ms;
static constexpr std::chrono::milliseconds kClockDisplayTimeout = 1000ms;
};
} // namespace multi_tasking
clock.cpp
// Copyright 2025 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/****************************************************************************
* @file clock.cpp
* @author Serge Ayer <serge.ayer@hefr.ch>
*
* @brief Implementation of the Clock class
*
* @date 2025-07-01
* @version 1.0.0
***************************************************************************/
#include "clock.hpp"
// zephyr
// zpp_lib
#include "zpp_include/this_thread.hpp"
#include "zpp_include/utils.hpp"
#include "zpp_include/zpp_assert.hpp"
#include "zpp_include/zpp_log.hpp"
ZPP_LOG_MODULE_DECLARE(multi_tasking, CONFIG_APP_LOG_LEVEL);
namespace multi_tasking {
Clock::Clock()
: _display_queue("CDQueue"), _display_work(zpp_lib::Work<Clock>(this, &Clock::display_current_time)), _update_queue("TQueue"),
_update_thread(zpp_lib::PreemptableThreadPriority::PriorityNormal, "TThread"),
_update_work(zpp_lib::Work<Clock>(this, &Clock::update_current_time)) {}
zpp_lib::ZephyrResult Clock::start() {
// Start a thread to run the _ticker_queue work queue.
// Events are dispatched to the queue in the tickerUpdate() method called by the
// ticker.
auto res = _update_thread.start([this] { _update_queue.run(); });
if (!res) {
ZPP_LOG_ERR("Cannot start ticker thread: %d", static_cast<int>(res.error()));
return res;
}
// Call the update_from_ticker() method every kClockUpdateTimeout (from ISR context)
TickerFunction update_from_ticker_function = [this] { update_from_ticker(); };
res = _update_ticker.attach(update_from_ticker_function, kClockUpdateTimeout);
if (!res) {
ZPP_LOG_ERR("Cannot attach update ticker: %d", static_cast<int>(res.error()));
return res;
}
// Call the display_from_ticker() method every kClockDisplayTimeout (from ISR context)
TickerFunction display_from_ticker_function = [this] { display_from_ticker(); };
res = _display_ticker.attach(display_from_ticker_function, kClockDisplayTimeout);
if (!res) {
ZPP_LOG_ERR("Cannot attach display ticker: %d", static_cast<int>(res.error()));
return res;
}
// log thread statistics
zpp_lib::Utils::log_threads_summary();
// run the _display_queue from the calling thread
_display_queue.run();
// should not get here
ZPP_ASSERT(false, "Should not get here");
return res;
}
// display_from_ticker is used as a callback function for the ticker
// NOLINTNEXTLINE(readability-make-member-function-const)
void Clock::display_from_ticker() {
// this method runs in ISR mode -> we cannot allocate memory or perform other
// forbidden operations
auto res = _display_queue.call(_display_work);
ZPP_ASSERT(res, "Cannot call display on queue: %d", static_cast<int>(res.error()));
}
// display_current_time is used as work handler and $
// must be non-const because the work handler is non-const
// NOLINTNEXTLINE(readability-make-member-function-const)
void Clock::display_current_time() {
DateTimeType dt = {};
#if CONFIG_CURRENT_TIME_MUTEX
auto res = _mutex.lock();
ZPP_ASSERT(res, "Cannot lock mutex: %d", static_cast<int>(res.error()));
#endif // CONFIG_CURRENT_TIME_MUTEX
dt.day = _current_time.day;
dt.hour = _current_time.hour;
#if CONFIG_DISPLAY_CURRENT_TIME_WAIT
using std::literals::chrono_literals::operator""s;
zpp_lib::ThisThread::sleep_for(1s);
#endif // CONFIG_DISPLAY_CURRENT_TIME_WAIT
dt.minute = _current_time.minute;
dt.second = _current_time.second;
#if CONFIG_CURRENT_TIME_MUTEX
res = _mutex.unlock();
ZPP_ASSERT(res, "Cannot unlock mutex: %d", static_cast<int>(res.error()));
#endif // CONFIG_CURRENT_TIME_MUTEX
printk("Day %u Hour %u min %u sec %u\n", dt.day, dt.hour, dt.minute, dt.second);
}
// update_from_ticker is used as a callback function for the ticker
// NOLINTNEXTLINE(readability-make-member-function-const)
void Clock::update_from_ticker() {
// this method runs in ISR mode -> we cannot allocate memory or perform other
// forbidden operations
auto res = _update_queue.call(_update_work);
ZPP_ASSERT(res, "Cannot call update on queue: %d", static_cast<int>(res.error()));
}
void Clock::update_current_time() {
#if CONFIG_CURRENT_TIME_MUTEX
auto res = _mutex.lock();
ZPP_ASSERT(res, "Cannot lock mutex: %d", static_cast<int>(res.error()));
#endif // CONFIG_CURRENT_TIME_MUTEX
_current_time.second += std::chrono::duration_cast<std::chrono::seconds>(kClockUpdateTimeout).count();
if (_current_time.second >= kNbrOfSecondsInMinute) {
_current_time.second = 0;
_current_time.minute++;
if (_current_time.minute >= kNbrOfMinutesInHour) {
_current_time.minute = 0;
_current_time.hour++;
if (_current_time.hour >= kNbrOfHoursInDay) {
_current_time.hour = 0;
_current_time.day++;
}
}
}
#if CONFIG_CURRENT_TIME_MUTEX
res = _mutex.unlock();
ZPP_ASSERT(res, "Cannot unlock mutex: %d", static_cast<int>(res.error()));
#endif // CONFIG_CURRENT_TIME_MUTEX
}
} // namespace multi_tasking
It is important to note that the display_from_ticker() and update_from_ticker() methods are
the tickers’ callback function and are both executed within the ISR. Since ISR methods must be short and cannot
contain blocking code, both methods must be deferred to separate WorkQueue instances.
The update of the current time (method update_current_time()) is not executed within the ISR, but rather
deferred to the _update_queue event queue - so it is executed by the _update_thread thread. The main reason for this
is that blocking behaviors are forbidden in ISRs, so acquiring a mutex in an ISR is not possible. Acquiring the mutex in
the update_current_time() method is not currently implemented, but we will show below that it is required.
If you execute the program, you should get the following output:
Clock times
Day 0 Hour 10 min 59 sec 59
Day 0 Hour 11 min 0 sec 0
Day 0 Hour 11 min 0 sec 1
Day 0 Hour 11 min 0 sec 2
Day 0 Hour 11 min 0 sec 3
Day 0 Hour 11 min 0 sec 5
Day 0 Hour 11 min 0 sec 6
...
Everything looks fine, and it seems that the above program will always execute correctly.
However, there is a race condition issue with this program. The problem is
that an interrupt at the wrong time may result in values being updated only partially in the
display_current_time() method. The issue can be explained as follows:
- Say the current time is
day: 0, hour: 10, minute: 59, second: 59or{0, 10, 59, 59}. - The first call to
display_current_time()is executed from themain()function as an event in theWorkQueueinstance that is run by the main thread. The first two instructions of the function are then executed, copying the values{ 0, 10 }todt.day/hour. - A timer interrupt occurs, prompting the other
WorkQueueinstance to update the current time to{0, 11, 0, 0}. - Later, the
display_current_time()function resumes execution and copies the remaining_current_timefields todt, thusdt.minute/second = {0, 0}. - The value of
dtis thus{0, 10, 0, 0}and the program believes that the time just has jumped backwards by one hour!
Although the failure case described above can happen, it is unlikely. To make it
happen easily, it is enough for our display_current_time() method to go to sleep
for \(1\,\mathsf{s}\) after the instruction updating the hour is executed. In this
case, the other ticker will definitely execute before the execution of display_current_time() resumes
and the failure will happen. If you modify the configuration parameters CONFIG_DISPLAY_CURRENT_TIME_WAIT=y,
the display_current_time() function is modified as follows:
clock_with_wait.cpp
...
void display_current_time() {
DateTimeType dt = {0};
dt.day = _current_time.day;
dt.hour = _current_time.hour;
using std::literals::chrono_literals::operator""s;
zpp_lib::ThisThread::busy_wait(1s);
dt.minute = _current_time.minute;
dt.second = _current_time.second;
printk("Day %d Hour %d min %d sec %d\n", dt.day, dt.hour, dt.minute, dt.second);
}
If you run this version, you should see an output on the console similar to the one below. This shows a jump in the clock time:
Clock times
Clock update program started
Day 0 Hour 10 min 0 sec 0
Day 0 Hour 11 min 0 sec 1
Day 0 Hour 11 min 0 sec 2
The above output exhibits a typical race condition problem. Preemption enables
the ISR to interrupt the execution of other code and potentially overwrite
data. To resolve this problem, one must ensure that the object — the
_current_time object in our case — is accessed in an atomic or indivisible way.
For uniprocessor systems, one solution would be to disable or turn off
interrupts at the beginning of the display_current_time() function and re-enable
them on later. While this solves the problem, it may lead to interrupt events to
be missed - in this particular case, the current displayed time may become
incorrect.
The solution that disables interrupts is given below:
clock_with_irq_disabled.hpp
void display_current_time() {
...
auto key = irq_lock();
dt.day = _currentTime.day;
dt.hour = _currentTime.hour;
static constexpr std::chrono::microseconds waitTime = 500ms;
zpp_lib::ThisThread::busyWait(waitTime);
dt.minute = _currentTime.minute;
dt.second = _currentTime.second;
irq_unlock(key);
...
}
In the code above, since we know that an ISR can write to our shared data
object, we disable the interrupt and save the current interrupt masking
state. We then restore the masking state at the end of the function. The same
code needs to be inserted in the update_current_time() method to protect
access to _current_time. Again, although this solves the problem, disabling
interrupts should be avoided since it may affect other processing requests.
Another way of protecting the critical section is with the use of mutual
exclusion objects or Mutex. The goal of a Mutex is to ensure that only one
thread is executing in a critical section at the same time. Mutexes can thus
synchronize the execution of threads and protect access to a shared resource as
illustrated below:

To protect the access to _current_time, you must declare a Mutex instance as
attribute of the class and protect the access to this attribute by locking and unlocking
the Mutex in the display_current_time() and update_current_time() methods. This behavior
is enabled by modifying CONFIG_CURRENT_TIME_MUTEX=y in the prj_multi_tasking.conf file.
Question Multi-tasking under Zephyr OS/3
The use of any Mutex object is not allowed in an ISR context. This is because locking a mutex can cause blocking,
and any blocking code is not permitted in an ISR context.
You must modify the ticker callback so that the Mutex object is accessed from the ISR context. Demonstrate how to
modify the callback and show the errors produced in the Zephyr RTOS kernel code.
Deadlock
Deadlocks can arise in situations where two or more threads wait for each other to complete the task. A deadlock is thus an infinite wait and it occurs when a thread enters a wait state because a shared resource that was requested is being held by another waiting thread, which in turn is waiting for another resource held by another waiting thread.
A number of solutions have been developed for analyzing and preventing deadlock situations. For analyzing situations where deadlocks may arise, Coffman proved that deadlock situation on a resource can arise if and only if all of the following conditions illustrated in the figure below hold simultaneously:
- Mutex: exclusive resource, non-shareable (Resource A and B in the figure below)
- Resource holding: request additional resources while holding one (Requesting resource B while holding Resource A, Resource B held by another process)
- No preemption: resource can not be de-allocated or forcibly removed (Resource B cannot be deallocated)
- Circular wait: circular dependency or a closed chain of dependency (A vs B, Process 1 vs Process 2).

Deadlock prevention works by preventing one of the four Coffman conditions from occurring:
- Removing the mutual exclusion condition means that no process will have exclusive access to a resource (called non-blocking synchronization algorithms).
- The hold and wait or resource holding conditions are changed by making processes ask for all the resources they will need before starting up (difficult to implement).
- Allowing the preemption condition will cause difficulty and the processing outcome may be inconsistent or thrashing may occur.
- The final condition is the circular wait condition. Approaches that avoid circular waits include disabling interrupts during critical sections.
A very simple example for demonstrating a deadlock condition is shown below:
multi_tasking/src/deadlock.cpp
// Copyright 2025 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/****************************************************************************
* @file deadlock.cpp
* @author Serge Ayer <serge.ayer@hefr.ch>
*
* @brief Implementation of the Deadlock class
*
* @date 2025-07-01
* @version 1.0.0
***************************************************************************/
#include "deadlock.hpp"
// zephyr
// zpp_lib
#include "zpp_include/zpp_assert.hpp"
#include "zpp_include/zpp_log.hpp"
ZPP_LOG_MODULE_DECLARE(multi_tasking, CONFIG_APP_LOG_LEVEL);
namespace multi_tasking {
Deadlock::Deadlock(uint8_t index, const char* threadName)
: c_index(index), _thread(zpp_lib::PreemptableThreadPriority::PriorityNormal, threadName) {
ZPP_ASSERT(index < kNbrOfMutexes, "Index must be smaller than %d", kNbrOfMutexes);
}
void Deadlock::start() {
auto res = _thread.start([this]() { this->execute(); });
ZPP_ASSERT(res, "Cannot start deadlock thread: %d", static_cast<int>(res.error()));
}
void Deadlock::wait() {
auto res = _thread.join();
ZPP_ASSERT(res, "Cannot join deadlock thread: %d", static_cast<int>(res.error()));
}
void Deadlock::execute() const {
// _index is initialized in the constructor and asserted for correctness
// NOLINTBEGIN(cppcoreguidelines-pro-bounds-constant-array-index)
// enter the first critical section
auto res = s_mutex[c_index].lock();
ZPP_ASSERT(res, "Cannot lock mutex: %d", static_cast<int>(res.error()));
printk("Thread %d entered critical section %d\n", c_index, c_index);
// perform some operations
zpp_lib::ThisThread::sleep_for(kProcessingWaitTime);
printk("Thread %d processing in mutex %d done\n", c_index, c_index);
// enter the second critical section
int second_index = (c_index + 1) % kNbrOfMutexes;
printk("Thread %d trying to enter critical section %d\n", c_index, second_index);
res = s_mutex[second_index].lock();
ZPP_ASSERT(res, "Cannot lock mutex: %d", static_cast<int>(res.error()));
printk("Thread %d entered critical section %d\n", c_index, second_index);
// perform some operations
zpp_lib::ThisThread::busy_wait(kProcessingWaitTime);
printk("Thread %d processing in mutex %d and %d done\n", c_index, c_index, second_index);
// exit the second critical section
res = s_mutex[second_index].unlock();
ZPP_ASSERT(res, "Cannot unlock mutex: %d", static_cast<int>(res.error()));
// perform some operations
zpp_lib::ThisThread::busy_wait(kProcessingWaitTime);
printk("Thread %d processing in mutex %d done\n", c_index, c_index);
// exit the first critical section
res = s_mutex[c_index].unlock();
ZPP_ASSERT(res, "Cannot unlock mutex: %d", static_cast<int>(res.error()));
// NOLINTEND(cppcoreguidelines-pro-bounds-constant-array-index)
}
} // namespace multi_tasking
multi_tasking/src/deadlock.hpp
// Copyright 2025 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/****************************************************************************
* @file deadlock.hpp
* @author Serge Ayer <serge.ayer@hefr.ch>
*
* @brief Declaration of the Deadlock class
*
* @date 2025-07-01
* @version 1.0.0
***************************************************************************/
#pragma once
// stl
#include <chrono>
// zpp_lib
#include "zpp_include/mutex.hpp"
#include "zpp_include/this_thread.hpp"
#include "zpp_include/thread.hpp"
namespace multi_tasking {
using std::literals::chrono_literals::operator""us;
class Deadlock {
public:
Deadlock(uint8_t index, const char* threadName);
void start();
void wait();
private:
void execute() const;
// time that the threads should spend processing (e.g. wait in our case)
static constexpr std::chrono::microseconds kProcessingWaitTime = 1000000us;
static constexpr int kNbrOfMutexes = 2;
const uint8_t c_index;
zpp_lib::Thread _thread;
// the mutex must be declared as static for being a class instance
// kNbrOfMutexes is a constant, so using a c array is safe
// NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays,modernize-avoid-c-arrays)
static inline zpp_lib::Mutex s_mutex[kNbrOfMutexes] = {};
};
} // namespace multi_tasking
In this example, we use two Mutex instances for synchronizing the execution of
two threads. The program implements the circular wait as described in the
conditions and in the figure above. The output of the program is shown below. As
you can observe Thread0 is blocked waiting for mutex 1 (owned by Thread1) and
Thread1 is blocked waiting for mutex 0 (owned by Thread0), since no thread can
enter the second critical section.
Deadlock program
Multi-tasking program started
Starting Deadlock demo
Thread 0 entered critical section 0
Thread 1 entered critical section 1
Thread 0 processing in mutex 0 done
Thread 0 trying to enter critical section 1
Thread 1 processing in mutex 1 done
Thread 1 trying to enter critical section 0
Shared Data and Semaphore
Is a Mutex a Binary Semaphore?
Like a mutex, a semaphore can manage and control access to shared resources. Unlike a mutex, it does not have the concept of an owner (a mutex is owned by the thread that entered the critical section). On the contrary, a semaphore defines the maximum number of threads that are allowed to use a particular resource at the same time. A mutex is a locking mechanism while a semaphore is a signaling mechanism.
It is important to mention that mutex and semaphore are different concepts and that it is incorrect to think of a mutex as a binary semaphore. As explained in Mutexes and Semaphores Demystified, a mutex is meant to be taken and released, always in that order, by each task that uses the shared resource. By contrast, tasks that use semaphores either signal or wait - not both. In such, they are often related to producer-consumer problems. Also, mutex objects cannot be used within an ISR, while semaphores can be used to signal from ISR context to another thread - signaling is not a blocking mechanism. This makes semaphores a powerful mechanism for signaling from ISR context.
Semaphore for Access to Shared Resources
For illustrating the use of semaphores, we demonstrate a typical producer-consumer problem. The problem can be solved using different {{ mbedos }} concurrency primitives and we will also evaluate their advantages and disadvantages of each approach.
The problem is stated as follows:
- The scenario is executed by two separate tasks/threads: the producer thread
implemented in the
Producerclass (_producerThread) and the consumer thread implemented in theConsumerclass (_consumerThread). - Both threads share a FIFO buffer implemented in the
Bufferclass (_buffer). TheBufferclass is a C++ template class that can store elements of typeT. - One producer repeatedly produces values at a
random rate (
Producer::produce()method). It then appends the produced value to the FIFO buffer (Buffer::append()method). - One consumer repeatedly extracts values from the FIFO buffer
(
Buffer::extract()method) and consume them (Consumer::consume()method). - The FIFO buffer has no mutual exclusion and no under- or overflow protection. This means that the consumer may consume values that have not yet been produced, and that the producer may overwrite values that have not yet been consumed.
- To simulate computing time required for producing/appending and
consuming/extracting data to/from the buffer, calls to the
busyWait()method are added to theBuffer::extract()andBuffer::append()methods. - LED1 is used to indicate when data is being appended to the buffer, while LED2 is used to indicate when data is being extracted from the buffer. Both LEDs are on when either event occurs.
The basic implementation of the Buffer class is given below:
buffer.hpp
// Copyright 2025 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/****************************************************************************
* @file buffer.hpp
* @author Serge Ayer <serge.ayer@hefr.ch>
*
* @brief Declaration/Implementation of the Buffer class
*
* @date 2025-07-01
* @version 1.0.0
***************************************************************************/
#pragma once
// zephyr
#include <zephyr/random/random.h>
// zpp_lib
#include "zpp_include/digital_out.hpp"
#include "zpp_include/mutex.hpp"
#include "zpp_include/non_copyable.hpp"
#include "zpp_include/semaphore.hpp"
#include "zpp_include/this_thread.hpp"
#include "zpp_include/zpp_assert.hpp"
namespace multi_tasking {
using std::literals::chrono_literals::operator""ms;
static constexpr bool kLedOff = false;
static constexpr bool kLedOn = true;
template <typename T> class Buffer : public zpp_lib::NonCopyable {
public:
Buffer() : _producer_led(zpp_lib::DigitalOut::PinName::LED0, kLedOff), _consumer_led(zpp_lib::DigitalOut::PinName::LED1, kLedOff) {}
[[nodiscard]] uint32_t append(const T& data) {
#if CONFIG_BUFFER_USE_MUTEX_AND_SEMAPHORE
// make sure that we can produce without overflow
auto res = _in_semaphore.acquire();
ZPP_ASSERT(res, "Cannot acquire inSemaphore: %d", (int)res.error());
// lock buffer
res = _producer_consumer_mutex.lock();
ZPP_ASSERT(res, "Cannot lock mutex: %d", (int)res.error());
#endif // CONFIG_BUFFER_USE_MUTEX_AND_SEMAPHORE
_producer_led = kLedOn;
// _producer_index is updated with % kBufferSize
// NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-constant-array-index)
_buffer[_producer_index] = data;
uint32_t index = _producer_index;
_producer_index = (_producer_index + 1) % kBufferSize;
zpp_lib::ThisThread::busy_wait(compute_random_wait_time(kApppendWaitTime));
_producer_led = kLedOff;
#if CONFIG_BUFFER_USE_MUTEX_AND_SEMAPHORE
// unlock buffer
res = _producer_consumer_mutex.unlock();
ZPP_ASSERT(res, "Cannot unlock mutex: %d", (int)res.error());
// tell that one element is available for consumer
res = _out_semaphore.release();
ZPP_ASSERT(res, "Cannot release outSemaphore: %d", (int)res.error());
#endif // CONFIG_BUFFER_USE_MUTEX_AND_SEMAPHORE
return index;
}
[[nodiscard]] uint32_t extract(T& data) {
#if CONFIG_BUFFER_USE_MUTEX_AND_SEMAPHORE
// make sure that we can consume without underflow
auto res = _out_semaphore.acquire();
ZPP_ASSERT(res, "Cannot acquire outSemaphore: %d", (int)res.error());
// lock buffer
res = _producer_consumer_mutex.lock();
ZPP_ASSERT(res, "Cannot lock mutex: %d", (int)res.error());
#endif // CONFIG_BUFFER_USE_MUTEX_AND_SEMAPHORE
_consumer_led = kLedOn;
// _consumer_index is updated with % kBufferSize
// NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-constant-array-index)
data = _buffer[_consumer_index];
uint32_t index = _consumer_index;
_consumer_index = (_consumer_index + 1) % kBufferSize;
zpp_lib::ThisThread::busy_wait(compute_random_wait_time(kExtractWaitTime));
_consumer_led = kLedOff;
#if CONFIG_BUFFER_USE_MUTEX_AND_SEMAPHORE
// unlock buffer
res = _producer_consumer_mutex.unlock();
ZPP_ASSERT(res, "Cannot unlock mutex: %d", (int)res.error());
// tell that one element is available for producer
res = _in_semaphore.release();
ZPP_ASSERT(res, "Cannot release inSemaphore: %d", (int)res.error());
#endif // CONFIG_BUFFER_USE_MUTEX_AND_SEMAPHORE
return index;
}
[[nodiscard]] std::chrono::milliseconds compute_random_wait_time(const std::chrono::milliseconds& wait_time) {
return std::chrono::milliseconds((sys_rand32_get() % wait_time.count()) + wait_time.count());
}
private:
static constexpr std::chrono::milliseconds kApppendWaitTime = 500ms;
static constexpr std::chrono::milliseconds kExtractWaitTime = 500ms;
static constexpr uint8_t kBufferSize = 10;
zpp_lib::DigitalOut _producer_led;
zpp_lib::DigitalOut _consumer_led;
#if CONFIG_BUFFER_USE_MUTEX_AND_SEMAPHORE
zpp_lib::Mutex _producer_consumer_mutex;
zpp_lib::Semaphore _out_semaphore{0, kBufferSize - 1};
zpp_lib::Semaphore _in_semaphore{kBufferSize - 1, kBufferSize - 1};
#endif // CONFIG_BUFFER_USE_MUTEX_AND_SEMAPHORE
// kBufferSize is a constant, so using a c array is safe
// NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays,modernize-avoid-c-arrays)
T _buffer[kBufferSize] = {};
uint32_t _producer_index = 0;
uint32_t _consumer_index = 0;
};
} // namespace multi_tasking
The code of the Producerclass that uses the Buffer class is:
producer.hpp
// Copyright 2025 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/****************************************************************************
* @file buffer.hpp
* @author Serge Ayer <serge.ayer@hefr.ch>
*
* @brief Declaration/Implementation of the Producer class
*
* @date 2025-07-01
* @version 1.0.0
***************************************************************************/
#pragma once
// zephyr
#include <zephyr/random/random.h>
// stl
#include <iostream>
// zpp_lib
#include "zpp_include/non_copyable.hpp"
#include "zpp_include/thread.hpp"
#include "zpp_include/zpp_assert.hpp"
// local
#include "buffer.hpp"
namespace multi_tasking {
template <typename T, class DataGenerator> class Producer : public zpp_lib::NonCopyable {
public:
explicit Producer(Buffer<T>& buffer)
: _buffer(buffer), _producer_thread(zpp_lib::PreemptableThreadPriority::PriorityNormal, "ProducerThread") {}
void start() {
auto res = _producer_thread.start([this] { producer_method(); });
ZPP_ASSERT(res, "Cannot start producer thread: %d", (int)res.error());
}
void wait() {
auto res = _producer_thread.join();
ZPP_ASSERT(res, "Cannot join producer thread: %d", (int)res.error());
}
private:
void producer_method() {
while (true) {
T producer_data = DataGenerator::s_produce_next_value();
uint32_t index = _buffer.append(producer_data);
std::cout << "Producer data is " << producer_data << " (index in buffer " << index << ")" << std::endl;
}
}
static constexpr std::chrono::milliseconds kProduceWaitTime = 500ms;
Buffer<T>& _buffer;
zpp_lib::Thread _producer_thread;
};
} // namespace multi_tasking
The code of the Consumerclass that uses the Buffer class is:
consumer.hpp
// Copyright 2025 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/****************************************************************************
* @file consumer.hpp
* @author Serge Ayer <serge.ayer@hefr.ch>
*
* @brief Declaration/Implementation of the Consumer class
*
* @date 2025-07-01
* @version 1.0.0
***************************************************************************/
#pragma once
// stl
#include <iostream>
// zpp_lib
#include "zpp_include/non_copyable.hpp"
#include "zpp_include/thread.hpp"
// local
#include "buffer.hpp"
namespace multi_tasking {
template <typename T> class Consumer : public zpp_lib::NonCopyable {
public:
explicit Consumer(Buffer<T>& buffer)
: _buffer(buffer), _consumer_thread(zpp_lib::PreemptableThreadPriority::PriorityNormal, "ConsumerThread") {}
void start() {
auto res = _consumer_thread.start([this] { consumer_method(); });
ZPP_ASSERT(res, "Cannot start consumer thread: %d", (int)res.error());
}
void wait() {
auto res = _consumer_thread.join();
ZPP_ASSERT(res, "Cannot join consumer thread: %d", (int)res.error());
}
private:
void consume(T data) {
// does nothing
}
void consumer_method() {
while (true) {
T consumer_data = {};
uint32_t index = _buffer.extract(consumer_data);
consume(consumer_data);
std::cout << "Consumer data is " << consumer_data << " (index in buffer " << index << ")" << std::endl;
}
}
Buffer<T>& _buffer;
zpp_lib::Thread _consumer_thread;
};
} // namespace multi_tasking
The code to use all classes and simulate a producer/consumer scenario is given in the main program.
To gain an in-depth understanding of the program, make sure that the configuration parameter
CONFIG_BUFFER_USE_MUTEX_AND_SEMAPHORE is set to n and proceed as follows:
-
Scenario 1: Start the program with the producer running only (comment the call to
consumer.start()in themainfunction). Make sure to run the program, describe what you observe and give an explanation of your observations. In particular, answer the following questions:- Which led is blinking?
- How is the
_producer_indexvalue of theBufferclass changing? - Can the producer overwrite values that have not be consumed?
-
Scenario 2: Do the opposite and start the program with the consumer running only. Answer the same corresponding questions.
- Scenario 3 / 4: Then, start the program with both the producer and consumer running. Answer
the same questions again. Experiment with different time values, simulating
both cases where production is faster than consumption (Scenario 3) and where consumption
is faster than production (Scenario 4). This can be accomplished by making
kApppendWaitTimeorkExtractWaitTimemuch smaller than the other waiting time.
Question Multi-tasking under Zephyr OS/4
Answer all questions for each different scenario above.
From the experiments and observations made above, one should be able to make the following conclusions:
- The access to the buffer should be protected and one should ensure that only
one thread can access the buffer at the same time. This
mechanism can be implemented with a
Mutexas seen in a previous section of this codelab. - There should be a mechanism for handling buffer over-consumption or over-production. In other words, the consumer should not be able to consume an element when the buffer is empty and the producer should not be able to produce an element when the buffer is full. This control mechanism should move the threads in a waiting state until the requested operation becomes possible. This is where semaphores enter the game!
The Semaphore mechanism allows the producer thread to signal when an element is
added to the buffer and the consumer thread to wait for at least one element to
be present in the buffer. The Semaphore also implements a counting mechanism.
To implement the changes documented above, you must change the configuration parameter
CONFIG_BUFFER_USE_MUTEX_AND_SEMAPHORE to y in the prj_multi_tasking.conf file. With this
parameter, the Buffer class is modified as follows:
-
A
Mutexis added in theBufferclass and accesses to_bufferand indices are protected. -
A mechanism to make sure that there will be at least one element in the buffer before the consumer can consume it is implemented. It uses a
Semaphorewith the initial value corresponding to no element produced. This solves the over-consumption or underflow issue. -
A similar mechanism to prevent that no element can be produced when the buffer is full is implemented, by using another
Semaphoreinstance. In this case, the semaphore is initialized at the buffer capacity and with inversed roles: the producer wait for the semaphore while the consumer signals it. This solves the over-production or overflow issue.
Question Multi-tasking under Zephyr OS/5
Assuming that the Mutex and Semaphore mechanisms are fully and correctly implemented, describe what happens if
you remove the calls below in the Buffer class:
_in_semaphore.acquire()_in_semaphore.release()_out_semaphore.acquire()_out_semaphore.release()
Built-in Mechanisms for Passing Data
It is worth mentioning at this point that {zephyr_os} provides other built-in mechanisms for passing data between several tasks/threads:
zpp_lib encapsulates the message queue mechanism into a C++ class. The zpp_lib::MessageQueue
class
is a template class that allows you to create message queues with T as the
type of messages being passed and queueSize as the size of the queue.
Shared Data and MessageQueue
As mentioned previously, RTOS typically offer higher-level abstractions for
implementing buffers and data-passing scenarios. Therefore, it is thus possible to implement the
same Buffer class as in the previous step using these higher-level mechanisms.
The goal here is to implement the Buffer with a MessageQueue
thus replacing all internal buffer, mutex and semaphore related fields.
Interestingly, all the changes required to make the implementation correct are
made solely in the Buffer class. This shows that encapsulating functionality
within a well-designed API facilitates the development, debugging, and
maintenance of a well-designed program.
Furthermore, template classes allow one class to be specified for passing data of any type, as demonstrated in the demo program:
- In the “main.cpp” file, several classes are defined for generating data (i.e.
RandomIntGenerator,RandomDoubleGeneratorandRandomRectGenerator). This demonstrates how a template class can be used to implement specific behaviour, such as theproduceNextValue()method within theProducerclass. - In the
main.cppfile, the operator for printing ‘Rect’ values to the console is also defined (i.e. ‘std::ostream& operator« (std::ostream& os, const Rect& rect)’). This enables the ‘Producer’ and ‘Consumer’ classes to print data of different types without amending the classes’ code.
Exercice Multi-tasking under Zephyr OS/6
Modify the Buffer class implementation to use the zpp_lib::MessageQueue mechanism.
Wrap-Up
By the end of this codelab, you should have achieved the following:
- You understand the basic principles behind the Zephyr RTOS schedulers.
- You know how to monitor the different threads of a multi-tasking application.
- You understand how threads of different or identical priorities interact with each other.
- You understand the principles of deadlock.
-
You understand the following multi-tasking mechanisms:
- Mutex
- Semaphore
- Data passing mechanisms
- You understand how template classes can be used to create a single class for exchanging data of different types.
Deliverables
Deliverables/Requirements (Project Phase C)
The deliverables/requirements for project Phase C include the following:
- The answers to all questions of this codelab are documented in the project
README.mdfile