Interrupções

Polling vs Interrupts

Muitas vezes o nosso programa tem que reagir à mudança dum sinal externo, por exemplo a mudança do nível lógico dum sinal gerado por um botão. Na página sobre Portas I/O este problema foi resolvido fazendo-se sucessivas leituras do pino de entrada até que a mudança do valor lógico fosse detetada. Esta abordagem, designada por polling tem contudo o senão de nos obrigar a sondar ciclicamente o pino com a agravante do instante em que ocorre a mudança ser determinado de forma pouco precisa pois depende do período dessa sondagem.

Uma abordagem alternativa vai ser descrita nesta página e recorre a interrupções. Neste caso começa-se por definir uma rotina (normalmente designada de ISR – Interrupt Service Rotine) que irá ser chamada assim que ocorrer a dita mudança de valor lógico no pino igualmente a definir.

Nem todos os pinos permitem esta abordagem. No caso do PIC32 existem 5 pinos com esta funcionalidade, sendo designados de INT0, INT1, INT2, INT3 e INT4 (respetivamente nos pinos 38, 2, 7, 8 e 35 da placa ChipKIT UNO 32:

chipkituno32_pinout

Projeto intcount

Vamos agora apresentar o projeto intcount que incrementa um contador sempre que se prime um botão ligado ao pino 38 (onde se encontra a interrupção externa INT0).

Antes porém, vamos confirmar as localizações mencinadas no final da secção anterior recorrendo à função digitalPinToInterrupt(), em que passando o número do pino da placa, recebemos o número x do pino INTx respetivo:

#include "Arduino.h"

void setup() {
  Serial.begin(115200);

  int n, x;
  for(n=0; n<=41; n++) {
    x = digitalPinToInterrupt(n);
    if (x >= 0)
      Serial.printf("Pino %d = INT%d\n", n, x);
  }
}

void loop() {
  delay(1000);
}

Agora sim, vamos incrementar o contador (variável cnt) sempre que se prime o botão, que desce o sinal no pino 38 (INT0) ao nível zero quando se prime o botão. Para que a ISR (a rotina count neste caso) seja chamada nessas condições bastará usar a função attachInterrupt(). O último parâmetro desta função pode tomar os valores FALLING, RISING ou CHANGE consoante se pretenda uma interrupção na descida, subida ou ambas, do sinal em causa.

#include "Arduino.h"

// Pin 38: INT0
#define INTPIN 38

void count();
int cnt = 0;
bool bPrint = true;

void setup() {
  Serial.begin(115200);
  Serial.println("Project intcount");
  pinMode(INTPIN, INPUT);
  attachInterrupt(digitalPinToInterrupt(INTPIN), count, FALLING);
}

void loop() {
  if (bPrint) {
    bPrint = false;
    Serial.println(cnt);
  }
}

// INT0 ISR (make it simple!)
void count() {
  cnt++;
  bPrint = true;
}

De notar que a ISR deverá ser o mais rápida possível (de modo a interromper o programa principal o menor tempo possível). De outro modo corremos o risco de ela ser interrompida por ela própria. Daí que neste exemplo a impressão do contador decorra não na ISR mas fora dela!

Caso se pretenda mais tarde no programa usar o pino 38 sem as interrupções bastará usar a função detachInterrupt().

Interrupções com Timers

Uma funcionalidade útil, seria ter a possibilidade de associarmos as interrupções a temporizadores (Timers). A ideia seria que no final duma dada temporização fosse despoletada uma interrupção. De notar que ao contrário das temporizações realizadas com base nas funções millis() ou micros(), o uso de Timers permite que as temporizações sejam feitas em Hardware e portanto em paralelo (i.e. de forma independente) à execução do programa.

Como o microcontrolador PIC32 é bastante diferente neste assunto em relação aos usados nas placas Arduino, justifica-se rever a página sobre o uso dos Timers no PIC32.

Na página atrás mencionada, a funcionalidade é explorada recorrendo à escrita/leitura dos SFRs (Special Function Registers) e respetivos bits. Existe contudo uma biblioteca (Timer) que permite “esconder” estes elementos por traz dum objeto.

O seguinte código mantém a funcionalidade do programa anterior fazendo também piscar o LED4 (pino 13) da placa ChipKIT UNO32 a cada segundo. Em ambas as funcionalidades (“contador de toques no botão” e “piscar do LED”) são usadas interrupções:

#include "Arduino.h"
#include <Timer.h>

Timer4 timer;
#define LED 13
void __attribute__((interrupt)) isrTimer();

// Pin 38: INT0
#define INTPIN 38

void count();
int cnt = 0;
bool bPrint = true;

void setup() {
  Serial.begin(115200);
  Serial.println("Project intcount");

  pinMode(INTPIN, INPUT);
  attachInterrupt(digitalPinToInterrupt(INTPIN), count, FALLING);

  pinMode(LED, OUTPUT);
  timer.setFrequency(10);
  timer.attachInterrupt(isrTimer);
  timer.start();
}

void loop() {
  if (bPrint) {
    bPrint = false;
    Serial.println(cnt);
  }
}

// INT0 ISR (make it simple!)
void count() {
  cnt++;
  bPrint = true;
}

void __attribute__((interrupt)) isrTimer() {
  static uint8_t state = LOW;
  static int c = 0;

  if (++c == 10) {
    digitalWrite(LED, state);
    state = !state;
    c = 0;
  }

  clearIntFlag(_TIMER_4_IRQ);
}

Alguns comentários sobre este código:

Dado que o Timer usado (Timer4) tem apenas 16 bits e mesmo com o prescaler no seu valor máximo (256) só são permitidas temporizações até (2^16)*256/80e6 ≈ 0.21s, pelo que foi feita uma temporização de 0,1s (10Hz). Este é mais um exemplo de que às vezes só conhecendo bem o microcontrolador ao nível dos seus SFRs se percebe as opções feitas no código C!

A ISR do Timer4 não é uma rotina como as outras e portanto tem que ser declarada de forma adequada: void __attribute__((interrupt)) isrTimer()

Para que as variáveis dessa ISR se mantenham entre chamadas à rotina, é necessário declará-las como estáticas: static 

O uso de interrupções visto ao longo desta página tem a grande virtude de permitir a chamada de rotinas quando determinados eventos acontecem, fazendo a interrupção do programa principal mesmo quando se encontram em funções bloqueantes como o delay(). Isso pode ser comprovado substituindo no programa anterior a função loop por esta outra:

void loop() {
  delay(3000);
  Serial.println(cnt);
}

Como se pode constatar, apesar do delay de 3s, toques no botão durante esse período continuam a ser detetados já que o contador é incrementado!

ToDo

No final da página sobre comunicação série, foram pedidas na secção ToDo, várias melhorias ao programa butledserial. Repita o ponto 4 mas agora recorrendo às funcionalidades descritas nesta página, ou seja use Timers e Interrupções.