ESP32-P4 GPIOs Stuck at 1.2 V: The Internal LDO Power Domain Trap

Four unrelated-looking motor and encoder bugs on an ESP32-P4 board turned out to be one missing line: GPIO45-54 sit on a power domain fed by the chip internal LDO, and nobody had turned it on.

This was the most expensive bug in the whole robot project, not because the fix was hard (it is one line), but because the symptoms were spread across four things that looked completely unrelated. If you are porting a board definition to the ESP32-P4 and some GPIOs behave strangely, read this before you re-check your wiring.

The symptoms

After wiring four N20 gear motors to two DRV8833 drivers and flashing the firmware:

My first theory was the obvious one: a mis-numbered header pin or a swapped jumper. I wrote up a multimeter checklist for probing each driver input. The measurement that came back broke the theory:

“Nothing is wired wrong, but GPIO46, 47 and 48 all read 1.2 V.”

Not 0 V. Not 3.3 V. 1.2 V.

Root cause

On the ESP32-P4, GPIO45 through GPIO54 belong to two separate I/O power domains (VDDPST_5 / VDDPST_6). On the Waveshare ESP32-P4-WIFI6 board those domains are supplied by channel 4 of the chip’s internal LDO (VO4) rather than from the board’s 3.3 V rail directly.

If the firmware never enables that LDO channel, the entire domain floats at roughly 1.2 V. Outputs cannot drive high; input buffers are running under-voltage and return noise.

Other P4 board definitions in the same code base (the wireless-tag boards, for example) call esp_ldo_acquire_channel for channels 3 and 4 during initialisation. The custom headless board definition I was working from had skipped that step.

ESP32-P4 GPIO0–44 VDDPST (main) 3.3 V FR / RR driver inputs → brake state OK GPIO45–54 VDDPST_5 / VDDPST_6 ~1.2 V RL / FL inputs, 4× hall → counts are noise 3V3 rail internal LDO channel VO4 never enabled esp_ldo_acquire_channel(4, 3300 mV) → closes the switch
Two I/O power domains on the ESP32-P4. The one feeding GPIO45–54 hangs off internal LDO channel VO4, which the board code never enabled.

One root cause, four symptoms

Symptom Explanation
GPIO46/47/48 read 1.2 V The whole domain is unpowered
Rear-left motor spins constantly IN1 = GPIO46 (dead domain, reads as low) and IN2 = GPIO33 (healthy domain, 3.3 V in brake state) → the driver sees (0, 1) = full-speed reverse
Front-left motor never moves Both inputs are in the dead domain → (0, 0) = coast
Front-right / rear-right sit still Their pins (GPIO32/27/26/23) are in a healthy domain, so brake state works
RPM readings are garbage, 491 RPM phantom All four hall inputs are GPIO49-52, entirely in the dead domain; the PCNT units are counting noise

The fix

In the board constructor, before anything else touches those pins:

#include "esp_ldo_regulator.h"

esp_ldo_channel_handle_t ldo_vo4;
esp_ldo_channel_config_t cfg = {
    .chan_id = 4,
    .voltage_mv = 3300,
};
ESP_ERROR_CHECK(esp_ldo_acquire_channel(&cfg, &ldo_vo4));

After reflashing: the rear-left motor stopped immediately, GPIO46/47/48 measured 3.3 V, and the RPM array read a clean [0, 0, 0, 0].

Lessons

  1. A voltage that is wrong but not zero (1.2 V, 0.8 V, that kind of value) is almost always a power-domain or floating-rail problem, not a wiring problem. Wiring errors give you 0 V or 3.3 V on the wrong pin.
  2. A contiguous block of GPIOs all misbehaving at once points at a shared rail. Check the datasheet’s power-domain table before checking cables.
  3. When porting a board definition, read what the other boards for the same chip do in their init code. Anything you skip because “we don’t need that” is a candidate root cause later.
  4. Ask for a multimeter reading early. One measurement from the bench beat an hour of reasoning about pin tables.

The line now has a comment above it in the source that says, in effect, “do not delete this, see bug #2”. Some lessons deserve a permanent marker.