A minimal bare-metal SDK for Espressif ESP32 RISC-V microcontrollers, developed without relying on the ESP-IDF.
This SDK provides low-level access to ESP32 hardware, enabling firmware development using direct register-level programming, custom startup code, and linker scripts with minimal abstraction.
Currently supported SoCs:
- ESP32-C6
- ESP32-P4
- No ESP-IDF dependency
- Uses direct boot mode(No second stage bootloader required)
- Direct register-level programming
- Custom linker scripts and startup code
- Example applications (e.g., uart_echo, blink_ws2812, esp_hosted)
- Compatible with CMake and Ninja build systems
- Core platform: custom startup, interrupt setup, timers, SoC/peripheral register access
- GPIO driver: pin direction, read/write, pull-up and pull-down configuration
- I2C driver: master-mode transactions for peripheral bring-up
- SPI driver: low-level SPI transfer support
- UART driver: polling-based high-power UART TX/RX with configurable pin routing
- I2S driver: TX path with DMA streaming support for continuous PCM playback
- ES8311 codec driver: register-level codec initialization and playback control
- SD/MMC and SD SPI drivers: card access paths for block reads/writes
- FAT32 library (
sdFat32): basic file operations on SD storage - WS2812 library: LED strip output using RMT/SPI backend
- USB serial driver: runtime logging/console output over USB serial
- ESP-Hosted host driver (ESP32-P4): SDIO transport + control/data channels for co-processor integration
- BLE host stack on ESP32-P4: NimBLE host integrated over ESP-Hosted HCI transport
- lwIP hosted networking on ESP32-P4: hosted STA data path + DHCP flow
- Bare-metal runtime model: polling-based main loop, no RTOS dependency
esp_hostedBLE + Wi-Fi/lwIP integration is supported onESP32-P4target only.uart_echodefaults toGPIO20/GPIO19onESP32-C6andGPIO10/GPIO11onESP32-P4.- Generic peripheral drivers/libraries are available across supported SoCs where hardware capability matches.
| SoC | Architecture | Notes |
|---|---|---|
| ESP32-C6 | RISC-V | WiFi + BLE MCU |
| ESP32-P4 | RISC-V | High-performance application processor |
Targets are selected during CMake configuration.
- Espressif RISC-V Toolchain
- esptool.py for flashing
- Build system: CMake with Make or Ninja
- Docker (optional, for containerized builds)
Some low-level components (including portions of HAL headers, register definitions, and peripheral structure declarations) are adapted from Espressif's ESP-IDF SDK. These were used as reference to avoid reverse engineering hardware register layouts and peripheral interfaces, while keeping this project independent of the ESP-IDF framework.
git clone --recurse-submodules https://github.com/pdlsurya/esp32-riscv-bare-metal-sdk.git
cd esp32-riscv-bare-metal-sdkIf you already cloned the repository without submodules, run:
git submodule update --init --recursivecd examples/uart_echo
./build.sh
cd ../blink_ws2812
./build.sh
cd ../esp_hosted
./build.shcd examples/uart_echo
./flash.shIf you want the previous one-command flow, use ./build_flash.sh in any example directory.
Container builds are optional. The regular host build flow still works, and Docker is intended as a reproducible alternative for onboarding and CI.
docker build -t esp32-rv-sdk -f docker/Dockerfile .
./scripts/docker-build.sh examples/uart_echo
./scripts/docker-build.sh examples/blink_ws2812
./scripts/docker-build.sh examples/esp_hostedOpen an interactive shell in the build container:
./scripts/docker-shell.shNotes:
- Docker support is aimed at building artifacts, not replacing host flashing.
./scripts/docker-build.shreuses the existing Docker image by default. SetREBUILD_IMAGE=1when you want to rebuild it after changing the Dockerfile or container dependencies../scripts/docker-build.sh ...binds the SDK source tree from the host and builds directly into the requested examplebuild/directory.- Flashing from the host is usually the simplest path, especially on macOS where USB passthrough into Docker can be unreliable.
- The Docker image currently defaults to the Espressif
riscv32-esp-elfesp-14.2.0_20251107Linux toolchain and can be overridden with Docker build arguments if you want to pin a different release.
External projects should point CMake at the SDK before calling project(...), just like the SDK examples do.
Minimal CMakeLists.txt pattern:
cmake_minimum_required(VERSION 3.13)
set(SDK_PATH "$ENV{SDK_PATH}" CACHE PATH "Path to esp32-rv-bare-metal-sdk")
if(SDK_PATH STREQUAL "")
set(SDK_PATH "/absolute/path/to/esp32-rv-bare-metal-sdk")
endif()
get_filename_component(SDK_PATH "${SDK_PATH}" ABSOLUTE)
set(TARGET_SOC "esp32c6" CACHE STRING "Target SOC")
set(CMAKE_TOOLCHAIN_FILE "${SDK_PATH}/cmake/toolchain-${TARGET_SOC}.cmake" CACHE FILEPATH "SDK toolchain file")
project(my_app LANGUAGES C CXX ASM)
include(${SDK_PATH}/cmake/sdk-utils.cmake)
add_executable(app.elf source/main.c)
sdk_config(app.elf)
add_extra_outputs(app.elf)Build an external project on the host:
cmake -S . -B build -GNinja -DSDK_PATH=/absolute/path/to/esp32-rv-bare-metal-sdk
cmake --build buildBuild an external project with Docker:
docker run --rm -it \
-v /absolute/path/to/my-project:/work/app \
-v /absolute/path/to/esp32-rv-bare-metal-sdk:/work/sdk \
-w /work/app \
esp32-rv-sdk \
bash -lc 'cmake -S . -B build-docker -GNinja -DSDK_PATH=/work/sdk && cmake --build build-docker'Use a separate Docker build directory such as build-docker/ so container builds do not reuse a host-generated build/ cache with absolute host paths.
If your project depends on other local components such as an RTOS or board library, mount them as additional volumes and pass matching -D..._PATH=/work/... arguments during CMake configure. For example:
docker run --rm -it \
-v /absolute/path/to/my-project:/work/app \
-v /absolute/path/to/esp32-rv-bare-metal-sdk:/work/sdk \
-v /absolute/path/to/sanoRTOS:/work/sanoRTOS \
-w /work/app \
esp32-rv-sdk \
bash -lc 'cmake -S . -B build-docker -GNinja -DSDK_PATH=/work/sdk -DSANORTOS_PATH=/work/sanoRTOS && cmake --build build-docker'