Tutoriais

Interrupção: O que é e Como Utilizar no Arduino

Eletrogate 28 de dezembro de 2021

Introdução

Nesse post vamos dar um resumo sobre um dos recursos mais importantes dos sistemas embarcados, as interrupções, explicando seu funcionamento com 3 exemplos usando um Arduino Uno.

O que são Interrupções?

Você provavelmente já deve ter ouvido falar em interrupções. Mas, o que de fato são elas? Como o próprio nome já diz, ela interrompe o fluxo do código após receber um sinal interno ou externo, executando uma função e depois retornando ao fluxo normal do programa. Uma interrupção pode ocorrer a qualquer momento, independente da tarefa atual do código (a não ser que a tarefa atual seja outra interrupção de mesmo nível).

A função que a interrupção chama é especial: seu nome é ISR (Interrupt Service Routine), ela não pode receber nem retornar nada, deve ser o mais curta possível e, por questão de segurança, todas as variáveis globais que serão usadas pelas ISR devem ser volatile (volatile indica que a variável pode ser modificada sem que o programa principal saiba, isso diz ao compilador para não tentar fazer otimizações onde aquela variável é usada).


Interrupções Externas

Imagine a seguinte situação: você está em sua casa fazendo suas tarefas, quando alguém toca a campainha, então você para de fazer sua tarefa para atender a porta. Essa é a ideia por trás das interrupções externas, você pode usar um sensor, botão ou até mesmo outra placa, conectados a pinos especiais para tratar um acontecimento na mesma hora que ele ocorrer.

Interrupções externas no Arduino

O Arduino tem duas funções para lidar com interrupções externas, attachInterrupt() e a digitalPinToInterrupt(), e são usadas da seguinte forma:

Primeiro, se deve colocar o pino que será usado no modo INPUT – no Arduino Uno só dois pinos podem ser usados como interrupção externa, o pino 2 e 3. Cada placa tem pinos diferentes para isso, você pode checar nessa lista da Arduino.cc(caso a placa que você use não esteja nessa lista, é necessário checar o seu datasheet).

Após isso, use a função attachInterrupt(). Ela recebe três argumentos: no primeiro argumento, se passa o pino da interrupção. Para converter um pino de digital-input para um de interrupção use a função digitalPinToInterrupt() (lembrando que apenas o pino 2 e 3 podem ser usados para isso no Arduino Uno) Ex:attachInterrupt(digitalPinToInterrupt(pinX), ….).

No segundo, se passa à função que ele vai executar Ex: attachInterrupt(digitalPinToInterrupt(pinX), funcao_a_ser_executada_na_interrupção,…..).

No terceiro e ultimo argumento é passado o modo em que o sinal vai ser enviado. Existem 5 modos:

  • LOW acionar a interrupção quando o estado do pino for LOW.
  • CHANGE acionar a interrupção quando o sempre estado do pino mudar.
  • RISING acionar a interrupção quando o estado do pino for de LOW para HIGH apenas.
  • FALLING acionar a interrupção quando o estado do pino for de HIGH para LOW apenas.

Placas Due, Zero e MKR1000 suportam também:

  • HIGH acionar a interrupção quando o estado do pino for HIGH.

(Fonte: https://www.arduino.cc/reference/pt/language/functions/external-interrupts/attachinterrupt/)

Ex. de uma função attachInterrupt completa:

attachInterrupt(digitalPinToInterrupt(pinX), ISR, LOW);


Exemplo 1: Recebendo Sinais Externos

Nesse exemplo vamos piscar 2 LED’s usando o que acabamos de aprender sobre interrupções externas.

Materiais necessários para o projeto Recebendo Sinais Externos

Esquema

Código:

//pinos usados
#define LedInterrupt 4
#define Led 5
#define InterruptPin 3

//estado atual do led, o atributo volatile pois ele vai ser usado na função ISR
volatile bool estado = false;

void setup() {
  //pinos do led como saida
  pinMode(LedInterrupt, OUTPUT);
  pinMode(Led, OUTPUT);
  
  //pino da interrupção como INPUT
  pinMode(InterruptPin, INPUT);
  
  //configura interrupção por qualquer mudança no pino 3
  attachInterrupt(digitalPinToInterrupt(InterruptPin), inverte_led, CHANGE);

}

void loop() {
  //pisca o led
  digitalWrite(Led, HIGH);
  delay(1000);
  digitalWrite(Led, LOW);
  delay(1000);
}

//ISR que inverte o valor de LedInterrupt
void inverte_led(){
  if(estado){
    digitalWrite(LedInterrupt, HIGH);
    estado = false;
  }else{
    digitalWrite(LedInterrupt, LOW);
    estado = true;
  }
}

Resultado:

Como você pode ver, mesmo fora do loop o primeiro led pisca, isso ocorre pois a cada vez que o segundo led muda de estado (de HIGH para LOW e vice-versa) ele chama a ISR “inverte_led”, saindo do loop, executando a função e retornando logo em seguida.


Interrupções Internas

São interrupções geradas por periféricos do próprio microcontrolador, seguindo a mesma ideia do exemplo anterior, imagine que você está em sua casa, fazendo um bolo, após colocar o bolo no forno você define um alarme e vai fazer suas tarefas, quando o alarme tocar, você para o que estava fazendo e tira o bolo do forno. Você pode configurar os periféricos do seu microcontrolador para enviar sinais de interrupção.

Interrupções internas no Arduino

O microcontrolador do Arduino Uno (ATmega328p),  possui 26 tipos de interrupção, sendo 6 externas e 19 internas.

(Fonte: https://ww1.microchip.com/downloads/en/DeviceDoc/Atmel-7810-Automotive-Microcontrollers-ATmega328P_Datasheet.pdf, Pag 49)

Iremos abordar nesse post apenas um tipo, a Interrupção por overflow do timer (ou seja, quando o timer atingir seu valor máximo). Timers são um dos recursos mais importantes dos microcontroladores. Eles são responsáveis por gerar o PWM e te permitem fazer uma contagem síncrona independente do estado do código (curiosidade: a função millis() funciona assim). Essa contagem varia de acordo com o tamanho em bits do timer, ou seja, timers de 8 bits contam de 0 a 255, timers de 16 bits contam de 0 a 65536 e assim por diante. O Arduino Uno tem três timers: dois de 8bits e um de 16bits, cada um podendo gerar uma interrupção.

Infelizmente, apesar de algumas funções usarem interrupções internas, a IDE Arduino não disponibiliza funções que as acessem diretamente, por isso nós teremos que configura-los diretamente pelos registradores.


O que são Registradores?

Registradores são pequenas células de memória que ficam no processador. Elas são poucas e guardam informações simples, porém são extremamente rápidas. Elas auxiliam o processador guardando informações como o resultado de alguma instrução, a próxima instrução a ser executada, os periféricos e etc, disponibilizando essas informações de forma eficiente.

Como os registradores configuram um periférico? Imagine que cada bit do registrador seja um interruptor.

Em sinais elétricos, 1 representa o nível lógico HIGH (ligado) e 0 (zero) representa o nível lógico LOW (desligado). Assim como uma máquina de lavar roupa, um micro-ondas e etc, a sequência de botões define sua funcionalidade e om os registradores de periféricos não é diferente.

Apesar de mais complicado, o uso direto dos registradores torna nosso código mais leve e eficiente.


Configurando o Timer no Arduino Uno (ATmega328p)

Um timer deste microcontrolador tem 3 registradores para sua configuração: TCCRxA, TCCRxB, TIMSKx, onde “x” é o número do timer. Em nosso exemplo usaremos apenas o timer 1 em modo normal.

(Fonte: https://ww1.microchip.com/downloads/en/DeviceDoc/Atmel-7810-Automotive-Microcontrollers-ATmega328P_Datasheet.pdf, Pag: 108 – 112)

Como nosso objetivo é a interrupção por overflow do timer, não daremos foco às outras funcionalidades.

Usaremos o timer no modo Normal. Para fazer isso teremos que configurar os bits WGMx0 – WGMx2, localizados nos registradores TCCRxA e TCCRxB (onde “x” é o número do timer), seguindo o datasheet:

(Fonte: https://ww1.microchip.com/downloads/en/DeviceDoc/Atmel-7810-Automotive-Microcontrollers-ATmega328P_Datasheet.pdf, Pag 109)

Como podemos ver, o modo normal pode ser configurado colocando todos os bits em 0.

Depois disso, temos que definir o prescaler, usando os  bits CSx2 – CSx0, localizados nos primeiros bits do registrador TCCRxB (onde “x” é o número de timer), seguindo essa tabela:

(Fonte: https://ww1.microchip.com/downloads/en/DeviceDoc/Atmel-7810-Automotive-Microcontrollers-ATmega328P_Datasheet.pdf, Pag 110)

Prescaler, o fator de divisão de frequência, é usado para aumentar ou diminuir a velocidade que o timer vai de 0 ao seu máximo. O tempo que o timer demora para completar seu ciclo é definido pela fórmula:

onde T é o tempo em microssegundos,  max é o valor máximo que o timer atinge (255, para um Timer de 8 bits), Prescaler é o fator de divisão definido no último tópico e frequência é a frequência do nosso microcontrolador, que, no Arduino Uno, é de aproximadamente 16Mhz.

Ou seja, se nós definimos um prescaler de 256, o tempo que o timer vai demorar para alcançar o overflow é:

Depois habilitamos a interrupção por overflow, definindo o bit TOIEx no registrador TIMSKx (onde “x” é o número do timer).

E, por fim, habilitamos as interrupções globais chamando a função sei(). Também podemos desabilitá-las usando a função cli(), se necessário.

Agora, para criarmos uma ISR, será um pouco mais complicado. Teremos que usar o nome “ISR” para declarar a função, passando como argumento o nome da interrupção, substituindo os espaços por underline e adicionando _vect no final, exemplo:

ISR(TIMER1_OVF_vect);


Exemplo 2: Piscando um LED

Nesse exemplo vamos piscar 2 leds ao mesmo tempo usando timers. Um usando delay() e o outro timer.

Materiais necessários para o projeto Piscando um LED

  • Arduino Uno;
  • 2x LED difuso vermelho;
  • 2x Resistores 1 kohm.

Esquema:

Código:

#define led 4
#define led_timer1 5

volatile byte led1_status = LOW;

//ISR + nome da interrupção
ISR(TIMER1_OVF_vect){
    digitalWrite(led_timer1, led1_status);
    //inverte o estado atual do led
    led1_status = !led1_status;
    
}

void setup() {
  pinMode(led, OUTPUT);
  pinMode(led_timer1, OUTPUT);
 
  // o timer 1 tem 16bits, ou seja conta de 0 a 65536
  // TODOS OS BITS EM 0, Nota: não é nescessario fazer essa atribuição se todos os valores forem 0
  TCCR1A = 0b00000000; // WGM11 E WGM10 em 0
  TIMSK1 = 0b00000001; //Interrupção por overflow do timer 1
  
  sei(); //interrupções globais habilitadas

  //definição do Prescaler
  TCCR1B = 0b00000100; //Prescaler de 256, Aproximadamente 1seg ---- WGM12 em 0
  
}

void loop() {
  digitalWrite(led, HIGH);
  delay(250);
  digitalWrite(led, LOW);
  delay(250);
}

Resultado:

Como podemos observar, a cada 1 seg o segundo led pisca, sem interferir no primeiro led. Isso ocorre pois demora cerca de 1 seg para o timer 1 contar de 0 a 65536 com um prescaler de 256 e a cada vez que ele atinge o máximo ele envia um sinal de interrupção, que chama nossa ISR, e volta a contagem para 0.


Exemplo 3: Event Handler

Nesse exemplo vamos juntar tudo que aprendemos e explorar um pouco da linguagem C++ para criar uma abstração semelhante ao attachInterrupt() para nosso timer. |Então, usamos isso para mudar a velocidade que um led pisca apenas com interrupções.

Materiais necessários para o projeto Event Handler

Esquema:

Código:

#define led 4 //nosso led
#define external_pin 3 //nosso botão como interrupção externa

using ISR_callback = void(*)(); //definição de tipo para nossa callback

volatile byte estado_led = LOW;//estado atual do led
volatile byte estado_prescaler;//Prescaler atual

//Uma enumeração da tabela de prescaler vista no datasheet, uma forma facil de vizualizar no codigo
enum Prescaler{
  OFF = 0,
  Prescaler_1,
  Prescaler_8,
  Prescaler_64,
  Prescaler_256,
  Prescaler_1024
};

//nossa ISR, repare no tipo volatile, significa que ISR_function vai ser chamada na nossa ISR
//mesmo sendo um callback ainda é necessário o modificador "volatile"
volatile ISR_callback ISR_function; 
ISR(TIMER1_OVF_vect){
  ISR_function();
}

//nossa função para configurar o timer
void Event_handler(ISR_callback callback, Prescaler Feq){
  cli(); // desliga as interrupções para não haver error durante a configuração
  
  ISR_function = callback; //recebe uma função do tipo ISR
  
  estado_prescaler = Feq; //salva a configuração atual do prescaler
  
  TCCR1A = 0b00000000;//modo normal
  
  TCCR1B = Feq & 0x7;//configuração do prescaler, "& 0x7" serve para ganrantir que vamos manipular somente os bits de Prescaler 
  
  TIMSK1 = 0b00000001;//habilita a interrupção por overflow do timer 
  
  sei();//habilita interrupções globais
}

//muda o estado do led
void pisca_led(){
  digitalWrite(led, estado_led);
  estado_led = !estado_led;
}

//função para modificar o prescaler do timer1
void mod_prescaler(){
  estado_prescaler++;//muda para o proximo prescaler (8 - 64 - 256 - 1024)
  if(estado_prescaler > Prescaler_1024){
   estado_prescaler = Prescaler_8; //abaixo disso não é possivel visualizar o led piscar
  }
  TCCR1B = estado_prescaler & 0x7;//configuração do prescaler, "& 0x7" serve para ganrantir que vamos manipular somente os bits de Prescaler 
}
//-----------------Principal--------------------------------------
void setup() {
  pinMode(led, OUTPUT);
  
  //Pino da interrupção externa como input
  pinMode(external_pin, INPUT);
  
  //Nossa função que recebe uma ISR e o prescaler 
  Event_handler(pisca_led, Prescaler_256);
  
  //interrupção no pino 3 na mudança do estado LOW para HIGH
  attachInterrupt(digitalPinToInterrupt(external_pin),mod_prescaler,RISING);
}
void loop() {
  //void loop vazia
}

Esse código é apenas um exemplo simplificado, algumas regras de boas praticas talvez tenham sido quebradas.

Resultado:

Como podemos ver, mesmo sem nenhum código no loop conseguimos mudar a velocidade do led. Isso ocorre porque a cada vez que o botão é pressionado ele chama a ISR “mod_prescaler”, que altera o prescaler. Fazendo isso, o tempo que o timer demora para ir de 0 a o seu máximo muda, e cada vez que o timer atinge seu máximo ele chama a ISR “pisca_led”.


Vantagens das Interrupções

Interrupções permitem de uma forma fácil tratar eventos em tempo ágil. Mas qual é a utilidade desse controle de tempo? Imagine um carro autônomo: para não causar nenhum acidente, ele tem um sensor para detectar se há pessoas, objetos ou animais em sua frente. Usando delay() ou millis(), o seu programa poderia ter que esperar até o tempo de um loop para alcançar a instrução para verificar este sensor. Assim, o carro poderia frear tarde demais, resultando num acidente. O uso do millis() é ideal em algumas situações, mas quando pessoas podem se machucar, devemos garantir que tudo aconteça no tempo certo. É aqui que as interrupções entram: assim que o sensor detectar algo, ele enviaria um sinal que ativaria uma interrupção, freando o carro na hora, evitando um acidente.

Interrupções também permitem economizar recursos como memória, uma vez que não é necessário o uso de condicionais nem variáveis para validar que um evento ocorreu, o que por sua vez aumenta a eficiência do programa.


Cuidados Usando Interrupções

Como diria Stan Lee nas HQs do homem-aranha: Com grandes poderes vêm grandes responsabilidades”. Para não ter dor de cabeça usando interrupções, é necessário tomar os seguintes cuidados:

Interrupções devem ser o mais curtas possíveis. Enquanto uma ISR está sendo executada, todo resto tem que esperar, inclusive outras interrupções de mesmo nível ou inferior. Você pode acabar perdendo informações importantes se ela demorar muito tempo.

Use com moderação: Se seu programa chama várias interrupções seguidas, seu código principal nunca vai rodar.

Não misture com outras funções do Arduino. O Arduino usa interrupções em algumas funções, como exemplo a millis que usa o timer 0. Vale lembrar que os timers também controlam o PWM, portanto, configurar uma interrupção pode desligar outras funcionalidades.

Em nenhuma hipótese use algum tipo de delay numa ISR, isso quebra completamente a primeira recomendação.


Conclusão

Interrupções são recursos que possibilitam extrair 100% da eficiência do seu programa. Sair da zona de conforto que o Arduino provêm para usar outros recursos do microcontrolador, como os timers, te coloca num nível de conhecimento profissional. Dominar estes recursos permitem que você faça coisas incríveis usando algo simples como o ATmega328p.

Se você se interessou por esse assunto de interrupções, recursos do hardware, registradores e programação com C++ e quer mais posts sobre esses temas, deixe seu feedback aqui nos comentários.

Um forte abraço e até a próxima!

Tenha a Metodologia Eletrogate na sua Escola! Conheça nosso Programa de Robótica Educacional.


Sobre o Autor


Guilherme Schultz
LinkedIn

Apaixonado por tecnologia, Autodidata em eletrônica e desenvolvimento embarcado.


Eletrogate

28 de dezembro de 2021

A Eletrogate é uma loja virtual de componentes eletrônicos do Brasil e possui diversos produtos relacionados à Arduino, Automação, Robótica e Eletrônica em geral.

Tenha a Metodologia Eletrogate dentro da sua Escola! Conheça nosso Programa de Robótica nas Escolas!

Eletrogate Robô

Cadastre-se e fique por
dentro de novidades!