A comprehensive template for ESP32-C3 embedded development in Rust with modular examples from basic GPIO to WiFi connectivity.
- Bare metal development using esp-hal only (no esp-idf or RTOS overhead)
- Modular project structure with progressive complexity levels
- Direct console output via RTT - no helper scripts needed
- Container-based development with full USB hardware access
- WiFi connectivity module with DHCP and network stack
- Clean modular architecture ready for MQTT and IoT applications
- Comprehensive documentation with usage examples and troubleshooting
- ESP32-C3 development board (ESP32-C3-DevKitC-02 or compatible)
- USB cable for programming and power
- Remote or Local Linux Server with Podman/Docker installed (from now we will call it HOST)
- Rust toolchain (will be installed in container)
- probe-rs for flashing (will be installed in container)
- VSCode with Remote-SSH extension (for IDE development)
This template focuses on bare metal development using only esp-hal (Hardware Abstraction Layer) for maximum performance and minimal resource usage:
- No esp-idf: Avoids the heavy ESP-IDF framework and its overhead
- No RTOS: Direct hardware control without real-time operating system complexity
- Pure Rust: Leverages Rust's zero-cost abstractions and memory safety
- Minimal dependencies: Only essential crates for hardware interaction
- Fast builds: Reduced compilation time compared to esp-idf solutions
- Small binaries: Optimized for embedded constraints and flash usage
This approach is ideal for:
- IoT devices requiring minimal power consumption
- Real-time applications needing predictable performance
- Learning embedded Rust without framework abstractions
- Projects where every byte of memory and CPU cycle matters
Basic ESP32-C3 LED control and RTT console
- Hardware verification and GPIO control
- RTT debugging setup
- Perfect starting point for new projects
Modular WiFi connectivity with network stack
- DHCP IP acquisition with real IP display
- Network monitoring and status reporting
- Ready for MQTT and TCP applications
- Clean modular architecture without complex lifetimes
# Clone this template (on the HOST)
git clone https://github.com/mvcorrea-ufc/rust-esp32-tmpl.git my-esp32c3-project
cd my-esp32c3-project
# Initialize as new repository
rm -rf .git
git init
git add .
git commit -m "Initial ESP32-C3 project from template"On HOST server (always where you got podman/docker installed):
# Navigate to project root directory (where Dockerfile and podman-compose.yml are)
cd my-esp32c3-project
# Build and start development container (or use docker-compose with '-f podman-compose.yml')
podman-compose up --build -d
# Then you should have your container up and running
# To connect to it you could
podman-compose exec esp32dev bash
or via ssh
ssh root@HOST -p2222 (where HOST is the place podman/docker is installed)
# Verify ESP32-C3 detection
podman-compose exec esp32dev probe-rs list
# Should show: ESP JTAG -- 303a:1001:F0:F5:BD:C9:4A:90 (EspJtag)
# from time to time we loose the /dev/tty* access, then you should restart the container 'podman-compose restart'# Enter development container
podman-compose exec esp32dev bash
# Navigate to blinky module
cd /workspace/blinky
# Build and run basic LED example
cargo run --releaseExpected Output:
10:02:47.191: esp32-c3 is booting!
10:02:47.703: status: High
10:02:48.216: status: Low
10:02:48.728: status: High
...
# Enter development container
podman-compose exec esp32dev bash
# Configure WiFi credentials first
cd /workspace/wifi-simple
# Edit .cargo/config.toml with your WiFi network details
# Build and run WiFi example
cargo run --releaseExpected Output:
ESP32-C3 WiFi Simple - Clean Modular Implementation
Config: SSID=YourNetwork Hostname=ESP32-C3-WiFi-Test
WiFi: Scanning for networks...
WiFi: Connected successfully
DHCP: IP address acquired successfully
Network: IP=192.168.1.100 Gateway=192.168.1.1 Subnet=/24 DNS=Some(8.8.8.8)
NETWORK READY
Status: CONNECTED IP=192.168.1.100 GW=192.168.1.1
rust-esp32-tmpl/
├── .vscode/ # VSCode configuration
│ ├── settings.json # Rust-analyzer and editor settings
│ ├── extensions.json # Recommended extensions
│ ├── launch.json # Debug configurations
│ └── tasks.json # Build and run tasks
├── workspace/ # Development modules (see workspace/README.md)
│ ├── blinky/ # Basic LED control example
│ │ ├── src/main.rs # GPIO control with RTT console
│ │ ├── Cargo.toml # Basic ESP-HAL dependencies
│ │ ├── build.rs # Linker configuration
│ │ └── README.md # Module documentation
│ ├── wifi-simple/ # WiFi connectivity module
│ │ ├── src/
│ │ │ ├── main.rs # WiFi application example
│ │ │ └── wifi.rs # Modular WiFi helper functions
│ │ ├── .cargo/
│ │ │ └── config.toml # WiFi credentials configuration
│ │ ├── Cargo.toml # WiFi and networking dependencies
│ │ ├── build.rs # Linker configuration
│ │ └── README.md # WiFi module documentation
│ ├── Cargo.toml # Workspace configuration
│ ├── rust-toolchain.toml # Rust toolchain specification
│ └── README.md # Workspace and module overview
├── Dockerfile # Container build configuration
├── podman-compose.yml # Container orchestration
└── README.md # This file
use rtt_target::{rprintln, rtt_init_print};
#[main]
fn main() -> ! {
// Initialize RTT for console output
rtt_init_print!();
// Your application code...
rprintln!("Hello ESP32-C3!");
}[dependencies]
esp-hal = { version = "0.23.1", features = ["esp32c3"] }
rtt-target = "0.5"
panic-rtt-target = "0.1"[target.riscv32imc-unknown-none-elf]
runner = "probe-rs run --chip=esp32c3 --preverify --always-print-stacktrace --no-location --catch-hardfault"# Debug build (faster compilation, larger binary)
cargo build
# Release build (optimized for size/performance)
cargo build --release
# Clean build artifacts
cargo clean# Flash and run with RTT console output
cargo run --release
# Just flash without console (background)
probe-rs run --chip=esp32c3 target/riscv32imc-unknown-none-elf/release/blinky# List connected ESP32 devices
probe-rs list
# Check if RTT symbols are compiled into binary
cargo objdump --release -- -s | grep SEGGER
# Attach to running target for RTT debugging
probe-rs attach --chip esp32c3 target/riscv32imc-unknown-none-elf/release/blinky# Edit source code
vim src/main.rs
# Build and test
cargo run --release
# Ctrl+C to stop and flash new changes# Add new crate to Cargo.toml
[dependencies]
serde = { version = "1.0", default-features = false }
# Update and build
cargo build// Configure different GPIO pin
let mut led = Output::new(peripherals.GPIO2, Level::Low, OutputConfig::default());
// Use different delay timing
delay.delay_millis(1000); // 1 second# Restart container to refresh USB connections
podman-compose down && podman-compose up -d
# Verify device detection
podman-compose exec esp32dev probe-rs list
# Check USB device mapping in host
lsusb | grep Espressif# Clean and rebuild
cargo clean
cargo build --release
# Update dependencies
cargo update
# Check toolchain
rustc --version --verbose# Verify RTT is initialized
grep -n "rtt_init_print" src/main.rs
# Check for SEGGER symbols in binary
cargo objdump --release -- -s | grep SEGGER
# Test with simple rprintln
rprintln!("Debug: RTT working!");# Verify container is running
podman-compose ps
# Check SSH access
ssh root@HOST -p 2222 # password: rootpass
# Restart if needed
podman-compose restart esp32dev// In main.rs, modify GPIO pin
let mut led = Output::new(peripherals.GPIO10, Level::Low, OutputConfig::default());use esp_hal::{
gpio::{Level, Output, OutputConfig},
i2c::I2C,
spi::Spi,
};
// Configure I2C, SPI, etc.// Format messages
rprintln!("Temperature: {:.2}°C", temp);
// Debug with multiple values
rprintln!("GPIO: {} | Counter: {}", pin_state, counter);
// Conditional output
if error_condition {
rprintln!("ERROR: {}", error_msg);
}- VSCode installed on your local machine
- Remote-SSH extension installed in VSCode
- SSH access to your remote server (HOST)
Add to your local ~/.ssh/config:
Host esp32-dev
HostName <HOST IP>
Port 2222
User root
PasswordAuthentication yes
StrictHostKeyChecking no
UserKnownHostsFile /dev/null
- Open VSCode on your local machine
- Press
Ctrl+Shift+P(orCmd+Shift+Pon Mac) - Type "Remote-SSH: Connect to Host"
- Select
esp32-devfrom the list - Enter password when prompted:
rootpass - Open folder
/workspacein the remote VSCode window
VSCode will automatically suggest installing the recommended extensions defined in .vscode/extensions.json:
- rust-analyzer - Rust language support
- probe-rs-debugger - ESP32 debugging support
- Embedded Tools - Additional embedded development tools
- Hex Editor - For binary file inspection
- Build Debug:
Ctrl+Shift+P→ "Tasks: Run Task" → "cargo-build-debug" - Build Release:
Ctrl+Shift+P→ "Tasks: Run Task" → "cargo-build-release" - Flash and Run:
Ctrl+Shift+P→ "Tasks: Run Task" → "cargo-run-release" - Clean:
Ctrl+Shift+P→ "Tasks: Run Task" → "cargo-clean"
- Set breakpoints in
workspace/blinky/src/main.rs - Press
F5or go to Run and Debug panel - Select "ESP32-C3 Debug" or "ESP32-C3 Release Debug"
- Debug session will start with probe-rs debugger
- Open Terminal: `Ctrl+`` (backtick)
- Navigate to project: Terminal opens in
/workspace/blinkyby default - Run commands: All cargo and probe-rs commands work directly
- Auto-completion for ESP-HAL functions
- Error highlighting and quick fixes
- Type hints and parameter suggestions
- Go to definition for ESP32-C3 peripherals
- Documentation on hover for HAL functions
- Device detection: Use Command Palette → "probe-rs-list"
- RTT output: Integrated console shows RTT messages during debugging
- Memory viewer: Inspect ESP32-C3 memory during debugging
- Register viewer: Monitor peripheral registers
- File Explorer: Navigate project structure
- Symbol search:
Ctrl+Shift+Ofor functions and types - Global search:
Ctrl+Shift+Facross entire codebase - Git integration: Built-in version control
# Test SSH connection manually
ssh root@HOST -p 2222
# Restart container if connection fails
podman-compose restart esp32dev- Rust-analyzer not working: Restart VSCode Remote session
- Debug not starting: Verify ESP32-C3 is connected with "probe-rs-list" task
- RTT not showing: Check that
rtt_init_print!()is called in main.rs
- Exclude target folder: Already configured in
.vscode/settings.json - Disable unused extensions: Only install recommended embedded extensions
- Use release builds: Debug builds are slower on ESP32-C3
If you prefer terminal-based development:
# SSH directly to container
ssh root@HOST -p 2222
# Use vim/nano for editing
cd /workspace/blinky
vim src/main.rs
# Build and run as normal
cargo run --releaseThe development environment uses a privileged Podman container with:
- USB device mapping for ESP32-C3 access
- SSH server on port 2222 (password:
rootpass) - Rust toolchain with RISC-V target
- probe-rs tools for flashing and debugging
- Git version control for project management
- Begin with blinky - Verify hardware and development environment
- Progress to wifi-simple - Add network connectivity to your application
- Combine functionality - Use wifi-simple as foundation for IoT projects
// MQTT Client: wifi-simple + rust-mqtt
// HTTP Client: wifi-simple + reqwest
// Sensor Network: blinky + wifi-simple + sensor libraries
// IoT Dashboard: wifi-simple + web server + sensor dataFor wifi-simple module, configure your network in .cargo/config.toml:
[env]
# WiFi credentials - Replace with your network details
WIFI_SSID = "YourNetworkName"
WIFI_PASSWORD = "YourNetworkPassword"
CARGO_CFG_PORTABLE_ATOMIC_UNSAFE_ASSUME_SINGLE_CORE = ""Important: Replace the placeholder values with your actual WiFi network credentials before building the wifi-simple module.
- Start with blinky - Verify your hardware setup works correctly
- Try wifi-simple - Add network connectivity to your projects
- Read module READMEs - Each module has detailed documentation
- Build MQTT applications - Use wifi-simple as foundation
- Add sensors - Integrate I2C/SPI sensors with existing modules
- Create IoT solutions - Combine modules for complete applications
This template is provided as-is for educational and development purposes.