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.
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).
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.
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:
Placas Due, Zero e MKR1000 suportam também:
(Fonte: https://www.arduino.cc/reference/pt/language/functions/external-interrupts/attachinterrupt/)
Ex. de uma função attachInterrupt completa:
attachInterrupt(digitalPinToInterrupt(pinX), ISR, LOW);
Nesse exemplo vamos piscar 2 LED’s usando o que acabamos de aprender sobre interrupções externas.
//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; } }
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.
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.
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.
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.
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);
Nesse exemplo vamos piscar 2 leds ao mesmo tempo usando timers. Um usando delay() e o outro timer.
#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); }
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.
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.
#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.
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”.
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.
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.
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.
|
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.
Conheça a Metodologia Eletrogate e Lecione um Curso de Robótica nas Escolas da sua Região!