Some of you may be using STM32 microcontrollers—they’re everywhere. Many Nucleo and Discovery boards are affordable and easy to get, and I bet some of you already own one. Using them with Zephyr is fairly straightforward, and if you followed the previous post, you’re pretty much ready to go. Still, this guide is dedicated just for you.
Pull our docker container with zephyr, make a new directory and west init
with using our template in Github
$ docker pull modularmx/zephyros:latest
$ mkdir myProject
$ cd myProject
$ docker run -it --rm -w /home/user/workspace --mount type=bind,src="$(pwd)",dst=/home/user/workspace modularmx/zephyros:latest
$ west init -m https://github.com/ModularMX/zephyr-template.git
Modify the manifest file changing hal_nordic for hal_stm32
name-allowlist:
# placehere all the packages you want to import
- cmsis
- hal_stm32
west update
and then type west boards
to list all the stm32 supported boards, you can filtered using grep, for instance in my case I’m using an stm32h5
$ west update
...
$ west boards | grep "stm32h5"
weact_stm32h5_core
stm32h573i_dk
Choose the board of your preference and modify the CMakeLists.txt, in my case I’m using a stm32h573dk that comes with a stlink and for that reason I’m going to use openocd to flash and/or debug
# set enviroemnt variables like the board nad the debug server
set(BOARD stm32h573i_dk)
set(BOARD_FLASH_RUNNER openocd)
write some code in your main.c file, like the one below just for testing purposes
/* Include libraries */
#include <zephyr/kernel.h>
#include <zephyr/drivers/gpio.h>
/* Delay of 1000 ms */
#define SLEEP_TIME_MS 1000
/* Get the DeviceTree alias for the "led0" alias */
#define LED0_NODE DT_ALIAS( led0 )
/* Get pin specification (device, pin, flags) for LED0 from the Devicetree */
const struct gpio_dt_spec led0 = GPIO_DT_SPEC_GET( LED0_NODE, gpios );
int main( void )
{
/* Configure led0 as output*/
gpio_pin_configure_dt( &led0, GPIO_OUTPUT );
while(1)
{
/* Toggle LED led0 */
gpio_pin_toggle_dt( &led0 );
/* Sleep for 1000ms (delay) */
k_msleep( SLEEP_TIME_MS );
}
return 0;
}
build as usual to test everything is working as expected, and from here code anything you like
$ west build -p always app
...
-- Zephyr version: 4.1.0 (/home/user/workspace/deps/zephyr), build: v4.1.0
[141/141] Linking C executable zephyr/zephyr.elf
Memory region Used Size Region Size %age Used
FLASH: 40946 B 2 MB 1.95%
RAM: 4376 B 256 KB 1.67%
SRAM1: 0 GB 256 KB 0.00%
SRAM2: 0 GB 64 KB 0.00%
SRAM3: 0 GB 320 KB 0.00%
EXTMEM: 0 GB 64 MB 0.00%
IDT_LIST: 0 GB 32 KB 0.00%
Generating files from /home/user/workspace/build/zephyr/zephyr.elf for board: stm32h573i_dk
west flash with OpenOCD
The Zephyr container comes with an OpenOCD version bundled from the Zephyr SDK. We can try to use it like this, but it will fail miserably. Why? Because the OpenOCD version included is really outdated. If you’re working with an older board like a Nucleo F0, it probably won’t be a problem. But for newer boards, it definitely will
$ west flash
-- west flash: rebuilding
ninja: no work to do.
-- west flash: using runner openocd
-- runners.openocd: Flashing file: /home/user/workspace/build/zephyr/zephyr.hex
Open On-Chip Debugger 0.11.0+dev-00728-gb6f95a16c-dirty (2024-10-20-01:26)
Licensed under GNU GPL v2
For bug reports, read
http://openocd.org/doc/doxygen/bugs.html
/home/user/workspace/deps/zephyr/boards/st/stm32h573i_dk/support/openocd.cfg:3: Error: Can't find target/stm32h5x.cfg
in procedure 'script'
at file "embedded:startup.tcl", line 26
at file "/home/user/workspace/deps/zephyr/boards/st/stm32h573i_dk/support/openocd.cfg", line 3
FATAL ERROR: command exited with status 1: /opt/zephyr-sdk-0.17.0/sysroots/x86_64-pokysdk-linux/usr/bin/openocd
-s /home/user/workspace/deps/zephyr/boards/st/stm32h573i_dk/support
-s /opt/zephyr-sdk-0.17.0/sysroots/x86_64-pokysdk-linux/usr/share/openocd/scripts
-f /home/user/workspace/deps/zephyr/boards/st/stm32h573i_dk/support/openocd.cfg
-c 'gdb_report_data_abort enable' '-c init' '-c targets' -c 'reset init'
-c 'flash write_image erase /home/user/workspace/build/zephyr/zephyr.hex' -c 'reset run' -c shutdown
The solution is to build OpenOCD from source. In fact, ST provides its own fork that already includes support for their latest boards. We’ll handle this by creating a new Dockerfile. The version we’re going to build will be dedicated solely to STM32 devices that use the ST-Link interface.
# Fetch the modular image for Zephyr OS as baseline
FROM modularmx/zephyros:latest
# switch to "root" user
USER root
# install all the necessary dependencies
RUN apt-get update && apt-get install -qqy wget sudo git libusb-1.0-0-dev \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Download and install the OpenOCD stm32 flavor software package
RUN git clone --recursive https://github.com/STMicroelectronics/OpenOCD.git \
&& cd OpenOCD \
&& ./bootstrap \
&& ./configure --enable-stlink \
&& make \
&& make install \
&& cd .. \
&& rm -rf OpenOCD
# set sudo priviledge to user (terrible idea, i know)
RUN usermod -aG root user
# siwtchback to "user" user
USER user
Build the container with a proper name (don’t forget this has to be done outside the previous running container)
$ docker build -t west_openocd .
Run the container with the shared directory and access to our USB devices
$ docker run --rm -it -w /home/user/workspace --mount type=bind,src="$(pwd)",dst=/home/user/workspace --device=/dev/bus/usb:/dev/bus/usb west_openocd
before flashing we need to indicate west we are using a different OpenOCD version installed on a different location inside our container, we can do this using adding teh following lines in the CMakeLists.txt file, build again.
set(OPENOCD /usr/local/bin/openocd)
set(OPENOCD_DEFAULT_PATH /usr/local/openocd/share/openocd/scripts)
Inside your new container type west flash
, and enjoy your led achievement
$ west flash
-- west flash: rebuilding
ninja: no work to do.
-- west flash: using runner openocd
-- runners.openocd: Flashing file: /home/user/workspace/build/zephyr/zephyr.hex
Open On-Chip Debugger 0.12.0-00033-g0de861e21 (2025-08-21-23:16) [https://github.com/STMicroelectronics/OpenOCD]
Licensed under GNU GPL v2
...
shutdown command invoked
user@bed3aae5e989:~/workspace$
Debugging with VSCode
There’s no way to use Ozone with OpenOCD (although some STM32 boards can be reprogrammed with a J-Link). Instead, we’ll use VS Code to debug our code. There are other alternatives, but we won’t cover them here. To make things easier, we’ll use Dev Containers. Create a new file called compose.yml
in the root directory of your project.
services:
zephyr: # container name
image: west_openocd # image with zephyr and openocd
working_dir: /home/user/workspace
volumes: # mount directory
- type: bind
source: ./
target: /home/user/workspace
devices: # device mapping to usb ports
- /dev/bus/usb:/dev/bus/usb
command: # keep the container running
sleep infinity
Also create the corresponding devcontainer file in the same directory
{
// Name of the dev container
"name": "devtools",
// compose file to run containers
"dockerComposeFile": "compose.yml",
// indicate the container to run from the compose file
"service": "zephyr",
// indicate the working directory inside the container
"workspaceFolder": "/home/user/workspace",
// set bash as the default shell
"postStartCommand": "/bin/bash",
// set some usefull vs code configuration only exsiting in our contianer
"customizations": {
"vscode": {
// Add some extensions
"extensions": [
"ms-vscode.cpptools",
"trond-snekvik.gnu-mapfiles",
"twxs.cmake",
"marus25.cortex-debug"
],
// Set a fancy themes
"settings": {
"workbench.colorTheme": "One Dark Pro Night Flat"
}
}
}
}
Of course, we’ll also create a launch.json
file with the following configuration. Just modify the device
and configFiles
options to match the STM32 board or chip of your choice. And remember—you can always check out our post about debugging with VS Code for more details on this configuration
{
"version": "1.12.1",
"configurations": [
{
"type": "cortex-debug",
"request": "launch",
"name": "Debug (OpenOCD)",
"servertype": "openocd",
"interface": "swd",
"cwd": "${workspaceRoot}",
"runToEntryPoint": "main",
"executable": "${workspaceRoot}/build/zephyr/zephyr.elf",
"device": "STM32H573I",
"configFiles": [
"board/st_nucleo_h5.cfg"
],
"armToolchainPath": "/opt/zephyr-sdk/arm-zephyr-eabi/bin",
"toolchainPrefix": "arm-zephyr-eabi"
}
]
}
Run your devcontainer and launch your debug session as usual using the plugin cortex-debug, easy and simple

Using a separate container
However, it’s much better to reuse our Modular-MX OpenOCD container, which is built specifically for STM32. We can orchestrate it separately with Compose and integrate it with VS Code—just like we did earlier with J-Link in the previous post
services:
server: # container name
image: modularmx/openocd:latest
ports: # port mapping
- 3333:3333
devices: # device mapping to usb ports
- /dev/bus/usb:/dev/bus/usb
networks:
static_net: # use network static_net
ipv4_address: 172.25.0.2 # assing ip address to container
command: openocd -f board/st_nucleo_h5.cfg -c "bindto 172.26.0.2"
zephyr: # container name
image: modularmx/zephyros:latest
working_dir: /home/user/workspace
volumes: # volume mapping
- type: bind
source: ./
target: /home/user/workspace
depends_on:
- server
networks:
static_net: # use network static_net
ipv4_address: 172.25.0.3 # assing ip address to container
command: # keep the container running
sleep infinity
networks:
static_net: # network name to be used by containers
driver: bridge # network driver
ipam:
config: # network configuration
- subnet: 172.25.0.0/16 # network subnet
gateway: 172.25.0.1 # network gateway
In the launch.json file just set the servertype
to external and write the IP address of the OpenOCD container assigned in the compose file (this last part is really important)
{
"version": "1.12.1",
"configurations": [
{
"type": "cortex-debug",
"request": "launch",
"name": "Debug (OpenOCD)",
"servertype": "external",
"gdbTarget": "172.25.0.2:3333",
"interface": "swd",
"cwd": "${workspaceRoot}",
"runToEntryPoint": "main",
"executable": "${workspaceRoot}/build/zephyr/zephyr.elf",
"svdFile": "${workspaceRoot}/deps/modules/hal/nordic/nrfx/mdk/nrf54l15_application.svd",
"armToolchainPath": "/opt/zephyr-sdk/arm-zephyr-eabi/bin",
"toolchainPrefix": "arm-zephyr-eabi"
}
]
}
Launch the devcontainer and the debug session as usual (notice the .devcontainer.json file remains the same) and that’s it, now you have a full functional set-up for your stm32 favorite boards. If you want to add a serial port support you can read the previous post, the process is pretty much the same