Pular para o conteúdo

Lopaka Basics 02: texto, fontes e ícones ​

Um módulo OLED SSD1306 mostrando um painel matinal com ícones de Wi-Fi e bateria, a data FRI 24 OCT, o horário 07:45 e uma barra de status branca com um ícone de sol e Sunny 23C

Um relógio, o clima e uma imagem da lua, tudo em uma tela do tamanho de um selo postal. Texto, fontes, ícones e imagens tornam uma telinha útil, e em um microcontrolador cada um deles custa memória.

Neste episódio, você vai descobrir o que é uma fonte em um microcontrolador, onde ela fica armazenada e quanto espaço ela ocupa. Depois você vai montar um pequeno painel matinal no Lopaka com duas telas, uma barra de status com texto invertido e sua própria imagem transformada em pontos, e executar ele em um Arduino UNO simulado.

O que você vai construir: um painel OLED de 128×64 com duas telas. Morning mostra o horário, a data, ícones de Wi-Fi e bateria e uma barra de clima. Night mostra uma imagem da lua e o horário. Do que você precisa: um navegador e o projeto do simulador do episódio 1. Uma placa real é opcional.

Uma fonte é um conjunto de imagens ​

Uma imagem por caractere ​

Um microcontrolador não sabe o que são letras. Para mostrar "07:45", a biblioteca gráfica precisa de uma imagenzinha para cada caractere: uma para 0, uma para 7, uma para : e assim por diante. Uma fonte em um microcontrolador é um conjunto de imagenzinhas, uma para cada caractere que ela consegue desenhar.

Texto maior, imagens maiores ​

Cada imagem é uma grade de pixels. Uma fonte maior significa grades maiores: o mesmo dígito 2 tem 5×9 pixels em uma fonte pequena e 12×18 em uma grande. Cada uma dessas imagens precisa ser armazenada no chip, então uma fonte maior ocupa mais memória.

Uma letra é uma imagenzinha: o dígito 7 em uma grade de pixels, as cinco imagens que formam 07:45 e o dígito 2 em três tamanhos, 5×9, 6×11 e 12×18

Dois tipos de memória ​

A flash é a estante, a RAM é a mesa ​

Um microcontrolador tem dois tipos de memória:

  • Flash guarda seu programa, suas fontes e suas imagens. Ela funciona como uma estante: tudo de que o programa pode precisar fica guardado ali, e continua ali quando a energia acaba.
  • RAM é a mesinha onde o trabalho acontece: os valores com os quais o programa está trabalhando agora.

Em um Arduino UNO, a estante guarda 32 KB e a mesa, só 2 KB.

Quanto custa uma fonte ​

Uma fonte pequena ocupa cerca de um kilobyte de flash, uma grande de dois a quatro. Medimos isso compilando um sketch para um UNO com um texto em uma única fonte e comparando com um sketch sem texto: 4x6 adicionou 1,1 KB, profont22 2,1 KB, profont29 2,5 KB e timR24 3,7 KB.

Dois tipos de memória em um Arduino UNO: 32 KB de flash, a estante para programa, fontes e imagens, com fontes de 1,1, 2,1, 2,5 e 3,7 KB ao lado do programa, e 2 KB de RAM, a mesa

Cada fonte é armazenada uma vez, por mais textos que a usem. A regra prática: duas ou três fontes por tela. Para ver quais fontes o seu design usa, procure as linhas setFont no painel de código do Lopaka: cada nome de fonte diferente ali é outra fonte na estante.

Etapa 1: um novo projeto ​

O mesmo começo do episódio 1: clique em + New Project, escolha a plataforma U8g2, mantenha o tamanho da tela em 128×64 e dê um título ao projeto, "Morning Dashboard". Clique em Salvar.

Etapa 2: texto ​

O horário em uma fonte grande ​

Escolha a ferramenta de texto (Aa), clique na tela de desenho e selecione a nova camada string 1. No inspetor, substitua o texto por 07:45, abra a lista Font e escolha profont29, uma fonte fácil de ler do outro lado da sala.

Clique no botão de alinhamento centralizar horizontalmente no topo do inspetor e defina Y como 40. O código agora mostra:

cpp
u8g2.setFont(u8g2_font_profont29_tr);
u8g2.drawStr(24, 40, "07:45");

A lista de fontes aberta no inspetor com profont29 selecionada para o texto 07:45, que fica centralizado na tela de desenho em X 24 e Y 40

Os dois números podem ser conferidos à mão. Cada caractere da profont29 tem 16 pixels de largura, então "07:45" tem 5 × 16 = 80 pixels de largura, e (128 − 80) / 2 = 24 coloca ele no meio. O 40 é a linha de base, a linha sobre a qual os dígitos ficam, como no episódio 1: os dígitos preenchem as linhas acima dela.

A data em uma fonte pequena ​

Adicione outro texto, FRI 24 OCT. Um texto novo começa na última fonte que você usou, então mude ela para a pequena 5x8. Centralize ele horizontalmente e defina Y como 11. Dez caracteres de 5 pixels fazem 50 pixels, o que dá drawStr(39, 11, "FRI 24 OCT").

Sua própria fonte ​

Quer um visual diferente? O primeiro item da lista Font, Import font..., recebe um arquivo TrueType (.ttf; .otf, .woff e .bdf também funcionam), e o Lopaka transforma ele em pixels na altura que você escolher. No vídeo, importamos Press Start 2P, uma fonte pixel gratuita, e definimos Font Height como 8. A pré-visualização mostra a fonte inteira: 95 imagenzinhas, cada caractere imprimível, do espaço ao ~.

A caixa de diálogo Font Preview de Press Start 2P com Font Height 8 e todos os 95 caracteres em três linhas

Clique em Import, e a fonte fica pronta para usar. O texto selecionado muda para ela, e o código ganha uma linha extra, #include "Press_Start_2P_8.h". Esse cabeçalho guarda os dados da fonte. Um botão com o mesmo nome aparece sob o código: baixe o arquivo e coloque ele ao lado do seu sketch, ou o sketch não vai compilar.

O painel mantém as fontes integradas. Se você testou a importação no horário, volte ele para profont29.

Etapa 3: ícones ​

Pesquise e adicione ​

O Lopaka tem mais de dois mil ícones integrados. Abra a guia Imagens à esquerda, digite no campo de busca e clique em um ícone para adicionar ele à tela. Adicione três:

  • wifi_25 (busque "wifi"),
  • battery_30 (busque "battery"),
  • weather_sun (busque "sun").

Os três estão no conjunto Gaai.

Coloque cada ícone no lugar dele ​

Arraste os ícones para o lugar na tela de desenho: Wi-Fi no canto superior esquerdo, a bateria no canto superior direito, o sol no canto inferior esquerdo.

Um ícone tem 32 bytes ​

Um ícone também é uma imagenzinha. O sol tem 15 pixels de largura e 16 de altura, e em uma tela de uma cor cada pixel é um bit: aceso ou apagado. Oito pixels cabem em um byte. Uma linha de 15 pixels precisa de dois bytes, e o último bit fica sem uso, então o ícone inteiro tem 2 bytes × 16 linhas = 32 bytes.

O ícone do sol de 15 por 16 pixels com uma linha destacada: seus pixels como os bits 00010000 00010000, os bytes 0x08 0x08, 8 pixels por byte, 2 bytes por linha vezes 16 linhas é igual a 32 bytes, e o array image_weather_sun_bits

No código, o ícone vira esta lista de números. A biblioteca lê eles e desenha os pixels:

cpp
static const unsigned char image_weather_sun_bits[] U8X8_PROGMEM = {0x80,0x00,0x84,0x10,0x08,0x08,0xc0,0x01,0x31,0x46,0x12,0x24,0x08,0x08,0x08,0x08,0x08,0x08,0x12,0x24,0x31,0x46,0xc0,0x01,0x08,0x08,0x84,0x10,0x80,0x00,0x00,0x00};
cpp
u8g2.drawXBMP(5, 47, 15, 16, image_weather_sun_bits);

drawXBMP recebe a posição, a largura e a altura da imagem e o array. Neste formato, chamado XBM, o primeiro pixel de uma linha é o bit mais baixo do byte. É por isso que a linha destacada se lê 00010000 da esquerda para a direita, mas é armazenada como 0x08.

Os outros dois ícones têm 19×16 e 24×16 pixels. Os dois precisam de três bytes por linha, 48 bytes cada.

Etapa 4: texto invertido ​

Uma barra branca com texto escuro ​

Para uma barra de status, desenhe um retângulo preenchido e coloque texto por cima:

  1. Escolha a ferramenta retângulo, arraste um retângulo na tela de desenho e defina X 0, Y 46, W 128, H 18. Ative Filled. A barra agora cobre as 18 linhas inferiores da tela, as linhas de 46 a 63.
  2. O retângulo novo é a camada superior e esconde o sol. Pressione Ctrl + ↓ (Cmd + ↓ em um Mac) para mover ele uma camada para baixo, sob weather_sun.
  3. Selecione weather_sun e ative Inverted (XOR).
  4. Adicione um texto Sunny 23C (com dois espaços), escolha a fonte 6x10, defina X 28 e Y 59 e ative Inverted (XOR).

O sol e o texto atravessam a barra branca em preto.

A tela Morning com uma barra branca na parte inferior; o sol e o texto Sunny 23C são desenhados em preto sobre ela, e o inspetor mostra string 7 com a fonte 6x10, X 28, Y 59 e Inverted (XOR) ativado

XOR: onde as formas se sobrepõem, os pixels invertem ​

Inverted usa um truque chamado XOR. Onde uma forma nova se sobrepõe a algo já desenhado, os pixels invertem: um pixel aceso sob a forma apaga, um apagado acende. Em uma tela de uma cor, é assim que você obtém texto escuro em uma barra clara.

XOR, a sobreposição inverte: 07:45 escuro em uma barra branca; aceso mais apagado dá aceso, aceso mais aceso dá apagado

No código, isso é uma linha antes do sol:

cpp
// rect 6
u8g2.drawBox(0, 46, 128, 18);
// weather_sun
u8g2.setDrawColor(2);
u8g2.drawXBMP(5, 47, 15, 16, image_weather_sun_bits);
// string 7
u8g2.setFont(u8g2_font_6x10_tr);
u8g2.drawStr(28, 59, "Sunny  23C");

setDrawColor(2) muda o U8g2 para o modo XOR, e o modo continua ativo até o código mudar ele de novo. É por isso que o texto depois do sol não precisa de uma segunda chamada. A ordem das camadas importa aqui: a barra precisa ser desenhada primeiro, para o sol e o texto terem algo para inverter.

Etapa 5: sua própria imagem ​

Envie uma imagem ​

Você também pode trazer suas próprias imagens. Na guia Imagens, clique em Upload e escolha um arquivo de imagem. O Lopaka abre uma caixa de diálogo de importação que converte ela para o display. Usamos uma imagem fotográfica da lua.

Defina Dimensions como W 48 e H 48 e ative Invert palette: a lua é clara em um fundo escuro, e isso torna a lua os pixels acesos no OLED.

O dithering transforma cinza em pontos ​

Esta tela não tem cinza. Cada pixel é preto ou branco. A conversão mais simples corta na metade do brilho: pixels mais brilhantes acendem, mais escuros apagam. Na lua, isso mantém só uma mancha branca, e todo o sombreamento se perde.

Dithering resolve isso. Ele mistura pontos pretos e brancos, mais densos onde a imagem é mais brilhante, então de longe seu olho vê tons de cinza.

A mesma lua de três jeitos: a original em tons de cinza, cortada em 50% em uma mancha branca chapada e com dithering em pontos que fingem o cinza, com um close do padrão de pontos

Escolha um estilo na lista Dithering: Floyd-Steinberg, Atkinson, Bayer 8x8 ou No dithering. Mantivemos Floyd-Steinberg. Clique em Import.

A caixa de diálogo de importação de imagem com a lua em 48 por 48 pixels, dithering Floyd-Steinberg e Invert palette ativado; a pré-visualização mostra a lua como um padrão de pontos

Em 48 por 48 pixels, a imagem ocupa 48 / 8 × 48 = 288 bytes.

Uma segunda tela para a noite ​

Vamos dar à lua uma tela só dela. Um projeto pode ter quantas telas você precisar.

  1. Clique duas vezes no nome da tela acima da tela de desenho e renomeie Screen 1 para Morning.
  2. Clique em Adicionar tela no painel Camadas e renomeie a nova tela para Night.

Coloque a lua na tela Night em X 6, Y 8 e adicione dois textos:

  • Good night em 6x10 em X 62, Y 26,
  • 22:30 em profont22 em X 62, Y 50.

As duas telas lado a lado na tela de desenho do Lopaka: Morning com o horário 07:45 e a barra de clima, Night com a lua com dithering, Good night e 22:30; o painel de código tem uma guia para cada tela

Cada tela vira sua própria função no código: drawMorning() e drawNight(), com o nome das telas. O painel de código tem uma guia para cada tela. Seu sketch decide qual mostrar, e quando.

Etapa 6: PROGMEM ​

Uma configuração que vale conhecer: Declare as PROGMEM, na lista de interruptores ao lado do código. Ela vem ativada por padrão.

A guia de código Night: image_moon_bits é declarada com U8X8_PROGMEM e desenhada com drawXBMP; os interruptores ao lado do código começam com Declare as PROGMEM, ativado

Em uma placa AVR como o UNO, um array constante é copiado para a RAM quando o programa começa, a menos que seja marcado para a flash. U8X8_PROGMEM mantém os dados da imagem na flash, na estante, para não lotar a mesinha da RAM, e drawXBMP (com o P) lê eles de lá. Com o interruptor desligado, o Lopaka escreve drawXBM e os arrays sem U8X8_PROGMEM.

A diferença para este painel: as quatro imagens ocupam 416 bytes. Compilado para um UNO, o sketch usa 78% da RAM com PROGMEM e 98% sem ele. Sem ele, sobram só 32 bytes para tudo o que o programa faz enquanto executa, e o sketch pode travar ou se comportar de forma estranha.

RAM usada em um Arduino UNO: 78% com PROGMEM, 98% sem PROGMEM, só 32 bytes restantes

Etapa 7: execute ele ​

O código ​

Clique em Copiar na guia Morning, cole ele no seu sketch e depois faça o mesmo na guia Night. O Copiar pega o código da guia aberta. Este é o código que o Lopaka gera para as duas telas:

cpp
// [BEGIN lopaka generated]
static const unsigned char image_battery_30_bits[] U8X8_PROGMEM = {0x00,0x00,0x00,0xf0,0xff,0x7f,0x08,0x00,0x80,0x08,0x00,0xaa,0x0e,0x00,0xaa,0x01,0x00,0xaa,0x01,0x00,0xaa,0x01,0x00,0xaa,0x01,0x00,0xaa,0x01,0x00,0xaa,0x0e,0x00,0xaa,0x08,0x00,0xaa,0x08,0x00,0x80,0xf0,0xff,0x7f,0x00,0x00,0x00,0x00,0x00,0x00};
static const unsigned char image_weather_sun_bits[] U8X8_PROGMEM = {0x80,0x00,0x84,0x10,0x08,0x08,0xc0,0x01,0x31,0x46,0x12,0x24,0x08,0x08,0x08,0x08,0x08,0x08,0x12,0x24,0x31,0x46,0xc0,0x01,0x08,0x08,0x84,0x10,0x80,0x00,0x00,0x00};
static const unsigned char image_wifi_25_bits[] U8X8_PROGMEM = {0x80,0x0f,0x00,0x60,0x30,0x00,0x18,0xc0,0x00,0x84,0x0f,0x01,0x62,0x30,0x02,0x11,0x40,0x04,0x0a,0x87,0x02,0xc4,0x18,0x01,0x28,0xa0,0x00,0x10,0x47,0x00,0xa0,0x2f,0x00,0xc0,0x1d,0x00,0x80,0x0a,0x00,0x00,0x07,0x00,0x00,0x02,0x00,0x00,0x00,0x00};
void drawMorning(void) {
    u8g2.setFontMode(1);
    u8g2.setBitmapMode(1);
    // string 1
    u8g2.setFont(u8g2_font_profont29_tr);
    u8g2.drawStr(24, 40, "07:45");
    // string 2
    u8g2.setFont(u8g2_font_5x8_tr);
    u8g2.drawStr(39, 11, "FRI 24 OCT");
    // wifi_25
    u8g2.drawXBMP(2, 0, 19, 16, image_wifi_25_bits);
    // battery_30
    u8g2.drawXBMP(102, 0, 24, 16, image_battery_30_bits);
    // rect 6
    u8g2.drawBox(0, 46, 128, 18);
    // weather_sun
    u8g2.setDrawColor(2);
    u8g2.drawXBMP(5, 47, 15, 16, image_weather_sun_bits);
    // string 7
    u8g2.setFont(u8g2_font_6x10_tr);
    u8g2.drawStr(28, 59, "Sunny  23C");
}
// [END lopaka generated]

// [BEGIN lopaka generated]
static const unsigned char image_moon_bits[] U8X8_PROGMEM = {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xd0,0x0a,0x00,0x00,0x00,0x00,0x7f,0x57,0x00,0x00,0x00,0xc0,0xee,0xad,0x00,0x00,0x00,0xf0,0xbf,0xbb,0x05,0x00,0x00,0xd8,0xfb,0x56,0x09,0x00,0x00,0xfc,0xbf,0xbf,0x12,0x00,0x00,0xfe,0xff,0xd5,0x06,0x00,0x00,0xdf,0x6f,0xbf,0x2a,0x00,0x80,0x7f,0xbb,0xd5,0x42,0x00,0xc0,0xdf,0x7f,0xbf,0x2a,0x00,0xc0,0xff,0xfe,0xd6,0x8a,0x00,0xe0,0xff,0xdf,0xbb,0x2a,0x00,0x60,0xff,0xff,0x6e,0x95,0x00,0xf0,0xff,0xbf,0xb7,0x4a,0x00,0xd0,0xff,0xef,0xdd,0x12,0x02,0xf0,0xff,0xff,0xb7,0x56,0x00,0xf0,0xff,0xbd,0x6e,0x09,0x01,0xf8,0xbf,0xef,0x5b,0x53,0x00,0xe8,0xfd,0xff,0xd6,0x14,0x10,0x78,0xff,0xdd,0xad,0x8a,0x00,0xe8,0x6f,0x77,0x5b,0x11,0x00,0xf8,0xff,0xdf,0x56,0x45,0x04,0xd0,0x5e,0x7d,0xab,0x10,0x00,0xf8,0xfb,0xd7,0x56,0x05,0x00,0x50,0xef,0xbe,0x55,0x41,0x00,0xf0,0xbd,0x6b,0x55,0x0a,0x04,0x50,0xf7,0xd6,0xaa,0x00,0x00,0xd0,0xad,0xbd,0x0a,0x25,0x00,0xa0,0x76,0xab,0x52,0x00,0x02,0x60,0xdb,0x56,0x25,0x01,0x00,0x40,0xad,0x55,0x49,0x08,0x00,0x80,0x6a,0x55,0x15,0x82,0x00,0x80,0x5a,0x55,0x82,0x00,0x00,0x00,0xa5,0x4a,0x29,0x00,0x00,0x00,0x2a,0x29,0x00,0x00,0x00,0x00,0x48,0x92,0x02,0x20,0x00,0x00,0xa4,0x04,0x08,0x04,0x00,0x00,0x00,0x51,0x80,0x00,0x00,0x00,0x20,0x04,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x20,0x00,0x00,0x00,0x00,0x20,0x08,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00};
void drawNight(void) {
    u8g2.setFontMode(1);
    u8g2.setBitmapMode(1);
    // moon
    u8g2.drawXBMP(6, 8, 48, 48, image_moon_bits);
    // string 9
    u8g2.setFont(u8g2_font_6x10_tr);
    u8g2.drawStr(62, 26, "Good night");
    // string 10
    u8g2.setFont(u8g2_font_profont22_tr);
    u8g2.drawStr(62, 50, "22:30");
}
// [END lopaka generated]

6x10 aparece nas duas funções, mas é a mesma fonte e é armazenada na flash uma vez. O design inteiro usa quatro fontes: profont29, 5x8, 6x10 e profont22.

O sketch ​

Comece pelo sketch do episódio passado e mude loop() para ele mostrar cada tela por três segundos:

cpp
#include <U8g2lib.h>
#include <Wire.h>

// 128x64 OLED display on I2C
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);

// Paste the Lopaka code here

void setup() {
  u8g2.begin();
}

void loop() {
  u8g2.clearBuffer();
  // show each screen for 3 seconds
  if ((millis() / 3000) % 2 == 0) drawMorning();
  else drawNight();
  u8g2.sendBuffer();
}

millis() conta os milissegundos desde que a placa começou. Dividido por 3000, ele cresce um a cada três segundos, e % 2 transforma isso em 0, 1, 0, 1. Em 0, o sketch desenha Morning; em 1, Night. Como no episódio 1, as duas funções desenham entre clearBuffer() e sendBuffer().

Execute ele no simulador ​

Abra o projeto do simulador do episódio 1: um Arduino UNO, um OLED SSD1306 ligado a GND, 5V, A5 (SCL) e A4 (SDA) e a biblioteca U8g2. Substitua o sketch pelo de cima, cole o código do Lopaka no lugar dele e pressione Play. O display alterna entre as duas telas a cada três segundos.

O simulador executando o sketch: o OLED simulado conectado a um Arduino UNO mostra a tela Morning com 07:45 e Sunny 23C

Conferimos as duas telas pixel por pixel: a tela de desenho do Lopaka, a tela desenhada pela biblioteca U8g2 e o display simulado são idênticos.

Quanta memória tudo isso ocupa ​

Tudo isso, com quatro fontes, três ícones e uma imagem, cabe em menos da metade da flash do UNO. O compilador informa:

Sketch uses 15394 bytes (47%) of program storage space. Maximum is 32256 bytes.
Global variables use 1600 bytes (78%) of dynamic memory, leaving 448 bytes for local variables. Maximum is 2048 bytes.

A maior parte dos 1.600 bytes de RAM é o buffer de quadro de 1.024 bytes do episódio 1. Para este sketch, isso não é problema.

Recapitulação ​

  1. Fontes e ícones são imagens: cada caractere de uma fonte e cada ícone é uma pequena grade de pixels, um bit por pixel.
  2. Imagens custam memória: uma fonte ocupa de um a quatro kilobytes de flash, então use duas ou três por tela e mantenha as imagens na flash com PROGMEM.
  3. O dithering transforma cinza em pontos: escolha um estilo de dithering quando importar uma foto, e a tela de uma cor mostra tons.

Próximo episódio: Going Colour. O mesmo tipo de interface em um display TFT colorido, e o que RGB565 significa. Leia o episódio 3 ou assista no YouTube.

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