01 · Punto de partida
De los caracteres a los píxeles
El LCD del tema 37 guardaba una tabla de letras y el programa enviaba códigos ASCII. Una pantalla gráfica no sabe qué es una letra: solo conoce píxeles. La letra «A» es una pequeña imagen que dibuja una biblioteca en el programa, y el controlador de la pantalla apenas recibe una lista de bytes con el color de cada punto. Eso da libertad total, y trae tres consecuencias de ingeniería:
- Memoria: alguien tiene que guardar la imagen, y su tamaño crece con el área y la profundidad de color: 1 KB para un OLED monocromo, 150 KB para un TFT en color.
- Ancho de banda: mandar la imagen completa por un bus lleva tiempo (26 ms por I2C a 400 kHz para el OLED), que se resta al del resto del programa.
- Diseño: con tanta libertad, lo difícil es que lo mostrado se lea bien en una pantalla de 128 × 64 puntos.
Las dos familias del tema se reparten el terreno: el OLED monocromo (SSD1306, 0,96 pulgadas, I2C o SPI) para valores, textos grandes y gráficos simples, y los TFT en color (ST7735, ILI9341; SPI) para interfaces más ricas, con la cuenta de memoria y de tiempo mucho más exigente.
02 · La pantalla
El OLED y el controlador SSD1306
En un OLED (organic light-emitting diode) cada píxel es un diodo orgánico que emite su propia luz: no hay retroiluminación. De ahí sus virtudes: negro verdadero (un píxel apagado no emite), contraste alto, ángulo de visión amplio y respuesta de microsegundos. Y de ahí también sus dos particularidades:
- Consumo proporcional a lo encendido: un módulo de 0,96 pulgadas consume del orden de 4 mA con la pantalla casi negra y puede llegar a unos 20 mA con muchos píxeles blancos y contraste máximo (valores de referencia: depende del módulo). Una pantalla de texto delgado es barata; una imagen blanca, no.
- Envejecimiento desigual: los píxeles que más se usan pierden brillo antes (burn-in): una imagen estática durante meses deja una huella. Se reduce con contraste moderado, apagado de la pantalla tras un tiempo sin actividad y desplazamiento ocasional de los elementos fijos.
El módulo más común es un panel de 128 × 64 (o 128 × 32) píxeles monocromo, blanco, azul o con una franja superior amarilla, gobernado por el SSD1306. Se conecta por I2C (cuatro cables: VCC, GND, SDA y SCL, dirección 0x3C o 0x3D) o por SPI (más rápido, con los pines extra DC y RES). Alimentado con 3,3 V, el módulo trae su propio elevador de tensión interno (charge pump) para el panel, y suele traer resistencias de pull-up en el bus.
El programa le habla con comandos y datos: en I2C, cada transacción empieza con un byte de control (0x00 si lo que sigue son comandos, 0x40 si son datos de imagen). Algunos comandos útiles:
| Comando | Efecto |
|---|---|
0xAE / 0xAF | Pantalla apagada / encendida (la memoria se conserva) |
0x81 + valor | Contraste (0 a 255): controla el brillo y el consumo |
0xA6 / 0xA7 | Imagen normal / invertida |
0x20 + modo | Modo de direccionamiento de la memoria: horizontal, vertical o por página |
0x21, 0x22 + rango | Ventana de columnas y de páginas donde se escribirán los datos |
0x8D + 0x14 | Activa el elevador de tensión interno |
0xA0/0xA1, 0xC0/0xC8 | Espejado horizontal y vertical (pantalla montada al revés) |
Casi nadie los envía a mano: las bibliotecas lo hacen. Pero conviene saber que existen, porque explican fenómenos como «la pantalla está al revés» (se arregla con un comando) y porque el SH1106, un chip parecido con 132 columnas, se parece lo bastante al SSD1306 como para confundirlos: con la biblioteca equivocada la imagen sale corrida 2 píxeles y con ruido en el borde.
03 · Imagen en memoria
La memoria de video
El SSD1306 tiene una memoria de imagen (GDDRAM) de 128 × 64 / 8 = 1024 bytes. Está organizada en 8 páginas de 8 filas de píxeles; dentro de cada página, un byte es una columna de 8 píxeles verticales, con el bit 0 arriba. Para ubicar el píxel (x, y):
Por ejemplo, el píxel (10, 20) está en el byte 10 + 2 × 128 = 266, página 2, bit 4 (0x10); el (127, 63), la última esquina, en el byte 1023, bit 7 (0x80). En código, encender un píxel es una operación OR y apagarlo, una AND con la máscara invertida: buf[x + (y >> 3) * 128] |= 1 << (y & 7).
Hay dos formas de tener esa memoria disponible para el programa:
- Búfer completo en la RAM del microcontrolador (Adafruit_SSD1306): se dibuja todo en un arreglo de 1024 bytes y
display()lo envía de una vez. Es lo más simple, pero consume la mitad de los 2 KB de un Arduino Uno. - Modo de páginas (U8g2 con
firstPage()/nextPage()): se guarda una sola página (128 bytes) y se redibuja todo ocho veces, una por página. Cambia RAM por tiempo de CPU.
Y la cuenta general de memoria de imagen, que es la que dicta qué pantallas caben en qué microcontrolador:
| Pantalla | Píxeles | Bits por píxel | Framebuffer |
|---|---|---|---|
| OLED 128 × 32 monocromo | 4096 | 1 | 512 B |
| OLED 128 × 64 monocromo | 8192 | 1 | 1024 B |
| TFT 128 × 128 (RGB565) | 16 384 | 16 | 32 768 B |
| TFT 128 × 160 (RGB565) | 20 480 | 16 | 40 960 B |
| TFT 240 × 320 (RGB565) | 76 800 | 16 | 153 600 B |
Un TFT de 240 × 320 no cabe en un Arduino Uno (2 KB) y ocupa una parte importante de la memoria de un ESP32. De ahí que los TFT se manejen a menudo sin framebuffer, dibujando directamente sobre el panel por regiones (sección 7).
04 · Transferencia
I2C o SPI: el cuello de botella
Enviar la imagen completa es lo que más tiempo consume. Para el OLED de 1024 bytes:
| Interfaz | Tiempo | Cuadros por segundo máximos |
|---|---|---|
| I2C a 100 kHz (bloques de 16 B) | 105,0 ms | 9,5 |
| I2C a 400 kHz (bloques de 16 B) | 26,2 ms | 38,1 |
| I2C a 400 kHz (bloques de 128 B) | 23,4 ms | 42,7 |
| SPI a 8 MHz | 1,02 ms | 977 |
| SPI a 20 MHz | 0,41 ms | 2441 |
En I2C, cada byte de datos cuesta 9 tiempos de bit (8 más el reconocimiento) y cada transacción agrega el inicio, la dirección, el byte de control y la parada. El búfer de 32 bytes de la biblioteca Wire en un AVR obliga a trocear la imagen en transacciones cortas (de 16 a 31 bytes de datos); en los cálculos usamos 16, el caso desfavorable. Con Wire.setClock(400000) el tiempo baja casi cuatro veces respecto de los 100 kHz de fábrica. En SPI no hay ese gasto y se envía a la velocidad del reloj: 8 bits por cada ciclo.
Dos consecuencias para el programa:
- Con las bibliotecas habituales, el envío bloquea el programa. A 30 cuadros por segundo por I2C a 400 kHz, el microcontrolador pasa
26,2 ms × 30 = 79 %del tiempo mandando píxeles. Sin ser un problema para un termómetro que se refresca una vez por segundo, sí lo es para un lazo de control (tema 43). - Se puede enviar solo lo que cambió. El SSD1306 permite fijar una ventana de columnas y páginas (
0x21y0x22) y mandar solo esa región: dos páginas (256 bytes) cuestan 6,6 ms por I2C a 400 kHz, un cuarto del cuadro. Las bibliotecas más completas ofrecen actualizaciones parciales.
05 · Texto
Fuentes y capacidad de texto
Las letras son mapas de bits guardados en la memoria de programa. La fuente clásica de las bibliotecas Adafruit_GFX es de 5 × 7 píxeles dentro de una celda de 6 × 8 (una columna y una fila de separación): 5 bytes por carácter, 480 bytes para los 96 imprimibles. Una escala entera la agranda: tamaño 2 son celdas de 12 × 16, tamaño 3 de 18 × 24. La capacidad de una pantalla sale de dividir:
| Pantalla | Tamaño 1 | Tamaño 2 | Tamaño 3 |
|---|---|---|---|
| OLED 128 × 64 | 21 × 8 = 168 | 10 × 4 = 40 | 7 × 2 = 14 |
| OLED 128 × 32 | 21 × 4 = 84 | 10 × 2 = 20 | 7 × 1 = 7 |
| TFT 128 × 160 | 21 × 20 = 420 | 10 × 10 = 100 | 7 × 6 = 42 |
| TFT 240 × 320 | 40 × 40 = 1600 | 20 × 20 = 400 | 13 × 13 = 169 |
Con esos números se diseña. Un valor como «23,5 C» (6 caracteres) a tamaño 3 mide 108 píxeles de ancho y cabe en una línea del OLED de 128; con un caracter más ya no. Una pantalla de 128 × 32 solo admite una línea de tamaño 3, o dos de tamaño 2. Las reglas de legibilidad: el dato principal grande, las etiquetas y unidades pequeñas, poca información por pantalla y contraste fuerte.
Las fuentes proporcionales (GFXfont, U8g2) dan letras de ancho variable, más agradables y compactas; ocupan más memoria de programa y hay que calcular el ancho real del texto para centrarlo (getTextBounds). Una limitación: la fuente base solo trae ASCII; los acentos y la ñ requieren una fuente extendida o dibujarlos a mano.
06 · Dibujar
Primitivas y gráficos de sensores
Una biblioteca gráfica (Adafruit_GFX, U8g2, TFT_eSPI) ofrece un conjunto estándar de primitivas: píxel, línea, rectángulo (vacío o lleno), círculo, triángulo, texto y mapa de bits. Las líneas usan el algoritmo de Bresenham que vimos en el tema 34 para los ejes coordinados: enteros, sin divisiones, desvío máximo de medio píxel. Y los círculos, una variante del mismo estilo (punto medio). Un logotipo o un icono es un mapa de bits de 1 bit por píxel guardado en la memoria de programa: 128 × 64 son 1024 bytes de flash.
La utilidad principal con sensores es mostrar la evolución de un valor. El procedimiento es siempre el mismo:
- Guardar la historia en un búfer circular de tantas muestras como columnas (128), actualizado a una frecuencia baja: una muestra por segundo cubre 128 s (2,1 min), y una cada 10 s, unos 21 min.
- Escalar el valor a una fila de la pantalla, con límites que lo mantengan dentro:
y = alto − 1 − round((v − vmín) / (vmáx − vmín) × (alto − 1)). Con 23,5 °C en una escala de 15 a 35 °C sobre 64 filas, la fila es la 36; con una escala de 0 a 100 °C, la 48. - Unir los puntos con líneas entre muestras contiguas.
La resolución vertical la fija la escala: 20 °C sobre 63 filas son 0,32 °C por fila (se ve el cambio de medio grado); 100 °C sobre 63 filas, 1,59 °C por fila (el gráfico es casi una línea plana). Elegir la escala es una decisión de diseño: una escala fija, la más legible y comparable, o autoescala, que ajusta los límites al mínimo y máximo de la historia pero hace que la forma cambie sin que los valores lo hagan. Un buen gráfico lleva, además, el valor actual en número, las unidades y una referencia (límites o líneas de la grilla).
Otras visualizaciones baratas: la barra horizontal (nivel de un tanque, intensidad de una señal), el medidor semicircular, el indicador de tendencia (flecha arriba o abajo) y los iconos de estado (batería, conectividad). Todos se reducen a rectángulos, líneas y texto.
07 · Color
Las pantallas TFT en color
Los módulos TFT baratos (ST7735 de 1,8 pulgadas y 128 × 160, ILI9341 de 2,4 a 2,8 pulgadas y 240 × 320, ST7789 de 240 × 240) usan un panel LCD de transistores en película delgada con retroiluminación y un controlador con memoria propia. Se conectan por SPI con cuatro o cinco señales: SCK, MOSI, CS, DC (indica si lo que sigue es comando o dato), RST y el pin de la retroiluminación (LED), que suele necesitar un transistor y admite PWM para regular el brillo (tema 35). La lógica es de 3,3 V: muchos módulos traen un regulador y adaptadores de nivel para entradas de 5 V, y conviene confirmarlo en el esquema. Un controlador táctil (XPT2046) puede compartir el bus SPI con su propio CS.
Cada píxel se envía en RGB565: 16 bits, con 5 bits de rojo, 6 de verde y 5 de azul. La conversión de un color de 8 bits por canal es:
Rojo puro es 0xF800, verde 0x07E0, azul 0x001F, blanco 0xFFFF y naranja (255, 128, 0) es 0xFC00. El costo de comprimir es la cuantización: el rojo y el azul tienen 32 niveles (saltos de 8), y el verde, 64 (saltos de 4). El color vuelve con un pequeño error (el naranja, a (255, 130, 0)), y los degradados suaves muestran bandas. Un gris de 128 queda como (132, 130, 132): ya no es perfectamente neutro, y los únicos grises exactos son 32.
La transferencia es siempre por ventanas: se envían los comandos que fijan columnas (0x2A) y filas (0x2B), luego 0x2C y se emite un flujo de píxeles que llena la ventana. Rellenar un rectángulo de 100 × 50 son 10 000 bytes, nada más. Eso permite dibujar directamente sobre el panel sin framebuffer, pero el tiempo de una pantalla entera es el de un volcado:
| Pantalla | 8 MHz | 20 MHz | 40 MHz | 80 MHz |
|---|---|---|---|---|
| 128 × 160 | 41,0 ms (24 c/s) | 16,4 ms (61) | 8,2 ms (122) | 4,1 ms (244) |
| 240 × 320 | 153,6 ms (6,5) | 61,4 ms (16) | 30,7 ms (33) | 15,4 ms (65) |
Los 40 MHz son el límite práctico de muchos módulos con cable corto, y alcanzar los 30 cuadros por segundo en la pantalla grande exige un ESP32 con DMA (la transferencia ocurre sin ocupar la CPU) o dibujar solo lo que cambia. Una animación a pantalla completa en un Arduino Uno es, sencillamente, inviable.
08 · Tiempo y apariencia
Refresco, parpadeo y desgarro
Hay que distinguir dos «refrescos». El del panel: la pantalla se redibuja sola desde su propia memoria a una frecuencia fija (del orden de 60 Hz en un TFT), sin intervención del programa. El del programa: cada vez que el microcontrolador envía una imagen nueva. Lo primero no se controla; lo segundo, sí, y solo se hace cuando algo cambió.
- Parpadeo (flicker): aparece cuando se borra y se redibuja directamente sobre el panel. Un
fillScreen(NEGRO)seguido de texto deja ver la pantalla vacía unos milisegundos en cada actualización. En un OLED con búfer, no ocurre: se construye la imagen en RAM y se envía de una vez. - Desgarro (tearing): si se escribe en la memoria mientras el panel la está leyendo, una parte de la pantalla muestra la imagen vieja y otra la nueva. Se nota en animaciones rápidas; algunos controladores dan una señal (TE) para sincronizar.
Las técnicas, de más simple a más compleja:
- Texto con fondo opaco: escribir con color de texto y de fondo (
setTextColor(blanco, negro)) y ancho fijo, de modo que cada carácter nuevo borra al anterior sin un borrado previo. - Borrar solo el rectángulo del campo que cambia, no la pantalla.
- Actualizar solo cuando el valor cambia, o a una frecuencia que una persona lee (2 a 4 Hz).
- Sprites o búferes parciales: dibujar una región en una memoria fuera de pantalla y enviarla de una vez (
pushSpriteen TFT_eSPI). - Doble búfer completo, si la RAM lo permite (ESP32 con una pantalla chica), y DMA para transferir mientras la CPU calcula el siguiente cuadro.
La pregunta práctica, antes de optimizar, es siempre ¿cuántos cuadros por segundo necesito? Un termómetro, uno; un menú, los que sigan la mano; una animación fluida, 20 a 30. Medir el tiempo del envío con micros() y comparar con el presupuesto del lazo evita optimizar lo que no importa.
09 · Programación
Código para manejar pantallas
Cuatro piezas: el monitor de temperatura con gráfico en un OLED, el acceso directo a la memoria de video, la conversión a RGB565 y una actualización sin parpadeo en un TFT.
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
Adafruit_SSD1306 oled(128, 64, &Wire, -1);
const uint8_t N = 128; // una muestra por columna
int16_t historia[N]; // décimas de grado
uint8_t cabeza = 0, cuenta = 0;
void agregarMuestra(int16_t decimas) {
historia[cabeza] = decimas;
cabeza = (cabeza + 1) % N;
if (cuenta < N) cuenta++;
}
int fila(int16_t v, int16_t vmin, int16_t vmax) { // escala al área del gráfico (filas 16 a 63)
v = constrain(v, vmin, vmax);
return 63 - (long)(v - vmin) * 47 / (vmax - vmin);
}
void dibujar(float t) {
oled.clearDisplay(); // solo borra el búfer en RAM: no hay parpadeo
oled.setTextSize(2);
oled.setTextColor(SSD1306_WHITE);
oled.setCursor(0, 0);
oled.print(t, 1); oled.print(" C"); // dato principal, grande
int16_t anterior = 0;
for (uint8_t i = 0; i < cuenta; i++) {
int16_t v = historia[(cabeza + N - cuenta + i) % N];
int y = fila(v, 150, 350); // escala fija: 15,0 a 35,0 °C
if (i > 0) oled.drawLine(i - 1, anterior, i, y, SSD1306_WHITE); // Bresenham
anterior = y;
}
oled.display(); // envía 1024 B: ~26 ms por I2C a 400 kHz
}
void setup() {
Wire.begin();
Wire.setClock(400000); // 4 veces más rápido que los 100 kHz de fábrica
oled.begin(SSD1306_SWITCHCAPVCC, 0x3C);
}
// --- Acceso directo a la memoria de video (páginas de 8 filas) ---
uint8_t buf[1024];
void poner(uint8_t x, uint8_t y, bool encendido) {
uint16_t i = x + (y >> 3) * 128;
if (encendido) buf[i] |= (1 << (y & 7)); else buf[i] &= ~(1 << (y & 7));
}
// --- Color de 16 bits para un TFT ---
uint16_t color565(uint8_t r, uint8_t g, uint8_t b) {
return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);
}
// --- TFT sin parpadeo: texto con fondo opaco y ancho fijo (TFT_eSPI) ---
void mostrarTemperaturaTft(float t) {
char buf[8];
dtostrf(t, 5, 1, buf); // " 9.9", " 23.5"
tft.setTextColor(TFT_WHITE, TFT_BLACK); // el fondo borra lo anterior, sin fillScreen
tft.setTextSize(3);
tft.setCursor(10, 20);
tft.print(buf); tft.print(" C");
}
La separación es la de los demás temas: la aplicación llama a mostrarTemperatura(t) y solo el módulo conoce la pantalla, su tamaño, su bus y su estrategia de refresco. Pasar de un OLED a un TFT cambia el módulo y nada más.
10 · Verificación
Fallas y verificación
| Síntoma | Causa | Defensa |
|---|---|---|
| Pantalla en negro | Dirección I2C equivocada (0x3C o 0x3D), cableado, falta de display() | Escáner I2C; revisar SDA y SCL; llamar a display() |
| Imagen corrida 2 píxeles y ruido en el borde | Módulo SH1106 con biblioteca de SSD1306 | Usar la biblioteca del chip real |
| Imagen al revés o en espejo | Pantalla montada al revés | Comandos de espejado (0xA0/0xC0) o rotación de la biblioteca |
| El programa no arranca o se reinicia | El búfer de 1 KB agota la RAM de un Uno | Menos memoria usada, modo de páginas o una placa con más RAM |
| Animación entrecortada | Envío bloqueante lento (I2C a 100 kHz) | 400 kHz, SPI, actualizaciones parciales |
| Parpadeo en el TFT | Borrado y redibujado directo sobre el panel | Texto con fondo opaco, borrar solo el campo, sprites |
| TFT con colores invertidos o cambiados | Orden RGB/BGR o inversión del panel | Parámetro del controlador en la biblioteca |
| Bandas en degradados | RGB565 (32 y 64 niveles) | Menos degradados o tramado (dithering) |
| Pantalla blanca o sin imagen en un TFT | Reset sin pulso, pines de CS, DC o RST mal puestos | Revisar los cuatro pines y la tensión de 3,3 V |
| Huellas de imagen en el OLED | Elementos estáticos durante meses con contraste alto | Contraste moderado, apagado automático, desplazamiento |
- Probá con una imagen mínima: un rectángulo en el borde (0,0 a 127,63) revela si las esquinas y el tamaño son los esperados.
- Mostrá un texto fijo antes de datos reales; si sale correcto, la comunicación funciona.
- Mediá el tiempo del envío con
micros()y compará con el cálculo de bits por velocidad del bus. - Revisá la memoria libre antes y después de crear el búfer.
- Probá el peor caso de texto: el valor más largo, el negativo y el mensaje de error.
- Mirá la pantalla de lado y a oscuras: en un OLED, el contraste y los píxeles quemados se ven mejor.
11 · Analogía con programación
Memoria de video, tiempos de bus, texto, color y gráficos
Calculemos los cinco elementos del tema: la memoria de imagen de cada pantalla y la posición de un píxel, el tiempo de refresco por I2C y SPI, la capacidad de texto, la conversión a RGB565 con su error y la escala de un sensor a filas de pantalla.
Python en tu navegador. Los bloques son editables: cambiá los valores y presioná «Ejecutar Python». La primera vez tarda unos segundos en cargar.
import math
# ---- Memoria de video ----
print("pantalla píxeles bits/píxel framebuffer")
for nombre, w, h, bpp in (("OLED 128x32 mono", 128, 32, 1), ("OLED 128x64 mono", 128, 64, 1),
("TFT 128x128 RGB565", 128, 128, 16), ("TFT 128x160 RGB565", 128, 160, 16),
("TFT 240x320 RGB565", 240, 320, 16)):
print(f"{nombre:20s} {w * h:8d} {bpp:8d} {w * h * bpp // 8:8d} B")
def posicion(x, y, ancho=128):
return x + (y // 8) * ancho, y % 8
for x, y in ((0, 0), (10, 20), (127, 63)):
i, bit = posicion(x, y)
print(f"píxel ({x:3d},{y:2d}): byte {i:4d} (página {y // 8}, columna {x}), bit {bit}, valor 0x{1 << bit:02X}")
# ---- Tiempo de refresco de la pantalla completa ----
def t_i2c(f, n_bytes, bloque=16):
transacciones = math.ceil(n_bytes / bloque)
bits = transacciones * (20 + 9 * bloque) # inicio, dirección, control, datos y parada
return bits / f
def t_spi(f, n_bytes):
return n_bytes * 8 / f
print()
print("OLED 128x64 (1024 B):")
for f in (100e3, 400e3):
for bloque in (16, 128):
t = t_i2c(f, 1024, bloque)
print(f" I2C {f / 1000:3.0f} kHz, bloques de {bloque:3d} B: {t * 1000:6.1f} ms -> {1 / t:5.1f} cuadros/s")
for f in (8e6, 20e6):
t = t_spi(f, 1024)
print(f" SPI {f / 1e6:2.0f} MHz: {t * 1000:5.2f} ms -> {1 / t:6.0f} cuadros/s")
t = t_i2c(400e3, 256)
print(f" actualizar solo 2 páginas (256 B) por I2C a 400 kHz: {t * 1000:.1f} ms")
print()
print("TFT en pantalla completa (16 bits por píxel):")
for nombre, w, h in (("128x160", 128, 160), ("240x320", 240, 320)):
for f in (8e6, 20e6, 40e6, 80e6):
t = t_spi(f, w * h * 2)
print(f" {nombre} a {f / 1e6:2.0f} MHz: {t * 1000:6.1f} ms -> {1 / t:5.1f} cuadros/s")
# ---- Texto: fuente clásica de 5x7 en celda de 6x8 ----
print()
for nombre, w, h in (("OLED 128x64", 128, 64), ("OLED 128x32", 128, 32), ("TFT 128x160", 128, 160), ("TFT 240x320", 240, 320)):
for tam in (1, 2, 3):
cols, filas = w // (6 * tam), h // (8 * tam)
print(f"{nombre} tamaño {tam}: {cols:2d} columnas x {filas:2d} filas = {cols * filas:4d} caracteres")
print(f"'23,5 C' (6 caracteres) a tamaño 3: {6 * 6 * 3} píxeles de ancho (OLED 128: {'cabe' if 6 * 6 * 3 <= 128 else 'no cabe'})")
print(f"Fuente clásica: {96 * 5} B para los 96 caracteres imprimibles (5 columnas de un byte cada uno)")
# ---- RGB565 ----
def rgb565(r, g, b):
return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)
def expandir(c):
r5, g6, b5 = (c >> 11) & 31, (c >> 5) & 63, c & 31
return (r5 << 3) | (r5 >> 2), (g6 << 2) | (g6 >> 4), (b5 << 3) | (b5 >> 2)
print()
for nombre, rgb in (("rojo", (255, 0, 0)), ("verde", (0, 255, 0)), ("azul", (0, 0, 255)), ("blanco", (255, 255, 255)),
("naranja", (255, 128, 0)), ("gris 50 %", (128, 128, 128)), ("marrón", (150, 75, 0))):
c = rgb565(*rgb)
rec = expandir(c)
err = max(abs(a - b) for a, b in zip(rgb, rec))
print(f"{nombre:10s} {rgb} -> 0x{c:04X} vuelve a {rec}, error máx. {err}")
print("niveles por canal: rojo y azul 32, verde 64; grises exactos:", 32)
# ---- Gráfico de un sensor en una pantalla de 64 filas ----
def fila(v, vmin, vmax, alto=64):
v = min(max(v, vmin), vmax)
return alto - 1 - round((v - vmin) / (vmax - vmin) * (alto - 1))
print()
print("23,5 °C con escala 15..35 °C en 64 filas: fila", fila(23.5, 15, 35), "; con escala 0..100 °C: fila", fila(23.5, 0, 100))
print("resolución vertical: 20 °C / 63 = %.3f °C por fila; 100 °C / 63 = %.3f °C por fila" % (20 / 63, 100 / 63))
print("historia visible a 1 muestra por segundo con 128 columnas: %d s (%.1f min)" % (128, 128 / 60))
Resultado: el OLED de 128 × 64 necesita 1024 bytes de imagen y un TFT de 240 × 320 en RGB565, 153 600 bytes; el píxel (10, 20) está en el byte 266, página 2, bit 4 (0x10). Por I2C, el cuadro completo tarda 105,0 ms a 100 kHz (9,5 cuadros por segundo) y 26,2 ms a 400 kHz (38,1); por SPI a 8 MHz, 1,02 ms (977), y dos páginas por I2C a 400 kHz, 6,6 ms. Un TFT de 240 × 320 por SPI tarda 153,6 ms a 8 MHz (6,5 cuadros por segundo), 61,4 ms a 20 MHz y 30,7 ms a 40 MHz (32,6). Con la fuente clásica, el OLED de 128 × 64 admite 21 × 8 = 168 caracteres a tamaño 1 y 10 × 4 = 40 a tamaño 2; «23,5 C» a tamaño 3 ocupa 108 píxeles. En RGB565, el naranja (255, 128, 0) es 0xFC00 y vuelve a (255, 130, 0); el gris de 128 es 0x8410 y vuelve a (132, 130, 132), con un error máximo de 4. 23,5 °C cae en la fila 36 con una escala de 15 a 35 °C (0,32 °C por fila) y en la fila 48 con una de 0 a 100 °C (1,59 °C por fila); 128 columnas a una muestra por segundo guardan 128 s.
12 · Exploración interactiva
Memoria, refresco, texto y color
El primer instrumento localiza un píxel en la memoria del SSD1306. El segundo calcula el tiempo de refresco y la ocupación del bus. El tercero dimensiona el texto de una pantalla y el cuarto convierte un color a RGB565. Los cálculos de I2C usan bloques de 16 bytes (caso desfavorable de un AVR) y el límite de RAM de un Arduino Uno (2 KB).
Instrumento 1: un píxel en la memoria del SSD1306
Instrumento 2: refresco de la pantalla
Instrumento 3: capacidad de texto
Instrumento 4: color RGB565
Recorrido guiado
- Con el píxel (10, 20): byte 266, página 2, bit 4 y valor 0x10; el búfer es de 1024 bytes. Movelo a (127, 63): byte 1023, página 7, bit 7 (0x80).
- Cambiá a 128 × 32: la fila máxima pasa a 31, el búfer a 512 bytes y el último byte es el 511.
- Poné el chip real en SH1106 con la biblioteca de SSD1306: el estado avisa que la imagen sale corrida 2 píxeles; el dibujo muestra dónde cae el píxel.
- En el refresco, con el OLED por I2C a 400 kHz, 30 cuadros por segundo y pantalla completa: 26,2 ms por cuadro, 38,1 cuadros máximos y 79 % del tiempo bloqueado. Pasá a 100 kHz: 105 ms y 9,5 cuadros, que no alcanzan.
- Volvé a 400 kHz y enviá solo el 25 %: 256 bytes en 6,6 ms y el 20 % del tiempo. Pasá a SPI de 8 MHz con pantalla completa: 1,02 ms.
- Elegí el TFT de 240 × 320 con SPI de 40 MHz: 30,7 ms y 32,6 cuadros máximos, justo para 30. El framebuffer de 153 600 bytes no cabe en un Uno, y el estado lo avisa. Probá con 8 MHz: 153,6 ms. Cambiá el microcontrolador a un ESP32: el framebuffer ya cabe, aunque ocupa casi la mitad de su RAM.
- En el texto, con el OLED de 128 × 64 a tamaño 1: 21 × 8 = 168 caracteres; a tamaño 2: 10 × 4 = 40. Con 8 caracteres a tamaño 3 (144 px) el texto no entra en una línea y ocupa dos.
- En el color, el naranja (255, 128, 0) da 0xFC00 y vuelve a (255, 130, 0). Poné un gris de 128 en los tres canales: 0x8410 y (132, 130, 132), un gris con tinte.
Desafío: un monitor de temperatura con historia
Un OLED de 128 × 64 muestra arriba la temperatura en tamaño 2 (16 filas) y abajo un gráfico de 48 filas por 128 columnas, con una escala fija de 15 a 35 °C. ¿Qué resolución vertical tiene y cuánto cuesta actualizar el gráfico por I2C a 400 kHz? Resolución: 20 °C sobre 47 filas ≈ 0,43 °C por fila. El gráfico ocupa 6 páginas, 768 bytes: 48 transacciones de 164 tiempos de bit a 400 kHz son unos 19,7 ms; el texto superior, 2 páginas, 256 bytes, 6,6 ms; el cuadro entero, 26,2 ms. Si se toma una muestra cada 10 segundos, se redibuja solo al llegar una nueva: 26 ms cada 10 s, un 0,26 % del tiempo, y la historia cubre unos 21 minutos. Mejora: actualizar el texto cada segundo (6,6 ms) y el gráfico solo con muestra nueva. Y para evitar huellas, apagar la pantalla tras unos minutos sin actividad.
13 · Método de trabajo
Cómo integrar una pantalla, para cualquier proyecto
- Elegí por contenido y por memoria: OLED para números y gráficos simples, TFT para color, y verificá que el framebuffer cabe (o dibujá sin él).
- Calculá el tiempo de refresco (bytes × 8 / velocidad del bus) y comparalo con los cuadros por segundo que necesitás.
- Subí la velocidad del bus (400 kHz en I2C, SPI de varios MHz) antes de optimizar otra cosa.
- Actualizá solo cuando algo cambia, y solo la región afectada.
- Evitá el parpadeo: búfer en memoria (OLED), texto con fondo opaco o sprites (TFT).
- Diseñá con las cuentas de texto: dato principal grande, ancho fijo, unidades pequeñas; verificá el peor caso.
- Escalá los gráficos con una escala fija elegida, con límites (
constrain), y valor numérico al lado. - Cuidá el OLED: contraste moderado y apagado por inactividad; encapsulá la pantalla en un módulo con una función de «mostrar».
Si la pantalla no muestra nada, revisá dirección, cableado y display(); si la imagen está corrida, el chip; si el programa va lento, medí el envío; si parpadea, no borres directamente sobre el panel.
14 · Comprensión
Confusiones frecuentes
«Un OLED y un LCD son lo mismo con otra tecnología»
El OLED emite su propia luz (consumo según los píxeles encendidos, contraste alto, desgaste desigual), y el LCD usa retroiluminación constante. Además, las pantallas gráficas no tienen tabla de letras: el texto lo dibuja el programa.
«Basta con llamar a print() y la pantalla se actualiza»
Con búfer en RAM, print() solo modifica el arreglo; hasta display() la pantalla no cambia, y esa llamada es la que cuesta decenas de milisegundos.
«El refresco de la pantalla lo fija mi programa»
El panel se redibuja solo desde su memoria; el programa solo decide cuándo manda una imagen nueva y cuánto tarda en hacerlo.
«Subir los cuadros por segundo es siempre posible»
El techo lo pone el bus: 38 cuadros por segundo por I2C a 400 kHz para un OLED y menos de 7 para un TFT de 240 × 320 a 8 MHz. Y el envío bloquea la CPU.
«RGB565 es lo mismo que RGB888 pero más chico»
Pierde información: 32 niveles de rojo y azul y 64 de verde. Los colores se corren unas unidades y los degradados suaves muestran bandas.
«Para cambiar un número, hay que borrar la pantalla y redibujar todo»
Borrar directamente sobre un TFT produce parpadeo. Basta escribir el campo con fondo opaco y ancho fijo, o borrar solo su rectángulo.
15 · Práctica guiada
Predecí antes de comprobar
1. Posición de un píxel
¿En qué byte y bit de la memoria de un SSD1306 de 128 columnas está el píxel (100, 45)? ¿Qué valor tiene el byte si es el único encendido?
Ver solución
Página = 45 ÷ 8 = 5; byte = 100 + 5 × 128 = 740; bit = 45 mod 8 = 5, valor 0x20.
2. Memoria
¿Cuántos bytes ocupa el framebuffer de un TFT de 160 × 128 en RGB565? ¿Cabe en un Arduino Uno?
Ver solución
160 × 128 × 2 = 40 960 bytes: no cabe (2048 bytes); hay que dibujar sin framebuffer o usar un microcontrolador con más RAM.
3. Tiempo por SPI
¿Cuánto tarda en enviarse una pantalla completa de 128 × 160 en RGB565 por SPI a 20 MHz y cuántos cuadros por segundo da?
Ver solución
40 960 bytes × 8 / 20 000 000 = 16,4 ms → unos 61 cuadros por segundo.
4. I2C
Con I2C a 100 kHz y bloques de 16 bytes, ¿llega un OLED de 128 × 64 a 10 cuadros por segundo?
Ver solución
Tarda 105 ms por cuadro: 9,5 cuadros por segundo, no llega. Con 400 kHz son 38.
5. Color
¿Qué valor RGB565 tiene el color (0, 255, 128) y a qué color vuelve?
Ver solución
R = 0; G: (255 & 0xFC) << 3 = 0x07E0; B: 128 >> 3 = 0x10; total 0x07F0. Al expandir: (0, 255, 132): un error de 4 en el azul.
6. Diagnóstico
En un TFT, la temperatura parpadea cada vez que se actualiza. El código hace fillScreen(NEGRO) y luego imprime el texto. ¿Por qué y qué cambiás?
Ver solución
El borrado y el dibujo se hacen directamente sobre el panel: durante los milisegundos entre ambos se ve la pantalla vacía. Se escribe el texto con color de fondo opaco y ancho fijo (setTextColor(blanco, negro)), o se borra solo el rectángulo del campo, o se arma en un sprite y se envía de una vez.
16 · Síntesis
Ideas para recordar
- Una pantalla gráfica solo conoce píxeles; las letras y los gráficos los dibuja la biblioteca en una memoria de video.
- El OLED emite luz propia: negro verdadero y alto contraste, consumo proporcional a los píxeles encendidos y desgaste desigual (apagado e inactividad).
- El SSD1306 guarda 1024 bytes en 8 páginas; el píxel (x, y) está en el byte x + (y ÷ 8) × 128, bit y mod 8 (el 0 arriba).
- Enviar un cuadro cuesta bytes × 8 ÷ velocidad: 26 ms por I2C a 400 kHz para el OLED, 31 ms para un TFT de 240 × 320 por SPI a 40 MHz; el envío bloquea el programa.
- Se envía solo lo que cambió, a la frecuencia que una persona lee, y con el bus lo más rápido que permita el módulo.
- La fuente clásica es de 5 × 7 en una celda de 6 × 8: 21 × 8 caracteres en el OLED de 128 × 64; se dimensiona el texto con columnas = ancho ÷ (6 × tamaño).
- Un TFT usa RGB565 (5-6-5 bits): 2 bytes por píxel, 153 600 bytes de framebuffer en 240 × 320, y colores cuantizados con bandas en los degradados.
- El parpadeo viene de borrar directamente sobre el panel: se evita con búfer, texto de fondo opaco o sprites.
¿Listo para el tema 39?
Antes de avanzar, ubicá el píxel (64, 32) en la memoria de un SSD1306, calculá el tiempo de enviar un cuadro de 1024 bytes por I2C a 400 kHz con bloques de 16 bytes y explicá cómo evitar el parpadeo al actualizar un número en un TFT. Si podés justificar las tres respuestas, ya sabés manejar pantallas gráficas.
En el próximo tema pasamos del ojo al oído: los zumbadores y el sonido, con el buzzer activo y el pasivo, la generación de tonos y las melodías.