How to Build a Dual-Ethernet Gateway with W5500 on Raspberry Pi Pico?
This project turns a Raspberry Pi Pico-class RP2040 design into a dual-port wired Ethernet board using two WIZnet W5500 controllers
Summary
This project turns a Raspberry Pi Pico-class RP2040 design into a dual-port wired Ethernet board using two WIZnet W5500 controllers, with one port intended for PoE-backed deployment. In the supplied Rust demo firmware, each W5500 becomes a separate SPI-attached Ethernet interface for an Embassy-based network stack, while the application demonstrates DHCP bring-up, TCP data handling across two ports, NTP time sync, and local status display on an SSD1306 OLED.

What the Project Does
The hardware is a Pi Pico dual-Ethernet board built around two W5500-based interfaces, one of which is intended for PoE-backed use. The demo firmware brings up both Ethernet ports, acquires network configuration, runs NTP on one interface, accepts TCP connections on the other, and forwards received data through a second TCP path while showing status on an SSD1306 display. That makes it a useful reference for small gateway or machine-networking designs, but the stronger takeaway is that it demonstrates a clean dual-W5500 software structure in Rust rather than a one-off board hack.
Where WIZnet Fits
The exact WIZnet part here is the W5500, used twice. In the firmware, each chip is treated as its own SPI Ethernet device with its own SPI peripheral, chip select, interrupt, reset line, MAC address, runner task, and network stack. The code exposes that separation clearly: init_ethernet_1() handles one W5500 path and init_ethernet_2() handles the other, so the dual-port architecture is explicit in both hardware binding and application structure.
This is also where the “easy in Rust” angle is justified. The repository uses embassy-net, embassy-net-wiznet, embassy-rp, and async Embassy tasks, so the programmer is mostly composing existing typed building blocks instead of hand-rolling duplicated driver state and scheduler plumbing. The embassy-net-wiznet crate documents that it integrates WIZnet SPI Ethernet chips with embassy-net in MACRAW mode, which fits the structure used in this project.
A fair comparison point is C. An equivalent C implementation is certainly possible, but this particular dual-interface pattern would usually be more tedious and more error-prone in C because you would typically manage more manual duplication around SPI device instances, initialization order, task or callback plumbing, static buffers, and ownership of shared state. The main claim here is not that “C cannot do it,” but that this repository shows how naturally Rust expresses two W5500s side by side. That conclusion is an inference from the structure of the codebase and the abstractions it depends on.
Implementation Notes
The easiest evidence is in demo-firmware/src/main.rs, where dual Ethernet bring-up is reduced to two direct calls:
let net_stack_1 = network::init_ethernet_1(r.ethernet_1, spawner).await;
let net_stack_2 = network::init_ethernet_2(r.ethernet_2, spawner).await;File: demo-firmware/src/main.rs
Why it matters: the project’s core idea is visible immediately. Two W5500-backed interfaces are brought up with symmetrical Rust APIs, and the rest of the application just assigns jobs to stack 1 and stack 2. That is exactly the kind of clarity that makes the Rust version feel easy to scale from one Ethernet chip to two.
The second important section is in demo-firmware/src/network.rs, where each W5500 path follows the same pattern:
let device = SpiDeviceWithConfig::new(spi, cs, w5500_spi_config());
let (device, runner) = embassy_net_wiznet::new(mac_addr, state, device, w5500_int, w5500_reset)
.await
.unwrap();File: demo-firmware/src/network.rs
Why it matters: this is the real integration point, and it stays compact even though the board has two Ethernet controllers. The code shows that once the SPI bus, chip select, interrupt, reset, and MAC address are defined for each port, the W5500-to-network-stack handoff is almost identical for both interfaces. That is the strongest code-level argument for saying this project demonstrates how easy dual-W5500 use can be in Rust.
Practical Tips / Pitfalls
Keep each W5500 on its own clearly defined resource set. This project assigns separate SPI instances, DMA channels, CS, INT, and RESET lines to each interface, which keeps the dual-port model simple and readable.
Reuse the same initialization pattern for both ports instead of inventing two different driver paths. The strength of this code is its symmetry.
Be precise about what the software stack is doing. This repository uses embassy-net-wiznet with embassy-net, so the design is best described as Rust-managed dual Ethernet built on W5500 hardware, not as a direct showcase of the W5500 hardware socket API.
Do not overstate the C comparison. A C version is feasible, but it would usually involve more boilerplate and more manual bookkeeping than the Rust version shown here. The article should present that as an engineering trade-off, not as an impossibility claim.
Use this project as a pattern for multi-port embedded networking. The dual-W5500 architecture is clearer than the application logic itself, which is why it works well as a technical reference article.
FAQ
Why use the W5500 for this project?
Because the goal is not just “Ethernet on RP2040,” but two independent wired Ethernet interfaces on a small MCU platform. The repository is explicitly built around “Pi Pico x 2 W5500 driven Ethernet interfaces,” and the code mirrors that hardware layout directly with one Rust initialization path per controller.
How does each W5500 connect to the platform?
Each W5500 is connected over SPI with dedicated CS, INT, and RESET signals. The first port uses SPI0 resources and the second uses SPI1 resources, so the board is wired and coded as two distinct Ethernet devices rather than one shared transport.
What role does the W5500 play in this specific codebase?
Each W5500 is the physical wired Ethernet interface for one network stack. The firmware initializes both, then uses one stack for NTP and the other for TCP listener tasks and message forwarding.
Can beginners follow this project?
A motivated beginner can learn from it, but it is better suited to someone already comfortable with Rust embedded development, Embassy tasks, SPI peripherals, and RP2040 resource mapping. The code is clean, though, which is exactly why it is a strong example of dual-W5500 integration in Rust.
How does this compare with writing the same design in C?
The same hardware can absolutely be driven from C, but this repository makes a strong case for Rust because the two-port structure stays compact and symmetrical. In C, an equivalent design would usually require more manual setup for duplicated driver instances, static memory layout, task integration, and shared-state handling. The practical claim is that Rust makes the two-W5500 architecture easier to express cleanly, not that C is incapable


