Skip to Content

Erste Schritte mit dem CrowPanel 2,1-Zoll-HMI ESP32 Rotary Display

Erste Schritte mit dem CrowPanel 2,1-Zoll-HMI ESP32 Rotary Display

Das CrowPanel 2.1inch-HMI ESP32 Rotary Display von Elecrow ist ein großes, rundes Modul, das einen ESP32-S3 Mikrocontroller, einen 2,1-Zoll 480×480 IPS Rund-Touchscreen und einen Drehencoder mit Druckknopf in einer Einheit vereint.

In diesem Tutorial gehen wir die wesentlichen Schritte durch, um mit diesem Display zu starten – von der ersten Einrichtung bis zum Testen der Anzeige- und Eingabefunktionen. Du lernst, wie du die Daten des Drehencoders für eine flüssige Benutzereingabe ausliest und Berührungsereignisse des runden Displays erfasst.

Benötigte Teile

Für dieses Projekt benötigst du nur das CrowPanel 2.1inch-HMI ESP32 Rotary Display. Das Displaymodul wird mit einem USB-Kabel und GPIO-Stecker geliefert, sodass keine weitere Hardware erforderlich ist.

CrowPanel 2.1inch-HMI ESP32 Rotary Display

Makerguides is a participant in affiliate advertising programs designed to provide a means for sites to earn advertising fees by linking to Amazon, AliExpress, Elecrow, and other sites. As an Affiliate we may earn from qualifying purchases.

Hardware des CrowPanel 2.1inch-HMI ESP32 Rotary Display

Das CrowPanel 2.1inch-HMI ESP32 Rotary Display ist ein Displaymodul, das ein hochauflösendes rundes IPS-Display, einen kapazitiven Touchscreen und einen Drehencoder mit Druckknopf-Funktion kombiniert.

Das Modul basiert auf dem ESP32‑S3R8 System-on-Chip, der einen 32-Bit Xtensa LX7 Dual-Core-Prozessor mit bis zu 240 MHz Taktfrequenz nutzt. Es ist mit 8 Mbit PSRAM und 16 MByte Onboard-Flashspeicher ausgestattet.

Der Displayteil ist ein rundes 2,1-Zoll RGB IPS-Panel (ST7701 Controller) mit einer Auflösung von 480 × 480 Pixeln, einer kapazitiven Touch-Overlay-Schicht für vollflächige Touch-Bedienung und einer Hintergrundbeleuchtung.

CrowPanel 2.1inch-HMI ESP32 Rotary Display
CrowPanel 2.1inch-HMI ESP32 Rotary Display (source)

Physikalisch verfügt die Hardware über einen Drehencoder-Knopf. Der Knopf kann im Uhrzeigersinn und gegen den Uhrzeigersinn gedreht werden und lässt sich zudem nach innen drücken (wie ein Schalter) für Benutzereingaben.

Zur Konnektivität und Erweiterung unterstützt das Modul sowohl drahtlose als auch kabelgebundene Schnittstellen. Drahtlos sind IEEE 802.11 a/b/g/n (2,4 GHz) WiFi sowie Bluetooth Low Energy (BLE) / Bluetooth 5.0 verfügbar.

Kabelgebunden bietet das Modul eine 5 V Lade-/Schnittstellenbuchse, die sowohl als Stromversorgung als auch als Programmieranschluss dient. Zusätzlich gibt es drei Erweiterungsschnittstellen: eine UART-Schnittstelle, eine I2C-Schnittstelle und einen 12-poligen FPC-Stecker für weitere Verbindungen.

Connectors of the CrowPanel 2.1inch-HMI ESP32 Rotary Display
Anschlüsse des CrowPanel 2.1inch-HMI ESP32 Rotary Display (source)

Pin-Belegungen

Die folgende Tabelle listet die Pins auf, die zur Steuerung aller Komponenten des Displaymoduls benötigt werden:

FunktionsgruppeSignal / ZweckPin-NummerAnmerkungen
I²C-SchnittstelleSDA38Primäre I²C-Datenleitung
SCL39Primäre I²C-Taktleitung
DrehencoderEncoder A42Quadraturkanal A
Encoder B4Quadraturkanal B
Display-HintergrundbeleuchtungBacklight-Steuerung6PWM-fähiger Pin zur Helligkeitssteuerung der LCD-Hintergrundbeleuchtung
OLED-SteuerungRESET–1Kein Reset-Pin verwendet (Software-Reset)
RGB-Display-SchnittstelleCS16Chip-Select-Leitung
SCK2Display-Serientakt
SDA1Display-Seriendaten
DE40Datenaktivierung
VSYNC7Vertikale Synchronisation
HSYNC15Horizontale Synchronisation
PCLK41Pixeltakt
RGB-Farb-Datenpins (5-6-5 Format)R046Rot Bit 0
R13Rot Bit 1
R28Rot Bit 2
R318Rot Bit 3
R417Rot Bit 4
G014Grün Bit 0
G113Grün Bit 1
G212Grün Bit 2
G311Grün Bit 3
G410Grün Bit 4
G59Grün Bit 5
B05Blau Bit 0
B145Blau Bit 1
B248Blau Bit 2
B347Blau Bit 3
B421Blau Bit 4
PCF8574 I/O ExpanderI²C-Adresse0x21Verbunden mit Pins 38 (SDA) und 39 (SCL)

Technische Spezifikation

Die folgende Tabelle fasst die technischen Spezifikationen des CrowPanel 2.1inch-HMI ESP32 Rotary Display Moduls zusammen:

SpezifikationskategorieDetails
HauptprozessorSoC: Xtensa LX7 Dual-Core (32-Bit) im ESP32‑S3R8 (bis zu 240 MHz)
Speicher & SpeicherplatzSystem-RAM: 512 kB SRAM; PSRAM: 8 MB; Flash: 16 MB
Display-PanelGröße: 2,1 Zoll; Typ: IPS; Auflösung: 480 × 480 Pixel; Touch: kapazitiver Vollbild-Touch-Overlay
BenutzereingabemechanismenDreh-“Knopf” (Uhrzeigersinn / gegen Uhrzeigersinn + kompletter Druck); kapazitive Touch-Eingabe auf der Displayoberfläche
Drahtlose KonnektivitätWiFi: IEEE 802.11 a/b/g/n (2,4 GHz); Bluetooth: BLE / Bluetooth 5.0
Schnittstellen / Erweiterungen5 V Eingang (Laden und Programmieren); UART-Schnittstelle: 1× UART0 + 1× UART1 via ZX-MX 1.25-4P; I²C-Schnittstelle; 12-poliger FPC-Stecker (Strom/Programmierung und GPIO)
Tasten & AnzeigenOnboard RESET-Taste; BOOT-Taste; Knopf-Druckschalter (Bestätigungstaste); Power-LED; LCD-Hintergrundbeleuchtung
Stromversorgung & SpannungModul-Eingang: 5 V / 1 A; Hauptchip Logikpegel: 3,3 V; Betrieb des Moduls bei 5 V nominal
Mechanisch / PhysikalischAbmessungen: 79 × 79 × 30 mm; Nettogewicht: 80 g; Gehäuse: Aluminiumlegierung + Kunststoff + Acryl
TemperaturbereichBetrieb: –20 °C bis +65 °C; Lagerung: –40 °C bis +80 °C
Software- / EntwicklungsumgebungKompatibel mit Arduino IDE, ESP-IDF, Lua RTOS, MicroPython, PlatformIO; UI-Grafikbibliothek: LVGL unterstützt

Installation des ESP32 Core

Wenn dies dein erstes Projekt mit einem Board der ESP32-Serie ist, musst du zuerst den ESP32 Core installieren. Für das CrowPanel 2.1inch Display benötigst du jedoch eine spezifische Version (2.0.14) des ESP32 Core.

Öffne zunächst den Preferences-Dialog in der Arduino IDE über „Preferences…“ im „File“-Menü. Es öffnet sich der unten gezeigte Preferences-Dialog.

Unter dem Reiter Settings findest du unten im Dialog ein Eingabefeld mit der Bezeichnung „Additional boards manager URLs“:

Additional boards manager URLs in Preferences
Zusätzliche URLs für den Boards Manager in den Preferences

Kopiere in dieses Eingabefeld die folgende URL:

https://espressif.github.io/arduino-esp32/package_esp32_dev_index.json

Damit weiß die Arduino IDE, wo sie die ESP32 Core-Bibliotheken findet. Als Nächstes installieren wir die ESP32 Boards über den Boards Manager.

Öffne den Boards Manager über „Tools -> Boards -> Board Manager“. Der Boards Manager erscheint in der linken Seitenleiste. Gib oben im Suchfeld „ESP32“ ein, und du solltest zwei Arten von ESP32 Boards sehen: die „Arduino ESP32 Boards“ und die „esp32 by Espressif“ Boards. Wir wollen die „esp32 libraries by Espressif“.

Wähle im Dropdown-Menü die Version „2.0.14“ aus und klicke dann auf den „INSTALL“-Button. Der Screenshot unten zeigt, wie der Boards Manager nach erfolgreicher Installation des ESP32 Core 2.0.14 aussehen sollte:

Install ESP32 Core 2.0.14
ESP32 Core 2.0.14 installieren

Beachte, dass du Versionen bis 2.0.17 installieren kannst, aber nicht 3.x. Die Bibliotheken, die wir im späteren Abschnitt installieren, funktionieren nicht mit dem ESP32 Core 3.x.

Board-Auswahl

Zum Schluss müssen wir ein ESP32 Board auswählen. Für das CrowPanel 2.1inch-HMI ESP32 Rotary Display wählen wir das generische „ESP32S3 Dev Module“. Klicke dazu im Dropdown-Menü auf „Select other board and port…“:

Drop-down Menu for Board Selection
Dropdown-Menü zur Board-Auswahl

Es öffnet sich ein Dialog, in dem du „esp32s3 dev“ in die Suchleiste eingibst. Du siehst das Board „ESP32S3 Dev Module“ unter Boards. Klicke darauf, wähle den COM-Port aus, um es zu aktivieren, und bestätige mit OK:

Board Selection Dialog "ESP32S3 Dev Module" board
Board-Auswahl-Dialog „ESP32S3 Dev Module“ Board

Beachte, dass du das Board per USB-Kabel mit deinem Computer verbinden musst, bevor du einen COM-Port auswählen kannst. Verbinde das mitgelieferte USB-Kabel des CrowPanel Display Moduls direkt mit dem Anschluss „USB-5V-IN“, wie unten gezeigt:

USB Port of CrowPanel Display Module
USB-Anschluss des CrowPanel Display Moduls

Tool-Einstellungen

Nachfolgend findest du die Einstellungen, die du für das CrowPanel Display Modul in der Arduino IDE unter dem Tools-Menü verwenden solltest.

Tool settings for CrowPanel 2.1inch-HMI ESP32 Rotary Display
Tool-Einstellungen für CrowPanel 2.1inch-HMI ESP32 Rotary Display

Wichtig für die Codebeispiele in den folgenden Abschnitten ist, USB CDC on Boot auf „Enabled“ zu setzen. So kannst du Daten über die serielle Schnittstelle senden und empfangen, was für das Debugging nötig ist. Außerdem müssen „OPI PSRAM“, „Huge APP“ und „Flash Size“ korrekt für die Display-Codebeispiele eingestellt sein.

Bibliotheken installieren

Als Nächstes müssen wir spezifische Bibliotheken und Versionen installieren, damit der Code für das CrowPanel 2.1inch-HMI ESP32 Rotary Display funktioniert.

Gehe zum Elecrow github repo für das CrowPanel 2.1inch-HMI Display. Klicke auf den grünen „<> Code“-Button und dann auf „Download ZIP“, um das Repository als ZIP-Datei herunterzuladen:

Entpacke anschließend die ZIP-Datei, um deren Inhalt zu extrahieren. Du solltest folgende Dateien in einem entpackten Ordner namens „CrowPanel-2.1inch-HMI-ESP32-Rotary-Display-480-480-IPS-Round-Touch-Knob-Screen-master“ sehen:

Ignoriere die Dateien, die sich auf das „CrowPanel 1.28inch …“ Display beziehen. Wir müssen nur den Inhalt des Ordners „example/libraries“ in den „libraries“-Ordner der Arduino IDE kopieren. Unter Windows befindet sich der „libraries“-Ordner typischerweise hier:

C:\Users\<username>\OneDrive\Documents\Arduino\libraries

Da dieser Ordner bereits installierte Bibliotheken enthält, empfehle ich, ihn vorübergehend umzubenennen, z. B. in „_libraries“, und einen neuen Ordner namens „libraries“ anzulegen. So vermeidest du Konflikte mit bereits installierten Bibliotheken und verlierst sie nicht. Später kannst du die Änderung leicht rückgängig machen. Das Bild unten zeigt, wie dein „Arduino“-Ordner mit den Bibliotheken aussehen sollte:

Kopiere nun die Dateien aus dem Ordner „example/libraries“ in den neuen „libraries“-Ordner, wie unten gezeigt:

Du musst die durchgestrichenen Bibliotheken nicht kopieren, da sie mit der LVGL UI zusammenhängen, die wir nicht verwenden werden. In den nächsten Abschnitten zeige ich dir einige Codebeispiele, um das Display auszuprobieren.

Codebeispiel: Serielle Schnittstelle

Wir beginnen mit dem Test der seriellen Kommunikation. Öffne deine Arduino IDE, gib den folgenden Code ein und lade ihn auf das CrowPanel Display Modul hoch.

void setup() {
  Serial.begin(115200);
}

void loop() {
  Serial.println("Makerguides");
  delay(2000);
}

Öffne dann den Serial Monitor, und du solltest alle zwei Sekunden den Text „Makerguides“ sehen. Falls nicht, überprüfe deine Tool-Einstellungen und die Baudrate (115200). Wichtig ist, dass „USB CDC on Boot“ auf „Enabled“ steht.

Codebeispiel: Encoder

Im nächsten Codebeispiel lernst du, wie du den Encoder und den Encoder-Schalter nutzt. Wir verwenden eine interruptgesteuerte Methode, um einen quadraturkodierten Drehencoder auszulesen und den PCF8574, um den Status des Encoder-Druckknopfs zu erfassen.

Die Encoder-Drehung wird in einer schnellen ISR mit Richtungserkennung verarbeitet, während die Hauptschleife sich auf das Melden von Änderungen und das Auslesen des Knopfzustands konzentriert. Schau dir den Code zuerst kurz an, dann besprechen wir die Details.

#include <Wire.h>
#include <PCF8574.h>

// I2C to PCF8574
#define I2C_SDA_PIN 38
#define I2C_SCL_PIN 39

#define ENCODER_CLK 42  
#define ENCODER_DT 4   

volatile int counter = 0;
volatile int encState = 0;
volatile int oldState = -1;
volatile bool hasChanged = true;

PCF8574 pcf8574(0x21);

void IRAM_ATTR encoder_irq() {
  encState = digitalRead(ENCODER_CLK);
  if (encState != oldState) {
    counter += (digitalRead(ENCODER_DT) == encState) ? +1 : -1;
    oldState = encState;
    hasChanged = true;
  }
}

void initPins() {
  Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
  pcf8574.pinMode(P5, INPUT_PULLUP);  //encoder SW
  if (!pcf8574.begin()) {
    Serial.println("Can't init pcf8574");
  }
}

void initEncoder() {
  pinMode(ENCODER_CLK, INPUT_PULLUP);
  pinMode(ENCODER_DT, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(ENCODER_CLK), encoder_irq, CHANGE);
}

void setup() {
  Serial.begin(115200);
  initPins();
  initEncoder();
}

void loop() {
  if (hasChanged) {
    hasChanged = false;
    Serial.printf("COUNTER: %d\n", counter);
  }
  int button = pcf8574.digitalRead(P5, true);
  if (!button) {
    Serial.printf("BTN pressed\n");
    delay(500);
  }  
  delay(1);
}

Includes

Das Sketch beginnt mit dem Einbinden zweier Bibliotheken, die die I2C-Kommunikation und den PCF8574 I/O Expander handhaben. Diese Header müssen eingebunden sein, bevor du Wire oder PCF8574 Objekte verwenden kannst.

#include <Wire.h>
#include <PCF8574.h>

Wire.h stellt die Standard-Arduino-I2C-(TWI)-Schnittstelle bereit. PCF8574.h kapselt den Low-Level-I2C-Zugriff auf den PCF8574-Chip, sodass du seine Pins fast wie normale digitale Pins behandeln kannst.

Konstanten

Als Nächstes definiert der Code die Pins für I2C und den Drehencoder:

#define I2C_SDA_PIN 38
#define I2C_SCL_PIN 39

#define ENCODER_CLK 42  
#define ENCODER_DT 4   

I2C_SDA_PIN und I2C_SCL_PIN legen fest, welche ESP32-Pins als SDA und SCL für den Hardware-I2C-Bus verwendet werden. Beim ESP32 kann I2C auf verschiedene Pins gelegt werden, daher ist die Angabe hier notwendig.

ENCODER_CLK und ENCODER_DT sind die beiden Quadraturausgänge des Drehencoders. Das CLK-Signal ist typischerweise der Hauptkanal für den Interrupt, und DT ist der zweite Kanal zur Bestimmung der Drehrichtung.

Globale Zustände und volatile Variablen

Der Code verwendet mehrere globale Variablen, um den Encoder-Zustand zu verfolgen. Diese Variablen sind mit volatile markiert, da sie innerhalb einer Interrupt-Service-Routine geändert werden.

volatile int counter = 0;
volatile int encState = 0;
volatile int oldState = -1;
volatile bool hasChanged = true;

counter hält die akkumulierte Position des Encoders. Jeder Schritt des Encoders erhöht oder verringert diesen Wert.

encState repräsentiert den aktuellen Logikpegel des Encoder-CLK-Pins, wie er im Interrupt-Handler gesehen wird. oldState speichert den vorherigen CLK-Zustand, damit der Code Übergänge erkennen und Mehrfachzählungen vermeiden kann.

hasChanged wird als Flag verwendet, um der Hauptschleife zu signalisieren, dass der Zähler im Interrupt aktualisiert wurde. Die Markierung als volatile teilt dem Compiler mit, dass diese Variablen sich jederzeit ändern können und nicht in Registern zwischengespeichert werden dürfen – wichtig, wenn sie von einer ISR geändert werden.

PCF8574 Objekt

Der Code erstellt dann eine globale Instanz der PCF8574-Klasse und gibt die I2C-Adresse des Chips an.

PCF8574 pcf8574(0x21);

Diese Zeile teilt der PCF8574-Bibliothek mit, dass der I/O Expander unter der I2C-Adresse 0x21 erreichbar ist. Alle folgenden Aufrufe von pcf8574.pinMode oder pcf8574.digitalRead verwenden diese Adresse, um mit diesem Gerät auf dem I2C-Bus zu kommunizieren.

Die Encoder Interrupt-Service-Routine

Der zeitkritischste Teil des Codes ist die Interrupt-Service-Routine (ISR), die Encoder-Änderungen verarbeitet. Sie wird mit dem IRAM_ATTR Attribut in IRAM platziert, damit sie schnell und zuverlässig auf dem ESP32 ausgeführt wird.

void IRAM_ATTR encoder_irq() {
  encState = digitalRead(ENCODER_CLK);
  if (encState != oldState) {
    counter += (digitalRead(ENCODER_DT) == encState) ? +1 : -1;
    oldState = encState;
    hasChanged = true;
  }
}

IRAM_ATTR sorgt dafür, dass die Funktion im Instruktions-RAM statt im Flash gespeichert wird, was Verzögerungen durch Flash-Zugriffe vermeidet und für ISRs auf dem ESP32 empfohlen wird.

Innerhalb der ISR wird der aktuelle Pegel des Encoder-CLK-Signals mit digitalRead(ENCODER_CLK) ausgelesen und in encState gespeichert. Der Code prüft dann, ob dieser Zustand von oldState abweicht. Diese Bedingung filtert redundante Aufrufe heraus, da der Interrupt bei jeder Änderung (steigende oder fallende Flanke) ausgelöst wird und der Handler mehrfach mit demselben Pegel laufen könnte.

Wenn sich der Zustand geändert hat, wird die Drehrichtung ermittelt, indem das DT-Signal mit dem CLK-Signal verglichen wird. Die folgende Zeile liest den DT-Pin und prüft, ob sein Pegel mit dem aktuellen CLK-Pegel übereinstimmt:

counter += (digitalRead(ENCODER_DT) == encState) ? +1 : -1;

Für einen Quadraturencoder zeigt diese Beziehung die Drehrichtung an. Wenn DT gleich CLK ist, dreht der Encoder in eine Richtung und counter wird erhöht; wenn sie unterschiedlich sind, wird counter verringert.

Nach der Aktualisierung des Zählers wird oldState auf den neuen CLK-Zustand gesetzt und hasChanged auf true gesetzt. Dieses Flag informiert die Hauptschleife, dass neue Encoder-Daten verarbeitet oder angezeigt werden sollen. Die ISR bleibt kurz und effizient, was gute Praxis für Interrupt-Routinen ist.

Pin- und I2C-Initialisierung

Die initPins Funktion konfiguriert den I2C-Bus und den PCF8574-Pin, der für den Encoder-Druckknopf verwendet wird.

void initPins() {
  Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
  pcf8574.pinMode(P5, INPUT_PULLUP);  //encoder SW
  if (!pcf8574.begin()) {
    Serial.println("Can't init pcf8574");
  }
}

Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN); initialisiert das I2C-Peripheriegerät mit benutzerdefinierten SDA- und SCL-Pins.

pcf8574.pinMode(P5, INPUT_PULLUP); konfiguriert Pin P5 am PCF8574 als Eingang mit internem Pull-up-Widerstand. Dieser Pin ist mit dem Encoder-Druckknopf verbunden. Die Pull-up-Konfiguration bedeutet, dass der Pin bei nicht gedrücktem Knopf hoch liest und bei gedrücktem Knopf auf Masse gezogen wird.

pcf8574.begin() startet die Kommunikation mit dem PCF8574-Gerät an der konfigurierten I2C-Adresse. Falls dies fehlschlägt, gibt der Code eine Fehlermeldung über Serial.println aus, was bei der Diagnose von Verdrahtungs- oder Adressproblemen hilft.

Encoder-Initialisierung

Die initEncoder Funktion richtet die beiden Encoder-Signale ein und hängt einen Interrupt an den CLK-Pin.

void initEncoder() {
  pinMode(ENCODER_CLK, INPUT_PULLUP);
  pinMode(ENCODER_DT, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(ENCODER_CLK), encoder_irq, CHANGE);
}

pinMode(ENCODER_CLK, INPUT_PULLUP); und pinMode(ENCODER_DT, INPUT_PULLUP); konfigurieren beide Encoder-Kanäle als Eingänge mit internem Pull-up. Dies ist eine typische Konfiguration für mechanische Drehencoder, die die Pins beim Drehen in einem Gray-Code-Muster auf Masse ziehen.

attachInterrupt(digitalPinToInterrupt(ENCODER_CLK), encoder_irq, CHANGE); hängt den Interrupt-Handler encoder_irq an den Encoder-CLK-Pin. Der CHANGE Modus bedeutet, dass die ISR bei steigenden und fallenden Flanken ausgelöst wird. So wird der Encoder bei jeder Signaländerung abgetastet, was eine höhere Auflösung und genauere Zählung ermöglicht.

Setup

Die setup Funktion wird einmal beim Start ausgeführt und führt die Initialisierung durch.

void setup() {
  Serial.begin(115200);
  initPins();
  initEncoder();
}

Serial.begin(115200); startet die serielle Schnittstelle mit 115200 Baud, was für Debugging und Monitoring nützlich ist. Sie muss vor allen Serial.print oder Serial.printf Aufrufen erfolgen.

initPins(); initialisiert den I2C-Bus und den PCF8574 I/O Expander. So kann der Encoder-Knopf ausgelesen werden und die I2C-Hardware ist bereit.

initEncoder(); konfiguriert die GPIO-Pins des Drehencoders und richtet den Interrupt ein, sodass Drehereignisse sofort erfasst werden.

Loop

Die loop Funktion läuft wiederholt und übernimmt zwei Hauptaufgaben: das Ausgeben des Encoder-Werts bei Änderung und das Auslesen des Encoder-Druckknopfs.

void loop() {
  if (hasChanged) {
    hasChanged = false;
    Serial.printf("COUNTER: %d\n", counter);
  }
  int button = pcf8574.digitalRead(P5, true);
  if (!button) {
    Serial.printf("BTN pressed\n");
    delay(500);
  }  
  delay(1);
}

Der erste Block prüft, ob der Encoder-Zähler von der ISR aktualisiert wurde. Das hasChanged Flag wird im Interrupt auf true gesetzt, wenn sich die Encoder-Position ändert.

if (hasChanged) {
  hasChanged = false;
  Serial.printf("COUNTER: %d\n", counter);
}

Ist hasChanged wahr, wird das Flag sofort auf false zurückgesetzt und der aktuelle Wert von counter mit Serial.printf an den Serial Monitor ausgegeben. So wird das Drucken nicht in der ISR gemacht und die zeitkritische Interrupt-Logik von der langsameren seriellen Ausgabe entkoppelt.

Anschließend liest der Code den Zustand des Encoder-Druckknopfs, der am PCF8574 angeschlossen ist.

int button = pcf8574.digitalRead(P5, true);
if (!button) {
  Serial.printf("BTN pressed\n");
  delay(500);
}

pcf8574.digitalRead(P5, true); liest den aktuellen Pegel von Pin P5. Das zweite Argument true bewirkt meist, dass die Bibliothek eine sofortige I2C-Abfrage durchführt, statt einen zwischengespeicherten Wert zu verwenden, was eine frische Messung garantiert.

Da der Pin mit Pull-up konfiguriert ist, liest ein nicht gedrückter Knopf als hoch (nicht null), und ein gedrückter Knopf zieht die Leitung auf Masse und liest als niedrig (null). Daher prüft die Bedingung if (!button) den gedrückten Zustand. Wird der Knopf als gedrückt erkannt, gibt der Code "BTN pressed" aus und wartet 500 Millisekunden. Diese Verzögerung wirkt als einfache Entprellung und verhindert, dass die Meldung bei gedrücktem Knopf ständig ausgegeben wird.

Zum Schluss endet die Schleife mit einer kurzen Verzögerung.

delay(1);

Diese kleine Pause gibt dem Scheduler Zeit für Hintergrundaufgaben und verhindert eine sehr enge, CPU-intensive Schleife. Außerdem sorgt sie dafür, dass I2C- und serielle Subsysteme etwas Luft bekommen.

Ausgabe im Serial Monitor

Der folgende Screenshot zeigt, was du im Serial Monitor sehen solltest, wenn du den Encoder drehst oder den Encoder-Knopf drückst:

Codebeispiel: Display und Touchscreen

Das folgende Sketch zeigt, wie du das Display und das Touchpad nutzt. Es setzt den Bildschirmhintergrund auf Rot, schreibt den Text „Makerguides“ in die Mitte und zeichnet schwarze Kreise an jede Stelle, die berührt wird:

Schau dir den kompletten Code unten kurz an, bevor wir die Details besprechen:

#include <Wire.h>
#include <Arduino_GFX_Library.h>
#include <PCF8574.h>
#include <Adafruit_CST8XX.h>

// I2C to PCF8574
#define I2C_SDA_PIN 38
#define I2C_SCL_PIN 39
#define BKL_PIN 6

#define I2C_TOUCH_ADDR 0x15

PCF8574 pcf8574(0x21);

Adafruit_CST8XX tsPanel = Adafruit_CST8XX();

Arduino_ESP32RGBPanel *bus = new Arduino_ESP32RGBPanel(
  16 /* CS */, 2 /* SCK */, 1 /* SDA */,
  40 /* DE */, 7 /* VSYNC */, 15 /* HSYNC */, 41 /* PCLK */,
  46 /* R0 */, 3 /* R1 */, 8 /* R2 */, 18 /* R3 */, 17 /* R4 */,
  14 /* G0 */, 13 /* G1 */, 12 /* G2 */, 11 /* G3 */, 10 /* G4 */, 9 /* G5 */,
  5 /* B0 */, 45 /* B1 */, 48 /* B2 */, 47 /* B3 */, 21 /* B4 */
);

Arduino_ST7701_RGBPanel *gfx = new Arduino_ST7701_RGBPanel(
  bus,
  GFX_NOT_DEFINED,  // RST pin (not used, we reset via PCF8574)
  0,                // rotation
  false,            // IPS
  480, 480,         // width, height
  st7701_type5_init_operations,
  sizeof(st7701_type5_init_operations),
  true,       // BGR
  10, 4, 20,  // hsync front porch, pulse width, back porch
  10, 4, 20   // vsync front porch, pulse width, back porch
);

void initBacklight() {
  pinMode(BKL_PIN, OUTPUT);
  analogWrite(BKL_PIN, 200);
}

void initPins() {
  Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);

  pcf8574.pinMode(P0, OUTPUT);        //tp RST
  pcf8574.pinMode(P2, OUTPUT);        //tp INT
  pcf8574.pinMode(P3, OUTPUT);        //lcd power
  pcf8574.pinMode(P4, OUTPUT);        //lcd reset

  if (!pcf8574.begin()) {
    Serial.println("Can't init pcf8574");
  }

  // LCD
  pcf8574.digitalWrite(P3, HIGH);
  delay(100);
  pcf8574.digitalWrite(P4, HIGH);
  delay(100);
  pcf8574.digitalWrite(P4, LOW);
  delay(120);
  pcf8574.digitalWrite(P4, HIGH);
  delay(120);

  // Touchpad
  pcf8574.digitalWrite(P0, HIGH);
  delay(100);
  pcf8574.digitalWrite(P0, LOW);
  delay(120);
  pcf8574.digitalWrite(P0, HIGH);
  delay(120);
  pcf8574.digitalWrite(P2, HIGH);
  delay(120);

  if (!tsPanel.begin(&Wire, I2C_TOUCH_ADDR)) {
    Serial.println("No touchscreen found");
  } 
}

void initLCD() {
  gfx->begin();
  gfx->fillScreen(RED);
  gfx->setTextSize(5);
  gfx->setTextColor(WHITE);  
}

void drawOnLCD() {
  gfx->setCursor(90, 210);
  gfx->print("Makerguides");
}

void setup() {
  Serial.begin(115200);
  initPins();
  initBacklight();
  initLCD();
  drawOnLCD();
}

void loop() {
  if (tsPanel.touched()) {
    CST_TS_Point p = tsPanel.getPoint(0);
    Serial.printf("TOUCH: %d, %d\n", p.x, p.y);
    gfx->fillCircle(p.x, p.y, 10, BLACK);
    delay(10);
  }
}

Includes

Dieses Sketch beginnt mit dem Einbinden der benötigten Bibliotheken für I2C, Grafik, den I/O Expander und den Touch-Controller.

#include <Wire.h>
#include <Arduino_GFX_Library.h>
#include <PCF8574.h>
#include <Adafruit_CST8XX.h>

Wire.h ist dieselbe I2C-Bibliothek, die du schon mit dem PCF8574 verwendet hast. Arduino_GFX_Library.h stellt die Grafikabstraktion für das RGB-Panel und den ST7701 Display-Treiber bereit. PCF8574.h kümmert sich erneut um den I/O Expander auf dem I2C-Bus. Adafruit_CST8XX.h ist der Treiber für den CST8xx kapazitiven Touch-Controller, der über I2C angeschlossen ist und zum Auslesen der Touch-Koordinaten des runden Displays verwendet wird.

Konstanten und I2C-Konfiguration

Der Code definiert Pinbelegungen für den I2C-Bus, die Backlight-Steuerung und die I2C-Adresse des Touch-Controllers. Das folgt dem gleichen Muster wie im vorherigen Beispiel, wo SDA und SCL explizit konfiguriert wurden.

#define I2C_SDA_PIN 38
#define I2C_SCL_PIN 39
#define BKL_PIN 6

#define I2C_TOUCH_ADDR 0x15

I2C_SDA_PIN und I2C_SCL_PIN wählen die ESP32-Pins für den gemeinsamen I2C-Bus, der sowohl den PCF8574 als auch den CST8xx Touch-Controller verbindet. BKL_PIN ist ein PWM-fähiger Pin, der die LCD-Hintergrundbeleuchtung steuert. I2C_TOUCH_ADDR gibt die 7-Bit-I2C-Adresse des Touch-Controllers an, damit der Treiber mit ihm kommunizieren kann.

Globale Objekte: PCF8574, Touch-Controller, RGB-Bus und Panel

Die globalen Objekte richten den Zugriff auf den I/O Expander, den Touch-Controller und das Display ein.

pcf8574(0x21) ist wie im vorherigen Beispiel: eine Instanz, die an die I2C-Adresse 0x21 gebunden ist. Dieser Chip steuert mehrere Leitungen wie LCD-Strom, LCD-Reset sowie Touch-Reset und Interrupt.

tsPanel ist eine Instanz des Adafruit CST8xx Treibers, die mit dem Standardkonstruktor erstellt und später mit begin() über die gemeinsame Wire Schnittstelle und die Touch-Adresse initialisiert wird.

PCF8574 pcf8574(0x21);

Adafruit_CST8XX tsPanel = Adafruit_CST8XX();

RGB-Panel

Das nächste Objekt konfiguriert den RGB-Datenbus zwischen ESP32-S3 und dem ST7701 Display-Treiber. Es listet alle Timing- und Farb-Pins für die parallele RGB-Schnittstelle auf.

Arduino_ESP32RGBPanel *bus = new Arduino_ESP32RGBPanel(
  16 /* CS */, 2 /* SCK */, 1 /* SDA */,
  40 /* DE */, 7 /* VSYNC */, 15 /* HSYNC */, 41 /* PCLK */,
  46 /* R0 */, 3 /* R1 */, 8 /* R2 */, 18 /* R3 */, 17 /* R4 */,
  14 /* G0 */, 13 /* G1 */, 12 /* G2 */, 11 /* G3 */, 10 /* G4 */, 9 /* G5 */,
  5 /* B0 */, 45 /* B1 */, 48 /* B2 */, 47 /* B3 */, 21 /* B4 */
);

Dieses Arduino_ESP32RGBPanel Objekt definiert die genaue Zuordnung zwischen ESP32 GPIOs und den RGB-Panel-Signalen. Die ersten drei Pins dienen als Steuerleitungen für den Panel-Bus (Chip Select, Takt und Daten für die Konfigurationsschnittstelle). DE, VSYNC, HSYNC und PCLK sind die Timing-Signale, ähnlich wie bei klassischen RGB-Display-Schnittstellen. Die übrigen Gruppen ordnen die fünf roten, sechs grünen und fünf blauen Bits des 16-Bit-Farb-Busses (5-6-5 Format) bestimmten GPIO-Pins zu.

Das hochrangige Grafikobjekt gfx repräsentiert das ST7701-gesteuerte LCD, das über diesen RGB-Bus angesteuert wird.

Arduino_ST7701_RGBPanel *gfx = new Arduino_ST7701_RGBPanel(
  bus,
  GFX_NOT_DEFINED,  // RST pin (not used, we reset via PCF8574)
  0,                // rotation
  false,            // IPS
  480, 480,         // width, height
  st7701_type5_init_operations,
  sizeof(st7701_type5_init_operations),
  true,       // BGR
  10, 4, 20,  // hsync front porch, pulse width, back porch
  10, 4, 20   // vsync front porch, pulse width, back porch
);

Das erste Argument ist der zuvor definierte RGB-Bus. Der Reset-Pin ist auf GFX_NOT_DEFINED gesetzt, da das Panel über PCF8574-Pins zurückgesetzt wird und nicht direkt über einen ESP32 GPIO. Der Rotationsparameter ist null, was die Standardorientierung bedeutet. Das false IPS-Flag ist eine Panel-Option, und 480, 480 definiert die Displayauflösung.

Die st7701_type5_init_operations Zeiger und Größe liefern die Low-Level-Initialisierungssequenz für den ST7701-Treiber, die die Bibliothek beim Start an das Panel sendet. Das true BGR-Flag weist den Treiber an, Farbdaten als BGR statt RGB zu interpretieren.

Schließlich definieren die horizontalen und vertikalen Porch- und Pulswerte das RGB-Timing, ähnlich den Parametern klassischer Videotimings: Front Porch, Pulsbreite und Back Porch für HSYNC und VSYNC.

Backlight-Initialisierung

Die Hintergrundbeleuchtung wird getrennt von der Panel-Elektronik gesteuert. Die initBacklight Funktion konfiguriert den Backlight-Pin und setzt eine Anfangshelligkeit per PWM.

void initBacklight() {
  pinMode(BKL_PIN, OUTPUT);
  analogWrite(BKL_PIN, 200);
}

pinMode(BKL_PIN, OUTPUT); konfiguriert den Backlight-Pin als Ausgang. analogWrite(BKL_PIN, 200); aktiviert PWM auf diesem Pin mit einem Tastverhältnis, das einer Helligkeit von 200 auf einer typischen Skala von 0–255 entspricht. So kannst du die Hintergrundbeleuchtung später durch Schreiben eines anderen Werts anpassen, ohne die Hardware zu ändern.

Pin- und Peripherie-Initialisierung

Die initPins Funktion richtet den gemeinsamen I2C-Bus ein, konfiguriert die PCF8574-Pins, führt Reset- und Stromversorgungssequenzen für LCD und Touch-Controller durch und initialisiert den Touch-Treiber. Sie entspricht in der Funktion dem initPins aus dem vorherigen Beispiel, umfasst aber mehr Peripheriegeräte.

void initPins() {
  Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);

  pcf8574.pinMode(P0, OUTPUT);        //tp RST
  pcf8574.pinMode(P2, OUTPUT);        //tp INT
  pcf8574.pinMode(P3, OUTPUT);        //lcd power
  pcf8574.pinMode(P4, OUTPUT);        //lcd reset

  if (!pcf8574.begin()) {
    Serial.println("Can't init pcf8574");
  }

Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN); ist konzeptionell identisch zum früheren Sketch: Es startet das I2C-Peripheriegerät mit benutzerdefinierten SDA- und SCL-Pins. Die vier pcf8574.pinMode Aufrufe konfigurieren bestimmte PCF8574-Leitungen als Ausgänge: P0 für Touch-Reset, P2 für Touch-Interrupt-Leitung, P3 für LCD-Stromversorgung und P4 für LCD-Reset. Wie zuvor initialisiert pcf8574.begin() die Kommunikation mit dem Expander und gibt eine Fehlermeldung aus, falls dies fehlschlägt.

Der nächste Block führt die Strom- und Reset-Sequenz für das LCD aus. Diese genauen Verzögerungen und Umschaltmuster sind oft von LCD-Controllern gefordert und basieren auf dem Datenblatt des Displays.

  // LCD
  pcf8574.digitalWrite(P3, HIGH);
  delay(100);
  pcf8574.digitalWrite(P4, HIGH);
  delay(100);
  pcf8574.digitalWrite(P4, LOW);
  delay(120);
  pcf8574.digitalWrite(P4, HIGH);
  delay(120);

Das Setzen von P3 auf High aktiviert die LCD-Stromversorgung. Nach einer kurzen Verzögerung, um die Versorgung zu stabilisieren, wird P4 umgeschaltet, um einen korrekten Reset-Impuls für das Panel zu erzeugen. Die Sequenz High, Low, High mit spezifischen Verzögerungen stellt sicher, dass der ST7701-Controller in einem definierten Zustand startet.

Der Touchpad-Abschnitt führt eine sehr ähnliche Reset-Sequenz durch, jedoch für die Reset- und Interrupt-Leitungen des Touch-Controllers.

  // Touchpad
  pcf8574.digitalWrite(P0, HIGH);
  delay(100);
  pcf8574.digitalWrite(P0, LOW);
  delay(120);
  pcf8574.digitalWrite(P0, HIGH);
  delay(120);
  pcf8574.digitalWrite(P2, HIGH);
  delay(120);

Hier wird P0 umgeschaltet, um den CST8xx-Controller zurückzusetzen, und P2 wird auf High gesetzt, um die Interrupt-Leitung in einen definierten Zustand zu versetzen. Diese Maßnahmen stellen sicher, dass der Touch-Controller bereit ist, bevor der Treiber die I2C-Kommunikation startet.

Zum Schluss wird der Touch-Controller mit dem Adafruit-Treiber initialisiert.

  if (!tsPanel.begin(&Wire, I2C_TOUCH_ADDR)) {
    Serial.println("No touchscreen found");
  } 
}

tsPanel.begin(&Wire, I2C_TOUCH_ADDR) weist das CST8xx-Objekt an, die globale Wire Instanz zu verwenden und verbindet es mit der zuvor definierten I2C-Adresse. Schlägt dieser Aufruf fehl, gibt das Sketch eine Diagnosemeldung aus.

LCD-Initialisierung

Die initLCD Funktion bereitet den Grafik-Kontext vor und konfiguriert einen einfachen Zeichenstatus.

void initLCD() {
  gfx->begin();
  gfx->fillScreen(RED);
  gfx->setTextSize(5);
  gfx->setTextColor(WHITE);  
}

gfx->begin(); initialisiert das ST7701-Panel über den RGB-Bus und führt die zuvor übergebene Init-Sequenz aus. Nach diesem Aufruf ist das Display bereit, Zeichenbefehle zu empfangen.

gfx->fillScreen(RED); löscht den gesamten Bildschirm mit rotem Hintergrund. gfx->setTextSize(5); setzt einen relativ großen Textskalierungsfaktor, damit der Text auf dem 480×480 runden Display gut lesbar ist. gfx->setTextColor(WHITE); definiert die Vordergrundfarbe für nachfolgende Textzeichenoperationen.

drawOnLCD

Die drawOnLCD Funktion kapselt eine einfache Zeichenaktion, die ein Textlabel in der mittleren Bildschirmregion platziert.

void drawOnLCD() {
  gfx->setCursor(90, 210);
  gfx->print("Makerguides");
}

gfx->setCursor(90, 210); setzt den Textcursor auf die Position (90, 210) in Pixelkoordinaten. Bei einem 480×480 Display ist das ungefähr die Mitte, abhängig von der Schriftgröße. gfx->print("Makerguides"); rendert dann den Textstring mit der zuvor konfigurierten Textgröße und Farbe auf dem Bildschirm.

Setup

Die setup Funktion liefert erneut eine Initialisierungssequenz, ähnlich dem vorherigen Sketch, der Serial, Pins und Encoder einrichtete.

void setup() {
  Serial.begin(115200);
  initPins();
  initBacklight();
  initLCD();
  drawOnLCD();
}

Serial.begin(115200); startet die serielle Kommunikation für Debugging. initPins(); konfiguriert den I2C-Bus, PCF8574-Pins und führt die LCD- und Touch-Reset-Sequenzen wie oben beschrieben aus. initBacklight(); aktiviert und setzt die Hintergrundbeleuchtung, damit der Displayinhalt sichtbar ist. initLCD(); initialisiert den Grafiktreiber und malt den roten Hintergrund, und drawOnLCD(); platziert den initialen „Makerguides“-String auf dem Bildschirm.

Loop

Die Hauptschleife prüft ständig, ob das Touchpanel berührt wird. Wird eine Berührung erkannt, liest sie die Koordinaten aus und zeichnet an dieser Stelle einen kleinen schwarzen Kreis.

void loop() {
  if (tsPanel.touched()) {
    CST_TS_Point p = tsPanel.getPoint(0);
    Serial.printf("TOUCH: %d, %d\n", p.x, p.y);
    gfx->fillCircle(p.x, p.y, 10, BLACK);
    delay(10);
  }
}

tsPanel.touched(); fragt den CST8xx-Treiber ab, ob aktuell Touchpunkte aktiv sind. Gibt die Funktion true zurück, ist mindestens ein Finger auf dem Bildschirm. tsPanel.getPoint(0); holt den ersten Touchpunkt als CST_TS_Point Struktur mit x und y Koordinaten. Diese Koordinaten werden zur Fehlersuche mit Serial.printf an den Serial Monitor ausgegeben, ähnlich wie zuvor der Encoder-Zähler und der Knopfzustand.

gfx->fillCircle(p.x, p.y, 10, BLACK); zeichnet einen gefüllten Kreis mit Radius 10 Pixel in Schwarz an der Berührungsposition. Der delay(10); Aufruf sorgt für eine kurze Pause, um die Aktualisierungsrate zu begrenzen und zu verhindern, dass der I2C-Bus und der Grafiktreiber mit zu vielen Operationen pro Sekunde überlastet werden.

Codebeispiel: Display, Touch und Encoder

Dieses letzte Sketch vereint die Konzepte der vorherigen Beispiele: I2C-Konfiguration und PCF8574-Steuerung, RGB-Display und Touch-Bedienung sowie einen interruptgesteuerten Drehencoder.

Der Code erlaubt es, die Displayhelligkeit durch Drehen des Encoder-Rings zu steuern und zeigt den aktuellen Helligkeitswert (0…255) in der Bildschirmmitte an. Der Encoder-Knopf löst eine Farbänderung des Displays zu Blau aus, und Touch-Ereignisse werden weiterhin als schwarze Punkte auf dem Display registriert.

Schau dir den kompletten Code unten kurz an, dann besprechen wir die Details.

#include <Wire.h>
#include <Arduino_GFX_Library.h>
#include <PCF8574.h>
#include <Adafruit_CST8XX.h>

// I2C to PCF8574
#define I2C_SDA_PIN 38
#define I2C_SCL_PIN 39
#define BKL_PIN 6

#define ENCODER_CLK 42
#define ENCODER_DT 4

#define I2C_TOUCH_ADDR 0x15

volatile int brightness = 100;
volatile int encState = 0;
volatile int oldState = -1;
volatile bool brightnessChanged = true;

PCF8574 pcf8574(0x21);

Adafruit_CST8XX tsPanel = Adafruit_CST8XX();

Arduino_ESP32RGBPanel *bus = new Arduino_ESP32RGBPanel(
  16 /* CS */, 2 /* SCK */, 1 /* SDA */,
  40 /* DE */, 7 /* VSYNC */, 15 /* HSYNC */, 41 /* PCLK */,
  46 /* R0 */, 3 /* R1 */, 8 /* R2 */, 18 /* R3 */, 17 /* R4 */,
  14 /* G0 */, 13 /* G1 */, 12 /* G2 */, 11 /* G3 */, 10 /* G4 */, 9 /* G5 */,
  5 /* B0 */, 45 /* B1 */, 48 /* B2 */, 47 /* B3 */, 21 /* B4 */
);

Arduino_ST7701_RGBPanel *gfx = new Arduino_ST7701_RGBPanel(
  bus,
  GFX_NOT_DEFINED,  // RST pin (not used, we reset via PCF8574)
  0,                // rotation
  false,            // IPS
  480, 480,         // width, height
  st7701_type5_init_operations,
  sizeof(st7701_type5_init_operations),
  true,       // BGR
  10, 4, 20,  // hsync front porch, pulse width, back porch
  10, 4, 20   // vsync front porch, pulse width, back porch
);

void initBacklight() {
  pinMode(BKL_PIN, OUTPUT);
  analogWrite(BKL_PIN, brightness);
}

void initPins() {
  Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);

  pcf8574.pinMode(P0, OUTPUT);        //tp RST
  pcf8574.pinMode(P2, OUTPUT);        //tp INT
  pcf8574.pinMode(P3, OUTPUT);        //lcd power
  pcf8574.pinMode(P4, OUTPUT);        //lcd reset
  pcf8574.pinMode(P5, INPUT_PULLUP);  //encoder SW

  if (!pcf8574.begin()) {
    Serial.println("Can't init pcf8574");
  }

  // LCD
  pcf8574.digitalWrite(P3, HIGH);
  delay(100);
  pcf8574.digitalWrite(P4, HIGH);
  delay(100);
  pcf8574.digitalWrite(P4, LOW);
  delay(120);
  pcf8574.digitalWrite(P4, HIGH);
  delay(120);

  // Touchpad
  pcf8574.digitalWrite(P0, HIGH);
  delay(100);
  pcf8574.digitalWrite(P0, LOW);
  delay(120);
  pcf8574.digitalWrite(P0, HIGH);
  delay(120);
  pcf8574.digitalWrite(P2, HIGH);
  delay(120);

  if (!tsPanel.begin(&Wire, I2C_TOUCH_ADDR)) {
    Serial.println("No touchscreen found");
  } 
}

void IRAM_ATTR encoder_irq() {
  encState = digitalRead(ENCODER_CLK);
  if (encState != oldState) {
    brightness += (digitalRead(ENCODER_DT) == encState) ? -5 : +5;
    brightness = constrain(brightness, 5, 255);
    oldState = encState;
    brightnessChanged = true;
  }
}

void initEncoder() {
  pinMode(ENCODER_CLK, INPUT_PULLUP);
  pinMode(ENCODER_DT, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(ENCODER_CLK), encoder_irq, CHANGE);
}

void initLCD() {
  gfx->begin();
  gfx->fillScreen(RED);
  gfx->setTextSize(10);
  gfx->setTextColor(WHITE);  
}

void updateBrightness() {  
  analogWrite(BKL_PIN, brightness);
  gfx->fillScreen(RED);
  gfx->setCursor(150, 200);
  gfx->printf("%3d", brightness);
}

void setup() {
  Serial.begin(115200);
  initPins();
  initEncoder();
  initBacklight();
  initLCD();
  updateBrightness();
}

void loop() {
  int button = pcf8574.digitalRead(P5, true);
  if (!button) {
    Serial.printf("BTN pressed\n");
    gfx->fillScreen(BLUE);
  }

  if (tsPanel.touched()) {
    CST_TS_Point p = tsPanel.getPoint(0);
    Serial.printf("TOUCH: %d, %d\n", p.x, p.y);
    gfx->fillCircle(p.x, p.y, 10, BLACK);
  }

  if (brightnessChanged) {
    brightnessChanged = false;
    updateBrightness();
  }  

  delay(100);
}

Includes

Dieses Sketch kombiniert alles aus den vorherigen Beispielen: I2C-Peripherie, RGB-Display, Touch und einen interruptgesteuerten Drehencoder, und bindet die notwendigen Bibliotheken ein:

#include <Wire.h>
#include <Arduino_GFX_Library.h>
#include <PCF8574.h>
#include <Adafruit_CST8XX.h>

Konstanten und globaler Zustand

Der nächste Abschnitt definiert die Pins für I2C, Backlight, Encoder-Signale und die Adresse des Touch-Controllers. Außerdem werden mehrere volatile globale Variablen eingeführt, die die aktuelle Helligkeit und den Encoder-Zustand halten, ähnlich dem vorherigen Encoder-Zähler-Beispiel:

// I2C to PCF8574
#define I2C_SDA_PIN 38
#define I2C_SCL_PIN 39
#define BKL_PIN 6

#define ENCODER_CLK 42
#define ENCODER_DT 4

#define I2C_TOUCH_ADDR 0x15

volatile int brightness = 100;
volatile int encState = 0;
volatile int oldState = -1;
volatile bool brightnessChanged = true;

I2C_SDA_PIN und I2C_SCL_PIN konfigurieren den gemeinsamen I2C-Bus wie zuvor. BKL_PIN ist der PWM-Pin zur Ansteuerung der LCD-Hintergrundbeleuchtung. ENCODER_CLK und ENCODER_DT sind die Quadratursignale des Drehencoders, identisch zur Rolle im vorherigen Encoder-Sketch. I2C_TOUCH_ADDR hält die Adresse des CST8xx Touch-Controllers.

brightness speichert den aktuellen Helligkeitswert der Hintergrundbeleuchtung. Es ist als volatile deklariert, da es in einer Interrupt-Service-Routine geändert wird. encState und oldState werden verwendet, um Übergänge auf der Encoder-CLK-Leitung zu erkennen, und brightnessChanged ist ein Flag, das die Hauptschleife informiert, dass ein neuer Helligkeitswert vorliegt.

PCF8574-, Touch- und Display-Objekte

Die globalen Objekte für den I/O Expander, das Touchpanel und den Display-Bus sind dieselben wie im vorherigen Display-und-Touch-Sketch. Sie definieren, wie der ESP32 mit den externen Chips und dem RGB-Panel interagiert.

PCF8574 pcf8574(0x21);

Adafruit_CST8XX tsPanel = Adafruit_CST8XX();

Arduino_ESP32RGBPanel *bus = new Arduino_ESP32RGBPanel(
  16 /* CS */, 2 /* SCK */, 1 /* SDA */,
  40 /* DE */, 7 /* VSYNC */, 15 /* HSYNC */, 41 /* PCLK */,
  46 /* R0 */, 3 /* R1 */, 8 /* R2 */, 18 /* R3 */, 17 /* R4 */,
  14 /* G0 */, 13 /* G1 */, 12 /* G2 */, 11 /* G3 */, 10 /* G4 */, 9 /* G5 */,
  5 /* B0 */, 45 /* B1 */, 48 /* B2 */, 47 /* B3 */, 21 /* B4 */
);

Das pcf8574 Objekt ist an die Adresse 0x21 gebunden und verantwortlich für LCD-Strom, Reset und den Encoder-Druckknopf. Das tsPanel Objekt kapselt den CST8xx Touch-Controller. Der bus Zeiger definiert die genaue Zuordnung der RGB- und Timing-Signale zu ESP32 GPIOs, wie zuvor mit deiner R0–R4, G0–G5 und B0–B4 Pin-Tabelle.

Das hochrangige Grafikobjekt für das ST7701 RGB-Panel wird dann auf diesem Bus erstellt.

Arduino_ST7701_RGBPanel *gfx = new Arduino_ST7701_RGBPanel(
  bus,
  GFX_NOT_DEFINED,  // RST pin (not used, we reset via PCF8574)
  0,                // rotation
  false,            // IPS
  480, 480,         // width, height
  st7701_type5_init_operations,
  sizeof(st7701_type5_init_operations),
  true,       // BGR
  10, 4, 20,  // hsync front porch, pulse width, back porch
  10, 4, 20   // vsync front porch, pulse width, back porch
);

Wie zuvor kapselt es die Low-Level ST7701 Initialisierungssequenz, Timing-Parameter und Farbformat. Der Reset-Pin ist als undefiniert markiert, da der Reset über die PCF8574-Pins während initPins erfolgt.

Backlight-Initialisierung

Die Backlight-Initialisierungsfunktion nimmt den aktuellen brightness Wert und wendet ihn auf den Backlight-PWM-Pin an:

void initBacklight() {
  pinMode(BKL_PIN, OUTPUT);
  analogWrite(BKL_PIN, brightness);
}

pinMode(BKL_PIN, OUTPUT); konfiguriert den Backlight-Pin als digitalen Ausgang. analogWrite(BKL_PIN, brightness); startet die PWM-Ausgabe auf diesem Pin mit dem brightness Wert als Tastverhältnis.

Pin- und Peripherie-Initialisierung

Die initPins Funktion ist eine erweiterte Version der zuvor gesehenen. Sie richtet I2C, die PCF8574-Pins, die LCD-Strom- und Reset-Sequenz, die Touch-Reset-Sequenz ein und konfiguriert auch den PCF8574-Pin, der den Encoder-Druckknopf liest.

void initPins() {
  Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);

  pcf8574.pinMode(P0, OUTPUT);        //tp RST
  pcf8574.pinMode(P2, OUTPUT);        //tp INT
  pcf8574.pinMode(P3, OUTPUT);        //lcd power
  pcf8574.pinMode(P4, OUTPUT);        //lcd reset
  pcf8574.pinMode(P5, INPUT_PULLUP);  //encoder SW

  if (!pcf8574.begin()) {
    Serial.println("Can't init pcf8574");
  }

Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN); startet den I2C-Bus mit den angegebenen Pins, wie in den vorherigen Sketches. P0, P2, P3 und P4 werden als Ausgänge konfiguriert, um Touch-Reset, Touch-Interrupt-Leitung, LCD-Strom und LCD-Reset zu steuern. P5 wird als INPUT_PULLUP konfiguriert, da es mit dem Encoder-Schalter verbunden ist. Das entspricht der Konfiguration von P5 als Encoder-Knopf-Eingang im ursprünglichen Encoder-Beispiel.

Die LCD-Strom- und Reset-Timing-Sequenz folgt, identisch zur früheren Display-Code-Struktur.

  // LCD
  pcf8574.digitalWrite(P3, HIGH);
  delay(100);
  pcf8574.digitalWrite(P4, HIGH);
  delay(100);
  pcf8574.digitalWrite(P4, LOW);
  delay(120);
  pcf8574.digitalWrite(P4, HIGH);
  delay(120);

P3 aktiviert die LCD-Stromversorgung, dann wird P4 mit spezifischen Verzögerungen umgeschaltet, um einen Hardware-Reset des ST7701-Controllers durchzuführen.

Die Touchpad-Reset- und Konfigurationssequenz ist ebenfalls wie zuvor.

  // Touchpad
  pcf8574.digitalWrite(P0, HIGH);
  delay(100);
  pcf8574.digitalWrite(P0, LOW);
  delay(120);
  pcf8574.digitalWrite(P0, HIGH);
  delay(120);
  pcf8574.digitalWrite(P2, HIGH);
  delay(120);

P0 wird gepulst, um den CST8xx-Controller zurückzusetzen, und P2 wird auf High gesetzt, um einen definierten Zustand für die Touch-Interrupt-Leitung herzustellen.

Der Touch-Controller wird dann initialisiert. tsPanel.begin(&Wire, I2C_TOUCH_ADDR) bindet den CST8xx-Treiber an den gemeinsamen I2C-Bus und die angegebene Adresse und gibt eine Diagnosemeldung aus, falls das Gerät nicht gefunden wird:

  if (!tsPanel.begin(&Wire, I2C_TOUCH_ADDR)) {
    Serial.println("No touchscreen found");
  } 
}

Encoder Interrupt-Service-Routine

Der Encoder-Interrupt-Handler ähnelt der früheren encoder_irq Funktion, aktualisiert aber statt eines Positionszählers die brightness Variable in Schritten von fünf.

void IRAM_ATTR encoder_irq() {
  encState = digitalRead(ENCODER_CLK);
  if (encState != oldState) {
    brightness += (digitalRead(ENCODER_DT) == encState) ? -5 : +5;
    brightness = constrain(brightness, 5, 255);
    oldState = encState;
    brightnessChanged = true;
  }
}

IRAM_ATTR sorgt dafür, dass die ISR im Instruktions-RAM für schnelle Ausführung auf dem ESP32 liegt, wie zuvor besprochen. Innerhalb der Funktion wird encState auf den aktuellen Logikpegel des Encoder-CLK-Pins mit digitalRead(ENCODER_CLK) gesetzt. Die Bedingung if (encState != oldState) stellt sicher, dass der Code nur reagiert, wenn sich das CLK-Signal tatsächlich ändert, um Mehrfachaktualisierungen auf demselben Pegel zu vermeiden.

Die Drehrichtung wird wieder durch den Vergleich des DT-Signals mit dem aktuellen CLK-Zustand bestimmt. In diesem Sketch ist die Bedingung im Vergleich zum früheren Beispiel invertiert, um ein natürliches Gefühl für Helligkeitszunahme und -abnahme zu erzeugen.

Die folgende Zeile subtrahiert entweder 5 oder addiert 5 zur brightness Variable, basierend auf der relativen Phase der Quadratursignale. Positive Drehung erhöht die Helligkeit, negative verringert sie.

brightness += (digitalRead(ENCODER_DT) == encState) ? -5 : +5;

Und die folgende Zeile stellt sicher, dass die Helligkeit innerhalb eines definierten unteren Grenzwerts von 5 (um ein komplett ausgeschaltetes Display zu vermeiden) und einem oberen Grenzwert von 255 (maximaler PWM-Wert für volle Helligkeit) bleibt:

brightness = constrain(brightness, 5, 255);

oldState wird auf den neuen CLK-Zustand gesetzt, und brightnessChanged wird auf true gesetzt, um die Hauptschleife zu informieren, dass Hintergrundbeleuchtung und On-Screen-Display aktualisiert werden sollen. Wie zuvor wird alle aufwändige Arbeit wie serielle und Grafik-I/O aus der ISR herausgehalten und in der Hauptschleife erledigt.

Encoder-Initialisierung

Die initEncoder Funktion konfiguriert die Encoder-Pins und hängt den Interrupt an die CLK-Leitung. Das entspricht effektiv dem Muster deines ursprünglichen Encoder-Sketches.

void initEncoder() {
  pinMode(ENCODER_CLK, INPUT_PULLUP);
  pinMode(ENCODER_DT, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(ENCODER_CLK), encoder_irq, CHANGE);
}

Beide Encoder-Kanäle werden als Eingänge mit internem Pull-up konfiguriert. Der Interrupt wird mit attachInterrupt am CLK-Pin angebracht, mit dem Modus CHANGE, der bei steigenden und fallenden Flanken auslöst. Bei jeder Änderung wird der encoder_irq Handler aufgerufen, der die Helligkeit aktualisiert.

LCD-Initialisierung und Helligkeitsanzeige

Die LCD-Initialisierungsfunktion bringt das ST7701-Panel hoch und konfiguriert die Texteinstellungen. Sie ähnelt der früheren initLCD, verwendet aber eine größere Textgröße, um den Helligkeitswert deutlich anzuzeigen:

void initLCD() {
  gfx->begin();
  gfx->fillScreen(RED);
  gfx->setTextSize(10);
  gfx->setTextColor(WHITE);  
}

gfx->begin(); initialisiert den Display-Controller und sendet die Konfigurationssequenz. gfx->fillScreen(RED); setzt einen roten Hintergrund. gfx->setTextSize(10); wählt einen großen Skalierungsfaktor, damit der numerische Helligkeitswert gut sichtbar ist. gfx->setTextColor(WHITE); konfiguriert Weiß als Vordergrundfarbe für die Textdarstellung.

Die updateBrightness Funktion verbindet den logischen Helligkeitswert mit der physischen Hintergrundbeleuchtung und der On-Screen-Anzeige.

void updateBrightness() {  
  analogWrite(BKL_PIN, brightness);
  gfx->fillScreen(RED);
  gfx->setCursor(150, 200);
  gfx->printf("%3d", brightness);
}

analogWrite(BKL_PIN, brightness); aktualisiert das PWM-Tastverhältnis und ändert so die LED-Hintergrundbeleuchtungsintensität. Der Bildschirm wird dann mit gfx->fillScreen(RED); erneut rot gefüllt.

Der Cursor wird auf die Koordinaten (150, 200) gesetzt, und gfx->printf("%3d", brightness); gibt die Helligkeit als dreistellige Zahl aus.

Setup

Die setup Funktion initialisiert alle Subsysteme: Serial, I2C-Pins und externe Chips, den Encoder, die Hintergrundbeleuchtung und das LCD und zeigt schließlich die Anfangshelligkeit auf dem Display an.

void setup() {
  Serial.begin(115200);
  initPins();
  initEncoder();
  initBacklight();
  initLCD();
  updateBrightness();
}

Serial.begin(115200); startet die UART für Debugging-Ausgaben. initPins(); bereitet den I2C-Bus, PCF8574, LCD und Touch-Controller wie zuvor beschrieben vor. initEncoder(); aktiviert die interruptgesteuerte Encoder-Schnittstelle. initBacklight(); wendet den Anfangswert brightness auf den Backlight-Pin an. initLCD(); richtet den Grafik-Kontext ein, und updateBrightness(); synchronisiert sofort die On-Screen-Anzeige und die Backlight-PWM mit dem aktuellen Helligkeitswert.

Loop

Die Hauptschleife prüft periodisch den Encoder-Knopf, die Touch-Eingabe und das Helligkeits-Update-Flag. Sie reagiert auf jedes dieser Ereignisse mit den zuvor konfigurierten Peripheriegeräten.

void loop() {
  int button = pcf8574.digitalRead(P5, true);
  if (!button) {
    Serial.printf("BTN pressed\n");
    gfx->fillScreen(BLUE);
  }

  if (tsPanel.touched()) {
    CST_TS_Point p = tsPanel.getPoint(0);
    Serial.printf("TOUCH: %d, %d\n", p.x, p.y);
    gfx->fillCircle(p.x, p.y, 10, BLACK);
  }

  if (brightnessChanged) {
    brightnessChanged = false;
    updateBrightness();
  }  

  delay(100);
}

Der erste Block liest den Encoder-Druckknopf über den PCF8574 aus. pcf8574.digitalRead(P5, true); liest Pin P5 mit einer sofortigen I2C-Transaktion. Da P5 als INPUT_PULLUP konfiguriert ist, liest der Knopf hoch, wenn er losgelassen ist, und niedrig, wenn er gedrückt wird. Die Bedingung if (!button) erkennt den gedrückten Zustand. Beim Drücken gibt das Sketch eine Meldung im Serial Monitor aus und füllt das Display mit Blau, um visuell anzuzeigen, dass der Knopf gedrückt wurde.

Der nächste Block verarbeitet kapazitive Touch-Eingaben und verwendet die gleiche Logik wie im vorherigen Zeichenbeispiel auf dem LCD. Wenn tsPanel.touched() true zurückgibt, ist mindestens ein Touchpunkt aktiv. Die Funktion tsPanel.getPoint(0); holt den ersten Touchpunkt, der dann im Serial Monitor ausgegeben wird. gfx->fillCircle(p.x, p.y, 10, BLACK); zeichnet einen kleinen schwarzen Kreis an den Touch-Koordinaten, sodass der Benutzer auf dem aktuellen Bildschirminhalt zeichnen kann.

Der dritte Block prüft, ob die Encoder-ISR die brightness Variable aktualisiert hat. Wenn brightnessChanged true ist, setzt die Hauptschleife das Flag zurück und ruft updateBrightness(); auf. Diese Funktion wendet die neue Helligkeit auf die Hintergrundbeleuchtung an und zeichnet den numerischen Wert auf dem Bildschirm neu. Schnelles Drehen des Encoders erzeugt mehrere Interrupts und setzt brightnessChanged wiederholt, aber die Schleife sorgt dafür, dass Helligkeitsupdates im Hauptkontext verarbeitet werden, wo serielle und grafische Operationen sicher sind.

Der abschließende delay(100); fügt eine kurze Pause ein, um die Schleifenfrequenz zu begrenzen und die Benutzerinteraktion zu glätten, ohne CPU oder I2C zu überlasten.

Fazit

Dieses Tutorial hat dir Codebeispiele gezeigt, um mit dem CrowPanel 2.1inch-HMI ESP32 Rotary Display zu starten. Siehe Elecrows Wiki für weitere Informationen und mehr Codebeispiele siehe das github repo.

Beachte, dass du ältere Bibliotheken und eine ältere Version des ESP32 Core (2.0.14) installieren musst, damit der Code funktioniert. Einige der dortigen Codebeispiele verwenden die LVGL-Bibliothek, die ich hier vermieden habe, um den Code einfach zu halten.

Wenn du ein ähnliches Displaymodul mit Drehencoder-Ring suchst, schau dir das CrowPanel 1.28inch-HMI ESP32 Rotary Display oder das Matouch 1.28″ ToolSet_Controller an. Falls du nur ein rundes Display (ohne Drehencoder-Ring) brauchst, könnte das Digital Clock on CrowPanel 1.28″ Round Display Tutorial hilfreich sein.

Zum Schluss, wenn du mehr über Drehencoder lernen möchtest, wirf einen Blick auf unser How To Interface A Quadrature Rotary Encoder Tutorial.

Wenn du Fragen hast, hinterlasse sie gerne im Kommentarbereich.

Viel Spaß beim Tüfteln 😉