Skip to Content

Iniziare con CrowPanel 2.1inch-HMI ESP32 Rotary Display

Iniziare con CrowPanel 2.1inch-HMI ESP32 Rotary Display

Il CrowPanel 2.1inch-HMI ESP32 Rotary Display di Elecrow è un modulo grande e rotondo che integra un microcontrollore ESP32-S3, un touchscreen circolare IPS da 2,1 pollici con risoluzione 480×480 e un encoder rotativo con pulsante integrato in un’unica unità.

In questo tutorial, vedremo i passaggi essenziali per iniziare a usare questo display; dalla configurazione iniziale al test delle sue capacità di visualizzazione e input. Imparerai come leggere i dati dell’encoder rotativo per un input utente fluido e come catturare gli eventi touch dal display circolare.

Componenti necessari

Per questo progetto ti servirà solo il CrowPanel 2.1inch-HMI ESP32 Rotary Display. Il modulo display include un cavo USB e un connettore GPIO, quindi non è necessario altro hardware.

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 del CrowPanel 2.1inch-HMI ESP32 Rotary Display

Il CrowPanel 2.1inch-HMI ESP32 Rotary Display è un modulo display che combina un display IPS rotondo ad alta risoluzione, un touchscreen capacitivo e un encoder rotativo con funzione pulsante.

Il modulo è basato sul sistema-on-chip ESP32‑S3R8 che utilizza un processore dual-core Xtensa LX7 a 32 bit con frequenze fino a 240 MHz. È dotato di 8 Mbit di PSRAM e 16 Mbyte di memoria flash onboard.

La parte display è un pannello IPS RGB circolare da 2,1 pollici (ST7701 controller) con risoluzione 480 × 480 pixel, con overlay touch capacitivo a schermo intero e retroilluminazione.

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

Dal punto di vista operativo, l’hardware incorpora un meccanismo a manopola con encoder rotativo. La manopola può ruotare in senso orario e antiorario e premuta verso l’interno (funzione switch) per l’input utente.

Per connettività ed espansione, il modulo supporta interfacce wireless e cablate. Sul lato wireless dispone di WiFi IEEE 802.11 a/b/g/n (2.4 GHz) e Bluetooth Low Energy (BLE) / Bluetooth 5.0.

Sul lato cablato il modulo espone una porta di ricarica/interfaccia a 5 V che funge sia da alimentazione che da porta di programmazione. Inoltre, la scheda offre tre interfacce di espansione: una UART, una I2C e un connettore FPC (12-pin) per ulteriori connessioni.

Connectors of the CrowPanel 2.1inch-HMI ESP32 Rotary Display
Connettori del CrowPanel 2.1inch-HMI ESP32 Rotary Display (source)

Definizione dei Pin

La tabella seguente elenca i pin necessari per controllare tutti i componenti del modulo display:

Gruppo FunzioniSegnale / ScopoNumero PinNote
Interfaccia I²CSDA38Linea dati I²C primaria
SCL39Linea clock I²C primaria
Encoder RotativoEncoder A42Canale quadratura A
Encoder B4Canale quadratura B
Retroilluminazione DisplayControllo Retroilluminazione6Pin PWM usato per la luminosità della retroilluminazione LCD
Controllo OLEDRESET–1Nessun pin reset usato (reset software)
Interfaccia Display RGBCS16Linea chip select
SCK2Clock seriale display
SDA1Dati seriali display
DE40Data enable
VSYNC7Sincronizzazione verticale
HSYNC15Sincronizzazione orizzontale
PCLK41Clock pixel
Pin dati colore RGB (formato 5-6-5)R046Bit rosso 0
R13Bit rosso 1
R28Bit rosso 2
R318Bit rosso 3
R417Bit rosso 4
G014Bit verde 0
G113Bit verde 1
G212Bit verde 2
G311Bit verde 3
G410Bit verde 4
G59Bit verde 5
B05Bit blu 0
B145Bit blu 1
B248Bit blu 2
B347Bit blu 3
B421Bit blu 4
PCF8574 I/O ExpanderIndirizzo I²C0x21Collegato ai pin 38 (SDA) e 39 (SCL)

Specifiche Tecniche

La tabella seguente riassume le specifiche tecniche del modulo CrowPanel 2.1inch-HMI ESP32 Rotary Display:

Categoria SpecificaDettagli
Processore principaleSoC: Xtensa LX7 dual-core (32-bit) nell’ESP32‑S3R8 (fino a 240 MHz)
Memoria e storageRAM di sistema: 512 kB SRAM; PSRAM: 8 MB; Flash: 16 MB
Pannello displayDimensione: 2,1 pollici; Tipo: IPS; Risoluzione: 480 × 480 pixel; Touch: overlay capacitivo a schermo intero
Meccanismi di input utenteManopola rotativa (rotazione oraria/antioraria + pressione completa); input touch capacitivo sulla superficie del display
Connettività wirelessWiFi: IEEE 802.11 a/b/g/n (2.4 GHz); Bluetooth: BLE / Bluetooth 5.0
Porte di interfaccia / espansioneIngresso 5 V (carica e programmazione); interfaccia UART: 1× UART0 + 1× UART1 via ZX-MX 1.25-4P; interfaccia I²C; connettore FPC 12-pin (alimentazione/programmazione e GPIO)
Pulsanti e indicatoriPulsante RESET onboard; pulsante BOOT; switch pressione manopola (pulsante di conferma); LED indicatore di alimentazione; retroilluminazione LCD
Alimentazione e tensioneIngresso modulo: 5 V / 1 A; livello logico chip principale: 3.3 V; funzionamento modulo a 5 V nominali
Meccanica / fisicoDimensioni: 79 × 79 × 30 mm; peso netto: 80 g; scocca: lega di alluminio + plastica + acrilico
Intervallo di temperaturaOperativo: –20 °C a +65 °C; stoccaggio: –40 °C a +80 °C
Supporto software / ambiente di sviluppoCompatibile con Arduino IDE, ESP-IDF, Lua RTOS, MicroPython, PlatformIO; libreria grafica UI: supporto LVGL

Installazione del Core ESP32

Se è il tuo primo progetto con una scheda della serie ESP32, devi prima installare il core ESP32. Tuttavia, per il CrowPanel 2.1inch Display devi installare una versione specifica (2.0.14) del core ESP32.

Inizia aprendo la finestra Preferenze nell’Arduino IDE selezionando “Preferences…” dal menu “File”. Si aprirà la finestra Preferenze mostrata qui sotto.

Nella scheda Settings troverai una casella di testo in fondo alla finestra etichettata “Additional boards manager URLs“:

Additional boards manager URLs in Preferences
URL aggiuntivi per il gestore schede nelle Preferenze

In questo campo incolla il seguente URL:

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

Questo permetterà all’Arduino IDE di sapere dove trovare le librerie del core ESP32. Successivamente installeremo le schede ESP32 tramite il Boards Manager.

Apri il Boards Manager tramite “Tools -> Boards -> Board Manager”. Vedrai il Boards Manager apparire nella barra laterale sinistra. Digita “ESP32” nel campo di ricerca in alto e dovresti vedere due tipi di schede ESP32; le “Arduino ESP32 Boards” e le schede “esp32 by Espressif”. Noi vogliamo le “esp32 libraries by Espressif”.

Nel menu a tendina seleziona la Versione “2.0.14” e poi premi il pulsante “INSTALL”. Lo screenshot qui sotto mostra cosa dovresti vedere nel BOARDS MANAGER dopo una corretta installazione del Core ESP32 2.0.14:

Install ESP32 Core 2.0.14
Installa Core ESP32 2.0.14

Nota che puoi installare versioni fino alla 2.0.17 ma non la 3.x. Le librerie che installeremo nelle sezioni successive non funzionano con il core ESP32 3.x.

Selezione della Scheda

Infine dobbiamo selezionare una scheda ESP32. Nel caso del CrowPanel 2.1inch-HMI ESP32 Rotary Display, scegliamo la scheda generica “ESP32S3 Dev Module”. Per farlo, clicca sul menu a tendina e poi su “Select other board and port…”:

Drop-down Menu for Board Selection
Menu a tendina per la selezione della scheda

Si aprirà una finestra dove puoi digitare “esp32s3 dev” nella barra di ricerca. Vedrai la scheda “ESP32S3 Dev Module” sotto Boards. Cliccaci sopra, seleziona la porta COM per attivarla e poi clicca OK:

Board Selection Dialog "ESP32S3 Dev Module" board
Finestra di selezione scheda “ESP32S3 Dev Module”

Nota che devi collegare la scheda al computer tramite cavo USB prima di poter selezionare una porta COM. Collega il cavo USB fornito con il modulo CrowPanel direttamente alla porta etichettata “USB-5V-IN” come mostrato qui sotto:

USB Port of CrowPanel Display Module
Porta USB del modulo CrowPanel Display

Impostazioni Strumenti

Di seguito le impostazioni da usare con il modulo CrowPanel Display. Le trovi nel menu Tools del tuo Arduino IDE.

Tool settings for CrowPanel 2.1inch-HMI ESP32 Rotary Display
Impostazioni strumenti per CrowPanel 2.1inch-HMI ESP32 Rotary Display

Fondamentale per gli esempi di codice nelle sezioni successive è impostare USB CDC on Boot su “Enabled”. Questo ti permette di inviare o ricevere dati via porta seriale, utile per il debug. Inoltre, devi impostare correttamente “OPI PSRAM”, “Huge APP” e “Flash Size” per gli esempi di codice del display.

Installazione delle Librerie

Ora dobbiamo installare librerie specifiche e versioni particolari per far funzionare il codice del CrowPanel 2.1inch-HMI ESP32 Rotary Display.

Vai alla pagina Elecrow github repo per il CrowPanel 2.1inch-HMI Display. Clicca sul pulsante verde “<> Code” e poi su “Download ZIP” per scaricare il repository come file ZIP:

Estrai il file ZIP per accedere ai contenuti. Dovresti vedere i seguenti file in una cartella estratta chiamata “CrowPanel-2.1inch-HMI-ESP32-Rotary-Display-480-480-IPS-Round-Touch-Knob-Screen-master”:

Ignora i file relativi al display “CrowPanel 1.28inch …”. Dobbiamo solo copiare il contenuto della cartella “example/libraries” nella cartella “libraries” dell’Arduino IDE. Su Windows la cartella “libraries” si trova tipicamente in:

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

Poiché questa cartella contiene già librerie installate, ti consiglio di rinominarla temporaneamente, ad esempio in “_libraries”, e creare una nuova cartella chiamata “libraries”. Così eviti conflitti con le librerie già installate senza perderle. Potrai facilmente tornare indietro in seguito. L’immagine sotto mostra come dovrebbe apparire la tua cartella “Arduino” con le librerie:

Ora copia i file dalla cartella “example/libraries” nella nuova cartella “libraries” come mostrato sotto:

Non è necessario copiare le librerie barrate, perché sono relative all’interfaccia LVGL, che non useremo. Nelle sezioni successive ti mostrerò alcuni esempi di codice per provare il display.

Esempio di Codice: Interfaccia Serial

Iniziamo testando la comunicazione seriale. Apri l’Arduino IDE, inserisci il seguente codice e caricalo sul modulo CrowPanel Display.

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

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

Poi apri il Monitor Seriale e dovresti vedere il testo “Makerguides” stampato ogni due secondi. Se non succede, verifica che le impostazioni Tools e la velocità Baud (115200) siano corrette. In particolare, USB CDC on Boot deve essere “Enabled”.

Esempio di Codice: Encoder

Nel prossimo esempio imparerai a usare l’encoder e il suo pulsante. Useremo un approccio basato su interrupt per leggere un encoder rotativo in quadratura e il PCF8574 per leggere lo stato del pulsante dell’encoder.

La rotazione dell’encoder è gestita in una ISR veloce con rilevamento della direzione, mentre il loop principale si occupa di segnalare i cambiamenti e leggere lo stato del pulsante. Dai un’occhiata veloce al codice e poi ne discuteremo i dettagli.

#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);
}

Importazioni

Lo sketch inizia includendo due librerie che gestiscono la comunicazione I2C e l’expander PCF8574. Questi header devono essere inclusi prima di poter usare Wire o PCF8574 oggetti.

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

Wire.h fornisce l’interfaccia I2C (TWI) standard di Arduino. PCF8574.h incapsula l’accesso I2C a basso livello al chip PCF8574 così puoi trattare i suoi pin quasi come pin digitali normali.

Costanti

Il codice definisce i pin usati per I2C e per l’encoder rotativo:

#define I2C_SDA_PIN 38
#define I2C_SCL_PIN 39

#define ENCODER_CLK 42  
#define ENCODER_DT 4   

I2C_SDA_PIN e I2C_SCL_PIN selezionano quali pin ESP32 sono usati come SDA e SCL per il bus I2C hardware. Sull’ESP32 puoi instradare I2C su vari pin, quindi specificarli qui è necessario.

ENCODER_CLK e ENCODER_DT sono le due uscite in quadratura dell’encoder rotativo. Il segnale CLK è tipicamente il canale principale usato per l’interrupt, mentre DT è il secondo canale usato per determinare la direzione di rotazione.

Stato Globale e Variabili Volatili

Il codice usa diverse variabili globali per tracciare lo stato dell’encoder. Queste variabili sono marcate volatile perché modificate all’interno di una routine di servizio interrupt.

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

counter tiene la posizione accumulata dell’encoder. Ogni passo incrementa o decrementa questo valore.

encState rappresenta il livello logico corrente del pin CLK dell’encoder come visto nell’handler dell’interrupt. oldState conserva lo stato precedente di CLK così il codice può rilevare transizioni ed evitare di contare lo stesso livello più volte.

hasChanged è usato come flag per segnalare al loop principale che il contatore è stato aggiornato nell’interrupt. Marcare queste variabili come volatile dice al compilatore che possono cambiare in qualsiasi momento e non devono essere memorizzate in registri, cosa critica quando sono modificate da un ISR.

Oggetto PCF8574

Il codice crea quindi un’istanza globale della classe PCF8574, specificando l’indirizzo I2C del chip.

PCF8574 pcf8574(0x21);

Questa riga dice alla libreria PCF8574 che l’expander è accessibile all’indirizzo I2C 0x21. Tutte le chiamate successive a pcf8574.pinMode o pcf8574.digitalRead useranno questo indirizzo per comunicare con quel dispositivo specifico sul bus I2C.

Routine di Servizio Interrupt dell’Encoder

La parte più sensibile al tempo del codice è la routine di servizio interrupt (ISR) che gestisce i cambiamenti dell’encoder. È posizionata in IRAM usando l’attributo IRAM_ATTR per eseguire velocemente e in modo affidabile sull’ESP32.

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 garantisce che la funzione sia memorizzata nella RAM delle istruzioni invece che nella flash, evitando ritardi dovuti all’accesso alla flash ed è raccomandato per ISR su ESP32.

Dentro l’ISR, il livello corrente del segnale CLK dell’encoder è letto usando digitalRead(ENCODER_CLK) e memorizzato in encState. Il codice verifica poi se questo stato è diverso da oldState. Questa condizione filtra chiamate ridondanti perché l’interrupt è impostato per attivarsi su ogni cambiamento (fronti di salita o discesa) e l’handler potrebbe essere eseguito più volte con lo stesso livello logico.

Se lo stato è cambiato, la direzione di rotazione è calcolata confrontando il segnale DT con CLK. La riga seguente legge il pin DT e verifica se il suo livello corrisponde a quello corrente di CLK:

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

Per un encoder in quadratura, questa relazione indica la direzione di rotazione. Se DT è uguale a CLK, l’encoder si muove in una direzione e counter viene incrementato; se differiscono, counter viene decrementato.

Dopo aver aggiornato il contatore, oldState viene aggiornato al nuovo stato CLK e hasChanged è impostato a true. Questo flag informa il loop principale che ci sono nuovi dati dell’encoder da processare o visualizzare. L’ISR rimane breve ed efficiente, buona pratica per routine di interrupt.

Inizializzazione Pin e I2C

La funzione initPins configura il bus I2C e il pin PCF8574 usato per il pulsante dell’encoder.

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); inizializza la periferica I2C con pin SDA e SCL personalizzati.

pcf8574.pinMode(P5, INPUT_PULLUP); configura il pin P5 sul PCF8574 come input con resistenza di pull-up interna. Questo pin è collegato al pulsante dell’encoder (switch). La configurazione pull-up fa sì che il pin legga alto quando il pulsante non è premuto e basso quando è premuto, assumendo che il pulsante colleghi il pin a massa.

pcf8574.begin() avvia la comunicazione con il dispositivo PCF8574 all’indirizzo I2C configurato. Se fallisce, il codice stampa un messaggio di errore via Serial.println, utile per diagnosticare problemi di cablaggio o configurazione indirizzo.

Inizializzazione Encoder

La funzione initEncoder configura i due segnali dell’encoder e collega un interrupt al pin CLK.

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

pinMode(ENCODER_CLK, INPUT_PULLUP); e pinMode(ENCODER_DT, INPUT_PULLUP); configurano entrambi i canali dell’encoder come input con resistenze di pull-up interne. Questa è una configurazione tipica per encoder rotativi meccanici, che solitamente collegano i pin a massa in uno schema di codice grigio durante la rotazione.

attachInterrupt(digitalPinToInterrupt(ENCODER_CLK), encoder_irq, CHANGE); collega l’handler dell’interrupt encoder_irq al pin CLK dell’encoder. La modalità CHANGE significa che l’ISR sarà attivata sia sui fronti di salita che di discesa del segnale. Questo fornisce una risoluzione più alta e un conteggio più accurato, poiché l’encoder è campionato ad ogni transizione.

Setup

La funzione setup viene eseguita una volta all’avvio e fornisce una sequenza di inizializzazione.

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

Serial.begin(115200); avvia la porta seriale a 115200 baud, utile per debug e monitoraggio. Deve essere chiamata prima di qualsiasi Serial.print o Serial.printf.

initPins(); inizializza il bus I2C e l’expander PCF8574. Questo assicura che il pulsante dell’encoder possa essere letto e che l’hardware I2C sia pronto.

initEncoder(); configura i pin GPIO usati dall’encoder rotativo e imposta l’interrupt così che gli eventi di rotazione siano catturati appena il loop principale parte.

Loop

La funzione loop viene eseguita ripetutamente e gestisce due compiti principali: stampare il valore dell’encoder quando cambia e leggere lo stato del pulsante dell’encoder.

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);
}

Il primo blocco verifica se il contatore dell’encoder è stato aggiornato dall’ISR. Il flag hasChanged è impostato a true dentro l’interrupt quando la posizione dell’encoder cambia.

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

Se hasChanged è vero, il flag viene subito resettato a false e il valore corrente di counter viene stampato sul monitor seriale usando Serial.printf. Questo evita di stampare nell’ISR e separa la logica critica dell’interrupt dall’output seriale più lento.

Poi il codice legge lo stato del pulsante dell’encoder, collegato al PCF8574.

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

pcf8574.digitalRead(P5, true); legge il livello logico corrente del pin P5. Passare true come secondo argomento dice tipicamente alla libreria di eseguire una lettura I2C immediata invece di usare un valore in cache, garantendo una lettura fresca ogni volta.

Poiché il pin è configurato con pull-up, un pulsante non premuto legge alto (non zero), mentre premuto porta la linea a massa e legge basso (zero). Quindi la condizione if (!button) verifica lo stato premuto. Quando il pulsante è premuto, il codice stampa "BTN pressed" e attende 500 millisecondi. Questo ritardo agisce come un semplice debounce e impedisce che il messaggio venga stampato continuamente mentre il pulsante è tenuto premuto.

Infine, il loop termina con un breve ritardo.

delay(1);

Questo piccolo ritardo dà al scheduler la possibilità di gestire attività in background e aiuta a evitare un loop troppo serrato e intensivo per la CPU. Garantisce anche un po’ di respiro ai sottosistemi I2C e seriale.

Output sul Monitor Seriale

Lo screenshot seguente mostra cosa dovresti vedere sul Monitor Seriale quando giri la ghiera dell’encoder o premi il pulsante dell’encoder:

Esempio di Codice: Display e Touchscreen

Il seguente sketch mostra come usare il display e il touchpad. Imposta lo sfondo dello schermo in rosso, stampa il testo “Makerguides” al centro e disegna cerchi neri ovunque si tocchi lo schermo:

Dai un’occhiata veloce al codice completo qui sotto prima di discuterne i dettagli:

#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);
  }
}

Importazioni

Questo sketch inizia includendo le librerie necessarie per I2C, grafica, l’expander I/O e il controller touch.

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

Wire.h è la stessa libreria I2C usata prima con il PCF8574. Arduino_GFX_Library.h fornisce l’astrazione grafica per pilotare il pannello RGB e il driver display ST7701. PCF8574.h gestisce ancora l’expander I/O sul bus I2C. Adafruit_CST8XX.h è il driver per il controller touch capacitivo CST8xx, collegato via I2C e usato per leggere le coordinate touch dal display rotondo.

Costanti e Configurazione I2C

Il codice definisce le assegnazioni pin per il bus I2C, il controllo della retroilluminazione e l’indirizzo I2C del controller touch. Segue lo stesso schema dell’esempio precedente dove SDA e SCL erano configurati esplicitamente.

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

#define I2C_TOUCH_ADDR 0x15

I2C_SDA_PIN e I2C_SCL_PIN selezionano i pin ESP32 usati per il bus I2C condiviso che collega sia il PCF8574 che il controller touch CST8xx. BKL_PIN è un pin PWM che pilota la retroilluminazione LCD. I2C_TOUCH_ADDR specifica l’indirizzo I2C a 7 bit del controller touch così il driver può comunicare con esso.

Oggetti Globali: PCF8574, Controller Touch, Bus RGB e Pannello

Gli oggetti globali configurano l’accesso all’expander I/O, al controller touch e al display.

pcf8574(0x21) è lo stesso dell’esempio precedente: crea un’istanza legata all’indirizzo I2C 0x21. Questo chip controlla diverse linee di controllo come alimentazione LCD, reset LCD e reset/interrupt touch.

tsPanel è un’istanza del driver Adafruit CST8xx, creata con il costruttore di default e poi inizializzata con begin() usando l’interfaccia Wire condivisa e l’indirizzo touch.

PCF8574 pcf8574(0x21);

Adafruit_CST8XX tsPanel = Adafruit_CST8XX();

Pannello RGB

L’oggetto successivo configura il bus dati RGB tra ESP32-S3 e il driver display ST7701. Elenca tutti i pin di timing e colore usati per l’interfaccia RGB parallela.

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 */
);

Questo oggetto Arduino_ESP32RGBPanel definisce la mappatura esatta tra GPIO ESP32 e i segnali del pannello RGB. I primi tre pin sono usati come linee di controllo per il bus del pannello (chip select, clock e dati per l’interfaccia di configurazione). DE, VSYNC, HSYNC, e PCLK sono i segnali di timing, simili a quelli di un’interfaccia display RGB classica. I gruppi rimanenti mappano i cinque bit rossi, sei verdi e cinque blu del bus colore a 16 bit (formato 5-6-5) su pin GPIO specifici.

L’oggetto grafico di alto livello gfx rappresenta l’LCD controllato da ST7701 pilotato tramite questo bus RGB.

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
);

Il primo argomento è il bus RGB definito prima. Il pin di reset è impostato a GFX_NOT_DEFINED perché il pannello viene resettato tramite i pin PCF8574 invece che direttamente da un GPIO ESP32. Il parametro di rotazione è zero, cioè orientamento di default. La flag false IPS è un’opzione del pannello, e 480, 480 definiscono la risoluzione del display.

Il puntatore st7701_type5_init_operations e la dimensione forniscono la sequenza di inizializzazione a basso livello per il driver ST7701, che la libreria invierà al pannello all’avvio. La flag true BGR dice al driver di trattare i dati colore come BGR invece che RGB.

Infine, i valori di porch orizzontale e verticale e di impulso definiscono il timing RGB, simile ai parametri di timing video classici: front porch, larghezza impulso e back porch per HSYNC e VSYNC.

Inizializzazione Retroilluminazione

La retroilluminazione è controllata separatamente dall’elettronica del pannello. La funzione initBacklight configura il pin della retroilluminazione e imposta una luminosità iniziale usando PWM.

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

pinMode(BKL_PIN, OUTPUT); configura il pin della retroilluminazione come output. analogWrite(BKL_PIN, 200); abilita il PWM su quel pin con un duty cycle corrispondente a un livello di luminosità di 200 su una scala tipica 0–255. Questo ti permette di regolare la luminosità della retroilluminazione in seguito scrivendo un valore diverso senza cambiare l’hardware.

Inizializzazione Pin e Periferiche

La funzione initPins configura il bus I2C condiviso, i pin PCF8574, esegue le sequenze di reset e alimentazione per LCD e controller touch, e inizializza il driver touch. Ha un ruolo simile a initPins nell’esempio precedente ma con più periferiche coinvolte.

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); è identica nel concetto allo sketch precedente: avvia la periferica I2C con pin SDA e SCL personalizzati. Le quattro chiamate pcf8574.pinMode configurano linee specifiche del PCF8574 come output: P0 per reset touch, P2 per linea interrupt touch, P3 per abilitazione alimentazione LCD e P4 per reset LCD. Come prima, pcf8574.begin() inizializza la comunicazione con l’expander e stampa un messaggio di errore se fallisce.

Il blocco successivo esegue la sequenza di alimentazione e reset per l’LCD. Questi ritardi precisi e pattern di toggle sono spesso richiesti dai controller LCD e sono codificati secondo il datasheet del display.

  // 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);

Impostare P3 alto abilita la linea di alimentazione LCD. Dopo un breve ritardo per stabilizzare l’alimentazione, P4 viene commutato per generare un impulso di reset corretto per il pannello. La sequenza alto, basso, alto con ritardi specifici assicura che il controller ST7701 parta in uno stato noto.

La sezione touchpad esegue una sequenza di reset molto simile, ma sulle linee di reset e interrupt del controller touch.

  // 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);

Qui P0 viene commutato per resettare il controller CST8xx, e P2 viene portato alto per configurare la linea interrupt in uno stato definito. Queste azioni assicurano che il controller touch sia pronto prima che il driver tenti di comunicare via I2C.

Infine, il controller touch viene inizializzato usando il driver Adafruit.

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

tsPanel.begin(&Wire, I2C_TOUCH_ADDR) dice all’oggetto CST8xx di usare l’istanza globale Wire e lo collega all’indirizzo I2C definito. Se questa chiamata fallisce, lo sketch stampa un messaggio diagnostico.

Inizializzazione LCD

La funzione initLCD prepara il contesto grafico e configura uno stato di disegno base.

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

gfx->begin(); inizializza il pannello ST7701 tramite il bus RGB ed esegue le operazioni di init fornite. Dopo questa chiamata il display è pronto a ricevere comandi di disegno.

gfx->fillScreen(RED); cancella l’intero schermo con uno sfondo rosso. gfx->setTextSize(5); imposta un fattore di scala testo relativamente grande così il testo è facilmente leggibile sul display rotondo 480×480. gfx->setTextColor(WHITE); definisce il colore del testo in primo piano per le successive operazioni di disegno testo.

drawOnLCD

La funzione drawOnLCD incapsula una semplice azione di disegno, posizionando un’etichetta di testo nella regione centrale dello schermo.

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

gfx->setCursor(90, 210); sposta il cursore del testo alla posizione (90, 210) in coordinate pixel. Con un display 480×480, questa è più o meno la posizione centrale a seconda della dimensione del font. gfx->print("Makerguides"); poi rende la stringa di testo con la dimensione e il colore configurati precedentemente sullo schermo.

Setup

La funzione setup fornisce nuovamente una sequenza di inizializzazione, simile allo sketch precedente che ha configurato seriale, pin e encoder.

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

Serial.begin(115200); avvia la comunicazione seriale per il debug. initPins(); configura il bus I2C, i pin PCF8574, ed esegue le sequenze di reset LCD e touch come descritto sopra. initBacklight(); abilita e imposta la luminosità della retroilluminazione così il contenuto sul display è visibile. initLCD(); inizializza il driver grafico e dipinge lo sfondo rosso, e drawOnLCD(); posiziona la stringa iniziale “Makerguides” sullo schermo.

Loop

Il loop principale controlla costantemente se il pannello touch viene toccato. Quando viene rilevato un tocco, legge le coordinate e disegna un piccolo cerchio nero in quella posizione.

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(); interroga il driver CST8xx per verificare se ci sono punti touch attivi. Se la funzione restituisce true, almeno un dito è sullo schermo. tsPanel.getPoint(0); recupera il primo punto touch come una struttura CST_TS_Point contenente le coordinate x e y. Queste coordinate sono stampate sul monitor seriale per il debug con Serial.printf, simile a come hai stampato il contatore dell’encoder e lo stato del pulsante precedentemente.

gfx->fillCircle(p.x, p.y, 10, BLACK); disegna un cerchio pieno di raggio 10 pixel in nero nella posizione del tocco. La chiamata delay(10); fornisce una breve pausa per limitare la frequenza di aggiornamento ed evitare di sovraccaricare il bus I2C e il driver grafico con troppe operazioni al secondo.

Esempio di Codice: Display, Touch e Encoder

Questo ultimo sketch unisce i concetti degli esempi precedenti: configurazione I2C e controllo PCF8574, display RGB e gestione touch, e un encoder rotativo basato su interrupt.

Il codice ti permette di controllare la luminosità del display girando la ghiera dell’encoder e mostra anche il valore corrente di luminosità (0…255) al centro dello schermo. Il pulsante dell’encoder cambia il colore del display in blu e gli eventi touch sono ancora registrati come punti neri sul display.

Dai un’occhiata veloce al codice completo qui sotto e poi ne discuteremo i dettagli.

#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);
}

Importazioni

Questo sketch combina tutto dagli esempi precedenti: periferiche I2C, display RGB, touch e encoder rotativo basato su interrupt, includendo le librerie necessarie:

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

Costanti e Stato Globale

La sezione successiva definisce i pin per I2C, retroilluminazione, segnali encoder e indirizzo controller touch. Introduce anche diverse variabili globali volatile che tengono il valore corrente di luminosità e lo stato dell’encoder, simili all’esempio precedente del contatore encoder:

// 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 e I2C_SCL_PIN configurano il bus I2C condiviso, come prima. BKL_PIN è il pin PWM usato per pilotare la retroilluminazione LCD. ENCODER_CLK e ENCODER_DT sono i segnali in quadratura dell’encoder rotativo, identici nel ruolo allo sketch encoder precedente. I2C_TOUCH_ADDR contiene l’indirizzo del controller touch CST8xx.

brightness memorizza il valore corrente di luminosità della retroilluminazione. È dichiarato volatile perché modificato all’interno di una routine di servizio interrupt. encState e oldState sono usati per rilevare transizioni sulla linea CLK dell’encoder, e brightnessChanged è un flag che informa il loop principale che un nuovo livello di luminosità è disponibile.

Oggetti PCF8574, Touch e Display

Gli oggetti globali per l’expander I/O, il pannello touch e il bus display sono gli stessi dello sketch precedente display-e-touch. Definiscono come l’ESP32 interagisce con i chip esterni e il pannello RGB.

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 */
);

L’oggetto pcf8574 è legato all’indirizzo 0x21 ed è responsabile dell’alimentazione LCD, reset e pulsante push dell’encoder. L’oggetto tsPanel incapsula il controller touch CST8xx. Il puntatore bus definisce la mappatura dei segnali RGB e di timing ai GPIO ESP32 esattamente come prima, usando la tua tabella pin R0–R4, G0–G5 e B0–B4.

L’oggetto grafico di alto livello per il pannello RGB ST7701 è poi creato sopra questo bus.

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
);

Come prima, questo incapsula la sequenza di inizializzazione a basso livello ST7701, i parametri di timing e il formato colore. Il pin di reset è segnato come indefinito perché il reset è pilotato tramite i pin PCF8574 durante initPins.

Inizializzazione Retroilluminazione

La funzione di inizializzazione della retroilluminazione prende il valore corrente brightness e lo applica al pin PWM della retroilluminazione:

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

pinMode(BKL_PIN, OUTPUT); configura il pin della retroilluminazione come output digitale. analogWrite(BKL_PIN, brightness); avvia l’output PWM su questo pin usando il valore brightness come duty cycle.

Inizializzazione Pin e Periferiche

La funzione initPins è una versione estesa di quella vista prima. Configura I2C, i pin PCF8574, la sequenza di alimentazione e reset LCD, la sequenza di reset touch e anche il pin PCF8574 che legge il pulsante push dell’encoder.

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); avvia il bus I2C con i pin specificati, come negli sketch precedenti. P0, P2, P3 e P4 sono configurati come output per controllare reset touch, linea interrupt touch, alimentazione LCD e reset LCD. P5 è configurato come INPUT_PULLUP perché collegato allo switch dell’encoder. Questo rispecchia come hai configurato P5 come input pulsante encoder nell’esempio originale solo encoder.

Segue la sequenza di temporizzazione di alimentazione e reset LCD, identica nella struttura al codice display precedente.

  // 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 abilita la linea di alimentazione LCD, poi P4 viene commutato con ritardi specifici per eseguire un reset hardware del controller ST7701.

La sequenza di reset e configurazione del touchpad è anche la stessa di prima.

  // 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 viene pulsato per resettare il controller CST8xx e P2 viene portato alto per stabilire uno stato definito per la linea interrupt touch.

Il controller touch viene quindi inizializzato. tsPanel.begin(&Wire, I2C_TOUCH_ADDR) lega il driver CST8xx al bus I2C condiviso e all’indirizzo specificato, e stampa un messaggio diagnostico se il dispositivo non viene trovato:

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

Routine di Servizio Interrupt Encoder

L’handler dell’interrupt dell’encoder è simile alla funzione encoder_irq vista prima, ma invece di mantenere un contatore di posizione, aggiorna la variabile brightness a passi di cinque.

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 garantisce che l’ISR sia posizionata nella RAM delle istruzioni per un’esecuzione veloce su ESP32, come discusso prima. All’interno della funzione, encState è impostato al livello logico corrente del pin CLK dell’encoder usando digitalRead(ENCODER_CLK). La condizione if (encState != oldState) assicura che il codice reagisca solo quando il segnale CLK cambia realmente, evitando aggiornamenti multipli sullo stesso livello.

La direzione di rotazione è di nuovo determinata confrontando il segnale DT con lo stato corrente di CLK. In questo sketch, la condizione è invertita rispetto all’esempio precedente per dare una sensazione naturale di aumento e diminuzione della luminosità.

La riga seguente sottrae 5 o aggiunge 5 alla variabile brightness in base alla fase relativa dei segnali in quadratura. La rotazione positiva aumenta la luminosità, quella negativa la diminuisce.

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

E la riga seguente assicura che la luminosità rimanga entro un limite inferiore definito di 5 (evitando display completamente spento) e un limite superiore di 255 (valore PWM massimo per massima luminosità):

brightness = constrain(brightness, 5, 255);

oldState viene aggiornata al nuovo stato CLK, e brightnessChanged è impostato a true per notificare al loop principale che la retroilluminazione e la visualizzazione sullo schermo devono essere aggiornate. Come prima, tutto il lavoro pesante come I/O seriale e grafico è tenuto fuori dall’ISR e gestito nel loop principale.

Inizializzazione Encoder

La funzione initEncoder configura i pin dell’encoder e collega l’interrupt alla linea CLK. È praticamente lo stesso schema dello sketch encoder originale.

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

Entrambi i canali dell’encoder sono configurati come input con resistenze di pull-up interne. L’interrupt è collegato al pin CLK usando attachInterrupt, con modalità CHANGE per attivarsi su fronti di salita e discesa. Ad ogni cambiamento, viene invocato l’handler encoder_irq, che aggiorna la luminosità.

Inizializzazione LCD e Visualizzazione Luminosità

La funzione di inizializzazione LCD avvia il pannello ST7701 e configura le impostazioni testo. È simile a initLCD vista prima, ma usa una dimensione testo più grande per mostrare chiaramente il valore di luminosità:

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

gfx->begin(); inizializza il controller display e invia la sequenza di configurazione. gfx->fillScreen(RED); imposta uno sfondo rosso. gfx->setTextSize(10); sceglie un fattore di scala grande così il valore numerico della luminosità risalta chiaramente. gfx->setTextColor(WHITE); configura il bianco come colore del testo.

La funzione updateBrightness lega il valore logico di luminosità sia alla retroilluminazione fisica che alla visualizzazione sullo schermo.

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

analogWrite(BKL_PIN, brightness); aggiorna il duty cycle PWM, cambiando effettivamente l’intensità della retroilluminazione LED. Lo schermo viene poi nuovamente pulito in rosso usando gfx->fillScreen(RED);.

Il cursore viene posizionato alle coordinate (150, 200), e gfx->printf("%3d", brightness); stampa la luminosità come numero a tre cifre.

Setup

La funzione setup inizializza tutti i sottosistemi: seriale, pin I2C e chip esterni, encoder, retroilluminazione e LCD, e infine visualizza la luminosità iniziale sul display.

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

Serial.begin(115200); avvia la UART per l’output di debug. initPins(); prepara il bus I2C, PCF8574, LCD e controller touch come discusso prima. initEncoder(); abilita l’interfaccia encoder basata su interrupt. initBacklight(); applica il valore iniziale brightness al pin della retroilluminazione. initLCD(); avvia il contesto grafico, e updateBrightness(); sincronizza immediatamente la visualizzazione sullo schermo e il PWM della retroilluminazione con il valore corrente di luminosità.

Loop

Il loop principale controlla periodicamente il pulsante dell’encoder, l’input touch e il flag di aggiornamento luminosità. Risponde a ciascuno di questi eventi usando le periferiche configurate.

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);
}

Il primo blocco legge il pulsante push dell’encoder tramite PCF8574. pcf8574.digitalRead(P5, true); legge il pin P5 con una transazione I2C immediata. Poiché P5 è configurato come INPUT_PULLUP, il pulsante legge alto quando rilasciato e basso quando premuto. La condizione if (!button) rileva lo stato premuto. Alla pressione, lo sketch stampa un messaggio sul monitor seriale e riempie il display di blu, fornendo un’indicazione visiva semplice che il pulsante è stato premuto.

Il blocco successivo gestisce l’input touch capacitivo, riutilizzando la stessa logica dell’esempio precedente sul LCD. Se tsPanel.touched() restituisce true, almeno un punto touch è attivo. La funzione tsPanel.getPoint(0); recupera il primo punto touch, che viene poi stampato sul monitor seriale. gfx->fillCircle(p.x, p.y, 10, BLACK); disegna un piccolo cerchio nero alle coordinate del tocco, permettendo all’utente di disegnare sopra il contenuto attuale dello schermo.

Il terzo blocco verifica se l’ISR dell’encoder ha aggiornato la variabile brightness. Se brightnessChanged è true, il loop principale resetta il flag e chiama updateBrightness();. Questo applica la nuova luminosità alla retroilluminazione e ridisegna il valore numerico sullo schermo. Muovendo rapidamente l’encoder si generano più interrupt e si imposta ripetutamente brightnessChanged, ma il loop assicura che gli aggiornamenti di luminosità siano gestiti nel contesto principale dove è sicuro fare operazioni seriali e grafiche.

L’ultimo delay(100); introduce una breve pausa per limitare la frequenza del loop e rendere più fluida l’interazione utente senza sovraccaricare CPU o I2C.

Conclusioni

Questo tutorial ti ha fornito esempi di codice per iniziare con il CrowPanel 2.1inch-HMI ESP32 Rotary Display. Consulta la pagina Elecrow Wiki per informazioni aggiuntive e per altri esempi di codice vedi il github repo.

Nota che devi installare librerie più vecchie e una versione precedente del core ESP32 (2.0.14) per far funzionare il codice. Inoltre alcuni esempi lì usano la libreria LVGL, che ho evitato qui per mantenere il codice semplice.

Se cerchi un modulo display simile con ghiera encoder rotativa dai un’occhiata al CrowPanel 1.28inch-HMI ESP32 Rotary Display o al Matouch 1.28″ ToolSet_Controller. Se ti serve solo un display rotondo (senza ghiera encoder), il tutorial Digital Clock on CrowPanel 1.28″ Round Display potrebbe esserti utile.

Infine, se vuoi imparare di più sugli encoder rotativi, dai un’occhiata al nostro tutorial How To Interface A Quadrature Rotary Encoder.

Se hai domande sentiti libero di lasciarle nella sezione commenti.

Buon divertimento con il fai-da-te 😉