Skip to content

ESP32-P4: DSIBus (MIPI-DSI video mode, PPA rotation) and two builder fixes - #16

Open
bitcoin3us wants to merge 3 commits into
MicroPythonOS:integrationfrom
bitcoin3us:feat/dsi-bus
Open

bitcoin3us wants to merge 3 commits into
MicroPythonOS:integrationfrom
bitcoin3us:feat/dsi-bus

Conversation

@bitcoin3us

@bitcoin3us bitcoin3us commented Sep 26, 2026 •

Copy link
Copy Markdown

What

Display support for the ESP32-P4's MIPI-DSI interface, brought up on the Waveshare ESP32-P4-WIFI6-Touch-LCD-4.3 (ST7701, 480x800, 2 lanes) for MicroPythonOS. Three commits, on top of current integration:

  1. lcd_bus: add DSIBus — esp32_src/dsi_bus.c existed but was neither compiled nor registered and did not build against ESP-IDF 5.5. Now wired in (guarded by SOC_MIPI_DSI_SUPPORTED) and reworked around how the IDF DPI panel works:
    • the DPI panel driver owns screen-sized frame buffers that its DMA scans out continuously, so allocate_framebuffer() hands out real, correctly sized buffers which init() repoints at the panel's frame buffers: LVGL renders straight into what is scanned out (partial buffers are refused with a clear error);
    • the video stream is started on the first tx_color, so the display driver configures the panel over DCS first (tx_param/rx_param through the DBI panel IO), the order Espressif's own MIPI panel drivers use;
    • a flush is a cache write-back plus a buffer swap at the next frame boundary; the refresh-done interrupt reports flush_ready. Intermediate areas of a multi-area update complete immediately;
    • kwargs: bus_id, data_lanes, freq (lane Mbps), dpi_clock_freq (MHz), virtual_channel, the h/v sync porches and pulse widths, phy_ldo_channel/phy_ldo_voltage_mv (the P4's internal LDO that powers the DSI PHY, channel 3 on the Espressif and Waveshare boards), rotation.
  2. lcd_bus: DSIBus rotation through the PPA — video-interface panels cannot swap rows and columns, so DSIBus(rotation=90|180|270) lets LVGL render the rotated picture into the bus's own buffers and rotates every finished update into the panel's back buffer with the P4's pixel processing accelerator (tear-free, panel side always double-buffered). Pass the same rotation to the LVGL display; LVGL rotates touch coordinates itself.
  3. Build-time patches instead of builder changes (per CONTRIBUTING.md), applied by MicroPythonOS's scripts/build_mpos.sh (companion PR: build: apply lvgl_micropython's USER_C_MODULES and RISC-V native patches MicroPythonOS#312):
    • esp32_user_c_modules_abspath.patch (applied in this repo's root): builder/esp32.py passes USER_C_MODULES as an absolute path. The relative ../../../../../ext_mod/micropython.cmake is resolved against lib/micropython's real location, so a build from a git worktree with lib/ symlinked silently compiled the other checkout's ext_mod.
    • esp32_riscv_frozen_native_align.patch (applied in lib/micropython): frozen @micropython.native/viper functions crashed with an illegal instruction on the P4. mpy-tool.py emits their machine code with aligned(2), but the RISC-V assembler never pads a 2-byte alignment inside a code section, so arrays after an odd-sized one start at an odd address. The patch uses aligned(4) for RV32/RV64 (as for Xtensa) and compiles frozen_content.c with -mno-relax on RISC-V (both are needed: with relaxation on, 4-byte padding fails to link on odd sizes). This affects every RISC-V ESP32 target (C3/C6/P4) with frozen native code; Xtensa builds are unchanged.

Verified on the board

  • Patch form verified with a P4 build from pristine files: both patches applied forward, the worktree's own ext_mod was compiled via the absolute path, 0 frozen native-code arrays at an odd address, and WAV playback (frozen native helpers) runs on the board without a crash.

  • DCS reads answer (RDDPM 0x9C, RDDMADCTL 0x00, RDDCOLMOD 0x50), LVGL renders into the panel buffers, forced full-screen refreshes complete in one 60 Hz frame (14-17 ms) unrotated and ~29 ms with PPA rotation (render + rotation + vsync), no DMA underruns with PSRAM at 200 MHz.

  • All 4213 frozen fun_data arrays 4-byte aligned after the alignment fix (5 were odd before); WAV playback through the frozen native helpers runs to completion.

  • GT911 touch and the ES8311 speaker work through the same image (board file in the MicroPythonOS PR).

The MicroPythonOS side (esp32p4 build target, board detection, ST7701-over-DSI driver, board file) is a separate PR that depends on this one; the plan is in MicroPythonOS/MicroPythonOS#307. Happy to offer the DSI bus upstream once it has settled here.

🤖 Generated with Claude Code

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

@ThomasFarstrike

Copy link
Copy Markdown

Wow, huge work again!

Same question here, whether https://github.com/MicroPythonOS/lvgl_micropython/blob/integration/CONTRIBUTING.md was followed. I'm myself unsure how we can best work on our lvgl_micropython branch while still allowing to resync with upstream if it gets updated... so suggestions are always welcome!

@bitcoin3us

Copy link
Copy Markdown
Author

Good question, and partly no: #13 to #16 target integration with fix/ / feat/ branches, but they edit files directly rather than shipping .patch files.

My suggestion for the split:

Happy to convert #15 to a patch too if you'd rather have all api_drivers changes as patches.

bitcoin3us and others added 3 commits September 29, 2026 14:31
esp32_src/dsi_bus.c existed but was neither compiled nor registered,
and did not build against ESP-IDF 5.5 (esp_lcd_new_panel_io_dsi is
esp_lcd_new_panel_io_dbi there, plus a few typos). This wires it in,
guarded by SOC_MIPI_DSI_SUPPORTED, and reworks it around how the IDF
DPI panel actually works:

- The DPI panel driver allocates screen-sized frame buffers itself and
  its DMA scans them out continuously. allocate_framebuffer() therefore
  hands out real, correctly sized buffers that init() repoints at the
  panel's frame buffers (so len() is right for the display framework
  and LVGL renders straight into the scanned-out memory); partial
  buffers are refused with a clear error.
- esp_lcd_panel_init() (the start of the video stream) is deferred to
  the first tx_color, so the display driver can configure the panel
  over DCS (tx_param/rx_param through the DBI panel IO) first, the
  order Espressif's own MIPI panel drivers use.
- A flush is a cache write-back plus, when the flushed buffer is not
  the one being scanned out, a swap at the next frame boundary; the
  refresh-done interrupt reports completion (flush_ready) once the
  panel has picked the buffer up. Intermediate areas of a multi-area
  update complete immediately. LVGL's inclusive area coordinates are
  converted for esp_lcd_panel_draw_bitmap().
- The DSI PHY's internal LDO (ESP32-P4: channel 3, 2500 mV) can be
  acquired by the bus (phy_ldo_channel / phy_ldo_voltage_mv kwargs),
  dpi_clock_freq (MHz) is separate from the lane bit rate, and
  del/free paths no longer leak or double-declare.

Verified on the Waveshare ESP32-P4-WIFI6-Touch-LCD-4.3 (ST7701 480x800,
2 lanes at 500 Mbps, DPI 30 MHz, RGB565, two PSRAM frame buffers):
DCS reads answer (RDDPM 0x9C, RDDMADCTL 0x00, RDDCOLMOD 0x50), LVGL
renders into the panel buffers and full-screen refreshes complete in
one frame period (14-17 ms) with no DMA underruns at 200 MHz PSRAM.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Panels on the DSI video interface cannot swap rows and columns, so a
landscape UI on a portrait panel needs the picture rotated on the way
to the frame buffer. DSIBus(rotation=90|180|270) does that with the
ESP32-P4's PPA (pixel processing accelerator): LVGL renders the rotated
(logical) picture into the bus's own screen-sized buffers, and on the
last flush of an update the whole picture is rotated by the PPA into
the panel's back buffer, which is then swapped in at the next frame
boundary (the refresh-done interrupt reports completion as before).
The panel side is always double-buffered in this mode; intermediate
areas of a multi-area update complete immediately. The angle follows
LVGL's convention, so pass the same rotation to lv_display.

Frame buffers are now allocated cache-line aligned (the PPA and the DPI
DMA both need it), the PPA driver's include path is added where the
chip has one, and rotation is refused on chips without a PPA.

Verified on the Waveshare ESP32-P4-WIFI6-Touch-LCD-4.3 (480x800 panel,
rotation=90): LVGL runs at 800x480, a forced full-screen refresh takes
~29 ms (render + PPA rotation + vsync) against ~15 ms unrotated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… alignment

Two build fixes as .patch files applied by MicroPythonOS's
scripts/build_mpos.sh (see CONTRIBUTING.md), rather than edits to
builder/ in this branch.

esp32_user_c_modules_abspath.patch (apply in this repo's root):
builder/esp32.py passed USER_C_MODULES=../../../../../ext_mod/micropython.cmake,
which CMake resolves against lib/micropython/ports/esp32's real location.
When lib/ is symlinked to another checkout (git worktrees sharing one
MicroPython tree and build directory) that silently compiles the OTHER
checkout's ext_mod. The patch passes the absolute path instead.

esp32_riscv_frozen_native_align.patch (apply in lib/micropython):
frozen @micropython.native / viper functions crashed with an illegal
instruction on their first call on the ESP32-P4. tools/mpy-tool.py
emits their machine code in a code section with aligned(2) for RISC-V,
but the RISC-V assembler never pads a 2-byte alignment inside a code
section, so every array following an odd-sized one starts at an odd
address; jalr clears the low bit and the CPU decodes garbage. A 4-byte
alignment is padded, but only when the file is assembled without
linker relaxation (with relaxation the padding is R_RISCV_ALIGN NOPs,
which fail on odd sizes: "3 bytes required for alignment ... can't
relax section"). So the patch makes mpy-tool.py use aligned(4) for
RV32/RV64, as it already does for Xtensa, and compiles frozen_content.c
with -mno-relax on RISC-V targets. Xtensa builds are unchanged.

Both were previously done by builder/esp32.py code in this PR.

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

Copy link
Copy Markdown

Awesome!

Agreed for the suggestion on the split, although rather than just committing on integration, wouldn't it make sense to have each tweak done on a feat/ branch and then merged into integration? Or is that overkill? Does that mean you have to create 2 pull requests - one for the new feat/ branch and one for integration?

@bitcoin3us

Copy link
Copy Markdown
Author

Yes, that's exactly the flow, sorry for the ambiguous wording. By "direct commits" I meant normal commits rather than .patch files, not pushing to integration itself. Each tweak lives on its own fix/ or feat/ branch (e.g. fix/lvgl-tick-single-source, feat/dsi-bus) and reaches integration through one PR, so no double PRs. A second PR only happens when a fix is generic enough to offer upstream as well, and that one goes to lvgl-micropython/lvgl_micropython from a branch off upstream's main, like lvgl-micropython#607 for the tick fix. Long-running work would use a topic/ branch as CONTRIBUTING.md describes.

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.

2 participants