10 KiB
End-to-end setup for a Waveshare ESP32-S3-ETH board (W5500 ethernet over SPI), using the std Rust toolchain (esp-idf-svc). By the end you'll have a project that connects to ethernet, gets a DHCP lease, and is structured to build real functionality on top of.
This walkthough was AI generated
0. Hardware reference
Confirm these against your board's silkscreen before wiring anything extra — Waveshare's pin assignments have varied slightly across production batches.
| Signal | GPIO |
|---|---|
| SCLK | 13 |
| MOSI (SDO) | 11 |
| MISO (SDI) | 12 |
| CS | 14 |
| INT | 10 |
| RST | 9 |
| SPI host | SPI2 |
| SPI clock | 20–25 MHz |
The W5500 has no burned-in MAC address — you supply one in software (any locally-administered address, e.g. 02:00:00:12:34:56, works).
1. Prerequisites
- USB-C cable, board connected to your dev machine.
- ~5GB free disk space (toolchain + ESP-IDF + submodules).
- Linux/macOS/WSL. (Windows native works too but paths below assume a POSIX shell.)
git, a C compiler,python3, and standard build tools already on the system (build-essentialon Debian/Ubuntu, or equivalent).
2. Install the Rust + Espressif toolchain
# Rust itself, if not already installed
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# espup: installs the Xtensa-patched Rust toolchain ESP32-S3 needs
cargo install espup
cargo install ldproxy
espup install
# Load the Xtensa toolchain into your shell (needed every new shell)
. $HOME/export-esp.sh
Add the . $HOME/export-esp.sh line to your shell rc file (.bashrc/.zshrc/config.fish) so you don't have to re-run it manually every session.
Verify the Xtensa target is present:
rustc --print target-list | grep xtensa
You should see xtensa-esp32s3-none-elf and similar in the output.
3. Scaffold the project
Use esp-idf-template, not esp-generate — the latter scaffolds no_std/esp-hal projects, which have no W5500/ethernet driver and no standard HTTP server story.
cargo install cargo-generate
cargo generate esp-rs/esp-idf-template cargo
You'll be prompted interactively. Answers for this board/use case:
| Prompt | Answer | Why |
|---|---|---|
| Project name | your choice | |
| MCU | esp32s3 |
matches this board |
| Configure advanced options? | Yes | need to reach the STD toggle below |
| STD support | Yes | pulls in esp-idf-svc; without this you're back on no_std |
| ESP-IDF version | v5.3.4 |
most stable match to current esp-idf-svc/hal crates; avoid master |
| Use git (not crates.io) esp-idf-* crates | false | crates.io releases are stable and documented; git HEAD drifts underneath you |
| Installation location of managed ESP-IDF | global | shared across projects, avoids re-downloading ~1–2GB per project |
| Configure Dev Containers? | false | flashing real hardware over USB is simpler done natively |
| Configure Wokwi simulation? | false | Wokwi doesn't model the W5500/ethernet peripheral |
| Add CI files? | false | add later if/when you push to GitHub |
This produces:
your-project/
├── .cargo/config.toml
├── Cargo.toml
├── build.rs
├── rust-toolchain.toml
├── sdkconfig.defaults
└── src/main.rs
4. Confirm the generated build target
Open .cargo/config.toml and confirm:
[build]
target = "xtensa-esp32s3-espidf"
If this is missing or wrong, cargo build will silently try to compile for your host machine instead of the chip and fail with Unsupported target 'x86_64-unknown-linux-gnu' (or similar). The template sets this correctly by default — just verify it.
5. Configure sdkconfig.defaults
Edit sdkconfig.defaults (created empty or near-empty by the template) to include:
CONFIG_ETH_SPI_ETHERNET_W5500=y
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8000
CONFIG_HTTPD_MAX_REQ_HDR_LEN=1024
The first line is required — without it, ESP-IDF doesn't build the W5500 driver component in at all, and your ethernet code will fail to link or fail at runtime. The other two are reasonable general-purpose values (headroom for the main task stack, generous HTTP header limit if you add a server later).
6. Configure Cargo.toml
Replace the [dependencies] section with at minimum:
[dependencies]
esp-idf-svc = { version = "0.51", features = ["std"] }
anyhow = "1"
log = "0.4"
Add more crates as your project needs them — see the companion note on porting non-embedded dependencies if you're bringing code over from another platform.
7. First build (this installs ESP-IDF itself)
cargo build --release
The first build does a lot of one-time work: downloads ESP-IDF, sets up a Python virtualenv, installs xtensa-esp-elf, esp-clang, cmake, ninja, and more. Expect this to take 10–20+ minutes and several GB of downloads. Subsequent builds are fast.
Known first-build failure points
These are common enough to check for proactively rather than debug blind:
a) esp-clang: error while loading shared libraries: libxml2.so.2 Rolling-release Linux distros (Arch-based, etc.) ship a newer libxml2 soname than the pinned esp-clang binary expects. Fix:
ldconfig -p | grep libxml2 # find your installed version, e.g. libxml2.so.16
sudo ln -s /usr/lib/libxml2.so.16 /usr/lib/libxml2.so.2 # adjust path/version to match
sudo ldconfig
ls -la /usr/lib/libxml2.so.2
The final command should show a symlink of libxml2.so.2 to libxml2.so.16
b) Missing esp-mqtt submodule / CMake git_submodule_check errors The managed ESP-IDF checkout needs its git submodules initialized (several components are submodules even if your project doesn't use them):
cd .embuild/espressif/esp-idf/v5.3.4
git submodule update --init --recursive
cd -
cargo build --release
If that's still broken, delete and let it re-clone fresh: rm -rf .embuild/espressif/esp-idf/v5.3.4.
A successful build ends with a normal Compiling <your-project> / Finished release output.
8. Write a minimal ethernet-only main.rs
This is the "ready for proper code" checkpoint — confirms SPI, the W5500, and DHCP all work before you build real functionality on top:
use std::thread;
use std::time::Duration;
use esp_idf_svc::eth::{BlockingEth, EspEth, EthDriver, SpiEthChipset};
use esp_idf_svc::eventloop::EspSystemEventLoop;
use esp_idf_svc::hal::peripherals::Peripherals;
use esp_idf_svc::hal::prelude::*;
use esp_idf_svc::hal::spi::{config::DriverConfig, Dma, SpiDriver};
use log::info;
fn main() -> anyhow::Result<()> {
esp_idf_svc::sys::link_patches();
esp_idf_svc::log::EspLogger::initialize_default();
let peripherals = Peripherals::take()?;
let sysloop = EspSystemEventLoop::take()?;
let pins = peripherals.pins;
let spi_driver = SpiDriver::new(
peripherals.spi2,
pins.gpio13, // SCLK
pins.gpio11, // MOSI
Some(pins.gpio12), // MISO
&DriverConfig {
dma: Dma::Auto(4096), // required — default (no DMA) can't move full ethernet frames
..Default::default()
},
)?;
let eth_driver = EthDriver::new_spi(
spi_driver,
pins.gpio10, // INT
Some(pins.gpio14), // CS
Some(pins.gpio9), // RST
SpiEthChipset::W5500,
20.MHz().into(),
Some(&[0x02, 0x00, 0x00, 0x12, 0x34, 0x56]), // locally-administered MAC
None, // default PHY address
sysloop.clone(),
)?;
let eth = EspEth::wrap(eth_driver)?;
let mut eth = BlockingEth::wrap(eth, sysloop)?;
info!("Starting eth...");
eth.start()?;
info!("Waiting for DHCP lease...");
eth.wait_netif_up()?;
let ip_info = eth.eth().netif().get_ip_info()?;
info!("Got IP: {:?}", ip_info);
loop {
thread::sleep(Duration::from_secs(1));
}
}
9. Flash and verify
cargo install espflash # one-time, if not already installed
cargo build --release
espflash flash --monitor target/xtensa-esp32s3-espidf/release/<your-project-name>
If Linux user needs permission for port access:
sudo usermod -aG uucp $USER # Arch Systems
sudo usermod -aG dialout $USER # Debian Systems
Then reboot.
Watch the serial monitor. Success looks like:
I (...) esp_idf_svc::eth: Driver initialized
I (...) hello_eth: Starting eth...
I (...) hello_eth: Waiting for DHCP lease...
I (...) hello_eth: Got IP: ...
If it hangs after "Waiting for DHCP lease..." with repeating w5500.mac: w5500_spi_write failed / w5500_spi_read failed errors in the log, the SPI bus doesn't have DMA enabled — double check the Dma::Auto(4096) line in step 8 is present.
If it hangs with no errors at all and no IP, check physically: ethernet cable plugged into a live switch/router port, link LED on the RJ45 jack lit.
10. You're ready to build on this
At this point you have a project that: builds for the correct target, links the W5500 driver, brings up ethernet, and gets a DHCP lease. From here, typical next additions:
- An HTTP server (
esp_idf_svc::http::server::EspHttpServer) - GPIO for LEDs/buttons/sensors (
esp_idf_svc::hal::gpio::PinDriver) - SPI/I2C peripherals sharing the bus alongside the W5500 (own CS pin, same or different SPI host)
- Async networking (
tokio,async-nats, etc.) — needs additional rustflags and feature trimming; see the note on porting non-embedded async code if relevant to your project
Quick-reference: build error → fix
| Error | Cause | Fix |
|---|---|---|
Unsupported target 'x86_64-unknown-linux-gnu' |
.cargo/config.toml target missing/wrong, or Xtensa toolchain not loaded in shell |
Check target = "xtensa-esp32s3-espidf"; re-run . $HOME/export-esp.sh |
esp-clang: ... libxml2.so.2 |
Host libxml2 soname mismatch | Symlink to your installed version (§7a) |
Missing esp-mqtt submodule / CMake errors |
Managed ESP-IDF checkout missing submodules | git submodule update --init --recursive in the IDF checkout (§7b) |
txdata transfer > host maximum / w5500 read/write failures |
SPI DMA disabled | Dma::Auto(4096) in DriverConfig |
EthDriver::new_spi argument/type errors |
API differs across esp-idf-svc versions | Match the compiler's reported signature exactly, not older examples/tutorials |
| No W5500 driver at runtime | CONFIG_ETH_SPI_ETHERNET_W5500=y missing from sdkconfig.defaults |
Add it, rebuild |