Skip to Content

Primeros pasos con CrowPanel 2.1inch-HMI ESP32 Rotary Display

Primeros pasos con CrowPanel 2.1inch-HMI ESP32 Rotary Display

El CrowPanel 2.1inch-HMI ESP32 Rotary Display de Elecrow es un módulo grande y redondo que integra un microcontrolador ESP32-S3, una pantalla táctil circular IPS de 2,1 pulgadas con resolución 480×480, y un codificador rotatorio con botón pulsador en una sola unidad.

En este tutorial, repasaremos los pasos esenciales para comenzar con esta pantalla; desde la configuración inicial hasta probar sus capacidades de visualización y entrada. Aprenderás a leer datos del codificador rotatorio para una entrada de usuario fluida y a capturar eventos táctiles de la pantalla circular.

Partes necesarias

Solo necesitarás el CrowPanel 2.1inch-HMI ESP32 Rotary Display para este proyecto. El módulo de pantalla viene con un cable USB y un conector GPIO, por lo que no se requiere hardware adicional.

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

El CrowPanel 2.1inch-HMI ESP32 Rotary Display es un módulo de pantalla que combina un panel IPS redondo de alta resolución, una pantalla táctil capacitiva y un codificador rotatorio con función de botón pulsador.

El módulo está construido alrededor del sistema en chip ESP32‑S3R8 que utiliza un procesador dual-core Xtensa LX7 de 32 bits capaz de alcanzar velocidades de hasta 240 MHz. Está equipado con 8 Mbit de PSRAM junto con 16 Mbyte de almacenamiento flash a bordo.

La parte de la pantalla es un panel IPS RGB circular de 2,1 pulgadas (ST7701 controlador) con una resolución de 480 × 480 píxeles, con una capa táctil capacitiva que soporta operación táctil en toda la pantalla y una retroiluminación.

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

En cuanto a la operación física, el hardware incorpora un mecanismo de “perilla” con codificador rotatorio. La perilla puede girar en sentido horario y antihorario, además de presionarse hacia adentro (funcionando como un interruptor) para la entrada del usuario.

Para conectividad y expansión, el módulo soporta interfaces inalámbricas y por cable. En el lado inalámbrico cuenta con WiFi IEEE 802.11 a/b/g/n (2.4 GHz) y Bluetooth Low Energy (BLE) / Bluetooth 5.0.

En el lado por cable, el módulo expone un conector de carga/interfaz de 5 V que funciona tanto como fuente de alimentación como puerto de programación. Además, la placa ofrece tres interfaces de expansión: una interfaz UART, una interfaz I2C y un conector FPC (12 pines) para conectividad adicional.

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

Definiciones de pines

La siguiente tabla lista los pines necesarios para controlar todos los componentes del módulo de pantalla:

Grupo de funciónSeñal / PropósitoNúmero de pinNotas
Interfaz I²CSDA38Línea de datos I²C principal
SCL39Línea de reloj I²C principal
Codificador rotatorioEncoder A42Canal de cuadratura A
Encoder B4Canal de cuadratura B
Retroiluminación de pantallaControl de retroiluminación6Pin con capacidad PWM usado para el brillo de la retroiluminación LCD
Control OLEDRESET–1No se usa pin de reset (reset por software)
Interfaz de pantalla RGBCS16Línea de selección de chip
SCK2Reloj serial de pantalla
SDA1Datos seriales de pantalla
DE40Habilitación de datos
VSYNC7Sincronización vertical
HSYNC15Sincronización horizontal
PCLK41Reloj de píxeles
Pines de datos de color RGB (formato 5-6-5)R046Bit 0 rojo
R13Bit 1 rojo
R28Bit 2 rojo
R318Bit 3 rojo
R417Bit 4 rojo
G014Bit 0 verde
G113Bit 1 verde
G212Bit 2 verde
G311Bit 3 verde
G410Bit 4 verde
G59Bit 5 verde
B05Bit 0 azul
B145Bit 1 azul
B248Bit 2 azul
B347Bit 3 azul
B421Bit 4 azul
Expansor I/O PCF8574Dirección I²C0x21Conectado a los pines 38 (SDA) y 39 (SCL)

Especificaciones técnicas

La siguiente tabla resume las especificaciones técnicas del módulo CrowPanel 2.1inch-HMI ESP32 Rotary Display:

Categoría de especificaciónDetalles
Procesador principalSoC: Xtensa LX7 dual-core (32 bits) en el ESP32‑S3R8 (hasta 240 MHz)
Memoria y almacenamientoRAM del sistema: 512 kB SRAM; PSRAM: 8 MB; Flash: 16 MB
Panel de pantallaTamaño: 2.1 pulgadas; Tipo: IPS; Resolución: 480 × 480 píxeles; Táctil: capa táctil capacitiva de pantalla completa
Mecanismos de entrada de usuarioPerilla rotatoria (rotación en sentido horario/antihorario + pulsación completa); Entrada táctil capacitiva en la superficie de la pantalla
Conectividad inalámbricaWiFi: IEEE 802.11 a/b/g/n (2.4 GHz); Bluetooth: BLE / Bluetooth 5.0
Puertos de interfaz / expansiónEntrada de 5 V (carga y programación); Interfaz UART: 1× UART0 + 1× UART1 vía ZX-MX 1.25-4P; Interfaz I²C; Conector FPC de 12 pines (alimentación/programación y GPIO)
Botones e indicadoresBotón RESET a bordo; botón BOOT; interruptor de pulsación de perilla (botón de confirmación); LED indicador de encendido; retroiluminación LCD
Alimentación y voltajeEntrada del módulo: 5 V / 1 A; Nivel lógico del chip principal: 3.3 V; Operación nominal del módulo a 5 V
Mecánico / físicoDimensiones: 79 × 79 × 30 mm; Peso neto: 80 g; Carcasa: aleación de aluminio + plástico + acrílico
Rango de temperaturaOperación: –20 °C a +65 °C; Almacenamiento: –40 °C a +80 °C
Soporte de software / entorno de desarrolloCompatible con Arduino IDE, ESP-IDF, Lua RTOS, MicroPython, PlatformIO; Biblioteca gráfica UI: soporte LVGL

Instalación del Core ESP32

Si este es tu primer proyecto con una placa de la serie ESP32, primero deberás instalar el core ESP32. Sin embargo, para el CrowPanel 2.1inch Display necesitarás instalar una versión específica (2.0.14) del core ESP32.

Comienza abriendo el diálogo de Preferencias en el Arduino IDE seleccionando “Preferences…” en el menú “File”. Esto abrirá el diálogo de Preferencias que se muestra a continuación.

En la pestaña Settings encontrarás un cuadro de edición en la parte inferior del diálogo etiquetado como “Additional boards manager URLs“:

Additional boards manager URLs in Preferences
URLs adicionales del gestor de placas en Preferencias

En este campo de entrada copia la siguiente URL:

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

Esto permitirá que el Arduino IDE sepa dónde encontrar las librerías del core ESP32. A continuación instalaremos las placas ESP32 usando el Gestor de Placas.

Abre el Gestor de Placas vía «Tools -> Boards -> Board Manager». Verás el Gestor de Placas en la barra lateral izquierda. Escribe «ESP32» en el campo de búsqueda en la parte superior y deberías ver dos tipos de placas ESP32; las «Arduino ESP32 Boards» y las placas «esp32 by Espressif». Queremos las «esp32 libraries by Espressif».

En el menú desplegable selecciona la versión «2.0.14» y luego pulsa el botón «INSTALL». La captura de pantalla a continuación muestra lo que deberías ver en el GESTOR DE PLACAS tras una instalación exitosa del Core ESP32 2.0.14:

Install ESP32 Core 2.0.14
Instalar Core ESP32 2.0.14

Ten en cuenta que puedes instalar versiones hasta la 2.0.17 pero no la 3.x. Las librerías que instalaremos en la sección posterior no funcionan con el core ESP32 3.x.

Selección de placa

Finalmente necesitamos seleccionar una placa ESP32. En el caso del CrowPanel 2.1inch-HMI ESP32 Rotary Display, elegimos la genérica «ESP32S3 Dev Module». Para ello, haz clic en el menú desplegable y luego en «Select other board and port…»:

Drop-down Menu for Board Selection
Menú desplegable para selección de placa

Esto abrirá un diálogo donde puedes escribir «esp32s3 dev» en la barra de búsqueda. Verás la placa «ESP32S3 Dev Module» bajo Boards. Haz clic en ella y selecciona el puerto COM para activarla, luego pulsa OK:

Board Selection Dialog "ESP32S3 Dev Module" board
Diálogo de selección de placa «ESP32S3 Dev Module»

Ten en cuenta que debes conectar la placa mediante el cable USB a tu ordenador antes de poder seleccionar un puerto COM. Conecta el cable USB que viene con el módulo CrowPanel Display directamente al puerto etiquetado como «USB-5V-IN» como se muestra a continuación:

USB Port of CrowPanel Display Module
Puerto USB del módulo CrowPanel Display

Configuración de herramientas

A continuación se muestran las configuraciones que debes usar con el módulo CrowPanel Display. Las encontrarás en el menú Tools de tu Arduino IDE.

Tool settings for CrowPanel 2.1inch-HMI ESP32 Rotary Display
Configuración de herramientas para CrowPanel 2.1inch-HMI ESP32 Rotary Display

Lo más importante para los ejemplos de código en las siguientes secciones es configurar USB CDC on Boot en «Enabled». Esto te permite enviar o recibir datos vía el puerto serial, necesario para depuración. También necesitarás configurar correctamente «OPI PSRAM», «Huge APP» y «Flash Size» para los ejemplos de código de la pantalla.

Instalación de librerías

A continuación necesitamos instalar librerías específicas y versiones concretas para que el código del CrowPanel 2.1inch-HMI ESP32 Rotary Display funcione.

Ve a la página de Elecrow github repo para el CrowPanel 2.1inch-HMI Display. Haz clic en el botón verde «<> Code» y luego en «Download ZIP» para descargar el repositorio como un archivo ZIP:

Luego descomprime el archivo ZIP para extraer su contenido. Deberías ver los siguientes archivos en una carpeta descomprimida llamada «CrowPanel-2.1inch-HMI-ESP32-Rotary-Display-480-480-IPS-Round-Touch-Knob-Screen-master»:

Ignora los archivos relacionados con la pantalla «CrowPanel 1.28inch …». Solo necesitamos copiar el contenido de la carpeta «example/libraries» dentro de la carpeta «libraries» del Arduino IDE. En Windows, la carpeta «libraries» suele estar ubicada en:

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

Como esta carpeta ya contiene librerías instaladas, te recomiendo renombrarla temporalmente, por ejemplo a «_libraries», y crear una nueva carpeta llamada «libraries». Así evitas conflictos con las librerías ya instaladas y no las pierdes. Luego puedes revertir fácilmente este cambio. La imagen a continuación muestra cómo debería verse tu carpeta «Arduino» con las librerías:

Luego copiamos los archivos de la carpeta «example/libraries» a la nueva carpeta «libraries» como se muestra a continuación:

No necesitas copiar las librerías que están tachadas, ya que están relacionadas con la UI LVGL, que no usaremos. En las siguientes secciones te mostraré algunos ejemplos de código para probar la pantalla.

Ejemplo de código: Interfaz serial

Comenzamos probando la comunicación serial. Abre tu Arduino IDE, introduce el siguiente código y súbelo al módulo CrowPanel Display.

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

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

Luego abre el Monitor Serial y deberías ver el texto «Makerguides» impreso cada dos segundos. Si no, asegúrate de que la configuración de Tools y la velocidad en baudios (115200) sean correctas. En particular, necesitas que «USB CDC on Boot» esté en «Enabled».

Ejemplo de código: Encoder

En este siguiente ejemplo aprenderás a usar el encoder y el interruptor del encoder. Usaremos un enfoque basado en interrupciones para leer un codificador rotatorio en cuadratura y el PCF8574 para leer el estado del botón pulsador del encoder.

La rotación del encoder se procesa en una ISR rápida con detección de dirección, mientras que el loop principal se encarga de reportar cambios y leer el estado del botón. Echa un vistazo rápido al código y luego discutiremos sus detalles.

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

Importaciones

El sketch comienza incluyendo dos librerías que manejan la comunicación I2C y el expansor de E/S PCF8574. Estos encabezados deben incluirse antes de poder usar Wire o PCF8574 objetos.

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

Wire.h proporciona la interfaz estándar Arduino I2C (TWI). PCF8574.h envuelve el acceso I2C de bajo nivel al chip PCF8574 para que puedas tratar sus pines casi como pines digitales normales.

Constantes

Luego, el código define los pines usados para I2C y para el codificador rotatorio:

#define I2C_SDA_PIN 38
#define I2C_SCL_PIN 39

#define ENCODER_CLK 42  
#define ENCODER_DT 4   

I2C_SDA_PIN y I2C_SCL_PIN seleccionan qué pines del ESP32 se usan como SDA y SCL para el bus I2C hardware. En el ESP32 puedes enrutar I2C a varios pines, por lo que es necesario especificarlos aquí.

ENCODER_CLK y ENCODER_DT son las dos salidas en cuadratura del codificador rotatorio. La señal CLK es típicamente el canal principal usado para la interrupción, y DT es el segundo canal usado para determinar la dirección de rotación.

Estado global y variables volátiles

El código usa varias variables globales para rastrear el estado del encoder. Estas variables están marcadas volatile porque se modifican dentro de una rutina de servicio de interrupción.

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

counter mantiene la posición acumulada del encoder. Cada paso del encoder incrementa o decrementa este valor.

encState representa el nivel lógico actual del pin CLK del encoder visto en el manejador de interrupción. oldState guarda el estado anterior de CLK para que el código pueda detectar transiciones y evitar contar el mismo nivel varias veces.

hasChanged se usa como bandera para señalar al loop principal que el contador ha sido actualizado en la interrupción. Marcar estas variables como volatile indica al compilador que pueden cambiar en cualquier momento y no deben almacenarse en registros, lo cual es crítico cuando son modificadas por una ISR.

Objeto PCF8574

Luego el código crea una instancia global de la clase PCF8574, especificando la dirección I2C del chip.

PCF8574 pcf8574(0x21);

Esta línea indica a la librería PCF8574 que el expansor de E/S es accesible en la dirección I2C 0x21. Todas las llamadas posteriores a pcf8574.pinMode o pcf8574.digitalRead usarán esta dirección para comunicarse con ese dispositivo específico en el bus I2C.

Rutina de servicio de interrupción del encoder

La parte más sensible al tiempo del código es la rutina de servicio de interrupción (ISR) que maneja los cambios del encoder. Está ubicada en IRAM usando el atributo IRAM_ATTR para que pueda ejecutarse rápida y confiablemente en el 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 asegura que la función se almacene en la RAM de instrucciones en lugar de en flash, evitando retrasos causados por el acceso a flash y es recomendado para ISRs en ESP32.

Dentro de la ISR, se lee el nivel actual de la señal CLK del encoder usando digitalRead(ENCODER_CLK) y se guarda en encState. Luego el código verifica si este estado es diferente de oldState. Esta condición filtra llamadas redundantes porque la interrupción está configurada para activarse en cualquier cambio (flanco ascendente o descendente) y el manejador podría ejecutarse varias veces con el mismo nivel lógico.

Si el estado ha cambiado, la dirección de rotación se calcula comparando la señal DT con CLK. La siguiente línea lee el pin DT y verifica si su nivel coincide con el nivel actual de CLK:

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

Para un encoder en cuadratura, esta relación indica la dirección de rotación. Si DT es igual a CLK, el encoder se mueve en una dirección y counter se incrementa; si difieren, counter se decrementa.

Después de actualizar el contador, oldState se actualiza al nuevo estado de CLK y hasChanged se establece en true. Esta bandera informa al loop principal que hay nuevos datos del encoder para procesar o mostrar. La ISR se mantiene corta y eficiente, lo cual es buena práctica para rutinas de interrupción.

Inicialización de pines e I2C

La función initPins configura el bus I2C y el pin del PCF8574 usado para el botón pulsador del 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); inicializa el periférico I2C con pines SDA y SCL personalizados.

pcf8574.pinMode(P5, INPUT_PULLUP); configura el pin P5 en el PCF8574 como entrada con resistencia pull-up interna. Este pin está conectado al botón pulsador del encoder. La configuración pull-up significa que el pin leerá alto cuando el botón no esté presionado y bajo cuando sí, asumiendo que el botón conecta el pin a tierra.

pcf8574.begin() inicia la comunicación con el dispositivo PCF8574 en la dirección I2C configurada. Si falla, el código imprime un mensaje de error vía Serial.println, lo que ayuda a diagnosticar problemas de cableado o configuración de dirección.

Inicialización del encoder

La función initEncoder configura las dos señales del encoder y adjunta una interrupción 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); y pinMode(ENCODER_DT, INPUT_PULLUP); configuran ambos canales del encoder como entradas con resistencias pull-up internas. Esta es una configuración típica para codificadores rotatorios mecánicos, que suelen conectar los pines a tierra en un patrón de código Gray al girar la perilla.

attachInterrupt(digitalPinToInterrupt(ENCODER_CLK), encoder_irq, CHANGE); adjunta el manejador de interrupción encoder_irq al pin CLK del encoder. El modo CHANGE significa que la ISR se activará en ambos flancos, ascendente y descendente, de la señal. Esto proporciona mayor resolución y conteo más preciso, ya que el encoder se muestrea en cada transición.

Setup

La función setup se ejecuta una vez al inicio y proporciona una secuencia de inicialización.

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

Serial.begin(115200); inicia el puerto serial a 115200 baudios, útil para depuración y monitoreo. Debe llamarse antes de cualquier Serial.print o Serial.printf.

initPins(); inicializa el bus I2C y el expansor de E/S PCF8574. Esto asegura que se pueda leer el botón del encoder y que el hardware I2C esté listo.

initEncoder(); configura los pines GPIO usados por el codificador rotatorio y configura la interrupción para que los eventos de rotación se capturen tan pronto como comience el loop principal.

Loop

La función loop se ejecuta repetidamente y maneja dos tareas principales: imprimir el valor del encoder cuando cambia y leer el estado del botón pulsador del 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);
}

El primer bloque verifica si el contador del encoder ha sido actualizado por la ISR. La bandera hasChanged se establece en true dentro de la interrupción cuando la posición del encoder cambia.

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

Si hasChanged es verdadero, la bandera se resetea inmediatamente a falso y se imprime el valor actual de counter en el monitor serial usando Serial.printf. Esto evita imprimir dentro de la ISR y desacopla la lógica crítica de interrupción de la salida serial más lenta.

Luego, el código lee el estado del botón pulsador del encoder, que está conectado al PCF8574.

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

pcf8574.digitalRead(P5, true); lee el nivel lógico actual del pin P5. Pasar true como segundo argumento típicamente indica a la librería que realice una lectura I2C inmediata en lugar de usar un valor en caché, asegurando una lectura fresca cada vez.

Como el pin está configurado con pull-up, un botón sin presionar lee alto (no cero), y un botón presionado lleva la línea a tierra y lee bajo (cero). Por lo tanto, la condición if (!button) verifica el estado presionado. Cuando se detecta el botón presionado, el código imprime "BTN pressed" y espera 500 milisegundos. Este retardo actúa como un simple debounce y evita que el mensaje se imprima continuamente mientras el botón está presionado.

Finalmente, el loop termina con un pequeño retardo.

delay(1);

Este pequeño retardo da oportunidad al scheduler para manejar tareas en segundo plano y ayuda a evitar un loop muy cerrado y que consuma mucho CPU. También asegura que los subsistemas I2C y serial tengan un poco de margen.

Salida en el Monitor Serial

La siguiente captura muestra lo que deberías ver en el Monitor Serial al girar el anillo del encoder o presionar el botón del encoder:

Ejemplo de código: Pantalla y pantalla táctil

El siguiente sketch muestra cómo usar la pantalla y el panel táctil. Establece el fondo de pantalla en rojo, imprime el texto «Makerguides» en el centro y dibuja círculos negros donde sea que se toque la pantalla:

Echa un vistazo rápido al código completo a continuación antes de discutir los detalles:

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

Importaciones

Este sketch comienza incluyendo las librerías necesarias para I2C, gráficos, el expansor de E/S y el controlador táctil.

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

Wire.h es la misma librería I2C que usaste antes con el PCF8574. Arduino_GFX_Library.h proporciona la abstracción gráfica para manejar el panel RGB y el controlador de pantalla ST7701. PCF8574.h nuevamente maneja el expansor de E/S en el bus I2C. Adafruit_CST8XX.h es el controlador para el controlador táctil capacitivo CST8xx, conectado vía I2C y que se usará para leer coordenadas táctiles de la pantalla redonda.

Constantes y configuración I2C

El código define asignaciones de pines para el bus I2C, el control de retroiluminación y la dirección I2C del controlador táctil. Esto sigue el mismo patrón que en el ejemplo anterior donde se configuraron explícitamente los pines SDA y SCL.

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

#define I2C_TOUCH_ADDR 0x15

I2C_SDA_PIN y I2C_SCL_PIN seleccionan los pines ESP32 usados para el bus I2C compartido que conecta tanto el PCF8574 como el controlador táctil CST8xx. BKL_PIN es un pin con capacidad PWM que controla la retroiluminación LCD. I2C_TOUCH_ADDR especifica la dirección I2C de 7 bits del controlador táctil para que el driver pueda comunicarse con él.

Objetos globales: PCF8574, controlador táctil, bus RGB y panel

Los objetos globales configuran el acceso al expansor de E/S, el controlador táctil y la pantalla.

pcf8574(0x21) es igual que en el ejemplo anterior: crea una instancia ligada a la dirección I2C 0x21. Este chip controla varias líneas de control como alimentación LCD, reset LCD y reset e interrupción táctil.

tsPanel es una instancia del driver Adafruit CST8xx, creada con el constructor por defecto y luego inicializada con begin() usando la interfaz Wire compartida y la dirección táctil.

PCF8574 pcf8574(0x21);

Adafruit_CST8XX tsPanel = Adafruit_CST8XX();

Panel RGB

El siguiente objeto configura el bus de datos RGB entre el ESP32-S3 y el controlador de pantalla ST7701. Enumera todas las señales de temporización y color usadas para la interfaz RGB paralela.

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

Este objeto Arduino_ESP32RGBPanel define el mapeo exacto entre los GPIO del ESP32 y las señales del panel RGB. Los primeros tres pines se usan como líneas de control para el bus del panel (selección de chip, reloj y datos para la interfaz de configuración). DE, VSYNC, HSYNC, y PCLK son las señales de temporización, similares a las de una interfaz clásica de pantalla RGB. Los grupos restantes asignan los cinco bits rojos, seis verdes y cinco azules del bus de color de 16 bits (formato 5-6-5) a pines GPIO específicos.

El objeto gráfico de alto nivel gfx representa el LCD controlado por ST7701 que se maneja a través de este 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
);

El primer argumento es el bus RGB definido previamente. El pin de reset se establece en GFX_NOT_DEFINED porque el panel se resetea usando pines PCF8574 en lugar de un GPIO directo del ESP32. El parámetro de rotación es cero, lo que significa orientación por defecto. La bandera false IPS es una opción del panel, y 480, 480 define la resolución de la pantalla.

El puntero st7701_type5_init_operations y el tamaño proporcionan la secuencia de inicialización de bajo nivel para el driver ST7701, que la librería enviará al panel al arrancar. La bandera true BGR indica al driver que trate los datos de color como BGR en lugar de RGB.

Finalmente, los valores de porch y pulso horizontal y vertical definen la temporización RGB, similar a parámetros encontrados en temporización clásica de video: front porch, ancho de pulso y back porch para HSYNC y VSYNC.

Inicialización de retroiluminación

La retroiluminación se controla separadamente de la electrónica del panel. La función initBacklight configura el pin de retroiluminación y establece un brillo inicial usando PWM.

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

pinMode(BKL_PIN, OUTPUT); configura el pin de retroiluminación como salida. analogWrite(BKL_PIN, 200); habilita PWM en ese pin con un ciclo de trabajo correspondiente a un nivel de brillo de 200 en una escala típica de 0 a 255. Esto permite ajustar el brillo de la retroiluminación más adelante escribiendo un valor diferente sin cambiar el hardware.

Inicialización de pines y periféricos

La función initPins configura el bus I2C compartido, configura los pines PCF8574, realiza secuencias de reset y alimentación para el LCD y el controlador táctil, e inicializa el driver táctil. Cumple un rol similar a initPins en el ejemplo anterior pero con más periféricos involucrados.

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); es idéntica en concepto al sketch anterior: inicia el periférico I2C con pines SDA y SCL personalizados. Las cuatro llamadas pcf8574.pinMode configuran líneas específicas del PCF8574 como salidas: P0 para reset táctil, P2 para línea de interrupción táctil, P3 para habilitar alimentación LCD y P4 para reset LCD. Como antes, pcf8574.begin() inicia la comunicación con el expansor e imprime un mensaje de error si falla.

El siguiente bloque realiza la secuencia de alimentación y reset para el LCD. Estos retardos precisos y patrones de conmutación suelen ser requeridos por controladores LCD y están codificados según la hoja de datos de la pantalla.

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

Poner P3 en alto habilita la línea de alimentación del LCD. Tras un breve retardo para estabilizar la fuente, P4 se conmuta para generar un pulso de reset adecuado para el panel. La secuencia alto, bajo, alto con retardos específicos asegura que el controlador ST7701 arranque en un estado conocido.

La sección del panel táctil realiza una secuencia de reset muy similar, pero en las líneas de reset e interrupción del controlador táctil.

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

Aquí P0 se conmuta para resetear el controlador CST8xx, y P2 se pone en alto para configurar la línea de interrupción en un estado definido. Estas acciones aseguran que el controlador táctil esté listo antes de que el driver intente comunicarse vía I2C.

Finalmente, se inicializa el controlador táctil usando el driver Adafruit.

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

tsPanel.begin(&Wire, I2C_TOUCH_ADDR) indica al objeto CST8xx que use la instancia global Wire y lo conecta a la dirección I2C definida. Si esta llamada falla, el sketch imprime un mensaje diagnóstico.

Inicialización del LCD

La función initLCD prepara el contexto gráfico y configura un estado básico de dibujo.

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

gfx->begin(); inicializa el panel ST7701 sobre el bus RGB y ejecuta las operaciones de inicialización suministradas. Tras esta llamada, la pantalla está lista para aceptar comandos de dibujo.

gfx->fillScreen(RED); limpia toda la pantalla con un fondo rojo. gfx->setTextSize(5); establece un factor de escala de texto relativamente grande para que el texto sea fácilmente legible en la pantalla redonda de 480×480. gfx->setTextColor(WHITE); define el color de texto en primer plano para las operaciones de dibujo de texto posteriores.

drawOnLCD

La función drawOnLCD encapsula una acción simple de dibujo, colocando una etiqueta de texto en la región central de la pantalla.

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

gfx->setCursor(90, 210); mueve el cursor de texto a la posición (90, 210) en coordenadas de píxeles. Con una pantalla de 480×480, esto es aproximadamente el centro dependiendo del tamaño de fuente. gfx->print("Makerguides"); luego renderiza la cadena de texto con el tamaño y color configurados previamente en la pantalla.

Setup

La función setup nuevamente proporciona una secuencia de inicialización, similar al sketch anterior que configuró serial, pines y encoder.

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

Serial.begin(115200); inicia la comunicación serial para depuración. initPins(); configura el bus I2C, pines PCF8574 y realiza las secuencias de reset del LCD y táctil como se describió arriba. initBacklight(); habilita y ajusta el brillo de la retroiluminación para que el contenido en la pantalla sea visible. initLCD(); inicializa el driver gráfico y pinta el fondo rojo, y drawOnLCD(); coloca la cadena inicial “Makerguides” en la pantalla.

Loop

El loop principal verifica constantemente si el panel táctil está siendo tocado. Cuando se detecta un toque, lee las coordenadas y dibuja un pequeño círculo negro en esa posición.

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(); consulta al driver CST8xx para ver si hay puntos táctiles activos. Si la función devuelve true, al menos un dedo está sobre la pantalla. tsPanel.getPoint(0); obtiene el primer punto táctil como una estructura CST_TS_Point que contiene las coordenadas x y y. Estas coordenadas se imprimen en el monitor serial para depuración con Serial.printf, similar a cómo imprimiste el contador del encoder y el estado del botón anteriormente.

gfx->fillCircle(p.x, p.y, 10, BLACK); dibuja un círculo relleno con radio de 10 píxeles en negro en la ubicación del toque. La llamada delay(10); proporciona una breve pausa para limitar la tasa de actualización y evitar saturar el bus I2C y el driver gráfico con demasiadas operaciones por segundo.

Ejemplo de código: Pantalla, táctil y encoder

Este último sketch reúne los conceptos de los ejemplos anteriores: configuración I2C y control PCF8574, pantalla RGB y manejo táctil, y un codificador rotatorio basado en interrupciones.

El código te permite controlar el brillo de la pantalla girando el anillo del encoder y también mostrar el valor actual de brillo (0…255) en el centro de la pantalla. El botón del encoder cambia el color de la pantalla a azul y los eventos táctiles siguen registrándose como puntos negros en la pantalla.

Echa un vistazo rápido al código completo a continuación y luego discutiremos sus detalles.

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

Importaciones

Este sketch combina todo de los ejemplos anteriores: periféricos I2C, pantalla RGB, táctil y un codificador rotatorio basado en interrupciones, e incluye las librerías necesarias:

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

Constantes y estado global

La siguiente sección define los pines para I2C, retroiluminación, señales del encoder y la dirección del controlador táctil. También introduce varias variables globales volatile que mantienen el brillo actual y el estado del encoder, similar al ejemplo previo del contador del 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 y I2C_SCL_PIN configuran el bus I2C compartido, como antes. BKL_PIN es el pin PWM usado para controlar la retroiluminación LCD. ENCODER_CLK y ENCODER_DT son las señales en cuadratura del codificador rotatorio, idénticas en función al sketch anterior del encoder. I2C_TOUCH_ADDR mantiene la dirección del controlador táctil CST8xx.

brightness almacena el valor actual del brillo de la retroiluminación. Se declara volatile porque se modifica dentro de una rutina de servicio de interrupción. encState y oldState se usan para detectar transiciones en la línea CLK del encoder, y brightnessChanged es una bandera que informa al loop principal que hay un nuevo nivel de brillo disponible.

Objetos PCF8574, táctil y pantalla

Los objetos globales para el expansor de E/S, el panel táctil y el bus de pantalla son los mismos que en el sketch anterior de pantalla y táctil. Definen cómo el ESP32 interactúa con los chips externos y el panel 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 */
);

El objeto pcf8574 está ligado a la dirección 0x21 y es responsable de la alimentación LCD, reset y el botón pulsador del encoder. El objeto tsPanel envuelve el controlador táctil CST8xx. El puntero bus define el mapeo de señales RGB y de temporización a los GPIO del ESP32 exactamente como antes, usando tu tabla de pines R0–R4, G0–G5 y B0–B4.

El objeto gráfico de alto nivel para el panel RGB ST7701 se crea encima de este 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
);

Como antes, esto encapsula la secuencia de inicialización de bajo nivel ST7701, parámetros de temporización y formato de color. El pin de reset se marca como indefinido porque el reset se maneja vía los pines PCF8574 durante initPins.

Inicialización de retroiluminación

La función de inicialización de retroiluminación toma el valor actual brightness y lo aplica al pin PWM de retroiluminación:

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

pinMode(BKL_PIN, OUTPUT); configura el pin de retroiluminación como salida digital. analogWrite(BKL_PIN, brightness); inicia la salida PWM en este pin usando el valor brightness como ciclo de trabajo.

Inicialización de pines y periféricos

La función initPins es una versión extendida de la que viste previamente. Configura I2C, los pines PCF8574, la secuencia de alimentación y reset del LCD, la secuencia de reset táctil y también configura el pin PCF8574 que lee el botón pulsador del 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); inicia el bus I2C con los pines especificados, como en los sketches anteriores. P0, P2, P3 y P4 se configuran como salidas para controlar reset táctil, línea de interrupción táctil, alimentación LCD y reset LCD. P5 se configura como INPUT_PULLUP porque está conectado al interruptor del encoder. Esto refleja cómo configuraste P5 como entrada del botón del encoder en el ejemplo original solo con encoder.

La secuencia de temporización de alimentación y reset del LCD sigue, idéntica en estructura al código de pantalla anterior.

  // 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 habilita la línea de alimentación del LCD, luego P4 se conmuta con retardos específicos para realizar un reset hardware del controlador ST7701.

La secuencia de reset y configuración del panel táctil también es igual que antes.

  // 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 se pulsa para resetear el controlador CST8xx y P2 se pone en alto para establecer un estado definido para la línea de interrupción táctil.

Luego se inicializa el controlador táctil. tsPanel.begin(&Wire, I2C_TOUCH_ADDR) vincula el driver CST8xx al bus I2C compartido y a la dirección especificada, e imprime un mensaje diagnóstico si no se encuentra el dispositivo:

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

Rutina de servicio de interrupción del encoder

El manejador de interrupción del encoder es similar a la función encoder_irq anterior, pero en lugar de mantener un contador de posición, actualiza la variable brightness en pasos de cinco.

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 asegura que la ISR se coloque en la RAM de instrucciones para ejecución rápida en ESP32, como se discutió antes. Dentro de la función, encState se establece al nivel lógico actual del pin CLK del encoder usando digitalRead(ENCODER_CLK). La condición if (encState != oldState) asegura que el código solo reaccione cuando la señal CLK realmente cambia, evitando múltiples actualizaciones en el mismo nivel.

La dirección de rotación se determina nuevamente comparando la señal DT con el estado actual de CLK. En este sketch, la condición está invertida respecto al ejemplo anterior para dar una sensación natural de aumento y disminución del brillo.

La siguiente línea resta 5 o suma 5 a la variable brightness según la fase relativa de las señales en cuadratura. La rotación positiva aumenta el brillo, la negativa lo disminuye.

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

Y la siguiente línea asegura que el brillo se mantenga dentro de un límite inferior definido de 5 (evitando pantalla completamente apagada) y un límite superior de 255 (valor PWM máximo para brillo total):

brightness = constrain(brightness, 5, 255);

oldState se actualiza al nuevo estado de CLK, y brightnessChanged se establece en true para notificar al loop principal que la retroiluminación y la pantalla deben actualizarse. Como antes, todo el trabajo pesado como I/O serial y gráfico se mantiene fuera de la ISR y se maneja en el loop principal.

Inicialización del encoder

La función initEncoder configura los pines del encoder y adjunta la interrupción a la línea CLK. Esto es efectivamente el mismo patrón que en tu sketch original del encoder.

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

Ambos canales del encoder se configuran como entradas con resistencias pull-up internas. La interrupción se adjunta al pin CLK usando attachInterrupt, con modo CHANGE para activarse en ambos flancos, ascendente y descendente. En cada cambio, se invoca el manejador encoder_irq, actualizando el brillo.

Inicialización del LCD y visualización del brillo

La función de inicialización del LCD levanta el panel ST7701 y configura ajustes de texto. Es similar a la función initLCD anterior, pero usa un tamaño de texto mayor para mostrar el valor de brillo de forma destacada:

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

gfx->begin(); inicializa el controlador de pantalla y envía la secuencia de configuración. gfx->fillScreen(RED); establece un fondo rojo. gfx->setTextSize(10); elige un factor de escala grande para que el valor numérico del brillo resalte claramente. gfx->setTextColor(WHITE); configura el color blanco como color de primer plano para el texto.

La función updateBrightness vincula el valor lógico de brillo tanto a la retroiluminación física como a la visualización en pantalla.

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

analogWrite(BKL_PIN, brightness); actualiza el ciclo de trabajo PWM, cambiando realmente la intensidad de la retroiluminación LED. Luego la pantalla se limpia de nuevo a rojo usando gfx->fillScreen(RED);.

El cursor se coloca en las coordenadas (150, 200), y gfx->printf("%3d", brightness); imprime el brillo como un número de tres dígitos.

Setup

La función setup inicializa todos los subsistemas: serial, pines I2C y chips externos, el encoder, la retroiluminación y el LCD, y finalmente dibuja el brillo inicial en la pantalla.

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

Serial.begin(115200); inicia el UART para salida de depuración. initPins(); prepara el bus I2C, PCF8574, LCD y controlador táctil como se discutió antes. initEncoder(); habilita la interfaz del encoder basada en interrupciones. initBacklight(); aplica el valor inicial brightness al pin de retroiluminación. initLCD(); levanta el contexto gráfico, y updateBrightness(); sincroniza inmediatamente la visualización en pantalla y el PWM de retroiluminación con el valor actual de brillo.

Loop

El loop principal verifica periódicamente el botón del encoder, la entrada táctil y la bandera de actualización de brillo. Reacciona a cada uno de estos eventos usando los periféricos configurados anteriormente.

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

El primer bloque lee el botón pulsador del encoder vía PCF8574. pcf8574.digitalRead(P5, true); lee el pin P5 con una transacción I2C inmediata. Como P5 está configurado como INPUT_PULLUP, el botón lee alto cuando está liberado y bajo cuando está presionado. La condición if (!button) detecta el estado presionado. Al presionar, el sketch imprime un mensaje en el monitor serial y llena la pantalla de azul, proporcionando una indicación visual simple de que el botón fue presionado.

El siguiente bloque maneja la entrada táctil capacitiva, reutilizando la misma lógica del ejemplo anterior de dibujo en el LCD. Si tsPanel.touched() devuelve true, al menos un punto táctil está activo. La función tsPanel.getPoint(0); obtiene el primer punto táctil, que luego se imprime en el monitor serial. gfx->fillCircle(p.x, p.y, 10, BLACK); dibuja un pequeño círculo negro en las coordenadas táctiles, permitiendo al usuario dibujar sobre el contenido actual de la pantalla.

El tercer bloque verifica si la ISR del encoder ha actualizado la variable brightness. Si brightnessChanged es true, el loop principal limpia la bandera y llama a updateBrightness();. Esto aplica el nuevo brillo a la retroiluminación y redibuja el valor numérico en la pantalla. Girar el encoder rápidamente generará múltiples interrupciones y establecerá brightnessChanged repetidamente, pero el loop asegura que las actualizaciones de brillo se manejen en el contexto principal donde es seguro hacer operaciones seriales y gráficas.

El último delay(100); introduce una breve pausa para limitar la frecuencia del loop y suavizar la interacción del usuario sin sobrecargar la CPU o el bus I2C.

Conclusiones

Este tutorial te proporcionó ejemplos de código para comenzar con el CrowPanel 2.1inch-HMI ESP32 Rotary Display. Consulta la Wiki de Elecrow para información adicional y para más ejemplos de código, visita la github repo.

Ten en cuenta que necesitas instalar librerías antiguas y una versión antigua del core ESP32 (2.0.14) para que el código funcione. Además, algunos ejemplos de código allí usan la librería LVGL, que aquí evité para mantener el código simple.

Si buscas un módulo de pantalla similar con anillo codificador rotatorio, echa un vistazo al CrowPanel 1.28inch-HMI ESP32 Rotary Display o al Matouch 1.28″ ToolSet_Controller. Si solo necesitas una pantalla redonda (sin el anillo codificador rotatorio), el tutorial Digital Clock on CrowPanel 1.28″ Round Display podría ser útil.

Finalmente, si quieres aprender más sobre codificadores rotatorios, consulta nuestro tutorial How To Interface A Quadrature Rotary Encoder.

Si tienes alguna pregunta, no dudes en dejarlas en la sección de comentarios.

¡Feliz bricolaje! 😉