ESP-IDF, the official development framework for the ESP32 Series SoCs, supports integration of components written in C/C++ and Rust which is gaining traction for embedded development due to its safety features. This article outlines the project layout generated by using the esp-idf-template
utility and its support for using Rust in CMake-based ESP-IDF projects.
Note that this article assumes prior knolwedge in the ESP-IDF build system (CMake).
Follow all the steps in the CMake tutorial. With the steps outlined there, generate a project named test
. I.e.
cargo generate --vcs none --git https://github.com/esp-rs/esp-idf-template cmake --name test
Here's how your project directory might look after following the guide:
test/
|-- CMakeLists.txt
|-- main/
| |-- CMakeLists.txt
| |-- main.c
|-- sdkconfig
|-- components/
| |-- rust-test/
| |-- CMakeLists.txt
| |-- placeholder.c
| |-- build.rs
| |-- Cargo.toml
| |-- rust-toolchain.toml
| |-- src/
| |-- lib.rs
test
contains the main C code like any other ESP-IDF application.- An ESP-IDF component, with name
rust-test
is stored in a subdirectory namedcomponents
. - The Rust code is stored directly as part of the
rust-test
ESP-IDF component, i.e. therust-test
ESP-IDF component is simultaneously an ESP-IDF component as well as a Rust library crate.
The component can be uploaded later to Component Manager.
The CMake build for your rust-test
component would proceed as usual and will compile all C code in the component (which is not much - just an empty placeholder.c
file which needs to be there for the approach to work).
More importantly, components/rust-test/CMakeLists.txt
is setup by the generator in such a way, so as to also call the Rust standard build utility - cargo
- which would take care of compiling the Rust source code in your ESP-IDF component.
The Rust source code (or as it is named in the Rust world - the Rust crate) is setup - in components/rust-test/Cargo.toml
- to have the shape of a static library - just like any other ESP-IDF component. When the CMake build calls cargo
to compile your rust-test
Rust crate, you'll essentially end up with a static library named librust_test.a
, which is then linked to the rest of the ESP-IDF project by CMake.
We would be skipping over the project root (the test/
top-level directory), as it is pretty standard and there is nothing Rust-specific in it. All the interesting stuff happens in the components/rust-test
sub-folder. Before going into the details of each file, let's do the following separation:
|-- components/
| |-- rust-test/
| |-- CMakeLists.txt
| |-- placeholder.c
| ^^^ These two files are standard ESP-IDF CMake component build files, with CMakeLists.txt being slightly more complicated than usual.
| |
| |-- build.rs
| |-- Cargo.toml
| |-- rust-toolchain.toml
| |-- src/
| |-- lib.rs
| ^^^ All these other files are what typically constitutes the layout of a Rust library crate and they are NOT specifc to ESP-IDF.
| You can google any Rust source on the internet as to their purpose and meaning.
This file builds the /components/rust-test
ESP-IDF component. It is however more complex than usual, as it needs to setup quite a few things so that calling into Rust cargo
is running smoothly. The content of this file is the main boilerplate that the esp-idf-template
automates.
You can of course modify this file post-generation. The various variables and overall behavior of the file is documented with inline comments.
As mentioned previously, this is an empty C file which is only there so as the CMake->Cargo integration magic to work.
This file is a standard Rust build scriptlet. It would be empty if you have not opted into the "HAL" option (note that by default "HAL" is enabled).
If you have selected the "HAL" option, the build.rs
file will contain a call into the embuild
library that nakes sure that your component can properly link against - and in general - can "talk" to the Safe Rust bindings for ESP-IDF (the "HAL").
This is a pretty standard and mostly empty cargo
build manifest file, which sets up your Rust crate to be built as a Rust static libray.
If you have selected the "HAL" option when generating the project, the manifest would also list:
- a dependency to
esp-idf-svc
(i.e. the safe Rust wrappers for ESP-IDF); - a built-time dependency to
embuild
which helps the build integration between ESP-IDF and Rust to run smoothly); - the popular Rust
log
crate.
Post-generation, you can add/remove additional dependencies on Rust crates here as well as modify how Rust compiles your project (as in optimization level and others).
This file instructs cargo
what Rust compiler toolchain to use when building your component. It would list either the esp
toolchain (a Rust toolchain provided by Espressif that has extra support for the esp32 xtensa MCUs), or the standard Rust nightly
toolchain. The latter can only be used with Espressif RiscV MCUs.
The actual source code of your Rust library. You can extend here at will.
- If you encounter linker errors, you may need to update your Rust flags. For example, you might need to add the
-Zbuild-std-features=compiler-builtins-weak-intrinsics
flag toCARGO_BUILD_FLAGS
in yourCMakeLists.txt
.
Wokwi for Visual Studio Code provides a simulation solution for embedded and IoT system engineers. The extension integrates with your existing development environment, allowing you to simulate your projects directly from your code editor.
- Install VS Code
- Install the Wokwi plugin
- Activate Wokwi plugin - command palette, search for
Wokwi: Start Simulator
, select and activate the plugin using web browser
Create wokwi.toml
in the root of the project. The file contains references to BIN and ELF previously built by idf.py
.
[wokwi]
version = 1
elf = "build/test.elf"
firmware = "build/test.bin"
Create diagram.json
. The file contains board selected for the simulation. Here's the list of available boards.
{
"version": 1,
"author": "Espressif Systems",
"editor": "wokwi",
"parts": [ { "type": "wokwi-esp32-devkit-v1", "id": "esp", "top": 0, "left": 0, "attrs": {} } ],
"connections": [ [ "esp:TX0", "$serialMonitor:RX", "", [] ], [ "esp:RX0", "$serialMonitor:TX", "", [] ] ],
"dependencies": {}
}
Open VS Code, open command palette (CMD/Ctrl+Shift+P), search for Wokwi: Start Simulator
, select the option to start simulation.
Use Pause button to display state of pins.
The plugin auto-reload application if the binary was updated.
If we want to add CI/CD to our project, we can leverage the Wokwi CI. For example, the following YAML file will build and check that the generated project runs properly:
name: CI
on:
push:
pull_request:
workflow_dispatch:
env:
CARGO_TERM_COLOR: always
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ESP_TARGET: esp32
ESP_IDF_VERSION: v5.1
jobs:
build-check:
name: Checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup | Rust (RISC-V)
if: env.ESP_TARGET != 'esp32' && env.ESP_TARGET != 'esp32s2' && env.ESP_TARGET != 'esp32s3'
uses: dtolnay/rust-toolchain@v1
with:
target: riscv32imc-unknown-none-elf
toolchain: nightly
components: rust-src
- name: Setup | Rust (Xtensa)
if: env.ESP_TARGET == 'esp32' && env.ESP_TARGET == 'esp32s2' && env.ESP_TARGET == 'esp32s3'
uses: esp-rs/xtensa-toolchain@v1.5
with:
default: true
buildtargets: {{ env.ESP_TARGET }}
ldproxy: false
- uses: Swatinem/rust-cache@v2
- name: Setup | ESP-IDF
shell: bash
run: |
git clone -b {{ env.ESP_IDF_VERSION }} --shallow-submodules --single-branch --recursive https://github.com/espressif/esp-idf.git /home/runner/work/esp-idf
/home/runner/work/esp-idf/install.sh {{ env.ESP_TARGET }}
- name: Build project
shell: bash
run: |
. /home/runner/work/esp-idf/export.sh
idf.py set-target {{ env.ESP_TARGET }}
idf.py build
- name: Wokwi CI check
uses: wokwi/wokwi-ci-action@v1
with:
token: ${{ secrets.WOKWI_CLI_TOKEN }}
timeout: 10000
expect_text: 'Hello ESP-RS. https://github.com/esp-rs'
fail_text: 'Error'
Its important to note that we need to set the WOKWI_CLI_TOKEN
secret:
Also, the CI file needs to be modified if used for other targets.