ESP32-P4 + C6 Wi-Fi: When esp_wifi_set_ps() Does Nothing and Your First Packet Takes 1 Second

On the ESP32-P4-WIFI6 the radio lives on a separate C6 chip over SDIO, and the P4 cannot switch off its power saving. Here is how to get 10 ms UDP latency anyway.

After moving the robot’s joystick channel from MQTT to direct UDP over the local network, the round-trip time looked great while the stick was moving: consistently around 10 ms. Then I let go of the stick for a few seconds, pushed it again, and the first packet took anywhere from 100 ms to a full second to be acknowledged.

Steady-state fast, first-packet slow, is a signature. It means something on the path goes to sleep when idle.

The usual fix, and why it did not work here

On a plain ESP32 or ESP32-S3 you would call:

esp_wifi_set_ps(WIFI_PS_NONE);

and the station stops entering DTIM power-save. I added it. Nothing changed.

The reason is architectural. The ESP32-P4 has no radio of its own. On the Waveshare ESP32-P4-WIFI6 board, Wi-Fi is provided by an on-board ESP32-C6 connected over SDIO, running its own firmware and exposing the connection to the P4 through Espressif’s esp_wifi_remote / esp_hosted layer. The P4-side esp_wifi_set_ps() call is forwarded across that link, but the C6’s hosted firmware does not honour it — the modem keeps its default power-save behaviour.

Two smaller things worth knowing while you are here:

App joystick / ping 20–25 Hz IPv6 UDP ESP32-C6 Wi-Fi 6 radio + MAC esp_hosted firmware power-save policy decided here SDIO ESP32-P4 application · no radio esp_wifi_set_ps(WIFI_PS_NONE) returns ESP_OK … after got-IP forwarded over SDIO, not honoured Idle link → C6 sleeps → first packet 100–1000 ms. Keep it busy: ping at 20 Hz when the stick is centred → P50 = 10 ms.
The radio and its power-save policy live on the C6. The P4's request crosses the SDIO link and is ignored.

If you cannot turn off power saving, you can prevent the condition that triggers it: idleness.

The control protocol already required the app to stream joystick packets at 20-25 Hz while the stick is deflected, because the firmware brakes if it hears nothing for 300 ms. The change was to extend that rule to the idle case:

While a control session is open, the app never lets the link go quiet. Stick centred means send a lightweight ping at 20 Hz; the firmware answers with a status packet.

That keeps the C6 out of its sleep state without any firmware changes on the radio side. The ping reply doubles as an RTT probe and a liveness check, so the app can show connection quality for free.

Result after the change: P50 round-trip 10 ms, first-packet penalty gone.

Cost

A 20 Hz ping of ~60 bytes is about 1.2 KB/s each way. On a local network that is nothing; on a metered link you would want to think about it. The C6 also stays awake, which costs some tens of milliamps — irrelevant on a robot whose motors draw amps, worth accounting for on a battery sensor node.

The general lesson

Radio power management is often not under the control of the side you are writing code for. Hosted-Wi-Fi designs (P4 + C6, or any MCU with an external network co-processor) put the modem’s policy in the co-processor’s firmware. Before fighting that policy through an API that may or may not be forwarded, ask whether the traffic pattern can simply avoid triggering it. Here it could, and the fix took ten lines in the app instead of a custom C6 firmware build.