Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions modules/displayif/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ failure modes (P4 `mipidsi`, Qualia `dotclockframebuffer.DotClockFramebuffer`):
| `dotclockframebuffer` | `esp32` (RGB LCD), `mimxrt` (1062 eLCDIF) | **FBDisplay** |
| `i80bus` | `esp32` (S3), `rp2` (PIO+DMA), `mimxrt` (1062 FlexIO), `samd` (SAMD51 GPIO) | **BusDisplay** |
| `qspibus` | `esp32` (S3 esp_lcd SPI quad_mode); stubs elsewhere | **BusDisplay** |
| `mipidsi` | `esp32` (P4), `mimxrt` (1176) | **FBDisplay** |
| `mipidsi` | `esp32` (P4), `mimxrt` (1176), `stm32` (H747/H757) | **FBDisplay** |
| `picodvi` | `rp2` (RP2040 PIO / RP2350 HSTX) | **FBDisplay** |
| `rgbmatrix` | `esp32` (S3) / `mimxrt` (1062) / `samd` (SAMD51) / `rp2` | **FBDisplay** |
| `usdl2` | `desktop` (MicroPython `unix` / `windows`; CircuitPython unix as a user C module) | **SDLDisplay** |
| stubs | `samd` / `rp2` / non-1062 mimxrt (`dotclockframebuffer.DotClockFramebuffer`, `mipidsi`); non-1176 mimxrt (`mipidsi`); non-S3 `qspibus` | ctor raises |
| stubs | `samd` / `rp2` / non-1062 mimxrt (`dotclockframebuffer.DotClockFramebuffer`, `mipidsi`); non-1176 mimxrt and non-H747/H757 stm32 (`mipidsi`); non-S3 `qspibus` | ctor raises |


Parallel dot-clock RGB uses **`dotclockframebuffer.DotClockFramebuffer`** (same module name as CircuitPython) — no `RGBDisplay`.
Expand Down
21 changes: 21 additions & 0 deletions modules/displayif/docs/port-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ names (`mipidsi`, `i80bus`, `picodvi`, …).
| `src/ports/mimxrt` | `i80bus` | bus driver | FlexIO MCULCD 8080 on MIMXRT1062 |
| [`src/ports/samd`](../src/ports/samd) | `i80bus` | bus driver | GPIO bit-bang (SAMD51) via `common/i80bus` |
| `src/ports/samd` | stubs | `dotclockframebuffer`, `mipidsi` | import ok; ctor raises |
| [`src/ports/stm32`](../src/ports/stm32) | `mipidsi` | **FBDisplay** | MIPI DSI on STM32H747/H757 (DSI host + LTDC) |

All parallel dot-clock RGB panels use **`dotclockframebuffer.DotClockFramebuffer`** + **`FBDisplay`**.
There is no separate `RGBDisplay` / `present()` path. CircuitPython board configs use the same import name.
Expand Down Expand Up @@ -147,6 +148,24 @@ make USER_C_MODULES=../../.. BOARD=ADAFRUIT_METRO_M4_EXPRESS
make USER_C_MODULES=../../.. BOARD=ADAFRUIT_FEATHER_M4_EXPRESS
```

### stm32

`mipidsi` (**STM32H747/H757**): drives the chip's DSI host in video burst mode, fed by the
**LTDC**, which scans an RGB565 framebuffer allocated from the MicroPython heap (SDRAM on the
Arduino GIGA R1). The LTDC reads that memory continuously, so `refresh()` only writes the CPU's
dirty data-cache lines back to it; `blit` and `fill_rect` do the same after drawing. `Bus` takes
`frequency` (the lane bit rate, default 500 MHz) and `num_lanes` (1 or 2). `Display` takes the
same keyword arguments as on esp32, with `virtual_channel=0` and `color_depth=16`; its
`pixel_clock_frequency` is met by changing only PLL3's R divider, the LTDC's clock, and leaving
PLL3's other outputs (SPI1/2/3) alone. While a display is up, the ADC's kernel clock, also
PLL3 R, runs at the pixel clock; deinit and soft reset put the divider back. Init commands go
out as DCS writes over the DSI host's command interface. Panel power and backlight GPIO are
owned by board_config, not `Display`. Other stm32 chips get the stub.

The board needs the HAL's DSI and LTDC drivers: the `ARDUINO_GIGA` `DISPLAY_SHIELD` variant in
the [boards table](../../../docs/reference/boards.md) adds them, through its
`stm32h7xx_hal_conf.h` and `HAL_SRC_C`. Example pydevices config: `fbdisplay/arduino-giga-display-shield`.

---

## pydevices board configs (MicroPython)
Expand All @@ -162,6 +181,7 @@ These configs import displayif modules when firmware is built with the matching
| `fbdisplay/mimxrt1060_evk_rk043_rgb` | `dotclockframebuffer` |
| `fbdisplay/mimxrt1170_evk_waveshare_5dsi` | `mipidsi` |
| `fbdisplay/esp32-p4-wifi6-touch-lcd-4b` | `mipidsi` |
| `fbdisplay/arduino-giga-display-shield` | `mipidsi` |
| `fbdisplay/pico2_dvi_sock_640x480` | `picodvi` |
| `fbdisplay/t-rgb_480` | `dotclockframebuffer` |
| parallel RGB `fbdisplay/*` (MP) | `dotclockframebuffer` |
Expand Down Expand Up @@ -204,6 +224,7 @@ displayif/
│ ├── esp32/ # dotclockframebuffer, i80bus, qspibus, mipidsi, rgbmatrix (S3)
│ ├── mimxrt/ # rgbmatrix; eLCDIF dotclockframebuffer; RT1176 mipidsi; FlexIO i80bus
│ ├── samd/ # rgbmatrix; GPIO i80bus (SAMD51); stubs
│ ├── stm32/ # H747/H757 mipidsi; stubs
│ └── rp2/ # rgbmatrix; i80bus PIO+DMA; picodvi
├── docs/ # markdown (port notes under docs/ports/)
├── tests/ # unit tests only
Expand Down
103 changes: 103 additions & 0 deletions modules/displayif/mipidsi.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# SPDX-FileCopyrightText: 2026 Brad Barnett
#
# SPDX-License-Identifier: MIT

"""Drive a MIPI DSI panel from a framebuffer, with CircuitPython's ``mipidsi`` names.

Built on the ESP32-P4, the MIMXRT1176 and the STM32H747/H757; elsewhere the
module imports and its constructors raise `NotImplementedError`. A
`Display` is a buffer of RGB565 pixels that displaydev's ``FBDisplay``
draws into.
"""

from typing import Any

__revision__: str

class Bus:
"""The DSI host and its PHY.

Args:
frequency: The bit rate of each data lane, in Hz.
num_lanes: The number of data lanes, 1 to 4 (1 or 2 on the
STM32H747).
ldo_chan: ESP32-P4 only: the LDO channel that powers the DSI PHY.
ldo_voltage_mv: ESP32-P4 only: that LDO's voltage, in millivolts.
"""

def __init__(
self,
*,
frequency: int = 500_000_000,
num_lanes: int = 2,
ldo_chan: int = 3,
ldo_voltage_mv: int = 2500,
) -> None: ...
def deinit(self) -> None:
"""Release the DSI host, and any `Display` on it."""

class Display:
"""A DSI panel in video mode, scanning a framebuffer.

Supports the buffer protocol: the bytes are the framebuffer, RGB565,
``row_stride`` bytes per row.

Args:
bus: The `Bus` the panel is on.
init_sequence: The panel's start-up commands: each is a command
byte, a parameter count (``| 0x80`` when a delay in milliseconds
follows the parameters, 255 meaning 500 ms), then the parameters.
width: The panel's width in pixels.
height: The panel's height in pixels.
hsync_pulse_width: Horizontal sync, in pixels.
hsync_back_porch: Horizontal back porch, in pixels.
hsync_front_porch: Horizontal front porch, in pixels.
vsync_pulse_width: Vertical sync, in lines.
vsync_back_porch: Vertical back porch, in lines.
vsync_front_porch: Vertical front porch, in lines.
pixel_clock_frequency: The pixel clock, in Hz. The STM32H747 uses
the nearest it can make, and reports it as
``pixel_clock_frequency``.
virtual_channel: The DSI virtual channel, 0 to 3 on the ESP32-P4;
0 elsewhere.
color_depth: Bits per pixel; only 16 is supported.
non_burst: ESP32-P4 only: non-burst video with sync pulses.
continuous_clock: ESP32-P4 only: keep the clock lane in high-speed
mode between lines.
"""

width: int
height: int
row_stride: int
pixel_clock_frequency: int
"""STM32H747 only: the pixel clock the display runs at, in Hz."""

def __init__(
self,
bus: Bus,
init_sequence: Any,
*,
width: int,
height: int,
hsync_pulse_width: int,
hsync_back_porch: int,
hsync_front_porch: int,
vsync_pulse_width: int,
vsync_back_porch: int,
vsync_front_porch: int,
pixel_clock_frequency: int,
virtual_channel: int = 0,
color_depth: int = 16,
non_burst: bool = False,
continuous_clock: bool = False,
) -> None: ...
def refresh(self, *args: Any) -> None:
"""Present what has been drawn into the framebuffer."""
def refresh_rect(self, x: int, y: int, w: int, h: int) -> None:
"""ESP32-P4 only: present the rows ``y`` to ``y + h`` alone."""
def blit(self, buf: Any, x: int, y: int, w: int, h: int) -> None:
"""ESP32-P4 and STM32H747: copy ``w`` x ``h`` RGB565 pixels from ``buf`` to (``x``, ``y``)."""
def fill_rect(self, x: int, y: int, w: int, h: int, color: int) -> None:
"""Fill a rectangle with one RGB565 colour."""
def deinit(self) -> None:
"""Stop scanning out. Drawing into the display afterwards raises."""
12 changes: 11 additions & 1 deletion modules/displayif/src/ports/stm32/micropython.mk
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# stm32 port — portable spibus/i2cbus (+ rgbmatrix bitbang); no DotClock/MIPI/i80 yet.
# stm32 port — portable spibus/i2cbus (+ rgbmatrix bitbang); MIPI DSI on the
# H747/H757 (DSI host + LTDC); no DotClock/i80 yet.

CFLAGS_USERMOD += -I$(DISPLAYIF_MOD_DIR)/src/ports/stm32

Expand All @@ -7,3 +8,12 @@ SRC_USERMOD_C += \
$(DISPLAYIF_MOD_DIR)/src/ports/common/notimpl/mod_mipidsi.c \
$(DISPLAYIF_MOD_DIR)/src/ports/common/notimpl/mod_i80bus.c \
$(DISPLAYIF_MOD_DIR)/src/ports/common/notimpl/mod_qspibus.c

# MIPI DSI: the H747/H757 have a DSI host. The board's stm32h7xx_hal_conf.h
# must define HAL_DSI_MODULE_ENABLED and include the DSI and LTDC HAL headers,
# and its make settings must add hal_dsi.c and hal_ltdc.c to HAL_SRC_C
# (STM32LIB_HAL_BASE is not defined yet when user modules are included).
ifneq ($(filter STM32H747xx STM32H757xx,$(CMSIS_MCU)),)
SRC_USERMOD_C := $(filter-out $(DISPLAYIF_MOD_DIR)/src/ports/common/notimpl/mod_mipidsi.c,$(SRC_USERMOD_C))
SRC_USERMOD_C += $(DISPLAYIF_MOD_DIR)/src/ports/stm32/mod_mipidsi.c
endif
Loading
Loading