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