This blog post shows how to setup a Zephyr project that you can use for several boards.
Changelog
- 2024-03-03 added an ESP32-C3 board
Why multiple boards in one project?
- You start with a development board (such as STM Nucleo or Disco) while you
wait for the actual hardware prototype. - You want to run hardware-independent unit tests, either on your desktop or on
a CI/CD server like Jenkins. - You have to develop for many similar devices that only have slight
differences, and you don’t want to have many almost-identical source trees.
(Ab)use of Makefiles
The following is orchestrated mostly by a Makefile.
Even when Zephyr itself uses CMake and Ninja, Makefiles are a nicer way to
bundle lots of shell snippets into one Makefile. You can view this Makefile also
as a collection of knowledge, or as a way to have things replicable.
This blog post is based on …
This post depends and improves on Zepyhr: reproducible project setup and uses it’s Makefile.zephyr_init.
Board related
Get list of defined board
If we just enter “make
” to compile our sources, we instead see a list of of boards.
If we have one set of source files but several target boards, we need a way to
configure for a specific board. So if there is no “build/
” directory, we are
asked to first configure for a specific board:
:~/src/multi-board-zephyr$ make
-----------------------------------------------------------------------------
You must first select with with board you want to work:
native configure for native (used for unit-tests)
nucleo compile for STM32 Nucleo
local configure for locally defined board
-----------------------------------------------------------------------------
Configure and compile for one of the boards
We select one of the boards, e.g. the provided STM32 Nucleo one:
|
|
There are some special things here at work:
- In line 3, we order “
west
” to use a pristine environment whenever
the configuration changes. So you can do “make local
” and then “make nucleo
” and the “build/
” directory will completely switch. There’s no need
for you to manually remove it with “rm -rf build
”.
to. - In line [BROKEN LINK: nucleo/f303re], we actually select the wanted board. This one is
provided by Zephyr itself, and you can find it in
https://github.com/zephyrproject-rtos/zephyr/tree/main/boards/arm/nucleo/f303re. - Line 5 tells Zephyr’s CMake to use Ninja, which compiles as if we would
ask CMake to generate makefiles. - The two dashes in line 6 tell “
west
” to pass over all the future
command-line options as-is to CMake. - Line 7 tells CMake to generate a compilation database. Use this with an
LSP daemon like clangd or other tools that depend on it. Many editors like
Emacs, Visual Studio, etc., offer special services if LSP is present. See more on
LSP in the post Zepyhr: fixing LSP issues - Line 8 tells the build system to configure itself according to the
specified configuration file. They are in a Linux-style KConfig / “.config”
syntax. Note that only board-specific configurations should be placed there.
Anything that should be used project-wide has a better place in “prj.conf”.
If the configuration step succeed, this will also automatically compile your code.
For subsequent compilations, you just enter “make
” alone. Another “make nucleo
” would also re-configure the “build/
” directory. That would take more
time.
How this is implemented
The differentiation between “make
” doing just a re-compile or asking you to
select a board is done like this:
- in line [[(build.ninja))] it checks if the build environment inside the
“build/
” directory has been created. If not, it calls the Make function
“show_boards”. More on this function in a moment. - but if it exists, we just call in line 5 “
ninja
” with our build
directory as working dir
The make function is simple enought: basically only some decoration around “make help_boards
”:
define show_boards
@echo ""
@echo "-----------------------------------------------------------------------------"
@echo ""
@echo "You must first select with with board you want to work:"
@$(MAKE) --no-print-directory help_boards
@echo ""
@echo "-----------------------------------------------------------------------------"
@echo ""
endef
The reason I made this a function is so that it is easy to call from several
places. In this Makefile, not only “make all
” calls it eventually, but also
maybe “make menuconfig
” or “make xconfig
”.
Finally we have a multitude of “help_boards:” targets like this:
help help_boards::
@echo "nucleo configure and compile for STM32 Nucleo"
Configure and compile for simulated hardware
Zephyr includes a board called native_sim. Basically when you select this
“board”, your sources are compiled for your development compiter (in my case:
Linux). So they aren’t compiled for ARM or RISV-V, but for x86. The native
simulator even allows you to similar some hardware, e.g. an AT24 EEPROM.
However, what is most useful is that you can define unit-tests and run these
unit-tests than on your develpment compiter — or on a CI/CD server, like
Jenkins.
Here is how you configure Zephyr for this:
As before, any native-sim-related configuration should be put into"native_sim.conf
", (line 9).
Now, when we configure and compile, we now get a binary that we can run under
Linux (or WSL, if you’re on Windows):
~/src/multi-board-zephyr$ make native
west build \
--pristine \
-b native_sim \
-o "build.ninja" \
-- \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
-DOVERLAY_CONFIG="native_sim.conf"
-- west build: making build dir /home/holger/src/multi-board-zephyr/build pristine
-- west build: generating a build system
Loading Zephyr default modules (Zephyr base).
-- Application: /home/holger/src/multi-board-zephyr
# ... many lines omitted ...
[93/93] cd /home/holger/src/multi-board-zephyr/bui...ger/src/multi-board-zephyr/build/zephyr/zephyr.ex
It’s even named “*.exe
” :-)
$ file build/zephyr/zephyr.exe
build/zephyr/zephyr.exe: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=d4b863c9b8d6e9e2265fdef874ec0b9df70efdc9, for GNU/Linux 3.2.0, with debug_info, not stripped
And you can call it normally:
~/src/multi-board-zephyr$ build/zephyr/zephyr.exe
Running TESTSUITE tests
===================================================================
START - demo_test
PASS - demo_test in 0.000 seconds
===================================================================
TESTSUITE tests succeeded
------ TESTSUITE SUMMARY START ------
SUITE PASS - 100.00% [tests]: pass = 1, fail = 0, skip = 0, total = 1 duration = 0.000 seconds
- PASS - [tests.demo_test] duration = 0.000 seconds
------ TESTSUITE SUMMARY END ------
===================================================================
PROJECT EXECUTION SUCCESSFUL
I will create another blog soon on how to integrate this into Jenkings: by
converting the output into the TAP format.
Define a local board
So far, we used boards already defined by the Zephyr source code. But perhaps
you want to use Zephyr on one of your own boards, where you don’t plan to
publish it upstream? That’s entirely possible, and the board called “local” in
this project is exactly that: a board defined for Zephyr but out-of-tree. The
Makefile snippet for it sounds familiar …
… but there are some differences:
- Line 9 gives the full path to the default configuration of the
board. - Line 10 specifies our project (not Zephyr) as the board root, so
Zephyr won’t look into “=zephyr/boards/” but instead into “=boards/” when
looking for boards.
Now we need to have such a “boards/arm/local/
” directory and populate it with some files:
File | Purpose |
---|---|
Kconfig.board | this is where you introduce board-specific Kconfig options |
Kconfig.defconfig | without setting CONFIG_BOARD to the name of your board, Zephyr wouldn’t find the following files |
board.cmake | can contain CMake definitions, usually used for OpenOCD or JLink settings |
local.dts | the Device Tree for your board |
local_defconfig | the default configuaration for your board, only put things there that isn’t in “prj.conf ” |
support/openocd.cfg | if you use OpenOCD, this contains configuration for it |
Compiling some sources only for some boards
This can easily be done via “CMakeLists.txt
”:
- Any sources that must compile for every board are specified like in line
[BROKEN LINK: src/main]. Note that the hanging indent is there as a hint that you can
specify multiple source files in one “target\/source
” declaration. - According to line [BROKEN LINK: src\/local], the file “
board\/local.c
” will only be
compiled if your current board is the board named “local”. - And you guessed it; line [BROKEN LINK: src\/native] ensures that this source file is only
considered when compiling for the “native\/sim” board. Here, I’d put the
device-independent unit tests, for example.
You can use the CONFIG_ … variables also direcly in your C sources:
#ifdef CONFIG_BOARD_LOCAL
LOG_INF("Running on local")
endif
Configuration
You also learned about the various “*.conf
” files like
- board-specific native_sim.conf
- board-specific nucleo_f303re.conf
- board-specific ones like boards/arm/local/Kconfig.board,
boards/arm/local/Kconfig_defconfig and boards/arm/local/local_defconfig - the project-wide prj.conf file
But how to find out which “CONFIG_*
” settings you can use?
Use either
- “
make menuconfig
” or - “
make xconfig
”
When you make changes and save, you can then just run “make
” to compile your
board with these settings. However, to make these changes permanent (and
reproducible), you need to update one of the configuration files listed above.
Get help from make
I already showed “make help\/boards
”. The same method (multiple pseudo
makefile targets emitting helpful text) is available to get an idea of what the
Makefile can do for you:
~/src/multi-board-zephyr$ make help
init do all of these steps:
debs only install debian packages
venv create and check Python3 virtual environment
west install and configure the 'west' tool
zephyr clone Zephyr
modules install Zeyphr modules (e.g. ST and STM32 HAL, CMSIS ...)
module_cmsis update only CMSIS
module_espressif update only ESPRESSIF (ESP-32)
module_st update only ST HAL
module_stm32 update only STM32 HAL
all compile for current board
menuconfig run menuconfig for current board
xconfig run xconfig for current board
esp32c3 configure and compile for ESP32-C3 DevKit M
local configure and compile for locally defined board
native configure and compile for native (used for unit-tests)
nucleo configure and compile for STM32 Nucleo