Skip to Content

Começar com o CrowPanel 2.1inch-HMI ESP32 Rotary Display

Começar com o CrowPanel 2.1inch-HMI ESP32 Rotary Display

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

Neste tutorial, vamos passar pelos passos essenciais para começar a usar este ecrã; desde a configuração inicial até testar as suas capacidades de visualização e entrada. Vai aprender a ler dados do encoder rotativo para uma entrada de utilizador suave e a capturar eventos de toque no ecrã circular.

Peças Necessárias

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

O CrowPanel 2.1inch-HMI ESP32 Rotary Display é um módulo de ecrã que combina um ecrã IPS redondo de alta resolução, um ecrã tátil capacitivo e um encoder rotativo com funcionalidade de botão de pressão.

O módulo é construído em torno do sistema em chip ESP32‑S3R8 que utiliza um processador dual-core Xtensa LX7 de 32 bits capaz de velocidades de relógio até 240 MHz. Está equipado com 8 Mbit PSRAM e 16 Mbyte de armazenamento flash onboard.

A parte do ecrã é um painel IPS RGB circular de 2,1 polegadas (ST7701 controlador) com resolução de 480 × 480 pixels, com uma camada tátil capacitiva que suporta operação tátil em ecrã completo e uma luz de fundo.

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

Em termos de operação física, o hardware incorpora um mecanismo de “botão” de encoder rotativo. O botão é capaz de rotação no sentido horário e anti-horário, bem como uma pressão para dentro (funcionando como um interruptor) para entrada do utilizador.

Para conectividade e expansão, o módulo suporta interfaces sem fios e com fios. No lado sem fios, apresenta WiFi IEEE 802.11 a/b/g/n (2.4 GHz) e Bluetooth Low Energy (BLE) / Bluetooth 5.0.

No lado com fios, o módulo expõe uma tomada de carregamento/interface de 5 V que funciona como fonte de alimentação e porta de programação. Além disso, a placa fornece três interfaces de expansão: uma interface UART, uma interface I2C e um conector FPC (12 pinos) para conectividade adicional.

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

Definições dos Pinos

A tabela seguinte lista os pinos necessários para controlar todos os componentes do módulo de ecrã:

Grupo de FunçãoSinal / PropósitoNúmero do PinoNotas
Interface I²CSDA38Linha principal de dados I²C
SCL39Linha principal de relógio I²C
Encoder RotativoEncoder A42Canal quadratura A
Encoder B4Canal quadratura B
Luz de Fundo do EcrãControlo da Luz de Fundo6Pino com capacidade PWM usado para brilho da luz de fundo do LCD
Controlo OLEDRESET–1Sem pino de reset usado (reset por software)
Interface do Ecrã RGBCS16Linha de seleção do chip
SCK2Relógio serial do ecrã
SDA1Dados seriais do ecrã
DE40Ativação de dados
VSYNC7Sincronização vertical
HSYNC15Sincronização horizontal
PCLK41Relógio de pixel
Pinos de Dados de Cor RGB (formato 5-6-5)R046Bit 0 do vermelho
R13Bit 1 do vermelho
R28Bit 2 do vermelho
R318Bit 3 do vermelho
R417Bit 4 do vermelho
G014Bit 0 do verde
G113Bit 1 do verde
G212Bit 2 do verde
G311Bit 3 do verde
G410Bit 4 do verde
G59Bit 5 do verde
B05Bit 0 do azul
B145Bit 1 do azul
B248Bit 2 do azul
B347Bit 3 do azul
B421Bit 4 do azul
Expansor de I/O PCF8574Endereço I²C0x21Ligado aos pinos 38 (SDA) e 39 (SCL)

Especificações Técnicas

A tabela seguinte resume as especificações técnicas do módulo CrowPanel 2.1inch-HMI ESP32 Rotary Display:

Categoria de EspecificaçãoDetalhes
Processador principalSoC: Xtensa LX7 dual-core (32-bit) no ESP32‑S3R8 (até 240 MHz)
Memória & armazenamentoRAM do sistema: 512 kB SRAM; PSRAM: 8 MB; Flash: 16 MB
Painel de ecrãTamanho: 2,1 polegadas; Tipo: IPS; Resolução: 480 × 480 pixels; Toque: camada tátil capacitiva em ecrã completo
Mecanismos de entrada do utilizadorBotão rotativo (rotação horário/anti-horário + pressão total); Entrada tátil capacitiva na superfície do ecrã
Conectividade sem fiosWiFi: IEEE 802.11 a/b/g/n (2.4 GHz); Bluetooth: BLE / Bluetooth 5.0
Portas de interface / expansãoEntrada 5 V (carregamento e programação); Interface UART: 1× UART0 + 1× UART1 via ZX-MX 1.25-4P; Interface I²C; Conector FPC de 12 pinos (alimentação/programação e GPIO)
Botões & indicadoresBotão RESET onboard; Botão BOOT; Interruptor de pressão do botão rotativo (botão de confirmação); LED indicador de energia; Luz de fundo do LCD
Energia & voltagemEntrada do módulo: 5 V / 1 A; Nível lógico do chip principal: 3.3 V; Operação do módulo a 5 V nominal
Mecânico / físicoDimensões: 79 × 79 × 30 mm; Peso líquido: 80 g; Carcaça: liga de alumínio + plástico + acrílico
Faixa de temperaturaOperação: –20 °C a +65 °C; Armazenamento: –40 °C a +80 °C
Suporte de software / ambiente de desenvolvimentoCompatível com Arduino IDE, ESP-IDF, Lua RTOS, MicroPython, PlatformIO; Biblioteca gráfica UI: suporte LVGL

Instalação do Core ESP32

Se este for o seu primeiro projeto com uma placa da série ESP32, precisará instalar primeiro o core ESP32. No entanto, para o CrowPanel 2.1inch Display, deverá instalar uma versão específica (2.0.14) do core ESP32.

Comece por abrir o diálogo de Preferências no Arduino IDE 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 permitirá 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 verá dois tipos de placas ESP32; as “Arduino ESP32 Boards” e as placas “esp32 by Espressif”. Queremos as “esp32 libraries by Espressif”.

No menu dropdown selecione a Versão “2.0.14” e depois pressione o botão “INSTALL”. A captura de ecrã abaixo mostra o que deve ver no GESTOR DE PLACAS após uma instalação bem-sucedida do Core ESP32 2.0.14:

Install ESP32 Core 2.0.14
Instalar Core ESP32 2.0.14

Note que pode instalar versões até 2.0.17 mas não 3.x. As bibliotecas que iremos instalar na secção seguinte não funcionarão com o core ESP32 3.x.

Seleção da Placa

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

Drop-down Menu for Board Selection
Menu Dropdown 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. Ligue o cabo USB que acompanha o CrowPanel Display Module diretamente à porta rotulada “USB-5V-IN” como mostrado abaixo:

USB Port of CrowPanel Display Module
Porta USB do CrowPanel Display Module

Configurações da Ferramenta

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

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

O mais importante para os exemplos de código nas secções seguintes é definir USB CDC on Boot para “Enabled”. Isto permite enviar ou receber dados via porta serial, necessário para depuração. Também precisará de definir corretamente “OPI PSRAM”, “Huge APP” e “Flash Size” para os exemplos de código do ecrã.

Instalação das Bibliotecas

A seguir, precisamos instalar bibliotecas específicas e versões para fazer o código do CrowPanel 2.1inch-HMI ESP32 Rotary Display funcionar.

Vá ao Elecrow github repo para o CrowPanel 2.1inch-HMI Display. Clique no botão verde “<> Code” e depois em “Download ZIP” para descarregar o repositório como um ficheiro ZIP:

Depois, descompacte o ficheiro ZIP para extrair o seu conteúdo. Deve ver os seguintes ficheiros numa pasta descompactada chamada “CrowPanel-2.1inch-HMI-ESP32-Rotary-Display-480-480-IPS-Round-Touch-Knob-Screen-master”:

Ignore os ficheiros relacionados com o ecrã “CrowPanel 1.28inch …”. Só precisamos copiar o conteúdo da pasta “example/libraries” para a pasta “libraries” do Arduino IDE. No Windows, a pasta “libraries” está tipicamente localizada em:

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

Como esta pasta já contém bibliotecas instaladas, recomendo que a renomeie temporariamente, por exemplo para “_libraries”, e crie uma nova pasta chamada “libraries”. Assim evita conflitos com as bibliotecas já instaladas e não as perde. Depois pode reverter facilmente esta alteração. A imagem abaixo mostra como deve ficar a sua pasta “Arduino” com as bibliotecas:

Depois copiamos os ficheiros da pasta “example/libraries” para a nova pasta “libraries” como mostrado abaixo:

Não precisa copiar as bibliotecas riscadas, pois estão relacionadas com a UI LVGL, que não vamos usar. Nas próximas secções, mostrarei alguns exemplos de código para experimentar o ecrã.

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 carregue-o no 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, verifique se as configurações de Tools e a taxa de Baud (115200) estão corretas. Especificamente, precisa que “USB CDC on Boot” esteja definido como “Enabled”.

Exemplo de Código: Encoder

No próximo exemplo de código vai aprender a usar o encoder e o interruptor do encoder. Usaremos uma abordagem baseada em interrupções para ler um encoder rotativo quadratura e o PCF8574 para ler o estado do botão do encoder.

A rotação do encoder é processada numa ISR rápida com deteção de direção, enquanto o loop principal foca-se em reportar mudanças e ler o estado do botão. Dê uma olhada rápida no código e depois discutimos os detalhes.

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

Importações

O sketch começa por incluir duas bibliotecas que tratam da comunicação I2C e do expansor de I/O PCF8574. Estes headers devem ser incluídos antes de poder usar Wire ou PCF8574 objetos.

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

Wire.h fornece a interface I2C (TWI) padrão do Arduino. PCF8574.h encapsula o acesso I2C de baixo nível ao chip PCF8574 para que possa tratar os seus pinos quase como pinos digitais normais.

Constantes

De seguida, o código define os pinos usados para I2C e para o encoder rotativo:

#define I2C_SDA_PIN 38
#define I2C_SCL_PIN 39

#define ENCODER_CLK 42  
#define ENCODER_DT 4   

I2C_SDA_PIN e I2C_SCL_PIN selecionam quais os pinos ESP32 usados como SDA e SCL para o barramento I2C de hardware. No ESP32 pode-se encaminhar o I2C para vários pinos, por isso é necessário especificá-los aqui.

ENCODER_CLK e ENCODER_DT são as duas saídas quadratura do encoder rotativo. O sinal CLK é tipicamente o canal principal usado para a interrupção, e DT é o segundo canal usado para determinar a direção da rotação.

Estado Global e Variáveis Voláteis

O código usa várias variáveis globais para acompanhar o estado do encoder. Estas variáveis são marcadas volatile porque são modificadas dentro de uma rotina de serviço de interrupção.

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

counter mantém a posição acumulada do encoder. Cada passo do encoder incrementa ou decrementa este valor.

encState representa o nível lógico atual do pino CLK do encoder visto no manipulador de interrupção. oldState guarda o estado anterior do CLK para que o código possa detetar transições e evitar contar o mesmo nível várias vezes.

hasChanged é usado como uma flag para sinalizar ao loop principal que o contador foi atualizado na interrupção. Marcar estas variáveis como volatile indica ao compilador que podem mudar a qualquer momento e não devem ser armazenadas em registos, o que é crítico quando são modificadas por uma ISR.

Objeto PCF8574

O código cria então uma instância global da classe PCF8574, especificando o endereço I2C do chip.

PCF8574 pcf8574(0x21);

Esta linha indica à biblioteca PCF8574 que o expansor de I/O está acessível no endereço I2C 0x21. Todas as chamadas subsequentes a pcf8574.pinMode ou pcf8574.digitalRead usarão este endereço para comunicar com esse dispositivo específico no barramento I2C.

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

A parte mais sensível ao tempo do código é a rotina de serviço de interrupção (ISR) que trata das mudanças do encoder. Está colocada em IRAM usando o atributo IRAM_ATTR para que possa executar rápida e fiavelmente no 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 garante que a função é armazenada na RAM de instruções em vez de flash, evitando atrasos causados pelo acesso a flash e é recomendado para ISRs no ESP32.

Dentro da ISR, o nível atual do sinal CLK do encoder é lido usando digitalRead(ENCODER_CLK) e armazenado em encState. O código verifica então se este estado é diferente de oldState. Esta condição filtra chamadas redundantes porque a interrupção está configurada para disparar em qualquer mudança (borda ascendente ou descendente) e o manipulador pode executar várias vezes com o mesmo nível lógico.

Se o estado mudou, a direção da rotação é calculada comparando o sinal DT com o sinal CLK. A linha seguinte lê o pino DT e verifica se o seu nível corresponde ao nível atual do CLK:

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

Para um encoder quadratura, esta relação indica a direção da rotação. Se DT for igual a CLK, o encoder está a mover-se numa direção e counter é incrementado; se forem diferentes, counter é decrementado.

Após atualizar o contador, oldState é atualizado para o novo estado do CLK e hasChanged é definido como true. Esta flag informa o loop principal que há novos dados do encoder para processar ou mostrar. A ISR mantém-se curta e eficiente, o que é boa prática para rotinas de interrupção.

Inicialização dos Pinos e I2C

A função initPins configura o barramento I2C e o pino PCF8574 usado para o botão de pressão do 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 o periférico I2C com pinos SDA e SCL personalizados.

pcf8574.pinMode(P5, INPUT_PULLUP); configura o pino P5 no PCF8574 como entrada com resistor pull-up interno. Este pino está ligado ao botão de pressão do encoder (interruptor). A configuração pull-up significa que o pino lê alto quando o botão não está pressionado e baixo quando está pressionado, assumindo que o botão liga o pino ao terra.

pcf8574.begin() inicia a comunicação com o dispositivo PCF8574 no endereço I2C configurado. Se falhar, o código imprime uma mensagem de erro via Serial.println, o que ajuda a diagnosticar problemas de ligação ou configuração de endereço.

Inicialização do Encoder

A função initEncoder configura os dois sinais do encoder e anexa uma interrupção ao pino CLK.

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

pinMode(ENCODER_CLK, INPUT_PULLUP); e pinMode(ENCODER_DT, INPUT_PULLUP); configuram ambos os canais do encoder como entradas com resistores pull-up internos. Esta é uma configuração típica para encoders rotativos mecânicos, que normalmente ligam os pinos ao terra num padrão de código cinzento à medida que o botão roda.

attachInterrupt(digitalPinToInterrupt(ENCODER_CLK), encoder_irq, CHANGE); anexa o manipulador de interrupção encoder_irq ao pino CLK do encoder. O modo CHANGE significa que a ISR será disparada em ambas as bordas ascendente e descendente do sinal. Isto dá maior resolução e contagem mais precisa, pois o encoder é amostrado em cada transição.

Setup

A função setup é executada uma vez na inicialização e fornece uma sequência de inicialização.

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

Serial.begin(115200); inicia a porta serial a 115200 baud, útil para depuração e monitorização. Deve ser chamada antes de qualquer Serial.print ou Serial.printf chamadas.

initPins(); inicializa o barramento I2C e o expansor de I/O PCF8574. Isto garante que o botão do encoder pode ser lido e que o hardware I2C está pronto.

initEncoder(); configura os pinos GPIO usados pelo encoder rotativo e configura a interrupção para que os eventos de rotação sejam capturados assim que o loop principal começar.

Loop

A função loop executa-se repetidamente e trata duas tarefas principais: imprimir o valor do encoder quando muda e ler o estado do botão do 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);
}

O primeiro bloco verifica se o contador do encoder foi atualizado pela ISR. A flag hasChanged é definida como true dentro da interrupção quando a posição do encoder muda.

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

Se hasChanged for verdadeiro, a flag é imediatamente redefinida para falso e o valor atual de counter é impresso no monitor serial usando Serial.printf. Isto evita imprimir na ISR e desacopla a lógica crítica da interrupção da saída serial mais lenta.

De seguida, o código lê o estado do botão de pressão do encoder, que está ligado ao PCF8574.

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

pcf8574.digitalRead(P5, true); lê o nível lógico atual do pino P5. Passar true como segundo argumento normalmente indica à biblioteca para fazer uma leitura I2C imediata em vez de usar um valor em cache, garantindo uma leitura fresca a cada vez.

Como o pino está configurado com pull-up, um botão não pressionado lê alto (não zero), e um botão pressionado puxa a linha para terra e lê baixo (zero). Portanto, a condição if (!button) verifica o estado pressionado. Quando o botão é detectado como pressionado, o código imprime "BTN pressed" e espera 500 milissegundos. Este atraso atua como um debounce simples e evita que a mensagem seja impressa continuamente enquanto o botão está pressionado.

Finalmente, o loop termina com um pequeno atraso.

delay(1);

Este pequeno atraso dá ao scheduler a oportunidade de tratar tarefas em segundo plano e ajuda a evitar um loop muito apertado e intensivo para a CPU. Também garante que os subsistemas I2C e serial tenham algum tempo para respirar.

Saída no Monitor Serial

A captura de ecrã seguinte mostra o que deve ver no Monitor Serial quando roda o anel do encoder ou pressiona o botão do encoder:

Exemplo de Código: Ecrã e Ecrã Tátil

O sketch seguinte mostra como usar o ecrã e o touchpad. Define o fundo do ecrã a vermelho, imprime o texto “Makerguides” no centro e desenha círculos pretos onde quer que o ecrã seja tocado:

Dê uma olhada rápida no código completo abaixo antes de discutirmos os detalhes:

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

Importações

Este sketch começa por incluir as bibliotecas necessárias para I2C, gráficos, expansor de I/O e controlador de toque.

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

Wire.h é a mesma biblioteca I2C que usou antes com o PCF8574. Arduino_GFX_Library.h fornece a abstração gráfica para conduzir o painel RGB e o driver de ecrã ST7701. PCF8574.h novamente trata do expansor de I/O no barramento I2C. Adafruit_CST8XX.h é o driver para o controlador de toque capacitivo CST8xx, que está ligado via I2C e será usado para ler as coordenadas de toque do ecrã redondo.

Constantes e Configuração I2C

O código define as atribuições de pinos para o barramento I2C, controlo da luz de fundo e o endereço I2C do controlador de toque. Isto segue o mesmo padrão do exemplo anterior onde os pinos SDA e SCL foram configurados explicitamente.

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

#define I2C_TOUCH_ADDR 0x15

I2C_SDA_PIN e I2C_SCL_PIN selecionam os pinos ESP32 usados para o barramento I2C partilhado que conecta tanto o PCF8574 como o controlador de toque CST8xx. BKL_PIN é um pino com capacidade PWM que controla a luz de fundo do LCD. I2C_TOUCH_ADDR especifica o endereço I2C de 7 bits do controlador de toque para que o driver possa comunicar com ele.

Objetos Globais: PCF8574, Controlador de Toque, Barramento RGB e Painel

Os objetos globais configuram o acesso ao expansor de I/O, ao controlador de toque e ao ecrã.

pcf8574(0x21) é o mesmo do exemplo anterior: cria uma instância ligada ao endereço I2C 0x21. Este chip controla várias linhas de controlo como energia do LCD, reset do LCD e reset e interrupção do toque.

tsPanel é uma instância do driver Adafruit CST8xx, criada com o construtor padrão e depois inicializada com begin() usando a interface Wire partilhada e o endereço do toque.

PCF8574 pcf8574(0x21);

Adafruit_CST8XX tsPanel = Adafruit_CST8XX();

Painel RGB

O próximo objeto configura o barramento de dados RGB entre o ESP32-S3 e o driver de ecrã ST7701. Lista todos os pinos de temporização e cor usados para a interface 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 o mapeamento exato entre os GPIOs do ESP32 e os sinais do painel RGB. Os três primeiros pinos são usados como linhas de controlo para o barramento do painel (seleção do chip, relógio e dados para a interface de configuração). DE, VSYNC, HSYNC, e PCLK são os sinais de temporização, semelhantes aos de uma interface clássica de ecrã RGB. Os grupos restantes mapeiam os cinco bits vermelhos, seis verdes e cinco azuis do barramento de cor de 16 bits (formato 5-6-5) para pinos GPIO específicos.

O objeto gráfico de alto nível gfx representa o LCD controlado pelo ST7701 que é conduzido via este barramento 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
);

O primeiro argumento é o barramento RGB definido anteriormente. O pino de reset é definido como GFX_NOT_DEFINED porque o painel é resetado usando pinos PCF8574 em vez de um GPIO direto do ESP32. O parâmetro de rotação é zero, significando a orientação padrão. A flag false IPS é uma opção do painel, e 480, 480 definem a resolução do ecrã.

O ponteiro st7701_type5_init_operations e o tamanho fornecem a sequência de inicialização de baixo nível para o driver ST7701, que a biblioteca enviará ao painel na inicialização. A flag true BGR indica ao driver para tratar os dados de cor como BGR em vez de RGB.

Finalmente, os valores de porch horizontal e vertical e pulso definem a temporização RGB, semelhantes aos parâmetros encontrados em temporização de vídeo clássica: front porch, largura do pulso e back porch para HSYNC e VSYNC.

Inicialização da Luz de Fundo

A luz de fundo é controlada separadamente da eletrónica do painel. A função initBacklight configura o pino da luz de fundo e define um brilho inicial usando PWM.

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

pinMode(BKL_PIN, OUTPUT); configura o pino da luz de fundo como saída. analogWrite(BKL_PIN, 200); ativa PWM nesse pino com um ciclo de trabalho correspondente a um nível de brilho de 200 numa escala típica de 0–255. Isto permite ajustar o brilho da luz de fundo mais tarde escrevendo um valor diferente sem alterar o hardware.

Inicialização dos Pinos e Periféricos

A função initPins configura o barramento I2C partilhado, configura os pinos PCF8574, executa sequências de reset e energia para o LCD e controlador de toque, e inicializa o driver de toque. Tem um papel semelhante ao initPins no exemplo anterior, mas com mais periféricos envolvidos.

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); é idêntico em conceito ao sketch anterior: inicia o periférico I2C com pinos SDA e SCL personalizados. As quatro chamadas pcf8574.pinMode configuram linhas específicas do PCF8574 como saídas: P0 para reset do toque, P2 para linha de interrupção do toque, P3 para ativação da energia do LCD, e P4 para reset do LCD. Como antes, pcf8574.begin() inicializa a comunicação com o expansor e imprime uma mensagem de erro se falhar.

O bloco seguinte executa a sequência de energia e reset para o LCD. Estes atrasos precisos e padrões de comutação são frequentemente exigidos pelos controladores LCD e estão codificados segundo o datasheet do ecrã.

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

Definir P3 em alto ativa a linha de energia do LCD. Após um curto atraso para estabilizar a alimentação, P4 é alternado para gerar um pulso de reset adequado para o painel. A sequência alto, baixo, alto com atrasos específicos garante que o controlador ST7701 inicia num estado conhecido.

A secção do touchpad executa uma sequência de reset muito semelhante, mas nas linhas de reset e interrupção do controlador de toque.

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

Aqui, P0 é alternado para resetar o controlador CST8xx, e P2 é colocado em alto para configurar a linha de interrupção num estado definido. Estas ações garantem que o controlador de toque está pronto antes que o driver tente comunicar via I2C.

Finalmente, o controlador de toque é inicializado usando o driver Adafruit.

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

tsPanel.begin(&Wire, I2C_TOUCH_ADDR) indica ao objeto CST8xx para usar a instância global Wire e conecta-o ao endereço I2C definido. Se esta chamada falhar, o sketch imprime uma mensagem de diagnóstico.

Inicialização do LCD

A função initLCD prepara o contexto gráfico e configura um estado básico de desenho.

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

gfx->begin(); inicializa o painel ST7701 via o barramento RGB e executa as operações de inicialização fornecidas. Após esta chamada, o ecrã está pronto para aceitar comandos de desenho.

gfx->fillScreen(RED); limpa todo o ecrã com um fundo vermelho. gfx->setTextSize(5); define um fator de escala de texto relativamente grande para que o texto seja facilmente legível no ecrã redondo 480×480. gfx->setTextColor(WHITE); define a cor do texto em primeiro plano para operações subsequentes de desenho de texto.

drawOnLCD

A função drawOnLCD encapsula uma ação simples de desenho, colocando um rótulo de texto na região central do ecrã.

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

gfx->setCursor(90, 210); move o cursor de texto para a posição (90, 210) em coordenadas de pixels. Com um ecrã 480×480, isto é aproximadamente central dependendo do tamanho da fonte. gfx->print("Makerguides"); depois renderiza a string de texto com o tamanho e cor configurados anteriormente no ecrã.

Setup

A função setup fornece novamente uma sequência de inicialização, semelhante ao sketch anterior que configurou serial, pinos e encoder.

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

Serial.begin(115200); inicia a comunicação serial para depuração. initPins(); configura o barramento I2C, pinos PCF8574, e executa as sequências de reset do LCD e toque conforme descrito acima. initBacklight(); ativa e define o brilho da luz de fundo para que o conteúdo no ecrã seja visível. initLCD(); inicializa o driver gráfico e pinta o fundo vermelho, e drawOnLCD(); coloca a string inicial “Makerguides” no ecrã.

Loop

O loop principal verifica constantemente se o painel tátil está a ser tocado. Quando um toque é detetado, lê as coordenadas e desenha um pequeno círculo preto nessa posição.

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 o driver CST8xx para ver se algum ponto de toque está ativo. Se a função retornar verdadeiro, pelo menos um dedo está no ecrã. tsPanel.getPoint(0); obtém o primeiro ponto de toque como uma estrutura CST_TS_Point contendo as coordenadas x e y. Estas coordenadas são impressas no monitor serial para depuração com Serial.printf, semelhante a como imprimiu o contador do encoder e o estado do botão anteriormente.

gfx->fillCircle(p.x, p.y, 10, BLACK); desenha um círculo preenchido com raio de 10 pixels em preto na localização do toque. A chamada delay(10); fornece uma pausa curta para limitar a taxa de atualização e evitar sobrecarregar o barramento I2C e o driver gráfico com muitas operações por segundo.

Exemplo de Código: Ecrã, Toque e Encoder

Este último sketch junta os conceitos dos exemplos anteriores: configuração I2C e controlo PCF8574, ecrã RGB e tratamento de toque, e um encoder rotativo baseado em interrupção.

O código permite controlar o brilho do ecrã rodando o anel do encoder e também mostrar o valor atual do brilho (0…255) no centro do ecrã. O botão do encoder ativa uma mudança de cor do ecrã para azul e os eventos de toque continuam a ser registados como pontos pretos no ecrã.

Dê uma olhada rápida no código completo abaixo e depois discutiremos os seus detalhes.

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

Importações

Este sketch combina tudo dos exemplos anteriores: periféricos I2C, ecrã RGB, toque, e um encoder rotativo baseado em interrupção, e inclui as bibliotecas necessárias:

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

Constantes e Estado Global

A secção seguinte define os pinos para I2C, luz de fundo, sinais do encoder e o endereço do controlador de toque. Também introduz várias variáveis globais volatile que guardam o brilho atual e o estado do encoder, semelhante ao exemplo anterior do contador do encoder:

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

#define ENCODER_CLK 42
#define ENCODER_DT 4

#define I2C_TOUCH_ADDR 0x15

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

I2C_SDA_PIN e I2C_SCL_PIN configuram o barramento I2C partilhado, como antes. BKL_PIN é o pino PWM usado para controlar a luz de fundo do LCD. ENCODER_CLK e ENCODER_DT são os sinais quadratura do encoder rotativo, idênticos em função ao sketch anterior do encoder. I2C_TOUCH_ADDR guarda o endereço do controlador de toque CST8xx.

brightness armazena o valor atual do brilho da luz de fundo. É declarado volatile porque é modificado dentro de uma rotina de serviço de interrupção. encState e oldState são usados para detetar transições na linha CLK do encoder, e brightnessChanged é uma flag que informa o loop principal que um novo nível de brilho está disponível.

Objetos PCF8574, Toque e Ecrã

Os objetos globais para o expansor de I/O, painel de toque e barramento do ecrã são os mesmos do sketch anterior de ecrã e toque. Definem como o ESP32 interage com os chips externos e o painel 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 */
);

O objeto pcf8574 está ligado ao endereço 0x21 e é responsável pela energia do LCD, reset e botão de pressão do encoder. O objeto tsPanel encapsula o controlador de toque CST8xx. O ponteiro bus define o mapeamento dos sinais RGB e de temporização para os GPIOs do ESP32 exatamente como antes, usando a sua tabela de pinos R0–R4, G0–G5 e B0–B4.

O objeto gráfico de alto nível para o painel RGB ST7701 é então criado sobre este barramento.

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, isto encapsula a sequência de inicialização de baixo nível do ST7701, parâmetros de temporização e formato de cor. O pino de reset é marcado como indefinido porque o reset é conduzido via os pinos PCF8574 durante initPins.

Inicialização da Luz de Fundo

A função de inicialização da luz de fundo aplica o valor atual de brightness ao pino PWM da luz de fundo:

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

pinMode(BKL_PIN, OUTPUT); configura o pino da luz de fundo como saída digital. analogWrite(BKL_PIN, brightness); inicia a saída PWM neste pino usando o valor brightness como ciclo de trabalho.

Inicialização dos Pinos e Periféricos

A função initPins é uma versão estendida da que viu anteriormente. Configura o I2C, os pinos PCF8574, a sequência de energia e reset do LCD, a sequência de reset do toque e também configura o pino PCF8574 que lê o botão de pressão do 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 o barramento I2C com os pinos especificados, como nos sketches anteriores. P0, P2, P3 e P4 são configurados como saídas para controlar reset do toque, linha de interrupção do toque, energia do LCD e reset do LCD. P5 é configurado como INPUT_PULLUP porque está ligado ao interruptor do encoder. Isto espelha como configurou P5 como entrada do botão do encoder no exemplo original só do encoder.

Segue-se a sequência de temporização de energia e reset do LCD, idêntica em estrutura ao código anterior do ecrã.

  // 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 ativa a linha de energia do LCD, depois P4 é alternado com atrasos específicos para realizar um reset hardware do controlador ST7701.

A sequência de reset e configuração do touchpad é também igual à anterior.

  // 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 é pulsado para resetar o controlador CST8xx e P2 é colocado em alto para estabelecer um estado definido para a linha de interrupção do toque.

O controlador de toque é então inicializado. tsPanel.begin(&Wire, I2C_TOUCH_ADDR) liga o driver CST8xx ao barramento I2C partilhado e ao endereço especificado, e imprime uma mensagem de diagnóstico se o dispositivo não for encontrado:

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

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

O manipulador de interrupção do encoder é semelhante à função encoder_irq anterior, mas em vez de manter um contador de posição, atualiza a variável brightness em passos 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 garante que a ISR está colocada na RAM de instruções para execução rápida no ESP32, como discutido anteriormente. Dentro da função, encState é definido para o nível lógico atual do pino CLK do encoder usando digitalRead(ENCODER_CLK). A condição if (encState != oldState) assegura que o código só reage quando o sinal CLK realmente muda, prevenindo múltiplas atualizações no mesmo nível.

A direção da rotação é novamente determinada comparando o sinal DT com o estado atual do CLK. Neste sketch, a condicional é invertida em relação ao exemplo anterior para dar uma sensação natural de aumento e diminuição do brilho.

A linha seguinte subtrai 5 ou adiciona 5 à variável brightness com base na fase relativa dos sinais quadratura. A rotação positiva aumenta o brilho, a rotação negativa diminui.

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

E a linha seguinte assegura que o brilho se mantém dentro de um limite inferior definido de 5 (evitando um ecrã completamente apagado) e limite superior de 255 (o valor máximo PWM para brilho total):

brightness = constrain(brightness, 5, 255);

oldState é atualizado para o novo estado do CLK, e brightnessChanged é definido como verdadeiro para notificar o loop principal que a luz de fundo e o ecrã devem ser atualizados. Como antes, todo o trabalho pesado como I/O serial e gráfico é mantido fora da ISR e tratado no loop principal.

Inicialização do Encoder

A função initEncoder configura os pinos do encoder e anexa a interrupção à linha CLK. Este é efetivamente o mesmo padrão do seu sketch original do encoder.

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

Ambos os canais do encoder são configurados como entradas com resistores pull-up internos. A interrupção é anexada ao pino CLK usando attachInterrupt, com modo CHANGE para disparar em ambas as bordas ascendente e descendente. Em cada mudança, o manipulador encoder_irq é invocado, atualizando o brilho.

Inicialização do LCD e Exibição do Brilho

A função de inicialização do LCD levanta o painel ST7701 e configura as definições de texto. É semelhante ao initLCD anterior, mas usa um tamanho de texto maior para mostrar o valor do brilho de forma destacada:

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

gfx->begin(); inicializa o controlador do ecrã e envia a sequência de configuração. gfx->fillScreen(RED); define um fundo vermelho. gfx->setTextSize(10); escolhe um fator de escala grande para que o valor numérico do brilho se destaque claramente. gfx->setTextColor(WHITE); configura o branco como cor do texto para renderização.

A função updateBrightness liga o valor lógico do brilho tanto à luz de fundo física como à exibição no ecrã.

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

analogWrite(BKL_PIN, brightness); atualiza o ciclo de trabalho PWM, alterando efetivamente a intensidade da luz LED de fundo. O ecrã é então limpo novamente a vermelho usando gfx->fillScreen(RED);.

O cursor é colocado nas coordenadas (150, 200), e gfx->printf("%3d", brightness); imprime o brilho como um número de três dígitos.

Setup

A função setup inicializa todos os subsistemas: serial, pinos I2C e chips externos, o encoder, a luz de fundo e o LCD, e finalmente desenha o brilho inicial no ecrã.

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

Serial.begin(115200); inicia a UART para saída de depuração. initPins(); prepara o barramento I2C, PCF8574, LCD e controlador de toque conforme discutido anteriormente. initEncoder(); ativa a interface do encoder baseada em interrupção. initBacklight(); aplica o valor inicial brightness ao pino da luz de fundo. initLCD(); levanta o contexto gráfico, e updateBrightness(); sincroniza imediatamente a exibição no ecrã e o PWM da luz de fundo com o valor atual do brilho.

Loop

O loop principal verifica periodicamente o botão do encoder, a entrada tátil e a flag de atualização do brilho. Reage a cada um destes eventos usando os 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);
}

O primeiro bloco lê o botão de pressão do encoder via PCF8574. pcf8574.digitalRead(P5, true); lê o pino P5 com uma transação I2C imediata. Como P5 está configurado como INPUT_PULLUP, o botão lê alto quando solto e baixo quando pressionado. A condição if (!button) deteta o estado pressionado. Ao pressionar, o sketch imprime uma mensagem no monitor serial e preenche o ecrã com azul, fornecendo uma indicação visual simples de que o botão foi pressionado.

O bloco seguinte trata da entrada tátil capacitiva, reutilizando a mesma lógica do exemplo anterior de desenho no LCD. Se tsPanel.touched() retornar verdadeiro, pelo menos um ponto de toque está ativo. A função tsPanel.getPoint(0); obtém o primeiro ponto de toque, que é então impresso no monitor serial. gfx->fillCircle(p.x, p.y, 10, BLACK); desenha um pequeno círculo preto nas coordenadas do toque, permitindo ao utilizador desenhar sobre o conteúdo atual do ecrã.

O terceiro bloco verifica se a ISR do encoder atualizou a variável brightness. Se brightnessChanged for verdadeiro, o loop principal limpa a flag e chama updateBrightness();. Isto aplica o novo brilho à luz de fundo e redesenha o valor numérico no ecrã. Rodar o encoder rapidamente gerará múltiplas interrupções e definirá brightnessChanged repetidamente, mas o loop garante que as atualizações de brilho são tratadas no contexto principal onde é seguro fazer operações seriais e gráficas.

O último delay(100); introduz uma pausa curta para limitar a frequência do loop e suavizar a interação do utilizador sem sobrecarregar a CPU ou o I2C.

Conclusões

Este tutorial forneceu exemplos de código para começar a usar o CrowPanel 2.1inch-HMI ESP32 Rotary Display. Veja o Wiki da Elecrow para informações adicionais e para mais exemplos de código veja o github repo.

Note que precisa instalar bibliotecas antigas e uma versão antiga do core ESP32 (2.0.14) para fazer o código funcionar. Também alguns exemplos de código lá usam a biblioteca LVGL, que evitei aqui para manter o código simples.

Se procura um módulo de ecrã semelhante com anel encoder rotativo, dê uma olhada no CrowPanel 1.28inch-HMI ESP32 Rotary Display ou no Matouch 1.28″ ToolSet_Controller. Se precisar apenas de um ecrã redondo (sem o anel encoder rotativo), o tutorial Digital Clock on CrowPanel 1.28″ Round Display pode ser útil.

Finalmente, 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 deixá-la na secção de comentários.

Boas experiências 😉