For Applications & Industries
Free Online Training Events
Free Technical Resources
Lots of people have reported problems when trying to build and run their first Zephyr application - it can feel as if there are multiple, conflicting ways to install and setup a suitable environment and the build process itself can also feel very complex. For this article, we have selected a common development platform (Windows 11) and a popular, low-cost development board - the Raspberry Pi Pico2.
Zephyr supports over 1000 boards from multiple vendors and using multiple CPU architectures. Installing the tools and libraries to support all of these can take a long time and require a lot of disk space! Here, we are going to focus on only installing the tools for Arm-based MCUs and the libraries for the Pico (adding support for additional Arm-based boards is trivial).
The Zephyr project includes its own python-based command line tool named west that invokes the other tools required to configure, build, optimize, program and debug a Zephyr application. Two notable tools that are used by west are cmake and ninja, as shown below:

Additionally, the Device Tree Compiler (dts) and the arm-zephyr-eabi cross-compiler toolchain must be installed.
The complete toolchain has hundreds of dependencies - installing it manually is not a sensible option!
There are three ways to manage the installation:
Install Zephyr environment natively, following the commands in the Zephyr Getting Started Guide. This gives you maximum flexibility but the instructions were originally written for a Linux environment - you might run into problems with a Windows 11 environment (especially if you have previously installed Python development tools that are not compatible with west).
Use a pre-built OCI Container with Docker. This provides a pre-configured and isolated environment to run the Zephyr tools.
Use a vendor-supplied IDE or a Visual Studio Code extension such as Workbench for Zephyr. This can be a quick way to get started, provided the exact board that you want to use is supported - it might not be easy (or even possible) to add support for a different board yourself!
We chose option 2, using a container that supported the Arm toolchain, available from the Embedded Containers repository.
Install Docker Desktop for Windows from the Docker website or the Microsoft Store. Make sure that the docker engine is started and that docker is in your path for PowerShell.
Open a PowerShell terminal and fetch a docker image that provides the Arm toolchain for Zephyr: arm-1.0.1SDK (note that 1.0.1 was the latest version of the SDK when we wrote this).
> docker pull ghcr.io/embeddedcontainers/zephyr:arm-1.0.1SDKAlthough the Zephyr build tools will run in a docker container, the Zephyr workspace (containing the Zephyr library and application files) will be part of the Windows 11 filesystem. This makes it easy to edit and debug files using familiar tools such as Visual Studio Code.
Create a directory in a suitable location to hold the Zephyr workspace and move to that directory. e.g.
> mkdir C:\training\zephyr_workspace
> cd C:\training\zephyr_workspaceCreate a docker container using the arm-1.0.1SDK image and linked to the shared Windows 11 zephyr_workspace directory.
> docker run --name zephyr-cont1 -it -v "C:\training\zephyr_workspace:/workspace" ghcr.io/embeddedcontainers/zephyr:arm-1.0.1SDKThe PowerShell prompt should change to indicate that you are now running a bash shell in the new container, as user "root". The number after the @ is the container ID - you do not need to remember this since you can use the name zephyr-cont1 that you specified in the previous step to start the container again (use the docker start -i command).
If you enter the ls command you should see the contents of the Ubuntu root directory. Change to the workspace directory.
root@3f1a590260f4:/# ls
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var workspace
root@3f1a590260f4:/# cd workspace
root@3f1a590260f4:/workspace#Before we can install the Zephyr library, we need to convert the workspace directory to a Zephyr workspace, using the west init command. However, we only want to install those parts of the library that are required for our Pico board. To prevent unwanted files from being downloaded, we can create a "manifest" file west.yml in the directory where we will also put the code for our pico app.
Create a folder named "blinky" and a file within this directory named "west.yml".
root@3f1a590260f4:/workspace# mkdir blinky
root@3f1a590260f4:/workspace# touch blinky/west.ymlUsing your preferred Windows 11 editor, add the following code to the west.yml file (note that you need to edit this file in Windows since the docker container does not include a text editor). We have specified Zephyr revision: v4.4.2 - if you wish to install a different version, modify this line to match your chosen version number.
manifest:
remotes:
- name: zephyrproject
url-base: https://github.com/zephyrproject-rtos
projects:
- name: zephyr
repo-path: zephyr
remote: zephyrproject
revision: v4.4.2
import:
path-prefix: deps
name-allowlist:
- cmsis
- cmsis-dsp
- cmsis_6
- percepio
- picolibc
- mcuboot
- segger
- hal_rpi_pico
self:
path: blinkyInitialize the west workspace using the west.yml file in the blinky directory. Then run west update to download the latest version of the files from the Zephyr repository (this can take a long time).
root@3f1a590260f4:/workspace# west init -l blinky
=== Initializing from existing manifest repository blinky
--- Creating /workspace/.west and local configuration file
=== Initialized. Now run "west update" inside /workspace.root@3f1a590260f4:/workspace# west update
=== updating zephyr (deps/zephyr):
--- zephyr: initializing
Initialized empty Git repository in /workspace/deps/zephyr/.git/
--- zephyr: fetching, need revision v4.4.2
...
...Install the Python dependencies using the script that has been written to the deps/zephyr/scripts/ directory.
root@3f1a590260f4:/workspace# pip3 install --no-cache-dir -r deps/zephyr/scripts/requirements.txtList installed "projects" by running west list command.
root@3f1a590260f4:/workspace# west list
manifest blinky HEAD N/A
zephyr deps/zephyr v4.4.2 https://github.com/zephyrproject-rtos/zephyr
cmsis deps/modules/hal/cmsis 512cc7e895e8491696b61f7ba8066b4a182569b8 https://github.com/zephyrproject-rtos/cmsis
cmsis-dsp deps/modules/lib/cmsis-dsp 97512610ec92058f0119450b9e743eeb7e95b5c8 https://github.com/zephyrproject-rtos/cmsis-dsp
cmsis_6 deps/modules/hal/cmsis_6 30a859f44ef8ab4dc8f84b03ed586fd16ccf9d74 https://github.com/zephyrproject-rtos/CMSIS_6
hal_rpi_pico deps/modules/hal/rpi_pico 562b41e10a1d8b1a761b253b107c5c6a84cf4535 https://github.com/zephyrproject-rtos/hal_rpi_pico
mcuboot deps/bootloader/mcuboot 6d3b3d2c38ab20c242e5b9abb04d050086383eb2 https://github.com/zephyrproject-rtos/mcuboot
percepio deps/modules/debug/percepio f9159fe3c79578e8d90c8593e83f9862d9b8962c https://github.com/zephyrproject-rtos/percepio
picolibc deps/modules/lib/picolibc 01254932e8e81085817ed61fd858648584ffe37c https://github.com/zephyrproject-rtos/picolibc
segger deps/modules/debug/segger 50892fdbcf2f570e67baa72b8894a66b16946f72 https://github.com/zephyrproject-rtos/seggerZephyr includes a selection of sample projects, one of which is the "common" blinky app that flashes an LED on the board while writing a message to an output terminal. The files in the Zephyr sample need only minor alterations to run on the Pico2.
deps/zephyr/samples/basic/blinky
├── CMakeLists.txt
├── README.rst
├── boards
│ └── nrf54h20dk_nrf54h20_cpuppr.overlay
├── prj.conf
├── sample.yaml
└── src
└── main.cCopy all files from /workspace/deps/zephyr/samples/basic/blinky to workspace/blinky
root@3f1a590260f4:/workspace# cp -r deps/zephyr/samples/basic/blinky .Delete blinky/sample.yaml
root@3f1a590260f4:/workspace# rm -f blinky/sample.yamlExamine the source code in blinky/src/main.c (hint: use the bash cat command or open in a Windows editor).
root@3f1a590260f4:/workspace# cat blinky/src/main.cThis statement declares a macro LED0_NODE that corresponds to the `led0` peripheral in the devicetree:
#define LED0_NODE DT_ALIAS(led0)While this statement declares a struct variable named led that is mapped to the led0 peripheral:
static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED0_NODE, gpios);In the main function, these statements check that led is ready and if it is, configures the GPIO driver:
if (!gpio_is_ready_dt(&led)) {
return 0;
}
ret = gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE);Finally, the led is toggled by calling a built-in Zephyr function:
ret = gpio_pin_toggle_dt(&led);No changes are required to main.c
Rename blinky/boards/nrf54h20dk_nrf54h20_cpuppr.overlay to match pico2 board name.
root@3f1a590260f4:/workspace# mv blinky/boards/nrf54h20dk_nrf54h20_cpuppr.overlay blinky/boards/rpi_pico2_rp2350a_m33.overlayEdit (in Windows) the overlay file to match the Pico2 peripheral characteristics.
The GPIO and pin number will need to change:
/* Original code */
/ {
aliases {
led0 = &led0;
};
leds {
compatible = "gpio-leds";
led0: led_0 {
gpios = <&gpio9 0 GPIO_ACTIVE_HIGH>;
label = "Green LED 0";
};
};
};
&gpiote130 {
status = "okay";
};
&gpio9 {
status = "okay";
};The LED on the Pico2 is connected to gpio0 pin 25. Change the code as follows:
/* Modified code *// {
aliases {
led0 = &led0;
};
leds {
compatible = "gpio-leds";
led0: led_0 {
gpios = <&gpio0 25 GPIO_ACTIVE_HIGH>;
label = "Green LED 0";
};
};
};
&gpio0 {
status = "okay";
};Build the blinky application.
root@3f1a590260f4:/workspace# west build -p -b rpi_pico2/rp2350a/m33 blinkyWhen it has finished building, the output should be similar to this.
...
-- Zephyr version: 4.4.2 (/workspace/deps/zephyr), build: v4.4.2
[151/151] Linking C executable zephyr/zephyr.elf
Memory region Used Size Region Size %age Used
FLASH: 17456 B 4 MB 0.42%
RAM: 4032 B 520 KB 0.76%
IDT_LIST: 0 B 32 KB 0.00%
Generating files from /workspace/build/zephyr/zephyr.elf for board: rpi_pico2/rp2350a/m33
Converted to uf2, output size: 35328, start address: 0x10000000
Wrote 35328 bytes to zephyr.uf2
root@3f1a590260f4:/workspace#Note that the executable files are placed in the /workspace/build/zephyr directory and are named zephyr.elf, zephyr.uf2, etc.
Unlike many other development boards, the Pico2 does not include an onboard progamming/debug interface. This means that the west flash command cannot be used to program the Pico2 with the executable we have just built. Later, we will see how an external debug probe can be used to program the Pico2, but for now, we can use a manual approach to load the zephyr.uf2 file mentioned above.
Open a File Explorer window and navigate to the directory containing the executable (e.g. C:\training\zephyr_workspace\build\zephyr). Select the file zephyr.uf2

Connect the USB lead to the Pico2 board while holding down the BOOTSEL button.

Now release the BOOTSEL button. If you open a new File Explorer window, you should see a disk drive with a name like RP2350 (D:)

Use the mouse to drag the selected zephyr.uf2 file over the RP2350 (D:)disk and release (or Copy and Paste). The program should be flashed into the board and the Green LED should start blinking.
The main function in the blinky application includes a print statement:
printf("LED state: %s\n", led_state ? "ON" : "OFF");How can we see the printed output messages?
By default, the printf function sends output strings to UART0 on the Pico2. The default pin assignment for UART0 is pin 1 for TX and pin 2 for RX, as shown below:

We can display the printed messages on a Windows PC by connecting a suitable USB to UART bridge (such as the "FTDI Chip 3V3 TTL to USB Serial Converter Cable" or one based on a Silicon Labs CP210x chip) to pins 1, 2 and 3. Alternatively, we can connect the USB to UART bridge that is part of the Raspberry Pi Debug Probe to these pins.
Open the Windows Device Manager to check which serial port the bridge is connected to:

A terminal emulator such as PuTTY can be used to display the output. Open PuTTY and set the Connection type to Serial, the Serial line to the port found above and the Speed to 115200 then click Open:

You should now see the output messages as they are written to the terminal emulator:

The Pico2 has a Serial Wire Debug (SWD) port that supports both programming the board as well as debugging. Commercial debug probes such as SEGGER J-Link are supported by the west flash command (however note that older models might not support SWD). We are going to show how to use the Raspberry Pi Debug Probe since it is a low-cost device that is popular with hobbyists.
The Raspberry Pi Debug Probe works with OpenOCD and the GNU Project Debugger (GDB). Unfortunately, it requires a custom port of the open-source OpenOCD code that is not part of the Docker image that we have been using so far. However, it is easy to create an updated Docker image that includes the Raspberry Pi version of OpenOCD with the help of a dockerfile.
If you still have a docker container running, close it using the exit command:
root@3f1a590260f4:/workspace# exit
exit
PS C:\training\zephyr_workspace>Copy the code below to a new file named dockerfile
# dockerfile to build custom Zephyr arm-1.0.1SDK image wih RPi OpenOCD
# --- Stage 1: Build OpenOCD with modern hardware targets ---
FROM ubuntu:24.04 AS openocd-builder
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
git automake autoconf build-essential texinfo libtool \
libftdi-dev libusb-1.0-0-dev pkg-config libhidapi-dev \
libjim-dev \
&& rm -rf /var/lib/apt/lists/*
# Clone the main Raspberry Pi repository fork
RUN git clone https://github.com/raspberrypi/openocd.git /opt/openocd
WORKDIR /opt/openocd
# Bootstrap and run the compiler with the native host libraries linked
RUN ./bootstrap && \
./configure --enable-ftdi --enable-sysfsgpio --enable-picoprobe --enable-cmsis-dap && \
make -j$(nproc) && \
make install
# --- Stage 2: Lightweight Zephyr build container with OpenOCD ---
FROM ghcr.io/embeddedcontainers/zephyr:arm-1.0.1SDK
USER root
ENV DEBIAN_FRONTEND=noninteractive
# Install runtime dependencies for OpenOCD
RUN apt-get update && apt-get install -y --no-install-recommends \
libusb-1.0-0 libftdi1 libhidapi-hidraw0 "libjim0*" \
&& rm -rf /var/lib/apt/lists/*
# Inject the freshly compiled openocd and its configuration scripts
COPY --from=openocd-builder /usr/local/bin/openocd /usr/local/bin/openocd
COPY --from=openocd-builder /usr/local/share/openocd /usr/local/share/openocd
WORKDIR /workspace
ENTRYPOINT []
CMD ["/bin/bash"]Save dockerfile as a "Unix" file (LF line endings not CRLF). If your editor does not support saving in this format, you can do the conversion using the bash shell command dos2unix or from a PowerShell run:
> (Get-Content dockerfile) -join "`n" | Out-File -Encoding UTF8 dockerfileBuild a new Docker image using the dockerfile (Note this command is run from the same directory as dockerfile):
> docker build -t zephyr-openocd .Create a new Docker container named zephyr-cont2 using the zephyr-openocd image. The shared workspace will be the same as in the earlier steps.
> > docker run -it --name "zephyr-cont2" -v "C:\training\zephyr_workspace:/workspace" zephyr-openocd:latest
root@cd7d29467741:/workspace#Note that this container automatically changes the current directory to /workspace (set in dockerfile).
Add the python dependencies to the new Docker container:
root@cd7d29467741:/workspace# pip3 install --no-cache-dir -r deps/zephyr/scripts/requirements.txtRebuild the blinky application (note the -p switch ensures a clean build):
west build -p -b rpi_pico2/rp2350a/m33 blinkyWe need to install the Raspberry Pi port of OpenOCD in Windows. The easiest way of doing this, is to add the Raspberry Pi Pico extension to VS Code.
6.1. Open VS Code.
6.2. Open the Extensions marketplace <Ctrl> + <Shift> + <X>).
6.3. Search for Raspberry Pi Pico (the official extension published by Raspberry Pi).
6.4. Click Install.
6.5. Once installed, click the Raspberry Pi Pico icon that appears in your left-hand Activity Bar.
6.6. If the extension notices that dependencies are missing, it will automatically present a pop-up prompt to install the toolchains. Click Install to let it download OpenOCD, CMake, and the compiler in the background.
6.7. Locate the directory where openocd.exe is installed and add it to the PowerShell path (it should be installed in the user directory), e.g.
> ls $HOME\.pico-sdk\openocd\0.12.0+dev\
Directory: C:\Users\xxx\.pico-sdk\openocd\0.12.0+dev
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 27/03/2026 12:10 scripts
-a---- 27/03/2026 12:10 71539 libhidapi-0.dll
-a---- 27/03/2026 12:10 228396 libusb-1.0.dll
-a---- 27/03/2026 12:10 18071551 openocd.exeConnect the Raspberry Pi Debug Probe to the Pico2 SWD debug port:

Open a second PowerShell terminal and start OpenOCD.
> openocd.exe -s "$HOME\.pico-sdk\openocd\0.12.0+dev\scripts" -f interface/cmsis-dap.cfg -f target/rp2350.cfg -c "adapter speed 5000" -c "bindto 0.0.0.0"You should see messages telling you that OpenOCD is starting and waiting for a connection:
Open On-Chip Debugger 0.12.0+dev (2025-10-09-14:23)
Licensed under GNU GPL v2
For bug reports, read
http://openocd.org/doc/doxygen/bugs.html
Info : [rp2350.cm0] Hardware thread awareness created
Info : [rp2350.cm1] Hardware thread awareness created
ocd_process_reset_inner
Info : Listening on port 6666 for tcl connections
Info : Listening on port 4444 for telnet connections
...
Info : CMSIS-DAP: SWD supported
Info : CMSIS-DAP: Atomic commands supported
Info : CMSIS-DAP: Test domain timer supported
Info : CMSIS-DAP: FW Version = 2.0.0
Info : CMSIS-DAP: Interface Initialised (SWD)
Info : SWCLK/TCK = 0 SWDIO/TMS = 0 TDI = 0 TDO = 0 nTRST = 0 nRESET = 0
Info : CMSIS-DAP: Interface ready
Info : clock speed 5000 kHz
Info : SWD DPIDR 0x4c013477
Info : [rp2350.cm0] Cortex-M33 r1p0 processor detected
Info : [rp2350.cm0] target has 8 breakpoints, 4 watchpoints
Info : [rp2350.cm0] Examination succeed
Info : [rp2350.cm1] Cortex-M33 r1p0 processor detected
Info : [rp2350.cm1] target has 8 breakpoints, 4 watchpoints
Info : [rp2350.cm1] Examination succeed
Info : [rp2350.cm0] starting gdb server on 3333
Info : Listening on port 3333 for gdb connectionsIn the running container, use the local arm-zephyr-eabi-gdb to connect to the Windows OpenOCD session and flash the board:
root@cd7d29467741:/workspace# /opt/toolchains/zephyr-sdk-1.0.1/gnu/arm-zephyr-eabi/bin/arm-zephyr-eabi-gdb -nx --batch -ex "target extended-remote host.docker.internal:3333" -ex "load" -ex "monitor reset run" build/zephyr/zephyr.elfYou should see messages in the docker container as the binary is loaded on the Pico2:
__enable_irq () at /workspace/deps/modules/hal/cmsis_6/CMSIS/Core/Include/cmsis_gcc.h:800
800 __ASM volatile ("cpsie i" : : : "memory");
Loading section rom_start, size 0x12c lma 0x10000000
Loading section text, size 0x38f8 lma 0x1000012c
Loading section .ARM.exidx, size 0x8 lma 0x10003a24
Loading section initlevel, size 0x38 lma 0x10003a2c
Loading section device_area, size 0x70 lma 0x10003a64
Loading section sw_isr_table, size 0x1a0 lma 0x10003ad4
Loading section gpio_driver_api_area, size 0x24 lma 0x10003c74
Loading section reset_driver_api_area, size 0x10 lma 0x10003c98
Loading section clock_control_driver_api_area, size 0x1c lma 0x10003ca8
Loading section uart_driver_api_area, size 0x98 lma 0x10003cc4
Loading section rodata, size 0x65c lma 0x10003d5c
Loading section datas, size 0x6c lma 0x100043b8
Loading section device_states, size 0x8 lma 0x10004424
Loading section .last_section, size 0x4 lma 0x1000442c
Start address 0x10000efc, load size 17456
Transfer rate: 10 KB/sec, 1246 bytes/write.
available
[Inferior 1 (Remote target) detached]While in the other PowerShell terminal, you should see the corresponding messages from OpenOCD:
Info : New GDB Connection: 2, Target rp2350.cm0, state: halted
[rp2350.cm0] halted due to debug-request, current mode: Thread
xPSR: 0xf9000000 pc: 0x00000088 msp: 0xf0000000
[rp2350.cm1] halted due to debug-request, current mode: Thread
xPSR: 0xf9000000 pc: 0x00000088 msp: 0xf0000000
Info : RP2xxx ROM API function FC @ 3711
Info : Padding image section 0 at 0x10004430 with 208 bytes (bank write end alignment)
[rp2350.cm0] halted due to debug-request, current mode: Thread
xPSR: 0xf9000000 pc: 0x00000088 msp: 0xf0000000
[rp2350.cm1] halted due to debug-request, current mode: Thread
xPSR: 0xf9000000 pc: 0x00000088 msp: 0xf0000000
Info : dropped 'gdb' connectionThe gdb command we used in the Docker container was ugly! We can improve this by asking ninja to build an alternative command that is easier to use. Edit the blinky/CMakeLists.txt by inserting the add_custom_target function shown below to the end of the file:
# SPDX-License-Identifier: Apache-2.0
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(blinky)
target_sources(app PRIVATE src/main.c)
# Add the following code to generate a new native custom flash command
add_custom_target(flash_pico2
COMMAND /opt/toolchains/zephyr-sdk-1.0.1/gnu/arm-zephyr-eabi/bin/arm-zephyr-eabi-gdb
-nx --batch
-ex [[target extended-remote host.docker.internal:3333]]
-ex [[load]]
-ex [[monitor reset run]]
${APPLICATION_BINARY_DIR}/zephyr/zephyr.elf
DEPENDS zephyr_final
COMMENT "Flashing target via remote GDB link..."
USES_TERMINAL
)Rebuild the blinky application:
root@cd7d29467741:/workspace# west build -p -b rpi_pico2/rp2350a/m33 blinkyYou can now flash the board by running:
root@cd7d29467741:/workspace# ninja -C build flash_pico2If you wish to run a container that you have previously created:
Make sure the Docker engine is running by opening Docker Desktop
Start the container in a PowerShell using the name specified when the container was created, e.g.
> docker start -i zephyr-cont2Note that you can also start a container and open a terminal from Docker Desktop but that is less flexible.
Try making some simple changes to the code in main.c, such as the time between LED toggles and the content of the printf messages. Rebuild and re-flash the application and check that it works as expected!
Now you have a Zephyr application running on your Pico2 board, where should you go next?
Here are two suggestions, we will discuss each of these in a follow-on article:
Improvements to the development and debug environment.
Since our source code, the Zephyr library and OpenOCD are all installed accessible from Windows 11, we can use VS Code to write and debug applications. It is even possible to run the west commands in our Docker container from within VS Code by installing an extension for containers.
Accessing additional peripherals.
There are many different peripherals that we can connect to the Pico2, such as sensors and display devices, that are supported by Zephyr. Some of these are connected to "standard" GPIO pins while others require interfaces such as UART, I2C or SPI. To understand how we can make use of such peripherals, we will need to take a closer look at devicetree and board overlays.
Of course, there is still the requirement to learn about Zephyr's features and how to use them! For that, you might want to consider our Zephyr Essentials training.
Here is a list of the hardware and software components used in this article:
Raspberry Pi Pico2. This can be purchased with or without header pins - both are suitable but if you wish to connect peripherals or a debug probe the version with pins is more convenient (unless you are happy to solder the pins yourself).
Raspberry Pi Debug Probe (optional).
Cytron Maker Pi Pico (optional - requires Pico with header pins). This is a neat alternative to a conventional bread-board, especially if you want to connect any peripherals via a Grove port.
PuTTY - free terminal emulator.
This article was written by Dr David Long, Principal Member Technical Staff at Doulos.
Version 1.0 October 2026.
This article is Copyright © 2026 by Doulos. All rights are reserved.
Search Doulos KnowHow for the very latest information.