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:
- Rear-left motor ran at full speed constantly. Forward, reverse, brake — nothing changed it.
- Front-left motor did not move at all.
- Front-right and rear-right sat still, which at the time looked normal (brake state).
- The hall-encoder RPM readings in the telemetry were garbage. With no motors connected, one channel reported a steady 491 RPM.
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.
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
- 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.
- A contiguous block of GPIOs all misbehaving at once points at a shared rail. Check the datasheet’s power-domain table before checking cables.
- 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.
- 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.