Root cause (verified against our exact IDF tree, not the community guess): the "258" in "sdio_write_task: Failed to send data: 258" is NOT a timeout (that is 263). 258 = 0x102 = ESP_ERR_INVALID_ARG. On the ESP32-P4, block- mode CMD53 writes require the SOURCE buffer to be 64-byte (cache-line) aligned; the IDF sdmmc driver rejects a misaligned source with INVALID_ARG BEFORE any bus activity. esp_hosts write loop then declares "Unrecoverable host sdio state" and reboots the whole P4. The audio TX payload is not 64-aligned, so streaming mic audio wedged on the very FIRST frame (which is exactly what we saw: listening -> instant Failed to send -> reboot). This also explains why buffer/queue/clock/retry tuning all did nothing: the write never reached the bus. And why our symptom was instant, not after ~100 writes (the community block-mode-desync theory) — it is the first misaligned buffer, every time. Fix: vendored esp_hosted 2.12.11 as an editable local component (overrides the registry copy) and bounce a misaligned TX payload through one aligned DMA scratch buffer in hosted_sdio_write_block (port_esp_hosted_host_sdio.c). TX is serialized by the bus lock so a single static bounce buffer is safe; freed in hosted_sdio_deinit. Host-only change — no C6 reflash. VERIFIED ON HARDWARE (autonomous self-test): 40s of continuous mic-audio upstream streaming — the traffic that previously wedged on the first frame — ran clean, zero timeouts, zero reboots. A guarded SDIO_TX_SELFTEST harness is kept (compiled out) for future SDIO stress testing. Credit: root cause + patch designed via multi-agent investigation; the precise 258=INVALID_ARG decode (correcting the upstream community timeout assumption) came from checking our actual esp_err.h. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
6.7 KiB
Network Split Feature for ESP-Hosted MCU
| Supported Slave Targets | ESP32-C5 | ESP32-C6 | ESP32-S2 | ESP32-S3 |
|---|
Overview
Network Split allows the Host MCU and ESP32 Slave to share one IP address and split traffic between them.
It is especially useful when the host sleeps — the ESP continues handling selected network activity (e.g., MQTT, DNS).
Highlights:
- Port-based traffic routing
- Shared IP address
- Packet filtering and DPI
- Wake-on-packet support (e.g., MQTT)
- Power management integration
- Supported Slave Targets: ESP32-C5/C6/S2/S3 only
Configuration
On the Slave
-
Run:
idf.py menuconfig -
Enable:
Example Configuration └── [*] Enable Network Split -
(Optional) Customize under
Network Split Configuration:├── Host Static Port Forwarding │ ├── TCP dst: 22,80,443,8080,8554 │ └── UDP dst: 53,123 ├── Port Ranges │ ├── Host: 49152–61439 │ └── Slave: 61440–65535 └── Default Destination: slave / host / both
On the Host
-
Run:
idf.py menuconfig -
Enable:
Component config └── ESP-Hosted config └── [*] Enable Network Split -
Match port ranges with the slave:
└── LWIP port config ├── Host LWIP: 49152–61439 └── Slave LWIP: 61440–65535 -
Integrate into your app:
#include "esp_hosted.h" void app_main(void) { esp_hosted_init(); // Host now shares IP and splits traffic with slave }
Tip
The port ranges on the host and slave must match perfectly to avoid routing issues.
Packet Routing Decisions
All incoming network packets arriving at the slave device are evaluated by the logic in nw_split_router.c. For each packet received from the WiFi router, the slave determines whether it should be processed locally, forwarded to the host, or handled by both network stacks, according to defined routing rules.
| Packet Type | Destination Port Condition | Routed To |
|---|---|---|
| Broadcast, ARP Request, ICMP Request | N/A | Slave Network Stack |
| ARP Response, ICMP Response | N/A | Both Network Stacks |
| DHCP | Any | Both Network Stacks |
| TCP/UDP | Listed in Static Port Forwarding | Host Network Stack |
| TCP/UDP | Within Host Port Range | Host Network Stack |
| TCP/UDP | Within Slave Port Range | Slave Network Stack |
| TCP/UDP | Port 5001 (iperf) | Both Network Stacks |
| MQTT (Port 1883) | Payload contains "wakeup-host" |
Host Network Stack (Wake-up) |
| Others | Not matched by any rule | Default Destination (as configured) |
| Packet destined for Host Network Stack | Host is in deep sleep | Dropped (unless wake-up packet) |
Tip
The packet routing logic is fully customizable within the
nw_split_router.cfile, allowing users to adapt the network management behavior to their specific requirements.
The following diagrams illustrate the decision-making process for packet routing in the Network Split feature.
flowchart TD
A[New Packet] --> B{MAC Broadcast?}
B -->|Yes| C[Send to<br/>Slave Network Stack]
B -->|No| D{IP Packet?}
D -->|Yes| E[Protocol Processing]
D -->|No| F[ARP Processing]
E --> G{TCP?}
E --> H{UDP?}
E --> I{ICMP?}
classDef slaveNode fill:#d9f7be,stroke:#389e0d
classDef hostNode fill:#d6e4ff,stroke:#1d39c4
classDef invalidNode fill:#ffccc7,stroke:#cf1322
classDef bothNode fill:#fff1b8,stroke:#d4b106
class C slaveNode
TCP/UDP Packet Processing
Specific case of UDP/TCP packet processing is illustrated below
flowchart TD
G[TCP/UDP Packet] --> G1{Port config in <br/>Host Static Forwarding List?<br/>}
G1 -->|Yes| K[Send to<br/>Host Network Stack]
G1 -->|No| G2{Dest Port in<br/>Slave Range?}
G2 -->|Yes| C[Send to<br/>Slave Network Stack]
G2 -->|No| G3{Dest Port in<br/>Host Range?}
G3 -->|Yes| G4{Host Awake?}
G4 -->|Yes| K
G4 -->|No| G5{MQTT with<br/>wakeup signal?}
G5 -->|Yes| K
G5 -->|No| L[Drop Packet]
G3 -->|No| D[Send to<br/>Default Network Stack<br/>from config]
classDef slaveNode fill:#d9f7be,stroke:#389e0d
classDef hostNode fill:#d6e4ff,stroke:#1d39c4
classDef invalidNode fill:#ffccc7,stroke:#cf1322
classDef defaultNode fill:#fff,stroke:#333
class K hostNode
class C slaveNode
class L invalidNode
class D defaultNode
Internals
Core Routing API
// nw_split_router.c
hosted_l2_bridge nw_split_filter_and_route_packet(void *frame_data, uint16_t frame_length);
typedef enum {
SLAVE_LWIP_BRIDGE = 0,
HOST_LWIP_BRIDGE = 1,
BOTH_LWIP_BRIDGE = 2,
INVALID_BRIDGE = 3
} hosted_l2_bridge;
Wake-Up on MQTT
static bool host_mqtt_wakeup_triggered(const void *payload, uint16_t length) {
return memcmp(payload, "wakeup-host", strlen("wakeup-host")) == 0;
}
Extras
Use Same Port on Both Sides?
Yes — port 5001 (iperf) is a working example. Others can be added similarly in nw_split_router.c.
Files of Interest
| Purpose | File |
|---|---|
| Routing logic | slave/main/nw_split_router.c |
| Host-side config | host/api/include/esp_hosted_config.h |
| Slave control | slave/main/interface.h |
| API entry point | esp_hosted_init() |
Debugging & Tips
Checklist
- Port ranges must not overlap
- Logs from
nw_split_router.chelp debug routing - Static forwarding can override port ranges
- Only supported on ESP32-C5/C6/S2/S3
Debug Logging
Enable verbose logs in nw_split_router.c:
esp_log_level_set("nw_split_router", ESP_LOG_VERBOSE);
Wi-Fi sniffer to inspect raw traffic (if really needed)