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.

tut_lcd1

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,

RSR/WOperación
00Escribe en IR como una operación interna (borrar el display, mover el cursor, etc).
01Se lee el Busy Flag (DB7) y el contador de direcciones (DB0-DB6).
10Se escribe en el registro de datos (DR), y se ejecuta una operación interna (DDRAM o CGRAM)
11Se 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),

RSR/WDB7DB6DB5DB4DB3DB2DB1DB0
0000000001Borra el display y setea el contador de direcciones en 0.
000000001Setea el contador de direcciones en 0. Además si el display fue movido (shift), lo retorna a su posición original
00000001I/DSIndica la dirección de movimiento del cursor y si el display se mueve (shift).
0000001DCBPrende o apaga el display (D), el cursor (C) y el parpadeo (B).
00001DLNFEstablece 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,

RSR/WDB7DB6DB5DB4
(Esperar más de 15ms)
000011
(Esperar más de 4.1ms)
000011
(Esperar más de 100 microsegundos)
000011
000010A partir de esta instrucción es posible consultar el Busy Flag.
000010
00NFEn nuestro caso N = 1 (2 líneas) F = 0 (5×8 puntos por caracter)
000000
001DCBQueremos el display prendido D = 1, cursor, C = 1 y parpadeo B = 1.
000000
000001Limpiar el display.
000000
0001I/DSDejamos 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.