Files
escape_gallery_obsidian_vault/Projects/ESP32-S3-ETH/ESP32-S3-ETH Rust Project - Empty Directory to Ready-to-Code.md
T

10 KiB
Raw Blame History

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 2025 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

# 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 ~12GB 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 1020+ 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