DEV Community

Ripan Deuri
Ripan Deuri

Posted on

Zephyr RTOS on STM32 Nucleo: From Setup to Blinky

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 ...
Enter fullscreen mode Exit fullscreen mode

Verify the compiler:

~/zephyr-project/zephyr-sdk-0.17.4/arm-zephyr-eabi/bin/arm-zephyr-eabi-gcc --version
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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%
Enter fullscreen mode Exit fullscreen mode

Flash the Binary

west flash
Enter fullscreen mode Exit fullscreen mode

west flash requires STM32_Programmer_CLI.

Or manually using ST-LINK:

st-flash write build/zephyr/zephyr.bin 0x8000000
Enter fullscreen mode Exit fullscreen mode

4. Connecting and Verifying the Board

Connect your Nucleo via a data-capable micro-USB cable.

lsusb | grep -i st
st-info --probe
Enter fullscreen mode Exit fullscreen mode

Expected output:

Found 1 stlink programmers
  version: V2J33S25
  flash: 524288 (pagesize: 16384)
  sram: 98304
  chipid: 0x433
  dev-type: STM32F401xD_xE
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Then add to PATH:

export PATH=$PATH:$HOME/STMicroelectronics/STM32Cube/STM32CubeProgrammer/bin
source ~/.bashrc
Enter fullscreen mode Exit fullscreen mode

Verify:

STM32_Programmer_CLI --version
Enter fullscreen mode Exit fullscreen mode

Example output:

STM32CubeProgrammer v2.20.0
Enter fullscreen mode Exit fullscreen mode

To detect board using the cube programmer

STM32_Programmer_CLI -l
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Now flash Zephyr via West:

west flash
Enter fullscreen mode Exit fullscreen mode

6. Serial Console Output

screen /dev/ttyACM0 115200
Enter fullscreen mode Exit fullscreen mode

Expected boot message:

*** Booting Zephyr OS build v4.2.0-6152-gfd51dde8f5ca ***
LED state: ON
LED state: OFF
Enter fullscreen mode Exit fullscreen mode

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 */
    };
  };
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Top comments (0)