Skip to content

Board support: Waveshare ESP32-P4-WIFI6-Touch-LCD-4.3 (esp32p4 target, MIPI-DSI display, touch, speaker) - #308

Draft
bitcoin3us wants to merge 8 commits into
MicroPythonOS:mainfrom
bitcoin3us:feat/esp32p4-target
Draft

bitcoin3us wants to merge 8 commits into
MicroPythonOS:mainfrom
bitcoin3us:feat/esp32p4-target

Conversation

@bitcoin3us

@bitcoin3us bitcoin3us commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

What

First ESP32-P4 board for MicroPythonOS, following the plan in #307. Eight commits, on top of current main:

  1. build: add an esp32p4 target — ./scripts/build_mpos.sh esp32p4: ESP32_GENERIC_P4 / C6_WIFI variant (Wi-Fi 6 and BLE through the ESP32-C6 co-processor over SDIO, esp_hosted), 32 MB flash with 4 MiB app partitions, RISC-V frozen builds (rv32imc), PSRAM at 200 MHz and the 256 KB L2 cache with 128-byte lines (the MIPI-DSI panel is scanned out of PSRAM continuously). Also fixes c_mpos/src/quirc_decode.c's FreeRTOS stub guard, which keyed on __xtensa__.
  2. boards: detect the Waveshare ESP32-P4-WIFI6-Touch-LCD-4.3 — the P4 gets its own probe branch in detect_board() (GT911 at 0x5D/0x14 on SDA 7 / SCL 8): the xtensa boards' probe pins map to unrelated peripherals on the P4 and hang the chip. main.py also stops cleanly when a board file registers no LVGL display instead of panicking in init_rootscreen().
  3. boards: ... display and touch — drivers/display/st7701_dsi: ST7701 over lcd_bus.DSIBus (video mode; hardware reset, Waveshare's vendor register table over DCS, no window addressing) and the board file: 2-lane DSI at 500 Mbps, DPI 30 MHz, RGB565, two PSRAM frame buffers, backlight on GPIO26 (PWM, active low), GT911 polled (its RST/INT are not wired), drivers/indev/gt911.py fixed for interrupt_pin=None.
  4. boards: ... speaker output — ES8311 codec + the board amplifier (I2S MCLK 13 / BCLK 12 / LRCK 10 / data 9, PA enable GPIO53 active high), sequenced with the DAC soft-mute like the other ES8311 boards. The microphones sit behind a separate ES7210 ADC that has no driver yet.
  5. boards: ... in landscape — the UI runs 800x480 like the other boards. The panel is natively portrait and the ST7701 cannot rotate on the video interface, so the P4's PPA does it (DSIBus(rotation=270), same rotation on the LVGL display). LCD_ROTATION in the board file picks which way is up; 270 is upright in the enclosure.
  6. c_mpos: hosted module — hosted.host_version() / hosted.coprocessor_version() report the esp_hosted versions on both sides of the SDIO link, and hosted.ota_begin/ota_write/ota_end/ota_activate wrap esp_hosted's co-processor update calls. Compiled only where CONFIG_ESP_HOSTED_ENABLED is set (an empty file elsewhere; desktop builds don't include it). See "Wi-Fi co-processor firmware" below for why it matters on this board.
  7. boards: warm speaker output — now that WAVStream: opt-in warm output so back-to-back clips don't click (ES8311) #301 is in, the speaker uses warm_ms=30000 like the 3.5" board, so back-to-back clips don't click on every I2S restart.
  8. boards: ... Wi-Fi 30 s — the board file raises WifiService.CONNECT_TIMEOUT_S (added in WifiService: cancel a pending reconnect (#309), let a board lengthen the connection wait #311) to 30 s, because this board's C6 can need up to 30 s to join on channel 6 (see below). Until WifiService: cancel a pending reconnect (#309), let a board lengthen the connection wait #311 is merged nothing reads the attribute and the wait stays 13 s.

Depends on

Verified on the board

Wi-Fi co-processor firmware

The board's ESP32-C6 ships with esp_hosted firmware older than 1.4.7 (it does not answer the co-processor version request, which 1.4.7 already supports), while this build's host side is esp_hosted 2.7.0. esp_hosted warns about that mismatch at boot ("upgrade co-proc to avoid RPC timeouts"), but the message is lost while USB re-enumerates, hence the hosted module. On the test board the C6 was updated to 2.7.0 over SDIO: esp_hosted's slave project built for esp32c6, the image written into the P4's unused ota_1 with esptool, then streamed to the C6 with hosted.ota_begin/ota_write/ota_end (under 10 s; the old firmware switches and restarts at ota_end, 2.x needs ota_activate). A documented update path for users is a follow-up.

Separately, this board's C6 joins slowly or not at all on 2.4 GHz channels 6–9. Against a test AP 1 m away:

  • channels 1 and 11 join in ~3 s;
  • channel 6 joined in 6 of 12 attempts, after 14–30 s (none in the latest run);
  • channels 7–9 did not join at all;
  • the other way round (Mikey1774's test in Fixed macOS casing because make.py expects macOS instead of macos #7), with the C6 as a soft-AP, another ESP32 saw it on channels 6–9 in at most 2 of 12 scans per channel, against 6 of 6 on channels 1 and 11;
  • results differ between sessions.

It reproduces with Waveshare's own 04_wifistation example and with plain ESP-IDF. No clear improvement came from changing the co-processor firmware, the SDIO clock or stopping the display, nor from moving the PSRAM clock source from 400 to 320 MHz (CONFIG_SPIRAM_SPEED_80M, which also slows a full-screen refresh to ~53 ms, so the build stays at 200 MHz). Reported as waveshareteam/ESP32-P4-WIFI6-Touch-LCD-4.3#7. Commit 8 gives the board 30 s to connect, so the slow channel-6 joins succeed when they happen.

Not yet

ES7210 microphones, the TF card slot (slot 0 needs the P4's on-chip LDO channel 4 switched on, which machine.SDCard cannot do), the MIPI-CSI camera.

Flashing

USB-Serial-JTAG, so plain esptool --chip esp32p4 --before default_reset works (no TinyUSB caveats): full image at 0x0, app-only at 0x20000. Factory partition layout differs (the factory image is worth backing up first).

🤖 Generated with Claude Code

With thanks to the scientists and engineers who did the hard, unglamorous work that got us here.

bitcoin3us and others added 7 commits September 29, 2026 13:56
ESP32_GENERIC_P4 with MicroPython's C6_WIFI variant (esp_hosted +
esp_wifi_remote over SDIO, pulled in by the port's component manifest),
32 MB flash, 4 MiB OTA app partitions (boards ship 32 MB, no reason to
inherit the 3.5 MiB legacy size), freezefs native code as rv32imc.
Opt-in: nothing changes for the existing targets.

c_mpos/src/quirc_decode.c: the uxTaskGetStackHighWaterMark stub for
desktop/web builds was keyed on __xtensa__, which made the RISC-V
ESP32-P4 build fail with a conflicting prototype; key on INC_FREERTOS_H
(py/runtime.h already includes FreeRTOS on the ESP32 port) instead.

First target: Waveshare ESP32-P4-WIFI6-Touch-LCD-4.3 (MicroPythonOS#307).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ESP32-P4 boards get their own probe branch in detect_board(): the
xtensa boards' probe pins map to unrelated peripherals on the P4 (the
SDIO link to the ESP32-C6 Wi-Fi co-processor among them) and an I2C
probe on them hangs the chip before the REPL comes up. The P4 branch
probes the GT911 touch controller on the board's I2C bus (SDA 7, SCL 8,
address 0x5D or 0x14).

main.py now also stops cleanly when a board file registers no LVGL
display: init_rootscreen() would otherwise dereference a NULL display
and panic the chip into a reboot loop with no REPL to debug from.

The board file is a stage-1 placeholder (no display driver yet); the
MIPI-DSI display, touch and audio follow in later commits.

Verified on the board: clean stop at the REPL, DeviceInfo hardware id
set, Wi-Fi scan through the C6 (14 networks), I2C scan finds 0x5D
(GT911), 0x18 (ES8311) and 0x40 (ES7210).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The board file brings up the 4.3" 480x800 panel over 2-lane MIPI-DSI
and the GT911 touch controller, replacing the stage-1 placeholder:

- drivers/display/st7701_dsi: an ST7701 driver for lvgl_micropython's
  lcd_bus.DSIBus (video mode). The bus owns two screen-sized PSRAM
  frame buffers that LVGL renders into directly and the P4's DPI DMA
  scans out; the driver's job is the hardware reset and the panel's
  DCS register setup (Waveshare's vendor table, preceded by the bank-0
  MADCTL/COLMOD writes Espressif's esp_lcd_st7701 sends), sent before
  the bus starts the video stream on the first flush. Rotation is not
  supported (no row/column exchange on the video interface); the
  board runs the panel in its native portrait orientation.
- Backlight: LEDC PWM on GPIO26, active low (Waveshare's BSP drives
  its LEDC channel inverted), kept off until the first frame is out.
- Touch: GT911 polled at whichever I2C address its strap selected
  (0x5D or 0x14); neither its reset nor its interrupt line is wired to
  the P4 here, so drivers/indev/gt911.py no longer tries to build a
  Pin from interrupt_pin=None.
- build_mpos.sh (esp32p4): PSRAM at 200 MHz and the 256 KB L2 cache
  with 128-byte lines, as in Waveshare's and Espressif's P4 display
  examples. The panel is scanned out of PSRAM continuously (46 MB/s at
  60 Hz) and MicroPython's default 20 MHz PSRAM clock cannot feed it.

Needs lvgl_micropython with DSIBus (feat/dsi-bus); the submodule
pointer is bumped once that is merged.

Verified on the board: DCS reads answer (RDDPM 0x9C, RDDMADCTL 0x00,
RDDCOLMOD 0x50), LVGL renders the launcher into the panel buffers and
full-screen refreshes complete in one frame period (14-17 ms), GT911
reports product 911 / firmware 0x1060 / 480x800, no DMA underruns.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ES8311 codec (I2C 0x18 on the shared bus) driving the board's power
amplifier: I2S MCLK=13, BCLK=12, LRCK=10, data=9, amplifier enable
GPIO53 (active high), switched on around playback only and sequenced
with the DAC soft-mute like the other ES8311 boards. Starts from the
3.5" board's 76% DAC volume; this amplifier still needs an ear on it.

The microphones sit behind a separate ES7210 ADC (I2C 0x40) that
MicroPythonOS has no driver for yet, so only playback is wired up.

Verified on the board: a 1 s WAV plays to completion with the
amplifier enabled 69 ms and the DAC unmuted 254 ms after start (needs
the frozen native code alignment fix in lvgl_micropython's builder,
without it the first native helper call panics on the P4).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The UI runs landscape (800x480) like the other MicroPythonOS boards.
The ST7701 cannot rotate on the video interface, so the P4's PPA does
it: the DSI bus is created with rotation=270 and the ST7701_DSI driver
sets the same rotation on the LVGL display, which then renders
landscape into its own buffers while the bus rotates every finished
update into the panel's portrait frame buffers. LCD_ROTATION in the
board file picks which way is up (270 is upright in the enclosure, 90
is upside down); 0 gives the native portrait. The GT911 keeps
reporting panel coordinates, LVGL maps them to the rotated UI itself.

Needs lvgl_micropython with DSIBus rotation (feat/dsi-bus).

Verified on the board: LVGL at 800x480 with rotation 270, upright in
the enclosure, forced full-screen refreshes ~29 ms (render + PPA +
vsync), no DMA underruns.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Boards whose radio is a separate ESP-Hosted co-processor (the ESP32-P4
with an ESP32-C6 over SDIO) need to know which co-processor firmware
they talk to, and to be able to update it: the host warns at boot
when the major.minor versions differ ("upgrade co-proc to avoid RPC
timeouts"), but that log is lost while USB re-enumerates.

hosted.host_version() and hosted.coprocessor_version() return
(major, minor, patch); hosted.ota_begin/ota_write/ota_end/ota_activate
wrap esp_hosted's co-processor update calls. Compiled only where
CONFIG_ESP_HOSTED_ENABLED is set; other targets get an empty file.

Used on the Waveshare ESP32-P4-WIFI6-Touch-LCD-4.3 to find its C6 on
firmware older than esp_hosted 1.4.7 (it does not answer the version
request) against a 2.7.0 host, and to update it to 2.7.0 from an
image placed in the P4's spare app partition.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-4.3

Now that WAVStream supports an opt-in warm output (MicroPythonOS#301), the P4
board's ES8311 speaker uses it like the ESP32-S3-Touch-LCD-3.5: the
I2S clocks keep running and the codec stays unmuted for 30 s after a
clip, so back-to-back clips do not click on every MCLK/I2S restart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On this board the ESP32-C6 co-processor joins slowly or not at all on
2.4 GHz channels 6-9 (waveshareteam/ESP32-P4-WIFI6-Touch-LCD-4.3#7).
Against a test AP on channel 6, 1 m away, it joined in 6 of 9 attempts,
after 14-30 s; channels 1 and 11 take ~3 s. That is longer than
WifiService's 13 s wait, so the board file raises
WifiService.CONNECT_TIMEOUT_S (added in MicroPythonOS#311) to 30 s. Until MicroPythonOS#311 is
merged nothing reads the attribute and the wait stays 13 s.

PSRAM stays at 200 MHz: an 80 MHz build (PSRAM clock source at 320
instead of 400 MHz) made no consistent difference to channels 6-9 in a
same-day comparison and slowed a full-screen refresh from ~30 ms to
~53 ms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant