SolidCrystalI2C: un driver con buffer para pantallas HD44780 por I2C
Mastedore, septiembre de 2026. Corresponde a la versión 2.0.0 de la biblioteca.
Resumen
SolidCrystalI2C controla pantallas LCD de caracteres HD44780 conectadas a través de un expansor I2C PCF8574, el módulo barato que traen soldado atrás casi todas las pantallas de 16x2 y 20x4. Está escrita a partir de las hojas de datos y no adaptada del código habitual de LiquidCrystal_I2C, y llegar a su forma actual tomó unos dos meses de iteraciones. Su idea central es que, en este hardware, el cuello de botella es el bus y no la CPU. Cada byte que recibe el LCD cuesta cuatro transacciones I2C, así que la biblioteca guarda una copia de la pantalla en RAM, marca las celdas que cambian y solo manda esas. Sobre ese modelo agrega una segunda capa de dibujo para lo que debe flotar encima del texto normal, un caché para los ocho caracteres personalizados, un cursor que se queda donde el programa lo puso y una interfaz de flujo pequeña que no depende de String ni de Print de Arduino. Este documento explica cómo funciona cada una de esas partes y por qué se construyó así.
1. El hardware
1.1 El HD44780
El HD44780 es el controlador que hay detrás de casi cualquier LCD de caracteres. Tiene 80 bytes de RAM de pantalla (DDRAM) para los caracteres visibles, 64 bytes de RAM del generador de caracteres (CGRAM) para ocho caracteres de 5x8 definidos por el usuario, y un juego de instrucciones pequeño para borrar la pantalla, mover el contador de direcciones, prender o apagar la pantalla, el cursor y el parpadeo, entre otras cosas. La mayoría de las instrucciones tardan unos 37 microsegundos en ejecutarse. Borrar y regresar al inicio son las lentas, con alrededor de 1.52 ms.
El controlador tiene un bus de datos de 8 bits que también puede usarse de 4 en 4, mandando cada byte en dos mitades (nibbles). Además de las líneas de datos hay tres de control: RS elige entre instrucciones y datos, RW entre escribir y leer, y un flanco de bajada en E captura lo que haya en el bus.
1.2 El módulo PCF8574
El PCF8574 es un dispositivo I2C con ocho pines de salida. Cada byte que se le escribe aparece en esos pines hasta que llega el siguiente. El módulo típico para LCD los conecta así:
| Pin del expansor | Señal del LCD |
|---|---|
| P0 | RS |
| P1 | RW |
| P2 | E |
| P3 | transistor de la retroiluminación |
| P4 - P7 | D4 - D7 |
Solo cuatro pines llegan al bus de datos, así que el LCD tiene que trabajar en modo de 4 bits, y el expansor no sabe generar un pulso. Para capturar un nibble, el driver lo escribe una vez con E en alto y otra con E en bajo. Por lo tanto, mandar un byte al LCD cuesta cuatro escrituras I2C.
1.3 Lo que cuesta
A la velocidad estándar de I2C, 100 kHz, una escritura al expansor es una condición de inicio, un byte de dirección, un byte de dato y una condición de paro, unos 20 periodos de reloj:
Un nibble son dos escrituras más los 51 microsegundos de espera de sendNibble(), y un byte del LCD son dos nibbles:
donde y son los renglones y las columnas, y cada celda necesita un byte de posición y uno de carácter. De ahí salen estas cifras. Son estimaciones a partir de los tiempos del bus, sin contar la sobrecarga de la propia biblioteca Wire, no mediciones:
| Operación | Escrituras I2C | Tiempo aproximado |
|---|---|---|
| Un nibble | 2 | 0.45 ms |
| Un byte al LCD (instrucción o carácter) | 4 | 0.9 ms |
| Un carácter en una posición dada (dirección + dato) | 8 | 1.8 ms |
| Subir un carácter personalizado (dirección + 8 filas) | 36 | 8 ms |
| Redibujar completa una pantalla de 16x2 | 256 | 58 ms |
| Redibujar completa una pantalla de 20x4 | 640 | 145 ms |
En 58 ms, un AVR a 16 MHz pasa por ciclos de reloj. En este hardware la pregunta nunca es qué tan rápido corre el código, sino cuántos bytes cruzan el bus, y casi todas las decisiones de diseño que siguen salen de ahí.
2. Objetivos
La biblioteca se construyó alrededor de unas cuantas restricciones:
- Funcionar en un AVR de 8 bits con 2 KB de RAM, así que el uso de memoria tiene que ser pequeño, fijo y predecible.
- Seguir las hojas de datos al pie de la letra, de modo que cada byte que pasa por el bus se pueda rastrear hasta una línea de código, sin trabajo oculto en segundo plano.
- No usar
Stringde Arduino (reserva memoria en el heap mientras corre el programa) niPrint(una tabla virtual y una API que la biblioteca no necesita). - Recordar qué hay en la pantalla, para que la aplicación arme un cuadro en memoria y solo mande la diferencia.
- Tratar la pantalla como de solo escritura. La línea RW se queda en bajo todo el tiempo y la biblioteca nunca lee la bandera de ocupado ni la DDRAM. Leer a través del expansor pediría tráfico extra en el bus y cambios de dirección en los pines, y la copia en RAM ya sabe lo que muestra la pantalla.
3. Arquitectura
El código está organizado en tres capas, y cada una se apoya solo en la de abajo.
- Transporte (
sendi2c,sendNibble,sendByte) convierte un byte del LCD en escrituras al expansor. - Instrucciones (
clearAll,display,cursor,blink,backlight,textDirection,writePosition,writeChar,defineGlyph) es el juego de instrucciones del HD44780, una función por operación. - La API con buffer (
row,column,seti,home, los operadores<<yflush) escribe en RAM y decide qué tiene que llegar a la pantalla.
Una aplicación puede usar la capa 2 directamente cuando lo necesita (la animación de créditos de Owake escribe directo en la pantalla con writePosition y writeChar), pero el dibujo normal pasa por la capa 3.
4. Transporte
sendi2c() escribe un byte al expansor y le agrega el bit de la retroiluminación en cada llamada, porque el PCF8574 no tiene registros separados por pin. Cada escritura fija las ocho salidas a la vez, así que una escritura que olvidara ese bit apagaría la luz.
sendNibble() coloca cuatro bits de datos en P4-P7, pone RS en P0 y deja RW (P1) en 0. Escribe el byte una vez con E (P2) en alto y otra con E en bajo, y después espera 50 microsegundos, que cubren los 37 que necesitan la mayoría de las instrucciones. La hoja de datos pide un pulso de habilitación de unos cuantos cientos de nanosegundos, y una sola escritura I2C ya tarda mucho más que eso, así que el microsegundo de espera entre las dos escrituras es solo un margen.
sendByte() manda primero el nibble alto y luego el bajo. Todo lo que está por encima de esta capa trabaja con bytes completos.
5. Instrucciones e inicialización
5.1 Despertar al controlador
El HD44780 necesita tiempo después de encenderse antes de aceptar comandos: más de 40 ms desde que la alimentación llega a 2.7 V, según la hoja de datos. start() no usa una espera fija para eso. Espera a que micros() pase de 55 ms desde que arrancó el microcontrolador, así que si la aplicación ya gastó ese tiempo en otra cosa, la espera no cuesta nada.
El controlador puede despertar en modo de 8 bits, o a mitad de un byte de 4 bits si solo se reinició el microcontrolador. La secuencia de “inicialización por instrucción” de la hoja de datos resuelve ambos casos: mandar el nibble 0x3 tres veces (esperando después del primero y después del segundo) y luego 0x2, que cambia al modo de 4 bits. Solo después de eso es seguro mandar bytes completos.
Después viene la instrucción de configuración de función, 0 0 1 DL N F x x. DL se queda en 0 para el bus de 4 bits. N se enciende en pantallas de más de un renglón, y F (la fuente de 5x10) solo en pantallas de un renglón, porque el controlador no admite caracteres de 5x10 en modo de dos líneas. Luego start() enciende la pantalla con el cursor oculto, la borra, fija la escritura de izquierda a derecha y marca como vacías todas las ranuras de la CGRAM en el caché de glifos, ya que la CGRAM tiene datos al azar al encender.
5.2 Ajustes que comparten instrucción
Encender o apagar la pantalla, el cursor y el parpadeo son tres bits de una misma instrucción. La biblioteca guarda los tres en su campo de bits de configuración, y cada setter actualiza su bit y reenvía la instrucción completa, así que cambiar uno nunca altera los otros. La instrucción de modo de entrada (dirección del texto y desplazamiento de la pantalla) funciona igual.
5.3 Direccionamiento
La DDRAM no está ordenada renglón tras renglón. El renglón 0 empieza en la dirección 0x00 y el 1 en 0x40, y en pantallas de cuatro renglones el renglón 2 es la continuación del 0 (0x14) y el 3 la continuación del 1 (0x54). Cada línea tiene 40 bytes, de los que una pantalla de 16 o 20 columnas solo muestra la primera parte. writePosition(column, row) convierte una posición en dirección con una tabla de cuatro desplazamientos,
y manda la instrucción para fijar la dirección de la DDRAM.
5.4 Desplazamiento
Los cuatro operadores de incremento y decremento corresponden a la instrucción de “desplazar cursor o pantalla”. Los prefijos ++lcd y --lcd desplazan toda la pantalla a la derecha o a la izquierda, y los postfijos lcd++ y lcd-- solo mueven el cursor.
6. El modelo con buffer
6.1 Los buffers
La biblioteca guarda, por cada pantalla:
- Un buffer de texto (la capa del usuario), un byte por celda, inicializado con espacios.
- Un buffer de overlay opcional, un byte por celda, inicializado con un marcador transparente.
- Un mapa de bits de celdas modificadas, un bit por celda.
Escribir texto nunca toca el bus. Los operadores << escriben en la capa activa en la posición de escritura actual, avanzan esa posición y encienden el bit de la celda en el mapa de celdas modificadas. La única función que escribe en los buffers (bufwrt) es también la que marca la celda, así que ningún camino puede cambiar una celda y olvidar marcarla.
La posición de escritura es un renglón y una columna guardados en el campo de bits de configuración. seti() y geti() la convierten desde y hacia un índice plano , útil para diseños que se calculan como desplazamientos. La celda se registra en el bit del byte del mapa de celdas modificadas. Cuando una escritura se pasa de la última columna, la posición salta al inicio del siguiente renglón, y después del último renglón, al primero. En modo de derecha a izquierda salta en sentido contrario.
6.2 Dos caracteres de control
La biblioteca reserva dos códigos de carácter como marcadores dentro de los buffers: 22 (skip) y 23 (end). Los dos caen en el rango 0x10-0x1F, que la ROM de caracteres A00, la más común, deja en blanco, así que no se pierde ningún carácter imprimible. Nunca llegan a la pantalla.
- Skip significa “aquí no hay nada”. En el overlay vuelve transparente la celda y deja ver la capa del usuario. En la capa del usuario significa “deja lo que ya tenga la pantalla”.
- End significa “en blanco desde aquí hasta el final del renglón”. La aplicación lo escribe con
cchar::endl.
End existe por la forma en que se reconstruyen las pantallas. Cuando el texto de un renglón se acorta, por ejemplo al cambiar “Chronometer” por “Clock”, hay que borrar la cola vieja. Sin end, la aplicación tendría que rellenar con espacios hasta el ancho del renglón. Con él, lcd << "Clock" << cchar::endl borra exactamente lo que sobra.
6.3 El overlay
El overlay es una segunda capa que se dibuja encima del texto. Lo que se escribe en ella tapa la capa del usuario en esa celda, y escribir skip devuelve la celda. Está pensada para cosas que aparecen y desaparecen sobre contenido que no se debe redibujar: flechas de navegación en los bordes de un menú, un aviso de confirmación encima de una cuenta regresiva, una marca de estado. La aplicación elige la capa con lcd << writeLayer::overlay y regresa con lcd << writeLayer::user. El overlay es opcional: una pantalla creada sin él no reserva ese buffer, y las solicitudes de cambiar a esa capa se ignoran.
6.4 flush()
flush() recorre la pantalla renglón por renglón y decide, para cada celda, si la manda y qué manda:
para cada renglón:
reiniciar las banderas de fin del renglón
para cada celda del renglón:
si antes en este renglón apareció un END del overlay:
mandar un espacio y seguir
si la celda no cambió y no ha aparecido un END del usuario:
saltarla
revisar el overlay:
END -> mandar un espacio, recordar "END del overlay"
SKIP -> revisar la capa del usuario:
si antes apareció un END del usuario: mandar un espacio
END -> mandar un espacio, recordar "END del usuario"
SKIP -> no mandar nada
otro -> mandar el carácter
otro -> mandar el carácter del overlay
restaurar el cursor si está fijado (sección 8)
limpiar el mapa de celdas modificadas
De ese orden salen algunas propiedades. El overlay le gana al texto salvo que sea transparente. Un marcador de fin sigue actuando después de su propia celda: todo lo que viene después en el renglón se pone en blanco aunque esas celdas no estuvieran marcadas, así que un texto que antes era más largo no deja rastro. Después de un fin en la capa del usuario, los caracteres del overlay que siguen en el renglón se siguen viendo, de modo que una flecha en el borde derecho sobrevive a un texto corto. Las banderas de fin se reinician al empezar cada renglón, así que un marcador de fin nunca se pasa al siguiente.
6.5 clear() y clearAll()
clearAll() manda la instrucción de borrado del controlador. La pantalla queda en blanco de inmediato, y eso cuesta una transferencia por el bus y 1.52 ms. clear() solo reinicia los buffers: el texto con espacios y el overlay transparente. No marca ninguna celda como modificada, así que la pantalla conserva lo que tenía hasta que esas celdas se vuelvan a escribir.
Ese comportamiento es a propósito, aunque suele sorprender. clear() sirve para empezar un cuadro nuevo desde un estado conocido cuando la aplicación de todos modos va a escribir cada celda visible (normalmente con marcadores de fin haciendo el relleno), y un borrado físico solo agregaría un parpadeo visible entre cuadros. Cuando la pantalla sí tiene que quedar vacía en ese momento, se usan los dos juntos: clearAll() para la pantalla y clear() para los buffers.
7. Memoria
7.1 La palabra de configuración
Toda la configuración de la pantalla y la posición de escritura comparten un campo de bits de 32 bits:
| Campo | Bits | Significado |
|---|---|---|
| DisplayShift | 1 | modo de entrada: desplazar la pantalla con cada escritura |
| CursorDirection | 1 | modo de entrada: de izquierda a derecha o al revés |
| BlinkState | 1 | cursor de bloque parpadeante |
| CursorState | 1 | cursor de subrayado |
| DisplayState | 1 | pantalla encendida o apagada |
| BacklightState | 1 | bit de retroiluminación que va en cada escritura al expansor |
| FontSize | 1 | caracteres de 5x10 (solo pantallas de un renglón) |
| Ready | 1 | start() ya terminó |
| WriteRow | 2 | posición de escritura en el buffer, renglón |
| WriteColumn | 6 | posición de escritura en el buffer, columna |
| ConstantCursorRow | 2 | cursor fijado, renglón |
| ConstantCursorColumn | 6 | cursor fijado, columna |
| ConstantCursor | 1 | cursor fijado activo |
| WriteLayer | 1 | capa del usuario u overlay |
| WriteMode | 2 | raw, texto, hexadecimal o binario |
| Unused | 4 |
El estado por renglón de flush() (overlay activo, fin del usuario visto, fin del overlay visto) vive en un segundo campo de bits de un byte.
7.2 Totales
El objeto en sí ocupa 30 bytes en AVR: la dirección I2C y las dimensiones (3), los dos campos de bits (5), tres punteros a buffer (6) y el caché de glifos (16). Los buffers se reservan en el heap una sola vez, en el constructor, y nunca se liberan ni cambian de tamaño. Su tamaño depende de la geometría y de si el overlay está activo ( con overlay, sin él), más el encabezado de 2 bytes del asignador por bloque:
| Pantalla | Sin overlay | Con overlay |
|---|---|---|
| 16x2 | 36 bytes | 68 bytes |
| 20x4 | 90 bytes | 170 bytes |
Reservar al construir permite que una sola compilación de la biblioteca sirva para cualquier geometría sin plantillas, y como nunca se libera nada, el heap no se puede fragmentar.
8. El cursor
El cursor visible del HD44780 está donde apunte el contador de direcciones, y ese contador se mueve con cada escritura. Una biblioteca con buffer escribe muchas celdas por cada flush, así que al terminar el cursor quedaría detrás de la última celda enviada, que nunca es donde la aplicación lo quería.
La biblioteca lo resuelve con un cursor fijado. cursorPosition(column, row) enciende el cursor del hardware y guarda su posición en la palabra de configuración, y al final de cada flush() la biblioteca regresa la dirección a esa posición. constantCursor(false) apaga la función. El editor de hora de Owake lo usa para dejar el cursor sobre el campo que se está editando mientras se redibujan los dígitos de alrededor.
9. Caracteres personalizados
9.1 Subirlos
defineGlyph(bitmap, slot) escribe un mapa de bits de 5x8 en una de las ocho ranuras de la CGRAM: una instrucción para fijar la dirección de la CGRAM seguida de ocho bytes de datos. Se espera que los mapas de bits estén en la flash (PROGMEM), y en AVR se leen con pgm_read_byte. En otras arquitecturas la macro se vuelve una desreferencia normal. Imprimir la ranura es simplemente escribir el código de carácter .
Después de subir un glifo, el contador de direcciones del controlador sigue apuntando dentro de la CGRAM, y escribir un carácter suelto en ese momento sobrescribiría parte de un glifo. El camino con buffer está a salvo de esto porque flush() siempre fija una dirección de la DDRAM antes de escribir. El código que usa la capa 2 directamente tiene que llamar antes a writePosition().
9.2 El caché de glifos
Las aplicaciones suelen necesitar más de ocho caracteres personalizados a lo largo de su vida, y van cambiando de conjunto según la pantalla. Subir un conjunto de ocho cuesta unos 65 ms de bus, así que volver a subir glifos que ya están cargados es un desperdicio real. La biblioteca guarda una tabla de ocho entradas con la dirección del mapa de bits cargado en cada ranura.
Los glifos se identifican por la dirección de su mapa de bits y no por sus píxeles. Eso reduce la revisión a ocho comparaciones de punteros (16 bytes de RAM para la tabla), cuando comparar el contenido pediría hasta 64 lecturas de la flash. La consecuencia es que dos arreglos con los mismos píxeles cuentan como glifos distintos, cosa que en la práctica no importa porque cada glifo se define una sola vez.
defineGlyph() devuelve la ranura donde ya está el mapa de bits, sin mandar nada, o -1 después de subirlo. Una aplicación puede aprovechar eso para revisar un conjunto completo a bajo costo. Owake solo revisa si el primer glifo de su conjunto principal sigue en la ranura 0: si sigue ahí, el resto del conjunto también, y se ahorra otras siete subidas. lcd << bitmap busca el mapa de bits en el caché e imprime su ranura, o un * si no está cargado, que se nota más que un glifo equivocado.
9.3 Caracteres de 5x10
En modo de 5x10 (solo pantallas de un renglón), el controlador tiene cuatro ranuras en lugar de ocho, separadas por 16 bytes en la CGRAM y con 11 filas cada una (la undécima es la línea del cursor). La ranura empieza en la dirección de la CGRAM en lugar de , y su código de carácter es en lugar de , porque en este modo el controlador ignora el bit más bajo del código. La biblioteca se encarga del direccionamiento y de la traducción del código. El tipo del mapa de bits sigue teniendo 8 filas, eso sí, así que las tres filas inferiores de un glifo de 5x10 se mandan en blanco.
10. La interfaz de flujo
La salida usa operator<<, al estilo de los flujos de C++:
lcd << "Temp: " << temperature << "'C" << cchar::endl;
Hay sobrecargas para caracteres, cadenas, uint8_t y los enteros de 16, 32 y 64 bits, los caracteres de control, los mapas de bits de glifos y dos tipos de configuración: writeMode y writeLayer. Todos los enteros terminan en una sola función de 32 bits (los valores de 64 bits se truncan), que da formato según el modo de escritura:
rawmanda el valor mismo como código de carácter. Es el modo por defecto, así quelcd << uint8_t(3)imprime el glifo que esté en la ranura 3. Las aplicaciones que imprimen números cambian awriteMode::textuna vez, como hace Owake al arrancar.text,hexybinimprimen el valor en base 10, 16 o 2 usandoltoa. El buffer de conversión tiene el tamaño del peor caso, bytes, para 32 dígitos binarios, el terminador y un signo.
La misma comodidad se podría haber obtenido heredando de la clase Print de Arduino, y la biblioteca eligió no hacerlo. Print funciona con funciones virtuales, lo que en AVR implica una tabla virtual en la SRAM, y trae consigo una API (formato de punto flotante, sobrecargas para String) de la que esta biblioteca se mantiene lejos a propósito. Los operadores son funciones miembro comunes que el compilador puede integrar o eliminar.
11. Ciclo de vida y seguridad
Un LCD es dueño de sus buffers y corresponde a una pantalla física, así que copiarlo no tiene sentido. El constructor de copia y la asignación por copia están eliminados, y una copia accidental falla al compilar.
El destructor llama a FAULT(ILLEGAL_OPERATION), de la biblioteca de fallas del autor. Un objeto de pantalla está pensado como una variable global que vive lo mismo que el programa, y en AVR una global nunca se destruye. Si un destructor llega a correr, algo creó un LCD temporal o local, que además dejaría sus buffers sin liberar, y la pantalla de fallas lo avisa.
La biblioteca también ofrece isThereAnyLCD(), que devuelve verdadero cuando alguna pantalla terminó start(). La biblioteca de fallas la usa para decidir si puede mostrar un error en la pantalla. Esa impresión pasa por un objeto llamado lcd, que el header declara como extern, así que una aplicación que use las dos bibliotecas debe llamar lcd a su pantalla.
12. Limitaciones
- La pantalla es de solo escritura. Nunca se lee la bandera de ocupado, así que cada instrucción espera un tiempo fijo calculado para el caso más lento, y el contenido de la pantalla no se puede leer de vuelta. La copia en RAM es la fuente de verdad.
- La geometría está limitada por los campos de configuración y por el índice plano de 8 bits: hasta 4 renglones, y . Las pantallas de 40x4, que usan dos controladores, no están soportadas.
- Los glifos de 5x10 solo tienen 8 filas de datos.
- Los enteros de 64 bits se imprimen como de 32.
clear()no marca las celdas como modificadas (sección 6.5). Es intencional, pero hay que tenerlo presente.- La biblioteca depende de Facebits (por sus utilidades de máscaras de bits) y de la biblioteca de fallas (por el destructor).
13. Trabajo futuro
El costo que queda está en flush(), que manda una posición y un carácter por cada celda modificada, cada uno como dos bytes de dos nibbles de dos escrituras al expansor. Hay dos cambios que lo reducirían sin alterar lo que muestra la pantalla.
- Omitir las direcciones redundantes. Después de escribir un carácter, el contador de direcciones del controlador ya apunta a la celda siguiente. Cuando la siguiente celda modificada es la de al lado, la instrucción de posición sobra, y una racha de caracteres contiguos cuesta la mitad.
- Agrupar las escrituras al expansor. El PCF8574 actualiza sus salidas después de cada byte de datos, incluso cuando llegan varios en una sola transmisión I2C. Las escrituras con E en alto y en bajo de un nibble, o las cuatro escrituras de un byte del LCD, podrían ir en una sola transmisión y pagar la condición de inicio y el byte de dirección una vez en lugar de cuatro.
Ninguno de los dos está implementado todavía. El autor está evaluando cuál conviene, o si combinarlos.
14. Ejemplo
#include <SolidCrystalI2C.h>
const uint8_t arrow[8] PROGMEM = {0x10, 0x18, 0x1c, 0x1e, 0x1e, 0x1c, 0x18, 0x10};
LCD lcd(0x27, 16, 2, true); // direccion I2C, columnas, renglones, overlay activo
void setup()
{
lcd.start();
lcd.defineGlyph(arrow, 0);
lcd << writeMode::text;
lcd << "Temp: " << int16_t(23) << "'C" << cchar::endl;
lcd.home(1);
lcd << arrow << " Ajustes" << cchar::endl;
// Una marca en la esquina que el texto del renglon 0 no va a borrar
lcd << writeLayer::overlay;
lcd.row(0);
lcd.column(15);
lcd << '!';
lcd << writeLayer::user;
lcd.flush();
}
void loop()
{
}
Referencias
- Hitachi, HD44780U (LCD-II) Dot Matrix Liquid Crystal Display Controller/Driver, hoja de datos, 1998.
- NXP Semiconductors, PCF8574 Remote 8-bit I/O expander for I2C-bus, hoja de datos.
- Huang, H. W. (2009). The HCS12/9S12: An Introduction to Software and Hardware Interfacing (2.a ed.), pp. 323-338. Delmar Learning.
- Controlador de LCD Hitachi HD44780, resumen del juego de instrucciones, Wikipedia.
Este texto de Mastedore. Todos los derechos reservados.