Obs. Há diversos modelos de Encoders com as mais variadas finalidades, como por exemplo as que são utilizados em mouse de computador (aquela rodinha utilizada para o scroll), mas o que iremos abordar aqui são os chamados Rotary Encoders do tipo "Quadrature Enconder".
Enquanto um potenciômetro possui limites físico de mínimo e máximo, o que limita o ângulo de giro do mesmo, um Rotary Encoder não os possui, podendo ser rotacionado infinitamente para qualquer um dos sentidos: horário e anti-horário. Isso significa que há diferenças na forma de trabalharmos com cada um pois em um potenciômetro a leitura é feita de modo analógico, ou seja, basicamente o potenciômetro é um divisor de tensão variável que tem seu valor alterado de acordo com a posição física do mesmo, enquanto que um Rotary Encoder, a informação que ele nos dá, não é relacionada à posição física em que o eixo se encontra, e sim baseado no sentido ao qual ele está girando ou se está parado, de modo que conforme ele gira, são emitidos sinais através de dois pinos que indicam o sentido de giro. Há ainda um botão em seu próprio eixo, o qual é muito útil em diversas aplicações.
Existem vantagens e desvantagens em seu uso em comparação com potenciômetros. Em geral um único Rotary Encoder pode ser utilizado pra configurar diferentes parâmetros em uma mesma aplicação, como em um rádio de carro, onde o mesmo pode ser utilizado pra aumentar ou diminuir o volume, busca por estações de rádio, ajustes no som como balanço, agudo, grave, etc, o que seria mais complicado de se fazer com um potenciômetro.
Outra vantagem de um Rotary Encoder é que ele pode ser utilizado em paralelo com outras interfaces. Por exemplo, imagine que você tenha que projetar um dimmer com ajuste através de um Rotary Encoder, mas você também gostaria de fazer esse mesmo ajuste através de uma página Web ou ainda de um celular via bluetooth. Ou seja, você teria diversas interfaces para controlar o mesmo parâmetro, o que seria um pouco mais complicado de se fazer com um potenciômetro, já que se um potenciômetro está na metade, por exemplo, seria impossível fazer esse ajuste a partir de qualquer outro lugar, pois fisicamente o potenciômetro continuaria na mesma posição.
Porém em várias aplicações, esses ajustes ou configurações, devem ser “lembrados” (ou seja, gravados em algum lugar), para que quando a aplicação for desligada não perca os dados informados, algo que não seria necessário com o uso de potenciômetros, pois o estado do potenciômetro se mantem mesmo desligado, ao contrário do Rotary Encoder.
Nas duas imagens abaixo, podemos ver que internamente um Rotary Encoder é composto por um eixo rotativo (Rotating shaft), o qual possui fixado a si um disco (Slit disk) com várias fendas espaçadas igualmente. Oposto a esse disco há um segundo disco (Fixed slit) com as mesmas fendas igualmente espaçadas. Ambos os discos ficam entremeio a um led e um par de fotos-transistores. Conforme o eixo gira, a luz emitida pelo diodo é interrompida/liberada pelos discos, de modo que cada um dos fotos-transistores emitam sinais em momentos diferentes.
Em nossos exemplos iremos utilizar um módulo igual ao mostrado na imagem abaixo, composto por um Rotary encoder e dois resistores de pull-up, um ligado ao pino CLK e outro ao Pino DT, há ainda no próprio módulo, espaço para ser ligado um resitor de pull-up ao pino do botão, porém o resistor não vem conectado ao módulo, como pode ser visto na imagem abaixo a esquerda:
Exemplo sem interrupções
No nosso primeiro exemplo vamos ver como implementar uma classe pra manipular o Rotary Encoder, mas sem o uso de Interrupções. Em geral, vários dos exemplos que encontramos na internet se baseiam na comunicação com o Arduino, através do uso de interrupções, porém, nem sempre é viável utilizá-las, pois a quantidade de interrupções de um microcontrolador são limitadas e podem já estar em uso por outros dispositivos, ou mesmo que não estejam sendo usadas, podemos estar trabalhando em uma aplicação que não tenha rotinas que demandem tempo de processamento muito alto, o que poderia permitir o uso sem maiores problemas de uma solução sem interrupções. Uma desvantagem desse método, é que podem ocorrer perdas de leituras durante o uso do enconder caso o Arduino esteja executando outra tarefa no exato momento de mudança de estado do encoder. Em geral, perder alguns pulsos ao utilizar o Encoder não é tão crítico assim, já que o próprio usuário tem o feedback visual e faz a correção até alcançar o valor desejado, porém, se o encoder estiver sendo utilizado pra detectar os giros de um motor, por exemplo, perder pulsos não é aceitável e pode levar a erros de execução da aplicação, logo, essa solução não seria a mais adequada. Entretanto, o que vai definir qual tipo de solução a ser utilizada serão as necessidades do seu projeto. Nesse artigo veremos soluções com e sem interrupções.
Ligação
Existem vantagens e desvantagens em seu uso em comparação com potenciômetros. Em geral um único Rotary Encoder pode ser utilizado pra configurar diferentes parâmetros em uma mesma aplicação, como em um rádio de carro, onde o mesmo pode ser utilizado pra aumentar ou diminuir o volume, busca por estações de rádio, ajustes no som como balanço, agudo, grave, etc, o que seria mais complicado de se fazer com um potenciômetro.
Outra vantagem de um Rotary Encoder é que ele pode ser utilizado em paralelo com outras interfaces. Por exemplo, imagine que você tenha que projetar um dimmer com ajuste através de um Rotary Encoder, mas você também gostaria de fazer esse mesmo ajuste através de uma página Web ou ainda de um celular via bluetooth. Ou seja, você teria diversas interfaces para controlar o mesmo parâmetro, o que seria um pouco mais complicado de se fazer com um potenciômetro, já que se um potenciômetro está na metade, por exemplo, seria impossível fazer esse ajuste a partir de qualquer outro lugar, pois fisicamente o potenciômetro continuaria na mesma posição.
Porém em várias aplicações, esses ajustes ou configurações, devem ser “lembrados” (ou seja, gravados em algum lugar), para que quando a aplicação for desligada não perca os dados informados, algo que não seria necessário com o uso de potenciômetros, pois o estado do potenciômetro se mantem mesmo desligado, ao contrário do Rotary Encoder.
Nas duas imagens abaixo, podemos ver que internamente um Rotary Encoder é composto por um eixo rotativo (Rotating shaft), o qual possui fixado a si um disco (Slit disk) com várias fendas espaçadas igualmente. Oposto a esse disco há um segundo disco (Fixed slit) com as mesmas fendas igualmente espaçadas. Ambos os discos ficam entremeio a um led e um par de fotos-transistores. Conforme o eixo gira, a luz emitida pelo diodo é interrompida/liberada pelos discos, de modo que cada um dos fotos-transistores emitam sinais em momentos diferentes.
Com isso é possível, baseado em qual dos fotos-transistores (Quad input A ou B) foi a 1 (ou HIGH) primeiro, saber se está sendo girado o eixo no sentido horário ou anti-horário. A partir da contagem dos passos que são dados em determinada direção é possível saber o quanto o Rotary girou.
Em nossos exemplos iremos utilizar um módulo igual ao mostrado na imagem abaixo, composto por um Rotary encoder e dois resistores de pull-up, um ligado ao pino CLK e outro ao Pino DT, há ainda no próprio módulo, espaço para ser ligado um resitor de pull-up ao pino do botão, porém o resistor não vem conectado ao módulo, como pode ser visto na imagem abaixo a esquerda:
Exemplo sem interrupções
No nosso primeiro exemplo vamos ver como implementar uma classe pra manipular o Rotary Encoder, mas sem o uso de Interrupções. Em geral, vários dos exemplos que encontramos na internet se baseiam na comunicação com o Arduino, através do uso de interrupções, porém, nem sempre é viável utilizá-las, pois a quantidade de interrupções de um microcontrolador são limitadas e podem já estar em uso por outros dispositivos, ou mesmo que não estejam sendo usadas, podemos estar trabalhando em uma aplicação que não tenha rotinas que demandem tempo de processamento muito alto, o que poderia permitir o uso sem maiores problemas de uma solução sem interrupções. Uma desvantagem desse método, é que podem ocorrer perdas de leituras durante o uso do enconder caso o Arduino esteja executando outra tarefa no exato momento de mudança de estado do encoder. Em geral, perder alguns pulsos ao utilizar o Encoder não é tão crítico assim, já que o próprio usuário tem o feedback visual e faz a correção até alcançar o valor desejado, porém, se o encoder estiver sendo utilizado pra detectar os giros de um motor, por exemplo, perder pulsos não é aceitável e pode levar a erros de execução da aplicação, logo, essa solução não seria a mais adequada. Entretanto, o que vai definir qual tipo de solução a ser utilizada serão as necessidades do seu projeto. Nesse artigo veremos soluções com e sem interrupções.
Ligação
encoder | arduino
clk --> pino 2
dt --> pino 3
sw --> pino 8
/********************************************************************************** ************************************CLASSE ROTARY ENCODER************************** **********************************************************************************/ class RotaryEncoder { private: byte _pin_clk; byte _pin_dt; byte _pin_sw; int _result; public: RotaryEncoder(byte pin_clk, byte pin_dt, byte pin_sw = 255){ //parametro do botao opcional _pin_clk = pin_clk; _pin_dt = pin_dt; _pin_sw = pin_sw; pinMode(_pin_clk, INPUT); pinMode(_pin_dt, INPUT); if (_pin_sw != 255){ pinMode(_pin_sw, INPUT); digitalWrite(_pin_sw, HIGH); } } void update() { static int oldA = HIGH; static int oldB = HIGH; static unsigned long lmillis = 0; _result = 0; if (millis() - 1 > lmillis){ //previne leituras falsas int newA = digitalRead(_pin_clk); int newB = digitalRead(_pin_dt); if ( (newA != oldA || newB != oldB) && (oldA == HIGH && newA == LOW) ) { _result = (-oldB * 2 + 1); } oldA = newA; oldB = newB; lmillis = millis(); } } int read(){ return _result; } //retorn -1, 0 ou 1. int buttonRead(){ return (_pin_sw == 255) ? LOW : digitalRead(_pin_sw); } //retorna a leitura do pino do botão do encoder, caso tenha sido utilizado o botão }; /********************************************************************************** ************************************FIM CLASSE ROTARY ENCODER********************** **********************************************************************************/ RotaryEncoder re(3, 2, 8); //pinos clk, dt, sw int val = 0; void setup() { Serial.begin(9600); } void loop() { int pressionou = false; re.update(); //faz a leitura do rotary encoder if(re.buttonRead() == LOW) { if (val != 0) { pressionou = true; } val = 0; } if (re.read() != 0 || pressionou) { //se rotacionou ou pressionou o botão --> -1, 0 ou 1 val += re.read(); //incrementa ou decrementa Serial.print(val); Serial.print(" "); Serial.println(re.read()); } }
O que fizemos no código anterior foi criar uma classe que gerencia um Rotary Encoder a qual apenas nos diz em qual sentido o encoder girou ou se está parado. Porém a variável que acumula os incrementos e decrementos, não é gerenciada pela classe e sim externamente.
No nosso próximo exemplo vamos incluir algumas funcionalidades extras, onde a própria classe irá gerenciar a variável incrementada, mas com a possibilidade de serem gerenciadas mais de uma variável, além disso permitiremos definir limites mínimos e máximos para cada variável. Isso é útil quando o mesmo encoder é utilizado para configurar vários parâmetros diferentes, como acontece no rádio de um carro, onde o mesmo é utilizado para aumentar o volume, balanço, grave, etc.
Controlando múltiplos parâmetros com o mesmo encoder:
/********************************************************************************** ************************************CLASSE ROTARY ENCODER************************** **********************************************************************************/ #define ROTARY_NO_BUTTON 255 struct RotaryEncoderLimits{ int min; int max; }; class RotaryEncoder { private: byte _pin_clk; byte _pin_dt; byte _pin_sw; byte _num_results; int _result; int * _results; byte _index_result; RotaryEncoderLimits * _limits; public: RotaryEncoder(byte pin_clk, byte pin_dt, byte pin_sw = ROTARY_NO_BUTTON, byte num_results=1, RotaryEncoderLimits * limits=0){ //parametro do botao opcional _pin_clk = pin_clk; _pin_dt = pin_dt; _pin_sw = pin_sw; pinMode(_pin_clk, INPUT); pinMode(_pin_dt, INPUT); if (_pin_sw != ROTARY_NO_BUTTON){ pinMode(_pin_sw, INPUT); digitalWrite(_pin_sw, HIGH); } if (num_results == 0) { num_results = 1; } _num_results = num_results; _results = new int[_num_results]; for (int i; i<_num_results; i++){ _results[i] = (limits) ? limits[i].min : 0; } _index_result = 0; _limits = limits; } byte getIndex() { return _index_result; } void next() { _index_result++; if (_index_result >= _num_results) { _index_result = 0; } } void update() { static int oldA = HIGH; static int oldB = HIGH; static unsigned long lmillis = 0; _result = 0; if (millis() - 1 > lmillis){ //previne leituras falsas int newA = digitalRead(_pin_clk); int newB = digitalRead(_pin_dt); if ( (newA != oldA || newB != oldB) && (oldA == HIGH && newA == LOW) ) { _result = (-oldB * 2 + 1); } oldA = newA; oldB = newB; lmillis = millis(); } if (_results[_index_result]+_result >= _limits[_index_result].min && _results[_index_result]+_result <= _limits[_index_result].max ) { _results[_index_result] += _result; } } int read(){ return _result; } //retorn -1, 0 ou 1. int getValue() {return _results[_index_result]; } //retorna o valor da variável void setValue(int value){ _results[_index_result] = value; } //caso a variável inicializa em determinado valor diferente de zero, utilizar esse método. int buttonRead(){ return (_pin_sw == ROTARY_NO_BUTTON) ? LOW : digitalRead(_pin_sw); } }; /********************************************************************************** ************************************FIM CLASSE ROTARY ENCODER********************** **********************************************************************************/ RotaryEncoderLimits lim[] = { {0,10}, {-5,5}, {-32768, 32767} }; RotaryEncoder re(3, 2, 8, 3, lim); //pinos clk, dt, sw, 3 valores diferentes serão controlados byte lastButton = HIGH; void setup() { Serial.begin(9600); } void loop() { int pressionou = false; re.update(); //primeiro fazemos a chamada para o update para que seja feita a leitura do rotary encoder if( re.buttonRead() == LOW && lastButton != re.buttonRead() ) { //a cada clique, passamos a gerenciar a próxima variável re.next(); pressionou = true; lastButton = re.buttonRead(); delay(300); //debounce meia boca } else { lastButton = re.buttonRead(); } if (re.read() != 0 || pressionou) { //se rotacionou ou pressionou o botão --> -1, 0 ou 1 Serial.print("index: "); Serial.print(re.getIndex()); Serial.print(" = "); Serial.println(re.getValue()); } }
Exemplo com uma interrupção
Agora iremos modificar o código anterior, que executava apenas no loop, para que possamos controlar as chamadas ao método update através do uso de uma única interrupção.
A vantagem do uso de interrupções, é que ela nos dá mais garantias de que não iremos perder pulsos, pois a resposta se dá imediatamente com a detecção da interrupção.
Nos códigos anteriores atualizávamos nosso encoder através de chamadas ao método update dentro do próprio loop. Como agora devemos executar a chamada do método update através de uma interrupção, foi criada uma função chamada interrupt_re, a qual será responsável pela chamada ao método update da classe. Após ser executado o update, os dados internos da classe serão atualizados e poderão ser utilizados da maneira que necessitarmos em nosso programa.
Essa classe detecta de maneira muito precisa os incrementos (sentido horário) e decrementos (sentido anti-horário) de estados na maioria dos casos, mas em uma determinada situação, ele perde a leitura de decremento, mas apenas quando a leitura anterior foi um incremento, ou seja, imagine que você esteja girando o encoder no sentido horário, fazendo vários incrementos, e em determinado momento resolve girar no sentido contrário, realizando alguns decrementos. Esse primeiro pulso, no sentido contrário, será perdido, não sendo detectado, porém a partir dos próximos decrementos a leitura se dá normalmente. Em várias aplicações, esse problema não chega a ser prejudicial ao usuário, mas cabe a você como programador avaliar isso.
/********************************************************************************** ************************************CLASSE ROTARY ENCODER************************** **********************************************************************************/ #define ROTARY_NO_BUTTON 255 struct RotaryEncoderLimits{ int min; int max; }; class RotaryEncoder { private: byte _pin_clk; byte _pin_dt; byte _pin_sw; byte _num_results; volatile int _result; volatile int * _results; byte _index_result; RotaryEncoderLimits * _limits; public: RotaryEncoder(byte pin_clk, byte pin_dt, byte pin_sw = ROTARY_NO_BUTTON, byte num_results=1, RotaryEncoderLimits * limits=0){ //parametro do botao opcional _pin_clk = pin_clk; _pin_dt = pin_dt; _pin_sw = pin_sw; pinMode(_pin_clk, INPUT); pinMode(_pin_dt, INPUT); if (_pin_sw != ROTARY_NO_BUTTON){ pinMode(_pin_sw, INPUT); digitalWrite(_pin_sw, HIGH); } if (num_results == 0) { num_results = 1; } _num_results = num_results; _results = new int[_num_results]; for (int i; i<_num_results; i++){ _results[i] = (limits) ? limits[i].min : 0; } _index_result = 0; _limits = limits; } byte getIndex() { return _index_result; } void next() { _index_result++; if (_index_result >= _num_results) { _index_result = 0; } } void update() { static int oldA = HIGH; static int oldB = HIGH; _result = 0; int newA = digitalRead(_pin_clk); int newB = digitalRead(_pin_dt); if ( (newA != oldA || newB != oldB) && (oldA == HIGH && newA == LOW ) ) { _result = (- oldB * 2 + 1); } oldA = newA; oldB = newB; if (_results[_index_result]+_result >= _limits[_index_result].min && _results[_index_result]+_result <= _limits[_index_result].max ) { _results[_index_result] += _result; } } int read(){ return _result; } //retorn -1, 0 ou 1. int getValue() {return _results[_index_result]; } //retorna o valor da variável corrente void setValue(int value){ _results[_index_result] = value; } //caso a variável inicializa em determinado valor diferente de zero, utilizar esse método. int buttonRead(){ return (_pin_sw == ROTARY_NO_BUTTON) ? LOW : digitalRead(_pin_sw); } }; /********************************************************************************** ************************************FIM CLASSE ROTARY ENCODER********************** **********************************************************************************/ RotaryEncoderLimits lim[] = { {0,10}, {-5,5}, {-32768, 32767} }; //limites máximos e mínimos que as variaveis podem atingir RotaryEncoder re(2, 3, 8, 3, lim); //pino clk, pino dt, pino sw, variaveis //executado pela interrupcao void interrupt_re() { re.update(); if (re.read() != 0 ) { //se rotacionou --> -1, 0 ou 1 Serial.print("index: "); Serial.print(re.getIndex()); Serial.print(" = "); Serial.print(re.getValue()); Serial.print(" - "); Serial.println(re.read() > 0 ? "horario" : "anti-horario"); } } void setup() { Serial.begin(9600); attachInterrupt(INT1, interrupt_re, CHANGE); } void loop() { static byte b = HIGH; //pra ler apenas uma vez o botao ao pressionar if( re.buttonRead() == LOW && b != re.buttonRead() ) { re.next(); //passa para a próxima variável (index) Serial.print("index: "); Serial.print(re.getIndex()); Serial.print(" = "); Serial.print(re.getValue()); Serial.println(" - Click"); b = re.buttonRead(); delay(200); //debounce meia boca } else { b = re.buttonRead(); } }
Exemplo com duas interrupções
Esse próximo exemplo é o mais seguro se tratando de evitar perdas de pulsos, já que ele se utiliza de duas interrupções, uma para o pino de clock e outra para o pino dt do encoder.
Como pode ser visto agora, a classe possui dois métodos update, um a e outro b, que irão ser chamados de acordo com a detecção da interrupção de cada um dos pinos.
/********************************************************************************** ************************************CLASSE ROTARY ENCODER************************** **********************************************************************************/ #define ROTARY_NO_BUTTON 255 struct RotaryEncoderLimits{ int min; int max; }; class RotaryEncoder { private: byte _pin_clk; byte _pin_dt; byte _pin_sw; volatile byte _num_results; volatile int _result; volatile int * _results; byte _index_result; RotaryEncoderLimits * _limits; boolean _a; boolean _b; public: RotaryEncoder(byte pin_clk, byte pin_dt, byte pin_sw = ROTARY_NO_BUTTON, byte num_results=1, RotaryEncoderLimits * limits=0){ //parametro do botao opcional _pin_clk = pin_clk; _pin_dt = pin_dt; _pin_sw = pin_sw; pinMode(_pin_clk, INPUT); pinMode(_pin_dt, INPUT); if (_pin_sw != ROTARY_NO_BUTTON){ pinMode(_pin_sw, INPUT); digitalWrite(_pin_sw, HIGH); } if (num_results == 0) { num_results = 1; } _num_results = num_results; _results = new int[_num_results]; for (int i; i<_num_results; i++){ _results[i] = (limits) ? limits[i].min : 0; } _index_result = 0; _limits = limits; _a = false; _b = false; } byte getIndex() { return _index_result; } void next() { _index_result++; if (_index_result >= _num_results) { _index_result = 0; } } void update_a() { _result = 0; delay (1); if( digitalRead(_pin_clk) != _a ) { _a = !_a; if ( _a && !_b ) { _result = -1; } } if (_results[_index_result]+_result >= _limits[_index_result].min && _results[_index_result]+_result <= _limits[_index_result].max ) { _results[_index_result] += _result; } } void update_b() { _result = 0; delay (1); if( digitalRead(_pin_dt) != _b ) { _b = !_b; if ( _b && !_a ) { _result = +1; } } if (_results[_index_result]+_result >= _limits[_index_result].min && _results[_index_result]+_result <= _limits[_index_result].max ) { _results[_index_result] += _result; } } int read(){ return _result; } //retorn -1, 0 ou 1. int getValue() {return _results[_index_result]; } //retorna o valor da variável corrente void setValue(int value){ _results[_index_result] = value; } //caso a variável inicializa em determinado valor diferente de zero, utilizar esse método. int buttonRead(){ return (_pin_sw == ROTARY_NO_BUTTON) ? LOW : digitalRead(_pin_sw); } }; /********************************************************************************** ************************************FIM CLASSE ROTARY ENCODER********************** **********************************************************************************/ RotaryEncoderLimits lim[] = { {0,10}, {-5,5}, {-32768, 32767} }; //limites máximos e mínimos que as variaveis podem atingir RotaryEncoder re(2, 3, 8, 3, lim); //pino clk, pino dt, pino sw, variaveis //executados pela interrupcões void interrupt_re_a() { re.update_a(); print(); } void interrupt_re_b() { re.update_b(); print(); } void print(){ if (re.read() != 0 ) { //se rotacionou --> -1, 0 ou 1 Serial.print("index: "); Serial.print(re.getIndex()); Serial.print(" = "); Serial.print(re.getValue()); Serial.print(" - "); Serial.println(re.read() > 0 ? "horario" : "anti-horario"); } } void setup() { Serial.begin(9600); attachInterrupt(INT0, interrupt_re_a, CHANGE); attachInterrupt(INT1, interrupt_re_b, CHANGE); } void loop() { static byte b = HIGH; //pra ler apenas uma vez o botao ao pressionar if( re.buttonRead() == LOW && b != re.buttonRead() ) { re.next(); //passa para a próxima variável (index) Serial.print("index: "); Serial.print(re.getIndex()); Serial.print(" = "); Serial.print(re.getValue()); Serial.println(" - Click"); b = re.buttonRead(); delay(200); //debounce meia boca } else { b = re.buttonRead(); } }
Conclusão
Espero que as rotinas apresentadas aqui sejam úteis em seus projetos, pois como podemos ver há diversas situações a serem consideradas, por isso procurei abordar algumas delas, pois ao procurarmos soluções na internet sobre o assunto, há bastante coisas, e muitas delas não funcionam de acordo com o que precisamos e os próprios exemplos encontrados não são muito claros em seu funcionamento.
No início do artigo eu citei a necessidade que algumas aplicações têm de serem gravados os valores atuais do encoder para não se perderem ao serem reiniciadas (por exemplo o volume do som do carro). Porém acabei não implementado essa funcionalidade nos exemplos apresentados, pois deixaria o código um pouco mais complexo. Mas não é muito difícil, a partir dos exemplos, implementar tais funções. A solução mais recomendada nesses casos é utilizar a eeprom.
Atualização - 21/05/2016
Fiz um exemplo de como utilizar o encoder em um display lcd. Vou deixar o código e vídeo abaixo. Caso eu venha a fazer outros exemplos, irei adicionando aqui nesse mesmo artigo.
Para mais detalhes sobre como gerar barras de progresso no display LCD veja esse artigo: http://fabianoallex.blogspot.com.br/2015/10/arduino-lcd-progress-bar-barra-de.html
Código do exemplo do vídeo:
/* Fabiano A. Arndt - 2015 www.youtube.com/user/fabianoallex www.facebook.com/dicasarduino fabianoallex@gmail.com */ #include <LiquidCrystal.h> #include <SPI.h> /************************************************************************************************************* *******************************CLASSE LCD PROGRESS BAR******************************************************** **************************************************************************************************************/ byte c1[8] = {B10000, B10000, B10000, B10000, B10000, B10000, B10000, B10000}; byte c2[8] = {B11000, B11000, B11000, B11000, B11000, B11000, B11000, B11000}; byte c3[8] = {B11100, B11100, B11100, B11100, B11100, B11100, B11100, B11100}; byte c4[8] = {B11110, B11110, B11110, B11110, B11110, B11110, B11110, B11110}; byte c5[8] = {B11111, B11111, B11111, B11111, B11111, B11111, B11111, B11111}; class LCDProgressBar { private: LiquidCrystal * _lcd; int _row; int _col; int _len; int _perc; /*0..100*/ public: void createChars() { _lcd->createChar(0, c1); _lcd->createChar(1, c2); _lcd->createChar(2, c3); _lcd->createChar(3, c4); _lcd->createChar(4, c5); } LCDProgressBar(LiquidCrystal * lcd, int row, int col, int len) { _lcd = lcd; _row = row; _col = col; _len = len; } void setPerc(int perc){ _perc = perc; if (perc > 100) { _perc = 100; } if (perc < 000) { _perc = 000; } _lcd->setCursor(_col, _row); for (int i=0; i<(_len);i++) { _lcd->print(" "); } _lcd->setCursor(_col, _row); int bars = 5 * _len * _perc / 100; int div = bars / 5; //divisao int resto = bars % 5; //resto for (int i=0; i<div; i++) { _lcd->write((byte)4); } //pinta todo o quadro if (resto > 0 ) { _lcd->write((byte)(resto-1)); } //pinta o quadro com a quantidade de barras proporcional } }; /************************************************************************************************************* *******************************FIM CLASSE LCD PROGRESS BAR**************************************************** **************************************************************************************************************/ /************************************************************************************************************* ************************************CLASSE ROTARY ENCODER***************************************************** *************************************************************************************************************/ #define ROTARY_NO_BUTTON 255 struct RotaryEncoderLimits{ int min; int max; }; class RotaryEncoder { private: byte _pin_clk; byte _pin_dt; byte _pin_sw; volatile byte _num_results; volatile int _result; volatile int * _results; byte _index_result; RotaryEncoderLimits * _limits; boolean _a; boolean _b; public: RotaryEncoder(byte pin_clk, byte pin_dt, byte pin_sw = ROTARY_NO_BUTTON, byte num_results=1, RotaryEncoderLimits * limits=0){ //parametro do botao opcional _pin_clk = pin_clk; _pin_dt = pin_dt; _pin_sw = pin_sw; pinMode(_pin_clk, INPUT); pinMode(_pin_dt, INPUT); if (_pin_sw != ROTARY_NO_BUTTON){ pinMode(_pin_sw, INPUT); digitalWrite(_pin_sw, HIGH); } if (num_results == 0) { num_results = 1; } _num_results = num_results; _results = new int[_num_results]; for (int i; i<_num_results; i++){ _results[i] = (limits) ? limits[i].min : 0; } _index_result = 0; _limits = limits; _a = false; _b = false; } byte getIndex() { return _index_result; } void next() { _index_result++; if (_index_result >= _num_results) { _index_result = 0; } } void update_a() { _result = 0; delay (1); if( digitalRead(_pin_clk) != _a ) { _a = !_a; if ( _a && !_b ) { _result = -1; } } if (_results[_index_result]+_result >= _limits[_index_result].min && _results[_index_result]+_result <= _limits[_index_result].max ) { _results[_index_result] += _result; } } void update_b() { _result = 0; delay (1); if( digitalRead(_pin_dt) != _b ) { _b = !_b; if ( _b && !_a ) { _result = +1; } } if (_results[_index_result]+_result >= _limits[_index_result].min && _results[_index_result]+_result <= _limits[_index_result].max ) { _results[_index_result] += _result; } } int read(){ return _result; } //retorn -1, 0 ou 1. int getValue(int index=-1) { //retorna o valor da variável corrente ou a passada como parametro if (index < 0 ){ return _results[_index_result]; } return _results[index]; } void setValue(int value){ _results[_index_result] = value; } //caso a variável inicializa em determinado valor diferente de zero, utilizar esse método. int buttonRead(){ return (_pin_sw == ROTARY_NO_BUTTON) ? LOW : digitalRead(_pin_sw); } }; /************************************************************************************************************* ************************************FIM CLASSE ROTARY ENCODER************************************************* *************************************************************************************************************/ LiquidCrystal lcd(12, 11, 10, 9, 8, 7); LCDProgressBar lcdBar1(&lcd, 0, 10, 6); //inclui uma barra no lcd, primeira linha, coluna 10. tamanho 6 LCDProgressBar lcdBar2(&lcd, 1, 10, 6); //inclui outra barra no lcd, segunda linha, coluna 10. tamanho 6 RotaryEncoderLimits lim[] = { {0,20}, {0,50} }; //limites máximos e mínimos que as variaveis podem atingir RotaryEncoder re(2, 3, 4, 2, lim); //pino clk, pino dt, pino sw, variaveis void interrupt_re_a() { re.update_a(); } void interrupt_re_b() { re.update_b(); } void setup() { attachInterrupt(INT0, interrupt_re_a, CHANGE); attachInterrupt(INT1, interrupt_re_b, CHANGE); Serial.begin(9600); lcdBar1.createChars(); pinMode(5, OUTPUT); analogWrite(5, 100); //utilizado para aumentar o contraste lcd.begin(16, 2); } void loop() { lcd.setCursor(0, 0); int value = re.getValue(0); int perc = value/20.0 * 100; if (value < 10) {lcd.print("00");} else { if (value < 100) {lcd.print("0");} } lcd.print(value); lcd.print("/"); lcd.print(20); char c = ((re.getIndex() == 0)) ? '>' : ' '; lcd.setCursor(8, 0); lcd.print(c); lcdBar1.setPerc(perc); //atualização da primeira barra de progresso lcd.setCursor(0, 1); value = re.getValue(1); perc = value/50.0 * 100; if (value < 10) {lcd.print("00");} else { if (value < 100) {lcd.print("0");} } lcd.print(value); lcd.print("/"); lcd.print(50); c = ((re.getIndex() == 1)) ? '>' : ' '; lcd.setCursor(8, 1); lcd.print(c); lcdBar2.setPerc(perc); //atualização da segunda barra de progresso //controla o click do botao do enconder static byte b = HIGH; //pra ler apenas uma vez o botao ao pressionar if( re.buttonRead() == LOW && b != re.buttonRead() ) { re.next(); //passa para a próxima variável (index) delay(200); //debounce meia boca } b = re.buttonRead(); delay(100); }
Atualização 22/05/2016 - Exemplo por timer
Nas duas primeiras versões que mostrei no início do artigo, abordei exemplos que não se baseiam em interrupções, mas na atualização apenas dentro do loop, pois muitas vezes as interrupções disponíveis já podem estar em uso por outros dispositivos. Porém, como explicado inicialmente, o nosso loop precisa ser executado numa frequência alta para que qualquer movimento no encoder seja detectado. Qualquer processamento feito dentro do loop que demore um pouco mais do que o normal ou o uso de delays podem causar erros na leitura do encoder. Pensando nesses casos, onde não temos nem pinos de interrupções externas disponíveis e nem a garantia de execução numa frequencia alta no loop, criei uma outra solução, baseada em timers. Ou seja, foi criada uma interrupção por timer que constantemente verifica se houve leituras no encoder. Quanto maior a frequência de execução do timer, mais seguro será a execução da rotina, porém, mais processamento é exigido. Os valores mais apropriados para a frequência de execução do timer, foram entre 100 e 500 Hz, ou seja, executado entre 100 e 500 vezes por segundo. Valores mais próximos de 100 só tiveram leituras erradas quando o encoder foi girado um pouco mais rápido que o normal. Se isso ocorrer em sua aplicação o mais indicado é utilizar um valor mais próximo de 500Hz.
Código:
/********************************************************************************** ************************************CLASSE ROTARY ENCODER************************** **********************************************************************************/ #define ROTARY_NO_BUTTON 255 struct RotaryEncoderLimits{ int min; int max; }; class RotaryEncoder { private: byte _pin_clk; byte _pin_dt; byte _pin_sw; byte _num_results; int _result; int * _results; byte _index_result; RotaryEncoderLimits * _limits; public: RotaryEncoder(byte pin_clk, byte pin_dt, byte pin_sw = ROTARY_NO_BUTTON, byte num_results=1, RotaryEncoderLimits * limits=0){ //parametro do botao opcional _pin_clk = pin_clk; _pin_dt = pin_dt; _pin_sw = pin_sw; pinMode(_pin_clk, INPUT); pinMode(_pin_dt, INPUT); if (_pin_sw != ROTARY_NO_BUTTON){ pinMode(_pin_sw, INPUT); digitalWrite(_pin_sw, HIGH); } if (num_results == 0) { num_results = 1; } _num_results = num_results; _results = new int[_num_results]; _index_result = 0; _limits = limits; for (int i; i<_num_results; i++){ _results[i] = (limits) ? limits[i].min : 0; } } byte getIndex() { return _index_result; } void next() { _index_result++; if (_index_result >= _num_results) { _index_result = 0; } } void update() { static int oldA = HIGH; static int oldB = HIGH; static unsigned long lmillis = 0; _result = 0; if (millis() - 1 > lmillis){ //previne leituras falsas int newA = digitalRead(_pin_clk); int newB = digitalRead(_pin_dt); if ( (newA != oldA || newB != oldB) && (oldA == HIGH && newA == LOW) ) { _result = (oldB * 2 - 1); } oldA = newA; oldB = newB; lmillis = millis(); } if (_results[_index_result]+_result >= _limits[_index_result].min && _results[_index_result]+_result <= _limits[_index_result].max ) { _results[_index_result] += _result; } } int read(){ return _result; } //retorn -1, 0 ou 1. int getValue() {return _results[_index_result]; } //retorna o valor da variável void setValue(int value){ _results[_index_result] = value; } //caso a variável inicializa em determinado valor diferente de zero, utilizar esse método. int buttonRead(){ return (_pin_sw == ROTARY_NO_BUTTON) ? LOW : digitalRead(_pin_sw); } }; /********************************************************************************** ************************************FIM CLASSE ROTARY ENCODER********************** **********************************************************************************/ #define val_freq 500 //frequencia em hz para a execucao do timer1. quanto mais rápido for girado o encoder, maior deve ser essa frequencia. o resultado mais adequando foi 500x por segundo #define freq(x) 65536 - (16000000 / 1024 / x) //calcula a frequencia a ser utilizada pelo timer1 na nossa rotina abaixo. x indica o valor da frequencia desejada RotaryEncoderLimits lim[] = { {0,10}, {-5,5}, {-32768, 32767} }; RotaryEncoder re(3, 2, 4, 3, lim); //pinos clk, dt, sw, 3 valores diferentes serão controlados byte lastButton = HIGH; //interrupção do TIMER1 ISR(TIMER1_OVF_vect) { //atençao. essa rotina irá repetir várias vezes por segundo, portanto não adicione nada que seja muito lento aqui. TCNT1 = freq(val_freq); int pressionou = false; re.update(); //primeiro fazemos a chamada para o update para que seja feita a leitura do rotary encoder if( re.buttonRead() == LOW && lastButton != re.buttonRead() ) { //a cada clique, passamos a gerenciar a próxima variável re.next(); pressionou = true; lastButton = re.buttonRead(); delay(300); //debounce meia boca } else { lastButton = re.buttonRead(); } if (re.read() != 0 || pressionou) { //se rotacionou ou pressionou o botão --> -1, 0 ou 1 Serial.print("index: "); Serial.print(re.getIndex()); Serial.print(" = "); Serial.print(re.getValue()); Serial.print(" - "); Serial.println(re.read() > 0 ? "horario" : "anti-horario"); } } void setup() { Serial.begin(9600); // Configuração do timer1 TCCR1A = 0; //confira timer para operação normal pinos OC1A e OC1B desconectados TCCR1B = 0; //limpa registrador TCCR1B |= (1<<CS10)|(1 << CS12); // configura prescaler para 1024: CS12 = 1 e CS10 = 1 TCNT1 = freq(val_freq); // inicia o timer na frequencia ajustada na variavel val_freq. ajustar para frequencia mais indicada TIMSK1 |= (1 << TOIE1); // habilita a interrupção do TIMER1 } void loop() { //pra simular um loop lento, adicionei um delay de 2 segundos Serial.println(" - - - - - - - - teste - - - - - - - - "); delay(2000); }
Atualização 24/05/2016 - Exemplo utilizando Pin Change Interrupts - PCI
O exemplo mais preciso mostrado nesse artigo foi aquele utilizando interrupções externas, porém como vimos elas são bem limitadas, pois no caso do Arduino Uno temos apenas duas, e utiliza-las significa que se precisarmos de interrupções para outros dispositivos, poderemos vir a ter problemas. Porém mesmo que não possamos utilizar nenhuma das interrupções disponíveis, ainda assim temos outro recurso que o Arduino nos oferece, que é utilizar interrupções PCI - Pin Change Interrupts.
Na verdade essas interrupções estão associadas não a um pino específico, mas sim a um grupo de pinos, que seriam todos os pinos de uma determinada porta do Arduino. No Arduino Uno, por exemplo, temos 3 portas que são: PortB, PortC e PortD. Quando qualquer um dos pinos de uma determinada porta muda de estado, a interrupção referente a porta ao qual o pino pertence é disparada. Não vou entrar em detalhes do funcionamento desse tipo de interrupção aqui, até porque não é o foco, então sugiro uma leitura a respeito para entender melhor essa parte da programação do próximo exemplo.
Nesse próximo exemplo, utilizamos os pinos A0 e A1 como entradas dos pinos Clk e Dt do Encoder, como pode ser visto no código abaixo. Além disso utilizamos um display LCD para mostrar o resultado das leituras do encoder. Em relação ao outro exemplo com o display LCD, foi alterada a progress bar.
Vídeo:
Código:
/* Fabiano A. Arndt - 2016 www.youtube.com/user/fabianoallex www.facebook.com/dicasarduino fabianoallex@gmail.com */ #include <LiquidCrystal.h> /************************************************************************************************************* ************************************CLASSE ROTARY ENCODER***************************************************** *************************************************************************************************************/ #define ROTARY_NO_BUTTON 255 struct RotaryEncoderLimits{ int min; int max; }; class RotaryEncoder { private: byte _pin_clk; byte _pin_dt; byte _pin_sw; volatile byte _num_results; volatile int _result; volatile int * _results; byte _index_result; RotaryEncoderLimits * _limits; boolean _a; boolean _b; public: RotaryEncoder(byte pin_clk, byte pin_dt, byte pin_sw = ROTARY_NO_BUTTON, byte num_results=1, RotaryEncoderLimits * limits=0){ //parametro do botao opcional _pin_clk = pin_clk; _pin_dt = pin_dt; _pin_sw = pin_sw; pinMode(_pin_clk, INPUT); pinMode(_pin_dt, INPUT); if (_pin_sw != ROTARY_NO_BUTTON){ pinMode(_pin_sw, INPUT); digitalWrite(_pin_sw, HIGH); } if (num_results == 0) { num_results = 1; } _num_results = num_results; _results = new int[_num_results]; for (int i; i<_num_results; i++){ _results[i] = (limits) ? limits[i].min : 0; } _index_result = 0; _limits = limits; _a = false; _b = false; } byte getIndex() { return _index_result; } void next() { _index_result++; if (_index_result >= _num_results) { _index_result = 0; } } void update_a() { _result = 0; delay (1); if( digitalRead(_pin_clk) != _a ) { _a = !_a; if ( _a && !_b ) { _result = -1; } } if (_results[_index_result]+_result >= _limits[_index_result].min && _results[_index_result]+_result <= _limits[_index_result].max ) { _results[_index_result] += _result; } } void update_b() { _result = 0; delay (1); if( digitalRead(_pin_dt) != _b ) { _b = !_b; if ( _b && !_a ) { _result = +1; } } if (_results[_index_result]+_result >= _limits[_index_result].min && _results[_index_result]+_result <= _limits[_index_result].max ) { _results[_index_result] += _result; } } int read(){ return _result; } //retorn -1, 0 ou 1. int getValue(int index=-1) { //retorna o valor da variável corrente ou a passada como parametro if (index < 0 ){ return _results[_index_result]; } return _results[index]; } void setValue(int value){ _results[_index_result] = value; } //caso a variável inicializa em determinado valor diferente de zero, utilizar esse método. int buttonRead(){ return (_pin_sw == ROTARY_NO_BUTTON) ? LOW : digitalRead(_pin_sw); } }; /************************************************************************************************************* ************************************FIM CLASSE ROTARY ENCODER************************************************* *************************************************************************************************************/ /************************************************************************************************************* *******************************CLASSE LCD PROGRESS BAR******************************************************** **************************************************************************************************************/ /* 0 1 2 3 4 5 6 7 ***** ***** ***** ***** ***** ***** ***** ***** * ** *** **** ***** * *** *** * ** *** **** ***** * *** *** * ** *** **** ***** * *** *** * ** *** **** ***** * *** *** * ** *** **** ***** * *** *** * ** *** **** ***** * *** *** ***** ***** ***** ***** ***** ***** ***** ***** */ byte c0[8] = {B11111, B00000, B00000, B00000, B00000, B00000, B00000, B11111}; byte c1[8] = {B11111, B10000, B10000, B10000, B10000, B10000, B10000, B11111}; byte c2[8] = {B11111, B11000, B11000, B11000, B11000, B11000, B11000, B11111}; byte c3[8] = {B11111, B11100, B11100, B11100, B11100, B11100, B11100, B11111}; byte c4[8] = {B11111, B11110, B11110, B11110, B11110, B11110, B11110, B11111}; byte c5[8] = {B11111, B11111, B11111, B11111, B11111, B11111, B11111, B11111}; byte c6[8] = {B11111, B10111, B10111, B10111, B10111, B10111, B10111, B11111}; byte c7[8] = {B11111, B00111, B00111, B00111, B00111, B00111, B00111, B11111}; class LCDProgressBar { private: LiquidCrystal * _lcd; //ponteiro para um objeto lcd int _row; int _col; int _len; int _perc; /*0..100*/ public: void createChars() { _lcd->createChar(0, c0); _lcd->createChar(1, c1); _lcd->createChar(2, c2); _lcd->createChar(3, c3); _lcd->createChar(4, c4); _lcd->createChar(5, c5); _lcd->createChar(6, c6); _lcd->createChar(7, c7); } LCDProgressBar(LiquidCrystal * lcd, int row, int col, int len) { _lcd = lcd; _row = row; _col = col; _len = len; } void setPerc(int perc) { _perc = perc; if (perc > 100) { _perc = 100; } if (perc < 000) { _perc = 000; } _lcd->setCursor(_col, _row); if (_len == 1){ _lcd->write((byte)0); int bars = 5 * _perc / 100; int div = bars / 5; //divisao int resto = bars % 5; //resto _lcd->setCursor(_col, _row); if (div > 0) { _lcd->write((byte)5); } //pinta todo o quadro if (resto > 0 ) { _lcd->write((byte)(resto)); } //pinta o quadro com a quantidade de barras proporcional } else { int bars = (5*(_len-2) + 4) * _perc / 100; // -2 --> desconsidera os blocos de inicio e fim ; 4 --> considera as duas barras de cada um dos blocos de inicio e fim //preenche com caracteres vazio for (int i=0; i<(_len);i++) { _lcd->write((byte)0); } //reposiciona no bloco inicial _lcd->setCursor(_col, _row); //preenche o primeiro bloco da barra if (bars == 0) { _lcd->write((byte)3); } if (bars == 1) { _lcd->write((byte)4); } if (bars >= 2) { _lcd->write((byte)5); } int div = bars / 5; //divisao int resto = bars % 5; //resto for (int i=0; i<div; i++) { _lcd->write((byte)5); } //pinta todo o quadro if (resto > 0) { _lcd->write((byte)(resto-1)); } //pinta o quadro com a quantidade de barras proporcional //reposiciona no bloco final _lcd->setCursor(_col+_len-1, _row); //preenche ultimo bloco da barra if ((5*(_len-2) + 4) - bars == 0) { _lcd->write((byte)5); } if ((5*(_len-2) + 4) - bars == 1) { _lcd->write((byte)6); } if ((5*(_len-2) + 4) - bars >= 2) { _lcd->write((byte)7); } } } }; /************************************************************************************************************* *******************************FIM CLASSE LCD PROGRESS BAR**************************************************** **************************************************************************************************************/ LiquidCrystal lcd(12, 11, 10, 9, 8, 7); LCDProgressBar lcdBar1(&lcd, 0, 9, 6); //inclui uma barra no lcd, primeira linha, coluna 9. tamanho 6 LCDProgressBar lcdBar2(&lcd, 1, 11, 4); //inclui outra barra no lcd, segunda linha, coluna 11. tamanho 4 RotaryEncoderLimits lim[] = { {0,30}, {0,13} }; //limites máximos e mínimos que as variaveis podem atingir RotaryEncoder re(A0, A1, 4, 2, lim); //pino clk, pino dt, pino sw, variaveis //interrupções dos pinos A0 e A1 via Pin Change Interrupt ISR(PCINT1_vect) { volatile static byte lastVal_a0 = LOW; volatile static byte lastVal_a1 = LOW; byte val_a0 = digitalRead(A0); byte val_a1 = digitalRead(A1); if (lastVal_a0 != val_a0){ re.update_a(); lastVal_a0 = val_a0; } if (lastVal_a1 != val_a1){ re.update_b(); lastVal_a1 = val_a1; } } void indicador_rotary(){ char c = ((re.getIndex() == 0)) ? '>' : ' '; lcd.setCursor(7, 0); lcd.print(c); c = ((re.getIndex() == 1)) ? '>' : ' '; lcd.setCursor(7, 1); lcd.print(c); } void setup() { //-----PCI - Pin Change Interrupt ---- pinMode(A0,INPUT); // set Pin as Input (default) digitalWrite(A0,HIGH); // enable pullup resistor pinMode(A1,INPUT); // set Pin as Input (default) digitalWrite(A1,HIGH); // enable pullup resistor cli(); PCICR |= 0b00000010; // habilita a porta C - Pin Change Interrupts PCMSK1 |= 0b00000011; // habilita interrupção da porta c nos pinos: PCINT8 (A0) e PCINT9(A1) sei(); //----fim PCI---- Serial.begin(9600); lcdBar1.createChars(); lcd.begin(16, 2); indicador_rotary(); } void loop() { static int value1 = -1; static int value2 = -1; if (value1 != re.getValue(0)) { Serial.println("."); lcd.setCursor(0, 0); value1 = re.getValue(0); int perc = value1/30.0 * 100; if (value1 < 10) {lcd.print("00");} else { if (value1 < 100) {lcd.print("0");} } lcd.print(value1); lcd.print("/"); lcd.print(30); lcdBar1.setPerc(perc); //atualização da primeira barra de progresso } if (value2 != re.getValue(1)) { Serial.println("*"); lcd.setCursor(0, 1); value2 = re.getValue(1); int perc = value2/13.0 * 100; if (value2 < 10) {lcd.print("00");} else { if (value2 < 100) {lcd.print("0");} } lcd.print(value2); lcd.print("/"); lcd.print(13); lcdBar2.setPerc(perc); //atualização da segunda barra de progresso } //controla o click do botao do enconder static byte b = HIGH; //pra ler apenas uma vez o botao ao pressionar if( re.buttonRead() == LOW && b != re.buttonRead() ) { re.next(); //passa para a próxima variável (index) indicador_rotary(); delay(200); //debounce meia boca } b = re.buttonRead(); delay(100); }