O display OLED — Arduino e IoT — semana 9 do 1o trimestre
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
OLEDde 0,96 polegada emSDAnoGPIO21eSCLnoGPIO22. - 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 bibliotecaAdafruit_SSD1306faria 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
OLEDI2C de 0,96 polegada (128x64, controladorSSD1306) 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.
| Fio | Nome | Para onde vai | O que carrega |
|---|---|---|---|
| 1 | SDA | GPIO21 do ESP32 | o dado, nos dois sentidos |
| 2 | SCL | GPIO22 do ESP32 | o 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 pacote | Para onde vai | Exemplo |
|---|---|---|
0x00 | registro de comandos | ligar o display, escolher contraste |
0x40 | registro de dados, comecando na coluna 0 | os 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:
limparTela— zera os 1024 bytes do bufferescreverTexto— escreve os caracteres nas posicoesatualizarDisplay— 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:
VCCdo display na trilha+da protoboard.GNDdo display na trilha-.SDAdo display noGPIO21.SCLdo display noGPIO22.
Nota: 10 pontos. Critério de fim: o display mostra o número da volta e o monitor serial mostra o mesmo número.
- Antes de ligar qualquer coisa, rode o sketch de varrimento de endereco que o professor projetou, com o display desconectado. Quantas placas responderam?
- Ligue o circuito do enunciado e rode o varrimento de novo. Qual endereco apareceu? Era
0x3C? - Grave o sketch da resolucao e confirme que o número da tela e o do serial batem durante dez segundos.
- Troque a constante
ENDERECO_I2Cde0x3Cpara0x3De grave, sem trocar nenhum fio. Descreva o que o serial responde e o que a tela faz. Isso prova o que, sobre endereco? - Comente a linha do
limparTeladentro doloope grave. Espere a tela mostrar varios números. Explique em uma frase por que o texto antigo fica embaixo do novo. - Descomente o
limparTelae troqueVARIOSde 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ério | Pontos |
|---|---|
Display ligado em GPIO21 e GPIO22, com VCC e GND conferidos | 2 pontos |
Endereco I2C descoberto por varrimento, e não chute de 0x3C | 2 pontos |
| Ciclo de tela na ordem limpar, escrever, atualizar | 3 pontos |
| O buffer tem 1024 bytes, e o código explica a conta | 1 ponto |
| Item 4 respondido: o endereco vem do hardware, não do código | 2 pontos |
Erros comuns
| Erro | Como aparece | Correcao |
|---|---|---|
| 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 trocados | Display 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 22 | Nada funciona, sem erro | "No ESP32 esses dois pinos são fixos do I2C. Não ha escolha." |
Endereco 0x3D com módulo 0x3C | Serial 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 pixels | Display 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
millispara decidir quando redesenhar a tela, sem travar o programa comdelaylongo. - 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
OLEDI2C de 0,96 polegada (128x64, controladorSSD1306) por aluno - 1
LDRe 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 botao | digitalRead | Significado |
|---|---|---|
| Solto | HIGH | ninguem apertou |
| Apertado | LOW | apertado |
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:
VCCdo display na trilha+,GNDna trilha-,SDAnoGPIO21,SCLnoGPIO22.- Uma perna do
LDRna trilha+, a outra noGPIO34, com um resistor de 10 k doGPIO34para 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.
- Monte o circuito do enunciado e grave o sketch da resolucao.
- 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
HIGHsignifica "solto" e não "apertado". - Botao sem nenhum fio no
GPIO27epinModeemINPUT(sem pull-up). Grave e anote o que o serial imprime enquanto ninguem mexe. Explique em uma frase o que e uma entrada flutuante. - Volte para
INPUT_PULLUPe 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? - Troque
INTERVALO_TELAde 500 para 2000 e repita a atividade 4. Em uma frase: por que o serial registrou todos os toques e a tela não. - Acrescente uma quinta linha com a tensão lida em
analogReadconvertida 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ério | Pontos |
|---|---|
Display, LDR e botao montados, com o botao em INPUT_PULLUP | 2 pontos |
Botao lido fora do if do millis, e a razão disso explicada | 2 pontos |
| Tela com ciclo limpar, escrever, atualizar, e campos rotulados | 2 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 escrita | 2 pontos |
Erros comuns
| Erro | Como aparece | Correcao |
|---|---|---|
Ler botao com == HIGH como apertado | Tela 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.
