Waveshare ESP32-P4-Module-DEV-KIT-C: Kompaktes Entwicklungsboard mit 10,1 Zoll DSI-Display

Nachdem ich mir vor kurzem das CrowPanel Advance 7" ESP32-P4 angesehen habe, möchte ich euch heute das Waveshare ESP32-P4-Module-DEV-KIT-C vorstellen. Dieses Kit unterscheidet sich grundlegend von anderen "All-in-One"-Lösungen, da Display und Mikrocontroller zwei getrennte Komponenten sind, die auch gegen andere Modelle ausgetauscht werden können. Waveshare bietet daher das Kit auch in verschiedenen Varianten an: DEV-KIT-A ohne Display, DEV-KIT-B mit 7-Zoll-Display und Kamera oder das hier vorgestellte DEV-KIT-C mit großem 10,1-Zoll-Display und Kamera.

Die technischen Daten des DEV-KIT-C lesen sich auf den ersten Blick sehr beeindruckend:

  • SoC: Espressif ESP32-P4 (Dual-Core RISC-V @360 MHz + LP-Core @40 MHz)
  • Coprozessor: ESP32-C6 (WiFi 6, Bluetooth 5.4, via SDIO angebunden)
  • Speicher: 32 MB PSRAM, 16 MB Flash
  • Schnittstellen:
    • MIPI-DSI (2-Lane, 22-Pin FPC Connector)
    • MIPI-CSI (2-Lane, 22-Pin FPC Connector für Kameras bis 1080p)
    • 4x USB 2.0 Type-A Ports (Host-Mode)
    • 1x USB Type-C (OTG/Programmierung)
    • 100 Mbps Ethernet (RJ45)
    • 40-Pin GPIO Header (kompatibel zu Raspberry Pi Layout)
  • Audio: ES8311 Codec, NS4150B Class-D Verstärker (3W @ 4Ω)
  • Peripherie: microSD Slot, Batterieanschluss für RTC, I2C, SPI, UART, I2S
  • Stromversorgung: USB-C oder 5V via GPIO Header, optional PoE-Modul
  • Display: DSI 10,1 Zoll, IPS Panel mit 800 × 1280 Pixel, Kontrastverhältnis von 800:1 und 400 cd/m² Helligkeit
  • Kamera: 5 Megapixel (RPi Camera B) mit manuellem Fokus

Beim ersten Blick auf das Waveshare ESP32-P4-Module fällt sofort auf, dass es wie ein Raspberry Pi aussieht. Das Board verwendet die gleichen Abmessungen (85mm x 56mm), die gleiche Position der Befestigungslöcher, und sogar die Anordnung der wichtigsten Anschlüsse orientiert sich am Raspberry Pi Layout. Die 40-Pin GPIO-Leiste sitzt exakt an der gleichen Stelle, die vier USB-Ports und die Ethernet-Buchse befinden sich dort, wo man sie vom Pi gewohnt ist und sogar die FPC-Konnektoren für Kamera (CSI) und Display (DSI) sind an der gleichen Stelle wie bei Raspberry Pi.

Waveshare hat damit also eine Art "Raspberry Pi Pin-Compatible Microcontroller" geschaffen, allerdings mit einem entscheidenden Unterschied: Statt eines Linux-fähigen ARM-SoCs arbeitet hier ein 360 MHz RISC-V Mikrocontroller mit Echtzeit-Fähigkeiten. Man bekommt also die mechanische Kompatibilität eines Raspberry Pi, aber mit den Vorteilen eines Embedded-Systems, also deterministische Reaktionszeiten, direkter Hardware-Zugriff ohne Betriebssystem-Overhead und deutlich geringerer Stromverbrauch.

Die elektrische Kompatibilität ist aber nur teilweise gegeben. Während das Pin-Layout identisch ist, arbeiten die GPIOs des ESP32 mit 3.3V statt 5V. Solche Raspberry Pi HATs (Hardware Attached on Top), die für 3.3V ausgelegt sind, sollten damit theoretisch funktionieren. Aber die Software-Treiber muss man natürlich für das ESP-IDF neu schreiben.

Das ESP32-P4-Module-DEV-KIT

Das Herzstück des Boards ist das große ESP32-P4-Module, das Waveshare selbst entwickelt hat. Dieses Modul integriert alles, was für High-Performance-Anwendungen benötigt wird:

  • ESP32-P4 SoC: 360 MHz RISC-V Dual-Core Prozessor mit FPU und DSP-Erweiterungen, 32 MB PSRAM direkt im Package integriert
  • ESP32-C6-MINI-1: Coprozessor für WiFi 6 (802.11ax), Bluetooth 5.4 und Zigbee/Thread (802.15.4)
  • 16 MB NOR Flash: Onboard Flashspeicher für Firmware und Anwendungsdaten
  • IPEX-Steckverbinder: Verbindung zur einer externen Antenne für WiFi/Bluetooth

Die Verbindung zwischen ESP32-P4 und ESP32-C6 erfolgt über die SDIO-Schnittstelle. Darüber läuft die ESP-Hosted-MCU Firmware, die ich bereits beim CrowPanel Display beschrieben habe: Der C6 fungiert dabei als reiner WLAN-Adapter, während der P4 die Netzwerkbefehle transparent über SDIO durchreicht. Das Entwickeln für den P4 wird dadurch stark vereinfacht. Die gewohnten ESP-IDF WiFi-Funktionen stehen unverändert zur Verfügung, und die Tatsache, dass der C6 im Hintergrund die eigentliche Funkarbeit übernimmt, bleibt vollständig verborgen.

ESP32-P4-Module-DEV-KIT

Neben diesem Modul befinden sich noch folgende Komponenten auf dem DEV-KIT:

Programmierung und USB:

  • CH343P USB-UART-Bridge: Dieser dedizierte Chip sorgt für eine zuverlässige serielle Verbindung zum Flashen und Debuggen.
  • CH334F USB-Hub-Controller: Da der ESP32-P4 nur einen nativen USB 2.0 Port besitzt, erweitert dieser Hub-Controller die Anschlüsse auf vier vollwertige Type-A-Buchsen. Hier kann zum Beispiel ein USB-Stick, eine Maus oder eine Tastatur angeschlossen werden.
  • FSUSB42UMX USB-Multiplexer: Schaltet den USB-Datenpfad zwischen dem nativen USB-Port des P4 und dem CH343P UART-Chip um.

Netzwerk:

  • IP101GRI Ethernet PHY: Vollwertiger 100-Mbps-Fast-Ethernet-PHY-Chip.
  • HBJ-6117ANL RJ45-Buchse: Integrierter Netzwerkbuchse mit Übertrager und PoE-Unterstützung. Waveshare bietet separat ein PoE-Modul an, mit dem das Board über ein einzelnes Ethernet-Kabel sowohl mit Netzwerk als auch mit Strom versorgt werden kann.

Audio:

  • ES8311 Audio-Codec: 24-Bit ADC/DAC von Everest Semiconductor, angebunden über I2S (GPIO9–GPIO13) und I2C (GPIO7/GPIO8). Unterstützt Sample-Raten von 8 kHz bis 96 kHz.
  • NS4150B Class-D-Verstärker: 3W @ 4Ω Ausgangsleistung mit niedrigem Ruhestromverbrauch, steuerbar über GPIO53 (PA_CTRL).
  • MEMS-Mikrofon: Onboard-Mikrofon (MIC_P/MIC_N) direkt am ES8311 angeschlossen.
  • SH1.0 Speaker-Connector: 2-Pin Steckverbinder für einen 8Ω Lautsprecher, der im Lieferumfang enthalten ist.

Speicher und Peripherie:

  • microSD-Slot: Angebunden über SDIO 3.0 (GPIO39–GPIO45). Deutlich schneller als SPI-basierte SD-Karten-Lösungen.
  • RTC-Batterieanschluss: Der ESP32-P4 hat in seinem Low-Power-Subsystem (LP-Core) eine Echtzeituhr direkt integriert. Der Batterieanschluss versorgt also direkt den VDD_RTC Pin des Chips mit Strom. Waveshare weist ausdrücklich darauf hin, dass dieser Anschluss nur für wiederaufladbare RTC-Batterien (z.B. ML2032 oder LIR2032) ausgelegt ist! Das Board lädt den Akku auf, sobald es mit Strom versorgt wird. Eine normale CR2032 Knopfzelle würde hier überladen und zerstört werden.
  • Boot- und Reset-Taster: Klassische Taster für GPIO0 (Boot) und CHIP_PU (Reset).
  • Status-LED: Einfache Betriebs-LED an GPIO35.

Display und Kamera:

  • 15-Pin FPC-Connector (MIPI-DSI): 2-Lane MIPI-DSI Anschluss für Displays. Die Datenleitungen DSI_D0 und DSI_D1 sowie der Clock sind direkt vom ESP32-P4 Modul herausgeführt. Zusätzlich liegen GPIO7 und GPIO8 (I2C) am Connector an, um den Touch-Controller des Displays anzusprechen.
  • 15-Pin FPC-Connector (MIPI-CSI): 2-Lane MIPI-CSI Anschluss für Kameras. Kompatibel mit Raspberry Pi Kameramodulen (OV5647, IMX219 etc.). Auch hier sind I2C-Leitungen (CSI_IO0/IO1) für die Kamera-Konfiguration herausgeführt.

40-Pin GPIO-Header:

Der 40-polige Header ist der wohl wichtigste Erweiterungsanschluss des Boards. Die Pinbelegung orientiert sich am Raspberry-Pi-Layout, was die Verwendung von RPi-kompatiblen Gehäusen und Zubehör erlaubt. Das folgende Diagramm zeigt die Ähnlichkeit der beiden Anschlüsse:

gpio.png

Wie oben aber schon erwähnt ist die GPIO Leiste zwar im Layout mit dem  Raspberry-Pi kompatibel, aber nicht elektrisch. Alle GPIOs arbeiten bei ESP32 Mikrocontrollern mit 3,3V Logikpegel. Es dürfen also keine Raspberry HATs verwendet werden, die mit 5V Signalen arbeiten, das würde den P4 zerstören!

ESP32-C6 UART-Port:

An einem kleinen SH1.0 4-Pin-Steckverbinder sind die UART Leitungen des ESP32-C6 zugänglich. Damit kann man den C6 jederzeit über einen einfachen USB-Seriell-Adapter unabhängig vom P4 neu flashen. Sobald zum Beispiel Espressif das ESP-NOW Protokoll in ESP-Hosted-MCU integriert, reicht ein kurzes Firmware-Update des C6. Das ist ein entscheidender Vorteil gegenüber dem CrowPanel Advance ESP32-P4 Display, wo diese Möglichkeit fehlt und damit eine zukünftige Aktualisierung der ESP-Hosted-Firmware grundsätzlich nicht möglich ist.

ESP32-P4-Module-DEV-KIT UART ESP32-C6

Leider liefert Waveshare kein entsprechendes Kabel mit dem Display. In meiner Wühlkiste habe ich aber zufällig einen passenden Stecker gefunden, ein provisorisches Kabel gebaut und das Board mit meinem alten ESP-Prog Board verbunden.

ESP-ROM:esp32c6-20220919
Build:Sep 19 2022
rst:0x1 (POWERON),boot:0xc (SPI_FAST_FLASH_BOOT)
SPIWP:0xee
mode:DIO, clock div:2
load:0x40875720,len:0x163c
load:0x4086c110,len:0xe84
load:0x4086e610,len:0x2f74
entry 0x4086c11c
I (23) boot: ESP-IDF v5.4-dev-3602-ga97a7b0962 2nd stage bootloader
I (24) boot: compile time Oct 18 2024 14:21:41
I (24) boot: chip revision: v0.2
I (25) boot: efuse block revision: v0.3
I (29) boot.esp32c6: SPI Speed      : 80MHz
I (33) boot.esp32c6: SPI Mode       : DIO
I (36) boot.esp32c6: SPI Flash Size : 4MB
I (40) boot: Enabling RNG early entropy source...
I (45) boot: Partition Table:
I (47) boot: ## Label            Usage          Type ST Offset   Length
I (54) boot:  0 nvs              WiFi data        01 02 00009000 00004000
I (60) boot:  1 otadata          OTA data         01 00 0000d000 00002000
I (67) boot:  2 phy_init         RF data          01 01 0000f000 00001000
I (73) boot:  3 ota_0            OTA app          00 10 00010000 00180000
I (80) boot:  4 ota_1            OTA app          00 11 00190000 00180000
I (86) boot: End of partition table
I (89) esp_image: segment 0: paddr=00010020 vaddr=420c0020 size=316d4h (202452) map
I (135) esp_image: segment 1: paddr=000416fc vaddr=40800000 size=0e91ch ( 59676) load
I (149) esp_image: segment 2: paddr=00050020 vaddr=42000020 size=b8f30h (757552) map
I (294) esp_image: segment 3: paddr=00108f58 vaddr=4080e91c size=12500h ( 75008) load
I (311) esp_image: segment 4: paddr=0011b460 vaddr=40820e20 size=03c94h ( 15508) load
I (322) boot: Loaded app from partition at offset 0x10000
I (322) boot: Disabling RNG early entropy source...
I (332) cpu_start: Unicore app
I (341) cpu_start: Pro cpu start user code
I (341) cpu_start: cpu freq: 160000000 Hz
I (341) app_init: Application information:
I (341) app_init: Project name:     network_adapter
I (345) app_init: App version:      release/ng-v1.0.2-330-g83efce6
I (351) app_init: Compile time:     Oct 18 2024 14:21:32
I (356) app_init: ELF file SHA256:  40ff50890...
--- Warning: Checksum mismatch between flashed and built applications. Checksum of built application is ffcdf9ce083e5e2039400a42c885f3fe41393f85c813c5993136e6e9dda0b1b7
I (361) app_init: ESP-IDF:          v5.4-dev-3602-ga97a7b0962
I (366) efuse_init: Min chip rev:     v0.0
I (370) efuse_init: Max chip rev:     v0.99
I (374) efuse_init: Chip rev:         v0.2
I (378) heap_init: Initializing. RAM available for dynamic allocation:
I (384) heap_init: At 408320B0 len 0004A560 (297 KiB): RAM
I (389) heap_init: At 4087C610 len 00002F54 (11 KiB): RAM
I (394) heap_init: At 50000000 len 00003FE8 (15 KiB): RTCRAM
I (400) spi_flash: detected chip: generic
I (403) spi_flash: flash io: dio
W (406) spi_flash: Detected size(8192k) larger than the size in the binary image header(4096k). Using the size in the binary image header.
I (419) sleep_gpio: Configure to isolate all GPIO pins in sleep state
I (425) sleep_gpio: Enable automatic switching of GPIO sleep configuration
I (431) coexist: coex firmware version: 49d5b702e
I (450) coexist: coexist rom version 5b8dcfa
I (450) main_task: Started on CPU0
I (450) main_task: Calling app_main()
I (450) fg_mcu_slave: *********************************************************************
I (457) fg_mcu_slave:                 ESP-Hosted-MCU Slave FW version :: 0.0.6
I (466) fg_mcu_slave:                 Transport used :: SDIO only
I (473) fg_mcu_slave: *********************************************************************
I (481) fg_mcu_slave: Supported features are:
I (485) fg_mcu_slave: - WLAN over SDIO
I (489) h_bt: - BT/BLE
I (491) h_bt:    - HCI Over SDIO
I (494) h_bt:    - BLE only
I (496) fg_mcu_slave: capabilities: 0xd
I (500) fg_mcu_slave: Supported extended features are:
I (505) h_bt: - BT/BLE (extended)
I (508) fg_mcu_slave: extended capabilities: 0x0
I (516) h_bt: ESP Bluetooth MAC addr: a0:85:e3:af:7c:16
I (517) BLE_INIT: Using main XTAL as clock source
I (526) BLE_INIT: ble controller commit:[5bc682c]
W (527) BLE_INIT: BLE modem sleep is enabled
I (530) BLE_INIT: Bluetooth MAC: a0:85:e3:af:7c:16
I (535) phy_init: phy_version 320,348a293,Sep  3 2024,16:33:12
I (599) phy: libbtbb version: 04952fd, Sep  3 2024, 16:33:30
I (600) SDIO_SLAVE: Using SDIO interface
I (600) SDIO_SLAVE: sdio_init: sending mode: SDIO_SLAVE_SEND_STREAM
I (603) SDIO_SLAVE: sdio_init: ESP32-C6 SDIO RxQ[20] timing[0]

Nach einem Reset meldet sich der ESP32-C6 wie erwartet. Hier kann man sehen, wie ESP-Hosted-MCU auf dem C6 bootet. 

Weitere ausführliche Informationen zu dem ESP32-P4-Module gibt es im Wiki von Waveshare. Dort wird auch sehr detailliert die Entwicklung mit dem ESP-IDF Framework und Visual Studio Code erklärt.

10,1 Zoll MIPI-DSI Touch Display

Das DEV-KIT-C enthält ein sehr gutes 10,1-Zoll MIPI-DSI Display mit einer Auflösung von 800x1280 Pixel im Portrait-Format. Das Display basiert auf einem hochwertigen IPS-Panel mit 178° Betrachtungswinkeln und 400 cd/m² Helligkeit. Die Farben bleiben auch aus schrägen Blickwinkeln stabil und der Kontrast bricht nicht ein.

ESP32-P4-Module-DEV-KIT 10,1 Zoll Display

Ein besonderes Qualitätsmerkmal ist das Optical Bonding: Dabei wird das Deckglas direkt mit dem LCD-Panel verklebt. Das Ergebnis ist ein deutlich besserer Kontrast, sattere Farben und eine erheblich verbesserte Ablesbarkeit bei Sonneneinstrahlung. Gleichzeitig verhindert diese Bauweise das Eindringen von Staub zwischen Glas und Panel.

Die Touch-Funktion basiert auf einem kapazitiven 10-Punkt-Touchscreen, der über I2C angesteuert wird. Das gehärtete Deckglas erreicht eine Härte von 6H, was deutlich widerstandsfähiger als normales Glas ist.

Die Verbindung zum ESP32-P4 erfolgt über ein 22-Pin FPC-Kabel für MIPI-DSI. Das Display unterstützt bis zu 60 Hz Refresh-Rate über die 2-Lane MIPI-DSI Verbindung.

Das folgende Bild zeigt den Stromverbrauch des Boards bei ausgeschaltetem Display und bei voller Helligkeit:

ESP32-P4-Module-DEV-KIT Stromverbrauch

Neben dem 10,1-Zoll Display bietet Waveshare auch eine 7-Zoll Variante (DEV-KIT-B) mit 1024x600 Pixeln im Landscape-Format an. Im Waveshare Wiki gibt es ausführliche Informationen zu dem Display, auch wie man es beispielsweise an einen Raspberry Pi anschließt.

RPi Camera (B)

Im DEV-KIT-A, DEV-KIT-B und DEV-KIT-C ist die Raspberry Pi Camera (B) enthalten. Die Kamera basiert auf dem OV5647 Sensor von Omnivision mit 5 Megapixel Auflösung und unterstützt Videoaufnahmen bis 1080p.

  • Sensor: OV5647, 1/4 Zoll
  • Auflösung: 5 Megapixel, 1080p Video
  • Objektiv: 6mm Brennweite, F2.0 Blende
  • Blickwinkel: 60.6° diagonal
  • Fokus: Einstellbar (manuell)
  • Abmessungen: 32mm x 32mm
  • Anschluss: 15-Pin FFC-Kabel (15cm)

Die Kamera ist kompatibel mit dem MIPI-CSI Anschluss des ESP32-P4-Module-DEV-KIT und verbindet sich über das mitgelieferte Flachbandkabel. Auch zu der Kamera gibt es eine Wiki-Seite, aber hier werden ausschließlich Raspberry Pi Themen behandelt.

LVGL mit dem ESP32-P4-Module-DEV-KIT und ESP-IDF

Die Entwicklung grafischer Benutzeroberflächen auf dem ESP32-P4 unterscheidet sich deutlich von der klassischen ESP32-Entwicklung mit parallelen RGB-Displays. Der Hauptunterschied liegt in der Verwendung von MIPI-DSI statt der parallelen RGB565-Schnittstelle, die bei ESP32-S3 Boards üblich ist. 

MIPI-DSI ist ein serieller Hochgeschwindigkeits-Standard, der ursprünglich für Smartphones entwickelt wurde. Im Gegensatz zu parallelen RGB-Interfaces, die 16-24 Datenleitungen plus zusätzliche Steuersignale benötigen, überträgt MIPI-DSI die Bilddaten über nur wenige differentielle Leitungspaare (typischerweise 1-4 "Lanes"). Jede Lane besteht aus einem differentiellen Paar (D+ und D-) und kann Datenraten von mehreren hundert Megabit pro Sekunde bis zu mehreren Gigabit pro Sekunde erreichen. Der ESP32-P4 besitzt einen nativen MIPI-DSI Controller, der bis zu 4 Lanes unterstützt. Das Waveshare 10,1" Display nutzt eine 2-Lane Konfiguration mit 1500 Mbps pro Lane, was ausreicht für 1280x800 Pixel bei 60 Hz.

Waveshare bietet einige Code-Beispiele für das ESP32-P4-Module-DEV-KIT an, unter anderem auch für LVGL, die jedoch Waveshare-spezifische Komponenten und BSP-Bibliotheken verwenden. Für meine Demo wollte ich aber nur Espressif-Komponenten aus dem ESP Component Registry verwenden. Die Applikation ist sehr minimalistisch und regelt einfach die Helligkeit der Hintergrundbeleuchtung.

Steuerung der Hintergrundbeleuchtung ESP32-P4 Display

Das Layout ist mit EEZ Studio erstellt und enthält nur den Slider, ein Label mit dem Helligkeitswert und eine Action, die bei jeder Änderung aufgerufen wird. Die Struktur der Applikation sieht folgendermaßen aus:

lvgl_demo_waveshare/
├── .gitignore
├── CMakeLists.txt
├── partitions.csv
├── sdkconfig
├── sdkconfig.defaults
│
├── main/
│   ├── CMakeLists.txt
│   ├── idf_component.yml
│   ├── main.cpp
│   ├── display.c                  (Initialisierung I2C, Display, Touch, LVGL)
│   ├── display.h
│   └── ui/                        (EEZ Studio Code)
│       ├── ui.h
│       ├── ui.c
│       ├── screens.h
│       ├── screens.c
│       ├── styles.h
│       ├── styles.c
│       ├── images.h
│       ├── images.c
│       ├── fonts.h
│       ├── actions.h
│       ├── vars.h
│       └── structs.h
│
├── eez-studio/                     (EEZ Studio Projekt)
│
└── managed_components/             (ESP-IDF Komponenten)
    ├── lvgl__lvgl/                  
    ├── espressif__cmake_utilities/
    ├── espressif__esp_lcd_touch/
    ├── espressif__esp_lcd_touch_gt911/
    ├── espressif__esp_lvgl_port/
    └── espressif__esp_lcd_jd9365/

Die Komponenten des Projekts werden in der Datei idf_component.yml definiert:

## IDF Component Manager Manifest File
##
## Only espressif and lvgl components are used.
##
dependencies:
  idf:
    version: '>=5.5.0'
    
  ## LVGL 8.x graphics library
  lvgl/lvgl:
    version: "8.4.*"

  ## ESP LVGL Port - integrates LVGL with ESP-IDF
  ## Handles LVGL task, display driver, touch input
  espressif/esp_lvgl_port:
    version: "^2"

  ## GT911 capacitive touch controller driver
  espressif/esp_lcd_touch_gt911:
    version: "^1"

  ## JD9365 MIPI DSI LCD panel driver (handles DBI commands + DPI panel)
  espressif/esp_lcd_jd9365:
    version: "^2"

Jede Komponente hat eine spezifische Aufgabe:

  • lvgl/lvgl: Die eigentliche LVGL-Grafikbibliothek (Version 8.4)
  • espressif/esp_lvgl_port: Verbindungsschicht zwischen ESP-IDF und LVGL - verwaltet den LVGL-Task, Display-Buffer und Touch-Input
  • espressif/esp_lcd_touch_gt911: Treiber für den Goodix GT911 Touch-Controller
  • espressif/esp_lcd_jd9365: Treiber für den JD9365 Display-Controller (unterstützt MIPI-DSI + DBI-Kommandos)

Der weitaus komplexeste Teil des Programms ist die Initialisierung des Hardware. Sie läuft in mehreren Schritten ab, die ich hier etwas genauer erklären möchte:

I2C-Bus Initialisierung

Der I2C-Bus wird für zwei Komponenten benötigt: den Touch-Controller (GT911) und den Backlight-Controller. Beide Geräte teilen sich den gleichen Bus:

#define I2C_SCL_PIN         GPIO_NUM_8
#define I2C_SDA_PIN         GPIO_NUM_7
#define I2C_PORT_NUM        I2C_NUM_0
#define I2C_CLK_SPEED_HZ    400000

static esp_err_t init_i2c(void)
{
    ESP_LOGI(TAG, "Initializing I2C bus (SCL=%d, SDA=%d, %d Hz)",
             I2C_SCL_PIN, I2C_SDA_PIN, I2C_CLK_SPEED_HZ);

    i2c_master_bus_config_t bus_cfg = {
        .clk_source = I2C_CLK_SRC_DEFAULT,
        .i2c_port   = I2C_PORT_NUM,
        .scl_io_num = I2C_SCL_PIN,
        .sda_io_num = I2C_SDA_PIN,
        .glitch_ignore_cnt = 7,
        .flags.enable_internal_pullup = true,
    };
    
    return i2c_new_master_bus(&bus_cfg, &s_i2c_bus);
}

Der I2C-Bus läuft mit 400 kHz (Fast Mode) und nutzt die internen Pull-Up-Widerstände des ESP32-P4. Der glitch_ignore_cnt Parameter hilft, kurze Störimpulse auf den Leitungen zu ignorieren.

Backlight-Controller Initialisierung

Das Waveshare Display verwendet einen I2C-gesteuerten Backlight-Controller an Adresse 0x45. Die Helligkeit wird über Register 0x96 gesteuert:

#define BL_I2C_ADDR         0x45
#define BL_BRIGHTNESS_REG   0x96

static esp_err_t init_backlight(void)
{
    ESP_LOGI(TAG, "Initializing backlight (I2C addr=0x%02X)", BL_I2C_ADDR);

    i2c_device_config_t dev_cfg = {
        .dev_addr_length = I2C_ADDR_BIT_LEN_7,
        .device_address  = BL_I2C_ADDR,
        .scl_speed_hz    = 100000,
    };
    ESP_RETURN_ON_ERROR(i2c_master_bus_add_device(s_i2c_bus, &dev_cfg, &s_bl_dev),
                        TAG, "Failed to add backlight I2C device");

    uint8_t cmd1[] = {0x95, 0x11};
    ESP_RETURN_ON_ERROR(i2c_master_transmit(s_bl_dev, cmd1, sizeof(cmd1), 50),
                        TAG, "Backlight enable step 1 failed");
    uint8_t cmd2[] = {0x95, 0x17};
    ESP_RETURN_ON_ERROR(i2c_master_transmit(s_bl_dev, cmd2, sizeof(cmd2), 50),
                        TAG, "Backlight enable step 2 failed");
    uint8_t cmd3[] = {0x96, 0x00};
    ESP_RETURN_ON_ERROR(i2c_master_transmit(s_bl_dev, cmd3, sizeof(cmd3), 50),
                        TAG, "Backlight off failed");

    vTaskDelay(pdMS_TO_TICKS(100));

    display_set_brightness(10);

    vTaskDelay(pdMS_TO_TICKS(1000));

    ESP_LOGI(TAG, "Backlight initialized (full brightness)");
    return ESP_OK;
}

Die Helligkeitssteuerung erfolgt über eine einfache I2C-Transaktion:

esp_err_t display_set_brightness(uint8_t val)
{
    uint8_t cmd[] = {BL_BRIGHTNESS_REG, val};
    return i2c_master_transmit(s_bl_dev, cmd, sizeof(cmd), 50);
}

Die Werte gehen von 0 bis 255, wobei das Display erst bei einem Wert ab 4 die Hintergrundbeleuchtung einschaltet.

MIPI-DSI Display-Panel Initialisierung

Dies ist der komplexeste Teil der Initialisierung. Er besteht aus mehreren Unterschritten:

MIPI-DSI PHY Stromversorgung

Der MIPI-DSI PHY (Physical Layer) benötigt eine dedizierte Stromversorgung von 2,5V. Der ESP32-P4 besitzt interne LDOs (Low-Dropout-Regulatoren), die diese Spannung bereitstellen:

#define DSI_PHY_LDO_CHAN    3
#define DSI_PHY_LDO_MV     2500

esp_ldo_channel_handle_t phy_pwr = NULL;
esp_ldo_channel_config_t ldo_cfg = {
    .chan_id     = DSI_PHY_LDO_CHAN,
    .voltage_mv = DSI_PHY_LDO_MV,
};
ESP_RETURN_ON_ERROR(esp_ldo_acquire_channel(&ldo_cfg, &phy_pwr),
                    TAG, "Failed to acquire LDO channel");

DSI-Bus Erstellung

Der DSI-Bus wird mit einer 2-Lane-Konfiguration erstellt. Die Espressif-Komponente esp_lcd_jd9365 stellt vorgefertigte Makros zur Verfügung:

esp_lcd_dsi_bus_config_t bus_cfg = JD9365_PANEL_BUS_DSI_2CH_CONFIG();
ESP_RETURN_ON_ERROR(esp_lcd_new_dsi_bus(&bus_cfg, &s_dsi_bus),
                    TAG, "Failed to create DSI bus");

Das Makro JD9365_PANEL_BUS_DSI_2CH_CONFIG() expandiert zu einer vollständigen Konfigurationsstruktur mit den typischen Parametern für 2-Lane DSI (1500 Mbps pro Lane).

DBI Command Interface

MIPI-DSI Panels unterstützen zwei Modi:

  • DPI (Display Pixel Interface): Kontinuierlicher Video-Stream für die Bilddarstellung
  • DBI (Display Bus Interface): Kommando-Modus für Konfigurationsregister

Das DBI-Interface wird benötigt, um Initialisierungsbefehle an den JD9365-Controller zu senden:

esp_lcd_panel_io_handle_t dbi_io = NULL;
esp_lcd_dbi_io_config_t dbi_cfg = JD9365_PANEL_IO_DBI_CONFIG();
ESP_RETURN_ON_ERROR(esp_lcd_new_panel_io_dbi(s_dsi_bus, &dbi_cfg, &dbi_io),
                    TAG, "Failed to create DBI panel IO");

JD9365 Panel mit Waveshare-spezifischen Init-Commands

Der JD9365 ist ein generischer Display-Controller, der für verschiedene Panels verwendet werden kann. Jedes Panel benötigt eine spezifische Initialisierungssequenz, die Parameter wie Gamma-Kurven, GIP-Timing (Gate-In-Panel), Stromversorgung und Display-Timing konfiguriert.

Diese Sequenz ist herstellerspezifisch und wird als Array von Kommandos definiert. Ich habe diese Werte aus dem Waveshare Code kopiert:

static const jd9365_lcd_init_cmd_t s_ws_init_cmds[] = {
    /* Page 0: Unlock password & DSI lane config */
    {0xE0, (uint8_t[]){0x00}, 1, 0},
    {0xE1, (uint8_t[]){0x93}, 1, 0},
    {0xE2, (uint8_t[]){0x65}, 1, 0},
    {0xE3, (uint8_t[]){0xF8}, 1, 0},
    {0x80, (uint8_t[]){0x01}, 1, 0},   /* DSI 2-lane mode */
    
    /* Page 1: Power and Gamma curves */
    {0xE0, (uint8_t[]){0x01}, 1, 0},
    // ... viele weitere Kommandos
};

Der JD9365 verwendet ein paged register model: Register 0xE0 wählt die aktive "Page" aus, danach beziehen sich alle Schreibzugriffe auf Register dieser Page. Dies ist ein gängiges Muster bei Display-Controllern, um mehr als 256 Register addressieren zu können.

Die vollständige Panel-Konfiguration:

esp_lcd_dpi_panel_config_t dpi_cfg =
    JD9365_800_1280_PANEL_60HZ_DPI_CONFIG(LCD_COLOR_PIXEL_FORMAT_RGB565);
dpi_cfg.num_fbs = 2;  /* Double buffering */
dpi_cfg.video_timing.vsync_back_porch = 10;  /* Waveshare-specific */

jd9365_vendor_config_t vendor_cfg = {
    .init_cmds      = s_ws_init_cmds,
    .init_cmds_size = sizeof(s_ws_init_cmds) / sizeof(s_ws_init_cmds[0]),
    .mipi_config = {
        .dsi_bus    = s_dsi_bus,
        .dpi_config = &dpi_cfg,
        .lane_num   = 2,
    },
};

esp_lcd_panel_dev_config_t panel_cfg = {
    .reset_gpio_num = GPIO_NUM_NC,  /* Kein separater Reset-Pin */
    .rgb_ele_order  = LCD_RGB_ELEMENT_ORDER_RGB,
    .bits_per_pixel = 16,  /* RGB565 */
    .vendor_config  = &vendor_cfg,
};

ESP_RETURN_ON_ERROR(esp_lcd_new_panel_jd9365(dbi_io, &panel_cfg, &s_panel),
                    TAG, "Failed to create JD9365 panel");

Wichtig ist num_fbs = 2 für Double Buffering: Der ESP32-P4 rendert in einen Buffer, während der andere zum Display übertragen wird. Das vermeidet Tearing-Effekte.

Panel aktivieren

Nach dem Erstellen des Panel-Handles müssen noch drei finale Schritte ausgeführt werden:

ESP_RETURN_ON_ERROR(esp_lcd_panel_reset(s_panel), TAG, "Panel reset failed");
ESP_RETURN_ON_ERROR(esp_lcd_panel_init(s_panel), TAG, "Panel init failed");
ESP_RETURN_ON_ERROR(esp_lcd_panel_disp_on_off(s_panel, true),
                    TAG, "Panel display-on failed");
  • esp_lcd_panel_reset(): Führt einen Software-Reset durch
  • esp_lcd_panel_init(): Sendet alle Init-Commands an das Panel
  • esp_lcd_panel_disp_on_off(true): Schaltet das Display ein (SLPOUT + DISPON Kommandos)

Touch-Controller Initialisierung

Der GT911 Touch-Controller kommuniziert über I2C und wird über das generische esp_lcd_touch Interface angesprochen:

#define TOUCH_I2C_ADDR  ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS

esp_lcd_panel_io_handle_t tp_io = NULL;
esp_lcd_panel_io_i2c_config_t tp_io_cfg = {
    .dev_addr            = TOUCH_I2C_ADDR,
    .control_phase_bytes = 1,
    .dc_bit_offset       = 0,
    .lcd_cmd_bits        = 16,
    .flags = {
        .disable_control_phase = 1,
    },
    .scl_speed_hz = I2C_CLK_SPEED_HZ,
};
ESP_RETURN_ON_ERROR(esp_lcd_new_panel_io_i2c(s_i2c_bus, &tp_io_cfg, &tp_io),
                    TAG, "Failed to create touch I2C panel IO");

esp_lcd_touch_config_t touch_cfg = {
    .x_max        = PHYS_H_RES,  /* 800 */
    .y_max        = PHYS_V_RES,  /* 1280 */
    .rst_gpio_num = GPIO_NUM_NC,
    .int_gpio_num = GPIO_NUM_NC,
    .levels = {
        .reset     = 0,
        .interrupt = 0,
    },
    .flags = {
        .swap_xy  = 0,
        .mirror_x = 0,
        .mirror_y = 0,
    },
};

ESP_RETURN_ON_ERROR(
    esp_lcd_touch_new_i2c_gt911(tp_io, &touch_cfg, &s_touch),
    TAG, "Failed to create GT911 touch driver");

Die Touch-Konfiguration verwendet die physische Auflösung (800x1280 im Portrait-Modus). Die Rotation wird später in LVGL gehandhabt.

LVGL Integration

Der letzte Schritt verbindet alles mit LVGL. Das esp_lvgl_port Package übernimmt dabei die gesamte Integration:

LVGL Port Initialisierung

const lvgl_port_cfg_t port_cfg = ESP_LVGL_PORT_INIT_CONFIG();
ESP_RETURN_ON_ERROR(lvgl_port_init(&port_cfg), TAG, "LVGL port init failed");

Dies erstellt den LVGL-Task, initialisiert die Timer und richtet die Thread-Sicherheit ein (wichtig für Multi-Core-Systeme wie den ESP32-P4).

Display registrieren

const lvgl_port_display_cfg_t disp_cfg = {
    .io_handle      = NULL,
    .panel_handle   = s_panel,
    .control_handle = NULL,
    .buffer_size    = PHYS_H_RES * 100,  /* 800 * 100 Pixel */
    .double_buffer  = true,
    .hres           = PHYS_H_RES,  /* 800 */
    .vres           = PHYS_V_RES,  /* 1280 */
    .monochrome     = false,
    .rotation = {
        .swap_xy  = false,
        .mirror_x = false,
        .mirror_y = false,
    },
    .flags = {
        .buff_dma    = false,
        .buff_spiram = true,  /* Buffer im PSRAM */
        .sw_rotate   = true,  /* Software-Rotation aktivieren */
    },
};

const lvgl_port_display_dsi_cfg_t dsi_cfg = {
    .flags = {
        .avoid_tearing = false,  /* Muss false sein für sw_rotate */
    },
};

s_lvgl_display = lvgl_port_add_disp_dsi(&disp_cfg, &dsi_cfg);

Wichtige Parameter:

  • buffer_size = PHYS_H_RES * 100: Der Render-Buffer ist 100 Zeilen hoch. Größere Buffer = schneller, aber mehr RAM-Verbrauch.
  • double_buffer = true: LVGL verwendet zwei Buffer für flüssiges Rendering
  • buff_spiram = true: Buffer liegen im PSRAM (32 MB), nicht im begrenzten SRAM
  • sw_rotate = true: Aktiviert Software-Rotation (siehe nächster Abschnitt)

Landscape-Modus via Software-Rotation

Das Display ist physisch im Portrait-Modus verdrahtet (800x1280), aber für viele Anwendungen ist Landscape (1280x800) besser geeignet. Ich habe versucht, über die Hardware-Kommandos das Display zu drehen, aber das hat nicht funktioniert. Scheinbar ist das Display wirklich fix auf den Portrait-Modus eingestellt. Daher verwende ich die Software-Rotation von LVGL:

lvgl_port_lock(0);
lv_disp_set_rotation(s_lvgl_display, LV_DISP_ROT_90);
lvgl_port_unlock();

Nach dieser Rotation rendert LVGL in 1280x800 und rotiert das Bild intern, bevor es zum Display gesendet wird. Das kostet etwas Performance, ist aber deutlich flexibler als Hardware-Rotation. Die Variable avoid_tearing muss auf false stehen, wenn sw_rotate aktiv ist. Ansonsten kommt es zu Konflikten im Buffer-Management.

Touch-Input registrieren

const lvgl_port_touch_cfg_t touch_cfg = {
    .disp   = s_lvgl_display,
    .handle = s_touch,
};
lv_indev_t *touch_indev = lvgl_port_add_touch(&touch_cfg);

Das esp_lvgl_port Package übernimmt automatisch die Touch-Koordinaten-Transformation passend zur Display-Rotation.

Die Haupt-Applikation

Mit allen Initialisierungsschritten abgeschlossen, kann die eigentliche GUI-Anwendung starten:

extern "C" void app_main(void)
{
    ESP_LOGI(TAG, "============================================");
    ESP_LOGI(TAG, " ESP32-P4 + 10.1\" DSI Display Demo");
    ESP_LOGI(TAG, "============================================");

    /* Initialize display subsystem */
    ESP_ERROR_CHECK(display_init());

    /* Create GUI (from EEZ-Studio) */
    lvgl_port_lock(0);
    ui_init();  /* EEZ-generated code */
    lvgl_port_unlock();

    ESP_LOGI(TAG, "============================================");
    ESP_LOGI(TAG, " Initialization complete!");
    ESP_LOGI(TAG, "============================================");
}

Die Helligkeit kann dann zur Laufzeit über den Slider gesteuert werden:

void action_slider_set_backlight_brightness(lv_event_t * e) {
    lv_obj_t * slider = lv_event_get_target(e);
    int32_t value = lv_slider_get_value(slider);
    
    display_set_brightness(value);
    
    /* Update label */
    char str[10];
    sprintf(str, "%lu", value);
    lv_label_set_text(objects.label_brightness, str);
}

Zusammenfassung

Wie man sieht, ist die Initialisierung eines MIPI-DSI Displays deutlich komplexer als bei parallelen Displays mit dem ESP32-S3. Sie folgt aber einem klaren, modularen Aufbau:

  • I2C-Bus für Touch und Backlight
  • MIPI-DSI PHY Stromversorgung
  • DSI-Bus mit 2 Lanes erstellen
  • DBI Command Interface für Panel-Konfiguration
  • JD9365 Panel mit herstellerspezifischen Init-Commands
  • GT911 Touch über I2C
  • LVGL Integration mit Software-Rotation

Bei Verwendung der Waveshare Komponenten ist der Code deutlich kürzer, aber mir war wichtig, die einzelnen Schritte nachvollziehen zu können und auch zu verstehen. Der Code für diese kleine Demo liegt auf GitHub.

Fazit

An dieser Stelle möchte ich mich zunächst bei Waveshare bedanken, die mir das ESP32-P4-Module-DEV-KIT-C für diesen Review zur Verfügung gestellt haben.

Nachdem ich mich nun einige Tage intensiv mit dem Board beschäftigt habe, fällt mein Urteil durchweg positiv aus. Das DEV-KIT-C ist ein kompaktes und trotzdem sehr leistungsstarkes Board, mit dem sich viele interessante Projekte umsetzen lassen. 

Das absolute Highlight ist das große 10,1 Zoll Display. Dank des hochwertigen IPS-Panels sind Kontrast, Farbdarstellung und Blickwinkelstabilität absolut hervorragend. Zwar erfordert der Umstieg von klassischen ESP32-S3-Boards mit parallelem RGB-Interface hin zu MIPI-DSI eine steilere Lernkurve und eine deutlich komplexere Initialisierung, aber mit den Beispielen von Waveshare lässt sich diese Hürde schnell nehmen.

Einen echten Schwachpunkt konnte ich nicht finden. Einzig vielleicht das fehlende Kabel zum Flashen des ESP32-C6, aber das ist Jammern auf sehr hohem Niveau. Im Gegenteil: Die Tatsache, dass sich die C6-Firmware über die SH1.0-Stecker unabhängig aktualisieren lässt, ist ein großer Pluspunkt. Es macht das Board zukunftssicher für ESP-NOW-Anwendungen, sobald Espressif dieses Feature final in die ESP-Hosted-MCU-Firmware integriert.

Ich denke, ich habe endlich das Display für die nächste Generation meiner Wetterstation gefunden.

Konversation wird geladen