Saltar al contenido
IDE libre

Pantallas

I2C en la ESP32: OLED SSD1306 y LCD 16×2 con MicroPython

El bus I2C en la ESP32 (direcciones, scan, pull-ups, ENODEV) y cómo usar el OLED SSD1306 y el LCD 16×2 con PCF8574 en MicroPython, sin restos en la pantalla.

Por Prof. Rodrigo Faustino · Actualizado el · 12 min de lectura

Métricas de este artículo

Palabras
2354
Caracteres
13.121
Tiempo de lectura
~12 min

Un proyecto con sensor se vuelve mucho más interesante cuando muestra el resultado en una pantalla, lejos del computador. Los dos displays más comunes en el laboratorio, el OLED SSD1306 de 128 × 64 puntos y el LCD 16×2 con el módulo I2C, conversan con la ESP32 por el mismo camino: el I2C, un bus de dos cables que varios dispositivos comparten al mismo tiempo.

Este artículo empieza por el bus, que es donde viven casi todos los problemas ("el display no muestra nada"), y después estudia cada display: el OLED, que se dibuja punto a punto en una copia de la pantalla en la memoria, y el LCD, que muestra caracteres ya hechos y tiene sus propias manías: 5 V, contraste y números que "sobran" en la pantalla.

El bus I2C

El I2C (Inter-Integrated Circuit, se lee "i-dos-ce") usa dos líneas:

  • SDA (serial data): por donde pasan los bytes, en los dos sentidos;
  • SCL (serial clock): el reloj. La ESP32 marca el ritmo, y cada pulso transporta un bit.

Además de ellas, cada módulo necesita alimentación y el GND común con la placa. En un montaje típico, el OLED y el LCD quedan colgados de los mismos dos cables: SDA en el GPIO21 y SCL en el GPIO22.

Direcciones

Si todos escuchan los mismos cables, ¿cómo habla la ESP32 con uno solo? Cada módulo tiene una dirección de 7 bits, y toda conversación empieza con ella. Quien no es llamado se queda callado. Las direcciones de los dos displays de este artículo son:

  • OLED SSD1306: 0x3C (60 en decimal); algunos módulos vienen con 0x3D (61);
  • LCD 16×2 con el PCF8574: 0x27 (39); algunos vienen con 0x3F (63).

El 0x indica un número en hexadecimal, la manera en que los manuales escriben las direcciones. En Python, 0x3C y 60 son el mismo número; hex(60) escribe '0x3c'.

Un aviso sobre los manuales: la dirección tiene 7 bits, pero en el cable viaja junto con un octavo bit que dice si la conversación es de escritura o de lectura. Algunos manuales escriben la dirección ya "desplazada" con ese bit: 0x78 en lugar de 0x3C, 0x4E en lugar de 0x27. MicroPython siempre usa la forma de 7 bits, la misma que devuelve el scan(). Si un manual muestra una dirección que parece el doble de lo esperado, divídela por dos.

Nadie empuja el 1: los pull-ups

En el I2C, ni la ESP32 ni los módulos ponen la línea en 1. Todos solo saben tirar a 0 o soltar (salidas de drenador abierto). Eso es lo que permite que varios aparatos compartan el cable sin cortocircuito: si uno tira y los otros sueltan, nadie pelea. Cuando todos sueltan, quienes llevan la línea a 1 son las resistencias de pull-up, del SDA y del SCL hacia la alimentación.

Los módulos de OLED y LCD suelen traer esas resistencias en la plaquita (los del simulador las traen). El I2C(0) y el I2C(1) de la ESP32 además activan el pull-up interno del pin, que es débil, del orden de decenas de kΩ, pero ayuda. El SoftI2C, el I2C hecho por software en cualquier par de pines, no activa nada. Sin ningún pull-up, el simulador muestra el aviso i2c-sem-pullup: sin ellos, la línea SDA queda en 0, toda dirección parece responder y nadie entiende nada.

Diagrama del bus I2C: la ESP32 con SDA en el GPIO21 y SCL en el GPIO22, dos líneas horizontales con resistencias de pull-up hacia los 3,3 V, y dos módulos colgados de las mismas líneas: el OLED en la dirección 0x3C y el LCD en la dirección 0x27.
Dos cables, varios módulos: SDA y SCL se comparten, los pull-ups llevan las líneas a 1 y cada módulo responde solo a su dirección.
FastMindAsistente de IA local

La IA que recuerda tu vida y vive en tu ordenador

  • Memoria a largo plazo con la fuente de cada respuesta
  • Funciona sin conexión: tus datos se quedan en tu PC
  • Lee PDF, diapositivas, fotos y grabaciones de reuniones
Descargar gratis para Windows

Plan Esencial gratis para siempre

La primera prueba: scan()

Antes de escribir cualquier código de display, la primera prueba con un módulo I2C nuevo es preguntar quién está en el bus. El scan() llama a todas las direcciones válidas (de 0x08 a 0x77), una por una, y devuelve la lista de las que respondieron:

from machine import I2C, Pin

i2c = I2C(0, scl=Pin(22), sda=Pin(21))

enderecos = i2c.scan()
print('respondieron', len(enderecos), 'módulos:', enderecos)
for e in enderecos:
    print(e, 'en hexadecimal es', hex(e))

El resultado en este circuito es [39, 60]: el LCD y el OLED. Si un módulo no aparece en el scan(), no sirve de nada escribir el resto del programa: el problema es de cable, alimentación o dirección.

I2C(0), I2C(1) y SoftI2C

La ESP32 tiene dos controladores de I2C en hardware: I2C(0) y I2C(1) (I2C(2) da ValueError). Los pines se eligen al crear el objeto; sin elegir, el I2C(0) usa SCL en el 18 y SDA en el 19, y el I2C(1), SCL en el 25 y SDA en el 26. La frecuencia por defecto es 400 kHz, el "modo rápido" del I2C.

Como ocurre con Pin, el I2C(0) devuelve siempre el mismo objeto: crearlo de nuevo solo cambia lo que se pase. El SoftI2C(scl, sda), en cambio, crea un objeto nuevo en cada llamada y funciona en cualquier pin de salida. Existe para cuando los dos controladores están ocupados o los pines no ayudan, y es más lento y más exigente con los pull-ups: sin ellos, el SCL no sube, el SoftI2C espera el tiempo límite y da OSError: [Errno 116] ETIMEDOUT.

Cuando sale mal

Los errores del I2C siguen patrones, y conocerlos ahorra horas:

  • nadie respondió en la dirección: OSError: [Errno 19] ENODEV. Comprueba la dirección (0x3C × 0x3D, 0x27 × 0x3F), la alimentación y los cables;
  • SDA y SCL intercambiados: el scan() vuelve vacío. El simulador identifica el caso y muestra el aviso i2c-trocado;
  • dos módulos con la misma dirección en el mismo bus: aviso i2c-endereco-repetido. Cambia la dirección de uno de ellos (cuando el módulo lo permite) o usa el otro controlador;
  • pines 34 a 39: son solo de entrada y no sirven para SDA ni SCL.

Cuánto tarda una conversación

A 400 kHz, cada bit dura 2,5 µs, y cada byte ocupa 9 bits en el cable: los 8 del dato y uno de confirmación (el ACK, con el que el receptor dice "recibí"). Enviar 1 KB tarda, entonces, cerca de 23 ms. Parece poco, pero es tiempo en que el programa está parado, y va a importar en el OLED.

OLED SSD1306: dibuja en la memoria, después muestra

El OLED tiene 128 × 64 puntos que se encienden solos (no hay luz de fondo) y un chip controlador, el SSD1306. Para usarlo, MicroPython necesita un driver, y aquí está la primera sorpresa: el firmware de la ESP32 no trae el driver del SSD1306. Los módulos que vienen congelados en el firmware son pocos (dht, neopixel, onewire, ds18x20, umqtt y algunos más); el ssd1306.py es un archivo Python común de la biblioteca oficial micropython-lib, que queda junto a tu main.py.

En PyPlaca, el botón "Abrir en el simulador" del ejemplo de abajo pone el ssd1306.py en el proyecto. Abre su pestaña y léelo: son menos de 200 líneas que envían comandos con i2c.writeto(), nada de magia. En la placa real, el archivo también tiene que estar en la ESP32, y la exportación de la IDE ya lo incluye. Sin el archivo, import ssd1306 da 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)                      # borra todo (solo en la memoria)
oled.text('Hola ESP32', 0, 0)     # texto con la esquina en x = 0, y = 0
oled.text('PyPlaca', 0, 16)
oled.rect(0, 32, 128, 24, 1)      # marco: x, y, ancho, alto, color
oled.hline(8, 44, 112, 1)         # línea horizontal dentro del marco
oled.show()                       # ahora sí: manda todo a la pantalla

while True:                       # mantiene la pantalla encendida en el simulador
    time.sleep(1)
Captura de pantalla de PyPlaca con la lección del OLED: el código que dibuja texto, marco y línea en el editor y, en el circuito, el display OLED mostrando el dibujo.
La lección del OLED en el simulador: el dibujo solo aparece en la pantalla después del oled.show().

El frame buffer

El driver guarda una copia de la pantalla en la memoria de la ESP32: un bytearray de 128 × 64 ÷ 8 = 1024 bytes, un bit por punto. Ese es el frame buffer. Todos los métodos de dibujo (fill, pixel, text, hline, vline, line, rect, fill_rect, ellipse) tocan solo esa copia, sin tocar el I2C. Vienen del módulo framebuf, que forma parte del firmware y que el simulador traduce línea a línea del código C de MicroPython.

Es el show() el que envía la copia entera al display, de una vez. Y eso lleva tiempo: los 1024 bytes, más la dirección y un byte de control, pasan por el I2C en cerca de 23 ms a 400 kHz. De ahí la regla de oro del OLED: dibuja todo primero y llama a show() una vez por cuadro. Llamar a show() después de cada texto deja el programa lento y hace que la pantalla aparezca a medias.

Coordenadas y texto

El punto (0, 0) es la esquina de arriba a la izquierda. La x crece hacia la derecha, hasta 127; la y crece hacia abajo, hasta 63. El text() usa una fuente de 8 × 8 puntos: caben 16 letras por línea y 8 líneas de texto, en y = 0, 8, 16 y así hasta 56.

La pantalla del OLED de 128 por 64 puntos con el origen en la esquina superior izquierda, el eje x creciendo hacia la derecha hasta 127 y el eje y hacia abajo hasta 63, las ocho líneas de texto de 8 puntos de alto y la cuenta de 1024 bytes del frame buffer.
Las coordenadas del OLED: (0, 0) en la esquina de arriba a la izquierda, letras de 8 × 8 puntos (16 por línea, 8 líneas) y 1024 bytes de memoria para la pantalla entera.

Dos cuidados con el texto:

  • la fuente no tiene tildes. Cada carácter por encima del ASCII se convierte en un cuadradito ajedrezado. Escribe Cafe, no Café;
  • el text() no borra el fondo: solo enciende los puntos de la letra. Escribir 2 encima de un 3 deja los dos mezclados. Para cambiar un texto, borra antes: fill(0) limpia toda la pantalla, fill_rect(x, y, ancho, 8, 0) limpia solo un pedazo.

Otros métodos útiles del driver: invert(1) cambia encendido por apagado en toda la pantalla, contrast(x) cambia el brillo y poweroff()/poweron() apagan y encienden el panel.

Un marcador que se actualiza

Juntando la regla del show() con la del fill_rect, un contador que se actualiza queda así:

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('Cuenta', 0, 0)
for n in range(10, -1, -1):
    oled.fill_rect(0, 24, 128, 8, 0)   # borra solo la línea del número
    oled.text('%2d' % n, 56, 24)
    oled.show()                        # un envío por cuadro
    time.sleep_ms(500)

while True:
    time.sleep(1)

El título se dibuja una sola vez; en cada vuelta, solo se borra y se vuelve a dibujar la franja del número.

LCD 16×2: caracteres ya hechos

El LCD 16×2 muestra 2 líneas de 16 caracteres. A diferencia del OLED, no dibuja puntos sueltos: cada posición muestra un carácter ya hecho, sacado de una tabla grabada en el chip del LCD, el HD44780. Cada carácter es una cuadrícula de 5 × 8 puntos, y se pueden crear hasta 8 caracteres propios (una campana, un corazón, una flecha).

Solo, el HD44780 se conecta en paralelo, con tres señales de control (RS, RW y E) y cuatro u ocho cables de datos, además de la alimentación. La plaquita pegada detrás del LCD, con el chip PCF8574, hace de puente: recibe los comandos por el I2C y mueve los pines del LCD. Por eso bastan cuatro cables: GND, VCC, SDA y SCL.

El driver tampoco viene en el firmware y se divide en dos archivos:

  • lcd_api.py: los comandos del HD44780 (limpiar, posicionar, escribir), sin saber cómo está conectado el LCD;
  • i2c_lcd.py: cómo enviar esos comandos por el PCF8574. Importa el lcd_api, así que los dos tienen que estar en el proyecto (y en la 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)    # dirección, líneas, columnas

lcd.putstr('Hola, LCD!')
lcd.move_to(0, 1)                 # columna 0, línea 1 (la de abajo)
lcd.putstr('16x2 en el I2C')

while True:                       # mantiene la pantalla encendida en el simulador
    time.sleep(1)

Los métodos más usados:

  • putstr('texto') escribe a partir del cursor ('\n' salta a la otra línea);
  • move_to(coluna, linha) posiciona el cursor; fíjate: primero la columna, de 0 a 15, después la línea, 0 o 1;
  • clear() borra todo y devuelve el cursor al comienzo;
  • backlight_on()/backlight_off() encienden y apagan la luz de fondo;
  • show_cursor()/hide_cursor() muestran u ocultan el cursor.

Por qué el LCD va en el VIN

El vidrio del LCD necesita 5 V para mostrar los caracteres; su VCC va al VIN de la placa. La parte digital funciona con menos: conectado al 3V3, el LCD responde en el scan(), pero la pantalla queda vacía. El simulador muestra el aviso lcd-sem-5v en ese caso. Es uno de los "el programa está bien y no aparece nada" más comunes del banco de trabajo.

El contraste

Detrás del módulo hay un trimpot de contraste. Fuera de su punto, la pantalla parece vacía aunque todo esté bien. Cuando el scan() encuentra el 0x27 y la pantalla no muestra nada, este es el primer lugar donde mirar. En el simulador, el trimpot está en el panel de la pieza: arrástralo hasta el extremo y verás desaparecer el texto, con el programa siguiendo igual.

Sin tildes aquí también

La tabla de caracteres del HD44780 tiene letras sin tilde, números y signos. Las letras con tilde se convierten en otro símbolo. Escribe Hola, Temp, Hum. Un carácter útil de la tabla es el 223, el símbolo de grado: lcd.putstr(chr(223)).

FastTeacherPara profesores

Del programa al semestre planificado

  • Plan de enseñanza y calendario montados solos
  • Diario de clases listo para pegar en el SIGA
  • Diapositivas y listas de ejercicios con solucionario
Conocer FastTeacher

Versión Community gratis · Windows

Números que sobran en la pantalla

Un LCD no borra solo: cada carácter queda ahí hasta que alguien escribe otro en la misma posición. Ejecuta una cuenta regresiva de 12 a 0 y mira el paso del 10 al 9: aparece 90. El 10 ocupó dos posiciones; el 9 solo escribió encima de la primera, y el 0 anterior siguió ahí. Lo mismo ocurre con el OLED si no borras el fondo.

Tres maneras de resolverlo, de la más bruta a la más elegante:

  1. limpiar todo con clear() y escribir de nuevo. Funciona, pero borra también lo que no cambió y la pantalla puede parpadear;
  2. escribir espacios después del número, para cubrir lo que sobró: putstr(str(n) + ' ');
  3. formatear con ancho fijo, para que el número ocupe siempre el mismo espacio.
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('Cuenta:')

for n in range(12, -1, -1):
    lcd.move_to(0, 1)
    lcd.putstr('%3d' % n)      # siempre 3 posiciones, alineado a la derecha
    time.sleep_ms(400)

while True:
    time.sleep(1)

El formateo con % es la herramienta de los paneles: '%3d' % 7 da ' 7' (tres posiciones, alineado a la derecha), '%-3d' % 7 da '7 ' (alineado a la izquierda) y '%5.1f' % 9.5 da ' 9.5' (cinco posiciones con un decimal). Con él, cada información gana su pedazo de la pantalla, y cada actualización reescribe el pedazo entero.

¿OLED o LCD?

Los dos resuelven el problema de mostrar números, pero tienen vocaciones distintas:

OLED SSD1306LCD 16×2 con PCF8574
Qué muestrapuntos libres: texto, líneas, gráficos32 caracteres ya hechos
Dirección I2C0x3C (o 0x3D)0x27 (o 0x3F)
Alimentación3,3 V5 V (en el VIN)
Driverssd1306.pylcd_api.py + i2c_lcd.py
Actualizardibujar en la memoria y show()escribir en la posición del cursor
Tildesnono

El OLED es la elección para gráficos, íconos y pantallas más ricas; el LCD es imbatible en legibilidad desde lejos y simplicidad de uso. Como los dos hablan I2C en direcciones distintas, nada impide usar los dos en el mismo bus, como en el circuito del scan().

Guía de diagnóstico

Cuando un display I2C no muestra nada, sigue este orden:

  1. ¿el módulo aparece en el scan()? Si no: cables, alimentación, SDA/SCL intercambiados, GND común;
  2. ¿la dirección del código es la que mostró el scan()?
  3. ¿el driver (ssd1306.py o lcd_api.py + i2c_lcd.py) está en el proyecto?
  4. en el OLED: ¿el programa llama a show()?
  5. en el LCD: ¿el VCC está en el VIN (5 V)? ¿El contraste está en su punto?
  6. en el simulador: ¿el programa sigue ejecutándose? Cuando termina, el banco de trabajo se apaga y las pantallas se borran; por eso los ejemplos terminan en un while True con sleep.

El próximo artículo, Programar sin colgarse, muestra cómo actualizar una pantalla, hacer parpadear un LED y atender un botón al mismo tiempo, sin que un sleep lo cuelgue todo. En la ruta de pantallas practicas el scan(), el OLED, el LCD y también el anillo de LEDs NeoPixel, que usa un protocolo de un solo cable.

Para practicar en PyPlaca

Rutas

Lecciones

Fichas de componentes

FastTeacherPara profesores

Del programa al semestre planificado

  • Plan de enseñanza y calendario montados solos
  • Diario de clases listo para pegar en el SIGA
  • Diapositivas y listas de ejercicios con solucionario
Conocer FastTeacher

Versión Community gratis · Windows

FastMindAsistente de IA local

La IA que recuerda tu vida y vive en tu ordenador

  • Memoria a largo plazo con la fuente de cada respuesta
  • Funciona sin conexión: tus datos se quedan en tu PC
  • Lee PDF, diapositivas, fotos y grabaciones de reuniones
Descargar gratis para Windows

Plan Esencial gratis para siempre

FastTeacherPara profesores

Del programa al semestre planificado

  • Plan de enseñanza y calendario montados solos
  • Diario de clases listo para pegar en el SIGA
  • Diapositivas y listas de ejercicios con solucionario
Conocer FastTeacher

Versión Community gratis · Windows