Skip to Content

Começando com o CrowPanel 1.28inch-HMI ESP32 Rotary Display

Começando com o CrowPanel 1.28inch-HMI ESP32 Rotary Display

O CrowPanel 1.28-inch HMI ESP32 Rotary Display da Elecrow é um módulo compacto e redondo que integra um microcontrolador ESP32-S3, um ecrã circular IPS tátil de 1,28 polegadas com resolução 240×240, um anel de LED RGB e um encoder rotativo com botão de pressão numa única unidade.

Neste tutorial, vamos percorrer os passos essenciais para começar a usar o CrowPanel; desde a configuração inicial até testar as suas capacidades de ecrã e entrada. Vai aprender a controlar os LEDs RGB, ler os dados do encoder rotativo para uma entrada de utilizador suave e capturar eventos táteis do ecrã circular usando exemplos de código simples.

Peças Necessárias

Só vai precisar da placa CrowPanel 1.28-inch HMI ESP32 Rotary Display. O módulo de ecrã vem com um cabo USB e conector GPIO, por isso não é necessário 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 do CrowPanel 1.28inch-HMI ESP32 Rotary Display

O CrowPanel 1.28-inch HMI ESP32 Rotary Display é um módulo de hardware integrado que combina um ecrã IPS redondo de alta resolução, um ecrã tátil capacitivo, um anel de LEDs RGB e um encoder rotativo com funcionalidade de botão de pressão, tudo controlado por um microcontrolador ESP32-S3.

O módulo foi concebido para servir como uma interface homem-máquina (HMI) compacta para projetos embutidos e IoT onde é necessário feedback visual e controlo interativo num espaço reduzido. A imagem abaixo mostra a frente e o lado do módulo com as suas dimensões:

Dimensões do CrowPanel 1.28inch-HMI ESP32 Rotary Display (source)

Microcontrolador

No seu núcleo, o CrowPanel usa o ESP32-S3R8, um processador dual-core Xtensa LX7 a funcionar até 240 MHz. O microcontrolador integra Wi-Fi (802.11 b/g/n) e Bluetooth 5.0 LE, oferecendo opções de conectividade local e remota. Inclui 8 MB de PSRAM e 16 MB de memória flash.

Ecrã

O ecrã é um painel IPS circular de 1,28 polegadas com resolução de 240 × 240 pixels e suporte total a 65 mil cores. O ecrã usa comunicação SPI para transferência de dados e oferece um amplo ângulo de visão de até 178 graus. Um controlador tátil capacitivo está integrado no mesmo módulo, ligado via interface I²C, permitindo deteção precisa de múltiplos pontos de toque e suporte a gestos.

Para entrada física, o módulo inclui um encoder rotativo com botão de pressão incorporado. O encoder fornece feedback incremental de posição adequado para navegação em menus, ajuste de parâmetros ou controlos de seleção rotativa.

Anel de LED RGB

O módulo inclui também uma tira de LED RGB WS2812 integrada, composta por cinco LEDs individualmente endereçáveis. Os LEDs estão ligados em série e podem ser controlados usando bibliotecas comuns como Adafruit NeoPixel ou FastLED.

Cada LED WS2812 funciona com alimentação de 5 V e normalmente consome até 60 mA na máxima luminosidade quando todos os canais de cor (vermelho, verde e azul) estão ativos na intensidade máxima. Portanto, quando os cinco LEDs estão acesos na máxima luminosidade, o consumo total pode atingir aproximadamente 300 mA (5 × 60 mA).

Interfaces

A placa é alimentada através de um conector USB-C de 5 V e inclui um regulador de tensão onboard de 3,3 V para operação estável do ESP32 e dos componentes periféricos. Interfaces adicionais como UART, I²C, SPI, FPC e headers GPIO estão disponíveis para expansão externa, permitindo aos desenvolvedores ligar sensores, atuadores ou outros dispositivos de controlo.

O módulo inclui também um botão de reset, um botão de boot para flash de firmware e um LED de estado para feedback visual durante a operação. A imagem abaixo mostra a parte traseira do módulo com os botões e várias interfaces:

Parte traseira do CrowPanel 1.28inch-HMI ESP32 Rotary Display (source)

Pinos

A tabela seguinte mostra quais os pinos GPIO do ESP32-S3 ligados a quais componentes do módulo. Vai precisar desta informação se quiser escrever código para o módulo.

ComponenteInterface / FunçãoAtribuições de Pinos
Ecrã (GC9A01)SPISCLK = 10, MOSI = 11, MISO = -1, DC = 3, CS = 9, RST = 14
Retroiluminação do EcrãGPIOSCREEN_BACKLIGHT_PIN = 46
Ecrã Tátil (CST816D)I²CSDA = 6, SCL = 7, INT = 5, RST = 13
OLED (SSD1306)I²CSDA = 38, SCL = 39
LED RGB (WS2812)Saída DigitalLED_PIN = 48, LED_NUM = 5
Encoder RotativoGPIOENCODER_A_PIN = 45, ENCODER_B_PIN = 42, SWITCH_PIN = 41
Indicador de EnergiaGPIOPOWER_LIGHT_PIN = 40
Teste I/OGPIOIO4, IO12

Especificação Técnica

Finalmente, a especificação técnica do módulo de ecrã CrowPanel está resumida na tabela abaixo.

EspecificaçãoDetalhes
MicrocontroladorESP32-S3R8 dual-core Xtensa LX7 @ 240 MHz
Memória8 MB PSRAM, 16 MB Flash
Tipo de EcrãIPS TFT redondo de 1,28 polegadas
Resolução240 × 240 pixels
Profundidade de Cor65 mil cores
Ângulo de Visão178 graus
Interface TátilCapacitivo, comunicação I²C
Entrada RotativaEncoder incremental com botão de pressão
Interfaces de ComunicaçãoSPI, I²C, UART, GPIO
ConectividadeWi-Fi 802.11 b/g/n, Bluetooth 5.0 LE
Alimentação5 V via USB-C (regulador 3.3 V onboard)
Interface de ProgramaçãoUSB-C (suporta Arduino, ESP-IDF)
Funcionalidades AdicionaisBotões Reset e Boot, Anel de LED RGB

Instalação do Core ESP32

Se este é o seu primeiro projeto com uma placa da série ESP32, precisará de instalar primeiro o core ESP32. Se as placas ESP32 já estiverem instaladas no seu Arduino IDE, pode saltar esta secção.

Comece por abrir o diálogo de Preferências selecionando “Preferences…” no menu “File”. Isto abrirá o diálogo de Preferências mostrado abaixo.

Na aba Settings encontrará uma caixa de edição na parte inferior do diálogo rotulada “Additional boards manager URLs“:

Additional boards manager URLs in Preferences
URLs adicionais do gestor de placas nas Preferências

Neste campo copie a seguinte URL:

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

Isto permite ao Arduino IDE saber onde encontrar as bibliotecas do core ESP32. A seguir vamos instalar as placas ESP32 usando o Gestor de Placas.

Abra o Gestor de Placas via “Tools -> Boards -> Board Manager”. O Gestor de Placas aparecerá na barra lateral esquerda. Digite “ESP32” no campo de pesquisa no topo e deverá ver dois tipos de placas ESP32; as “Arduino ESP32 Boards” e as placas “esp32 by Espressif”. Queremos as “esp32 libraries by Espressif”. Clique no botão INSTALL e aguarde até o download e instalação terminarem.

Install ESP32 Core libraries
Instalar bibliotecas Core ESP32

Seleção da Placa

Finalmente, precisamos de selecionar uma placa ESP32. No caso do CrowPanel 1.28inch-HMI ESP32 Rotary Display, escolhemos o genérico “ESP32S3 Dev Module”. Para isso, clique no menu suspenso e depois em “Select other board and port…”:

Drop-down Menu for Board Selection
Menu suspenso para seleção da placa

Isto abrirá um diálogo onde pode digitar “esp32s3 dev” na barra de pesquisa. Verá a placa “ESP32S3 Dev Module” em Boards. Clique nela e selecione a porta COM para ativá-la e depois clique OK:

Board Selection Dialog "ESP32S3 Dev Module" board
Diálogo de seleção da placa “ESP32S3 Dev Module”

Note que precisa de ligar a placa via cabo USB ao seu computador antes de poder selecionar uma porta COM. O CrowPanel Display Module tem duas portas, uma porta USB e uma porta UART. Pode usar ambas para programar a placa, mas para a porta UART precisará de um conversor USB-to-TTL, veja abaixo:

UART port of CrowPanel Display Module
Porta UART do CrowPanel Display Module (source)

A forma mais fácil é ligar o cabo USB que vem com o CrowPanel Display Module diretamente à porta rotulada “USB” como mostrado abaixo. Neste caso não precisa de um conversor USB-to-TTL.

USB Port of CrowPanel Display
Porta USB do CrowPanel Display Module (source)

Configurações da Ferramenta

Abaixo estão as configurações que deve usar com a placa. Encontra-as no menu Tools do seu Arduino IDE.

Tool settings for CrowPanel 1.28inch-HMI ESP32 Rotary Display
Configurações da ferramenta para CrowPanel 1.28inch-HMI ESP32 Rotary Display

O mais importante para os exemplos de código nas secções seguintes é definir USB CDC on Boot para “Enabled” e também definir o modo USB para “Hardware CDC and JTAG”. Isto permite enviar ou receber dados via porta serial, o que será necessário para depuração.

Exemplo de Código: Interface Serial

Começamos por testar a comunicação Serial. Abra o seu Arduino IDE, insira o código seguinte e faça upload para o CrowPanel Display Module.

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

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

Depois abra o Monitor Serial e deverá ver o texto “Makerguides” impresso a cada dois segundos. Se não, certifique-se que as configurações em Tools e a taxa de Baud (115200) estão corretas.

Exemplo de Código: Anel de LED RGB

De seguida, experimentamos o LED RGB onboard. No entanto, precisará de instalar a Adafruit_NeoPixel biblioteca primeiro. Abra o LIBRARY MANAGER no seu Arduino IDE, escreva “Adafruit NeoPixel” na barra de pesquisa e instale a biblioteca Adafruit NeoPixel da Adafruit:

Installation of Adafruit NeoPixel for Arduino
Instalação do Adafruit NeoPixel para Arduino

O CrowPanel Display Module tem uma tira de LED RGB WS2812 integrada com cinco LEDs. O exemplo seguinte demonstra como inicializar a tira de LEDs, definir uma cor e aumentar gradualmente o brilho usando a biblioteca 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);  
  }
}

Importações

O programa começa por incluir a biblioteca Adafruit_NeoPixel, que fornece uma interface simples para controlar LEDs WS2812. Esta biblioteca gere o timing rigoroso exigido pelo protocolo de comunicação de fio único dos LEDs, permitindo que se concentre na lógica de cor e animação em vez dos detalhes de temporização de baixo nível.

#include <Adafruit_NeoPixel.h>

Constantes

Duas constantes são definidas para especificar quantos LEDs estão ligados e qual o pino GPIO usado para os controlar. O CrowPanel liga a tira WS2812 ao GPIO 48, e a tira contém cinco LEDs.

#define LED_PIN 48
#define LED_NUM 5

Criação do Objeto

De seguida, é criado um objeto da tira NeoPixel. O construtor recebe três parâmetros: o número de LEDs, o pino de controlo e a configuração do tipo de pixel. A flag NEO_GRB + NEO_KHZ800 especifica que cada LED usa a ordem de cor GRB (verde, vermelho, azul) e opera a 800 kHz, que é a frequência padrão para LEDs WS2812.

Adafruit_NeoPixel strip = Adafruit_NeoPixel(LED_NUM, LED_PIN, NEO_GRB + NEO_KHZ800);

Este objeto será usado ao longo do programa para controlar os LEDs — definir cores, ajustar brilho e enviar atualizações de dados.

A updateBrightness() Função

Esta função auxiliar ajusta dinamicamente o brilho da tira de LEDs. O método setBrightness() aceita um valor entre 0 (desligado) e 255 (brilho máximo). Depois de definir o brilho, a função show() deve ser chamada para enviar os dados atualizados para os LEDs.

void updateBrightness(int brightness) {    
  strip.setBrightness(brightness);
  strip.show();
  delay(100);
}

A curta pausa assegura uma transição suave e visível entre os níveis de brilho, criando um efeito gradual de fade-in.

A initLEDs() Função

Antes de poder controlar os LEDs, estes devem ser inicializados. A função begin() prepara o pino de dados para comunicação, e setBrightness(100) define um nível inicial de brilho para evitar começar na intensidade máxima.

O ciclo for itera sobre cada um dos cinco LEDs, atribuindo-lhes uma cor usando setPixelColor(). O método strip.Color(r, g, b) converte os valores individuais de vermelho, verde e azul no formato de cor de 24 bits exigido pelos 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 os dados de cor para todos os LEDs, iluminando-os simultaneamente na cor selecionada. No nosso caso será amarelo.

Setup

A função setup() é executada uma vez quando o ESP32 é ligado ou reiniciado. Começa por inicializar a comunicação serial a 115200 baud, permitindo imprimir mensagens no monitor serial. Depois chama initLEDs(255, 255, 50), que acende os cinco LEDs num tom amarelo suave.

void setup() {
  Serial.begin(115200);
  initLEDs(255, 255, 50); // Yellow
}

Loop

A função loop() é executada continuamente após a configuração. Neste exemplo, aumenta gradualmente o brilho de 5 para 255 em passos de 5, chamando updateBrightness() a cada vez. Após cada ajuste, imprime o valor atual do brilho no monitor serial.

void loop() {
  for(int brightness=5; brightness<255; brightness+=5) {
    updateBrightness(brightness);  
    Serial.printf("Brightness: %d\n", brightness);  
  }
}

Isto cria uma animação suave de fade-in que se repete continuamente. Certifique-se de não definir o brilho para zero. A tira de LEDs ficará apagada, mesmo que depois altere o brilho. Terá de definir as cores dos LEDs da tira novamente.

Exemplo de Código: Encoder Rotativo

Este sketch baseia-se no exemplo NeoPixel anterior e adiciona controlo de brilho em tempo real usando um encoder rotativo mecânico. Os LEDs continuam a usar a biblioteca Adafruit NeoPixel para temporização e gestão de cores, enquanto o encoder é lido via rotina de serviço de interrupção para uma interação responsiva e sem tremores. O resultado é um botão de brilho suave, controlado por hardware, para a tira WS2812 de cinco LEDs do 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);
}

Importações

O programa reutiliza a mesma biblioteca NeoPixel usada anteriormente, assim o sinal crítico para os LEDs WS2812 é totalmente gerido e pode concentrar-se na lógica de entrada e animação.

#include <Adafruit_NeoPixel.h>

Constantes

A tira de LEDs mantém-se ligada ao GPIO 48 e contém cinco pixels como antes. Duas constantes adicionais definem os canais A e B do encoder rotativo nos GPIO 45 e GPIO 42.

#define LED_PIN 48
#define LED_NUM 5
#define ENCODER_CLK 45  // A
#define ENCODER_DT 42   // B

Objetos

O objeto NeoPixel é configurado com os mesmos parâmetros explicados no exemplo anterior. A ordem de cor GRB e sinalização a 800 kHz correspondem aos requisitos WS2812.

Adafruit_NeoPixel strip = Adafruit_NeoPixel(LED_NUM, LED_PIN, NEO_GRB + NEO_KHZ800);

Variáveis de Estado

O sketch mantém um valor global de brilho no intervalo 0 a 255 e acompanha os estados instantâneo e anterior do encoder. Uma flag booleana sinaliza ao loop principal que há uma atualização de brilho, que causa o ajuste do brilho no loop principal.

int brightness = 50;  // 0..255
int enc_state = 0;
int old_state = -1;
bool has_changed = true;

A variável brightness é intencionalmente inicializada a um nível moderado para que os LEDs não liguem na carga máxima. Os estados do encoder são inicializados para que a primeira transição seja detetada corretamente, e has_changed começa como true para forçar uma renderização inicial.

Rotina de Serviço de Interrupção: encoder_irq

O encoder é decodificado num contexto de interrupção para manter a latência da interface baixa. A rotina é executada em ambas as bordas do canal CLK e compara o canal DT para determinar a direção da rotação. Se DT for igual a CLK, o código trata o movimento numa direção e subtrai cinco passos de brilho; se não for igual, adiciona cinco. O tamanho do passo de cinco é um compromisso prático entre controlo granular e responsividade.

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

O atributo IRAM_ATTR assegura que a rotina é colocada na RAM de instruções, evitando estados de espera na flash do ESP32 durante interrupções. A chamada constrain impõe um mínimo de 5 para evitar saída completamente escura e um máximo de 255 para limitar o brilho. A flag has_changed adia a atualização real dos LEDs para o thread principal, mantendo a ISR curta e determinística.

Atualização do Brilho: updateBrightness

As atualizações reais dos LEDs são centralizadas aqui. A função aplica o brilho atual à tira e chama show para transmitir os dados de pixel em buffer. Uma curta pausa é usada para tornar as mudanças sucessivas visíveis e evitar saturar a taxa de atualização dos LEDs.

void updateBrightness() {  
  strip.setBrightness(brightness);
  strip.show();
  delay(200);
}

Como o brilho no NeoPixel é um multiplicador global aplicado no momento da transmissão, esta abordagem atualiza a intensidade sem reescrever as cores individuais dos pixels.

Inicialização dos LEDs: initLEDs

A lógica de inicialização espelha o exemplo anterior, mas adia o brilho para updateBrightness para que o mesmo caminho de código seja usado tanto na inicialização como durante as rotações do botão. Todos os cinco pixels recebem a mesma cor inicial, que pode alterar para feedback 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();
}

Chamar updateBrightness no final garante que a cor inicial aparece ao nível de brilho global atual.

Inicialização do Encoder: initEncoder

Os canais do encoder são configurados como entradas com pull-ups internos porque muitos encoders de painel são do tipo open-collector ou interruptores mecânicos. A interrupção é ligada à linha CLK em ambas as bordas para que cada detente produza uma transição de estado que a ISR pode decodificar.

void initEncoder() {
  pinMode(ENCODER_CLK, INPUT_PULLUP);
  pinMode(ENCODER_DT, INPUT_PULLUP);

  attachInterrupt(digitalPinToInterrupt(ENCODER_CLK), encoder_irq, CHANGE);
}

Usar interrupções na linha CLK, em vez de polling no loop, elimina passos perdidos em rotações rápidas e reduz o uso da CPU. Se o seu encoder for particularmente ruidoso, pode adicionar um simples debounce ignorando mudanças dentro de uma janela de microssegundos ou amostrando ambos os canais novamente após uma curta pausa.

Setup

A saída serial é inicializada para visibilidade, o encoder é armado primeiro para capturar imediatamente a entrada do utilizador, e os LEDs são acesos com um tom amarelo. A flag de mudança é definida para garantir que o primeiro frame é enviado para a tira mesmo que o encoder ainda não tenha sido movido.

void setup() {
  Serial.begin(115200);
  initEncoder();
  initLEDs(255, 255, 50);
  has_changed = true;
}

Loop

O loop principal é deliberadamente minimalista. Verifica se a ISR sinalizou uma mudança de brilho e, se sim, aplica o novo nível e reporta-o via serial.

void loop() {
  if (has_changed) {
    has_changed = false;
    updateBrightness();
    Serial.printf("Brightness: %d\n", brightness);
  }
  delay(1);
}

Esta estrutura mantém o trabalho crítico e curto na interrupção e deixa a I/O mais pesada dos LEDs para o primeiro plano, que é um padrão robusto para código UI responsivo no ESP32.

Exemplo de Código: Ecrã

No próximo exemplo vamos controlar o brilho do ecrã usando o encoder rotativo. A imagem abaixo mostra três valores diferentes de brilho (10%, 50%, 100%) para o ecrã:

Different brightness settings for display
Configurações diferentes de brilho para o ecrã

Primeiro, precisará de instalar a biblioteca Arduino_GFX por moononournation para poder controlar o ecrã. Abra o LIBRARY MANAGER, escreva “GFX Library for Arduino” na barra de pesquisa, encontre a “GFX Library for Arduino” por Moon, e clique no botão INSTALL:

Installation of GFX Library for Arduino
Instalação da GFX Library para Arduino

O sketch seguinte liga o encoder rotativo ao ecrã TFT GC9A01 usando a biblioteca Arduino_GFX e adiciona controlo PWM da retroiluminação. Estende os conceitos do encoder dos exemplos anteriores e permitirá controlar o brilho do ecrã com um toggle de botão para a retroiluminação.

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

Importações

O programa depende do Arduino_GFX para primitivas rápidas de desenho SPI e um driver para o painel GC9A01. A biblioteca oculta comandos específicos do controlador e fornece uma API consistente para inicialização, preenchimento de cor e renderização de texto.

#include <Arduino_GFX_Library.h>

Constantes

Os pinos definem a ligação SPI ao TFT redondo, os trilhos de alimentação para o painel e o seu ânodo LED, o pino de retroiluminação controlado por PWM, e os canais e interruptor do encoder rotativo. O brilho é expresso em percentagem para que o feedback do utilizador possa ser impresso diretamente 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)

Variáveis de Estado

O estado em tempo de execução acompanha a percentagem atual de brilho, se a retroiluminação está ativada, o estado instantâneo do encoder e uma flag de mudança que permite à ISR agendar trabalho para o thread principal. Como antes, manter as ISRs curtas e adiar a I/O para o primeiro plano evita falhas de temporização.

int  brightness   = 50;   // 0..100 %
bool btn_display  = true;
int  enc_state    = 0;
int  old_state    = -1;
bool has_changed  = true;

Objetos do Bus e Driver do Ecrã

A stack Arduino_GFX está dividida em um objeto bus e um objeto painel. O bus envolve o host FSPI do ESP32 com DC, CS, SCLK e MOSI. O objeto painel destina-se ao controlador GC9A01 e sabe como inicializar um painel redondo IPS. A rotação está definida para zero e o último booleano marca o painel como IPS para selecionar a inversão de cor e gama corretas.

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

Rotina de Serviço de Interrupção do Encoder

A ISR do encoder espelha a lógica anterior, mas o tamanho do passo é de um por cento por borda para dar controlo fino. A rotina amostra o canal CLK para detetar uma borda, compara DT para determinar a direção, atualiza a percentagem, limita o resultado e inverte a flag de mudança.

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

Rotina de Serviço de Interrupção do Botão

O botão de pressão do encoder é ativo-baixo. A ISR lê o pino e alterna um booleano que representa o estado da retroiluminação. O desenho e a atualização PWM são adiados para o loop principal definindo a flag de mudança.

void IRAM_ATTR button_irq() {
  if (!digitalRead(ENCODER_BTN)) {
    btn_display = !btn_display;
    has_changed = true;
  }
}

Desenhar Texto Estático

Um pequeno auxiliar desenha uma etiqueta do site uma vez no arranque. O tamanho do texto e o cursor são posicionados para um ecrã redondo 240×240. Note que o Arduino_GFX usa um cursor baseado em pixels que avança com os prints.

void drawText() {
  gfx->setTextSize(2);
  gfx->setCursor(60, 55);
  gfx->println(F("Makerguides"));
}

Atualizar Brilho e Retroiluminação

Esta função atualiza o valor numérico no ecrã e aplica PWM ao pino da retroiluminação. A área retangular atrás dos dígitos é limpa a branco para evitar fantasmas quando os números mudam. A percentagem é mapeada para um ciclo de trabalho de 8 bits, que analogWrite se traduz num PWM hardware via LEDC do ESP32. Quando o toggle do ecrã está desligado, o ciclo de trabalho é forçado a zero.

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

Inicialização de Energia

Os trilhos de lógica do painel e do ânodo LED são ativados antes de qualquer comando de ecrã ser enviado. Ambos os enables são colocados em nível alto. O pino da retroiluminação é configurado para PWM e definido para brilho baixo (50), o que torna o texto visível mas não demasiado brilhante.

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

Inicialização do Ecrã

Após uma breve pausa para estabilização dos trilhos de energia, o driver GC9A01 é iniciado, o ecrã é limpo a branco e a cor do texto é definida para preto. A partir daqui o sketch pode desenhar primitivas e texto à vontade.

void initDisplay() {
  delay(20);                 
  gfx->begin();
  gfx->fillScreen(WHITE);
  gfx->setTextColor(BLACK);
}

Inicialização do Encoder

Os canais do encoder e o botão usam pull-ups internos, que correspondem a encoders típicos de contacto aberto. As interrupções são ligadas ao canal CLK para decodificação quadratura e ao botão para alternar. Usar CHANGE captura ambas as bordas de pressão e libertação; a própria ISR verifica a condição ativo-baixo.

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

O sistema alimenta o painel, arma as interrupções do encoder, inicializa o ecrã, desenha o título estático e prepara a flag de mudança para que o primeiro frame de brilho e atualização PWM aconteçam imediatamente. O registo serial está ativado para visibilidade durante os testes.

void setup() {
  Serial.begin(115200);
  initPower();
  initEncoder();
  initDisplay();
  drawText();
  has_changed = true;
}

Loop

O primeiro plano verifica se as ISRs solicitaram uma atualização. Quando ativado, redesenha o brilho numérico, define o brilho da retroiluminação e imprime uma linha de estado mostrando a percentagem e se a retroiluminação está atualmente ativada.

void loop() {
  if (has_changed) {
    has_changed = false;
    updateBrightness();
    Serial.printf("Brightness: %d%% (display %s)\n",
                  brightness, btn_display ? "ON" : "OFF");
  }
  delay(1);
}

Exemplo de Código: Ecrã Tátil

Este último exemplo demonstra como ler dados táteis do controlador capacitivo CST816D integrado no CrowPanel 1.28-inch HMI ESP32 Rotary Display. Desenha pontos vermelhos onde o utilizador toca no ecrã. Veja a foto de exemplo abaixo:

Touch events as red dots on CrowPanel Display
Eventos de toque como pontos vermelhos no CrowPanel Display

O código seguinte reutiliza o mesmo pipeline de ecrã Arduino_GFX introduzido anteriormente, adiciona a biblioteca Wire para leituras I²C de baixo nível, e decodifica os registos brutos do controlador tátil CST816D em coordenadas do ecrã em tempo 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);
  }
}

Importações

Duas bibliotecas são incluídas no topo do programa.
Wire.h gere a comunicação sobre o barramento I²C, que é usado para comunicar com o controlador tátil.
Arduino_GFX_Library.h gere a comunicação SPI com o ecrã TFT redondo GC9A01, permitindo desenhar texto e formas facilmente.

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

Definições de Pinos

Esta secção define todas as atribuições de pinos para o ecrã, controlador tátil e trilhos de energia.
Os pinos SPI ligam o ESP32 ao controlador TFT GC9A01, enquanto os pinos I²C (TOUCH_SDA e TOUCH_SCL) comunicam com o chip tátil CST816D. Pinos de enable separados controlam os trilhos de 3,3 V que alimentam a lógica do LCD (LCD_PWR_EN1) e a retroiluminação 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 do Bus e Driver do Ecrã

A biblioteca Arduino_GFX separa o driver do ecrã em dois objetos. Arduino_ESP32SPI define a configuração do bus SPI para o periférico FSPI do ESP32, incluindo os pinos de dados e controlo. Arduino_GC9A01 representa o controlador específico do ecrã e gere comandos como inicialização, preenchimento de cor e renderização 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 configuração corresponde ao ecrã IPS redondo 240×240 do CrowPanel.

A i2cRead() Função Auxiliar

Esta função fornece uma forma genérica de ler um ou mais registos de qualquer dispositivo I²C.
Começa a comunicação com o endereço do dispositivo especificado, escreve o registo a ler e depois solicita um número de bytes ao dispositivo. Cada byte é armazenado no buffer passado via reg_data. Se algum passo falhar, retorna -1 para sinalizar um erro.

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 abstração mantém a função readTouch() mais limpa e focada em interpretar dados táteis em vez de gerir transações no barramento.

Ler Dados Táteis com readTouch()

O controlador tátil CST816D fornece eventos de toque e dados de coordenadas via I²C no endereço 0x15.
Esta função lê sete bytes a partir do registo 0x02, que contêm informação sobre o estado atual do toque e coordenadas.

Se o código do evento indicar toque ou contacto (valores 0 ou 2), a função decodifica as posições X e Y combinando os bits altos e baixos dos registos de coordenadas.
Coordenadas válidas são retornadas através dos ponteiros x e y, e a função retorna 1 para sinalizar um 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;
}

Esta análise de registos de baixo nível segue o datasheet do CST816D, onde os bits superiores de cada byte de coordenada estão armazenados no nibble inferior do byte anterior.

Inicializar Energia e I/O com initPins()

Antes do ecrã ou controlador tátil poderem operar, os GPIOs apropriados devem ser configurados.
Esta função configura os trilhos de energia do LCD, PWM da retroiluminação e pinos da interface tátil. Ambas as linhas de enable do LCD são colocadas em nível alto para fornecer 3,3 V à lógica do ecrã e LEDs. O pino da retroiluminação (TFT_BLK) é configurado para controlo PWM, inicializado com um ciclo de trabalho modesto de 100 para brilho visível.

A linha de interrupção tátil é configurada como entrada com resistor pull-up interno. O pino de reset do CST816D é colocado em nível baixo por alguns milissegundos e depois libertado para nível alto para reiniciar corretamente o controlador tátil. Finalmente, o barramento I²C é inicializado nos pinos SDA e 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);
}

Inicialização do Ecrã

Após uma breve pausa para garantir estabilidade da energia, o ecrã GC9A01 é inicializado. O ecrã é limpo a branco, a cor do texto é definida para preto, e uma etiqueta “Touch” é desenhada no centro para indicar prontidão.

void initDisplay() {
  delay(20);              
  gfx->begin();
  gfx->fillScreen(WHITE);
  gfx->setTextColor(BLACK);
  gfx->setTextSize(3);
  gfx->setCursor(80, 105);
  gfx->print(F("Touch"));
}

Esta configuração cria uma tela limpa para desenhar indicadores de toque mais tarde no loop principal.

Setup

A função setup() é simples. Inicializa a interface serial para depuração, depois chama initPins() e initDisplay() para preparar o hardware.

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

Neste ponto, o ecrã está ativo e o controlador tátil pronto para reportar coordenadas.

Loop

O loop principal verifica continuamente a entrada tátil usando a função readTouch(). Se for detetado um toque válido, desenha um pequeno círculo vermelho nas coordenadas reportadas usando a função fillCircle() da biblioteca Arduino_GFX. Cada toque cria um novo ponto, permitindo ao utilizador “desenhar” no ecrã.

void loop() {
  static int x, y;
  if (readTouch(&x, &y) == 1) {
    gfx->fillCircle(x, y, 5, RED);
  }
}

Este loop minimalista destaca a rapidez de resposta do CST816D quando acedido via I²C e demonstra como é fácil renderizar feedback gráfico no ecrã GC9A01.

Conclusões

Este tutorial forneceu exemplos de código para começar a usar o CrowPanel 1.28inch-HMI ESP32 Rotary Display.

Para mais exemplos veja Elecrows’s Github repo for the 1.28inch-HMI ESP32 Rotary Display e o Wiki Page. Note, no entanto, que muitos dos exemplos de código aí usam as bibliotecas ui e lvgl, que evitei aqui para manter a complexidade baixa.

Se procura um módulo de ecrã semelhante com anel de encoder rotativo, dê uma vista de olhos ao Matouch 1.28″ ToolSet_Controller. Este não tem o anel de LED RGB mas inclui um relógio em tempo real (RTC) e um motor de vibração para feedback háptico. E se precisar apenas de um ecrã redondo (sem o anel de encoder rotativo), o tutorial Digital Clock on CrowPanel 1.28″ Round Display pode ser útil.

Além disso, se quiser aprender mais sobre encoders rotativos, veja o nosso tutorial How To Interface A Quadrature Rotary Encoder.

Se tiver alguma dúvida, sinta-se à vontade para deixar nos comentários.

Boas experiências de construção 😉