Pular para o conteúdo

Lopaka Basics 03: agora em cores ​

Um módulo TFT colorido de 320×240 mostrando um painel Living room às 07:45 com cartões laranja, azul e verde para TEMP 23C, HUMID 48% e AIR OK, e uma imagem de sol e nuvem ao lado de Outside 18C, sunny

A mesma ideia dos dois primeiros episódios, agora em cores: um painel de sala de estar em um display colorido, montado em poucos minutos.

Neste episódio, você vai descobrir como uma tela colorida faz as cores dela, o que RGB565 significa e por que uma única imagem em tela cheia não cabe em um Arduino UNO. Depois você vai montar o painel no Lopaka com cartões arredondados, texto colorido, ícones tingidos e uma imagenzinha colorida, e executar ele em um ESP32.

O que você vai construir: um painel colorido de 320×240 com um título e o horário, três cartões arredondados para temperatura, umidade e qualidade do ar e uma linha sobre o clima lá fora. Do que você precisa: um navegador e o simulador online do episódio 1. Uma placa real é opcional.

Como uma tela colorida funciona ​

Um pixel é três luzinhas ​

No OLED dos dois últimos episódios, um pixel está aceso ou apagado. Em um display colorido, cada pixel é feito de três luzinhas: vermelha, verde e azul. Cada uma pode brilhar com qualquer intensidade, e a uma distância normal de visualização seu olho mistura elas em uma cor.

Misture elas, e você obtém qualquer cor. Vermelho e verde juntos fazem amarelo. As três na potência máxima fazem branco. As três apagadas é preto.

Um pixel é três luzinhas: um pixel do painel ao lado de círculos vermelho, verde e azul sobrepostos; vermelho e verde fazem amarelo, as três fazem branco

RGB565: uma cor em dois bytes ​

Seu computador armazena uma cor em 24 bits: 8 para vermelho, 8 para verde e 8 para azul, cerca de 16,7 milhões de cores. Essa é a cor que você digita como #FF8200 em uma página web ou no Lopaka: dois dígitos hexadecimais por luz.

Chips pequenos e seus displays geralmente usam 16 bits, em um formato chamado RGB565:

  • 5 bits para vermelho,
  • 6 bits para verde,
  • 5 bits para azul.

O verde recebe o bit extra porque nossos olhos são mais sensíveis a ele. Isso dá 65.536 cores, e cada pixel ocupa exatamente dois bytes.

Cor no computador: 24 bits por pixel, 8 de vermelho, 8 de verde e 8 de azul para 16,7 milhões de cores; cor no microcontrolador: RGB565, 16 bits divididos em 5, 6 e 5, porque nossos olhos são mais sensíveis ao verde; 65.536 cores, 2 bytes por pixel

Por que uma imagem não cabe em um UNO ​

Agora a matemática. Uma tela de 320×240 tem 320 × 240 = 76.800 pixels. A dois bytes cada, uma imagem em tela cheia ocupa 153.600 bytes, cerca de 150 KB.

Um Arduino UNO tem 32 KB de flash para o programa inteiro. Uma imagem em tela cheia é quase cinco vezes isso. Então, em telas coloridas, você desenha com formas e texto e mantém as imagens pequenas.

A conta da memória: 320 × 240 = 76.800 pixels, × 2 bytes = 153.600 bytes; uma imagem de cerca de 150 KB é quase cinco vezes os 32 KB de flash de um Arduino UNO

Etapa 1: um novo projeto colorido ​

Clique em + New Project e abra a lista Platform. Para este episódio, escolha AdafruitGFX Color: Adafruit GFX é uma das bibliotecas gráficas mais populares para displays coloridos. O Lopaka descreve ela como "the shared drawing layer under most Adafruit display drivers": você combina ela com o driver do seu painel exato, como Adafruit_ILI9341, as cores são empacotadas como RGB565 e o desenho vai direto para o painel.

Depois preencha o restante da caixa de diálogo:

  • Title: "Color Weather".
  • Screen Size: 320×240.
  • Background Color: #0F1B2D, um azul escuro.

Clique em Salvar.

A caixa de diálogo Create New Project com Platform AdafruitGFX Color, o título Color Weather, Screen Size 320×240 e Background Color #0F1B2D

O Lopaka converte cada cor para RGB565 ​

O painel de código já tem uma linha:

cpp
tft.fillScreen(0x8C5);

fillScreen pinta o display inteiro de uma cor, e 0x8C5 é o azul escuro em RGB565. O Lopaka faz a conversão para cada cor que você escolhe. À mão, funciona assim: mantenha os 5 bits superiores do vermelho, os 6 bits superiores do verde e os 5 bits superiores do azul, e escreva eles um depois do outro.

HexBits superiores
red0x0F00001
green0x1B000110
blue0x2D00101

Juntos, isso é 0000100011000101, ou 0x08C5. O Lopaka escreve ele sem o zero à esquerda.

Etapa 2: três cartões arredondados ​

Escolha a ferramenta retângulo e arraste um cartão na tela de desenho. No inspetor, defina R como 12 para arredondar os cantos, ative Filled e digite #FF8200 em Fill color para laranja. Uma linha aparece no código:

cpp
tft.fillRoundRect(10, 56, 96, 140, 12, 0xFC00);

Os números são x, y, largura, altura, raio do canto e cor. Uma forma é uma linha de código, por maior que seja: este cartão abrange 96 × 140 pixels, e a linha que desenha ele ocupa poucos bytes de flash.

Repita para os outros dois cartões: azul #2D7FF9 e verde #22B573. Os três têm 96 pixels de largura e 140 de altura, em y 56, e começam em x 10, 112 e 214. Isso deixa uma margem de 10 pixels dos dois lados da tela e 6 pixels entre os cartões: 10 + 3 × 96 + 2 × 6 + 10 = 320.

Três cartões arredondados, laranja, azul e verde, na tela azul escura; o inspetor mostra o cartão verde em X 214, Y 56, W 96, H 140, R 12, Filled e Fill color #22B573, e o código tem três linhas fillRoundRect

Etapa 3: texto em cores ​

O texto funciona como nos dois primeiros episódios, e agora ele pode ter qualquer cor. Escolha a ferramenta de texto (Aa): uma nova camada de texto aparece na tela de desenho. Digite o texto no inspetor, escolha uma fonte e defina Fill color. Um texto novo começa na cor que você usou por último, então confira a cor toda vez.

O título e o horário ​

  • Living room em FreeSansBold12pt7b, branco #FFFFFF, no canto superior esquerdo.
  • 07:45 em FreeSans12pt7b, cinza-azulado claro #9FB3C8, no canto superior direito.
cpp
tft.setTextColor(0xFFFF);
tft.setTextWrap(false);
tft.setFont(&FreeSansBold12pt7b);
tft.setCursor(12, 35);
tft.print("Living room");

setTextColor define a cor para cada texto depois dele, então o Lopaka só escreve ele de novo quando a cor muda. setTextWrap(false) mantém um texto longo em uma linha, em vez de quebrar ele na borda da tela.

Com fontes FreeSans, o y de setCursor é a linha de base: a última linha de pixels das letras. No inspetor, o título tem Y 36, a linha sob aquela linha (as réguas contam as linhas entre os pixels, como no episódio 1), então o código diz 35.

Um rótulo pequeno e um número grande em cada cartão ​

Cada cartão recebe um rótulo pequeno na fonte clássica e um número grande em uma FreeSans em negrito:

  • Os rótulos TEMP, HUMID e AIR usam Adafruit 5x7, a fonte clássica integrada à biblioteca, com Size definido como 2.
  • Os números 23C, 48% e OK usam FreeSansBold18pt7b.

Os seis textos são brancos. Coloque cada rótulo perto do topo do cartão dele e cada número perto da parte inferior.

O rótulo TEMP selecionado: fonte Adafruit 5x7, Size 2, X 20 e Y 86 no inspetor; o código mostra setTextSize(2), setFont() e setCursor(20, 72)

A fonte clássica pode ser ampliada duas ou três vezes: Size 2 transforma cada pixel de uma letra em um bloco de 2×2. O código para o rótulo e o número:

cpp
tft.setTextSize(2);
tft.setFont();
tft.setCursor(20, 72);
tft.print("TEMP");
tft.setTextSize(1);
tft.setFont(&FreeSansBold18pt7b);
tft.setCursor(20, 179);
tft.print("23C");

setFont() sem nada entre os parênteses volta para a fonte clássica. A fonte clássica conta o y de outro jeito: setCursor recebe o canto superior esquerdo do texto, e não a linha de base. As letras do rótulo têm 7 pixels de altura, 14 no tamanho 2, então o 86 do inspetor vira 86 − 14 = 72 no código. O Lopaka faz essa conversão para você. Antes do número FreeSans, ele redefine o tamanho como 1, para a fonte grande não ser dobrada.

O texto diz 23C, e não 23°C: as fontes FreeSans contêm só os caracteres ASCII básicos, e o sinal de grau não é um deles.

Fontes custam flash só quando você usa elas ​

FreeSans e as outras fontes da lista vêm com a biblioteca Adafruit GFX. Cada fonte é um arquivo separado, e ela ocupa flash só quando a sua tela usa ela. O Lopaka adiciona um #include para cada fonte que o design usa, aqui três:

cpp
#include "FreeSans12pt7b.h"
#include "FreeSansBold12pt7b.h"
#include "FreeSansBold18pt7b.h"

Etapa 4: ícones tingidos ​

Abra a guia Imagens, busque um ícone e clique nele para adicionar ele à tela. Adicione três:

  • um termômetro (busque "thermo"),
  • uma gota (busque "drop"),
  • uma folha (busque "leaf").

Volte para Camadas e coloque cada ícone sob o rótulo do cartão dele.

Ícones da biblioteca têm uma cor, então você pode tingir eles para combinar com o seu design. Selecione cada ícone e defina o Fill color dele como o azul escuro do fundo, #0F1B2D. Nos cartões claros, os ícones agora parecem recortados.

O ícone do termômetro selecionado sob o rótulo TEMP, com Fill color #0F1B2D no inspetor; os três ícones são azul escuro nos cartões laranja, azul e verde

No código, um ícone de uma cor vira drawBitmap:

cpp
tft.drawBitmap(20, 104, image_streamline_weather_temperature_thermometer__weather_png_bits, 21, 21, 0x8C5);

Os argumentos são a posição, o array de bits, a largura e a altura e a cor. O array armazena um bit por pixel, como no OLED do episódio 2: o termômetro tem 21×21 pixels, 3 bytes por linha, 63 bytes no total. A cor é um número separado no código, então o mesmo ícone pode ser desenhado em qualquer cor sem uma segunda cópia.

Etapa 5: uma imagem colorida ​

Envie ela como RGB ​

Imagens também podem ser totalmente coloridas. Na guia Imagens, clique em Upload e escolha uma imagem, do mesmo jeito que no episódio 2. Desta vez, mude o conversor de Monochrome para RGB.

A caixa de diálogo de importação de imagem com a imagem do sol e da nuvem, o conversor mudado para RGB e 115 200 bytes no topo, para Dimensions 240 por 240

O contador no topo mostra o custo. No tamanho completo dela, de 240×240 pixels, esta imagem ocuparia 240 × 240 × 2 = 115.200 bytes, mais de três vezes a flash inteira de um UNO.

Encolha ela para 40×40 ​

Defina Dimensions como W 40 e H 40. Agora a imagem ocupa 40 × 40 × 2 = 3.200 bytes, cerca de três kilobytes. Esse é o ponto ideal para imagens coloridas: detalhes pequenos e coloridos. Clique em Import.

No código, a imagem vira um array de 1.600 números RGB565, um por pixel, e uma linha que desenha ela:

cpp
tft.drawRGBBitmap(12, 197, image_sun_cloud_pixels, 40, 40);

Diferente de drawBitmap, esta chamada não recebe uma cor: cada pixel traz a sua própria.

A linha de baixo ​

Solte a imagem no canto inferior esquerdo e adicione mais uma linha de texto ao lado dela: Outside: 18C, sunny em Adafruit 5x7, Size 2, no mesmo cinza-azulado #9FB3C8 do horário.

Etapa 6: pegue o código ​

Um cartão, três dialetos ​

Cada biblioteca fala seu próprio dialeto. O mesmo cartão arredondado é drawRBox no U8g2, fillRoundRect no Adafruit GFX e um objeto estilizado no LVGL. O Lopaka escreve o da plataforma que você escolheu.

Um cartão, três dialetos: u8g2.drawRBox(10, 56, 96, 140, 12) no U8g2, tft.fillRoundRect(10, 56, 96, 140, 12, 0xFC00) no Adafruit GFX e um objeto LVGL com posição, tamanho e um raio de canto de 12

Copie e baixe os arquivos de fonte ​

Clique em Copiar acima do código. Sob os interruptores ao lado do código, o Lopaka lista as Fonts e Images que o design usa. Clique em cada um dos três arquivos de fonte para baixar ele e coloque os arquivos ao lado do seu sketch. A imagem e os ícones já estão dentro do código copiado como arrays, então você não precisa dos arquivos de imagem.

O painel finalizado no editor; sob os interruptores do código, a lista Fonts mostra FreeSansBold12pt7b.h, FreeSans12pt7b.h e FreeSansBold18pt7b.h, e a lista Images mostra os quatro arquivos de imagem

Se você esquecer os arquivos de fonte, o compilador para com fatal error: FreeSans12pt7b.h: No such file or directory.

O Lopaka gera este código para o painel. O array da imagem tem 1.600 números, então ele está encurtado aqui; Copiar entrega ele inteiro para você.

cpp
// [BEGIN lopaka generated]
#include "FreeSans12pt7b.h"
#include "FreeSansBold12pt7b.h"
#include "FreeSansBold18pt7b.h"

static const unsigned char PROGMEM image_drop_bits[] = {0x10,0x38,0x7c,0x7c,0xfe,0xde,0xde,0xde,0x7c,0x38};

static const unsigned char PROGMEM image_streamline_ecology_leaf_bug__ecology_png_bits[] = {0x00,0x00,0x30,0x00,0x00,0x48,0x00,0x00,0x88,0x00,0x3f,0x08,0x03,0xe0,0x08,0x0c,0x00,0x08,0x10,0xe0,0x08,0x21,0x70,0x88,0x41,0xd1,0x08,0x40,0xe2,0x08,0x80,0x0c,0x08,0x80,0x30,0x10,0x80,0xc0,0x10,0x83,0x00,0x20,0x84,0x00,0x20,0x88,0x00,0x40,0x50,0x00,0x80,0x20,0x03,0x00,0x50,0x0c,0x00,0x8f,0xf0,0x00,0x80,0x00,0x00};

static const unsigned char PROGMEM image_streamline_weather_temperature_thermometer__weather_png_bits[] = {0x03,0x00,0x00,0x04,0x80,0x00,0x04,0x80,0x00,0x04,0x87,0x80,0x04,0x80,0x00,0x04,0x80,0x00,0x04,0x80,0x00,0x04,0x87,0x80,0x04,0x80,0x00,0x04,0x80,0x00,0x07,0x80,0x00,0x07,0x87,0x80,0x07,0x80,0x00,0x07,0x80,0x00,0x0f,0xc0,0x00,0x1f,0x60,0x00,0x1f,0xa0,0x00,0x1f,0xa0,0x00,0x1f,0xe0,0x00,0x0f,0xc0,0x00,0x07,0x80,0x00};

static const uint16_t PROGMEM image_sun_cloud_pixels[] = {/* 1,600 RGB565 values, shortened here */};

void drawScreen_1(void) {
    tft.fillScreen(0x8C5);
    // rect 1
    tft.fillRoundRect(10, 56, 96, 140, 12, 0xFC00);
    // rect 2
    tft.fillRoundRect(112, 56, 96, 140, 12, 0x2BFF);
    // rect 3
    tft.fillRoundRect(214, 56, 96, 140, 12, 0x25AE);
    // string 4
    tft.setTextColor(0xFFFF);
    tft.setTextWrap(false);
    tft.setFont(&FreeSansBold12pt7b);
    tft.setCursor(12, 35);
    tft.print("Living room");
    // string 5
    tft.setTextColor(0x9D99);
    tft.setFont(&FreeSans12pt7b);
    tft.setCursor(246, 35);
    tft.print("07:45");
    // string 6
    tft.setTextColor(0xFFFF);
    tft.setTextSize(2);
    tft.setFont();
    tft.setCursor(20, 72);
    tft.print("TEMP");
    // string 7
    tft.setTextSize(1);
    tft.setFont(&FreeSansBold18pt7b);
    tft.setCursor(20, 179);
    tft.print("23C");
    // string 8
    tft.setTextSize(2);
    tft.setFont();
    tft.setCursor(122, 72);
    tft.print("HUMID");
    // string 9
    tft.setTextSize(1);
    tft.setFont(&FreeSansBold18pt7b);
    tft.setCursor(122, 179);
    tft.print("48%");
    // string 10
    tft.setTextSize(2);
    tft.setFont();
    tft.setCursor(224, 72);
    tft.print("AIR");
    // string 11
    tft.setTextSize(1);
    tft.setFont(&FreeSansBold18pt7b);
    tft.setCursor(224, 179);
    tft.print("OK");
    // streamline-weather-temperature-thermometer--weather.png
    tft.drawBitmap(20, 104, image_streamline_weather_temperature_thermometer__weather_png_bits, 21, 21, 0x8C5);
    // drop
    tft.drawBitmap(122, 104, image_drop_bits, 7, 10, 0x8C5);
    // streamline-ecology-leaf-bug--ecology.png
    tft.drawBitmap(224, 104, image_streamline_ecology_leaf_bug__ecology_png_bits, 21, 21, 0x8C5);
    // sun_cloud
    tft.drawRGBBitmap(12, 197, image_sun_cloud_pixels, 40, 40);
    // string 16
    tft.setTextColor(0x9D99);
    tft.setTextSize(2);
    tft.setFont();
    tft.setCursor(62, 211);
    tft.print("Outside: 18C, sunny");
}
// [END lopaka generated]

Etapa 7: execute ele em um ESP32 ​

O sketch ​

Desta vez, executamos a tela em um ESP32, uma placa mais rápida, popular para telas coloridas; uma placa ESP32 típica tem 4 MB de flash. O display é um módulo ILI9341 de 320×240 em SPI.

cpp
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>

// ILI9341 320x240 color display on SPI
#define TFT_CS  15
#define TFT_DC  2
#define TFT_RST 4
Adafruit_ILI9341 tft(TFT_CS, TFT_DC, TFT_RST);

// Paste the Lopaka code here

void setup() {
  tft.begin();
  tft.setRotation(1);   // landscape, 320x240
  drawScreen_1();
}

void loop() {
}
  • Adafruit_ILI9341 tft(...) cria o objeto do display e diz para ele quais pinos ele usa para chip select, data/command e reset. O código do Lopaka chama ele de tft, então mantenha esse nome.
  • O ILI9341 é um painel retrato, com 240 de largura e 320 de altura. setRotation(1) gira ele para paisagem, 320×240, o tamanho que criamos.
  • drawScreen_1() executa uma vez, em setup(). A tela mantém a imagem dela até algo desenhar por cima, então loop() fica vazio por enquanto.

Ligue ele no simulador ​

No simulador do episódio 1, comece um novo projeto ESP32 e adicione um display ILI9341. Conecte ele:

  • VCC e LED (o backlight) ao 3V3, GND ao GND,
  • CS ao 15, D/C ao 2, RST ao 4,
  • MOSI ao 23, SCK ao 18, MISO ao 19.

Adicione as bibliotecas Adafruit ILI9341 e Adafruit GFX Library, cole o sketch com o código do Lopaka no lugar dele, adicione os três arquivos de fonte ao projeto e pressione Play. O painel aparece no display simulado.

O ESP32 simulado ligado a um display ILI9341 mostrando o painel Living room com os cartões TEMP, HUMID e AIR

Sem buffer de quadro ​

Lembra do buffer de quadro do episódio 1, a imagem inteira mantida na RAM antes de ir para o display? Em 320×240 em cores, ele precisaria dos mesmos 150 KB de uma imagem em tela cheia. Bibliotecas coloridas como Adafruit GFX pulam ele e desenham direto no display: cada fillRoundRect e print envia os pixels dele por SPI na hora.

Isso mantém o uso de RAM minúsculo. Compilado para o ESP32, o sketch usa 23% do espaço de programa e 7% da RAM. O mesmo sketch compilado para um Arduino UNO:

Sketch uses 25946 bytes (80%) of program storage space. Maximum is 32256 bytes.
Global variables use 317 bytes (15%) of dynamic memory, leaving 1731 bytes for local variables. Maximum is 2048 bytes.

Só 15% da RAM do UNO. A flash é a parte apertada: 80%, e as três fontes e a imagem de 3.200 bytes são uma fatia grande dela.

Em uma placa real ​

  • Tensão do sinal. O ESP32 trabalha em 3,3 V, como o ILI9341. Muitos módulos ILI9341 de baixo custo não têm conversor de nível, então em uma placa de 5 V como o UNO confira o anúncio do seu módulo antes de ligar ele.
  • O controlador. Alguns módulos de 320×240 usam o controlador ST7789. Para esses, inclua Adafruit_ST7789.h, crie o display como Adafruit_ST7789 tft(TFT_CS, TFT_DC, TFT_RST); e inicie ele com tft.init(240, 320); em vez de tft.begin();. O código do Lopaka continua o mesmo.

Precisa de ideias? A Projects Gallery tem milhares de projetos públicos. Abra um, pressione Fork, e o Lopaka copia ele para os seus projetos, pronto para mudar.

Recapitulação ​

  1. Três luzes fazem cada cor: cada pixel mistura vermelho, verde e azul.
  2. RGB565 empacota elas em dois bytes: 5 bits de vermelho, 6 de verde, 5 de azul, e o Lopaka converte cada cor para você.
  3. Formas vencem imagens grandes: um cartão é uma linha de código, uma imagem em tela cheia tem 150 KB, então mantenha as imagens pequenas.

Próximo episódio: Live Data. Fazemos esses números mudarem sozinhos. Leia o episódio 4 ou assista no YouTube.

Pronto para testar? Abra o Lopaka e monte seu próprio painel colorido.