Getting Started with Zephyr RTOS on ESP32: Why It Might Replace ESP-IDF for Your Next Product

Every ESP-IDF project starts with the same unspoken bet: that your next product will also run on Espressif silicon. Maybe it will. But when your hardware team comes back from a supply chain review and says the BOM needs an nRF5340 for power budget, or your next SKU targets an STM32 for cost, your firmware is a rewrite. Not a refactor. A rewrite. Different HALs, different driver APIs, different build systems, different mental models.
Zephyr RTOS eliminates that bet. It’s a vendor-neutral RTOS backed by the Linux Foundation with contributions from Intel, Nordic, NXP, Google, and Espressif themselves. It targets 600+ boards across 100+ SoCs with a single hardware abstraction layer, a single build system, and a single API surface. Your gpio_pin_toggle_dt() call compiles and runs on an ESP32 DevKitC, an nRF52840-DK, and a NUCLEO-F429ZI without changing a line of C.
This is your Zephyr RTOS ESP32 tutorial. It’s not a beginner introduction to real-time operating systems. It gets you from zero to a blinking LED in under 30 minutes, maps Zephyr concepts to the ESP-IDF workflow you already know, and makes an honest case for when Zephyr is and isn’t the right choice. BLE comes in the follow-up. No fluff.
Zephyr vs ESP-IDF: An Honest Tradeoff Table
Before you invest time in a new toolchain, you want to know what you’re gaining and what you’re giving up. Here’s the real comparison:
| Dimension | ESP-IDF | Zephyr RTOS |
|---|---|---|
| Vendor lock-in | Espressif only | 600+ boards, 100+ SoCs |
| RTOS kernel | FreeRTOS (modified) | Custom kernel (POSIX-compatible subset) |
| HW abstraction | ESP-specific drivers | Devicetree + generic APIs |
| Build system | CMake + idf.py | CMake + west |
| Config system | Kconfig + menuconfig | Kconfig + devicetree overlays |
| BLE stack | Bluedroid or NimBLE | Native Zephyr BLE stack (or NimBLE) |
| ESP32 maturity | Reference implementation (complete) | Growing; some peripheral gaps remain |
| Community | Espressif forums | Large cross-vendor community, Discord, GitHub |
| Best for | Ship fast on ESP32 only | Multi-platform products, long-term portability |
Be clear-eyed about the gaps. Zephyr’s ESP32 support is maturing rapidly, but some peripheral drivers (certain ADC modes, PCNT, LCD) lag behind ESP-IDF. Deep sleep support is more limited. If you need every last feature of the ESP32 silicon and you’re shipping on Espressif hardware exclusively, ESP-IDF remains the path of least resistance.
But if portability across silicon vendors, a cleaner long-term architecture, or a multi-chip product line matters to your roadmap, Zephyr wins. The ESP32 Zephyr vs ESP-IDF decision comes down to where you want to spend your engineering budget: fast first-product delivery, or compounding returns across products.
Prerequisites and Environment Setup
You need:
- Host OS: Linux preferred. macOS works. Windows via WSL2.
- Python 3.8+
- CMake 3.20+, Ninja, dtc (device tree compiler)
- Git
- A compatible ESP32 board. Any ESP32-DevKitC WROOM will do.
Here’s the minimal happy path. Don’t follow the full Zephyr getting started docs yet; they’re comprehensive but sprawling. This gets you building.
1. Install west:
pip3 install west2. Initialize a Zephyr workspace:
west init -m https://github.com/zephyrproject-rtos/zephyr --mr v4.0.0 zephyrproject
cd zephyrprojectReplace v4.0.0 with the current LTS tag.
3. Pull all modules (including the Espressif HAL):
west updateThis downloads ~2 GB. Go get coffee.
4. Install Python dependencies:
pip3 install -r zephyr/scripts/requirements.txt5. Install the Zephyr SDK toolchain bundle:
Download from the Zephyr SDK releases page, run the installer, and follow the prompts.
6. Set environment variables:
source zephyr/zephyr-env.shVerify your setup:
west boards | grep esp32You should see esp32_devkitc_wroom and variants listed. If you do, you’re ready to build.
If you’re coming from ESP-IDF: you do NOT need a separate ESP-IDF installation. Zephyr’s ESP32 HAL is pulled in via
west updateas a module. The Espressif toolchain and bootloader (MCUboot or the ESP bootloader) are handled internally.
Here’s what your workspace looks like:
zephyrproject/
├── .west/ <-- west workspace config
├── zephyr/ <-- Zephyr kernel + drivers + samples
│ ├── samples/
│ │ └── basic/
│ │ └── blinky/
│ ├── boards/
│ └── dts/ <-- base devicetree files
├── modules/
│ └── hal/
│ └── espressif/ <-- ESP32 HAL (pulled by west)
├── bootloader/
│ └── mcuboot/
└── tools/Mapping ESP-IDF Concepts to Zephyr
You don’t need to unlearn ESP-IDF. You need a translation layer. Here it is:
| ESP-IDF Concept | Zephyr Equivalent |
|---|---|
idf.py | west (meta-tool) |
sdkconfig / menuconfig | prj.conf (Kconfig fragments) |
| Component (driver) | Zephyr module / driver subsystem |
| Pin mux in C code | Devicetree overlay (.overlay file) |
partitions.csv | Devicetree flash partitions |
idf.py monitor | west espressif monitor or any serial terminal |
The single biggest mental shift: hardware description lives in devicetree, not in C. You don’t #define LED_PIN 2 and pass it to a GPIO call. You declare the LED in a .dts or .overlay file, reference it by alias in C, and the build system resolves the mapping at compile time. It feels indirect at first. It’s the mechanism that makes portability work. It’s worth the adjustment.
Building and Flashing Blinky on ESP32
Here’s the full build-flash-verify flow:
[prj.conf] [app.overlay] [main.c]
| | |
v v v
+-------------------------------------------+
| west build |
| (CMake + Ninja + Zephyr build system) |
+-------------------------------------------+
|
v
build/zephyr/zephyr.bin
|
v
+-------------------------------------------+
| west flash |
| (esptool under the hood) |
+-------------------------------------------+
|
v
ESP32 runs blinky
LED toggles @ 1HzStep 1: Build the sample.
cd zephyrproject
west build -b esp32_devkitc_wroom/esp32/procpu zephyr/samples/basic/blinkyThe board target string follows <board>/<soc>/<variant> format, introduced in Zephyr 3.7+. Many older blog posts use the deprecated esp32 board name. If you see build errors from a tutorial, check the target string first.
Step 2: Flash.
Connect your DevKitC via USB, then:
west flashUnder the hood, this invokes esptool.py, the same tool ESP-IDF uses. If you need to specify a port: west flash --esp-device /dev/ttyUSB0.
Step 3: Verify. The onboard LED blinks at 1 Hz.
Step 4: Understand what just happened.
Open zephyr/samples/basic/blinky/src/main.c. It’s about 20 lines that matter:
#define LED0_NODE DT_ALIAS(led0)
static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED0_NODE, gpios);
int main(void)
{
gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE);
while (1) {
gpio_pin_toggle_dt(&led);
k_msleep(1000);
}
return 0;
}Three things to notice:
DT_ALIAS(led0)— The LED’s GPIO pin isn’t hardcoded. It comes from the board’s devicetree file, which mapsled0to the correct GPIO foresp32_devkitc_wroom. On an nRF52840-DK, the same alias resolves to a different pin. Same C code, different hardware.gpio_pin_toggle_dt()— This is Zephyr’s generic GPIO API. It works identically across every supported SoC. Nogpio_set_level()from ESP-IDF’sdriver/gpio.h. Nonrf_gpio_pin_toggle()from Nordic’s HAL. One API.k_msleep(1000)— Zephyr’s kernel sleep primitive. Clean, predictable, and it cooperates with the scheduler the same way on every platform.
Step 5: The “aha” moment. Change the LED pin without touching C.
Create a file at zephyr/samples/basic/blinky/boards/esp32_devkitc_wroom.overlay:
/ {
aliases {
led0 = &myled;
};
leds {
compatible = "gpio-leds";
myled: my_led {
gpios = <&gpio0 2 GPIO_ACTIVE_HIGH>;
};
};
};Rebuild. The LED output is now on GPIO 2, and you didn’t change a single line of application code. This is the portability mechanism in action. When you retarget to a different board, you swap the overlay, not the source. When your hardware team revises the PCB and moves an LED from GPIO 2 to GPIO 18, you change one number in a .overlay file.
This is the architectural difference that compounds across a product line.
What Comes Next: BLE, Wi-Fi, and the Portability Payoff
You have a blinking LED. That’s table stakes. Here’s why the investment keeps paying off.
Zephyr’s ESP32 support already covers SPI, I2C, UART, Wi-Fi (with full networking stack including sockets, MQTT, CoAP), BLE (via NimBLE integration or Zephyr’s native host stack), and flash partitions with MCUboot for OTA. The ecosystem is not a toy.
The next article in this series covers Zephyr BLE on ESP32: advertising, GATT services, and connection handling using Zephyr’s BLE stack. That stack is one of Zephyr’s strongest differentiators, purpose-built, heavily tested across SoCs, and maintained by engineers at Nordic and other Bluetooth-first companies. If your product has a BLE component, and in 2025 most connected products do, this alone justifies evaluating Zephyr.
Building This Into Your Next Product Decision
Here’s what you did today: you set up a Zephyr workspace, built a blinky for ESP32, and flashed it. The same application code, without modification, compiles for hundreds of other boards. That’s not a demo trick. That’s an architectural capability that changes how you plan firmware across product generations.
The learning curve is real. Devicetree overlays will feel strange for the first week. west will annoy you when you reach for idf.py muscle memory. But the curve is front-loaded. Once you’ve internalized the model, adding a new board or SoC to your product line is an overlay and a prj.conf, not a quarter of engineering time.
If you’re starting a new product, or your existing one is about to fork for a second hardware platform, this is the moment to evaluate. Clone the workspace. Build blinky. Retarget it to an nRF52840 board if you have one sitting in a drawer. See what changes and what doesn’t.
Then come back for the BLE guide. That’s where it gets interesting.
Hubble Network connects your Bluetooth devices directly to satellites—no gateways, no infrastructure. See how it works →