Displays
I2C na ESP32: OLED SSD1306 e LCD 16×2 com MicroPython
O barramento I2C na ESP32 — endereços, scan, pull-ups e erros ENODEV — e como usar o OLED SSD1306 e o LCD 16×2 com PCF8574 em MicroPython, sem sobras na tela.
Por Prof. Rodrigo Faustino · Atualizado em · 12 min de leitura
Um projeto com sensor fica muito mais interessante quando mostra o resultado numa tela, longe do computador. Os dois displays mais comuns em laboratório — o OLED SSD1306 de 128 × 64 pontos e o LCD 16×2 com o módulo I2C — conversam com a ESP32 pelo mesmo caminho: o I2C, um barramento de dois fios que vários dispositivos dividem ao mesmo tempo.
Este artigo começa pelo barramento, que é onde moram quase todos os problemas ("o display não mostra nada"), e depois estuda cada display: o OLED, que é desenhado ponto a ponto numa cópia da tela na memória, e o LCD, que mostra caracteres prontos e tem as suas próprias manias — 5 V, contraste e números que "sobram" na tela.
O barramento I2C
O I2C (Inter-Integrated Circuit, lê-se "i-dois-cê") usa duas linhas:
- SDA (serial data): por onde os bytes passam, nos dois sentidos;
- SCL (serial clock): o relógio. A ESP32 marca o ritmo, e cada pulso transporta um bit.
Além delas, cada módulo precisa de alimentação e do GND comum com a placa. Numa montagem típica, o OLED e o LCD ficam pendurados nos mesmos dois fios: SDA no GPIO21 e SCL no GPIO22.
Endereços
Se todos escutam os mesmos fios, como a ESP32 fala com um só? Cada módulo tem um endereço de 7 bits, e toda conversa começa com ele. Quem não é chamado fica quieto. Os endereços dos dois displays deste artigo são:
- OLED SSD1306: 0x3C (60 em decimal); alguns módulos vêm com 0x3D (61);
- LCD 16×2 com o PCF8574: 0x27 (39); alguns vêm com 0x3F (63).
O 0x indica um número em hexadecimal, o jeito como os manuais escrevem endereços. Em Python, 0x3C e 60 são o mesmo número; hex(60) escreve '0x3c'.
Um aviso sobre manuais: o endereço tem 7 bits, mas no fio ele viaja junto com um oitavo bit que diz se a conversa é de escrita ou de leitura. Alguns manuais escrevem o endereço já "deslocado" com esse bit — 0x78 em vez de 0x3C, 0x4E em vez de 0x27. O MicroPython sempre usa a forma de 7 bits, a mesma que o scan() devolve. Se um manual mostra um endereço que parece o dobro do esperado, divida por dois.
Ninguém empurra o 1: os pull-ups
No I2C, nem a ESP32 nem os módulos colocam a linha em 1. Todos só sabem puxar para 0 ou soltar (saídas de dreno aberto). É isso que permite vários aparelhos dividirem o fio sem curto-circuito: se um puxa e os outros soltam, ninguém briga. Quando todos soltam, quem leva a linha para 1 são os resistores de pull-up, do SDA e do SCL para a alimentação.
Os módulos de OLED e LCD costumam trazer esses resistores na plaquinha (os do simulador trazem). O I2C(0) e o I2C(1) da ESP32 ainda ligam o pull-up interno do pino — que é fraco, da ordem de dezenas de kΩ, mas ajuda. O SoftI2C, o I2C feito por software em qualquer par de pinos, não liga nada. Sem pull-up nenhum, o simulador mostra o aviso i2c-sem-pullup: sem eles, a linha SDA fica em 0, todo endereço parece responder e ninguém entende nada.
Da ementa ao semestre planejado
- Plano de ensino e calendário montados sozinhos
- Diário de aulas pronto para colar no SIGA
- Slides e listas de exercícios com gabarito
Versão Community grátis · Windows
O primeiro teste: scan()
Antes de escrever qualquer código de display, o primeiro teste com um módulo I2C novo é perguntar quem está no barramento. O scan() chama todos os endereços válidos (de 0x08 a 0x77), um por um, e devolve a lista dos que responderam:
from machine import I2C, Pin
i2c = I2C(0, scl=Pin(22), sda=Pin(21))
enderecos = i2c.scan()
print('responderam', len(enderecos), 'módulos:', enderecos)
for e in enderecos:
print(e, 'em hexadecimal é', hex(e))O resultado neste circuito é [39, 60]: o LCD e o OLED. Se um módulo não aparece no scan(), não adianta escrever o resto do programa — o problema é de fio, alimentação ou endereço.
I2C(0), I2C(1) e SoftI2C
A ESP32 tem dois controladores de I2C em hardware: I2C(0) e I2C(1) (I2C(2) dá ValueError). Os pinos são escolhidos ao criar o objeto; sem escolher, o I2C(0) usa SCL no 18 e SDA no 19, e o I2C(1), SCL no 25 e SDA no 26. A frequência padrão é 400 kHz, o "modo rápido" do I2C.
Como acontece com Pin, o I2C(0) devolve sempre o mesmo objeto: criar de novo só muda o que for passado. Já o SoftI2C(scl, sda) cria um objeto novo a cada chamada e funciona em quaisquer pinos de saída. Ele existe para quando os dois controladores estão ocupados ou os pinos não ajudam, e é mais lento e mais exigente com os pull-ups: sem eles, o SCL não sobe, o SoftI2C espera o tempo-limite e dá OSError: [Errno 116] ETIMEDOUT.
Quando dá errado
Os erros do I2C seguem padrões, e conhecê-los economiza horas:
- ninguém respondeu no endereço:
OSError: [Errno 19] ENODEV. Confira o endereço (0x3C × 0x3D, 0x27 × 0x3F), a alimentação e os fios; - SDA e SCL trocados: o
scan()volta vazio. O simulador identifica o caso e mostra o aviso i2c-trocado; - dois módulos com o mesmo endereço no mesmo barramento: aviso i2c-endereco-repetido. Troque o endereço de um deles (quando o módulo deixa) ou use o outro controlador;
- pinos 34 a 39: são só de entrada e não servem para SDA nem SCL.
Quanto tempo leva uma conversa
A 400 kHz, cada bit dura 2,5 µs, e cada byte ocupa 9 bits no fio: os 8 do dado e um de confirmação (o ACK, com que o receptor diz "recebi"). Mandar 1 KB leva, então, cerca de 23 ms. Parece pouco, mas é tempo em que o programa está parado — e vai importar no OLED.
OLED SSD1306: desenhe na memória, depois mostre
O OLED tem 128 × 64 pontos que acendem sozinhos (não há luz de fundo) e um chip controlador, o SSD1306. Para usá-lo, o MicroPython precisa de um driver, e aqui está a primeira surpresa: o firmware da ESP32 não traz o driver do SSD1306. Os módulos que vêm congelados no firmware são poucos (dht, neopixel, onewire, ds18x20, umqtt e mais alguns); o ssd1306.py é um arquivo Python comum da biblioteca oficial micropython-lib, que fica ao lado do seu main.py.
No PyPlaca, o botão "Abrir no simulador" do exemplo abaixo coloca o ssd1306.py no projeto. Abra a aba dele e leia: são menos de 200 linhas que mandam comandos com i2c.writeto(), nada de mágico. Na placa real, o arquivo também precisa estar na ESP32, e a exportação da IDE já o inclui. Sem o arquivo, import ssd1306 dá ImportError: no module named 'ssd1306'.
from machine import I2C, Pin
import ssd1306
import time
i2c = I2C(0, scl=Pin(22), sda=Pin(21))
oled = ssd1306.SSD1306_I2C(128, 64, i2c)
oled.fill(0) # apaga tudo (só na memória)
oled.text('Ola, ESP32!', 0, 0) # texto com o canto em x = 0, y = 0
oled.text('PyPlaca', 0, 16)
oled.rect(0, 32, 128, 24, 1) # moldura: x, y, largura, altura, cor
oled.hline(8, 44, 112, 1) # linha deitada dentro da moldura
oled.show() # agora sim: manda tudo para a tela
while True: # mantém a tela ligada no simulador
time.sleep(1)
oled.show().O frame buffer
O driver guarda uma cópia da tela na memória da ESP32: um bytearray de 128 × 64 ÷ 8 = 1024 bytes, um bit por ponto. Esse é o frame buffer. Todos os métodos de desenho — fill, pixel, text, hline, vline, line, rect, fill_rect, ellipse — mexem só nessa cópia, sem tocar no I2C. Eles vêm do módulo framebuf, que faz parte do firmware e que o simulador traduz linha a linha do código C do MicroPython.
É o show() que envia a cópia inteira para o display, de uma vez. E isso leva tempo: os 1024 bytes, mais o endereço e um byte de controle, passam pelo I2C em cerca de 23 ms a 400 kHz. Daí a regra de ouro do OLED: desenhe tudo primeiro e chame show() uma vez por quadro. Chamar show() depois de cada texto deixa o programa lento e faz a tela aparecer pela metade.
Coordenadas e texto
O ponto (0, 0) é o canto de cima à esquerda. O x cresce para a direita, até 127; o y cresce para baixo, até 63. O text() usa uma fonte de 8 × 8 pontos: cabem 16 letras por linha e 8 linhas de texto, em y = 0, 8, 16 e assim por diante até 56.
Dois cuidados com o texto:
- a fonte não tem acentos. Cada caractere acima do ASCII vira um quadradinho xadrez. Escreva
Ola, nãoOlá; - o
text()não apaga o fundo: ele só acende os pontos da letra. Escrever2em cima de um3deixa os dois misturados. Para trocar um texto, apague antes —fill(0)limpa a tela toda,fill_rect(x, y, largura, 8, 0)limpa só um pedaço.
Outros métodos úteis do driver: invert(1) troca aceso por apagado na tela inteira, contrast(x) muda o brilho e poweroff()/poweron() desligam e ligam o painel.
Um mostrador que se atualiza
Juntando a regra do show() com a do fill_rect, um contador que se atualiza fica assim:
from machine import I2C, Pin
import ssd1306
import time
i2c = I2C(0, scl=Pin(22), sda=Pin(21))
oled = ssd1306.SSD1306_I2C(128, 64, i2c)
oled.fill(0)
oled.text('Contagem', 0, 0)
for n in range(10, -1, -1):
oled.fill_rect(0, 24, 128, 8, 0) # apaga só a linha do número
oled.text('%2d' % n, 56, 24)
oled.show() # um envio por quadro
time.sleep_ms(500)
while True:
time.sleep(1)O título é desenhado uma vez só; a cada volta, apenas a faixa do número é apagada e redesenhada.
LCD 16×2: caracteres prontos
O LCD 16×2 mostra 2 linhas de 16 caracteres. Diferente do OLED, ele não desenha pontos soltos: cada posição mostra um caractere pronto, tirado de uma tabela gravada no chip do LCD, o HD44780. Cada caractere é uma grade de 5 × 8 pontos, e dá para criar até 8 caracteres próprios (um sino, um coração, uma seta).
Sozinho, o HD44780 é ligado em paralelo, com três sinais de controle (RS, RW e E) e quatro ou oito fios de dados, além da alimentação. A plaquinha presa atrás do LCD, com o chip PCF8574, faz a ponte: recebe os comandos pelo I2C e mexe nos pinos do LCD. Por isso bastam quatro fios: GND, VCC, SDA e SCL.
O driver também não vem no firmware e é dividido em dois arquivos:
lcd_api.py: os comandos do HD44780 (limpar, posicionar, escrever), sem saber como o LCD está ligado;i2c_lcd.py: como mandar esses comandos pelo PCF8574. Ele importa olcd_api, então os dois precisam estar no projeto (e na placa).
from machine import I2C, Pin
from i2c_lcd import I2cLcd
import time
i2c = I2C(0, scl=Pin(22), sda=Pin(21))
lcd = I2cLcd(i2c, 0x27, 2, 16) # endereço, linhas, colunas
lcd.putstr('Ola, LCD!')
lcd.move_to(0, 1) # coluna 0, linha 1 (a de baixo)
lcd.putstr('16x2 no I2C')
while True: # mantém a tela ligada no simulador
time.sleep(1)Os métodos mais usados:
putstr('texto')escreve a partir do cursor ('\n'pula para a outra linha);move_to(coluna, linha)posiciona o cursor — repare: primeiro a coluna, de 0 a 15, depois a linha, 0 ou 1;clear()apaga tudo e volta o cursor para o começo;backlight_on()/backlight_off()ligam e desligam a luz de fundo;show_cursor()/hide_cursor()mostram ou escondem o cursor.
Por que o LCD vai no VIN
O vidro do LCD precisa de 5 V para mostrar os caracteres; o VCC dele vai no VIN da placa. A parte digital funciona com menos: ligado no 3V3, o LCD responde no scan(), mas a tela fica vazia. O simulador mostra o aviso lcd-sem-5v nesse caso. É um dos "o programa está certo e não aparece nada" mais comuns da bancada.
O contraste
Atrás do módulo há um trimpot de contraste. Fora do ponto, a tela parece vazia mesmo com tudo certo. Quando o scan() acha o 0x27 e a tela não mostra nada, este é o primeiro lugar para olhar. No simulador, o trimpot fica no painel da peça: arraste até a ponta e veja o texto sumir, com o programa continuando igual.
Sem acentos aqui também
A tabela de caracteres do HD44780 tem letras sem acento, números e sinais. Letras acentuadas viram outro símbolo. Escreva Ola, Temp, Umid. Um caractere útil da tabela é o 223, o símbolo de grau: lcd.putstr(chr(223)).
A IA que lembra da sua vida e mora no seu computador
- Memória de longo prazo com a fonte de cada resposta
- Funciona offline: seus dados ficam no seu PC
- Lê PDFs, slides, fotos e gravações de reunião
Plano Essencial grátis para sempre
Números que sobram na tela
Um LCD não apaga sozinho: cada caractere fica lá até alguém escrever outro na mesma posição. Rode uma contagem regressiva de 12 a 0 e olhe a passagem do 10 para o 9: aparece 90. O 10 ocupou duas posições; o 9 só escreveu por cima da primeira, e o 0 antigo continuou lá. O mesmo acontece com o OLED se você não apagar o fundo.
Três jeitos de resolver, do mais bruto ao mais elegante:
- limpar tudo com
clear()e escrever de novo. Funciona, mas apaga também o que não mudou e a tela pode piscar; - escrever espaços depois do número, para cobrir o que sobrou:
putstr(str(n) + ' '); - formatar com largura fixa, para o número ocupar sempre o mesmo espaço.
from machine import I2C, Pin
from i2c_lcd import I2cLcd
import time
i2c = I2C(0, scl=Pin(22), sda=Pin(21))
lcd = I2cLcd(i2c, 0x27, 2, 16)
lcd.putstr('Contagem:')
for n in range(12, -1, -1):
lcd.move_to(0, 1)
lcd.putstr('%3d' % n) # sempre 3 posições, alinhado à direita
time.sleep_ms(400)
while True:
time.sleep(1)A formatação com % é a ferramenta dos painéis: '%3d' % 7 dá ' 7' (três posições, alinhado à direita), '%-3d' % 7 dá '7 ' (alinhado à esquerda) e '%5.1f' % 9.5 dá ' 9.5' (cinco posições com uma casa decimal). Com ela, cada informação ganha o seu pedaço da tela, e cada atualização reescreve o pedaço inteiro.
OLED ou LCD?
Os dois resolvem o problema de mostrar números, mas têm vocações diferentes:
| OLED SSD1306 | LCD 16×2 com PCF8574 | |
|---|---|---|
| O que mostra | pontos livres: texto, linhas, gráficos | 32 caracteres prontos |
| Endereço I2C | 0x3C (ou 0x3D) | 0x27 (ou 0x3F) |
| Alimentação | 3,3 V | 5 V (no VIN) |
| Driver | ssd1306.py | lcd_api.py + i2c_lcd.py |
| Atualizar | desenhar na memória e show() | escrever na posição do cursor |
| Acentos | não | não |
O OLED é a escolha para gráficos, ícones e telas mais ricas; o LCD é imbatível em legibilidade de longe e simplicidade de uso. Como os dois falam I2C em endereços diferentes, nada impede usar os dois no mesmo barramento, como no circuito do scan().
Roteiro de diagnóstico
Quando um display I2C não mostra nada, siga esta ordem:
- o módulo aparece no
scan()? Se não: fios, alimentação, SDA/SCL trocados, GND comum; - o endereço do código é o que o
scan()mostrou? - o driver (
ssd1306.pyoulcd_api.py+i2c_lcd.py) está no projeto? - no OLED: o programa chama
show()? - no LCD: o VCC está no VIN (5 V)? O contraste está no ponto?
- no simulador: o programa continua rodando? Quando ele termina, a bancada desliga e as telas apagam — por isso os exemplos acabam num
while Truecomsleep.
O próximo artigo, Programar sem travar, mostra como atualizar uma tela, piscar um LED e atender um botão ao mesmo tempo, sem que um sleep trave tudo. Na trilha de displays você pratica o scan(), o OLED, o LCD e ainda o anel de LEDs NeoPixel, que usa um protocolo de um fio só.
Para praticar no PyPlaca
Trilhas
Lições
- I2C: dois fios, vários módulos
- OLED: desenhe na memória, depois mostre
- LCD 16×2 pelo I2C
- Números que mudam: o que sobra na tela