El LCD incluido en el PIC-MT utiliza el conocido controlador Hitachi HD44780. Si miramos el esquemático del PIC-MT, veremos que los bits de control RS, R/W y E se encuentran conectados a los pines 2, 3 y 5 del puerto A, respectivamente (en el esquemático se encuentran en LCD1).
Los datos, están conectados por medio de un bus de 4 bits, en a los pines RC0, RC1, RC2 y RC3. En el esquemático, esto corresponde a LCD2.

El LCD posee dos registros, un reigstro de instrucción, IR, y un registro de datos, DR.
En el registro de instrucciones se escriben los comandos que se quiera ejecutar en el display. Ejemplos de comandos son borrar el display o mover el cursor.
El módulo posee una memoria para datos (DDRAM) y una memoria para el generador de caracteres (CGRAM). El registro DR es usado para escribir en la memoria DDRAM o CGRAM y también para leer la memoria DDRAM y CGRAM.
El LCD posee un flag llamado Busy Flag (BF) que indica si el dispositivo se encuentra en operación interna o no (si es que se encuentra en este estado el dispositivo ignorará los comandos que le enviemos, por lo tanto es siempre importante consultar este flag, o alternativamente, conocer el tiempo que demoran las operaciones y realizar un delay suficiente antes de mandar una nueva instrucción).
Existe un contador de instrucciones encargado de guardar la dirección actual de escritura/lectura en la memoria DDRAM o CGRAM. Cada vez que se lee la memoria DDRAM o CGRAM el contador de direcciones se incrementa o decrementa según la configuración del dispositivo (que se puede cambiar escribiendo la instrucción adecuada en el IR).
Dependiendo de los valores de RS y R/W, se selecciona un registro. A continuación se presenta una tabla de resumen,
RS | R/W | Operación |
0 | 0 | Escribe en IR como una operación interna (borrar el display, mover el cursor, etc). |
0 | 1 | Se lee el Busy Flag (DB7) y el contador de direcciones (DB0-DB6). |
1 | 0 | Se escribe en el registro de datos (DR), y se ejecuta una operación interna (DDRAM o CGRAM) |
1 | 1 | Se lee en el registro de datos (DR), y se ejecuta una operación interna (DDRAM o CGRAM). |
En el caso del LCD del PIC-MT se cuenta con un display de 2 líneas de 16 caracteres visibles cada una. Ambas líneas poseen un total de 40 caracteres, por lo que el display se puede considerar como de 2×40 caracteres. Además, si no se desea utilizar toda la memoria del display para mostrar caracteres, ésta puede ser usada como memoria normal, en la cual el programador puede guardar y leer los datos que estime conveniente.
Las instrucciones de configuración que utilizaremos en este ejemplo son (se recomienda encarecidamente mirar el datasheet y leer el resto de las instrucciones),
RS | R/W | DB7 | DB6 | DB5 | DB4 | DB3 | DB2 | DB1 | DB0 | |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | Borra el display y setea el contador de direcciones en 0. |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | – | Setea el contador de direcciones en 0. Además si el display fue movido (shift), lo retorna a su posición original |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | I/D | S | Indica la dirección de movimiento del cursor y si el display se mueve (shift). |
0 | 0 | 0 | 0 | 0 | 0 | 1 | D | C | B | Prende o apaga el display (D), el cursor (C) y el parpadeo (B). |
0 | 0 | 0 | 0 | 1 | DL | N | F | – | – | Establece el largo de datos (4 u 8 bits), el número de líneas (N) y la fuente de los caracteres. |
El resto de las instrucciones las comentaremos cuando sea necesario usarlas.
Veamos entonces cómo se inicializa el LCD. Primero debemos tener claro que el dispositivo soporta un bus de 8 bits y también un bus de 4 bits. Por lo que vimos en el esquemático del PIC-MT, recordamos que el bus para el cual inicializaremos el LCD es de 4 bits. Según el datasheet, la secuencia de inicialización del display es,
RS | R/W | DB7 | DB6 | DB5 | DB4 | |
(Esperar más de 15ms) | ||||||
0 | 0 | 0 | 0 | 1 | 1 | |
(Esperar más de 4.1ms) | ||||||
0 | 0 | 0 | 0 | 1 | 1 | |
(Esperar más de 100 microsegundos) | ||||||
0 | 0 | 0 | 0 | 1 | 1 | |
0 | 0 | 0 | 0 | 1 | 0 | A partir de esta instrucción es posible consultar el Busy Flag. |
0 | 0 | 0 | 0 | 1 | 0 | |
0 | 0 | N | F | – | – | En nuestro caso N = 1 (2 líneas) F = 0 (5×8 puntos por caracter) |
0 | 0 | 0 | 0 | 0 | 0 | |
0 | 0 | 1 | D | C | B | Queremos el display prendido D = 1, cursor, C = 1 y parpadeo B = 1. |
0 | 0 | 0 | 0 | 0 | 0 | |
0 | 0 | 0 | 0 | 0 | 1 | Limpiar el display. |
0 | 0 | 0 | 0 | 0 | 0 | |
0 | 0 | 0 | 1 | I/D | S | Dejamos que el se incremente el contador en cada escritura lectura I/D = 1, y sin shift, S = 0. |
El compilador no define los bits que usaremos con un nombre simple, por lo que es aconsejable definirlos a mano, pues ahorra tiempo y permite que el código sea más legible.
#defineRSPORTA.2 #defineR_WPORTA.3 #defineEPORTA.5
El delay lo haremos con utilizando la función delay_ms. Ahora veamos cómo mandar todas las instrucciones anteriores al dispositivo. Un bit que no hemos mencionado y que es vital en este tipo de transmisión de datos es E. Cuando queramos transmitir un dato, debemos setear E=1, y luego E=0. El siguiente trozo de código inicializa el LCD y muestra el manejo del bit E. Recordemos que en el puerto C, RC0:RC3 se mapean a los bits DB4:DB7 del LCD.
RS = 0; R_W = 0; //LaprimeravezesperamosbastanteaqueelLCDseaalimentado delay_ms(1100); PORTC = 0b.0000.0011; //AlternamoselpinEnable //(locualgeneraqueeldispositivolealainstrucción). E = 1; E = 0; delay_ms(10); //Esperamosunpoco PORTC = 0b.1000.0011; E = 1; E = 0; delay_ms(10); //Esperamosunpoco PORTC = 0b.0000.0011; E = 1; E = 0; delay_ms(10); //Esperamosunpoco PORTC = 0b.0000.0010; E = 1; E = 0; //DesdeacáenadelantepodemospreguntarporelBusyFlag. //Porlotantoutilizaremosfuncionesparaenviarcomandosocaracteres
La funcion con la cual enviaremos comandos será send_cmd. Antes de revisar esta función, escribiremos otra que se encargue de verificar Busy Flag.
void lcd_busy_flag() { //BitqueindicaelvalordeBusyFlag bit i = 1; //Nuestrobusde4bits //correspondeaRC0:RC3,configuradosahoracomoinput TRISC |= 0b.0000.1111; //CuandoRS=0yR_W=1elLCD //entregaelBusyFlagyelcontadordedirecciones RS = 0; R_W = 1; while(i) { E = 1; //Iniciamoslalectura //RC3correspondeaDB7enlalecturadelosbitsdemayororden i = PORTC.3; E = 0; //Nonosinteresaleerlosbitsdemenororden //AsíquealternamoselbitEnable //sinsiquieraleerelcontenidodelbus E = 1; E = 0; } //Dejamoselbusde4bitsconfiguradocomosalida TRISA &= 0b.1111.0000; }
Ahora que tenemos la función que verifica si el LCD está listo para recibir un nuevo comando, podemos escribir la función que envíe tales comandos. El parámetro de la función será el comando completo (8 bits), y será la función la que se encargue de enviarlo por partes (de 4 bits) al dispositivo.
La función es,
void send_cmd(unsigned char u) { uns8 data_char; //EsperamosqueelLCDestélistopararecibirelcomando lcd_busy_flag(); //Dejamoslos4bitsmássignificativosendata_char data_char = d & 0b11110000; //Losmovemosdemaneraquequedenenlosbits0a3 data_char = data_char >> 4; //EscribimosenelpuertoClos4bitsmássignificativosdeu PORTC = data_char; //Configuramosparaescribirenelregistrodeinstrucciones R_W = 0; RS = 0; //AlternamoselbitEnableparaenviarlos4bits E = 1; E = 0; //Dejamoslos4bitsmenossignificativosendata_char data_char = d & 0b00001111; //EscribimosenelpuertoClos4bitsmenossignificativosdeu PORTC = data_char; //Configuramosparaescribirenelregistrodeinstrucciones R_W = 0; RS = 0; //AlternamoselbitEnableparaenviarlos4bitsrestantes E = 1; E = 0; }
El siguiente paso es finalizar la inicialización que habíamos dejado sin terminar. El código completo de la inicialización del LCD lo dejaremos en la función init_lcd,
void init_lcd() { RS = 0; R_W = 0; //LaprimeravezesperamosbastanteaqueelLCDseaalimentado delay_ms(1100); PORTC = 0b.0000.0011; //AlternamoselpinEnable //(locualgeneraqueeldispositivolealainstrucción). E = 1; E = 0; delay_ms(10); //Esperamosunpoco PORTC = 0b.1000.0011; E = 1; E = 0; delay_ms(10); //Esperamosunpoco PORTC = 0b.0000.0011; E = 1; E = 0; delay_ms(10); //Esperamosunpoco PORTC = 0b.0000.0010; E = 1; E = 0; //DesdeacáenadelantepodemospreguntarporelBusyFlag. //Porlotantoutilizaremosfuncionesparaenviarcomandosocaracteres send_cmd(0b.0010.1000); send_cmd(0b.0000.1100); send_cmd(0b.0000.0001); send_cmd(0b.0000.0110); }
Para que el código sea más legible se recomienda definir una macro para cada uno de los comandos a usar, por ejemplo,
#defineCLR_DISP0b.0000.0001
De esta manera, cada vez que se desee borrar el display, bastará con
send_cmd(CLR_DISP);
Lo cual claramente es más amigable que
send_cmd(0b.0000.0001);
Falta entonces escribir una función que permita enviar caracteres al LCD. Para enviar caracteres, se debe poner RS = 1 y R_W = 0. Dado que el resto de la función es idéntica a send_cmd, podemos modificarla ligeramente para que acepte un nuevo parámetro, el valor de RS.
void _send_cmd(unsigned char u, bit rs) { //... //Igualqueantes //... RS = rs; //... //Igualqueantes //... RS = rs; //... //Igualqueantes //... }
Y podemos escribir dos wrappers que nos hagan la vida algo más simple,
void send_cmd(unsigned char u) { _send_cmd(u, 0); } void send_char(unsigned char u) { _send_cmd(u, 1); }
Con todo lo que hemos construido hasta el momento, podemos crear nuestro primer programa, un “Hola Mundo”. A continuación el trozo de main que hace el “Hola Mundo”,
init_lcd(); send_char('H'); send_char('o'); send_char('l'); send_char('a'); send_char(''); send_char('M'); send_char('u'); send_char('n'); send_char('d'); send_char('o'); send_cmd(DD_RAM_ADDR2); send_char('w'); send_char('w'); send_char('w'); send_char('.'); send_char('o'); send_char('l'); send_char('i'); send_char('m'); send_char('e'); send_char('x'); send_char('.'); send_char('c'); send_char('l'); for(;;) { }
El código completo de este ejemplo se encuentra en el archivo tut_lcd0.c (también necesitará Delay.c)s.