Arquivos Projetos - Blog Ryndack Componentes
Rua Jovelina Claudino Buhrer, 440 - São José dos Pinhais - PR (41) 3383-3034
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. 

0

Projetos

O LED é um componente que traz muitas possibilidades de uso, pois ele serve tanto pra dar um toque para algum projeto, como também podemos criar coisas utilizando apenas eles e aproveitando para soltar a imaginação brincando com suas cores, variando formatos e caprichando na programação com os estilos de piscar. Por isso hoje trouxemos pra vocês um cubo de LEDs! Vamos começar então pela parte do funcionamento.

Funcionamento do cubo de leds

Note que os LEDs dessa montagem estão dispostos como uma matriz 3×9. Além disso, todos os LEDs de uma linha estão conectados pelos seus catodos, enquanto os LEDs de uma coluna estão ligados pelos seus anodos. Dessa forma, se queremos selecionar um LED específico, basta selecionar uma linha e uma coluna.


Analogamente para se selecionar os LEDs do cubo basta selecionar seu nível e sua coluna. Dessa forma podemos usar o arduino para selecionar posições no cubo de led. Assim, quando queremos acender determinado LED, devemos acionar a saída correspondente a linha, ou ao “andar” no caso do cubo, e a coluna específica, enquanto garantimos que as demais linhas se mantenham em zero. O código abaixo mostra como fazemos para acender o LED do centro do cubo.


void setup(){
   pinMode(A1, OUTPUT);
   pinMode(6 , OUTPUT);
}
void loop(){
   digitalWrite(A1, HIGH);

   digitalWrite(6 , HIGH);
}


Nesse trecho de código, chamamos a atenção para o fato que as linhas e as colunas estão enumeradas de acordo com a ordem em que elas estão conectadas no arduino. Além disso, destacamos que ao agrupar os leds em níveis, conseguimos reduzir a quantidade de portas usadas pelo arduino de 27 para 12. O nome desta técnica é multiplexação e ela é muito usada em eletrônica para reduzir o número de barramentos usados na transmissão de dados. Agora que já entendemos o funcionamento do cubo de LEDs, vamos para uma das partes mais legais: a montagem!

Montagem


Antes de tudo, precisamos ter em mãos os seguintes materiais:

  1. LEDs (27 unidades);

  2. Protoboard;

  3. Resistores de 330R (9 unidades);

  4. Resistores de 10k (3 unidades;

  5. Transistores 2N2222 (3 unidades);

  6. Fios para as ligações;

Com esses materiais em mãos, a primeira parte desse projeto é montar o cubo, para isso vamos construir uma guia, nela os leds serão posicionados lado a lado, com espaços constantes entre eles, assim conseguiremos montar o cubo de forma rápida, garantindo que seus lados fiquem retos. Essa guia consiste em um quadrado de 50mm de lado com nove furos com espaços de 20 mm entre eles. Cada furo deverá ter o diâmetro do led, que para o nosso caso é de 5 mm.

Para construir essa guia você pode fazer do modo que desejar, mas nós usamos uma impressora 3D. Assim, primeiro desenhamos ela com o tinkercad e geramos o desenho no formato STL deste link, em seguida usamos o Cura para criar o arquivo .gcode. Por fim imprimimos esse arquivo usando a nossa impressora 3D. A peça ficou como mostrado abaixo.



Agora que temos essa peça em mãos, posicionamos um led em cada furo, e soldamos os seus catodos com o do led ao lado, formando uma pequena grade, como mostra a figura abaixo, essas grades serão usadas para confeccionar os andares do nosso cubo, portanto precisamos de três peças deste tipo.



Finalmente usando algum tipo de apoio (no nosso caso usamos um pedaço de papelão), soldamos as três grades uma sobre a outra, unindo apenas os anodos dos leds de cima aos de baixo, o que nos deixa com um cubo de 3 níveis e 9 colunas, como as imagens abaixo.



Circuito de controle


Para realizar as conexões do circuito de controle pegamos um pedaço de cabo flat de 12 vias e o separamos em 4 pedaços de 3 vias, assim conseguimos soldar estes pedaços a cada nível e a cada coluna do cubo de led, ficando um pedaço para cada linha de colunas do cubo, enquanto o último pedaço é soldado nos níveis, conforme vocês podem ver na imagem abaixo.


Para os cabos soldados nas colunas, a outra extremidade é ligada nas saídas numeradas de 2 até 10 de um Arduino, com cada via passando antes por um resistor de 330Ω. Já para os 3 níveis conectamos cada via ao coletor de transistores NPN 2N2222. Esses transistores possuem seus emissores aterrados, enquanto suas bases estão ligadas às saídas A0, A1 e A2 do Arduino por meio de resistores de 10k. Isso está representado na imagem abaixo.




Agora que já montamos nosso cubo e já sabemos o básico sobre o seu funcionamento, vamos começar a criar e explorar alguns efeitos que podemos criar com ele!


Explorando com o cubo

1 – Função “seleciona_nivel()”


Essa função é bem simples e o objetivo dela é escrever como alto o nível selecionado, enquanto demais são mantidos em nível lógico baixo. Ela ficou implementada como segue:


void seleciona_nivel(int nivel){
   for (int i = 0; i < 3; ++i){
      digitalWrite(niveis[ i ], LOW);
}
   if (nivel >= 1 && nivel <= 4){
      if (nivel != 4) {
         digitalWrite(niveis[nivel - 1], HIGH);
      }
      else{
         for (int i = 0; i < 3; ++i){
            digitalWrite(niveis[ i ], HIGH);
         }
      }
   }
}

Aqui começamos escrevendo todos os níveis como baixo, em seguida é verificado se o valor de nível escolhido está no intervalo correto, se não está, então nada é realizado, apenas se mantêm os níveis como baixo, mas se ele está no intervalo correto, então o nível selecionado é escrito como alto. Observe que se o nível selecionado for 4 então todos os níveis são descritos como alto.

Veja que o nível foi acionado por meio de um vetor chamado “niveis[ i ]”, esse é um vetor de 3 posições com cada nível do cubo descrito nele. Além desse vetor, temos outro para as colunas (com nove posições) e uma matriz para os estados do cubo.


2 – Função “envia_estados()”


A segunda função implementada é a função enviar estados. Ela funciona com o auxílio da variável global “int estados[3][9];”, e é responsável por enviar os valores dessa matriz para a saída, então o que ela faz é simplesmente realizar uma varredura da matriz e enviar os valores dela para as saídas correspondentes do Arduino. Ela ficou como mostra abaixo.


//Envia a matriz que representa os niveis dos led para o cubo.
//Pode realizar uma varredura nivel a nivel (varredura 1) ou
//habilitar todos os niveis simultaneamente (varredura 2).
void envia_estados(int estados[ 3 ][ 9 ], int varredura){
   if(varredura==1){
      for(int i=0; i<=2; i++){
         for(int j=0; j<9; j++){
            digitalWrite(colunas[ j ], estados[ i ][ j ]);
            seleciona_nivel(i+1);
         }
      }
   }

   else if(varredura==2){
      for(int j=0; j<9; j++){
         digitalWrite(colunas[ j ], estados[ 0 ][ j ]);
      }
      seleciona_nivel(4);
   }
}

O código começa verificando o modo de varredura desejado, ou seja, se é desejado realizar uma varredura nível a nível, ou se o objetivo é controlar as colunas apenas. Se o modo escolhido for o 1, então temos dois loops, o primeiro acionando as colunas e o segundo selecionando o nível, enviando assim todos os valores da matriz para a saída. Entretanto, se a opção foi a 2 então temos apenas um loop lendo as colunas, e o único nível enviado para a saída é o nível 0 da matriz.


3 – Função “acende_limpa_cubo()”


Essa função é bem simples, ela é formada apenas pelos loops para a escrita em matriz, como mostrado abaixo:



//Função torna alto ou baixo o nivel lógico de todos os leds do cubo.
void acende_limpa_cubo(int on_off){

   for(int i=0; i<4; i++){
      for(int j=0; j<9; j++){
         estados[i][j]=on_off;
      }
   }
}

Nessa função temos que o valor de “on_off” é escrito em todas as posições da matriz “estados[ 3 ][ 9 ]”, assim, se passarmos 1 para esse função e posteriormente enviar essa matriz para a função “envia_estados()”, o cubo acenderá completamente, mas ele apagará completamente se o valor de on_off for 0.


4 – Função “led_aleatorio()”


Como o nome já diz, o objetivo dessa função é acender um led aleatório, ou seja, colocar em alto uma posição aleatória na matriz estados. Para isso usamos a função “random()” do Arduino para selecionar a posição aleatória, como no código abaixo.


//Acende ou apaga um led aleatório do cubo.
void led_aleatorio(int pausa, int on_off){
   estados[random(0,3)][random(0,9)]=on_off;
   for(int tempo=0; tempo<pausa; tempo++){
      envia_estados(estados, 1);
   }
}

Essa função recebe dois valores, a “pausa” determina por quanto tempo o led ficará aceso, porém, diferente de outros lugares do código, aqui não usamos a função “delay()” e sim um “for”. Fazemos isso porque o delay iria pausar a execução do código, mantendo apenas o último comando da função “envia_estados()’, por outro lado o “for” executa essa função completamente, várias vezes, dando a impressão de que o programa foi pausado.

O outro parâmetro recebido pela função é o “on_off”, que assim como o parâmetro de mesmo nome atua na função “acende_limpa_cubo()”, determina se os leds serão ligados ou desligados.


5 – Função “Plano_vertical()”


A próxima função que trazemos aqui é a função “plano_vertical()”, nessa função acendemos leds que estão posicionados lado a lado, gerando planos verticais. O código ficou como abaixo:


//Acende conjuntos de 3 colunas do cubo, formando planos.
void plano_vertical(int plano){
   if(plano>=1 && plano<=3){
      for(int i=0; i<3; i++){
         for(int j=(3*(plano-1)); j<(3*plano); j++){
            estados[i][j]=1;
         }
      }
   }
   else if(plano>=4 && plano <=6){
      for(int i=0; i<3; i++){
         for(int j=(plano-4); j<(plano+3); j+=3){
            estados[i][j]=1;
         }
      }
   }
}

Primeiramente os planos que essa função gera estão enumerados de 1 a 6, sendo que os de 1 a 3 são paralelos ao plano frontal e os de 4 a 6 são perpendiculares a estes. Assim, essa função recebe o parâmetro “plano” para selecionar o plano que será acionado, e por meio dos loops adequados ele envia nível alto para a matriz de estados.


6 – Função “gira_led()”

Agora vamos falar da última função criada para controlar o cubo, a função “gira_led()”.A ideia por trás dessa função está em usar uma matriz 4×9, sendo que em cada linha armazenamos os níveis lógicos de todas as colunas de led, dessa forma ao alternamos entre as linhas dessa matriz, os leds em nível alto irão aparentar ser deslocados.

Assim podemos gerar um efeito de rotação usado um loop que envia os valores presentes na linha para saída, e em seguida passa para a próxima linha após uma breve pausa.

Essa função ficou como abaixo:


//Função cria o efeito de leds girando em torno da coluna
//central. A quantidade de voltas pode ser inserida e o sentido
//de rotação pode ser horário ou anti-horário, sendo para horário
//e 2 para anti-horário.
void gira_led(int pausa, int voltas, int sentido) {
   // Padrões de ligar/desligar dos LEDs para cada passo do movimento
   const int padrao_horario[4][9] = {
      {LOW, LOW, HIGH, LOW, HIGH, LOW, HIGH, LOW, LOW},
      {LOW, LOW, LOW, HIGH, HIGH, HIGH, LOW, LOW, LOW},
      {HIGH, LOW, LOW, LOW, HIGH, LOW, LOW, LOW, HIGH},
      {LOW, HIGH, LOW, LOW, HIGH, LOW, LOW, HIGH, LOW}
   };
   const int padrao_antihorario[4][9] = {
      {LOW, LOW, HIGH, LOW, HIGH, LOW, HIGH, LOW, LOW},
      {LOW, HIGH, LOW, LOW, HIGH, LOW, LOW, HIGH, LOW},
      {HIGH, LOW, LOW, LOW, HIGH, LOW, LOW, LOW, HIGH},
      {LOW, LOW, LOW, HIGH, HIGH, HIGH, LOW, LOW, LOW}
   };

   int (*padrao)[9]; // Ponteiro para o padrão de acordo com o sentido
   if (sentido == 1) {
      padrao = padrao_horario;
   }
   else {
      padrao = padrao_antihorario;
   }
   for (int i = 0; i < voltas; i++) {
      for (int j = 0; j < 4; j++) {
         // Define o estado dos LEDs de acordo com o padrão atual
         for (int k = 0; k < 9; k++) {
            digitalWrite(colunas[k], padrao[j][k]);
         }
         delay(pausa);
      }
   }
}

Para essa função enviamos os valores de “pausa”, “voltas” bem como o “sentido”.

Começando pela “pausa”, esse parâmetro define o tempo entre um estado e outro, assim, se quisermos um efeito de rápida rotação passamos um valor baixo para a “pausa”. O parâmetro “voltas” define a quantidade de vezes que o efeito se repete, ou seja, se for enviado 10 para “voltas”, esse efeito será repetido 10 vezes. Finalmente o “sentido” de rotação é definido, sendo que 1 é para sentido horário e 2 para anti-horário.


Loop Principal


Finalmente vamos falar do nosso loop principal, nele, o que fazemos é usar as funções já apresentadas para criar diversos efeitos com o cubo. Por exemplo, o trecho abaixo apresenta uma forma de acender o cubo, acendendo leds de forma aleatória:


for(int n=0; n<100; n++){ //Cubo acendendo 1 led por vez, de forma aleatória
   led_aleatorio(500,1);  //até todos os leds serem acesos.
}
acende_limpa_cubo(1);
for(int n=0; n<1000; n++){
   envia_estados(estados, 1);
}

Aqui executamos a função “led_aleatório()” 100 vezes, assim o cubo vai acendendo lentamente, um led por vez. Entretanto para garantir que qualquer led que não tenha sido aceso ainda acenda, ao final da execução dessa função, usamos a função “acende_limpa_cubo(1)”. Para apagar o cubo lentamente podemos usar o mesmo código, apenas mudando o valor de “on_off” enviado para as funções “led_aleatório(500, on_off)” e “acende_limpa_cubo(on_off)”, ou seja mudar o valor de “on_off” para 0.

Outro efeito que podemos é fazer planos “caminharem” pelo cubo, para isso a função “planos_venticais()” é usada como mostrado abaixo.


acende_limpa_cubo(1);
for(int nivel=0; nivel<4; nivel++){ //"planos" subindo e descendo.
   seleciona_nivel(nivel);
   delay(250);
}
seleciona_nivel(0);
delay(250);
for(int nivel=3; nivel>=0; nivel--){
   seleciona_nivel(nivel);
   delay(250);
   if(nivel==0){
      acende_limpa_cubo(0);
      envia_estados(estados, 2);
   }
}

for(int n=0; n<3; n++){ //"planos" verticais se movendo para lateralmente.
   plano_vertical(n+4);
   envia_estados(estados,2);
   delay(250);
   acende_limpa_cubo(0);
   envia_estados(estados, 2);
}

delay(250);

for(int n=0; n<3; n++){
   plano_vertical(6-n);
   envia_estados(estados,2);
   delay(250);
   acende_limpa_cubo(0);
   envia_estados(estados, 2);
}

for(int n=0; n<3; n++){ //"planos" verticais se movendo para tras e para frente.
   plano_vertical(n+1);
   envia_estados(estados,2);
   delay(250);
   acende_limpa_cubo(0);
   envia_estados(estados, 2);
}

delay(250);

for(int n=0; n<3; n++){
   plano_vertical(3-n);
   envia_estados(estados,2);
   delay(250);

   acende_limpa_cubo(0);
   envia_estados(estados, 2);
}

A princípio temos a função “acende_limpa_cubo(1)” mantendo todos os valores da matriz de estados em alto, em seguida, os dois laços de repetição alternam os níveis acionados, causando o efeito de “subir e descer”. Os laços seguintes usam a função “planos_verticais()” para produzir um efeito similar nas demais direções.

O ultimo efeito do qual vamos falar usa a função “gira_led()”, ao contrário das funções anteriores essa não necessita de um loop() para se realizar um efeito, sendo que ela foi aplicada como abaixo:


seleciona_nivel(3); //Efeito de leds girando uma vez em cada nivel,
gira_led(75, 15, 2); //alternando o sentido.
seleciona_nivel(2);
gira_led(75, 15, 1);
seleciona_nivel(1);
gira_led(75, 15, 2);

Nesse trecho de código gera o efeito de rotação uma vez em cada nível em sentidos alternados, ou seja, no sentido anti-horário nos níveis 1 e 3 e no sentido horário no nível 2, gerando 15 rotações por nível.

Enfim, o código deste projeto bem como o de outros projetos do nosso blog podem ser encontrados no nosso github.

Não esqueça de contar para a gente o que você achou desse projeto nos comentários e qual o próximo que vocês querem ver por aqui!!!

0

Projetos


Os hamsters são animais conhecidos por seu habito de correr incessantemente em suas rodinhas, desse modo é normal se perguntar que velocidade eles alcançam, ou ainda, querer saber que distancia eles percorrem. Pensando nisso construímos um velocímetro e contador de voltas para rodinhas de hamsters. Curioso para saber como? Não se preocupe que contamos neste post.

Antes de mais nada vamos aos materiais usados, que são os seguintes:

  1. Arduino Uno;

  2. Display LCD 16×2;

  3. Sensor infravermelho TCRT5000;

  4. Resistores de 330Ω, 3,3kΩ e 10kΩ (1 de cada);

Montando o velocímetro

A ideia aqui é usar um sensor infravermelho TCRT5000  para encontrar o momento em que o hamster completa uma volta na sua rodinha, e com o tempo de cada volta, juntamente ao tamanho da rodinha calcularmos a velocidade e a distancia percorrida por ele, para em seguida exibir esses valores em um display LCD. Para isso, vamos montar o circuito como mostrado na figura abaixo.

Nessa figura temos que o display LCD esta ligado com os pinos D4 a D7 nos pinos de mesmo nome do Arduino (ou seja, nos pinos PD4 a PD7 no Atmega328p), além disso, os pinos  E, R/W e RS estão conectados nos pinos A0, A1 e A2 (ou PC0, PC1 e PC2) respectivamente. Para a alimentação do display, assim como o TCRT5000, esta sendo utilizado os 5V fornecidos pelo próprio Arduino.

O funcionamento do TCRT5000 se da por meio de um fototransistor acoplado a um LED infravermelho, assim, quando um objeto é posto próximo desse sensor, a luz infravermelha é refletida para o fototransistor, que atuara como um circuito fechado. No nosso circuito, o emissor do fototransistor esta conectado ao terra (gnd), portanto quando ele atuar como um circuito fechado temos que a tensão no coletor do fototransistor e consequentemente pino D8 do Arduino (ou PB0 para o Atmega, pino onde o conector esta ligado) sera de 0V. Já quando não existir nenhum objeto próximo ao TCRT5000, o fototransistor funciona como um circuito aberto, e a tensão de seu coletor sera de 5V, ja que o mesmo esta ligado nessa tensão por meio de um resistor de 10kΩ.

Programando nosso velocímetro

Agora que já temos os materiais em mãos e montamos o nosso circuito, iremos programar o Arduino. O código completo usado nesse projeto, além da biblioteca usada para controlar o LCD estão presentes no nosso GitHub.

O que vamos fazer no código é usar o timer0 para nos fornecer uma medida de tempo para o calculo da velocidade de nosso velocímetro, para isso, precisamos que ele seja reiniciado a cada volta completada pelo hamster. Com esse fim vamos usar a interrupção por mudança de estado do pino PB0, assim, sempre que o microcontrolador receber um sinal do TCRT5000, ele sofrera uma interrupção, e nela ocorrera a contagem da quantidade de voltas dada pelo Hamster e a inicialização do timer, sendo que após duas interrupções suscetivas, ou seja, duas voltas completas, a velocidade é calculada. Entretanto, se a rodinha ficar inativa por muito tempo (mais que 10 segundos), usaremos a interrupção por estouro do timer para desliga-lo.

Para simplificar a escrita e a compreensão do nosso código, usamos 5 funções (além da main e das interrupções), que servem para ligar o LCD, configurar o pino PB0 como entrada habilitando as interrupções, configurar o timer0, realizar os cálculos necessários e criar um loop que mantem o circuito sempre em funcionamento. Cada função usada esta explicada abaixo.

Função “Liga_LCD()”

A primeira função que vamos discutir é a função “Liga_LCD()”, ela foi implementada como presente abaixo.

void Liga_LCD(void){
          LCD_init();
          LCD_clear();
          LCD_move_cursor(0,1);
          LCD_write("Hodometro para");
          LCD_move_cursor(1,4);
          LCD_write("Hamsters");
          _delay_ms(2000);
          LCD_clear();
}

Nela iniciamos o display usando “LCD_init()”, então limpamos o display com “LCD_clear()” e finalmente a mensagem “Hodometro para Hamsters” é exibida por dois segundos com as funções “LCD_write()”, “LCD_move_cursor()” e “_delay_ms()”. 

A função LCD_init() envia um conjunto de instruções que configuram o display no modo de 4 bits com o cursor ligado. Enquanto isso a função LCD_clear() apaga tudo o que esta exibido no display e retorna o cursor para a primeira posição da primeira linha. Já a função LCD_move_cursor() posiciona o cursor na posição desejada no display, essas funções fazem parte da biblioteca LCD.h. Já a ultima função _delay_ms() pertence a biblioteca <util/delay.h> e pausa a execução do programa pelo tempo desejado.

Função “configura_contador()”

A próxima função configura PB0 como entrada no registrador DDRB, em seguida, o pull up é ativado, isso mantém o nível de tensão do pino PB0 em 5V, a menos que a entrada seja 0V, o que facilita a identificação de transições para baixo. Logo após, usamos os registradores PCICR e PCMSK0, para habilitar as interrupções por mudança de estado do pino PB0, assim, sempre que o pino PB0 muda de estado, ocorre uma interrupção. Essa função ficou como mostra o exemplo abaixo:

void configura_contador(void){
          DDRB &= ~(1<<PB0);
          PORTB |= (1<<PB0);

          PCICR |= (1<<PCIE0);
          PCMSK0 |= (1<<PCINT0);
}

A rotina da interrupção para a mudança de estado do pino PB0 em primeiro lugar realiza a leitura desse pino, e se este sofreu transição para nível alto, apenas um pequeno delay de 50ms é realizado, mas se a transição é para nível baixo, então o timer0 é reiniciado, a quantidade de voltas é incrementada, e o valor do tempo é salvo na variável “tempo”. Essa interrupção foi implementada como mostrado a seguir:

ISR(PCINT0_vect){
          if(PINB & (1<<PB0)){
          _delay_ms(50);
          }
          else{
                    voltas +=1;
                    voltas_total +=1;
                    tempo = i;
                    i = 0;
_delay_ms(50);

TCCR0B |= (1<<CS00);
TCNT0 = 0; } }

Função “configura_timer()”

Da mesma forma que a função configura contador, essa função configura uma interrupção, mas desta vez por estouro do timer0. Para isso, primeiro configura-se o timer0 no modo de operação normal e desconectado. Essa configuração foi selecionada, pois com ela é possível obter uma precisão de 0,16ms, que é o suficiente para a construção do velocímetro. Por fim a interrupção por estouro do timer é habilitada. A função que configura o timer esta presente abaixo.

void configura_timer(void){
          TCCR0A = 0;
          TCCR0B = 0;
          TIMSK0 |= (1<<TOIE0);
}

Com essa configuração temos 62500 interrupções por estouro por segundo, sendo que a cada interrupção a variável “i” é acrescida em 1 e quando ocorre uma interrupção por mudança de estado do pino PB0, o valor de “i” é salvo em “tempo” e “i” é reiniciada com zero. Entretanto, se não ocorre nenhuma mudança de estado em PB0 dentro de 10 segundos, então i>624999 e quando isso ocorre, a interrupção desconecta o timer e reinicia as variáveis “voltas” e “i”. Essa interrupção está abaixo.

ISR(TIMER0_OVF_vect){
          i++;
          if (i>624999){
                    i=0;
                 
                    voltas = 0;
                    TCNT0 = 0;
                    TCCR0B &= ~(1<<CS00);
          }
}

Ao se desconectar o timer o Arduino fica aguardando uma interrupção por mudança de estado do PB0 para reiniciar o timer. Além disso o velocímetro é zerado.

Função “calcula_velocidade()”

O código desta função está abaixo:


void calcula_velocidade(float tempo, float raio){
    if (voltas > 1){
        tempo = tempo/(62500);
        distancia = (voltas_total-1)*2*3.1416*raio;
        velocidade = 3.6*2*3.1416*raio/tempo;
    }
    else {
        velocidade = 0;
    }
}

Esta função é a responsável pelo calculo da velocidade do hamster em km/h. Para isso, primeiro é verificado se houveram 2 incrementos na variável “voltas”, uma vez que na primeira vez que o TCRT5000 é ativado, o timer está zerado. Se já houveram 2 incrementos em voltas, então o tempo em segundos é calculado usando:

(1)   \begin{equation*} tempo=\frac{tempo}{62500} \end{equation*}

Com o tempo em segundos calculamos a velocidade com a equação abaixo.

(2)   \begin{equation*} velocidade=3.6*\frac{2*\pi*raio}{tempo} \end{equation*}

Nessa equação temos que 2*\pi*raio é o comprimento da circunferência da rodinha do HHamster, ou seja, a distancia que ele percorre em uma volta completa. Assim a velocidade será obtida dividindo este valor pelo tempo. O fator 3.6 é usado para converter a velocidade (encontrada em metros por segundo) para quilômetros por hora.

Além disso, para calcular a distancia percorrida pelo Hamster basta multiplicar a circunferência de sua rodinha pelo numero de voltas dada, como abaixo.

(3)   \begin{equation*} distância=2*\pi*raio*numero-de-voltas \end{equation*}

Função “loop()”

Por fim temos a função loop(), nossa ultima função, que ficará sempre em execução, sendo implementada como abaixo:



void loop(void){
    char str_distancia[20];
    char str_velocidade[20];
    int vel_int;
    int vel_dec;
    int distancia_int;
    int distancia_dec;
   
    while(1){
                calcula_velocidade(tempo, raio);
                distancia_int = floor(distancia);
                distancia_dec = 100*(distancia-distancia_int);
                sprintf(str_distancia, "Dis: %d,%d[m]   ", distancia_int, distancia_dec);
                vel_int = floor(velocidade);
                vel_dec = 100*velocidade - 100*vel_int;
                sprintf(str_velocidade, "Vel: %d,%d[km/h]   ", vel_int, vel_dec);
                LCD_move_cursor(0,0);
                LCD_write(str_distancia);
                LCD_move_cursor(1,0);
                LCD_write(str_velocidade);
                voltas_cont=voltas;
         }
    }   
}

Nessa função, primeiro temos a declaração das variáveis a serem utilizadas sendo duas strings e quatro variáveis int. Em seguida, temos um loop infinito (while(1)), nele utiliza-se a função calcula_velocidade() para se calcular a velocidade e a distancia total percorridas pelo Hamster. Em seguida, estes valores são gravados em números inteiros, primeiro a parte inteira, depois as duas primeiras casas depois da virgula. Isso porque a função sprintf, usada para transformar números em strings, não funciona com variáveis do tipo float. Finalmente esses valores são exibidos no display do nosso velocímetro.

Função “main()”

Por fim, temos a função main do nosso programa, que ficou como a seguir:

void main(void){   
    SREG   |=  (1<<7);
    voltas  = 1;
    Liga_LCD();
    configura_contador();
    configura_timer();

    loop();
}

Na função main, apenas habilita-se as interrupções globais e chama-se as funções Liga_LCD(), configura_contador(), configura_timer() e loop().

Testando

Para testar o nosso projeto contamos com a ajuda do Antônio, o nosso hamster atleta, e ao final de uma noite ele correu por 3200 metros. No link abaixo você encontra o nosso projeto funcionando.
https://youtube.com/shorts/fL2wfnrB3ng?feature=share
E então, o que vocês acharam deste projeto? Conte pra gente nos comentários.


0

Projetos

É provável que você já tenha desejado reproduzir um áudio com o Arduino, mas como se faz isso? Vem com a gente, que nesse post você vai aprender!

A ideia por traz do projeto desse post é usar uma rede R2R, que é um conversor digital analógico formado por alguns resistores (se você ainda não conhece, confira o nosso post sobre ela), para reproduzir um áudio salvo na memória de um Arduino, de maneira simples e barata, usando poucos componentes.

Para esse projeto você não irá precisar de muita coisa, os materiais são:

  1. 25 resistores de 1kΩ

  2. 1 capacitor de 100nF

  3. Uma caixa de som

  4. Um Arduino

Esse projeto pode ser dividido em 3 etapas, sendo elas:

  1. Escolha e extração de um arquivo de áudio

  2. Escrita do código a ser executado

  3. Montagem do circuito


Escolha e Extração do Áudio 

A primeira etapa é a escolha de um arquivo de áudio. Para isso, precisamos levar em conta uma limitação comum em microcontroladores, a memória. No caso do Arduino, ela é de 32Kb, então precisamos garantir que o arquivo de áudio utilizado e o programa escrito não excedam este valor.

Assim, escolhemos um arquivo que seja curto, e com uma baixa frequência de amostragem. Para este artigo foi escolhido um áudio de cerca de 16 segundos com uma frequência de amostragem de 2000 amostras por segundo.
Com um arquivo de áudio selecionado, é preciso transformar ele em um vetor, que será gravado no Arduino. Para isso, usamos a função audioread do octave. O resultado dessa função é uma matriz [y fs], na qual y são os dados de áudio e fs a taxa de amostragem.

Os dados y são dispostos na forma de uma matriz que tem os frames de áudio colocados nas linhas e os canais nas colunas. A fim de simplificar o processo e de gerar um arquivo menor usamos um áudio mono, que fará com que y possua apenas uma coluna.

O próximo passo é transformar y em um vetor de uma unica linha com valores inteiros de 0 a 255, o que é possível com algumas contas simples e com a função reshape. Por fim, esse arquivo é salvo como um arquivo “.h”, que sera usado no código do Arduino. Isso esta demonstrado no script abaixo.

# Le o arquivo de audio, aqui usamos inicio e fim de modo que fim-inicio = numero_de_amostras. 
[y fs] = audioread("nome_do_arquivo_de_audio", [inicio fim]) 

# Determina o numero de linhas e o numero de colunas (1 para áudios mono) de y. 
[linha coluna] = size(y) 

# Transpõe y, em um vetor de 1 linha. 
y = reshape(y, 1, linha) 

# Transforma y em um vetor de 0 a 255 com números inteiros e salva em audio. 
audio = round(255*y+128) 

# Cria um arquivo .h com "const unsigned char pontos[ ] PROGMEM = {" salvo na primeira linha. 
dlmwrite("Audio.h", "const unsigned char pontos[ ] PROGMEM = {", "") 

# Adiciona o vetor com o audio ao arquivo .h criado. 
dlmwrite("Audio.h", audio, ",", "-append") 

# Fecha a chave. 
dlmwrite("Audio.h", "}", "", "-append")



Importante lembrar que para este script funcionar adequadamente, o arquivo de áudio deve estar salvo na mesma pasta que o octave. A fim de melhor aproveitar a memória do microcontrolador, foi usado o comando PROGMEM, que faz com que a variável, quando gravada, seja salva na memória flash, e não na RAM do Arduino.


Código do Arduino

O código implementado no Arduino ficou como está a seguir

#define F_CPU 16000000

#include <avr/pgmspace.h>
#include <avr/interrupt.h>
#include "Pontos.h"

int i=0, Min=130;

//Interrupção quando há um estouro do timer0.
ISR(TIMER0_OVF_vect){ 

     //Reinicia o timer0.
     TCNT0 = Min; 

     //Manda o valor da posição i do vetor com o audio para o PORTD.
     PORTD = pgm_read_byte_near(pontos + i); 

     //incrementa a posição do vetor com o audio.
     if(i>30998){ 
          i = 0;
     }
     else{
          i += 1;
     }
}

//Configura o timer0 e as saídas.
void main (void){
     //Habilita interruoções globais.
     SREG |= (1<<7); 

     //Configura o timer0 para operação normal, não conectada.
     TCCR0A = 0; 
     TCCR0B = 0;

     //Define o prescale como sendo 64.
     TCCR0B |= ((1 << CS01)+(1 << CS00)); 

     //Inicia o timer0.
     TCNT0 = Min; 

     //Habilita interrupção por estouro do timer 0.
     TIMSK0 |= (1<<TOIE0); 

     //Configura o PORTD (pinos 0 ao 7), como saídas.
     DDRD = 0xFF; 

     //Loop vazio
     while (1){

     }
}

No código acima, vimos que o registrador TCNT0 incrementa seu valor em uma unidade uma vez a cada \frac{N}{f_{CPU}} segundo com N sendo o valor do prescale (64). Além disso, haverá um estouro no timer sempre que o valor de TCNT0 atingir 255. Dessa forma, temos que a frequência do timer (f) será:

(1)   \begin{equation*} f=\frac{f_{CPU}}{N*(255-Min)} \end{equation*}

Com Min sendo o menor valor de TCNT0. A fim de que o audio seja reproduzido adequadamente, precisamos que f seja igual a fs. Usando fs=2000, f_{CPU} = 16MHz e N igual a 64, temos:

(2)   \begin{equation*} 2000=\frac{16000000}{64*(255-Min)} \end{equation*}

Resolvendo para Min encontramos Min = 130. Desse modo, temos um estouro do timer a cada 0,5ms, fazendo com que a rotina de interrupção ISR seja acionada. Nessa interrupção, primeiramente atualizamos o valor presente na saída (PORTD) e reiniciamos o valor de TCNT0.

Em seguida, atualizamos a posição do vetor que será mandado ao PORTD na próxima iteração.
Já na função main temos a configuração do PORTD como saída, configuração do timer0 no modo desconectado com o prescale definido e configuração da interrupção por estouro do timer.
Por fim temos o loop vazio, que mantem o microcontrolador ativo aguardando a próxima interrupção. O código usado e um arquivo .h com um áudio exemplo estão disponíveis em nosso GitHub.

Montagem

A montagem é bem simples, como mostra a imagem abaixo:


Ela basicamente consiste em uma rede R2R, com a saída ligada em um capacitor e uma caixa de som para computador. Já os pinos de 0 a 7 são ligados aos pinos de mesmos valores do Arduino.
O funcionamento desse circuito é simples, já que ele consiste basicamente em um conversor digital analógico. Esse conversor transforma a saída do Arduino em um sinal de tensão analógico que é usado na caixa de som. O capacitor presente é um capacitor de desacoplamento, já que a saída do R2R apresenta um nível de tensão CC e a componente de CA.
É importante observar que esse circuito não apresenta um amplificador, já que a caixa de som usada já possui um amplificador interno. Viu só como foi fácil? Agora me conta nos comentários, que som que você vai usar para esse projeto? 

0