ESP32 + W5500 Ethernet Loop Detector
Rust (no_std) network loop detection for ESP32 + W5500 Ethernet. Detects Layer 2 loops without STP using MACRAW socket probe frames. Japanese dev. 2025.
이 프로젝트는 ESP32와 WIZnet W5500 이더넷 컨트롤러 2개로 구성된 루프 사전 검지 세그먼트 체커입니다. 전체 코드를 Rust로 작성했습니다. 검사 대상 세그먼트에 가져가서 포트에 연결하기만 하면, 루프가 발생할지 여부를 공사 전에 100% 판정할 수 있습니다.
소스 코드: https://github.com/t13801206/esp32-loopchk
배경 및 개발 동기
왜 이 도구가 필요한가
L2 스위치의 루프는 발생한 순간부터 브로드캐스트 스톰을 일으키고, 수 초 만에 네트워크 전체를 마비시킵니다. 특히 다음과 같은 환경에서는 치명적입니다.
- STP(스패닝 트리 프로토콜)가 비활성화되어 있거나 없는 환경
- 구형 허브가 혼재하는 현장
- 공사 중이라 배선 실수가 발생하기 쉬운 환경
이 디바이스의 목표는 *"스톰이 발생한 후에 대처하는 것"*이 아닙니다. "스톰이 발생하기 전에 막는 것" 입니다.
개발 흐름
| 페이즈 | 내용 |
|---|---|
| Phase 1 | Arduino + Ethernet.h — ARP 기반 체커 (L3 계층 도통 확인) |
| Phase 2 | Rust + smoltcp + MACRAW — W5500 제어 PoC |
| Phase 3 | 현재 구현: w5500-hl의 고수준 UDP API 채택, MACRAW의 복잡성 제거 |
ARP 체커로 "사전 검사" 유효성을 확인한 후, DHCP도 IP 설계도 알 수 없는 혼돈의 현장에서도 사용할 수 있도록 L2 방식으로 재설계했습니다.
루프 검지 원리
┌─────────────┐
│ 스위치 / 허브 │
└───┬─────┬───┘
│ │
[Unit A] [Unit B]
169.254.1.1 169.254.1.2
port 8887 port 8888
│ │
│ UDP Bcast │
└────────┘
"CHECK_LOOP"
dst: 255.255.255.255:8888- Unit A(송신 측) 가 1초마다
255.255.255.255:8888으로 UDP 브로드캐스트를 전송합니다. 페이로드는 매직 바이트CHECK_LOOP(10바이트). - Unit B(수신 측) 는 UDP 포트 8888에서 수신 대기합니다.
- 루프 없음(정상): Unit A의 송신 프레임은 Unit B에 도달하지 않습니다 — 독립 세그먼트.
- 루프 있음: 브로드캐스트 프레임이 되돌아와 Unit B가
CHECK_LOOP를 수신합니다. 즉시 빨간 LED 점등(루프 경고). 케이블을 빼서 링크가 단절될 때까지 경고가 유지됩니다.
APIPA(Link-Local) 주소를 사용하는 이유
169.254.0.0/16은 IANA가 Link-Local용으로 예약한 주소 대역입니다. APIPA를 사용하면:
- IP 설계를 모르는 현장에서도 기존 호스트와 주소 충돌 없이 사용 가능
- DHCP 불필요
- L2 세그먼트만 연결되어 있으면 브로드캐스트 전달 가능
덕분에 네트워크의 IP 설계에 상관없이 어디서든 사용할 수 있습니다.
하드웨어
구성 부품
- MCU: ESP32
- 이더넷 컨트롤러: WIZnet W5500 × 2 (SPI)
- 표시: 녹색 LED(정상), 빨간 LED(루프 검지)
ESP32 핀 어사인
| ESP32 핀 | 신호 | 역할 |
|---|---|---|
| GPIO32 | RST | W5500 공통 하드웨어 리셋 (Low 유효) |
| GPIO14 | SCLK | SPI 클록 (2개 공유) |
| GPIO27 | MOSI | SPI 데이터 출력 (2개 공유) |
| GPIO33 | MISO | SPI 데이터 입력 (2개 공유) |
| GPIO25 | CS-A | Chip Select — Unit A (W5500 #1) |
| GPIO13 | CS-B | Chip Select — Unit B (W5500 #2) |
| GPIO18 | 녹색 LED | 정상 상태 표시 |
| GPIO19 | 빨간 LED | 루프 검지 경고 |
SPI 버스는 W5500 2개가 공유합니다. esp-idf-hal의 SpiDeviceDriver가 내부 Mutex로 배타 제어를 하기 때문에 멀티스레드 환경에서도 안전합니다.
네트워크 설정
| Unit A | Unit B | |
|---|---|---|
| IP 주소 | 169.254.1.1 | 169.254.1.2 |
| 서브넷 | 255.255.0.0 | 255.255.0.0 |
| MAC | 00:08:DC:01:00:01 | 00:08:DC:01:00:02 |
| UDP 소켓 | 송신 전용 (port 8887) | 수신 전용 (port 8888) |
| 브로드캐스트 대상 | 255.255.255.255:8888 | — |
SPI 주파수 설정 가이드
본 구현에서는 SPI를 20MHz(src/main.rs의 SPI_BAUDRATE_HZ)로 설정합니다.
| 환경 | 권장 주파수 |
|---|---|
| 표준 배선 | 20MHz (기본값) |
| 배선이 길거나 노이즈가 강한 환경 | 12MHz (안정성 우선) |
| 단거리 배선으로 안정적인 환경 | 26MHz 이상 (에러 로그 증가 시 되돌릴 것) |
실 운용에서는 송수신 에러율과 링크 안정성을 관찰하면서 단계적으로 최적화하는 것이 안전합니다.
LED 표시
| 상태 | 녹색 LED | 빨간 LED |
|---|---|---|
| 링크 업, 루프 없음(정상) | 점등 | 소등 |
| 루프 검지 | 소등 | 점등 (래치 유지) |
| 링크 업 대기 중 | 점멸 (양쪽 교대 500ms) | 점멸 (양쪽 교대 500ms) |
빨간 LED는 래치 방식입니다 — 루프가 한 번 검지되면, 물리적으로 케이블을 뽑아 링크가 단절될 때까지 경고가 유지됩니다. 순간적인 패킷을 놓치지 않기 위한 설계입니다.
기술적 설계 결정
✅ 구현한 것
w5500-hl의Udp트레이트(udp_bind/udp_send_to/udp_recv_from)를 이용한 UDP 브로드캐스트- APIPA 주소 대역 채택으로 기존 IP 체계에 무간섭
- PHY 링크 감시(
PHYCFGR레지스터)를 통한 링크 단절 감지 및 루프 플래그 자동 리셋 - 루프 검지 래치 — 순간적인 패킷을 놓치지 않도록, 물리 링크 단절까지 경고 지속
- 소켓 상태 확인(
sn_sr)과 자동 재바인드(udp_bind)를 통한 자기 복구
소켓 자동 복구(Rebind) 메커니즘
실 환경 검증에서, L2 스위치 측의 차단이나 링크 흔들림을 계기로 W5500의 UDP 소켓 상태가 Closed로 전이되는 케이스를 확인했습니다.
본 구현에서는 송신·수신 직전에 소켓 상태를 확인하고, Udp 이외의 상태인 경우 즉시 udp_bind를 재실행합니다.
sn_sr == Udp→ 그대로 송수신sn_sr != Udp→ 즉시 재바인드 시도- 재바인드 성공 →
INFO로그 - 재바인드 실패 →
WARN로그
- 재바인드 성공 →
이 자기 복구 메커니즘으로, 링크 복구 후에 수신이 멈춘 상태에서 자동으로 회복합니다.
❌ 의도적으로 채택하지 않은 것
| 접근법 | 채택하지 않은 이유 |
|---|---|
| MACRAW + smoltcp | Phase 2에서 이미 검증 완료. "스톰이 발생한 후에도 동작하는 견고성"은 본 도구의 요구사항 밖. 사전 검지에 특화하기 위해 w5500-hl의 고수준 UDP API로 충분하다고 판단. |
| DHCP 이용 | 미지의 세그먼트에서 기존 IP 체계를 오염·충돌시킬 위험을 없애기 위해 APIPA 채택. |
| STP 검출·제어 | 본 도구는 루프 존재 여부를 판정하기만 하는 안전 장치. 네트워크 제어는 수행하지 않는 설계. |
C언어(Arduino Ethernet.h) | Ethernet.h의 전역 상태 관리로는 W5500 2개를 안전하게 제어할 수 없음. Rust의 타입 안전 SPI 공유(SpiDeviceDriver + 내부 Mutex)로 오동작 없는 견고한 시스템 실현. |
⚠️ 사용상 주의: 본 도구는 루프 "사전" 검지용입니다. 이미 브로드캐스트 스톰이 발생 중인 환경에서는 수신 처리에 영향을 받을 수 있습니다.
왜 Rust인가?
Rust의 임베디드 에코시스템(no_std, embedded-hal)은 OS 없이도 메모리 안전성을 보장합니다. 이것이 의미하는 바는 다음과 같습니다.
- 버퍼 오버플로우와 use-after-free 버그를 컴파일 타임에 완전히 제거
- Rust의 소유권 모델 덕분에 두 개의 W5500이 SPI 버스를 안전하게 공유 가능
- C 기반 구현에서 흔히 발생하는 취약점 클래스 전체를 원천 차단
임베디드 이더넷 프로젝트에서 신뢰성이 중요한 경우, Rust는 점점 표준적인 선택이 되고 있습니다.
사용 라이브러리 및 크레이트
| 크레이트 | 버전 | 용도 |
|---|---|---|
log | 0.4 | Rust 표준 로그 파사드 (info! / warn! / debug! 매크로 제공) |
esp-idf-hal | 0.46 | SPI·GPIO 드라이버 (ESP-IDF HAL). SpiDeviceDriver로 타입 안전 버스 공유 |
esp-idf-svc | 0.52.1 | ESP-IDF 서비스 통합·로거 |
w5500-hl | 0.12 (feature: eh1) | W5500 고수준 드라이버. Udp 트레이트로 bind/send_to/recv_from 제공 |
w5500-ll | 0.13 (w5500-hl 의존) | W5500 레지스터 조작. Registers 트레이트, Eui48Addr, Ipv4Addr, PHY 설정 |
embuild | 0.33 | 빌드 스크립트 (ESP-IDF 자동 연동) |
Rust 툴체인: esp 채널 (Xtensa 대응 포크, 1.82 계열) — rust-toolchain.toml로 버전 고정.
빌드 및 플래시
사전 준비
# Rust ESP 툴체인 설치
rustup target add xtensa-esp32-espidf
# espflash 설치
cargo install cargo-espflash클론 및 빌드
git clone https://github.com/t13801206/esp32-loopchk
cd esp32-loopchk
# 개발 빌드 (디버그)
cargo run
# 릴리스 빌드 (사이즈 최적화, 본番 투입용)
cargo run --release타겟: xtensa-esp32-espidf (rust-toolchain.toml 및 .cargo/config.toml에 지정).
릴리스 프로파일은 Cargo.toml에서 opt-level = "s"(사이즈 최적화)로 설정되어 있습니다. 성능 우선으로 재조정할 경우 opt-level = 2 또는 3을 검토하세요.
동작 로그 예시
I (312) esp32_loopchk: === 루프 사전 검지 세그먼트 체커 시작 ===
I (318) esp32_loopchk: W5500 A OK IP=169.254.1.1 ver=0x04
I (324) esp32_loopchk: W5500 B OK IP=169.254.1.2 ver=0x04
I (330) esp32_loopchk: Unit A bind port=8887, Unit B bind port=8888
W (1412) esp32_loopchk: ⚠ 루프 검지! 송신 원: 169.254.1.1:8887로그 레벨 운용 가이드
| 레벨 | 출력되는 이벤트 |
|---|---|
INFO | 시작 완료, W5500 인식 성공, 링크 업, 루프 검지, 재바인드 성공 등 상태 변화 |
WARN | 링크 다운, 송수신 에러, 재바인드 실패 등 주의가 필요한 이상 |
DEBUG | 정상 감시 중의 송신 성공이나 비매직 수신 등 통상 운용에서는 불필요한 상세 정보 |
FAQ
Q. 네트워크 루프란 무엇이며 왜 위험한가요? L2 루프는 스위치 간에 STP가 없는 상태에서 두 개 이상의 활성 경로가 존재할 때 발생합니다. 브로드캐스트 프레임이 끝없이 순환하며 대역폭을 빠르게 포화시키고, 세그먼트 상의 모든 기기가 통신 불능 상태가 됩니다.
Q. STP를 사용하면 되지 않나요? STP는 관리형 스위치의 기능입니다. W5500은 단순한 이더넷 컨트롤러로, 기기 하나를 네트워크에 연결할 뿐이며 스위칭 패브릭이 없습니다. STP는 적용 불가합니다. 본 도구는 프로브 프레임을 이용해 엔드포인트 관점에서 루프를 검지합니다.
Q. C언어 대신 Rust로 작성한 이유는? Rust는 컴파일 타임에 메모리 안전성을 보장하여 버퍼 오버플로우와 use-after-free 버그를 방지합니다. 로우레벨 네트워크 프레임을 다루는 임베디드 코드에서 C 구현에 흔한 취약점 클래스 전체를 제거할 수 있습니다. 또한 Rust의 소유권 모델은 C로 올바르게 구현하기 어려운 멀티 디바이스 SPI 공유를 안전하게 실현합니다.
Q. 이미 스톰이 발생 중인 환경에서도 사용할 수 있나요? 본 도구는 연결 전 사전 검지용으로 설계되어 있습니다. 세그먼트에서 브로드캐스트 스톰이 이미 진행 중인 경우, 수신 성능에 영향이 있을 수 있습니다. 알 수 없는 세그먼트에 연결하기 전에 사용하세요.
Overview
"Before plugging into an unknown port, you want to know whether a loop exists." This is the tension every network administrator feels — and the problem this tool solves.
This project is a pre-emptive loop detection segment checker built with an ESP32 and two WIZnet W5500 Ethernet controllers, implemented entirely in Rust. Bring it to any target segment, plug it in, and it will definitively tell you whether a loop would occur — before you make the connection.
Source Code: https://github.com/t13801206/esp32-loopchk
Background & Motivation
Why This Tool Exists
A Layer 2 loop triggers a broadcast storm the instant it forms — within seconds, the entire network segment is paralyzed. This is especially catastrophic in environments where:
- STP (Spanning Tree Protocol) is disabled or absent
- Legacy hubs are mixed into the network
- Wiring errors are likely during construction or patching
The goal of this device is not "react after the storm" — it is "stop the storm before it starts."
Development Phases
| Phase | Description |
|---|---|
| Phase 1 | Arduino + Ethernet.h — ARP-based checker for Layer 3 connectivity verification |
| Phase 2 | Rust + smoltcp + MACRAW — W5500 control proof-of-concept |
| Phase 3 | Current implementation: adopted the high-level UDP API of w5500-hl, eliminating MACRAW complexity |
After validating the "pre-connection inspection" concept with the ARP checker, the approach was redesigned around Layer 2 to work even in chaotic environments where DHCP and IP addressing are unknown.
How Loop Detection Works
┌─────────────┐
│ Switch /jub │
└───┬─────┬───┘
│ │
[Unit A] [Unit B]
169.254.1.1 169.254.1.2
port 8887 port 8888
│ │
│ UDP Bcast │
└────────┘
"CHECK_LOOP"
dst: 255.255.255.255:8888- Unit A (transmitter) sends a UDP broadcast to
255.255.255.255:8888every second, with the magic payloadCHECK_LOOP(10 bytes). - Unit B (receiver) listens on UDP port 8888.
- No loop present (normal): Unit A's broadcast frame never reaches Unit B — they are on independent segments.
- Loop present: The broadcast frame bounces back and Unit B receives
CHECK_LOOP. The red LED illuminates immediately. The warning remains latched until the cable is physically disconnected.
Why APIPA (Link-Local) Addresses?
The 169.254.0.0/16 range is reserved by IANA for Link-Local use. By using APIPA addresses:
- No conflict with existing IP addressing schemes in unknown environments
- No DHCP required
- Broadcasts are delivered as long as the Layer 2 segment is connected
This makes the tool usable anywhere, regardless of the network's IP design.
Hardware
Components
- MCU: ESP32
- Ethernet Controllers: 2× WIZnet W5500 (SPI)
- Indicators: Green LED (normal), Red LED (loop detected)
ESP32 Pin Assignment
| ESP32 Pin | Signal | Role |
|---|---|---|
| GPIO32 | RST | W5500 shared hardware reset (active low) |
| GPIO14 | SCLK | SPI clock (shared by both W5500s) |
| GPIO27 | MOSI | SPI data out (shared) |
| GPIO33 | MISO | SPI data in (shared) |
| GPIO25 | CS-A | Chip Select — Unit A (W5500 #1) |
| GPIO13 | CS-B | Chip Select — Unit B (W5500 #2) |
| GPIO18 | Green LED | Normal status indicator |
| GPIO19 | Red LED | Loop detection warning |
Both W5500s share the SPI bus. esp-idf-hal's SpiDeviceDriver handles mutual exclusion with an internal Mutex, making it multi-thread safe.
Network Configuration
| Unit A | Unit B | |
|---|---|---|
| IP Address | 169.254.1.1 | 169.254.1.2 |
| Subnet | 255.255.0.0 | 255.255.0.0 |
| MAC | 00:08:DC:01:00:01 | 00:08:DC:01:00:02 |
| UDP Socket | TX only (port 8887) | RX only (port 8888) |
| Broadcast Target | 255.255.255.255:8888 | — |
SPI Frequency
The implementation uses 20 MHz (SPI_BAUDRATE_HZ in src/main.rs).
| Scenario | Recommended Frequency |
|---|---|
| Standard wiring | 20 MHz (default) |
| Long cables / noisy environment | 12 MHz (stability priority) |
| Short wiring, stable environment | 26 MHz+ (validate with error logs) |
Tune incrementally — monitor send/receive error rates and link stability in real environments.
LED Indicators
| State | Green LED | Red LED |
|---|---|---|
| Link up, no loop (normal) | ON | OFF |
| Loop detected | OFF | ON (latched) |
| Waiting for link up | Blinking (alternating 500ms) | Blinking (alternating 500ms) |
The red LED latches — once a loop is detected, the warning stays active until the physical link is broken. This ensures momentary packets are never missed.
Technical Design Decisions
✅ What Was Implemented
- UDP broadcast via
w5500-hlUdptrait (udp_bind/udp_send_to/udp_recv_from) - APIPA address range to avoid interfering with existing IP infrastructure
- PHY link monitoring via
PHYCFGRregister — detects link loss and auto-resets the loop flag - Loop detection latch — warning persists until physical link disconnect
- Socket health check (
sn_sr) with automatic rebind (udp_bind) for self-recovery
Socket Auto-Recovery (Rebind)
During real-world testing, it was observed that Layer 2 switch disconnection or link flapping can transition the W5500's UDP socket state to Closed.
This implementation checks socket state before every send/receive operation:
sn_sr == Udp→ proceed normallysn_sr != Udp→ attempt immediateudp_bind- Rebind success →
INFOlog - Rebind failure →
WARNlog
- Rebind success →
This self-recovery mechanism ensures the device resumes detection automatically after link restoration.
❌ Deliberate Design Exclusions
| Approach | Why It Was Not Used |
|---|---|
| MACRAW + smoltcp | Validated in Phase 2. Robustness "under an active storm" is not a requirement for a pre-emptive detection tool — w5500-hl's high-level UDP API is sufficient. |
| DHCP | Eliminated to avoid address conflicts or pollution in unknown IP environments. APIPA is used instead. |
| STP detection/control | This tool is a safety instrument that only judges loop presence. It does not control the network. |
C / Arduino Ethernet.h | Ethernet.h's global state management cannot safely drive two W5500s simultaneously. Rust's type-safe SPI sharing (SpiDeviceDriver + internal Mutex) provides a robust, bug-free system. |
⚠️ Usage Note: This tool is designed for pre-emptive loop detection. In an environment where a broadcast storm is already active, receive processing may be impacted.
Why Rust?
Rust's embedded ecosystem (no_std, embedded-hal) provides memory safety guarantees without an OS. This means:
- Buffer overflows and use-after-free bugs are eliminated at compile time
- Two W5500 instances can safely share an SPI bus through Rust's ownership model
- The result is reliable networking code without the vulnerability class common in C-based implementations
This is increasingly the standard for embedded Ethernet projects where reliability is critical.
Libraries & Crates
| Crate | Version | Purpose |
|---|---|---|
log | 0.4 | Standard Rust logging facade (info! / warn! / debug!) |
esp-idf-hal | 0.46 | SPI & GPIO drivers (ESP-IDF HAL). Type-safe bus sharing via SpiDeviceDriver |
esp-idf-svc | 0.52.1 | ESP-IDF service integration & logger |
w5500-hl | 0.12 (feature: eh1) | W5500 high-level driver. Udp trait for bind/send_to/recv_from |
w5500-ll | 0.13 (w5500-hl dep) | W5500 register access. Registers trait, Eui48Addr, Ipv4Addr, PHY config |
embuild | 0.33 | Build script for ESP-IDF auto-integration |
Rust Toolchain: esp channel (Xtensa-compatible fork, 1.82 series) — pinned via rust-toolchain.toml.
Build & Flash
Prerequisites
# Install Rust ESP toolchain rustup target add xtensa-esp32-espidf
# Install espflash cargo install cargo-espflashClone & Build
git clone https://github.com/t13801206/esp32-loopchk
cd esp32-loopchk
# Development build (debug) cargo run
# Release build (size-optimized, for production) cargo run --releaseTarget: xtensa-esp32-espidf (specified in rust-toolchain.toml and .cargo/config.toml).
The release profile is configured with opt-level = "s" (size optimization) in Cargo.toml. For performance-critical tuning, consider opt-level = 2 or 3.
Sample Log Output
I (312) esp32_loopchk: === Loop Pre-Detection Segment Checker Starting ===
I (318) esp32_loopchk: W5500 A OK IP=169.254.1.1 ver=0x04
I (324) esp32_loopchk: W5500 B OK IP=169.254.1.2 ver=0x04
I (330) esp32_loopchk: Unit A bind port=8887, Unit B bind port=8888
W (1412) esp32_loopchk: ⚠ Loop detected! Source: 169.254.1.1:8887Log Level Guide
| Level | Events |
|---|---|
INFO | Startup complete, W5500 recognized, link up, loop detected, rebind success |
WARN | Link down, send/receive errors, rebind failure |
DEBUG | Per-cycle TX success, non-magic RX — verbose detail not needed in normal operation |
FAQ
Q: What is a network loop and why is it harmful? A Layer 2 loop forms when two or more active paths exist between switches with no STP to block them. Broadcast frames circulate endlessly, rapidly saturating bandwidth and making all devices on the segment unreachable.
Q: Why not just use STP? STP is a managed switch feature. The W5500 is a simple Ethernet controller — it connects one device to the network and has no switching fabric. STP is not applicable. This tool detects loops from the endpoint's perspective using probe frames.
Q: Why write this in Rust instead of C? Rust provides compile-time memory safety that prevents buffer overflows and use-after-free bugs. For embedded code handling raw network frames, this eliminates an entire vulnerability class common in C implementations. Additionally, Rust's ownership model enables safe multi-device SPI sharing that is difficult to achieve correctly in C.
Q: Can this be used when a storm is already occurring? This tool is designed for pre-emptive detection — checking before you connect. If a broadcast storm is already active on the segment, receive performance may be affected. Use it before connecting to unknown segments.
