When I first picked up my STM32 Nucleo-F401RE board, my goal was simple: to explore how modern embedded systems can be built with Zephyr RTOS — a scalable, open-source real-time operating system backed by the Linux Foundation.
This post is my journey: setting up the workspace, flashing my first blinky.
1. Setting Up the Zephyr Workspace
Install Required Packages
sudo apt update && sudo apt upgrade -y
sudo apt install -y \
git cmake ninja-build gperf build-essential \
python3 python3-pip python3-venv python3-dev \
device-tree-compiler ccache dfu-util xz-utils file \
libusb-1.0-0-dev libncurses5 libtinfo5
sudo apt install -y stlink-tools
The stlink-tools
package gives you open-source utilities for flashing and probing ST-LINK devices.
Create a Python Virtual Environment
python3 -m venv ~/zephyr-venv
source ~/zephyr-venv/bin/activate
pip install --upgrade pip setuptools
pip install west
which west
west
is Zephyr’s official meta-tool used for workspace and module management.
Install the Zephyr SDK (Toolchain)
mkdir ~/zephyr-project
cd ~/zephyr-project
wget https://github.com/zephyrproject-rtos/sdk-ng/releases/download/v0.17.4/zephyr-sdk-0.17.4_linux-x86_64.tar.xz
tar -xvf zephyr-sdk-0.17.4_linux-x86_64.tar.xz
cd zephyr-sdk-0.17.4
./setup.sh
Once complete, you’ll see:
(zephyr-venv) ripan@ripan-notebook:~/zephyr-project/zephyr-sdk-0.17.4$ ./setup.sh
Zephyr SDK 0.17.4 Setup
** NOTE **
You only need to run this script once after extracting the Zephyr SDK
distribution bundle archive.
Install host tools [y/n]? y
Register Zephyr SDK CMake package [y/n]? y
Installing host tools ...
Registering Zephyr SDK CMake package ...
Zephyr-sdk (/home/ripan/zephyr-project/zephyr-sdk-0.17.4/cmake)
has been added to the user package registry in:
~/.cmake/packages/Zephyr-sdk
All done.
Press any key to exit ...
Verify the compiler:
~/zephyr-project/zephyr-sdk-0.17.4/arm-zephyr-eabi/bin/arm-zephyr-eabi-gcc --version
2. Create the Zephyr Workspace
mkdir -p ~/zephyr-project/ws
cd ~/zephyr-project/ws
west init -m https://github.com/zephyrproject-rtos/zephyr
west update
pip install -r zephyr/scripts/requirements.txt
source zephyr/zephyr-env.sh
This initializes Zephyr’s modules and environment variables.
3. Building and Flashing the First Sample
Build the “Blinky” Example
west build -b nucleo_f401re samples/basic/blinky
Zephyr uses CMake internally and automatically picks up your SDK toolchain.
A successful build shows something like:
Memory region Used Size Region Size %age Used
FLASH: 17720 B 512 KB 3.38%
RAM: 4544 B 96 KB 4.62%
SRAM0: 0 GB 96 KB 0.00%
IDT_LIST: 0 GB 32 KB 0.00%
Flash the Binary
west flash
west flash
requires STM32_Programmer_CLI.
Or manually using ST-LINK:
st-flash write build/zephyr/zephyr.bin 0x8000000
4. Connecting and Verifying the Board
Connect your Nucleo via a data-capable micro-USB cable.
lsusb | grep -i st
st-info --probe
Expected output:
Found 1 stlink programmers
version: V2J33S25
flash: 524288 (pagesize: 16384)
sram: 98304
chipid: 0x433
dev-type: STM32F401xD_xE
This confirms proper detection of your board.
5. Flashing Using STM32CubeProgrammer
Download from ST’s website and install:
unzip stm32cubeprg-lin-v2-20-0.zip
./SetupSTM32CubeProgrammer-2.20.0.linux
Then add to PATH:
export PATH=$PATH:$HOME/STMicroelectronics/STM32Cube/STM32CubeProgrammer/bin
source ~/.bashrc
Verify:
STM32_Programmer_CLI --version
Example output:
STM32CubeProgrammer v2.20.0
To detect board using the cube programmer
STM32_Programmer_CLI -l
Expected output:
===== STLink Interface =====
-------- Connected ST-LINK Probes List --------
ST-Link Probe 0 :
ST-LINK SN : 066BFF373654393143213418
ST-LINK FW : V2J33M25
Access Port Number : 1
Board Name : NUCLEO-F401RE
Now flash Zephyr via West:
west flash
6. Serial Console Output
screen /dev/ttyACM0 115200
Expected boot message:
*** Booting Zephyr OS build v4.2.0-6152-gfd51dde8f5ca ***
LED state: ON
LED state: OFF
Build version must match with build/zephyr/include/generated/zephyr/version.h
7. Understanding the “Blinky” Source
In the Nucleo-F401RE board file build/zephyr/zephyr.dts
aliases {
led0 = &green_led_2; /* in zephyr/boards/st/nucleo_f401re/nucleo_f401re.dts:55 */
sw0 = &user_button; /* in zephyr/boards/st/nucleo_f401re/nucleo_f401re.dts:56 */
pwm-led0 = &green_pwm_led; /* in zephyr/boards/st/nucleo_f401re/nucleo_f401re.dts:57 */
watchdog0 = &wwdg; /* in zephyr/boards/st/nucleo_f401re/nucleo_f401re.dts:58 */
die-temp0 = &die_temp; /* in zephyr/boards/st/nucleo_f401re/nucleo_f401re.dts:59 */
volt-sensor0 = &vref; /* in zephyr/boards/st/nucleo_f401re/nucleo_f401re.dts:60 */
volt-sensor1 = &vbat; /* in zephyr/boards/st/nucleo_f401re/nucleo_f401re.dts:61 */
};
/* node '/leds' defined in zephyr/boards/st/nucleo_f401re/nucleo_f401re.dts:27 */
leds: leds {
compatible = "gpio-leds"; /* in zephyr/boards/st/nucleo_f401re/nucleo_f401re.dts:28 */
/* node '/leds/led_2' defined in zephyr/boards/st/nucleo_f401re/nucleo_f401re.dts:30 */
green_led_2: led_2 {
gpios = < &gpioa 0x5 0x0 >; /* in zephyr/boards/st/nucleo_f401re/nucleo_f401re.dts:31 */
label = "User LD2"; /* in zephyr/boards/st/nucleo_f401re/nucleo_f401re.dts:32 */
};
};
gpios = < &gpioa 0x5 0x0 >
&gpioa
: use the GPIO controller named gpioa
5
: pin number (pin 5 on port A)
0
: flag GPIO_ACTIVE_LOW, tells Zephyr that logic 0 turns the LED on
samples/basic/blinky/src/main.c
:
/*
* blinky with inline explanations
*
* Build and run on Nucleo-F401RE (or any Zephyr board that provides
* an 'led0' alias in its DeviceTree).
*/
#include <stdio.h> /* for printf(); redirected to configured console (UART). */
#include <zephyr/kernel.h> /* kernel API: k_msleep(), threading, types, etc. */
#include <zephyr/drivers/gpio.h> /* GPIO API and device-tree helper macros */
/* 1000 msec = 1 sec -- used to sleep between toggles */
#define SLEEP_TIME_MS 1000
/*
* The devicetree alias name used by blinky samples is "led0".
* DT_ALIAS(led0) expands at build-time into a node identifier for the node
* pointed to by the alias 'led0' in the board's DeviceTree.
*
* For Nucleo-F401RE, the alias (in zephyr/boards/st/nucleo_f401re/nucleo_f401re.dts)
* maps to a node (e.g. &green_led_2) whose gpios property looks like:
*
* gpios = <&gpioa 5 GPIO_ACTIVE_LOW>;
*
* That says: this LED is on GPIO controller 'gpioa', pin 5, and it's active-low.
*/
#define LED0_NODE DT_ALIAS(led0)
/*
* Build-time construction of a gpio_dt_spec:
*
* GPIO_DT_SPEC_GET(node_id, prop) expands (behind the scenes) into a
* static const struct gpio_dt_spec containing:
* - .port : the device pointer for the GPIO controller (DEVICE_DT_GET(...))
* - .pin : the pin number (e.g. 5)
* - .dt_flags : flags from the DT (e.g. GPIO_ACTIVE_LOW)
*/
static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED0_NODE, gpios);
int main(void)
{
int ret;
bool led_state = true;
/*
* Check that the GPIO controller device (led.port) is ready.
* gpio_is_ready_dt() internally checks device_is_ready(led.port).
* If the driver failed to initialize, it's safer to abort than to proceed.
*/
if (!gpio_is_ready_dt(&led)) {
/* In a real app, consider logging an error or retrying initialization. */
printf("ERROR: GPIO controller for LED not ready\n");
return 0;
}
/*
* Configure the pin as an output and set its initial state to the
* "active" level (GPIO_OUTPUT_ACTIVE).
*
* Because the DeviceTree for this LED contains GPIO_ACTIVE_LOW,
* "active" means drive the pin LOW. If the LED were active-high,
* "active" would be HIGH. The DT flags hide that polarity detail
* from the application code.
*
* gpio_pin_configure_dt(&led, flags) is a convenience wrapper for:
* gpio_pin_configure(led.port, led.pin, <calculated_flags>);
*/
ret = gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE);
if (ret < 0) {
printf("ERROR: failed to configure LED pin (err %d)\n", ret);
return 0;
}
/*
* Main loop: toggle the LED, print state, then sleep the thread.
*
* - gpio_pin_toggle_dt(&led) toggles the physical pin level.
* It respects the active-low/active-high mapping defined in DT.
*
* - led_state is a simple logical tracker so the console prints
* reflect the LED's logical meaning (ON/OFF) rather than raw voltage.
*
* - printf() prints to the console backend (UART). On the host,
* this shows up on /dev/ttyACM0 because the board's console is
* wired to the ST-LINK CDC ACM interface.
*
* - k_msleep() is a kernel sleep: it yields the CPU for the duration,
* allowing other threads or interrupts to run. It's not a busy-wait.
*/
while (1) {
ret = gpio_pin_toggle_dt(&led);
if (ret < 0) {
printf("ERROR: toggle failed (err %d)\n", ret);
return 0;
}
/* flip logical LED state and print a human-readable message */
led_state = !led_state;
printf("LED state: %s\n", led_state ? "ON" : "OFF");
/* sleep for SLEEP_TIME_MS milliseconds; kernel schedules other work in the meantime */
k_msleep(SLEEP_TIME_MS);
}
return 0;
}
4. Setup Zephyr application
As my experiments grew, I moved away from samples and created an out-of-tree Zephyr app:
├── app
│ ├── CMakeLists.txt
│ └── src
│ ├── CMakeLists.txt
│ ├── core
│ │ ├── app_controller.c
│ │ └── include
│ │ └── core.h
│ ├── hw
│ │ ├── include
│ │ │ └── hw_iface.h
│ │ ├── native_sim
│ │ │ └── led.c
│ │ └── nucleo_f401re
│ │ └── led_gpio.c
│ ├── main.c
├── boards
│ └── nucleo_f401re.overlay
├── CMakeLists.txt
├── prj.conf
└── scripts
├── build.sh
└── flash.sh
core/
→ application logic
hw/
→ board/sensor interfaces
app/main.c
→ entry point
I can build it both for native simulation and the STM32 board:
# Native simulation
ZEPHYR_BASE="${HOME}/zephyr-project/ws/zephyr" west build -b native_sim . -d build_native_sim -p auto
west build -t run -d build_native_sim
# STM32
ZEPHYR_BASE="${HOME}/zephyr-project/ws/zephyr" west build -b nucleo_f401re . -d build -p auto
west flash -d build
Top comments (0)