zephyr-estructura-proyecto-compilacion

Structure of a Zephyr Project and Build System

  • 5 min

An out-of-tree application is a project that lives separately from the Zephyr source code and links to it during compilation.

This separation avoids modifying the examples or the operating system sources. This allows you to update Zephyr without losing changes or mixing your application code with the base project’s code.

Today we’re going to see how to create that structure from scratch and understand what each file does.

Anatomy of a Zephyr Project

A minimal Zephyr project is not just a .c file. We need to tell the system what hardware we are going to use, which OS libraries we need, and how to compile everything.

The minimum viable folder structure looks like this:

my-project/ ├── CMakeLists.txt <— The entry point of the build system ├── prj.conf <— Kernel Configuration (Kconfig) └── src/ └── main.c <— Your C source code

Let’s break down each one, because understanding this is the foundation of everything we will do later.

CMakeLists.txt

If you come from Arduino, this might seem alien. Zephyr uses CMake as its project generation system.

The CMakeLists.txt file is the “glue”. Its main job is to find where Zephyr is installed on your hard drive and tell it: “Hey, compile this project using your tools”.

A typical and minimal content is this:

cmake_minimum_required(VERSION 3.20.0)

# Find the Zephyr package. This loads all the OS machinery.
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})

# Define our project
project(my_project)

# Add our source files to the build
target_sources(app PRIVATE src/main.c)
Copied!

The line find_package(Zephyr ...) connects the project with Zephyr. It imports the board definitions, drivers, and compilation rules.

prj.conf (Kconfig)

This is arguably the most important and characteristic file in Zephyr.

In other systems, to enable a feature (like Bluetooth or USB), you usually have to search for a giant header file .h and uncomment lines, or use a graphical tool that generates code.

Zephyr uses Kconfig (the same system as the Linux Kernel). The prj.conf file is a list of configuration symbols that enable or disable parts of the Operating System.

For example, if you want to use printf and handle GPIO pins, your prj.conf will look like this:

CONFIG_GPIO=y
CONFIG_PRINTK=y
Copied!

Key concept: In Zephyr, only what you enable here gets compiled. If you don’t put CONFIG_BT=y, the Bluetooth stack won’t even be included in the final binary, saving memory. It’s an extremely modular system.

src/main.c

Your logic lives here. It’s a standard C file. The only difference from conventional C is that the main() function is the entry point for an operating system thread, not the bare-metal startup of the processor (Zephyr has already handled that before calling you).

Our Workspace

To keep things organized, I recommend the following folder topology, known as T2 or Freestanding Application within the workspace:

We go back to the zephyrproject folder where we ran west init. Next to the zephyr directory, we create another folder called app-test or my-projects.

zephyrproject/ ├── .west/ ├── bootloader/ ├── modules/ ├── tools/ ├── zephyr/ <— The OS (Do not touch!) └── my-projects/ └── hello-world/ <— We’ll work here ├── CMakeLists.txt ├── prj.conf └── src/ └── main.c

The Build Cycle with west

Now that we have the files, how does this get turned into a .hex or .bin binary for our board?

West orchestrates the process through the following flow:

West reads your CMakeLists.txt and the arguments you pass to it (like the board).

It calls CMake, which generates the build files (Makefiles or Ninja files) in a folder called build.

It calls Ninja (a super-fast build system) to compile the C code and link it.

It generates the final binaries.

Build Command

From the terminal, with the Python virtual environment activated, we enter my-projects/hello-world and run:

west build -p always -b <board_name> .
Copied!

Let’s break down this command:

  • build: The action to perform.
  • -p always: Means “pristine”. It tells West to delete the previous build folder and compile from scratch. It is useful when changing boards or to discard a problematic cache; for normal changes, CMake reconfigures what is necessary and incremental compilation is faster.
  • -b <board_name>: Specifies the board target. For example, nrf52840dk/nrf52840; check west boards to see available targets in your version.
  • .: Indicates that the project is in the current directory.

The build/ Folder

When you run the command, a build/ folder will appear inside your project.

Inside build/zephyr/ you will find:

  • zephyr.elf: The binary with debug symbols.
  • zephyr.hex or zephyr.bin: The file ready for flashing.
  • zephyr.dts: The final compiled device tree (very useful for hardware debugging).

Do not upload the build/ folder to Git. It contains temporary files specific to your machine. Add build/ to the .gitignore file.

With the structure prepared, we can now write code and make our first real program.