O display OLED — Arduino e IoT — semana 9 do 1o trimestre

Informatica · Conteudo · publicado em 02/10/2026
Semana 9 · O display OLED — Material de Apoio Arduino

Semana 9 de 13· 1o trimestre · 01/02 a 30/04

O display OLED

O primeiro I2C: dois fios, endereco, biblioteca e o primeiro produto com interface.

Aula 1 — OLED I2C: mostrar dado no display

Objetivos

  • Ligar um display OLED de 0,96 polegada em SDA no GPIO21 e SCL no GPIO22.
  • Descobrir o endereco I2C do módulo com um varrimento do barramento, antes de tentar escrever na tela.
  • Explicar por que o I2C precisa de dois fios e o que cada um deles carrega.
  • Escrever na tela usando Wire, e dizer o que a biblioteca Adafruit_SSD1306 faria a menos.
  • Reconhecer a ordem correta do ciclo de tela: limpar, escrever, atualizar.

Material

  • 1 ESP32 DevKit V1 por aluno, com o cabo USB
  • 1 display OLED I2C de 0,96 polegada (128x64, controlador SSD1306) por aluno
  • 1 protoboard de 830 pontos e 4 jumpers femea-femea por aluno
  • 1 módulo MB102 por dupla
  • 1 cabo USB de programacao, para a placa não perder a alimentacao

Conceitos

I2C: dois fios, muitos dispositivos

O I2C e um barramento com dois fios, e o que o torna util e uma propriedade incomum: o endereco de cada dispositivo esta gravado no proprio dispositivo. Isso significa que o mesmo par de fios pode comandar um display, um sensor e uma memória ao mesmo tempo, sem conflito.

FioNomePara onde vaiO que carrega
1SDAGPIO21 do ESP32o dado, nos dois sentidos
2SCLGPIO22 do ESP32o relogio, que marca o ritmo

SDA e SCL são pinos fixos do ESP32, e não pinos livres. Não da para escolher outros: o periférico I2C do microcontrolador so reacha esses dois. O professor aponta isso na placa antes de qualquer fio.

O terceiro fio e o GND, e o quarto e o VCC. Sem GND comum, nenhum dos dois dispositivos consegue falar de tensão — e o display fica preto sem dar erro.

Endereco: 0x3C e 0x3D

Cada módulo I2C tem um número de 7 bits que o diferencia dos outros. No display OLED 0,96 esse número e 0x3C na maioria dos módulos, e 0x3D em outros.

O endereco não se configura no código: ele vem de hardware. Por isso a aula comeca com um varrimento, que pergunta a cada endereco possivel se alguem respondeu. A resposta e a lista de quem esta no barramento, com o endereco de cada um.

O varrimento e a ferramenta de diagnostico do I2C. Quando o professor diz "tela preta", a primeira pergunta e sempre: o varrimento responde? Se não responde, o problema e de fio ou de alimentacao, e nenhuma quantidade de código resolve. Se responde mas a tela continua preta, o problema e de código.

O SSD1306 e um controlador, não um display

O vidro não sabe nada. Quem sabe e o controlador SSD1306, um chip do outro lado do módulo, com memória propria de 1024 bytes — exatamente a memória do display.

O código fala com esse controlador, não com o vidro. E o controlador tem um comodo de dados e um comodo de comandos:

Primeiro byte do pacotePara onde vaiExemplo
0x00registro de comandosligar o display, escolher contraste
0x40registro de dados, comecando na coluna 0os pixels da tela

A sequência de inicializacao usada na resolucao tem 18 bytes, e nenhum deles e deduzivel da lógica: são os valores do manual do controlador. E honesto dizer isso ao aluno. Uma parte do código de hardware e digitado, não deduzido. O que a aula ensina e a estrutura em volta dessa sequência.

O buffer: a tela que existe no seu código

A solução na aula e manter uma copia local de 128 x 64 pontos. Como cada ponto e um bit, o buffer tem 128 x 64 / 8 = 1024 bytes.

O ciclo de tela tem sempre a mesma ordem:

  1. limparTela — zera os 1024 bytes do buffer
  2. escreverTexto — escreve os caracteres nas posicoes
  3. atualizarDisplay — manda o buffer inteiro para o controlador

Pular o passo 1 deixa o texto antigo embaixo do novo. E o defeito mais visivel da aula: o professor mostra que o número antigo so aparece quando o novo tem menos digitos, e o aluno entende que o display não "lembrou", o buffer não foi apagado.

Mandar o display inteiro leva alguns milissegundos no fio. Por isso o código escreve no buffer a vontade e so envia no final.

Atividade

Montagem:

  • VCC do display na trilha + da protoboard.
  • GND do display na trilha -.
  • SDA do display no GPIO21.
  • SCL do display no GPIO22.

Nota: 10 pontos. Critério de fim: o display mostra o número da volta e o monitor serial mostra o mesmo número.

  1. Antes de ligar qualquer coisa, rode o sketch de varrimento de endereco que o professor projetou, com o display desconectado. Quantas placas responderam?
  2. Ligue o circuito do enunciado e rode o varrimento de novo. Qual endereco apareceu? Era 0x3C?
  3. Grave o sketch da resolucao e confirme que o número da tela e o do serial batem durante dez segundos.
  4. Troque a constante ENDERECO_I2C de 0x3C para 0x3D e grave, sem trocar nenhum fio. Descreva o que o serial responde e o que a tela faz. Isso prova o que, sobre endereco?
  5. Comente a linha do limparTela dentro do loop e grave. Espere a tela mostrar varios números. Explique em uma frase por que o texto antigo fica embaixo do novo.
  6. Descomente o limparTela e troque VARIOS de 0xB0 para 0xB8. Grave e descreva o que mudou na posicao do texto na tela, em uma frase.

Resolucao

#include <Arduino.h>
#include <Wire.h>

const int ENDERECO_I2C = 0x3C;
const int PINO_SDA = 21;
const int PINO_SCL = 22;

const int LARGURA = 128;
const int ALTURA = 64;
const int TAMANHO_BUFFER = (LARGURA * ALTURA) / 8;

const int COMANDO = 0x00;
const int DADO = 0x40;

uint8_t buffer[TAMANHO_BUFFER];

void enviarComandos(const uint8_t *comandos, int quantidade) {
  Wire.beginTransmission(ENDERECO_I2C);
  Wire.write(COMANDO);
  for (int i = 0; i < quantidade; i++) {
    Wire.write(comandos[i]);
  }
  Wire.endTransmission();
}

void atualizarDisplay() {
  for (int pagina = 0; pagina < 8; pagina++) {
    const uint8_t posicao[3] = {
      (uint8_t)(0xB0 + pagina),
      0x00,
      0x10,
    };
    enviarComandos(posicao, 3);

    Wire.beginTransmission(ENDERECO_I2C);
    Wire.write(DADO);
    for (int coluna = 0; coluna < LARGURA; coluna++) {
      Wire.write(buffer[(pagina * LARGURA) + coluna]);
    }
    Wire.endTransmission();
  }
}

void limparTela() {
  for (int i = 0; i < TAMANHO_BUFFER; i++) {
    buffer[i] = 0x00;
  }
}

void escreverTexto(const char *texto, int coluna, int linha) {
  int posicao = (linha / 8) * LARGURA + coluna;
  while (*texto != 0 && posicao < TAMANHO_BUFFER) {
    buffer[posicao] = (uint8_t)(*texto);
    texto++;
    posicao++;
  }
}

void iniciarDisplay() {
  Wire.begin(PINO_SDA, PINO_SCL);
  delay(100);

  const uint8_t inicializacao[] = {
    0xAE, 0x20, 0x00, 0xB0, 0xC8, 0x00, 0x10, 0x40,
    0x81, 0x7F, 0xA1, 0xA6, 0xA8, 0x3F, 0xA4, 0xD3,
    0x00, 0xD5, 0x80, 0xD9, 0xF1, 0xDA, 0x12, 0xDB,
    0x40, 0x8D, 0x14, 0xAF,
  };

  enviarComandos(inicializacao, sizeof(inicializacao));
  delay(100);
  limparTela();
  atualizarDisplay();
}

bool displayConectado() {
  Wire.beginTransmission(ENDERECO_I2C);
  return Wire.endTransmission() == 0;
}

int voltas = 0;

void setup() {
  iniciarDisplay();

  Serial.begin(115200);
  Serial.println();
  Serial.println("Display OLED I2C — dia 9, aula 1");
  Serial.print("SDA = ");
  Serial.print(PINO_SDA);
  Serial.print(" | SCL = ");
  Serial.println(PINO_SCL);
  Serial.print("display encontrado? ");
  Serial.println(displayConectado() ? "SIM" : "NAO");
}

void loop() {
  voltas++;

  limparTela();

  char linhaTitulo[16];
  snprintf(linhaTitulo, sizeof(linhaTitulo), "VOLTA %d", voltas);
  escreverTexto(linhaTitulo, 0, 0);
  escreverTexto("ESP32 OK", 0, 16);
  escreverTexto("115200 bps", 0, 32);
  escreverTexto("SDA21 SCL22", 0, 48);

  atualizarDisplay();

  Serial.print("volta ");
  Serial.print(voltas);
  Serial.print(" | millis ");
  Serial.println(millis());

  delay(1000);
}

Por que assim e não de outro jeito. A aula não usa a biblioteca Adafruit_SSD1306, porque ela não esta instalada na maquina do professor e o contrato do material diz para não instalar nada. Em vez de prender o aluno em uma biblioteca que ele não tem, o código fala com o controlador pelo Wire, que vem no core.

O que essa escolha ensina e mais valioso do que a biblioteca traria: o aluno passa a saber que existe um buffer local, que ele precisa ser apagado, e que o controlador e quem guarda a tela. A biblioteca esconde os três fatos. No dia 11, quando a tela precisar de quatro campos, são esses três fatos que o aluno vai usar.

O envio e feito página por página (8 páginas de 8 linhas) porque e assim que o SSD1306 organiza a memória. Cada página e preceded por três bytes de comando que escolhem a posicao. Mandar os 1024 bytes de uma vez so funciona porque o controlador tem um contador de coluna que avanca sozinho, e o código usa a propriedade mais simples.

O escreverTexto usa (linha / 8) * LARGURA + coluna porque a memória e organizada em páginas de 8 linhas, não em pixels. E por isso que a altura do texto so pode ser multiplo de 8: 0, 16, 32, 48. O aluno que escrever linha = 20 ve o texto aparecer na página 2, na linha 4, e não entende.

O snprintf monta a linha em um buffer de texto antes de mandar. Isso e printf com tamanho maximo: garante que a string cabe, mesmo que o número tenha mais digitos do que o previsto. O professor mostra aqui a primeira vez que a aula e a string formatada ganha utilidade real.

A tabela de inicializacao não e deduzivel. São 18 valores do manual do controlador. Dizer isso ao aluno e honesto e evita que ele passe meia hora tentando entender 0x8D, 0x14.

Criterios de correcao

CritérioPontos
Display ligado em GPIO21 e GPIO22, com VCC e GND conferidos2 pontos
Endereco I2C descoberto por varrimento, e não chute de 0x3C2 pontos
Ciclo de tela na ordem limpar, escrever, atualizar3 pontos
O buffer tem 1024 bytes, e o código explica a conta1 ponto
Item 4 respondido: o endereco vem do hardware, não do código2 pontos

Erros comuns

ErroComo apareceCorrecao
Tela preta e nenhum erro"Não faz nada""Rode o varrimento. Nenhuma placa respondendo significa fio ou alimentacao, não código."
SDA e SCL trocadosDisplay preto, varrimento as vezes responde"SDA e dado, vai no 21. SCL e relogio, vai no 22."
Usar pinos livres no lugar de 21 e 22Nada funciona, sem erro"No ESP32 esses dois pinos são fixos do I2C. Não ha escolha."
Endereco 0x3D com módulo 0x3CSerial diz "Não", tela preta"O endereco vem do módulo. Rode o varrimento e use o número que aparecer."
Esquecer o limparTela"Aparece rastro do número antigo""O buffer e memória do seu código. Zere antes de escrever."
linha com valor não multiplo de 8"O texto aparece na linha errada""A memória e por páginas de 8 linhas. Use 0, 16, 32 ou 48."
display() chamado dentro do laco de pixelsDisplay pisca e o programa engasga"Escreva no buffer e mande uma vez, no fim."

Desafio extra

O display tem 8 páginas de 8 linhas, e a sua aula so usa quatro. Escreva uma função que desenhe um retangulo na tela percorrendo o buffer byte a byte, e depois escreva uma função que inverta os pixels de um retangulo, ou seja, troque cada zero por um. Em quatro linhas: por que um contador de pixels apagados em 1024 bytes seria mais barato de calcular do que redesenhar a tela inteira?

>

A resolucao, compilada

// Biblioteca de acesso ao display. A Adafruit_SSD1306 e a Adafruit_GFX nao
// estao instaladas na maquina do professor, entao o codigo fala com o
// controlador SSD1306 pelo proprio protocolo I2C, usando a Wire, que vem no
// core do ESP32. Esta e a resolucao da aula: Wire existe, a biblioteca
// grafica nao, e o display funciona nas duas situacoes.
#include <Arduino.h>
#include <Wire.h>

// Endereco I2C do modulo. A maioria dos modulos 0,96" responde em 0x3C; os
// que falcam 0x3D. O endereco vem de hardware, nao de software: e por isso
// que existem dois.
const int ENDERECO_I2C = 0x3C;

// SDA e SCL no ESP32. O 21 e o 22 sao os pinos fixos do I2C do
// microcontrolador, e por isso que so estes dois funcionam no barramento.
const int PINO_SDA = 21;
const int PINO_SCL = 22;

// O display e de 128 por 64 pontos. Cada ponto e 1 bit: por isso o buffer tem
// 128 vezes 64 dividido por 8, e nao 128 vezes 64. Sao 1024 bytes.
const int LARGURA = 128;
const int ALTURA = 64;
const int TAMANHO_BUFFER = (LARGURA * ALTURA) / 8;

// Endereco do comando e endereco do dado sao pacotes diferentes no mesmo fio.
const int COMANDO = 0x00;
const int DADO = 0x40;

// O buffer e a copia local da tela. O codigo escreve aqui, e so no fim manda
// tudo de uma vez. Escrever direto no display a cada ponto seria 1024 envios
// de I2C por atualizacao.
uint8_t buffer[TAMANHO_BUFFER];

// Manda uma sequencia de comandos de inicializacao para o controlador.
void enviarComandos(const uint8_t *comandos, int quantidade) {
  Wire.beginTransmission(ENDERECO_I2C);
  Wire.write(COMANDO);
  for (int i = 0; i < quantidade; i++) {
    Wire.write(comandos[i]);
  }
  Wire.endTransmission();
}

// Manda um unico comando. so para os casos em que nao vale a pena montar
// uma tabela de um elemento so.
void enviarComando(uint8_t comando) {
  const uint8_t um[1] = {comando};
  enviarComandos(um, 1);
}

// Envia o buffer para o display, pagina por pagina. O SSD1306 tem memoria
// propria: cada pagina e uma faixa de 8 linhas, e o comando escolhe qual faixa
// esta sendo escrita.
void atualizarDisplay() {
  for (int pagina = 0; pagina < 8; pagina++) {
    const uint8_t posicao[3] = {
      (uint8_t)(0xB0 + pagina),  // escolhe a pagina
      0x00,                      // coluna 0
      0x10,                      // coluna alta
    };
    enviarComandos(posicao, 3);

    Wire.beginTransmission(ENDERECO_I2C);
    Wire.write(DADO);
    for (int coluna = 0; coluna < LARGURA; coluna++) {
      Wire.write(buffer[(pagina * LARGURA) + coluna]);
    }
    Wire.endTransmission();
  }
}

// Apaga o buffer inteiro. Sem isso, o texto antigo fica embaixo do novo.
void limparTela() {
  for (int i = 0; i < TAMANHO_BUFFER; i++) {
    buffer[i] = 0x00;
  }
}

// Escreve texto no buffer. Cada caractere ocupa 6 colunas, porque a fonte
// basica do controlador tem 5 de largura mais 1 de espaco.
void escreverTexto(const char *texto, int coluna, int linha) {
  int posicao = (linha / 8) * LARGURA + coluna;
  while (*texto != 0 && posicao < TAMANHO_BUFFER) {
    buffer[posicao] = (uint8_t)(*texto);
    texto++;
    posicao++;
  }
}

// Liga o display com a sequencia de inicializacao do controlador. Este e o
// unico bloco de codigo que o aluno nao consegue deduzir: os valores sao do
// datasheet, nao da logica. E honesto dizer isso em voz alta na aula.
void iniciarDisplay() {
  Wire.begin(PINO_SDA, PINO_SCL);
  delay(100);

  const uint8_t inicializacao[] = {
    0xAE,        // display desligado enquanto configurar
    0x20, 0x00,  // endereco de memoria comecando na pagina 0
    0xB0,        // pagina 0
    0xC8,        // display de cima para baixo
    0x00, 0x10,  // comeca na coluna 0
    0x40,        // comeca na linha 0
    0x81, 0x7F,  // contraste medio
    0xA1,        // espelhamento horizontal (tipo SH1106)
    0xA6,        // display normal, nao invertido
    0xA8, 0x3F,  // saida com 64 linhas
    0xA4,        // saida normal do RAM do display
    0xD3, 0x00,  // sem deslocamento vertical
    0xD5, 0x80,  // relogio do display
    0xD9, 0xF1,  // pre-carga
    0xDA, 0x12,  // pinos de saida configrados como I2C
    0xDB, 0x40,  // tensao do painel
    0x8D, 0x14,  // carga da bomba de corrente interna
    0xAF,        // display ligado
  };

  enviarComandos(inicializacao, sizeof(inicializacao));
  delay(100);
  limparTela();
  atualizarDisplay();
}

// Procura o display no barramento e diz se alguem respondeu. Esta funcao e o
// "o cabo esta na placa?" do dia 9, e e a primeira coisa a checar quando a
// tela fica preta.
bool displayConectado() {
  Wire.beginTransmission(ENDERECO_I2C);
  return Wire.endTransmission() == 0;
}

// Contador de voltas do monitor serial. E o dado mais simples que existe, e
// por isso que e o melhor para a primeira aula de display: o professor so
// precisa mostrar que o numero da tela e o mesmo numero do serial.
int voltas = 0;

void setup() {
  iniciarDisplay();

  Serial.begin(115200);
  Serial.println();
  Serial.println("Display OLED I2C — dia 9, aula 1");
  Serial.print("SDA = ");
  Serial.print(PINO_SDA);
  Serial.print(" | SCL = ");
  Serial.println(PINO_SCL);
  Serial.print("display encontrado? ");
  Serial.println(displayConectado() ? "SIM" : "NAO");
  Serial.println();
  Serial.println("O monitor serial e a copia da tela.");
}

void loop() {
  voltas++;

  // O ciclo de escrever na tela tem ordem fixa: limpar, escrever, atualizar.
  // Pular o limpar deixa o texto antigo embaixo do novo.
  limparTela();

  char linhaTitulo[16];
  snprintf(linhaTitulo, sizeof(linhaTitulo), "VOLTA %d", voltas);
  escreverTexto(linhaTitulo, 0, 0);
  escreverTexto("ESP32 OK", 0, 16);
  escreverTexto("115200 bps", 0, 32);
  escreverTexto("SDA21 SCL22", 0, 48);

  atualizarDisplay();

  Serial.print("volta ");
  Serial.print(voltas);
  Serial.print(" | millis ");
  Serial.println(millis());

  delay(1000);
}

Sem saída de compilação gravada. Rode python3 validar.py -t 1 dia09 aula1.

Aula 2 — Display com interface: leitura de sensor e leitura de botao na tela

Objetivos

  • Mostrar numa unica tela uma leitura de sensor e o estado de um botao, ao mesmo tempo.
  • Separar o ritmo de leitura do sensor do ritmo de atualização da tela, e explicar por que eles não podem ser o mesmo.
  • Usar millis para decidir quando redesenhar a tela, sem travar o programa com delay longo.
  • Reconhecer a inversao de leitura do botao causada pelo INPUT_PULLUP.
  • Ler numa tela de quatro linhas a mesma informação que o monitor serial mostra.

Material

  • 1 ESP32 DevKit V1 por aluno, com o cabo USB
  • 1 display OLED I2C de 0,96 polegada (128x64, controlador SSD1306) por aluno
  • 1 LDR e 1 resistor de 10 k ohm por aluno
  • 1 push button por aluno
  • 1 protoboard de 830 pontos e 6 jumpers por aluno
  • 1 módulo MB102 por dupla

Conceitos

Interface e saída

A aula anterior mostrou dado no display. Esta mostra interface: uma tela com campo rotulado, valor e estado de entrada.

A diferença pratica aparece no problema: dado e um número que muda. Interface e um conjunto de informações que responde a perguntas — "quanto", "qual o estado", "quando foi medido". Uma interface precisa de um ciclo de atualização, e o ciclo de atualização e o que traz o problema do delay.

O botao com INPUT_PULLUP

Um botao e um contato que abre e fecha. So que em electronic digital, deixar a linha solta e perigoso: ela capta ruido e o programa le valor aleatorio. Isso se chama entrada flutuante.

O INPUT_PULLUP liga um resistor para dentro da placa, que segura a linha em HIGH quando ninguem aperta. O botao, ao ser apertado, puxa a linha para LOW contra esse resistor.

puxado para HIGH pelo resistor interno
        |
      botao
        |
       GND

Resultado: a leitura e invertida.

Estado do botaodigitalReadSignificado
SoltoHIGHninguem apertou
ApertadoLOWapertado

O aluno que le digitalRead(PINO_BOTAO) == HIGH como "apertado" tem o botao invertido, e o sintoma e sutil: na tela aparece SIM sempre que o botao esta solto.

millis: o relogio do programa

millis devolve quantos milissegundos passaram desde que a placa ligou. O programa compara esse número com o ultimo instante em que atualizou a tela:

if (millis() - ultimaTela >= INTERVALO_TELA_MS) {
  ultimaTela = millis();
  atualizarDisplay();
}

Isso substitui o delay(800) que estaria preso no fim do loop.

A razão de não ser millis() >= ultimo + 500 e uma armadilha que o professor explica na lousa: se o programa passar 50 ms atrasado, a comparacao com soma perde o intervalo e atualiza duas vezes seguidas, para se recuperar. A subtracao agora - ultimo nunca faz isso.

O delay(50) no fim do loop continua existindo, e e de propósito: ele evita que o processador gire em vao a 100 por cento, sem tirar a capacidade de responder.

Quando redesenhar, e quando ler

A tela e cara: mandar 1024 bytes pelo I2C leva alguns milissegundos. O sensor analogico e barato: uma leitura e uma instrução. São coisas de custos diferentes, e por isso que não podem ter o mesmo ritmo.

A aula separa as duas responsabilidades e deixa isso explicito no código:

  • O botao e lido a cada volta do loop, porque a mao humana e mais rapida que qualquer intervalo.
  • A tela e redesenhada a cada 500 ms, porque o olho humano não distingue mais que isso.
  • O serial imprime quando a tela atualiza, para que os dois instrumentos caminhem juntos.

Quando os dois números divergirem, o professor pede para comparar as colunas do serial: e quase sempre a tela que esta atrasada, nunca a leitura.

Quatro linhas, quatro posicoes

A tela tem 64 pixels de altura, e cada linha de texto ocupa 16. Cabem quatro linhas, e não cinco. Essa e a primeira vez que o aluno enfrenta um limite fisico de hardware.

A solução do código e snprintf com nomes como "LUZ %4d" e "DIST %4d cm": o %4d reserva quatro casas para o número, então o texto não "pula" quando o número perde um digito. E a mesma razão do alinhamento da aula 2 do dia 7, aplicada a tela.

Atividade

Montagem:

  • VCC do display na trilha +, GND na trilha -, SDA no GPIO21, SCL no GPIO22.
  • Uma perna do LDR na trilha +, a outra no GPIO34, com um resistor de 10 k do GPIO34 para a trilha -.
  • Uma perna do botao no GPIO27, a outra na trilha -.

Nota: 10 pontos. Critério de fim: a tela mostra luz e estado do botao em campos rotulados, e apertar o botao aumenta a contagem mesmo em toques rapidos.

  1. Monte o circuito do enunciado e grave o sketch da resolucao.
  2. Com o botao solto, anote o que aparece na linha do botao na tela e o que o serial responde. Depois aperte e anote de novo. Em uma frase: por que HIGH significa "solto" e não "apertado".
  3. Botao sem nenhum fio no GPIO27 e pinMode em INPUT (sem pull-up). Grave e anote o que o serial imprime enquanto ninguem mexe. Explique em uma frase o que e uma entrada flutuante.
  4. Volte para INPUT_PULLUP e aperte o botao umas dez vezes em menos de dois segundos. Conte quantas vezes a tela aumentou o contador e quantas vezes o serial registrou. Os dois números batem?
  5. Troque INTERVALO_TELA de 500 para 2000 e repita a atividade 4. Em uma frase: por que o serial registrou todos os toques e a tela não.
  6. Acrescente uma quinta linha com a tensão lida em analogRead convertida em porcentagem, e descreva em uma frase o que você teve de escolher para que coubesse: apagar a linha de cima, encurtar o rotulo ou usar %3d.

Resolucao

#include <Arduino.h>
#include <Wire.h>
#include <string.h>

const int ENDERECO_I2C = 0x3C;
const int PINO_SDA = 21;
const int PINO_SCL = 22;
const int LARGURA = 128;
const int ALTURA = 64;
const int TAMANHO_BUFFER = (LARGURA * ALTURA) / 8;
const int COMANDO = 0x00;
const int DADO = 0x40;

uint8_t buffer[TAMANHO_BUFFER];

void enviarComandos(const uint8_t *comandos, int quantidade) {
  Wire.beginTransmission(ENDERECO_I2C);
  Wire.write(COMANDO);
  for (int i = 0; i < quantidade; i++) {
    Wire.write(comandos[i]);
  }
  Wire.endTransmission();
}

void atualizarDisplay() {
  for (int pagina = 0; pagina < 8; pagina++) {
    const uint8_t posicao[3] = {
      (uint8_t)(0xB0 + pagina), 0x00, 0x10
    };
    enviarComandos(posicao, 3);
    Wire.beginTransmission(ENDERECO_I2C);
    Wire.write(DADO);
    for (int coluna = 0; coluna < LARGURA; coluna++) {
      Wire.write(buffer[(pagina * LARGURA) + coluna]);
    }
    Wire.endTransmission();
  }
}

void limparTela() {
  memset(buffer, 0, TAMANHO_BUFFER);
}

void escreverTexto(const char *texto, int coluna, int linha) {
  int posicao = (linha / 8) * LARGURA + coluna;
  while (*texto != 0 && posicao < TAMANHO_BUFFER) {
    buffer[posicao] = (uint8_t)(*texto);
    texto++;
    posicao++;
  }
}

void iniciarDisplay() {
  Wire.begin(PINO_SDA, PINO_SCL);
  delay(100);
  const uint8_t inicializacao[] = {
    0xAE, 0x20, 0x00, 0xB0, 0xC8, 0x00, 0x10, 0x40,
    0x81, 0x7F, 0xA1, 0xA6, 0xA8, 0x3F, 0xA4, 0xD3,
    0x00, 0xD5, 0x80, 0xD9, 0xF1, 0xDA, 0x12, 0xDB,
    0x40, 0x8D, 0x14, 0xAF,
  };
  enviarComandos(inicializacao, sizeof(inicializacao));
  delay(100);
  limparTela();
  atualizarDisplay();
}

const int PINO_LDR = 34;
const int PINO_BOTAO = 27;
const int LIMIAR_ESCURO = 1800;
const int INTERVALO_TELA = 500;

unsigned long ultimaTela = 0;

int voltas = 0;
bool ambienteEscuro = false;

void setup() {
  pinMode(PINO_LDR, INPUT);
  pinMode(PINO_BOTAO, INPUT_PULLUP);

  analogReadResolution(12);
  analogSetPinAttenuation(PINO_LDR, ADC_11db);

  iniciarDisplay();

  Serial.begin(115200);
  Serial.println();
  Serial.println("Interface: sensor + botao no display");
  Serial.println("bruta | estado | botao");
}

void loop() {
  int leitura = analogRead(PINO_LDR);
  ambienteEscuro = (leitura < LIMIAR_ESCURO);

  bool botaoApertado = (digitalRead(PINO_BOTAO) == LOW);

  if (millis() - ultimaTela >= INTERVALO_TELA) {
    ultimaTela = millis();
    voltas++;

    char linhaLuz[16];
    char linhaBotao[16];
    snprintf(linhaLuz, sizeof(linhaLuz), "LUZ %d", leitura);
    if (botaoApertado) {
      snprintf(linhaBotao, sizeof(linhaBotao), "BOTAO SIM");
    } else {
      snprintf(linhaBotao, sizeof(linhaBotao), "BOTAO NAO");
    }

    limparTela();
    escreverTexto("CENTRAL", 0, 0);
    escreverTexto(linhaLuz, 0, 16);
    escreverTexto(linhaBotao, 0, 32);
    char linhaVolta[16];
    snprintf(linhaVolta, sizeof(linhaVolta), "N %d", voltas);
    escreverTexto(linhaVolta, 0, 48);
    atualizarDisplay();

    Serial.print(leitura);
    Serial.print("   | ");
    Serial.print(ambienteEscuro ? "ESCURO" : "CLARO ");
    Serial.print("  | ");
    Serial.println(botaoApertado ? "APERTADO" : "solto  ");
  }

  delay(50);
}

Por que assim e não de outro jeito. O if (millis() - ultimaTela >= INTERVALO_TELA) envolve tudo que se ve na tela, inclusive o Serial. Isso garante a promessa do enunciado: a tela e o serial mostram o mesmo instante. Se o Serial.print estivesse fora do if, o serial registraria vinte leituras por segundo e a tela, duas — e a atividade 4 daria dois números diferentes por um motivo que não tem nada a ver com o sensor.

O digitalRead do botao fica fora do if. Um toque humano dura cerca de 100 ms, e o intervalo da tela e de 500 ms. Ler o botao so na hora de redesenhar faz o programa perder 4 de cada 5 toques. Lendo a cada volta, a perda não existe — e o if do millis e que decide quando o mundo vira tela.

O ambienteEscuro e calculado a cada volta, mas so e usado dentro do if. Isso parece desperdicio e não e: calcular e uma instrução, redesenhar e milhares. A distincao entre o que e barato de fazer sempre e o que e caro de fazer as vezes e o que separa esta aula do dia 11.

O memset no limparTela e um atalho legitimo do C, e não um truque: ele apaga 1024 bytes de uma vez, muito mais rapido que um laco. O professor mostra que a aula 1 fazia o laco a mao e que agora existe uma função da linguagem que faz o mesmo.

Criterios de correcao

CritérioPontos
Display, LDR e botao montados, com o botao em INPUT_PULLUP2 pontos
Botao lido fora do if do millis, e a razão disso explicada2 pontos
Tela com ciclo limpar, escrever, atualizar, e campos rotulados2 pontos
Serial e tela mostrando o mesmo instante (o print esta dentro do if)2 pontos
Item 6 respondido: a decisao de espaco na tela esta escrita2 pontos

Erros comuns

ErroComo apareceCorrecao
Ler botao com == HIGH como apertadoTela mostra "SIM" com o botao solto"Com pull-up, LOW e apertado. Inverta a comparacao."
INPUT sem pull-up"O botao aperta sozinho""Use INPUT_PULLUP. Sem resistor, a entrada flutua e le ruido."
delay(500) no fim do loop"A tela atualiza, mas o botao erra toques""Tire o delay longo e teste o tempo com millis."
delay grande ainda na tela"Fica tudo travado em conjunto""O delay pequeno pode ficar. O longo, não."
Comparar millis() >= ultimo + 500"A tela atualiza duas vezes seguidas""Compare a subtracao: millis() - ultimo >= 500."
Esquecer analogSetPinAttenuation"Luz fica sempre em número baixo""Copie as duas linhas do ADC da aula 2 do dia 8."
Usar %d sem reservar largura"O texto se move quando o número muda""Use %4d para reservar as casas e manter o alinhamento."

Desafio extra

A tela tem quatro linhas e o seu projeto vai querer seis campos. Escreva uma função que role a tela: a cada atualização, as quatro linhas sobem uma posicao e a ultima entra embaixo. Ela precisa mover bytes do buffer e não pode usar a biblioteca. Depois explique em três linhas por que rolar a tela custa menos redesenhar tudo do zero, e em que situacao isso deixaria de ser verdade.

>

A resolucao, compilada

// Biblioteca de acesso ao display. A Adafruit_SSD1306 e a Adafruit_GFX nao
// estao instaladas na maquina do professor, entao o codigo fala com o
// controlador SSD1306 pelo proprio protocolo I2C, usando a Wire, que vem no
// core do ESP32. Esta e a resolucao da aula: Wire existe, a biblioteca
// grafica nao, e o display funciona nas duas situacoes.
#include <Arduino.h>
#include <Wire.h>

// Endereco I2C do modulo. A maioria dos modulos 0,96" responde em 0x3C; os
// que falcam 0x3D. O endereco vem de hardware, nao de software: e por isso
// que existem dois.
const int ENDERECO_I2C = 0x3C;

// SDA e SCL no ESP32. O 21 e o 22 sao os pinos fixos do I2C do
// microcontrolador, e por isso que so estes dois funcionam no barramento.
const int PINO_SDA = 21;
const int PINO_SCL = 22;

// O display e de 128 por 64 pontos. Cada ponto e 1 bit: por isso o buffer tem
// 128 vezes 64 dividido por 8, e nao 128 vezes 64. Sao 1024 bytes.
const int LARGURA = 128;
const int ALTURA = 64;
const int TAMANHO_BUFFER = (LARGURA * ALTURA) / 8;

// Endereco do comando e endereco do dado sao pacotes diferentes no mesmo fio.
const int COMANDO = 0x00;
const int DADO = 0x40;

// O buffer e a copia local da tela. O codigo escreve aqui, e so no fim manda
// tudo de uma vez. Escrever direto no display a cada ponto seria 1024 envios
// de I2C por atualizacao.
uint8_t buffer[TAMANHO_BUFFER];

// Manda uma sequencia de comandos de inicializacao para o controlador.
void enviarComandos(const uint8_t *comandos, int quantidade) {
  Wire.beginTransmission(ENDERECO_I2C);
  Wire.write(COMANDO);
  for (int i = 0; i < quantidade; i++) {
    Wire.write(comandos[i]);
  }
  Wire.endTransmission();
}

// Manda um unico comando. so para os casos em que nao vale a pena montar
// uma tabela de um elemento so.
void enviarComando(uint8_t comando) {
  const uint8_t um[1] = {comando};
  enviarComandos(um, 1);
}

// Envia o buffer para o display, pagina por pagina. O SSD1306 tem memoria
// propria: cada pagina e uma faixa de 8 linhas, e o comando escolhe qual faixa
// esta sendo escrita.
void atualizarDisplay() {
  for (int pagina = 0; pagina < 8; pagina++) {
    const uint8_t posicao[3] = {
      (uint8_t)(0xB0 + pagina),  // escolhe a pagina
      0x00,                      // coluna 0
      0x10,                      // coluna alta
    };
    enviarComandos(posicao, 3);

    Wire.beginTransmission(ENDERECO_I2C);
    Wire.write(DADO);
    for (int coluna = 0; coluna < LARGURA; coluna++) {
      Wire.write(buffer[(pagina * LARGURA) + coluna]);
    }
    Wire.endTransmission();
  }
}

// Apaga o buffer inteiro. Sem isso, o texto antigo fica embaixo do novo.
void limparTela() {
  for (int i = 0; i < TAMANHO_BUFFER; i++) {
    buffer[i] = 0x00;
  }
}

// Escreve texto no buffer. Cada caractere ocupa 6 colunas, porque a fonte
// basica do controlador tem 5 de largura mais 1 de espaco.
void escreverTexto(const char *texto, int coluna, int linha) {
  int posicao = (linha / 8) * LARGURA + coluna;
  while (*texto != 0 && posicao < TAMANHO_BUFFER) {
    buffer[posicao] = (uint8_t)(*texto);
    texto++;
    posicao++;
  }
}

// Liga o display com a sequencia de inicializacao do controlador. Este e o
// unico bloco de codigo que o aluno nao consegue deduzir: os valores sao do
// datasheet, nao da logica. E honesto dizer isso em voz alta na aula.
void iniciarDisplay() {
  Wire.begin(PINO_SDA, PINO_SCL);
  delay(100);

  const uint8_t inicializacao[] = {
    0xAE,        // display desligado enquanto configurar
    0x20, 0x00,  // endereco de memoria comecando na pagina 0
    0xB0,        // pagina 0
    0xC8,        // display de cima para baixo
    0x00, 0x10,  // comeca na coluna 0
    0x40,        // comeca na linha 0
    0x81, 0x7F,  // contraste medio
    0xA1,        // espelhamento horizontal (tipo SH1106)
    0xA6,        // display normal, nao invertido
    0xA8, 0x3F,  // saida com 64 linhas
    0xA4,        // saida normal do RAM do display
    0xD3, 0x00,  // sem deslocamento vertical
    0xD5, 0x80,  // relogio do display
    0xD9, 0xF1,  // pre-carga
    0xDA, 0x12,  // pinos de saida configrados como I2C
    0xDB, 0x40,  // tensao do painel
    0x8D, 0x14,  // carga da bomba de corrente interna
    0xAF,        // display ligado
  };

  enviarComandos(inicializacao, sizeof(inicializacao));
  delay(100);
  limparTela();
  atualizarDisplay();
}

// Procura o display no barramento e diz se alguem respondeu. Esta funcao e o
// "o cabo esta na placa?" do dia 9, e e a primeira coisa a checar quando a
// tela fica preta.
bool displayConectado() {
  Wire.beginTransmission(ENDERECO_I2C);
  return Wire.endTransmission() == 0;
}

// Tela que mostra duas leituras e o estado do botao. Esta e a primeira
// interface do curso: nao e um display que mostra dado, e uma tela que mostra
// estado. A diferenca aparece no problema de quando atualizar.

#include <string.h>

// Pino analogico do LDR, o mesmo sensor do dia 8.
const int PINO_LDR = 34;

// Botao com resistor de puxar para cima interno. Com o pull-up, o pino fica
// HIGH quando ninguem aperta e LOW quando aperta: por isso a leitura e invertida.
const int PINO_BOTAO = 27;

// Valor de leitura que separa escuro de claro.
const int LIMIAR_ESCURO = 1800;

// Tempo entre uma atualizacao de tela e a seguinte, em milissegundos.
const int INTERVALO_TELA = 500;

// Ultima vez que a tela foi redesenhada. O professor pergunta por que esta
// linha existe, e a resposta e: redesenhar a tela custa tempo que o programa
// nao pode perder.
unsigned long ultimaTela = 0;

int voltas = 0;
bool ambienteEscuro = false;

void setup() {
  pinMode(PINO_LDR, INPUT);
  pinMode(PINO_BOTAO, INPUT_PULLUP);

  // Configuracao do analogico identica a aula 2 do dia 8: 12 bits e faixa
  // ajustada. O display nao muda o ADC, e por isso que a linha se repete.
  analogReadResolution(12);
  analogSetPinAttenuation(PINO_LDR, ADC_11db);

  iniciarDisplay();

  Serial.begin(115200);
  Serial.println();
  Serial.println("Interface: sensor + botao no display");
  Serial.println("bruta | estado | botao");
}

void loop() {
  int leitura = analogRead(PINO_LDR);
  ambienteEscuro = (leitura < LIMIAR_ESCURO);

  // Inversao por causa do pull-up: o pino em HIGH significa botao solto.
  bool botaoApertado = (digitalRead(PINO_BOTAO) == LOW);

  // so redesenha a tela no intervalo. Fora dele, o monitor serial continua
  // recebendo leitura: sao dois instrumentos com ritmos diferentes.
  if (millis() - ultimaTela >= INTERVALO_TELA) {
    ultimaTela = millis();
    voltas++;

    char linhaLuz[16];
    char linhaBotao[16];
    snprintf(linhaLuz, sizeof(linhaLuz), "LUZ %d", leitura);
    if (botaoApertado) {
      snprintf(linhaBotao, sizeof(linhaBotao), "BOTAO SIM");
    } else {
      snprintf(linhaBotao, sizeof(linhaBotao), "BOTAO NAO");
    }

    limparTela();
    escreverTexto("CENTRAL", 0, 0);
    escreverTexto(linhaLuz, 0, 16);
    escreverTexto(linhaBotao, 0, 32);
    char linhaVolta[16];
    snprintf(linhaVolta, sizeof(linhaVolta), "N %d", voltas);
    escreverTexto(linhaVolta, 0, 48);
    atualizarDisplay();

    Serial.print(leitura);
    Serial.print("   | ");
    Serial.print(ambienteEscuro ? "ESCURO" : "CLARO ");
    Serial.print("  | ");
    Serial.println(botaoApertado ? "APERTADO" : "solto  ");
  }

  delay(50);
}

Sem saída de compilação gravada. Rode python3 validar.py -t 1 dia09 aula2.