Getting started with Zephyr RTOS
Tools for programming the codelabs and the project
To program your target device in the different codelabs, you need to use different tools. This codelab introduces these tools and allows you to write, compile and run your first Zephyr RTOS application on your target device.
What you’ll build
- A complete Zephyr RTOS development workspace using west (T3 topology)
- Your first
blinkyapplication in C++, using Zephyr RTOS drivers and thezpp_liblibrary to blink an LED.
What you’ll learn
- How Zephyr RTOS is structured and how to set it up.
- How to configure and use the Zephyr RTOS logging system.
- How to debug a Zephyr RTOS application using VS Code and Cortex-Debug.
- How to run your application on QEMU.
What you’ll need
- The Zephyr Development Environment for developing and debugging C++ code snippets.
- Some prior basic knowledge of C++ programming.
Installation of required software components
We will practise all the codelabs using the Zephyr Development Environment, which will allow you to to develop and debug Zephyr RTOS applications. For the codelabs and the project, we recommend that you use VS Code as your IDE and we also recommend that you create a Python virtual environment. The use of these tools and environment is not mandatory, but we will not document alternatives.
Warning
Pay attention to avoiding spaces in the path or the project folder itself, as this may prevent you from building the application(s). On Windows machines, choose a short installation path to prevent issues related to long paths.
The first steps for installing the development framework are:
- Install VS Code as documented here.
- Install the required Zephyr RTOS tools as described here. Note that it is highly recommended to install Python version 3.12.
- Launch a terminal and create a directory where you will store your codelabs and project.
- Launch VS Code and open the folder you just created.
- In this workspace, create a venv virtual Python environment in VS Code as documented here.
- Launch a new terminal in VS Code. This terminal should run in your Python virtual environment - a (.venv) should print in the command prompt.
Always starting a terminal in the virtual environment
To make sure that when you launch a terminal in VS Code it always activate
the virtual environment, you must add the following line in the .vscode/settings.json file.
You must create the file if it does not exist.
{
"python.terminal.activateEnvironment": true
}
Installing west
west is a repository management system distributed as a Python package. It is closely related to Zephyr RTOS, but is developed in its own repository. With Zephyr RTOS, west is used to manage project dependencies, but also to build, debug and flash programs. This is possible because west allows the definition of extension commands that add additional functionality, and the Zephyr SDK adds such commands to west for building, debugging and flashing applications.
If you type west in the terminal window, you should get an error saying that
west is unknown because it does not exist in your virtual environment. To
install west in this virtual environment, follow these
instructions.
After installing west, you can type west --version and the installed
version should print in the terminal.
Zephyr RTOS application types
When developing Zephyr RTOS applications, you can choose between different application types, depending on where the application folder is located. The application folder is the folder in which you write your own application, and this folder can be located:
- in the Zephyr RTOS repository, where the Zephyr RTOS source code is
located. These applications are called
repositoryapplications. - in the west workspace where the Zephyr RTOS is installed. These
applications are called
workspaceapplications and support different west topologies. We will use this model and give more details on how to use it below. - elsewhere on your system. These applications are called
freestandingapplications.
west Workspaces and Manifest File
west uses a manifest file, usually named west.yml. The directory in which
this file resides is called the manifest repository. For workspace
applications, the manifest repository may be located in different folders,
defining different west
topologies:
- In the T1 star topology, the manifest repository is the Zephyr RTOS repository.
The manifest file is thus the
west.ymlfile located in the Zephyr RTOS folder and the Zephyr RTOS repository acts as the super project with external modules imported inwest.yml. - In the T2 star topology, the application folder is the manifest repository
and the manifest file is the
west.ymlfile located in the application folder. This topology is useful if you are focusing on a single application and you want to restrict dependencies with other applications. - In the T3 forest topology, the
west.ymlfile is located in a separate folder. This topology is useful if you are developing several applications relying on the same dependencies and you want to restrict dependencies with other applications relying on different versions or modules.
As explained above, for our codelabs and project, we will create Zephyr RTOS workspace applications. Since we will develop several applications, we will apply the T3 forest topology. This configuration should not be confused with the freestanding application configuration, in which the manifest repository is the Zephyr RTOS repository. Because Zephyr RTOS evolves fast, it is also very important to work with fixed versions of all modules and Zephyr RTOS’s source code.
In our case, the folder structure of our workspace will look like:
tsm_advembsof-workspace/
├── .git/ # Git repository for the entire workspace (does not contain dependencies)
│
├── bike_computer/
│ ├── CMakeLists.txt
│ ├── prjconf.conf
│ └── src/
│ └── main.cpp
├── codelab-1/
│ ├── CMakeLists.txt
│ ├── prjconf.conf
│ └── src/
│ └── main.cpp
├── blinky/
│ ├── CMakeLists.txt
│ ├── prjconf.conf
│ └── src/
│ └── main.cpp
├── manifest-repo/ # never modified by west
│ └── west.yml # main manifest with optional import(s) and override(s)
├── deps/
│ └── modules/
│ └── hal/
│ └── cmsis_6/ # .git/ project from either the main manifest or
│ # from some import
│ └── zephyr/ # .git/ project
│ └── west.yml # This can be partially imported with lower precedence or ignored.
│ # Only the 'manifest-rev' version can be imported
│ └── zpp_lib/ # .git/ project
│ └── ... # Separate library delivered as a module
This folder structure corresponds to a T3 topology with 3 applications named
bike_computer, codelab-1 and blinky. All applications rely on the same
west.yml file and use the same versions of Zephyr RTOS and modules. Note
that all dependencies of the applications are stored in a folder named deps.
This is arbitrary and these dependencies could be stored using a different folder
structure.
Initializing your west workspace
For specifying the a west workspace using T3 forest topology, you must:
- Create a
manifest-repofolder in your workspace folder. - In the
manifest-repofolder, create a filewest.yml. This file will contain the list of all external dependencies. It is important to note that the manifest repository (containing the manifest file) is a direct sibling of the workspace root directory, and it is a git repository. -
Start editing the
west.ymlfile:manifest-repo/west.ymlmanifest: # lowest version of the manifest file schema that can parse this file’s data version: 0.13 -
Open a terminal and change directory to the
manifest-repofolder. For initializing the west workspace, typewest init -l .. This creates a.westdirectory in the workspace root directory, which in turn contains a configuration fileconfig. Theconfigfile contains the name and the location of thewest.yml, in our case:.west/config[manifest] path = manifest-repo file = west.yml -
Change directory back to the workspace root directory and run
west update. Given that no dependency were added in thewest.ymlfile, nothing should happen.
YAML specification
It is important to note that in YAML, structure is determined by
indentation, not by indicators (like {} in JSON). It is also important to
note that indentation is defined as zero or more spaces at the beginning of
a line, and that tabs may not be used in indentation. Each node must be indented
further than its parent node. All sibling nodes must use the exact same indentation level.
For full details on the YAML specification see the official documentation.
Specifying project dependencies
Dependencies can be added in the west.yml using the manifest.projects
key. With this key you can specify all the git repositories you want to add to
your workspace.
Dependencies are not necessarly related to Zephyr RTOS and can be any utility that is used in a project. Let’s say we plan to use Doxygen documentation for our project, so we want to add Doxygen Awesome to our workspace. We can add this external dependency as an entry in the manifest.projects key of our manifest file:
manifest:
# lowest version of the manifest file schema that can parse this file’s data
version: 0.13
projects:
- name: doxygen-awesome
url: https://github.com/jothepro/doxygen-awesome-css
# you can also use SSH instead of HTTPS:
# url: git@github.com:jothepro/doxygen-awesome-css.git
revision: main
path: deps/utilities/doxygen-awesome-css
At this point, it is important to note the following regarding the west.yml file:
- In YAML terms, the manifest file contains a mapping, with a key named
manifest. Any other keys and their contents are ignored. - The
manifestkey contains subsections, likedefaults,remotes,projects, andself. In YAML terms, the value of themanifestkey is also a mapping, with these subsections as keys. All of these subsection keys are optional. -
The
projectskey contains a list of repositories managed by west and associated metadata:- Every projet has a unique
name, heredoxygen-awesomefor example. - A
urlof the repository is typically specified. This can also be done through aremotethat is specified in theremoteskey. - In the example above, the
doxygen-awesomerepository is cloned to the pathdeps/utilities/doxygen-awesome-cssrelative to the root directory of the west workspace, since it has an explicitpathattribute. Without specifying the path, west uses the project name as the path. Note that the path cannot be outside the root directory of the west workspace, but the location inside the workspace is a personal choice. However, grouping dependencies into a single folder makes them easier to manage - for example, to delete this folder to save disk space when needed. - The
revisionkey specifies that the current tip of themainbranch should be fetched. Normally, one would specify a tag or even a specific commit in the revision, rather than the latest version.
- Every projet has a unique
-
You can find examples and more detailed explanation of all available options in the official documentation.
At this point, if you run west update after modifying the west.yml
file, you should notice that the doxygen-awesome-css repository is fetched to
your west workspace under the deps/utilities folder.
west update with remote west.yml file
Running west update only checks the contents of the local west.yml.
If your west.yml is stored on a git repository, it won’t try to pull
new changes in the manifest repository and it also won’t attempt to read the
file from the remote. If there were any changes to the west.yml file in
the repository, you must git pull them in your manifest repository. In fact,
west update will never attempt to modify the manifest repository.
Prelimary Note on Application Configurations and Builds
In this lecture, we will develop several applications with different configurations for various boards. As you will
learn, the reproducibility of application behaviors depends on the correct use of configuration parameters and board
definitions. In this codelab, you will build everything from scratch. In the following codelabs, however, you will
build applications using scripts, application configuration parameters, and board definitions made available with
the zpp_lib library. More details will be provided at the end of this codelab.
Create the blinky application
For any Zephyr RTOS application, the basic file structure is as follows:
├── blinky/ # .git/ project
│ ├── CMakeLists.txt
│ ├── prj.conf
│ └── src/
│ └── main.cpp
Basically, each application is made of at least these three files:
-
main.cpp: It contains the definition of themainfunction. Here, we put it in a subdirectory calledsrcand it has a.cppextension since we use C++ for coding our applications.blinky/src/main.cppint main(void) { while (true) {} return 0; } -
prj.conf: This is the configuration file required for any Zephyr RTOS application. It is empty at this point.blinky/prj.conf -
CMakeLists.txt: The build system is based oncmakeand aCMakeLists.txtfile is required. More details on this topic will be given later.blinky/CMakeLists.txtcmake_minimum_required(VERSION 3.20.0) find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) project( Blinky VERSION 0.1 DESCRIPTION "CPP Blinky application." LANGUAGES C CXX ) target_sources( app PRIVATE src/main.cpp )
You must accomplish the following:
- Create this file structure for your application in your workspace and copy the content of each file as documented above.
- Type
west build blinkyfrom the top directory of your workspace. You should get an error saying that thebuildcommand is unknown.
This is because the build command is not part of the basic west installation. The build command is a
command that is part of the Zephyr RTOS extension commands, such as the flash command. So
the next step is to introduce the dependency of our application on the Zephyr RTOS library.
Add Zephyr RTOS and Build the blinky Application
Zephyr RTOS can be added as dependency to the blinky application like any other dependency:
manifest:
# lowest version of the manifest file schema that can parse this file’s data
version: 0.13
projects:
...
- name: zephyr
revision: v4.4.0
url: https://github.com/zephyrproject-rtos/zephyr
path: deps/zephyr
# explicitly add Zephyr-specific west extensions
west-commands: scripts/west-commands.yml
With this definition, the v4.4.0 revision of the Zephyr RTOS
repository will be cloned into the relative path deps/zephyr of the top
directory. The west.yml file also indicates that the commands defined
in scripts/west-command.yml of the Zephyr RTOS repository will be imported.
To apply the changes to your local workspace, you need to run west update
again. This may take some time as the Zephyr RTOS library is quite large.
After updating your workspace, you can try running west build blinky again.
It should not run successfully and you should see an error saying that some
tools are missing. At this point, you need to install additional tools:
- Since we are using a Python virtual environment, we need to install all
Python packages required by Zephyr RTOS. This should be done by running
pip install -r deps/zephyr/scripts/requirements.txt` - The west build command is based on the use of
cmake. You should have installed if when installing the required Zephyr RTOS tools. Ifcmakeis not installed on your machine, you should get an error. In this case, follow the instructions in the Zephyr RTOS Getting Started Guide - Install Dependencies to installcmakeand the other required development tools. - A toolchain is required to build Zephyr RTOS applications. For this purpose, you need to install the Zephyr RTOS SDK.
-
Setup environment variables as described here. In particular, you must make sure that the following variables are set correctly:
ZEPHYR_BASE: it should contain the full path to yourdeps/zephyrfolder where the Zephyr RTOS repository was cloned.ZEPHYR_SDK_INSTALL_DIR: it should contain the full path to the folder in which you installed the Zephyr SDK.ZEPHYR_TOOLCHAIN_VARIANT: it should be set tozephyr.
After installing all required tools and setting the environement properly, if
you run west build blinky again, the build command should start but you should
still get an error saying that you need to specify a board for the build. This
is illustrated below:
ERROR: Cached board not defined, please provide it (provide --board, set default
with "west config build.board <BOARD>", or set BOARD in the environment)
FATAL ERROR: refusing to proceed without --force due to above error
Specifying the board at build time can be done in two different ways:
- Add this configuration in the west config with the
west config build.board nrf5340dk/nrf5340/cpuappcommand. - Add the
-b nrf5340dk/nrf5340/cpuappoption to thewest buildcommand.
For the remainder of the Codelabs, we will assume that the board has been set using the command:
west config build.board nrf5340dk/nrf5340/cpuapp. We will not repeat the option: -b nrf5340dk/nrf5340/cpuapp for the build commands.
You can now run the west build command again. And you will get another
error! We are not done yet… The error is related to the fact that we have
included only Zephyr RTOS without importing other specific dependencies. You
first need to modify the west.yml file as follows:
manifest:
# lowest version of the manifest file schema that can parse this file’s data
version: 0.13
projects:
...
- name: zephyr
revision: v4.4.0
url: https://github.com/zephyrproject-rtos/zephyr
# path: deps/zephyr -> no longer needed since we are now using path-prefix in the import section
# explicitly add Zephyr-specific west extensions
# west-commands: scripts/west-commands.yml -> no longer needed
# because we import the west.yml file from zephyr where commands are defined
import:
path-prefix: deps
file: west.yml # not strictly needed since it is the value by default
name-allowlist:
- cmsis_6
- hal_nordic
This west.yml file allows to import the west.yml file from
Zephyr RTOS and to import the cmsis_6 and hal_nordic projects defined in
this west.yml file. If you run west update again, these projects will
be imported under deps/modules/hal:
├── deps/
│ ├── ...
│ └── modules/
│ └── hal/
│ └── cmsis_6
│ └── hal_nordic
For instance, the repository https://github.com/zephyrproject-rtos/cmsis_6
for the cmsis_6 project was cloned to the deps/modules/hal folder.
Details on how the west.yml file from Zephyr RTOS is imported are
given here
and here.
You can now try to build your application. It should compile and produce a binary file, as illustrated below:
145/145] Linking CXX executable zephyr\zephyr.elf
Memory region Used Size Region Size %age Used
FLASH: 19880 B 1 MB 1.90%
RAM: 4448 B 448 KB 0.97%
IDT_LIST: 0 GB 32 KB 0.00%
Generating files from .../build/zephyr/zephyr.elf for board: nrf5340dk
Board programmer
To flash your board, you may need to install a specific programmer.
For Nordic boards, you need to install the nRFUtil software as described
here.
After installing nrfutil, you may need to run the nrfutil install device
and nrfutil upgrade device command to be able to flash your nRF board.
If not already done, you may also need to install Segger J-Link.
You can now flash your target device with the west flash command. The application should
be copied to the target device and it should start.
Force a clean build
It is important to note that, in some situations, it is necessary to force a
clean build. This is the case, for instance, when some application
configuration parameters are changed or when files are added to the project.
To force a clean build, add the ‘–pristine’ parameter to the end of the
west command.
Make the blinky Application Work
The main() function of our program is essentially empty, and to make one of
the leds on the development kit blink, we need to add some code. In this
lecture, we will use a
library written in C++
to encapsulate the Zephyr RTOS kernel and drivers into C++ classes.
The required steps for adding the library and writing the application are:
-
Enable C++ support for your application:
blinky/prj.conf# C++ support CONFIG_CPP=y CONFIG_REQUIRES_FULL_LIBCPP=y CONFIG_STD_CPP20=y -
Add the library to your
west.ymlmanifest file:manifest-repo/west.ymlmanifest: ... projects: ... - name: zpp_lib url: https://github.com/AdvEmbSof/zpp_lib.git revision: v1.0 path: deps/zpp_lib -
Enable the
zpp_liblibrary in the application configuration file:blinky/prj.conf# use zpp_lib CONFIG_USE_ZPP_LIB=y -
Enable gpios in a dedicated configuration file:
blinky/prj_gpio.conf# enable gpio CONFIG_GPIO=y -
Add the following code in your
main.cppfile:blinky/src/main.cpp// stl #include <chrono> // zpp_lib #include "zpp_include/thread.hpp" #include "zpp_include/this_thread.hpp" #include "zpp_include/digital_out.hpp" void blink() { zpp_lib::DigitalOut led(zpp_lib::DigitalOut::PinName::LED0); using std::literals::chrono_literals::operator""ms; static constexpr std::chrono::milliseconds blinkInterval = 1000ms; while (true) { led = !led; zpp_lib::ThisThread::sleep_for(blinkInterval); } } int main(void) { zpp_lib::Thread thread; auto res = thread.start(blink); if (! res) { return -1; } res = thread.join(); if (! res) { return -1; } return 0; }
Thread join() is mandatory
It is important to note that, in the main scope, we create a thread that runs on the main stack. Once the program leaves this scope, everything allocated on the main stack is released. In this case, that includes the thread responsible for blinking the LED.
The call to thread.join() ensures that the main program waits until the
thread has finished before continuing to the next instruction (which would
otherwise exit the main scope). Without this call, the application would
crash because the thread’s resources would no longer be valid.
After applying these changes, you need to run west update and west build blinky --extra-conf prj_gpio.conf
again. Your application should compile and when you run west flash, the LED
should blink!
Adding Logging to Your Application
Debugging is an important and necessary part of the development process. On embedded systems, debugging capabilities are often more limited than on other systems. Parts of the debugging process will be covered in a future codelab. In this section we will show how to use logging statements and predefined macros for efficient and flexible debugging via printing.
Zephyr RTOS offers advanced logging capabilities which are fully documented here. Logging is implemented through macros that allow easy configuration of logging at both compile time and run time:
- Using Kconfig logging options, logs can be added or removed at compile time. This can reduce image size and speed up execution if certain logs are not needed. Logging can be configured both at the global application level as well as for each specific library/module that implements logging.
- Logs can also be filtered at runtime and this can be done for each logging backend and source.
- Logging is based on four severity levels, each corresponding to a dedicated
set of macros: error (
ERR), warning (WRN), info (INF) and debug (DBG). When adding a logging message, a specific severity level is chosen, e.g.LOG_ERRfor logging an error message. When compiling the application, the severity level to be applied can be chosen for the entire application or for each specific module/library, using configuration parameters such asCONFIG_LOG_DEFAULT_LEVEL). - In a following codelab, we will introduce the use of software quality tools such
as
cpplintorclang-tidy. Zephyr RTOS logging primitives generate many warnings and errors when analyzed withclang-tidy. To use these primitives without generating warnings,zpp_libhas defined relatedZPP_XXprimitives that you must use in your code.
Enabling Logging for the blinky Application
Use multiple configuration files rather than editing a single configuration file
The best way to improve reproducibility when working in teams is to use multiple
configuration files instead of editing one configuration file to modify the application configuration parameters.
Therefore, it is wise to distinguish the application configuration at build time by
adding the necessary files to the build command without modifying the configuration
files themselves. This can be done using the --extra-conf "conf_file" argument in the command line.
We do this to enable logging in the application. When logging is needed, the prj.conf file remains
unchanged and the application is built using --extra-conf prj_log.conf.
To facilitate the use of different build profiles, we recommend using justfile introduced at the end of this codelab.
To configure logging for the blinky application, we must:
-
Enable logging globally by adding the following configuration parameter in the
prj_log.confconfiguration file:blinky/prj_log.confCONFIG_LOG=y -
Build and flash your application using the
west build blinky --pristine --extra-conf prj_log.conf --extra_conf prj_gpio.conf. Open a serial terminal and connect it to your board. You can use any serial monitor for this purpose. The default configuration parameters are115200/8/1without parity. For example, if you useminicom, the command would be beminicom -b 115200 -D /dev/ttyACM1. The terminal should display the following message:console*** Booting Zephyr OS build v4.4.0 *** -
Enable logging for the Zephyr RTOS kernel at severity level
DBG:blinky/prj_log.confCONFIG_KERNEL_LOG_LEVEL_DBG=y -
After enabling logging for the kernel, the terminal should display messages tagged as
<dbg> os:console*** Booting Zephyr OS build v4.1.0 *** [00:00:00.428,955] <dbg> os: z_impl_k_mutex_lock: 0x200006d0 took mutex 0x20000020, count: 1, orig prio: 0 [00:00:00.428,955] <dbg> os: z_impl_k_mutex_unlock: mutex 0x20000020 lock_count: 1 [00:00:00.428,985] <dbg> os: z_impl_k_mutex_unlock: new owner of mutex 0x20000020: 0 (prio: -1000) [00:00:00.428,985] <dbg> os: k_sched_unlock: scheduler unlocked (0x200006d0:0)
As you can see, the message indicates the severity level, the module name and the function/method.
-
Enable logging for the
zpp_liblibrary at severity levelDBG:blinky/prj_log.confCONFIG_ZPP_RTOS_LOG_LEVEL_DBG=y CONFIG_ZPP_DRIVERS_LOG_LEVEL_DBG=y -
After enabling logging for the kernel, the terminal should display messages tagged as
<dbg> zpp_rtosand<dbg> zpp_drivers:console*** Booting Zephyr OS build v4.1.0 *** [00:00:00.426,391] <dbg> zpp_rtos: constructor: Thread constructed with stack size 1024 [00:00:00.426,422] <dbg> zpp_rtos: start: Creating thread with stack size 1024 and priority 11 [00:00:00.426,452] <dbg> zpp_rtos: start: Thread created [00:00:00.426,483] <dbg> zpp_rtos: _thunk: Thread _thunk called [00:00:00.426,513] <dbg> zpp_drivers: DigitalOut: Pin gpio@842500 initialized
Adding Logging to the Blinky Application
For debugging purposes, it is also useful to add application specific logging messages to the application code. This can be implemented in few steps:
-
Add a
Kconfigfile at the root of the application folder with the following content:blinky/Kconfigmenu "Zephyr" source "Kconfig.zephyr" endmenu module = APP module-str = APP source "subsys/logging/Kconfig.template.log_config"This is required for defining the logging symbols specific to the application. More details will be given on
Kconfigin a next codelab. If you open the “subsys/logging/Kconfig.template.log_config” file located in the Zephyr RTOS directory, you will see that it defines a number of symbols used for logging, all based on the module variable defined in the “blinky/Kconfig” file. -
Enable logging for the application at severity level
DBG:blinky/prj_log.confCONFIG_APP_LOG_LEVEL_DBG=y -
Add logging messages in the application code, such as:
blinky/src/main.cpp// zephyr #include " ... // LOG_MODULE_REGISTER needs to be placed after all #includes // outside any namespace ZPP_LOG_MODULE_REGISTER(blinky, CONFIG_APP_LOG_LEVEL); ... int main(void) { ZPP_LOG_DBG("Running on board %s", CONFIG_BOARD_TARGET); ZPP_LOG_DBG("Starting thread"); zpp_lib::Thread thread(zpp_lib::PreemptableThreadPriority::PriorityNormal, "Blinky"); auto res = thread.start(blink); if (! res) { ZPP_LOG_ERR("Could not start thread (%d)", static_cast<int>(res.error())); return -1; } res = thread.join(); if (! res) { ZPP_LOG_ERR("Could not terminate thread (%d)", static_cast<int>(res.error())); return -1; } return 0; } -
After applying these changes, the terminal should display messages tagged as
<dbg> main:console*** Booting Zephyr OS build v4.1.0 *** [00:00:00.433,105] <dbg> main: main: Running on board nrf5340dk/nrf5340/cpuapp [00:00:00.433,135] <dbg> main: main: Starting thread
ZPP_LOG_MODULE_REGISTER vs ZPP_LOG_MODULE DECLARE
The ZPP_LOG_MODULE_REGISTER() must be placed after all #include and outside any namespace.
If the module consists of multiple files, then ZPP_LOG_MODULE_REGISTER() should appear in exactly one file.
Each other file should use ZPP_LOG_MODULE_DECLARE() to declare its membership in the module.
The second parameter for both statements is optional and it is a compile time log level for the module.
Default log level (CONFIG_LOG_DEFAULT_LEVEL) is used if no custom log level is provided.
Debugging a Zephyr RTOS Application
You can also debug any Zephyr RTOS application using the debugger. The following steps are required for this purpose:
-
First, install the cortex-debug extension available here. Extensions in VSCode can be installed as documented here.
-
In the
.vscodefolder, add thelaunch.jsonfile with the following content
launch.json
{
"version": "0.2.0",
"configurations": [
// Adopted from https://github.com/Marus/cortex-debug/issues/969
{
"name": "Launch nrf5340dk/nrf5340/cpuapp",
"device": "nRF5340_xxAA_app",
"cwd": "${workspaceFolder}",
"executable": path to executable file, e.g. "build/zephyr/zephyr.elf",
"request": "launch",
"type": "cortex-debug",
"runToEntryPoint": "main",
"servertype": "jlink",
"gdbPath": path to arm-zephyr-eabi-gdb.exe, e.g. "...zephyr-sdk-1.0.1/arm-zephyr-eabi/bin/arm-zephyr-eabi-gdb.exe",
"rtos": path to RTOSPlugin_Zephyr library, e.g. on Windows "C:/Program Files/SEGGER/JLink_V910/GDBServer/RTOSPlugin_Zephyr.dll"
// or, alternatively until the installation of Segger tools, "rtos": "auto"
},
{
"name": "Attach nrf5340dk/nrf5340/cpuapp",
"device": "nRF5340_xxAA_app",
"cwd": "${workspaceFolder}",
"executable": path to executable file e.g. "build/zephyr/zephyr.elf",
"request": "attach",
"type": "cortex-debug",
"runToEntryPoint": "main",
"servertype": "jlink",
"gdbPath": path to arm-zephyr-eabi-gdb.exe, e.g. "...zephyr-sdk-1.0.1/arm-zephyr-eabi/bin/arm-zephyr-eabi-gdb.exe",
"rtos": path to RTOSPlugin_Zephyr library, e.g. on Linux "/opt/SEGGER/JLink_V910/GDBServer/RTOSPlugin_Zephyr.so"
// or, alternatively until the installation of Segger tools, "rtos": "auto"
},
]
}
-
Enable the necessary debug configuration before starting the debugger. This is done by modifying the
prj.conffile as follows:blinky/prj_debug.confCONFIG_DEBUG=y CONFIG_DEBUG_OPTIMIZATIONS=y -
Start the debugger by selecting Run > Start Debugging. The debugger should then stop at the start of the
main()function. You can then continue execution and step through the code as you would with any other debugger.
Removing breakpoints
The debugger will halt at the start of the main() function by default. However, it may get stuck during startup. In this case, removing any breakpoints before starting may help it to start successfully.
Debug-time Runtime Errors using __ASSERT()
__ASSERT() is a debug-time runtime assertion mechanism that evaluates a condition at runtime. If the condition is false,
it prints a diagnostic message including the file name, line number, and a formatted message, then halts the system.
Note that __ASSERT() is only active when CONFIG_ASSERT=y is set in the prj_debug.conf file. In production builds,
the assertion mechanism is disabled and __ASSERT() compiles down to nothing and adds zero overhead. Thus, it is the
right tool for catching programming errors during development, such as verifying preconditions at a function entry point
or catching invalid argument values that should never occur in correct code, such as:
int z_impl_k_mutex_unlock(struct k_mutex *mutex)
{
...
__ASSERT(!arch_is_in_isr(), "mutexes cannot be used inside ISRs");
...
}
Zephyr RTOS also provides the __ASSERT_EVAL() macro that allows to specify different actions depending on whether
assertions are enabled or not. This is useful for preventing the compiler from generating errors or warnings about
unused variables when assertions are disabled. For example:
int x = foo();
__ASSERT(x != 0, "foo() returned zero!");
Disabling assertions may cause this code to generate a warning because x is assigned a value but never used. This
issue can be resolved by:
__ASSERT_EVAL ((void) foo(),
int x = foo(),
x != 0,
"foo() returned zero!");
The first parameter of ASSERT_EVAL() is evaluated when assertions are disabled and the second when assertions are enabled.
Because error handling with assertions disappear in production builds, __ASSERT() and ASSERT_EVAL() should never be
used to handle errors that can legitimately occur at runtime. They are strictly development and debugging tools.
Use ZPP_ASSERT_XXX macros
For the same reason that you must replace LOG_XX macros withZPP_LOG_XX macros, you must use
ZPP_ASSERT_XX macros in your code. This will prevent the generation of warnings and errors when the code
is analyzed with clang-tidy.
Compile-time Runtime Errors using BUILD_ASSERT()
BUILD_ASSERT() is a compile-time assertion mechanism, that evaluates a
constant expression at compile time and produces a compile error with a
descriptive message if the expression is false. Unlike __ASSERT() it has
absolutely no runtime cost and is never compiled away — it is a permanent
compile-time contract. It is the right tool for enforcing structural constraints
that can be determined at compile time, such as verifying sizes, alignments, or
configuration consistency, as in:
BUILD_ASSERT(sizeof(MyStruct) <= BLOCK_SIZE,
"MyStruct is too large for the memory slab block");
BUILD_ASSERT() should be preferred over __ASSERT() whenever the condition
can be evaluated at compile time, since it catches errors earlier and costs
nothing at runtime. However, with BUILD_ASSERT() the provided error message must
be a static string, without formatting or extra arguments.
Running blinky in QEMU
When it is not possible to run and test an application on a physical board, it is useful to be able to do so on an emulated device. This is where QEMU comes in. For many use cases, QEMU allows you to validate your code at any time by running tests, and it is runnable in CI/CD. With Zephyr RTOS, you can build your application for multiple boards, including simulated boards in QEMU. Knowing how to do this is useful.
First Step is to Install QEMU on your Computer
Zephyr RTOS’s documentation explains how to install QEMU on multiple platforms. here.
Adapt the blinky Application
- Create the directory
blinky/boards. This directory should contain board-specific configuration files, in this case:- qemu_x86.overlay — adds or overrides Devicetree (DTS) definitions for that board.
- qemu_x86.conf — adds or overrides Kconfig settings for that board.
When you build your application with west for a given board, Zephyr RTOS will
automatically apply the corresponding files from the boards directory located
in the application folder (if they exist).
-
Create the
boards/qemu_x86.conffile with the following content:blinky/boards/qemu_x86.confCONFIG_GPIO_EMUL=y # emulate the GPIO driver -
Create the
boards/qemu_x86.overlayfile with the following content:
blinky/boards/qemu_x86.overlay
#include <zephyr/dt-bindings/gpio/gpio.h>
/ {
gpio0: gpio_emul_0 {
status = "okay";
compatible = "zephyr,gpio-emul";
rising-edge;
falling-edge;
high-level;
low-level;
gpio-controller;
ngpios=<2>;
#gpio-cells = <2>;
};
i2c_emul: i2c_emul@1000 {
compatible = "zephyr,i2c-emul-controller";
reg = <0x1000 1>;
#address-cells = <1>;
#size-cells = <0>;
clock-frequency = <100000>;
bme280_emul: bme280@77 {
compatible = "bosch,bme280", "bosch,bme280-emul";
reg = <0x77>;
label = "BME280";
};
};
resources {
compatible = "test-gpio-basic-api";
out-gpios = <&gpio0 0 0>; /* Pin 0 */
in-gpios = <&gpio0 1 0>; /* Pin 1 */
};
leds {
compatible = "gpio-leds";
led0: led_0 {
gpios = <&gpio0 0 GPIO_ACTIVE_HIGH>;
label = "Green LED";
};
led1: led_1 {
gpios = <&gpio0 1 GPIO_ACTIVE_HIGH>;
label = "Yellow LED";
};
};
buttons {
compatible = "gpio-keys";
button0: button_0 {
gpios = <&gpio0 1 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>;
label = "Push button 1";
};
button1: button_1 {
gpios = <&gpio0 1 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>;
label = "Push button 2";
};
button2: button_2 {
gpios = <&gpio0 1 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>;
label = "Push button 3";
};
button3: button_3 {
gpios = <&gpio0 1 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>;
label = "Push button 4";
};
};
aliases {
led0 = &led0;
led1 = &led1;
sw0 = &button0;
sw1 = &button1;
sw2 = &button2;
sw3 = &button3;
};
chosen {
zephyr,display = &dummy_dc;
};
dummy_dc: dummy_dc {
compatible = "zephyr,dummy-dc";
height = <240>;
width = <320>;
};
};
- Then, run the command
west build -b qemu_x86 blinky --pristine --extra-conf prj_gpio.conf. The build should succeed. - To run it on QEMU, run the command
west build -t run. You should see your application’s logs in the terminal.
Recap for the Configuration Files
At this stage, your configuration files should include the definitions in the following files:
prj.conf
# C++ support
CONFIG_CPP=y
CONFIG_REQUIRES_FULL_LIBCPP=y
CONFIG_STD_CPP20=y
# we use zpp_lib
CONFIG_USE_ZPP_LIB=y
# fully disable logging
CONFIG_LOG=n
CONFIG_BOOT_BANNER=n
# increase the stack size of the main thread
CONFIG_MAIN_STACK_SIZE=2048
# set the priority of the main thread to PreemptableThreadPriority::PriorityNormal
# PreemptableThreadPriority::PriorityNormal is defined as CONFIG_NUM_PREEMPT_PRIORITIES - 6
CONFIG_MAIN_THREAD_PRIORITY=9
# compiler warnings must be treated as errors
CONFIG_COMPILER_WARNINGS_AS_ERRORS=y
prj_log.conf
# logging
CONFIG_LOG=y
CONFIG_LOG_MODE_IMMEDIATE=n
CONFIG_ZPP_RTOS_LOG_LEVEL_DBG=y
CONFIG_ZPP_DRIVERS_LOG_LEVEL_DBG=y
CONFIG_APP_LOG_LEVEL_DBG=y
CONFIG_KERNEL_LOG_LEVEL_DBG=n
prj_debug.conf
# disable ASSERT
CONFIG_ASSERT=y
# enable debugging information
CONFIG_DEBUG_OPTIMIZATIONS=y
prj_gpio.conf
# Enable GPIO
CONFIG_GPIO=y
prj_gpio_emul.conf
# Enable the emulation framework
CONFIG_EMUL=y
CONFIG_GPIO=y
CONFIG_GPIO_EMUL=y
Note that some configuration files like prj_gpio_emul.conf have not been introduced before. prj_gpio_emul.conf is
required when running the blinky application on qemu_x86, for which gpios are emulated. The template
application also contains other configuration files that will be used for other specific applications.
Using Scripts, Configuration Files and justfile to Make Builds Simpler
As you have started to realize, building a Zephyr RTOS application requires defining application configuration and board definitions. Since we will be building several applications, the following tools are made available in addition to the Zephyr Development Environment to ease the reproducibility of the builds within a team and within the class:
-
Python scripts are made available with the
zpp_liblibrary. These scripts simplify building, testing and quality checking of your various applications. -
The application configuration and board definitions are made available with the
zpp_liblibrary. While a Zephyr RTOS application usually defines its own application configuration and board definitions, using pre-defined definitions for all applications and configuring applications at build time eases development, particularly its reproducibility. Using multiple configuration files has the advantage of allowing an application to be built with different configurations without editing the configuration files. For example, the same application can be built with or without logging simply by modifying the build command line. This makes the build configuration explicit and improves reproducibility when working in teams. -
Some Python scripts use YAML configuration files that define all applications to be built and tested. This allows you to build and test all applications at once on your machine, as well as on the CI/CD of your GitHub repository. This configuration file also documents exactly how the applications should be built and tested. The configuration file for building the
blinkyapplication is provided below:ci/applications_for_build.yamlThis file specifies that the application should be built for three different boards with two different application parameter specifications. It also documents the location of the configuration files.applications: - app: blinky boards: - board: nrf5340dk/nrf5340/cpuapp - board: qemu_x86 - board: native_sim specs: - gpio - log+debug+gpio config_dir: ../deps/zpp_lib/configs -
The Python scripts can be used directly, but a justfile is also provided to simplify their use. Using the justfile also allows to easily and fully document which configurations should be supported when delivering a project. The installation of justfile is documented here. The
justfilegiven below can be copied to the root of your workspace. Then, you can typejust --listto list available recipes that are self-documented. For example, typingjust build blinky debug+log+gpio yeswill perform a clean build of theblinkyapplication with logging and debugging configurations. The command that will be executed iswest build blinky --shield adafruit_2_8_tft_touch_v2 --pristine --extra-conf prj_debug.conf --extra-conf prj_log.conf --extra-conf prj_gpio.conf. Note that thisjustfileuses the Python scripts that are defined in thezpp_liblibrary.
justfile
prefix := "/"
working_dir := justfile_directory()
zpp_lib_dir := "deps/zpp_lib"
default_board := "nrf5340dk/nrf5340/cpuapp"
default_shield := "adafruit_2_8_tft_touch_v2"
# CREATE A NEW APPLICATION FROM github template
create-app app:
python {{zpp_lib_dir}}/scripts/create_app.py {{app}}
# BUILD ALLS
# Build all applications as described in the configuration file
build-all yaml_file="ci/applications_for_build.yaml":
python {{zpp_lib_dir}}/scripts/build_from_yaml.py --yaml {{quote(yaml_file)}}
# BUILDS FOR THE APPLICATIONS RUNNING ON CONFIGURED REAL HARDWARE
# Build the specified application with all specs described in the configuration file, for the default board
build-yaml app yaml_file="ci/applications_for_build.yaml":
python {{zpp_lib_dir}}/scripts/build_from_yaml.py --yaml {{quote(yaml_file)}} --app {{app}} --board {{default_board}}
# Build the specified application with the specified configs, for the default board
build app configs pristine="yes" app_config="":
python {{zpp_lib_dir}}/scripts/build.py --app {{app}} --configs {{quote(configs)}} --board {{default_board}} \
--shield {{default_shield}} {{ if pristine == "yes" { "--pristine" } else { "" } }} \
{{ if app_config != "" { "--app-config " + quote(app_config) } else { "" } }}
# QEMU BUILDS
# Build the specified application with all configs described in the configuration file, for qemu_x86
build-qemu-yaml app yaml_file="ci/applications_for_build.yaml":
python {{zpp_lib_dir}}/scripts/build_from_yaml.py --yaml {{quote(yaml_file)}} --app {{app}} --board "qemu_x86"
# Build the specified application with the specified configs, for qemu_x86
build-qemu app configs pristine="yes":
python {{zpp_lib_dir}}/scripts/build.py --app {{app}} --configs {{quote(configs)}} --board "qemu_x86" {{ if pristine == "yes" { "--pristine" } else { "" } }}
# TESTS ON HARDWARE
# Launch all tests as described in the configuration file, for the default board
test-all map_file yaml_file="ci/applications_for_test.yaml":
python {{zpp_lib_dir}}/scripts/twister_from_yaml.py --yaml {{quote(yaml_file)}} --board {{default_board}} --map-file {{map_file}}
# Launch tests at the specified root, using hardware specified in the map file
test test_suite_root map_file tags="":
python {{zpp_lib_dir}}/scripts/twister.py --root {{test_suite_root}} --tags {{quote(tags)}} --board {{default_board}} --map-file {{map_file}}
# TESTS ON QEMU
# Launch all tests as described in the configuration file, using qemu_x86
test-all-qemu yaml_file="ci/applications_for_test.yaml":
python {{zpp_lib_dir}}/scripts/twister_from_yaml.py --yaml {{quote(yaml_file)}} --board "qemu_x86"
# Launch tests at the specified root, using qemu_x86
test-qemu test_suite_root tags="":
python {{zpp_lib_dir}}/scripts/twister.py --root {{test_suite_root}} --tags {{quote(tags)}} --board "qemu_x86"
# CLANG-TIDY
# Check only the main.cpp file of the application
clang-tidy app configs:
# Step 1 — build to get compile_commands.json (build with all conf files to get the most complete database)
python {{zpp_lib_dir}}/scripts/build.py --app {{app}} --configs {{quote(configs)}} --board "native_sim" --pristine
# Step 2 — filter the compile_commands.json file for compatibility with clang-tidy
mkdir -p build_clang
python3 {{zpp_lib_dir}}/scripts/filter_compile_commands.py build/compile_commands.json build_clang/compile_commands.json
# Step 3 — run clang-tidy against the filtered database
clang-tidy-22 -p build_clang {{working_dir}}/{{app}}/src/main.cpp --extra-arg=-v
# Check all application files
run-clang-tidy app configs app_config="":
python {{zpp_lib_dir}}/scripts/run_clang_tidy.py --app {{app}} --configs {{quote(configs)}} --wd {{working_dir}} \
{{ if app_config != "" { "--app-config " + quote(app_config) } else { "" } }}
Wrap-Up
By the end of this codelab, you should have completed the following steps:
- Your development environment (Python venv, VS Code, Zephyr SDK) should be fully set up.
- You understand the basic principles behind west, manifests, modules, and T3 workspaces.
- You understand the Zephyr RTOS logging functionality and can enable and configure Zephyr RTOS logging for the kernel, libraries, and app. You can also add logging to your own application.
- You know how to debug a Zephyr RTOS application, and you can step through
blinkyin VS Code using Cortex-Debug and the proper debuggingKconfigoptions. - Your
blinkyapplication should be functional, and you should be able to build, flash, and debug it. You should be able to build the application using different configuration parameters without modifying the configuration files. - You can build and run Blinky for the QEMU board platform with board-specific overrides.
- You understand how to use
__ASSERT()andBUILD_ASSERT().
If you are a new comer to C++ or if you wish to revise some C++ basics, the next step is to start reading the C++ programming exercises. It is essential that you understand the basic C++ concepts and the main differences between C++ and C programming languages.
Deliverables
Deliverables/Requirements (Project Phase A)
The deliverables/requirements for project Phase A include the following:
- The
blinkyapplication is functional and available on your Github repository -
The following commands runs successfully:
just build blinky gpio: it builds the application in production mode. After flashing the application, led 1 blinks.just build blinky gpio+debug+log: it builds the application in debug and logging mode. After flashing the application, led 1 blinks and messsages are printed in the console.just build-all: it builds theblinkyapplication in the two different configurations.- If you do not use justfile, then the corresponding commands must run successfully.
-
Building and flashing any of the builds runs successfully on both the nrf5340dk/nrf5340/cpuapp and qemu_x86 targets.