El LCD incluido en el AVR-MT utiliza el conocido controlador Hitachi HD44780. Si miramos el esquemático del AVR-MT, veremos que los bits de control RS y E se encuentran conectados a los pines 4 y 6 del puerto D, respectivamente (en el esquemático se encuentran en LCD1). El bit R/W del LCD se encuentra conectado a tierra.

Los datos, están conectados por medio de un bus de 4 bits, en a los pines PB0, PB1, PB2 y PB3. 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.

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).

Dado que en el AVR-MT, R/W se encuentra conectado a tierra, las opciones disponibles son escribir instrucciones y escribir caracteres en la memoria del LCD.

En el caso del LCD del AVR-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.

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.

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.

Para hacer nuestro código más legible, definimos una macro para poder encender y apagar el bit RS,

#definers_high()PORTD|=1<<PD4
#definers_low()PORTD&=~(1<<PD4)

La forma de transmitir datos al LCD es a través del bus de 4 bits compuesto por los 4 bits menos significativos de PORTB. Dado que las instrucciones y los caracteres a enviar al LCD son de 8 bits, es necesario enviar los datos en dos trozos de 4 bits. Se parte con los 4 bits de mayor orden de lo que se desee enviar. Luego se envían los 4 bits de menor orden. En cada uno de estos envíos el procedimiento consiste en llevar 4 bits al registro PORTB, y a continuación mandar un 1 seguido de un 0 en el bit E. Dado que utilizaremos bastante la secuencia E = 1, delay, E = 0, es conveniente escribir una función,

void toggle_e(void) {
        PORTD |= 1 << PD6;
        _delay_us(10);
        PORTD &= ~(1 << PD6);
}

El delay lo haremos utilizando la función _delay_us, de las librerías incluídas en la distribución del compilador.

Ahora veamos cómo mandar todas las instrucciones anteriores al dispositivo. El siguiente trozo de código inicializa el LCD y muestra el manejo del bit E. Recordemos que en el puerto C, PB0:PB3 se mapean a los bits DB4:DB7 del LCD.

//EsperaraqueelLCDseaalimentado
_delay_ms(20);

//RS=0enlarutinadeinicialización
rs_low();
PORTB = 0b00000011;     
toggle_e();

_delay_ms(10);

PORTB = 0b00000011;
toggle_e();

_delay_ms(10);



PORTB = 0b00000011;
toggle_e();

_delay_ms(10);

//Seestablecequeseusara
//unbusde4bits
PORTB = 0b00000010;
toggle_e();

En este punto el LCD se encuentra inicializado. Se debe configurar su comportamiento enviando comandos, los que se detallan a continuación (ante cualquier duda, revisar el datasheet del LCD).

//LCDdedoslíneasyfuente
//de5x8puntosporcaracter
send_cmd(0b00101000);

//Displayprendido,cursoryparpadeo
send_cmd(0b00001111);

//Limpiareldisplay
send_cmd(0b00000001);

//Elcontadorseincrementaencadaescritura
//Elcursorsemuevehacialaderecha
send_cmd(0b00000110);

La función send_cmd se encarga de escribir un comando en el registro de instrucción del LCD. Si vemos la tabla para RS y R/W al comienzo del tutorial, vemos que la única diferencia entre enviar un comando y enviar un caracter para ser escrito en la memoria del LCD, es que RS = 0 en el primer caso y RS = 1 en el segundo. Por lo tanto, podemos escribir de manera genérica,

void send_cmd(unsigned char u) {
        _send_cmd(u, 0);
}

void send_char(unsigned char u) {
        _send_cmd(u, 1);
}

Donde el segundo parámetro de la función _send_cmd es el valor de RS. Por lo tanto, la función _send_cmd sería,

void _send_cmd(unsigned char u, unsigned char rs) { volatile unsigned char data_char; //EsperamosqueelLCDesté //listopararecibirelcomando _delay_ms(10); //Dejamoslos4bitsmás //significativosendata_char data_char = (unsigned char)u & 0b11110000; //Losmovemosdemanera //quequedenenlosbits0a3 data_char = (unsigned char)data_char >> 4; //Escribimosenelpuerto //Blos4bitsmássignificativosdeu PORTB = data_char; //ConfiguramosRSsegúnelparámetro if(rs) rs_high(); else rs_low(); //Alternamoselvalor //delpinE toggle_e(); //Dejamoslos4bits //menossignificativosendata_char data_char = (unsigned char)u & 0b00001111; //Escribimosenelpuerto //Blos4bitsmenossignificativosdeu PORTB = (unsigned char)data_char; //ConfiguramosRSsegúnelparámetro if(rs) rs_high(); else rs_low(); //Alternamoselvalor //delpinE toggle_e(); } 

Tenemos entonces, rutinas para enviar caracteres y para enviar comandos. Nuestro programa de prueba será el clásico Hola Mundo. El código de main es,

int main(void) { //ConfigurarPORTB //yPORTD //comosalidas DDRB = 255; DDRD = 255; 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 LCD.c.