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-essential` on Debian/Ubuntu, or equivalent). --- ## 2. Install the Rust + Espressif toolchain ```bash # 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: ```bash 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. ```bash 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: ```toml [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: ```toml [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) ```bash 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: ```bash 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): ```bash 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 ` / `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: ```rust 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 ```bash cargo install espflash # one-time, if not already installed cargo build --release espflash flash --monitor target/xtensa-esp32s3-espidf/release/ ``` If Linux user needs permission for port access: ```shell 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|