Aula 45 — Display LCD I2C

Robótica com C — Bloco 2

Por que o LCD I2C?

Na Aula 41d você viu o LCD em modo paralelo — 6 fios de dados + controle, totalizando até 12 conexões. Para projetos com motores, sensores e Bluetooth, esses pinos fazem falta. O módulo adaptador I2C soldado atrás do LCD resolve isso: reduz toda a comunicação para apenas 2 fios (SDA e SCL), que no Arduino Uno ficam nos pinos A4 e A5 — dedicados ao barramento I2C e não conflitam com nada.

Aspecto LCD Paralelo LCD I2C
Fios de dados 6 pinos digitais (D2–D7 ou similar) 0 — comunicação via barramento
Fios totais até 12 conexões 4 — GND, VCC, SDA, SCL
Biblioteca LiquidCrystal.h LiquidCrystal_I2C.h
Endereço I2C Não aplicável Geralmente 0x27 ou 0x3F
Custo extra Nenhum Módulo adaptador PCF8574 (~R$5)
Ideal para Aprendizado inicial Projetos com múltiplos componentes

O protocolo I2C em uma linha: SDA (Serial Data) e SCL (Serial Clock) formam um barramento compartilhado onde vários dispositivos coexistem, cada um com um endereço único. O Arduino é o mestre — ele chama o endereço do LCD, e só aquele dispositivo responde. Você já usou I2C sem perceber no RTC DS3231 da Aula 41d.

Instalando a biblioteca LiquidCrystal_I2C

A biblioteca não vem com a IDE. Instale uma vez e ela fica disponível para todos os projetos:

  1. Na IDE Arduino: Sketch → Incluir Biblioteca → Gerenciar Bibliotecas
  2. Pesquise por LiquidCrystal I2C
  3. Instale a versão de Frank de Brabander (a mais usada e estável)
  4. Pronto — o #include já funciona

No Wokwi: a biblioteca já está disponível nativamente. Adicione um componente LCD 1602 I2C no diagrama e conecte: GND→GND, VCC→5V, SDA→A4, SCL→A5. O endereço padrão no simulador é 0x27.

Ligação física — 4 fios apenas

Pino do módulo I2C Conectar em Função
GND GND do Arduino Terra / referência
VCC 5V do Arduino Alimentação
SDA Pino A4 do Uno Dados seriais
SCL Pino A5 do Uno Clock serial

Contraste apagado? O módulo I2C tem um pequeno potenciômetro azul na parte traseira. Se o LCD ligar mas não mostrar nada, gire esse potenciômetro com uma chave de fenda até os caracteres aparecerem. É o ajuste de contraste — e a causa número 1 de "o LCD não funciona" em hardware físico.

Descobrindo o endereço I2C: a maioria dos módulos usa 0x27, mas alguns usam 0x3F. Se o LCD não inicializar, rode o sketch de scanner I2C abaixo para descobrir o endereço correto do seu módulo:

#include <Wire.h>

void setup() {
    Wire.begin();
    Serial.begin(9600);
    Serial.println("Procurando dispositivos I2C...");

    for (byte addr = 1; addr < 127; addr++) {
        Wire.beginTransmission(addr);
        if (Wire.endTransmission() == 0) {
            Serial.print("Encontrado: 0x");
            Serial.println(addr, HEX);
        }
    }
}
void loop() {}

Estrutura básica do código

A API da LiquidCrystal_I2C é quase idêntica à do LCD paralelo — as mesmas funções, só muda a inicialização:

// LCD I2C 16×2
// Endereço 0x27, 16 colunas, 2 linhas

#include <Wire.h>
#include <LiquidCrystal_I2C.h>

LiquidCrystal_I2C lcd(0x27, 16, 2);  // endereço, colunas, linhas

void setup() {
    lcd.init();         // inicializa o módulo I2C
    lcd.backlight();    // liga a luz de fundo

    lcd.setCursor(0, 0);        // coluna 0, linha 0
    lcd.print("Ola, Arduino!");

    lcd.setCursor(0, 1);        // coluna 0, linha 1
    lcd.print("Curso AFF :)");
}

void loop() { }

Ola, Arduino!   

Curso AFF :)    

Representação do display 16×2

Funções essenciais

Função O que faz Observação
lcd.init() Inicializa o módulo I2C Sempre no setup(), antes de qualquer outra chamada
lcd.backlight() Liga a luz de fundo Sem isso o display fica escuro mesmo funcionando
lcd.noBacklight() Desliga a luz de fundo Útil para economizar energia em modo sleep
lcd.setCursor(col, lin) Posiciona o cursor col: 0–15, lin: 0–1 para display 16×2
lcd.print(valor) Escreve na posição atual Aceita int, float, String, char[]
lcd.clear() Apaga tudo e volta para (0,0) Causa pisca — use espaços para sobrescrever
lcd.createChar(id, mapa) Cria caractere customizado 5×8 px Até 8 caracteres customizados por vez

Exibindo dados dinâmicos: temperatura em tempo real

O padrão correto para atualizar valores no LCD sem causar pisca é: escrever o rótulo fixo uma vez no setup() e atualizar apenas o valor no loop(), usando espaços para limpar dígitos antigos:

/*
 * LCD I2C + LM35 — temperatura em tempo real
 * Linha 0: rótulo fixo "Temperatura:"
 * Linha 1: valor atualizado a cada 500ms
 */

#include <Wire.h>
#include <LiquidCrystal_I2C.h>

LiquidCrystal_I2C lcd(0x27, 16, 2);
const int PINO_LM35 = A0;

void setup() {
    lcd.init();
    lcd.backlight();

    lcd.setCursor(0, 0);
    lcd.print("Temperatura:");    // rótulo — só no setup()
}

void loop() {
    int   bruto = analogRead(PINO_LM35);
    float temp  = bruto * (5.0 / 1023.0) * 100.0;

    lcd.setCursor(0, 1);
    lcd.print(temp, 1);           // ex: "24.3"
    lcd.print(" C      ");        // espaços apagam dígitos anteriores

    delay(500);
}

Regra dos espaços: se o valor anterior tinha mais dígitos que o atual (ex: "100.0" → "24.3"), os dígitos antigos ficam visíveis. Sempre termine o lcd.print() com alguns espaços extras para cobrir o pior caso. Para um float com 1 decimal, " " (6 espaços) é seguro.

Integrando com a Aula 44: semáforo com display

Agora vamos combinar a máquina de estados da Aula 44 com o LCD — o display mostra o estado atual, a contagem regressiva e uma mensagem para o motorista. Este é o primeiro projeto que combina dois subsistemas simultaneamente:

/*
 * Semáforo com LCD I2C
 * Linha 0: estado atual + ícone
 * Linha 1: contagem regressiva em segundos
 */

#include <Wire.h>
#include <LiquidCrystal_I2C.h>

LiquidCrystal_I2C lcd(0x27, 16, 2);

const int PINO_VERDE    = 9;
const int PINO_AMARELO  = 10;
const int PINO_VERMELHO = 11;

typedef enum { VERDE, AMARELO, VERMELHO } EstadoSemaforo;

EstadoSemaforo estado      = VERDE;
unsigned long  ultimaTroca = 0;

unsigned long tempoDuEstado(EstadoSemaforo e) {
    switch (e) {
        case VERDE:    return 4000;
        case AMARELO:  return 3000;
        case VERMELHO: return 5000;
    }
    return 1000;
}

EstadoSemaforo proximoEstado(EstadoSemaforo e) {
    switch (e) {
        case VERDE:    return AMARELO;
        case AMARELO:  return VERMELHO;
        case VERMELHO: return VERDE;
    }
    return VERDE;
}

void atualizarLEDs(EstadoSemaforo e) {
    digitalWrite(PINO_VERDE,    e == VERDE);
    digitalWrite(PINO_AMARELO,  e == AMARELO);
    digitalWrite(PINO_VERMELHO, e == VERMELHO);
}

// Atualiza o LCD com estado atual e tempo restante
void atualizarLCD(EstadoSemaforo e, unsigned long restante) {
    lcd.setCursor(0, 0);
    switch (e) {
        case VERDE:    lcd.print("VERDE   - SIGA  "); break;
        case AMARELO:  lcd.print("AMARELO - ATENC "); break;
        case VERMELHO: lcd.print("VERMELHO- PARE  "); break;
    }

    lcd.setCursor(0, 1);
    lcd.print("Restam: ");
    lcd.print(restante / 1000);   // ms → segundos
    lcd.print("s      ");
}

void setup() {
    pinMode(PINO_VERDE,    OUTPUT);
    pinMode(PINO_AMARELO,  OUTPUT);
    pinMode(PINO_VERMELHO, OUTPUT);

    lcd.init();
    lcd.backlight();

    atualizarLEDs(estado);
    ultimaTroca = millis();
    atualizarLCD(estado, tempoDuEstado(estado));
}

void loop() {
    unsigned long agora    = millis();
    unsigned long decorrido = agora - ultimaTroca;
    unsigned long duracao  = tempoDuEstado(estado);

    // Atualiza o display a cada 500ms (não a cada iteração — economiza ciclos)
    static unsigned long ultimaTelaAtualizada = 0;
    if (agora - ultimaTelaAtualizada >= 500) {
        ultimaTelaAtualizada = agora;
        unsigned long restante = (decorrido < duracao) ? duracao - decorrido : 0;
        atualizarLCD(estado, restante);
    }

    // Transição de estado
    if (decorrido >= duracao) {
        estado      = proximoEstado(estado);
        ultimaTroca = agora;
        atualizarLEDs(estado);
    }
}

VERDE - SIGA

Restam: 3s



AMARELO - ATENC

Restam: 2s

Variável static local: static unsigned long ultimaTelaAtualizada = 0 dentro do loop() é inicializada apenas uma vez — como uma variável global, mas com escopo local à função. Você usou static em C puro nas aulas de funções; aqui serve para controlar frequência de atualização do LCD sem declarar uma variável global a mais.

Caracteres customizados — ícones no LCD

O LCD permite criar até 8 ícones de 5×8 pixels. Cada linha do caractere é uma máscara de 5 bits representada como um byte. É manipulação de bits na prática — o mesmo &, | e << que você usou na Aula 41b:

// Ícone de termômetro — 5 colunas × 8 linhas
byte termometro[8] = {
    0b00100,   // linha 0 (topo): coluna do meio
    0b01010,   // linha 1
    0b01010,   // linha 2
    0b01110,   // linha 3
    0b01110,   // linha 4
    0b11111,   // linha 5
    0b11111,   // linha 6
    0b01110    // linha 7 (base): bolbo
};

void setup() {
    lcd.init();
    lcd.backlight();

    lcd.createChar(0, termometro);  // registra no slot 0

    lcd.setCursor(0, 0);
    lcd.write(0);                   // exibe o caractere customizado do slot 0
    lcd.print(" Temp: 24.3C");
}

Gerador online de caracteres: acesse maxpromer.github.io/LCD-Character-Creator para desenhar ícones visualmente e gerar o array automaticamente. Útil para criar símbolos de grau (°), setinhas, ícones de bateria ou qualquer símbolo que o LCD não tem nativamente.

C vs Arduino — Display LCD I2C

Conceito C puro Arduino + LCD I2C
Exibir texto printf("texto") no terminal lcd.print("texto") no display físico
Posicionar saída Sequencial — sem controle de posição lcd.setCursor(col, lin) — posição exata
Exibir float printf("%.1f", temp) lcd.print(temp, 1) — segundo parâmetro = casas decimais
Protocolo de comunicação Não aplicável (saída direta) I2C via Wire.h — endereço 0x27 ou 0x3F
Variável local persistente static dentro de função static dentro do loop() — idêntico
Caracteres customizados Não existe Bitmap 5×8 com createChar() + manipulação de bits

Quiz — Aula 45

Teste o que você aprendeu sobre o display LCD I2C:

1. Qual a principal vantagem do LCD I2C sobre o LCD paralelo em projetos com múltiplos componentes?

2. Quais pinos do Arduino Uno são usados para SDA e SCL do barramento I2C?

3. O LCD I2C inicializou mas não mostra nada na tela física. Qual é a causa mais provável?

4. Por que o rótulo "Temperatura:" é escrito no setup() e não no loop()?

5. O que acontece se você não adicionar espaços após lcd.print(temp, 1) quando o valor muda de "100.0" para "24.3"?

6. Para que serve static unsigned long ultimaTelaAtualizada = 0 dentro do loop()?

7. Quantos caracteres customizados o LCD pode armazenar simultaneamente?

8. No projeto semáforo + LCD, por que o display é atualizado a cada 500ms e não a cada iteração do loop()?