Projetos
Um dos jeitos mais interessantes de se interagir com equipamentos eletrônicos é por meio de um display LCD. Eles deixam a interface entre homem e máquina simples e intuitiva, afinal, é bem bacana poder visualizar as informações do seu display. Porém controla-los com um Arduino pode parecer desafiador, já que isso exige interfacear o microcontrolador e o display. Mas essa tarefa não é tão difícil assim, e é isso que mostraremos neste post.
Conhecendo os displays
Antes de começarmos a programar, vamos ver um pouco como os displays funcionam. O display que vamos usar para esse artigo é um 16×2, que são bastante comuns em equipamentos eletrônicos. Como o nome diz, eles possuem 16 colunas e 2 linhas, ou seja, conseguimos exibir neles até 32 caracteres. Cada caracter é formado por um conjunto de pequenos quadradinhos que podem ter sua cor alterada. Esses quadradinhos estão dispostos em 8 linhas com 5 quadradinhos cada, como na imagem abaixo.
Então, quando queremos escrever algo no display, ligamos ou desligamos os quadrados, fazendo um “pixel art” de ligado e desligado. Assim, se quisermos fazer um “R” por exemplo, precisamos ligar os quadradinhos adequados, como abaixo.
Como controlar 40 pontos (8×5) por espaço, diretamente com um controlador é algo praticamente impraticável, os displays contam com um driver. O driver para LCD mais comum é o HD44780, estando presente na grande maioria dos displays 16×2 disponíveis no mercado.
HD44780
O driver HD44780 possui os caracteres mais utilizados salvos em sua memória, em endereços que seguem a tabela abaixo.
fonte:https://www.sparkfun.com/datasheets/LCD/HD44780.pdf
Quando queremos escrever algum caracter, precisamos enviar o seu endereço para este driver. Além disso, ele pode ser operado em duas formas diferentes: no modo de 4 e no modo 8 bits.
O modo mais simples é o de 8 bits, que conecta oito pinos do seu microcontrolador com o HD44780, assim, os dados são enviados em paralelo, um bite por vez.O problema desse modo é o uso 8 pinos para enviar dados, o que limita a quantidade de pinos que podem ser usados para outras tarefas, como leitura de sensores e acionamento de atuadores.
No modo de quatro bits, enviando os dados em 2 nibbles (conjuntos de 4 bits) diferentes, primeiro o mais significativo seguido do menos significativo. Por exemplo, se queremos imprimir um “A” no display, precisamos fazer como abaixo.
Agora que já entendemos um pouco do funcionamento de nosso display, vamos aprender a controla-lo.
Montando o hardware
Começando pela montagem, usamos o modo de 4 bits, ligando os pinos de D4 ao D7 do display nos pinos de mesma numeração do Arduino. enquanto isso os pinos E, RS e RW estão ligados nos pinos A0, A1 e A2 do arduino respectivamente. Já os pinos BLA e BLK vão ligados em 5V e GND respectivamente (esses pinos são usados para ligar o back light). O pino VO é ligado no GND por meio de um potenciômetro de 10k, para o ajuste de contraste. Finalmente temos os pinos de alimentação VDD e GND são ligados em 5V e no GND do Arduino como mostrado abaixo.
Com essa ligação feita, temos os pinos D4 a D7 ligados no PORTD do Atmega, e os pinos RS, RW e E ligados no PORTC, assim, temos o PORTD como o port usado para envio de dados, que no código chamaremos de PORT_LCD. Por outro lado, o PORTC, usado para configurações será chamado de PORT_config.
No PORT_LCD, temos conectados os pinos nos quais enviaremos os endereços dos caracteres e as instruções para o LCD. Já no PORT_config temos “E”, que é o Enable do display, que bloqueia o envio de qualquer dado quando este esta em nivel baixo (0V). Seguido desse temos o pino R/W, que seleciona se os dados serão lidos (R/W=1) ou enviados (R/W=0) para o display. Finalmente temos o pino RS, que seleciona o registrador que sera usado para a transmissão de dados, sendo 0 para o registrador de instruções e 1 para o registrador de dados.
LCD_instruct
Com tudo já montado, podemos começar a falar do nosso código. Montamos uma biblioteca bem simples, contendo apenas 8 funções, mas que já permite um controle bem completo do display.
A primeira função da nossa biblioteca se chama “LCD_instruct()”, e o objetivo dela é mandar instruções para o display. O código dela ficou como abaixo.
// Implementa a rotina para envio de dados para o display, esses dados são enviados em duas etapas,
//Primeiro - Os bits mais significativos são enviados, em seguida os bits menos significativos.
void LCD_instruct(uint8_t dados){
PORT_config &= ~(1<<RS);
PORT_LCD = ((dados & 0xF0)|(PORT_LCD & 0x0F));
PORT_config |= (1<<Enable);
_delay_ms(1);
PORT_config &= ~(1<<Enable);
_delay_ms(1);
PORT_LCD = (((dados & 0x0F)<<4)|(PORT_LCD & 0x0F));
PORT_config |= (1<<Enable);
_delay_ms(1);
PORT_config &=~(1<<Enable);
_delay_ms(2);
}
Aqui começamos escrevendo “0” no pino RS, selecionando o registrador de instruções. Selecionamos este registrador para enviar instruções do tipo “mover cursor para posição x,y”, ou “Limpar todo o display”, entre outras.
A próxima etapa é enviar o nibble mais significativo do nosso comando, mantendo os bits menos significativos do PORT_LCD inalterados. Fazemos isso com um pouco de álgebra booleana. Primeiro aplicamos uma mascara no bite com os dados(usando &0xF0), zerando os menos significativos, e usamos um OR (com o “|”) para manter os bits menos significativos do por em seu estado atual. Fazemos isso para continuar usando o PORTD para outras funções, por exemplo para ler sensores, se comunicar com periféricos, etc.
Após isso. atualizamos o LCD com um pulso no pino “E” (Enable), enviando assim, os dados para o LCD. Em seguida repetimos esse processo para o nibble menos significativos, mas desta vez, deslocando-os em 4 casas para a esquerda, já que apenas os bits mais significativos do port estão ligados ao LCD.
LCD_init
Agora que já temos como enviar dados para o display, esta na hora de configura-lo para uso. Fazemos isso por meio da função LCD_init(), descrita abaixo.
// Função para a inicialização do display LCD, nela define-se os bits mais significativos do PORT_LCD
//e do PORT_config como saida, em seguida envia-se os comandos para configurar o display na operação
//no modo de 4 bit.
void LCD_init(){
DDR_LCD = ((0xF0)|(DDR_LCD));
DDR_config = ((0x07)|DDR_config);
PORT_config &= ~(1<<RW);
PORT_config &= ~(1<Enable);
PORT_config &= ~(1<<RS);
PORT_LCD = ((0x20)|(PORT_LCD & 0x0F));
PORT_config |= (1<<Enable);
_delay_ms(1);
PORT_config &=~(1<<Enable);
_delay_ms(2);
LCD_instruct(0x28);
LCD_instruct(0x0C);
LCD_clear();
_delay_ms(2);
}
Inicialmente definimos os pinos nos quais o LCD esta ligado como saídas. Logo após isso escolhemos o registrador de instruções no modo escrita (enviando 0 para “RS” e “R/W”) não habilitando o enable. Então configuramos o LCD para interface com 4 bits, enviando 0x20 para o registrador de instruções dando um pulso no Enable. Note que não usamos a função “envia_dados()”, pois o modo de operação é definido nesse passo, assim, essa função não funcionaria.
Agora que o modo de 4 bits foi definido, enviamos o comando 0x28, que comunica que o display que estamos usando possui 2 linhas, e 5×8 pontos por caracter.
Seguindo esse passo, deixamos o cursor desligado, enviando 0x0C, para o mesmo registrador, sendo que para liga-lo basta enviar 0x0E, ou caso queira ligar com blink (efeito de piscar o carácter da posição no qual ele se encontra) basta enviar 0x0F. Todas essas instruções estão presentes no datasheet do HD44780.
LCD_clear
A função LCD_clear é a mais simples que implementamos, tanto na parte de programação, quanto ma aplicabilidade dela. O que ela faz é limpar o display, ou seja, apagar todos os caracteres que estejam sendo exibidos na tela. O seu código ficou como abaixo.
// Envia os comandos para limpar o display LCD.
void LCD_clear(){
LCD_instruct(0x01);
_delay_ms(1);
}
Aqui, simplesmente enviamos 1 para o bit menos significativo do registrador de instruções. Essa função pode ser aplicada por exemplo na criação de menus, limpando as informações do menu anterior para exibir as informações do próximo.
LCD_shift_display
O HD44780 tem capacidade de armazenar até 40 caracteres por linha, e como o display LCD que estamos usando só tem a capacidade de exibir 16 caracteres por vez, então é possível escrever no display sem exibir o conteúdo, ou escolher a janela de conteúdo que se deseja exibir. Esse é o objetivo da função LCD_shift_display, que foi implementada como abaixo.
// Desloca o display para direita (dir=0) ou para esquerda (dir=1).
void LCD_shift_display(uint8_t dir){
if(dir<1){
LCD_instruct(0x18);
}
else{
LCD_instruct(0x1C);
}
}
Aqui, temos como entrada da função a direção, que pode ser 1 para a direita e 0 para a esquerda. O que fazemos então é enviar a instrução 0x18 para deslocamento o display a esquerda ou 0x1C para deslocar para a direita, alterando apenas o terceiro bit enviado (já que 8 em hexadecimal equivale a 1000 binário e C equivale a 1100). O interessante dessa função é que o conteúdo da memória de dados do display não é alterado, apenas o que está sendo exibido.
LCD_write_char
Essa função é bastante similar a função “LCD_instruct”, entretanto, desta vez selecionamos o registrador de dados (fazendo com que RS = 1), como abaixo.
// Escreve o caracter recebidor na posição atual do cursor
void LCD_write_char(char character){
PORT_config |= (1<<RS);
PORT_LCD = ((character) & 0xF0)|(PORT_LCD & 0x0F);
PORT_config |= (1<<Enable);
PORT_config &= ~(1<<Enable);
PORT_LCD = (((character & 0x0F)<<4)|(PORT_LCD & 0x0F));
PORT_config |= (1<<Enable);
PORT_config &=~(1<<Enable);
_delay_ms(2);
}
Aqui, começamos escrevendo 1 em RS, em seguida, enviamos o nibble mais significativo do carácter, mas mantendo os valores do nibble inferior do PORT_LCD, como já foi explicado na função “LCD_instruct”. Em seguida, após um pulso no Enable do LCD, envia-se a parte inferior do byte do caracter, deslocando-o em quatro bits para a esquerda, finalizando com um novo pulso no enable.
Os endereços dos caracteres seguem a tabela já apresenta anteriormente, mas os caracteres mais utilizados por esse display seguem a tabela ASCII, podendo ser apenas digitado normalmente entre “” na chamada dessa função. Entretanto alguns caracteres presentes não possuem correspondência com a tabela ASCII, e devem, portanto identificadas pelo código.
Assim, se quisermos escrever um “ö” (apesar de não ser algo comum), no display, deveremos enviar o código 0b11101111, em binário, ou 0xBF em hexadecimal ou 239 em decimal.
f – LCD_write
Esta função escreve palavras ou frases inteiras no display LCD, para isso, ela recebe uma string com texto que será escrito, em seguida ela envia cada caracter da string para a função “LCD_write_char”. Esta função ficou como abaixo.
// Envia todos os caracteres de uma string por meio de chamadas sucessivas da função LCD_write_char.
void LCD_write(char palavra[40]){
int i=0;
while (palavra[i]!='\0'){
LCD_write_char(palavra[i]);
i++;
}
}
Aqui, realizamos uma varredura por cada caracter, procurando encontrar o “\0”, que é uma marcação do C que indica o final de uma string, assim, cada valor diferente desse é um caracter que deve ser impresso.
g – LCD_move_cursor
A próxima função criada é usada para selecionar a posição no display na qual o caracter será escrito, ou seja, ela moverá o cursor até a posição desejada, ela ficou como abaixo.
// Movimenta o cursor para a posição (x, y) desejada.
void LCD_move_cursor (uint8_t x, uint8_t y){
LCD_instruct ((0x80|(y<<6))+x);
}
Nessa função, apenas selecionamos o registrador de instruções e enviamos um byte. Esse byte é da forma 1yxxxxxx, sendo que o bit mais significativo indica que se esta movendo o cursor. O imediatamente a sua direita seleciona a linha, começando a contagem em 0 na linha de cima, ou seja, 0 seleciona a primeira linha enquanto 1 seleciona a segunda. Os demais bits são para selecionar a posição do cursor na linha indo de 0 (000000) até 39 (100111), totalizando assim as 40 casas.
h – LCD_create_custom_char
Finalmente chegamos na última e mais divertida função criada para essa biblioteca, que é uma função para se criar um caracter customizado. Na tabela com os caracteres, temos que os códigos de 0b00000000 a 000000111, ou seja, entre 0 e 7 são destinados ao CGRAM, que é a memória RAM do gerador de caracteres, assim, usamos esses endereços para armazenar os caracteres que criaremos. O que resta fazer é enviar a “arte” dos caracteres, para isso, enviaremos linha a linha quais são os pontos que devem ser ligados, e quais devem ser desligados (como explicamos no começo deste artigo). O resultado está abaixo.
// Recebe um endereço de 0 a 7 e um ponteiro do endereço contendo um vetor com os carcteres mapeados, em seguida
// Salva o vetor no endereço recebido do CGRAM.
void LCD_create_custom_char(uint8_t endereco, unsigned char *charMap) {
if (endereco < 8) {
LCD_instruct(0x40 + endereco*8);
for (int i = 0; i < 8; i++) {
LCD_write_char(charMap[i]);
}
}
}
Aqui começamos verificando se o endereço está no intervalo permitido, em seguida acionamos esse endereço no CGRAM usando a função “LCD_instruct”. O próximo passo é enviar cada uma das linhas do nosso caracter, e fazemos isso usando a função “LCD_write_char”, passando por todas as posições do vetor que contém o mapeamento dos pontos que devem ser acesos ou apagados.
Com essas funções apresentadas conseguimos controlar o LCD com bastante flexibilidade, então se você tem interesse em usar essa biblioteca no seu projeto, você pode baixa-la do nosso github. Lá você encontra os arquivos .h e .c necessários para faze-la funcionar, além disso, la também um código exemplo mostrando como usar a nossa biblioteca.
O que você achou dessa biblioteca? Conte pra gente nos comentários.