Zephyr’s build system 101

3 mins read

When I first ventured into Zephyr Project, I was met with a build system that felt both familiar and foreign. Having worked with CMake in other projects, I assumed the transition would be straightforward. However, Zephyr introduces its own layer with west, Kconfig, and DeviceTree, adding some a bunch of complexity to the build process.
This article aims to demystify Zephyr’s build system to newcomers by breaking down its components and illustrating how they interact.

The role of West

At its core is Zephyr’s west meta-tool (why “west”? because Zephyr is the name of west wind, before being an RTOS), commonly seen as a swiss knife to work with Zephyr. It’s designed to manage multiple repositories and streamline the build process. It also handles tasks like fetching dependencies, building applications, and flashing firmware to devices.

For the sake of simplicity, we’ll assume for the rest of this article that you have correctly installed dependencies and have a Zephyr workspace already set up. If it’s not the case, the official documentation can be found here.

Anatomy of a Zephyr application

The simplest Zephyr application includes the following files:

my_app/
    ├── src/
    │ └── main.c 
    ├── prj.conf
    └── CMakeLists.txt
  • main.c: Contains the main() function. It’s the entry point of your application.
  • prj.conf: Specifies configuration options using Kconfig syntax.
  • CMakeLists.txt: Defines how to build the application using CMake.

Example CMakeLists.txt:

cmake cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(my_app)

target_sources(app PRIVATE src/main.c)

This setup tells CMake to include Zephyr’s build system and compile main.c as part of the application. In particular, we can make two observations on this minimal CMakeLists file:

  • There is only one source file to compile (target_sources).
  • ZEPHYR_BASE needs to be defined in the environment variables.

Building the application

To build the application for a specific board:

west build -b <board_name> path/to/my_app

This command performs several steps:

  1. Configuration: CMake processes CMakeLists.txt, integrating Zephyr’s build system.
  2. Kconfig processing: Reads prj.conf and other Kconfig files to generate a .config file.
  3. DeviceTree processing: Parses the board’s DeviceTree files to generate hardware definitions.
  4. Compilation: Uses Ninja (default) or Make to compile the application and Zephyr kernel into a firmware image.

The output is placed in a build/ directory that contains artifacts like zephyr.elf, zephyr.hex, and zephyr.bin.

Understanding prj.conf and Kconfig

The prj.conf file allows you to enable or configure various features of Zephyr. For example:

CONFIG_GPIO=y
CONFIG_LOG=y
CONFIG_MAIN_STACK_SIZE=1024

These options are processed by Kconfig to generate a .config file, which in turn influences the build process and the behavior of the application.

You can also interactively configure available options:

west build -t menuconfig

This opens a terminal-based interface to modify configuration options, similar to make menuconfig in the Linux kernel.

DeviceTree overlays

Zephyr uses DeviceTree to describe hardware components (one of its many similarities with the Linux kernel.) To customize hardware configurations, you can create board-specific overlay files inside boards/.

my_app/
├── boards/
│   ├── rpi_pico.overlay
│   └── arduino_nano_33_ble.overlay

The concept of device tree overlays is adding custom layers to describe (add, remove, modify) peripherals of the used microcontroller.
For example, an overlay file that enables an onboard LED (led0):

dts &led0 {
    status = "okay";
};

This overlay modifies the board’s DeviceTree to enable led0. During the build process, Zephyr merges this overlay with the base DeviceTree to generate hardware definitions.

Common build options

  • Pristine Builds: To ensure a clean build environment (to force the rebuild for another board for example):
west build -p always -b <board_name> path/to/my_app
  • Specifying Build Directory: To build in a specific directory:
west build -b <board_name> -d build_dir path/to/my_app