segunda-feira, 28 de setembro de 2015

Usando um Port Expander com Arduino

O MCP 23017 é um port expander, ele permite que você tenha mais entradas e saída que normalmente tem na Arduino. Por outro lado permite que você estenda a comunicação com apenas 4 fios (dois do I2C, um de alimentação e outro do terra).

As dicas aqui contidas resolveram problemas que eu encontrei, mas podem não ser a melhor forma de resolve-los, não exite em contribuir nos comentários.

Os exemplos de código consideram o uso da biblioteca da Adafruit para o MCP 23017.

É importante que alguns detalhes sejam observados:

1 - Coloque um capacitor de 1µF em paralelo com o botão (caso você use). Ative, no código, o pull-up interno do MCP23017 que é de 100KΩ, então teremos um resistor em série com o capacitor, quando o botão está aberto. E o tempo de suavização (anti-debounce) será de 0,1s (τ = RC => τ = 100K*1µ => τ = 0,1s).


O uso deste capacitor (um em paralelo para cada botão) além do efeito desejado que é a suavização durante o acionamento tem um efeito indesejado: quando o circuito é energizado o capacitor está descarregado e irá parecer para o MCP 23017 que ocorreu um acionamento do botão. Para eliminar este efeito indesejado é necessário incluir um delay de 60ms (testado empiricamente como um tempo adequado  20ms não deu certo 30ms sim, então escolhi 60ms para ter folga ) e uma leitura para limpar o buffer, no código da Arduino no setup depois da inicialização do MCP 23017.




#define button1 13
#define led 0

void setup() { 

//inicializa o MCP23017     
mcp.begin(); // use default address 0
mcp.setupInterrupts(true, false, LOW); // mirror habilitado - cada interrupcao em INTA e espelhada em INTB
//saídas (só para mostrar com se faz)    
mcp.pinMode(led, OUTPUT);//pino 0  mcp_PinA0 configurado com saída    
//entradas    
mcp.pinMode(button1, INPUT); // pino 13 mcp_PinB5 button1 configurado com entrada
mcp.pullUp(button1, HIGH); // ativa pullup interno de 100K
mcp.setupInterruptPin(button1, FALLING); // gera interrupcao quando de sai de nivel alto para nivel baixo

//tratamento para o uso dos capacitores em paralelo com os botões    
delay(60);//pequena espera antes da leitura inicial 
mcp.getLastInterruptPinValue();//leitura inicial, para limpar o buffer






2 - Cuidados no código com a interrupção

A) Quando se trabalha com Interrupções, uma boa prática é realizar o mínimo possível dentro da função  (interrupt service routine (ISR)) chamada pela interrupção, preferencialmente mude uma variável booleana e trate no loop. IMPORTANTE  está variável global e volatile, por exemplo:



volatile boolean interruptHappen = false;


Normalmente no loop você terá uma chamada que trata a interrupção, algo como:


if(interruptHappen)
    handleInterrupt();


mas ainda não implemente desta forma, veja o item C).


B) Para evitar que ocorra uma interrupção enquanto a função handleInterrupt() deve desligar a interrupção no seu inicio e ligar no final:

void handleInterrupt(){
  detachInterrupt(arduinoInterrupt);
   ....
  attachInterrupt(arduinoInterrupt,intCallBack,FALLING); }


C) É recomendável que se limite o intervalo mínimo entre uma interrupção e a próxima (depois de experimentar uma instabilidade, que travava o funcionamento do MCP 23017, eu resolvi com este intervalo mínimo, equivale a um anti debounce de software para a interrupção.


//variávei globais
volatile unsigned long button_time = 0;  //anti debounce de software na interrupção
volatile unsigned long last_button_time = 0; //anti debounce de software na interrupção
volatile int buttonInterruptInterval = 100; //intervalo mínimo entre uma interrupção e outra, evita problema de debounce
volatile uint8_t mcpPin;

...
dentro do loop:

if (interrupted)
     {
     button_time = millis(); //Anti debounce de software para a interrupção
     mcpPin=mcp.getLastInterruptPin(); //lê qual pino foi acionado
     if (((button_time - last_button_time) > buttonInterruptInterval)&&(mcpPin==button1))//somente trata se for um botão válido e em intervalos superiores a 100ms
       {
       handleInterrupt();
       last_button_time = button_time;
       }
     else mcp.getLastInterruptPinValue();//senão limpa o bus (ignora o acionamento)
     }

D) A função hanleInterrupt() deve ser a mais direta possível, não adicione código que poderia estar em outra função, veja um exemplo:



void handleInterrupt()
   {
   detachInterrupt(digitalPinToInterrupt(PIN_INTERRUPT));//desativa a interrupção para evitar multiplos acionamentos
     
   if(mcpPin==button1) {código de botão 1();} // false = desligado
     
   while( ! (mcp.digitalRead(button1) );//espera o botão ser solto
     
   updateLedsProfile();
   cleanInterrupts();
   attachInterrupt(digitalPinToInterrupt(PIN_INTERRUPT), intCallBack, FALLING);//reativa a interrupção
   }



E) Estará sendo usada comunicação I2C, que é uma comunicação serial, logo recomendo uma implementação como a usada acima, no passo C uma variável global volatil mcpPin é carregada usando mcp.getLastInterruptPin() esta mesma variável é usada dentro do handleInterrupt() para definir a ação, ao invés de carregar novamente. A ideia é: seja simples recebeu uma interrupção identifique o botão e trate.


Informações sobre debounce podem ser encontradas em:
http://www.gammon.com.au/forum/?id=10945

Informações sobre interrupções podem ser encontradas em: https://www.arduino.cc/en/Reference/AttachInterrupt

Atenção: Quando se trabalha com delay() no loop pode ocorrer instabilidade na chamada da função através de interrupção, mesmo com acionamento LOW (por exemplo). Esta instabilidade aumenta caso a interrupção seja acionada por transição como FALLING.