El CrowPanel 1.28-inch HMI ESP32 Rotary Display de Elecrow es un módulo compacto y redondo que integra un microcontrolador ESP32-S3, una pantalla táctil circular IPS de 1,28 pulgadas con resolución 240×240, un anillo de LED RGB y un codificador rotatorio con botón pulsador en una sola unidad.
En este tutorial, repasaremos los pasos esenciales para comenzar con el CrowPanel; desde la configuración inicial hasta probar sus capacidades de pantalla y entrada. Aprenderás a controlar los LEDs RGB, leer datos del codificador rotatorio para una entrada de usuario fluida y capturar eventos táctiles de la pantalla circular usando ejemplos de código sencillos.
Piezas necesarias
Solo necesitarás una placa CrowPanel 1.28-inch HMI ESP32 Rotary Display. El módulo de pantalla viene con un cable USB y un conector GPIO, por lo que no se requiere hardware adicional.

CrowPanel 1.28inch-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 1.28inch-HMI ESP32 Rotary Display
El CrowPanel 1.28-inch HMI ESP32 Rotary Display es un módulo de hardware integrado que combina una pantalla IPS redonda de alta resolución, una pantalla táctil capacitiva, un anillo de LEDs RGB y un codificador rotatorio con función de botón pulsador, todo controlado por un microcontrolador ESP32-S3.
El módulo está diseñado para servir como una interfaz hombre-máquina (HMI) compacta para proyectos embebidos e IoT donde se requiere retroalimentación visual y control interactivo en un espacio reducido. La imagen a continuación muestra el frente y el lateral del módulo con sus dimensiones:

Microcontrolador
En su núcleo, el CrowPanel utiliza el ESP32-S3R8, un procesador Xtensa LX7 de doble núcleo que funciona hasta 240 MHz. El microcontrolador integra Wi-Fi (802.11 b/g/n) y Bluetooth 5.0 LE, ofreciendo opciones de conectividad local y remota. Incluye 8 MB de PSRAM y 16 MB de memoria flash.
Pantalla
La pantalla es un panel IPS circular de 1,28 pulgadas con una resolución de 240 × 240 píxeles y soporte completo para 65 K colores. La pantalla usa comunicación SPI para la transferencia de datos y ofrece un amplio ángulo de visión de hasta 178 grados. Un controlador táctil capacitivo está integrado en el mismo módulo, conectado vía interfaz I²C, permitiendo detección precisa de múltiples puntos táctiles y soporte para gestos.
Para la entrada física, el módulo incluye un codificador rotatorio con botón pulsador incorporado. El codificador proporciona retroalimentación incremental de posición, ideal para navegación de menús, ajuste de parámetros o controles de selección rotatoria.
Tira de LED RGB
El módulo también incluye una tira de LED RGB WS2812 integrada, compuesta por cinco LEDs direccionables individualmente. Los LEDs están conectados en serie y pueden ser controlados usando librerías comunes como Adafruit NeoPixel o FastLED.
Cada LED WS2812 funciona con una alimentación de 5 V y típicamente consume hasta 60 mA a brillo máximo cuando todos los canales de color (rojo, verde y azul) están activos a máxima intensidad. Por lo tanto, cuando los cinco LEDs están encendidos al máximo brillo, el consumo total puede alcanzar aproximadamente 300 mA (5 × 60 mA).
Interfaces
La placa se alimenta a través de un conector USB-C de 5 V e incluye un regulador de voltaje a 3.3 V a bordo para una operación estable del ESP32 y componentes periféricos. Interfaces adicionales como UART, I²C, SPI, FPC y pines GPIO están disponibles para expansión externa, permitiendo conectar sensores, actuadores u otros dispositivos de control.
El módulo también incluye un botón de reset, un botón de arranque para flasheo de firmware y un LED de estado para retroalimentación visual durante la operación. La imagen a continuación muestra la parte trasera del módulo con los botones y diversas interfaces:

Pines
La siguiente tabla muestra qué pines GPIO del ESP32-S3 están conectados a qué componentes del módulo. Necesitarás esta información si quieres programar el módulo.
| Componente | Interfaz / Función | Asignación de Pines |
|---|---|---|
| Pantalla (GC9A01) | SPI | SCLK = 10, MOSI = 11, MISO = -1, DC = 3, CS = 9, RST = 14 |
| Retroiluminación de pantalla | GPIO | SCREEN_BACKLIGHT_PIN = 46 |
| Pantalla táctil (CST816D) | I²C | SDA = 6, SCL = 7, INT = 5, RST = 13 |
| OLED (SSD1306) | I²C | SDA = 38, SCL = 39 |
| LED RGB (WS2812) | Salida digital | LED_PIN = 48, LED_NUM = 5 |
| Codificador rotatorio | GPIO | ENCODER_A_PIN = 45, ENCODER_B_PIN = 42, SWITCH_PIN = 41 |
| Indicador de alimentación | GPIO | POWER_LIGHT_PIN = 40 |
| Prueba de E/S | GPIO | IO4, IO12 |
Especificaciones técnicas
Finalmente, las especificaciones técnicas del módulo de pantalla CrowPanel se resumen en la tabla siguiente.
| Especificación | Detalles |
|---|---|
| Microcontrolador | ESP32-S3R8 dual-core Xtensa LX7 @ 240 MHz |
| Memoria | 8 MB PSRAM, 16 MB Flash |
| Tipo de pantalla | IPS TFT redonda de 1.28 pulgadas |
| Resolución | 240 × 240 píxeles |
| Profundidad de color | 65 K colores |
| Ángulo de visión | 178 grados |
| Interfaz táctil | Capacitiva, comunicación I²C |
| Entrada rotatoria | Codificador incremental con botón pulsador |
| Interfaces de comunicación | SPI, I²C, UART, GPIO |
| Conectividad | Wi-Fi 802.11 b/g/n, Bluetooth 5.0 LE |
| Alimentación | 5 V vía USB-C (regulador 3.3 V a bordo) |
| Interfaz de programación | USB-C (compatible con Arduino, ESP-IDF) |
| Características adicionales | Botones Reset y Boot, tira de LED RGB |
Instalación del Core ESP32
Si es tu primer proyecto con una placa de la serie ESP32, necesitarás instalar primero el core ESP32. Si ya tienes instaladas las placas ESP32 en tu Arduino IDE, puedes saltarte esta sección.
Comienza abriendo el diálogo de Preferencias 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“:

En este campo 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 core del 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 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». Haz clic en el botón INSTALL y espera a que la descarga e instalación finalicen.

Selección de placa
Finalmente, necesitamos seleccionar una placa ESP32. En el caso del CrowPanel 1.28inch-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…»:

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 haz clic en OK:

Ten en cuenta que debes conectar la placa mediante el cable USB a tu ordenador antes de poder seleccionar un puerto COM. El módulo CrowPanel Display tiene dos puertos, uno USB y otro UART. Puedes usar ambos para programar la placa, pero para el puerto UART necesitarás un convertidor USB a TTL, como se muestra a continuación:

La forma más sencilla es conectar el cable USB que viene con el CrowPanel Display Module directamente al puerto etiquetado como «USB» como se muestra abajo. En este caso no necesitas un convertidor USB a TTL.

Configuración de herramientas
A continuación, las configuraciones que debes usar con la placa. Las encontrarás en el menú Tools de tu Arduino IDE.

Lo más importante para los ejemplos de código en las siguientes secciones es activar USB CDC on Boot en «Enabled» y también configurar USB mode en «Hardware CDC and JTAG». Esto te permite enviar o recibir datos vía puerto serial, necesario para depuración.
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 CrowPanel Display Module.
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 Baud (115200) sean correctas.
Ejemplo de código: tira de LED RGB
A continuación probamos el LED RGB integrado. Sin embargo, primero necesitarás instalar la Adafruit_NeoPixel librería. Abre el LIBRARY MANAGER en tu Arduino IDE, escribe «Adafruit NeoPixel» en la barra de búsqueda e instala la librería Adafruit NeoPixel de Adafruit:

El CrowPanel Display Module tiene una tira de LED RGB WS2812 integrada con cinco LEDs. El siguiente ejemplo muestra cómo inicializar la tira, establecer un color y aumentar gradualmente su brillo usando la librería Adafruit NeoPixel.
#include <Adafruit_NeoPixel.h>
#define LED_PIN 48
#define LED_NUM 5
Adafruit_NeoPixel strip = Adafruit_NeoPixel(LED_NUM, LED_PIN, NEO_GRB + NEO_KHZ800);
void updateBrightness(int brightness) {
strip.setBrightness(brightness);
strip.show();
delay(100);
}
void initLEDs(uint8_t r, uint8_t g, uint8_t b) {
strip.begin();
strip.setBrightness(100);
for (int i = 0; i < LED_NUM; i++) {
strip.setPixelColor(i, strip.Color(r, g, b));
}
strip.show();
}
void setup() {
Serial.begin(115200);
initLEDs(255, 255, 50); // Yellow
}
void loop() {
for(int brightness=5; brightness<255; brightness+=5) {
updateBrightness(brightness);
Serial.printf("Brightness: %d\n", brightness);
}
}
Importaciones
El programa comienza incluyendo la librería Adafruit_NeoPixel, que proporciona una interfaz sencilla para controlar LEDs WS2812. Esta librería maneja el estricto temporizado requerido por el protocolo de comunicación de un solo cable de los LEDs, permitiéndote centrarte en la lógica de color y animación en lugar de detalles de temporización de bajo nivel.
#include <Adafruit_NeoPixel.h>
Constantes
Se definen dos constantes para especificar cuántos LEDs están conectados y qué pin GPIO se usa para controlarlos. El CrowPanel conecta la tira WS2812 al GPIO 48, y la tira contiene cinco LEDs.
#define LED_PIN 48 #define LED_NUM 5
Creación del objeto
Luego, se crea un objeto tira NeoPixel. El constructor toma tres parámetros: número de LEDs, pin de control y configuración del tipo de píxel. La bandera NEO_GRB + NEO_KHZ800 indica que cada LED usa el orden de color GRB (verde, rojo, azul) y opera a 800 kHz, que es la frecuencia estándar para LEDs WS2812.
Adafruit_NeoPixel strip = Adafruit_NeoPixel(LED_NUM, LED_PIN, NEO_GRB + NEO_KHZ800);
Este objeto se usará durante todo el programa para controlar los LEDs — establecer colores, ajustar brillo y enviar actualizaciones de datos.
La updateBrightness() función
Esta función auxiliar ajusta dinámicamente el brillo de la tira de LEDs. El método setBrightness() acepta un valor entre 0 (apagado) y 255 (brillo máximo). Después de establecer el brillo, se debe llamar a la función show() para enviar los datos actualizados a los LEDs.
void updateBrightness(int brightness) {
strip.setBrightness(brightness);
strip.show();
delay(100);
}
La breve demora asegura una transición suave y visible entre niveles de brillo, creando un efecto de fundido gradual.
La initLEDs() función
Antes de poder controlar los LEDs, deben ser inicializados. La función begin() prepara el pin de datos para la comunicación, y setBrightness(100) establece un nivel inicial de brillo para evitar comenzar a máxima intensidad.
El bucle for luego itera sobre cada uno de los cinco LEDs, asignándoles un color usando setPixelColor(). El método strip.Color(r, g, b) convierte los valores individuales de rojo, verde y azul en el formato de color de 24 bits requerido por los LEDs.
void initLEDs(uint8_t r, uint8_t g, uint8_t b) {
strip.begin();
strip.setBrightness(100);
for (int i = 0; i < LED_NUM; i++) {
strip.setPixelColor(i, strip.Color(r, g, b));
}
strip.show();
}
Finalmente, show() transmite los datos de color a todos los LEDs, iluminándolos simultáneamente en el color seleccionado. En nuestro caso será amarillo.
Setup
La función setup() se ejecuta una vez cuando el ESP32 se enciende o reinicia. Comienza inicializando la comunicación serial a 115200 baudios, permitiendo imprimir mensajes en el monitor serial. Luego llama a initLEDs(255, 255, 50), que enciende los cinco LEDs con un tono amarillo suave.
void setup() {
Serial.begin(115200);
initLEDs(255, 255, 50); // Yellow
}
Loop
La función loop() se ejecuta continuamente tras completar setup. En este ejemplo, aumenta gradualmente el brillo de 5 a 255 en pasos de 5, llamando a updateBrightness() en cada paso. Después de cada ajuste, imprime el valor actual de brillo en el monitor serial.
void loop() {
for(int brightness=5; brightness<255; brightness+=5) {
updateBrightness(brightness);
Serial.printf("Brightness: %d\n", brightness);
}
}
Esto crea una animación de fundido suave que se repite continuamente. Asegúrate de no poner el brillo a cero. La tira de LEDs permanecerá apagada, incluso si luego cambias el brillo. Tendrás que volver a establecer los colores de la tira.
Ejemplo de código: Codificador rotatorio
Este sketch se basa en el ejemplo NeoPixel anterior y añade control de brillo en tiempo real usando un codificador rotatorio mecánico. Los LEDs siguen usando la librería Adafruit NeoPixel para el temporizado y manejo de color, mientras que el codificador se lee mediante una rutina de servicio de interrupción para una interacción sensible y sin vibraciones. El resultado es una perilla de brillo suave y controlada por hardware para la tira WS2812 de cinco LEDs del CrowPanel.
#include <Adafruit_NeoPixel.h>
#define LED_PIN 48
#define LED_NUM 5
#define ENCODER_CLK 45 // A
#define ENCODER_DT 42 // B
Adafruit_NeoPixel strip = Adafruit_NeoPixel(LED_NUM, LED_PIN, NEO_GRB + NEO_KHZ800);
int brightness = 50; // 0..255
int enc_state = 0;
int old_state = -1;
bool has_changed = true;
void IRAM_ATTR encoder_irq() {
enc_state = digitalRead(ENCODER_CLK);
if (enc_state != old_state) {
brightness += (digitalRead(ENCODER_DT) == enc_state) ? -5 : +5;
brightness = constrain(brightness, 5, 255);
old_state = enc_state;
has_changed = true;
}
}
void updateBrightness() {
strip.setBrightness(brightness);
strip.show();
delay(200);
}
void initLEDs(uint8_t r, uint8_t g, uint8_t b) {
strip.begin();
for (int i = 0; i < LED_NUM; i++) {
strip.setPixelColor(i, strip.Color(r, g, b));
}
updateBrightness();
}
void initEncoder() {
pinMode(ENCODER_CLK, INPUT_PULLUP);
pinMode(ENCODER_DT, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(ENCODER_CLK), encoder_irq, CHANGE);
}
void setup() {
Serial.begin(115200);
initEncoder();
initLEDs(255, 255, 50);
has_changed = true;
}
void loop() {
if (has_changed) {
has_changed = false;
updateBrightness();
Serial.printf("Brightness: %d\n", brightness);
}
delay(1);
}
Importaciones
El programa reutiliza la misma librería NeoPixel usada antes, por lo que la señalización crítica para los LEDs WS2812 está completamente gestionada y puedes centrarte en la lógica de entrada y animación.
#include <Adafruit_NeoPixel.h>
Constantes
La tira de LEDs permanece conectada al GPIO 48 y contiene cinco píxeles como antes. Dos constantes adicionales definen los canales A y B del codificador rotatorio en GPIO 45 y GPIO 42.
#define LED_PIN 48 #define LED_NUM 5 #define ENCODER_CLK 45 // A #define ENCODER_DT 42 // B
Objetos
El objeto NeoPixel se configura con los mismos parámetros explicados en el ejemplo anterior. El orden de color GRB y la señal a 800 kHz coinciden con los requisitos WS2812.
Adafruit_NeoPixel strip = Adafruit_NeoPixel(LED_NUM, LED_PIN, NEO_GRB + NEO_KHZ800);
Variables de estado
El sketch mantiene un valor global de brillo en el rango 0 a 255 y rastrea los estados instantáneo y previo del codificador. Un flag booleano señala al bucle principal que se debe actualizar el brillo, lo que provoca el ajuste en el bucle principal.
int brightness = 50; // 0..255 int enc_state = 0; int old_state = -1; bool has_changed = true;
La variable brightness se inicializa intencionadamente a un nivel moderado para que los LEDs no se enciendan a máxima carga. Los estados del codificador se inicializan para detectar correctamente el primer flanco, y has_changed comienza en true para forzar un renderizado inicial.
Rutina de servicio de interrupción: encoder_irq
El codificador se decodifica en un contexto de interrupción para mantener baja la latencia de la interfaz. La rutina se ejecuta en ambos flancos del canal CLK y compara el canal DT para determinar la dirección de rotación. Si DT es igual a CLK, el código interpreta el movimiento en una dirección y resta cinco pasos de brillo; si no es igual, suma cinco. El tamaño del paso de cinco es un compromiso práctico entre control granular y capacidad de respuesta.
void IRAM_ATTR encoder_irq() {
enc_state = digitalRead(ENCODER_CLK);
if (enc_state != old_state) {
brightness += (digitalRead(ENCODER_DT) == enc_state) ? -5 : +5;
brightness = constrain(brightness, 5, 255);
old_state = enc_state;
has_changed = true;
}
}
El atributo IRAM_ATTR asegura que la rutina se coloque en la RAM de instrucciones, evitando esperas en la flash del ESP32 durante interrupciones. La llamada constrain garantiza un mínimo de 5 para evitar salida completamente oscura y un máximo de 255 para limitar el brillo. El flag has_changed difiere la actualización real de los LEDs al hilo principal, manteniendo la ISR corta y determinista.
Actualización de brillo: updateBrightness
Las actualizaciones reales de los LEDs se centralizan aquí. La función aplica el brillo actual a la tira y llama a show para transmitir los datos de píxeles almacenados. Se usa una breve demora para hacer visibles los cambios sucesivos y evitar saturar la tasa de actualización de los LEDs.
void updateBrightness() {
strip.setBrightness(brightness);
strip.show();
delay(200);
}
Como el brillo en NeoPixel es un multiplicador global aplicado en el momento de la transmisión, este enfoque actualiza la intensidad sin reescribir los colores individuales de los píxeles.
Inicialización de LEDs: initLEDs
La lógica de inicialización refleja el ejemplo anterior pero difiere el brillo a updateBrightness para usar la misma ruta de código tanto al inicio como durante los giros de la perilla. Los cinco píxeles reciben el mismo color inicial, que puedes cambiar para retroalimentación de estado.
void initLEDs(uint8_t r, uint8_t g, uint8_t b) {
strip.begin();
for (int i = 0; i < LED_NUM; i++) {
strip.setPixelColor(i, strip.Color(r, g, b));
}
updateBrightness();
}
Llamar a updateBrightness al final garantiza que el color inicial aparezca al nivel de brillo global actual.
Inicialización del codificador: initEncoder
Los canales del codificador se configuran como entradas con pull-ups internos porque muchos codificadores de panel son de colector abierto o tipo interruptor mecánico. La interrupción se conecta a la línea CLK en ambos flancos para que cada muesca produzca una transición de estado que la ISR pueda decodificar.
void initEncoder() {
pinMode(ENCODER_CLK, INPUT_PULLUP);
pinMode(ENCODER_DT, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(ENCODER_CLK), encoder_irq, CHANGE);
}
Usar interrupciones en la línea CLK, en lugar de sondear en el bucle, elimina pasos perdidos a altas velocidades de rotación y reduce el uso de CPU. Si tu codificador es particularmente ruidoso, puedes añadir un simple debounce ignorando cambios dentro de una ventana de microsegundos o muestreando ambos canales nuevamente tras un breve retraso.
Setup
Se inicializa la salida serial para visibilidad, se arma primero el codificador para capturar la entrada del usuario inmediatamente, y se encienden los LEDs con un tono amarillo. El flag de cambio se activa para asegurar que el primer fotograma se envíe a la tira aunque el codificador no se haya movido aún.
void setup() {
Serial.begin(115200);
initEncoder();
initLEDs(255, 255, 50);
has_changed = true;
}
Loop
El bucle principal es deliberadamente mínimo. Comprueba si la ISR ha marcado un cambio de brillo y, si es así, aplica el nuevo nivel e informa por serial.
void loop() {
if (has_changed) {
has_changed = false;
updateBrightness();
Serial.printf("Brightness: %d\n", brightness);
}
delay(1);
}
Esta estructura mantiene el trabajo crítico y corto en la interrupción y deja la E/S más pesada de LEDs en primer plano, que es un patrón robusto para código UI sensible en el ESP32.
Ejemplo de código: Pantalla
En este siguiente ejemplo controlaremos el brillo de la pantalla usando el codificador rotatorio. La imagen a continuación muestra tres valores diferentes de brillo (10%, 50%, 100%) para la pantalla:

Primero, necesitarás instalar la Arduino_GFX librería de moononournation para poder controlar la pantalla. Abre el LIBRARY MANAGER, escribe «GFX Library for Arduino» en la barra de búsqueda, encuentra la «GFX Library for Arduino» de Moon y haz clic en el botón INSTALL:

El siguiente sketch conecta el codificador rotatorio a la pantalla TFT GC9A01 usando la librería Arduino_GFX y añade control PWM para la retroiluminación. Extiende los conceptos del codificador de los ejemplos anteriores y te permitirá controlar el brillo de la pantalla con un botón pulsador para alternar la retroiluminación.
#include <Arduino_GFX_Library.h>
#define TFT_SCLK 10
#define TFT_MOSI 11
#define TFT_DC 3
#define TFT_CS 9
#define TFT_RES 14
#define TFT_BLK 46
#define LCD_PWR_EN1 1 // LCD_3V3 rail
#define LCD_PWR_EN2 2 // LEDA_3V3 rail
#define ENCODER_CLK 45 // A
#define ENCODER_DT 42 // B
#define ENCODER_BTN 41 // SW (active-LOW)
int brightness = 50; // 0..100 %
bool btn_display = true;
int enc_state = 0;
int old_state = -1;
bool has_changed = true;
Arduino_ESP32SPI *bus = new Arduino_ESP32SPI(
TFT_DC, TFT_CS, TFT_SCLK, TFT_MOSI, GFX_NOT_DEFINED, FSPI, true
);
Arduino_GFX *gfx = new Arduino_GC9A01(bus, TFT_RES, 0 /* rotation */, true /* IPS */);
void IRAM_ATTR encoder_irq() {
enc_state = digitalRead(ENCODER_CLK);
if (enc_state != old_state) {
brightness += (digitalRead(ENCODER_DT) == enc_state) ? 1 : -1;
brightness = constrain(brightness, 0, 100);
old_state = enc_state;
has_changed = true;
}
}
void IRAM_ATTR button_irq() {
if (!digitalRead(ENCODER_BTN)) {
btn_display = !btn_display;
has_changed = true;
}
}
void drawText() {
gfx->setTextSize(2);
gfx->setCursor(60, 55);
gfx->println(F("Makerguides"));
}
void updateBrightness() {
gfx->fillRect(65, 95, 120, 65, WHITE);
gfx->setTextSize(6);
gfx->setCursor(65, 100);
gfx->printf("%3d", brightness);
int duty = map(brightness, 0, 100, 0, 255);
analogWrite(TFT_BLK, btn_display ? duty : 0);
}
void initPower() {
pinMode(LCD_PWR_EN1, OUTPUT);
pinMode(LCD_PWR_EN2, OUTPUT);
digitalWrite(LCD_PWR_EN1, HIGH); // must be HIGH
digitalWrite(LCD_PWR_EN2, HIGH); // must be HIGH
pinMode(TFT_BLK, OUTPUT);
analogWrite(TFT_BLK, 50);
}
void initDisplay() {
delay(20);
gfx->begin();
gfx->fillScreen(WHITE);
gfx->setTextColor(BLACK);
}
void initEncoder() {
pinMode(ENCODER_BTN, INPUT_PULLUP);
pinMode(ENCODER_CLK, INPUT_PULLUP);
pinMode(ENCODER_DT, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(ENCODER_CLK), encoder_irq, CHANGE);
attachInterrupt(digitalPinToInterrupt(ENCODER_BTN), button_irq, CHANGE);
}
void setup() {
Serial.begin(115200);
initPower();
initEncoder();
initDisplay();
drawText();
has_changed = true;
}
void loop() {
if (has_changed) {
has_changed = false;
updateBrightness();
Serial.printf("Brightness: %d%% (display %s)\n",
brightness, btn_display ? "ON" : "OFF");
}
delay(1);
}
Importaciones
El programa usa Arduino_GFX para primitivas rápidas de dibujo SPI y un driver para el panel GC9A01. La librería oculta comandos específicos del controlador y ofrece una API consistente para inicialización, rellenos de color y renderizado de texto.
#include <Arduino_GFX_Library.h>
Constantes
Los pines definen la conexión SPI a la TFT redonda, las líneas de habilitación de alimentación para el panel y su ánodo LED, el pin de retroiluminación controlado por PWM, y los canales y botón del codificador rotatorio. El brillo se expresa en porcentaje para que la retroalimentación al usuario pueda imprimirse directamente como 0–100%.
#define TFT_SCLK 10 #define TFT_MOSI 11 #define TFT_DC 3 #define TFT_CS 9 #define TFT_RES 14 #define TFT_BLK 46 #define LCD_PWR_EN1 1 // LCD_3V3 rail #define LCD_PWR_EN2 2 // LEDA_3V3 rail #define ENCODER_CLK 45 // A #define ENCODER_DT 42 // B #define ENCODER_BTN 41 // SW (active-LOW)
Variables de estado
El estado en tiempo de ejecución rastrea el porcentaje actual de brillo, si la retroiluminación está activada, el estado instantáneo del codificador y un flag de cambio que permite a la ISR programar trabajo para el hilo principal. Como antes, mantener las ISR cortas y diferir la E/S al primer plano evita problemas de temporización.
int brightness = 50; // 0..100 % bool btn_display = true; int enc_state = 0; int old_state = -1; bool has_changed = true;
Objetos de bus y driver de pantalla
La pila Arduino_GFX se divide en un objeto bus y un objeto panel. El bus envuelve el host FSPI del ESP32 con DC, CS, SCLK y MOSI. El objeto panel apunta al controlador GC9A01 y sabe cómo inicializar un panel IPS redondo. La rotación se establece en cero y el último booleano marca el panel como IPS para seleccionar la inversión de color y gamma correctas.
Arduino_ESP32SPI *bus = new Arduino_ESP32SPI( TFT_DC, TFT_CS, TFT_SCLK, TFT_MOSI, GFX_NOT_DEFINED, FSPI, true ); Arduino_GFX *gfx = new Arduino_GC9A01(bus, TFT_RES, 0 /* rotation */, true /* IPS */);
Rutina de servicio de interrupción del codificador
La ISR del codificador refleja la lógica anterior, pero el tamaño del paso es de un porcentaje por flanco para un control fino. La rutina muestrea el canal CLK para detectar un flanco, compara DT para determinar dirección, actualiza el porcentaje, lo limita y cambia el flag de cambio.
void IRAM_ATTR encoder_irq() {
enc_state = digitalRead(ENCODER_CLK);
if (enc_state != old_state) {
brightness += (digitalRead(ENCODER_DT) == enc_state) ? 1 : -1;
brightness = constrain(brightness, 0, 100);
old_state = enc_state;
has_changed = true;
}
}
Rutina de servicio de interrupción del botón
El botón pulsador del codificador es activo bajo. La ISR lee el pin y alterna un booleano que representa el estado de la retroiluminación. El dibujo y la actualización PWM se difieren al bucle principal activando el flag de cambio.
void IRAM_ATTR button_irq() {
if (!digitalRead(ENCODER_BTN)) {
btn_display = !btn_display;
has_changed = true;
}
}
Dibujo de texto estático
Un pequeño ayudante dibuja una etiqueta del sitio una vez al inicio. El tamaño del texto y el cursor se posicionan para una pantalla redonda 240×240. Nota que Arduino_GFX usa un cursor basado en píxeles que avanza con las impresiones.
void drawText() {
gfx->setTextSize(2);
gfx->setCursor(60, 55);
gfx->println(F("Makerguides"));
}
Actualización de brillo y retroiluminación
Esta función refresca el valor numérico en pantalla y aplica PWM al pin de retroiluminación. El área rectangular detrás de los dígitos se limpia a blanco para evitar imágenes fantasma al cambiar los números. El porcentaje se mapea a un ciclo de trabajo de 8 bits, que analogWrite se traduce en un PWM hardware vía LEDC del ESP32. Cuando el toggle de pantalla está apagado, el ciclo se fuerza a cero.
void updateBrightness() {
gfx->fillRect(65, 95, 120, 65, WHITE);
gfx->setTextSize(6);
gfx->setCursor(65, 100);
gfx->printf("%3d", brightness);
int duty = map(brightness, 0, 100, 0, 255);
analogWrite(TFT_BLK, btn_display ? duty : 0);
}
Inicialización de alimentación
Se habilitan las líneas de alimentación lógica y ánodo LED del panel antes de enviar comandos a la pantalla. Ambas líneas se ponen en alto. El pin de retroiluminación se configura para PWM y se establece en brillo bajo (50), haciendo el texto visible pero no demasiado brillante.
void initPower() {
pinMode(LCD_PWR_EN1, OUTPUT);
pinMode(LCD_PWR_EN2, OUTPUT);
digitalWrite(LCD_PWR_EN1, HIGH); // must be HIGH
digitalWrite(LCD_PWR_EN2, HIGH); // must be HIGH
pinMode(TFT_BLK, OUTPUT);
analogWrite(TFT_BLK, 50);
}
Inicialización de pantalla
Tras un breve retardo para estabilizar la alimentación, se inicia el driver GC9A01, se limpia la pantalla a blanco y se establece el color de texto a negro. Desde aquí el sketch puede dibujar primitivas y texto libremente.
void initDisplay() {
delay(20);
gfx->begin();
gfx->fillScreen(WHITE);
gfx->setTextColor(BLACK);
}
Inicialización del codificador
Los canales y botón del codificador usan pull-ups internos, que coinciden con codificadores de contacto abierto típicos. Las interrupciones se conectan al canal CLK para decodificación en cuadratura y al botón para alternar. Usar CHANGE captura ambos flancos de pulsación y liberación; la ISR verifica la condición activa baja.
void initEncoder() {
pinMode(ENCODER_BTN, INPUT_PULLUP);
pinMode(ENCODER_CLK, INPUT_PULLUP);
pinMode(ENCODER_DT, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(ENCODER_CLK), encoder_irq, CHANGE);
attachInterrupt(digitalPinToInterrupt(ENCODER_BTN), button_irq, CHANGE);
}
Setup
El sistema alimenta el panel, arma las interrupciones del codificador, inicializa la pantalla, dibuja el título estático y activa el flag de cambio para que el primer fotograma de brillo y actualización PWM ocurra inmediatamente. Se habilita el registro serial para visibilidad durante pruebas.
void setup() {
Serial.begin(115200);
initPower();
initEncoder();
initDisplay();
drawText();
has_changed = true;
}
Loop
El primer plano comprueba si las ISR han solicitado una actualización. Cuando está activado, redibuja el brillo numérico, ajusta el brillo de la retroiluminación e imprime una línea de estado mostrando el porcentaje y si la retroiluminación está activada.
void loop() {
if (has_changed) {
has_changed = false;
updateBrightness();
Serial.printf("Brightness: %d%% (display %s)\n",
brightness, btn_display ? "ON" : "OFF");
}
delay(1);
}
Ejemplo de código: Pantalla táctil
Este último ejemplo muestra cómo leer datos táctiles del controlador capacitivo CST816D integrado en el CrowPanel 1.28-inch HMI ESP32 Rotary Display. Dibuja puntos rojos donde el usuario toca la pantalla. Mira la foto de ejemplo a continuación:

El código siguiente reutiliza la misma tubería de pantalla Arduino_GFX introducida antes, añade la librería Wire para lecturas I²C de bajo nivel y decodifica registros crudos del controlador táctil CST816D en coordenadas de pantalla en tiempo real.
#include <Wire.h>
#include <Arduino_GFX_Library.h>
#define TFT_SCLK 10
#define TFT_MOSI 11
#define TFT_MISO -1
#define TFT_DC 3
#define TFT_CS 9
#define TFT_RES 14
#define TFT_BLK 46
#define LCD_PWR_EN1 1 // LCD_3V3 rail
#define LCD_PWR_EN2 2 // LEDA_3V3 rail
#define TOUCH_INT 5
#define TOUCH_SDA 6
#define TOUCH_SCL 7
#define TOUCH_RST 13
Arduino_ESP32SPI *bus = new Arduino_ESP32SPI(
TFT_DC, TFT_CS, TFT_SCLK, TFT_MOSI, GFX_NOT_DEFINED, FSPI, true
);
Arduino_GFX *gfx = new Arduino_GC9A01(bus, TFT_RES, 0 /* rotation */, true /* IPS */);
int i2cRead(uint16_t addr, uint8_t reg_addr, uint8_t *reg_data, uint32_t length) {
Wire.beginTransmission(addr);
Wire.write(reg_addr);
if (Wire.endTransmission(true)) return -1;
Wire.requestFrom((int)addr, (int)length, (int)true);
for (uint32_t i = 0; i < length && Wire.available(); i++) {
*reg_data++ = Wire.read();
}
return 0;
}
int readTouch(int *x, int *y) {
// CST816D @ 0x15, coordinates start at register 0x02
uint8_t data_raw[7] = {0};
if (i2cRead(0x15, 0x02, data_raw, 7) != 0) return 0;
// data_raw[1] bits 7..6 = event (0:down,1:up,2:contact), low nibble high bits of X
int event = data_raw[1] >> 6;
if (event == 2 || event == 0) { // treat contact or down as a valid touch
*x = (int)data_raw[2] + (int)(data_raw[1] & 0x0F) * 256;
*y = (int)data_raw[4] + (int)(data_raw[3] & 0x0F) * 256;
return 1;
}
return 0;
}
void initPins() {
// Power rails for the LCD/backlight
pinMode(LCD_PWR_EN1, OUTPUT);
pinMode(LCD_PWR_EN2, OUTPUT);
digitalWrite(LCD_PWR_EN1, HIGH);
digitalWrite(LCD_PWR_EN2, HIGH);
// Backlight
pinMode(TFT_BLK, OUTPUT);
analogWrite (TFT_BLK, 100);
// Touch
pinMode(TOUCH_INT, INPUT_PULLUP);
pinMode(TOUCH_RST, OUTPUT);
// Reset the CST816D
digitalWrite(TOUCH_RST, LOW);
delay(5);
digitalWrite(TOUCH_RST, HIGH);
delay(50);
Wire.begin(TOUCH_SDA, TOUCH_SCL);
}
void initDisplay() {
delay(20);
gfx->begin();
gfx->fillScreen(WHITE);
gfx->setTextColor(BLACK);
gfx->setTextSize(3);
gfx->setCursor(80, 105);
gfx->print(F("Touch"));
}
void setup(void) {
Serial.begin(115200);
initPins();
initDisplay();
}
void loop() {
static int x, y;
if (readTouch(&x, &y) == 1) {
gfx->fillCircle(x, y, 5, RED);
}
}
Importaciones
Se incluyen dos librerías al inicio del programa.Wire.h maneja la comunicación sobre el bus I²C, que se usa para hablar con el controlador táctil.Arduino_GFX_Library.h gestiona la comunicación SPI con la pantalla TFT redonda GC9A01, permitiendo dibujar texto y formas fácilmente.
#include <Wire.h> #include <Arduino_GFX_Library.h>
Definiciones de pines
Esta sección define todas las asignaciones de pines para la pantalla, controlador táctil y líneas de alimentación.
Los pines SPI conectan el ESP32 al controlador TFT GC9A01, mientras que los pines I²C (TOUCH_SDA y TOUCH_SCL) comunican con el chip táctil CST816D. Pines de habilitación separados controlan las líneas de 3.3 V que alimentan la lógica LCD (LCD_PWR_EN1) y la retroiluminación LED (LCD_PWR_EN2).
#define TFT_SCLK 10 #define TFT_MOSI 11 #define TFT_MISO -1 #define TFT_DC 3 #define TFT_CS 9 #define TFT_RES 14 #define TFT_BLK 46 #define LCD_PWR_EN1 1 // LCD_3V3 rail #define LCD_PWR_EN2 2 // LEDA_3V3 rail #define TOUCH_INT 5 #define TOUCH_SDA 6 #define TOUCH_SCL 7 #define TOUCH_RST 13
Objetos de bus y driver de pantalla
La librería Arduino_GFX separa el driver de pantalla en dos objetos. Arduino_ESP32SPI define la configuración del bus SPI para el periférico FSPI del ESP32, incluyendo pines de datos y control. Arduino_GC9A01 representa el controlador específico de pantalla y gestiona comandos como inicialización, rellenos de color y renderizado de texto.
Arduino_ESP32SPI *bus = new Arduino_ESP32SPI( TFT_DC, TFT_CS, TFT_SCLK, TFT_MOSI, GFX_NOT_DEFINED, FSPI, true ); Arduino_GFX *gfx = new Arduino_GC9A01(bus, TFT_RES, 0 /* rotation */, true /* IPS */);
Esta configuración coincide con la pantalla IPS redonda 240×240 del CrowPanel.
La i2cRead() función auxiliar
Esta función proporciona una forma genérica de leer uno o más registros de cualquier dispositivo I²C.
Inicia comunicación con la dirección del dispositivo especificado, escribe el registro a leer y luego solicita un número de bytes al dispositivo. Cada byte se almacena en el buffer pasado vía reg_data. Si algún paso falla, devuelve -1 para indicar error.
int i2cRead(uint16_t addr, uint8_t reg_addr, uint8_t *reg_data, uint32_t length) {
Wire.beginTransmission(addr);
Wire.write(reg_addr);
if (Wire.endTransmission(true)) return -1;
Wire.requestFrom((int)addr, (int)length, (int)true);
for (uint32_t i = 0; i < length && Wire.available(); i++) {
*reg_data++ = Wire.read();
}
return 0;
}
Esta abstracción mantiene la función readTouch() más limpia y centrada en interpretar datos táctiles en lugar de gestionar transacciones del bus.
Lectura de datos táctiles con readTouch()
El controlador táctil CST816D proporciona eventos táctiles y datos de coordenadas vía I²C en la dirección 0x15.
Esta función lee siete bytes comenzando desde el registro 0x02, que contienen información sobre el estado táctil actual y las coordenadas.
Si el código de evento indica toque o contacto (valores 0 o 2), la función decodifica las posiciones X e Y combinando los bits altos y bajos de los registros de coordenadas.
Las coordenadas válidas se devuelven mediante los punteros x y y, y la función retorna 1 para indicar un toque válido.
int readTouch(int *x, int *y) {
uint8_t data_raw[7] = {0};
if (i2cRead(0x15, 0x02, data_raw, 7) != 0) return 0;
int event = data_raw[1] >> 6;
if (event == 2 || event == 0) {
*x = (int)data_raw[2] + (int)(data_raw[1] & 0x0F) * 256;
*y = (int)data_raw[4] + (int)(data_raw[3] & 0x0F) * 256;
return 1;
}
return 0;
}
Este análisis de registros de bajo nivel sigue la hoja de datos CST816D, donde los bits superiores de cada byte de coordenada se almacenan en el nibble inferior del byte anterior.
Inicialización de alimentación y E/S con initPins()
Antes de que la pantalla o el controlador táctil puedan operar, los GPIO apropiados deben configurarse.
Esta función configura las líneas de alimentación LCD, PWM de retroiluminación y pines de interfaz táctil. Ambas líneas de habilitación de alimentación LCD se ponen en alto para suministrar 3.3 V a la lógica de pantalla y LEDs. El pin de retroiluminación (TFT_BLK) se configura para control PWM, inicializado con un ciclo de trabajo modesto de 100 para brillo visible.
La línea de interrupción táctil se configura como entrada con resistencia pull-up interna. El pin de reset del CST816D se pone bajo por unos milisegundos y luego se libera a alto para reiniciar correctamente el controlador táctil. Finalmente, el bus I²C se inicializa en los pines SDA y SCL especificados.
void initPins() {
pinMode(LCD_PWR_EN1, OUTPUT);
pinMode(LCD_PWR_EN2, OUTPUT);
digitalWrite(LCD_PWR_EN1, HIGH);
digitalWrite(LCD_PWR_EN2, HIGH);
pinMode(TFT_BLK, OUTPUT);
analogWrite(TFT_BLK, 100);
pinMode(TOUCH_INT, INPUT_PULLUP);
pinMode(TOUCH_RST, OUTPUT);
digitalWrite(TOUCH_RST, LOW);
delay(5);
digitalWrite(TOUCH_RST, HIGH);
delay(50);
Wire.begin(TOUCH_SDA, TOUCH_SCL);
}
Inicialización de pantalla
Tras un breve retardo para asegurar estabilidad de alimentación, se inicializa la pantalla GC9A01. La pantalla se limpia a blanco, el color de texto se pone en negro y se dibuja una etiqueta “Touch” en el centro para indicar que está lista.
void initDisplay() {
delay(20);
gfx->begin();
gfx->fillScreen(WHITE);
gfx->setTextColor(BLACK);
gfx->setTextSize(3);
gfx->setCursor(80, 105);
gfx->print(F("Touch"));
}
Esta configuración crea un lienzo claro para dibujar indicadores táctiles más adelante en el bucle principal.
Setup
La función setup() es sencilla. Inicializa la interfaz serial para depuración, luego llama a initPins() y initDisplay() para preparar el hardware.
void setup(void) {
Serial.begin(115200);
initPins();
initDisplay();
}
En esta etapa, la pantalla está activa y el controlador táctil listo para reportar coordenadas.
Loop
El bucle principal comprueba continuamente la entrada táctil usando la función readTouch(). Si se detecta un toque válido, dibuja un pequeño círculo rojo en las coordenadas reportadas usando la función fillCircle() de la librería Arduino_GFX. Cada toque crea un nuevo punto, permitiendo al usuario “dibujar” en la pantalla.
void loop() {
static int x, y;
if (readTouch(&x, &y) == 1) {
gfx->fillCircle(x, y, 5, RED);
}
}
Este bucle mínimo destaca lo sensible que es el CST816D cuando se accede vía I²C y demuestra lo fácil que es renderizar retroalimentación gráfica en la pantalla GC9A01.
Conclusiones
Este tutorial te ha proporcionado ejemplos de código para comenzar con el CrowPanel 1.28inch-HMI ESP32 Rotary Display.
Para más ejemplos, consulta Elecrows’s Github repo for the 1.28inch-HMI ESP32 Rotary Display y el Wiki Page. Ten en cuenta, sin embargo, que muchos ejemplos allí usan las librerías ui y lvgl, que he evitado aquí para mantener la complejidad baja.
Si buscas un módulo de pantalla similar con anillo codificador rotatorio, echa un vistazo al Matouch 1.28″ ToolSet_Controller. No tiene el anillo de LED RGB pero sí un reloj en tiempo real (RTC) y un motor de vibración para retroalimentación háptica. Y si solo necesitas una pantalla redonda (sin el anillo codificador), el tutorial Digital Clock on CrowPanel 1.28″ Round Display podría ser útil.
Además, si quieres aprender más sobre codificadores rotatorios, revisa nuestro tutorial How To Interface A Quadrature Rotary Encoder.
Si tienes alguna pregunta, no dudes en dejarla en la sección de comentarios.
¡Feliz bricolaje! 😉

