Hoje vamos falar sobre como
lidar com interrupções externas no ESP32. Considero esse assunto um pouco mais
avançado dentro da programação, mas nada que seja um bicho de sete cabeças, ok!
Vamos, então, criar um exemplo para acionar a interrupção através de um botão.
Quando falamos em interrupção,
parece algo bem subjetivo, certo? No entanto, isso se trata, na verdade, de uma
sub-rotina. Ou seja, é como se fosse uma função especial de algo que acontece
em um intervalo de tempo muito rápido.
Introdução
Como o próprio nome sugere, a “Interrupção”,
quando acionada, “interrompe” a tarefa atual do processador para, então,
processar o evento que a causou.
Quando não utilizamos
Interrupção, temos que fazer todas as verificações de eventos no loop
principal.
As interrupções são úteis para
fazer coisas acontecerem automaticamente em programas de microcontroladores e
podem ajudar a resolver problemas de tempo.
Fluxo e Interrupção
Como configurar uma interrupção externa
Temos duas funções principais
para configurar a interrupção. São elas: attachInterrupt e detachInterrupt.
Essas interrupções externas
devem ser utilizadas em pinos digitais.
Veremos um pouco sobre elas a
seguir.
attachInterrupt
attachInterrupt(digitalPinToInterrupt(pin), ISR, mode);
Essa chamada configura a
interrupção para determinado pino, indicando também qual evento que acionará a
interrupção e qual a função que responderá por ela.
pin: pino de interrupção.
ISR (Interrupt Service
Routine): é uma função que será chamada quando a interrupção ocorrer.
mode: define quando a
interrupção deve ser acionada. Existem quatro constantes predefinidas com
valores válidos (veremos mais adiante).
detachInterrupt
detachInterrupt(pin);
Essa chamada desativa a
interrupção do pino.
- Não tem retorno nenhum.
Interrupt Service Routine (ISR)
Os ISRs são tipos especiais de
funções que possuem algumas limitações exclusivas que a maioria das outras
funções não possuem. Um ISR não pode ter nenhum parâmetro e não deve retornar
nada.
- Um ISR deve ser o mais curto e rápido possível.
- Se seu programa utiliza múltiplos ISRs, apenas um pode ser executado por vez.
- millis() utiliza interrupções para contar, portanto, não incrementará dentro de um ISR.
- delay() requer interrupção, portanto, não funcionará dentro de um ISR.
- delayMicroseconds() não utiliza interrupções, portanto, funcionará normalmente.
MODE
Define quando a interrupção
será acionada.
Abaixo seguem as constantes
predefinidas:
- LOW : aciona a interrupção sempre que o pino estiver baixo.
- CHANGE: aciona a interrupção sempre que o pino muda de estado.
- RISING: aciona a interrupção quando o pino vai de baixo para alto (LOW > HIGH).
- FALLING: para acionar a interrupção quando o pino vai de alto para baixo (HIGH > LOW)
- HIGH : aciona a interrupção sempre que o pino estiver alto.
Outras informações importantes
·
Se for utilizar alguma variável para passar
dados entre uma ISR e o programa principal, a variável deve ser definida como
volátil (volatile), garantindo assim que elas sejam atualizadas corretamente.
·
Para recuperar o millis() dentro de uma ISR,
utilize xTaskGetTickCount().
·
Para mudar os valores das variáveis voláteis,
faça isso abrindo uma seção crítica, como no exemplo abaixo:
volatile byte state = LOW; // Para configurar seções críticas (interrupções de ativação e interrupções de desativação não disponíveis) // usado para desabilitar e interromper interrupções (cuida da sincronização entre codigo principal e interrupção) portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED; // instancia o mux portENTER_CRITICAL_ISR(&mux); // abre a seção critica state = !state; // altera o valor da variavel portEXIT_CRITICAL_ISR(&mux); // fecha a seção critica
Demonstração
WiFi NodeMCU-32S ESP-WROOM-32
Montagem
Programa
Faremos agora um programa no
qual, ao pressionar/soltar um botão, acionará a interrupção fazendo com que um
LED Verde ou Vermelho acenda. Iremos configurar a interrupção para ocorrer a
cada mudança de estado do pino, ou seja, vamos utilizar o modo CHANGE.
Botão pressionado > LED VERDE aceso
Botão pressionado > LED VERDE aceso
Botão solto > LED VERMELHO
aceso
Variáveis
Primeiramente, vamos definir o
pino de interrupção e o tempo máximo de debounce para o botão. Na sequência,
vamos definir os pinos de ligação dos Leds vermelho e verde.
Ainda nesta etapa, vamos
declarar voláteis as variáveis que serão compartilhadas pelo ISR e pelo código principal.
Apontamos as variáveis para controle dentro do loop.
#define pinBUTTON 23 //pino de interrupção (botão) #define DEBOUNCETIME 10 //tempo máximo de debounce para o botão (ms) #define pinREDled 2 //pino do led VERMELHO #define pinGREENled 4 //pino do led VERDE //É DECLARADA VOLÁTIL PORQUE SERÁ COMPARTILHADA PELO ISR E PELO CÓDIGO PRINCIPAL volatile int numberOfButtonInterrupts = 0; //número de vezes que a interrupção foi executada volatile bool lastState; //guarda o último estado do botão quando ocorreu a interrupção volatile uint32_t debounceTimeout = 0; //guarda o tempo de debounce //variáveis para controle dentro do loop uint32_t saveDebounceTimeout; bool saveLastState; int save; // For setting up critical sections (enableinterrupts and disableinterrupts not available) // used to disable and interrupt interrupts // Para configurar seções críticas (interrupções de ativação e interrupções de desativação não disponíveis) // usado para desabilitar e interromper interrupções portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED;
Setup
Inicializamos os pinos,
incluindo o de interrupção. Configuramos a interrupção do botão no evento
CHANGE para a função handleButtonInterrupt. Posteriormente, inicializamos o Led
vermelho como aceso.
void setup() { Serial.begin(115200); String taskMessage = "Debounced ButtonRead Task running on core "; taskMessage = taskMessage + xPortGetCoreID(); Serial.println(taskMessage); //mostra o core que o botão está executando // set up button Pin pinMode (pinREDled, OUTPUT); pinMode (pinGREENled, OUTPUT); pinMode(pinBUTTON, INPUT_PULLUP); // Pull up to 3.3V on input - some buttons already have this done attachInterrupt(digitalPinToInterrupt(pinBUTTON), handleButtonInterrupt, CHANGE); //configura a interrupção do botão no evento CHANGE para a função handleButtonInterrupt digitalWrite(pinREDled, HIGH); //inicializa o LED VERMELHO como aceso }
Loop
Aqui, iniciamos e finalizamos
a seção crítica. Recuperamos o estado atual do botão e definimos que, se o
botão está pressionado, então liga o Led verde e apaga o vermelho. Caso
contrário, liga o Led vermelho e apaga o verde.
Ainda, determinamos a
impressão de quantas vezes foi acionada a interrupção. Para tal, iniciamos a
seção crítica, reconhecemos que o botão foi pressionado e resetamos o contador
de interrupção, para, então, finalizar a seção crítica.
void loop() { portENTER_CRITICAL_ISR(&mux); // início da seção crítica save = numberOfButtonInterrupts; saveDebounceTimeout = debounceTimeout; saveLastState = lastState; portEXIT_CRITICAL_ISR(&mux); // fim da seção crítica bool currentState = digitalRead(pinBUTTON); //recupera o estado atual do botão //se o estado do botão mudou, atualiza o tempo de debounce if(currentState != saveLastState) { saveDebounceTimeout = millis(); } //se o tempo passado foi maior que o configurado para o debounce e o número de interrupções ocorridas é maior que ZERO (ou seja, ocorreu alguma), realiza os procedimentos if( (millis() - saveDebounceTimeout) > DEBOUNCETIME && (save != 0) ) { //se o botão está pressionado //liga o led verde e apaga o vermelho //caso contrário //liga o led vermelho e apaga o verde if(currentState) { digitalWrite(pinGREENled, HIGH); digitalWrite(pinREDled, LOW); } else{ digitalWrite(pinGREENled, LOW); digitalWrite(pinREDled, HIGH); } Serial.printf("Button Interrupt Triggered %d times, current State=%u, time since last trigger %dms\n", save, currentState, millis() - saveDebounceTimeout); portENTER_CRITICAL_ISR(&mux); //início da seção crítica numberOfButtonInterrupts = 0; // reconhece que o botão foi pressionado e reseta o contador de interrupção //acknowledge keypress and reset interrupt counter portEXIT_CRITICAL_ISR(&mux); //fim da seção crítica } }
handleButtonInterrupt
Por fim, trabalhamos com a interrupção
da rotina de serviço.
Temos a função IRAM_ATTR,
utilizada para indicar que tal trecho de código ficará na seção do barramento
de instruções da RAM (maior velocidade), e não na Flash. Já a função portENTER_CRITICAL_ISR
/ portEXIT_CRITICAL_ISR é necessária porque a variável que vamos usar também é
alterada pelo loop principal, como visto anteriormente. E precisamos evitar
problemas de acesso simultâneo.
Ainda nesta etapa,
incrementamos o contador de interrupções, fazemos a leitura do estado atual do
botão e analisamos a versão do millis que funciona a partir da interrupção.
// Interrupt Service Routine - Keep it short! //Interrupção //Interrompe a rotina de serviço //IRAM_ATTR --> é utilizado para indicar que esse trecho de código ficará na seção do barramento de instruções da RAM (maior velocidade) e não na Flash //portENTER_CRITICAL_ISR /´portEXIT_CRITICAL_ISR --> Isso é necessário porque a variável que vamos usar também é alterada pelo loop principal, //como visto anteriormente, e precisamos evitar problemas de acesso simultâneo. void IRAM_ATTR handleButtonInterrupt() { portENTER_CRITICAL_ISR(&mux); numberOfButtonInterrupts++; lastState = digitalRead(pinBUTTON); debounceTimeout = xTaskGetTickCount(); //versão do millis () que funciona a partir da interrupção //version of millis() that works from interrupt portEXIT_CRITICAL_ISR(&mux); }
11 Comentários
Olá, eu tenho uma esp8266-07 e estou tentando captar tanto RISING quanto FALLING de sinal, consegui captar o RISING com perfeição, porém o FALLING está contando 2 vezes, quando desce e sobe o sinal. O que eu faço para corrigir isso?
ResponderExcluirattachInterrupt(digitalPinToInterrupt(pulsador), isr, FALLING);
Excluir..
if(currentState) mudar por: if(currentState == false)
Não entendi uma coisa no exemplo:
ResponderExcluir- se a interrupção ocorre apenas quando o botão muda de estado, por que durante o loop a gente tem que testar se houve mudança de estado de novo?
Para fazer o debounce do ruído de acionamento do botão.
ExcluirMuito interessante o post. Parabéns, Fernando!
ResponderExcluirMuito bom artigo! E se eu precisasse da interrupção de algum outro periférico, como A/D ou Serial, como faria?
ResponderExcluirQuantos pinos do Esp32 podem ser usados como IRQ?
ResponderExcluirBom dia, adorei este artigo, não encontrei outra explicação melhor e mais detalhada, parabéns.
ResponderExcluirUma pequena dúvida, você descreveu acima se as funções millis(), delay() e delayMicrosseconds() utilizam ou não interrupções. Mas e a função micros()? Utiliza interrupções ou não? Gostaria de utilizá-la para ter uma medição mais precisa.
Faz um certo tempo já, talvez vc ja até tenha resolvido seu problema, mas é o seguinte: se a função utiliza interrupção ou não eu não sei, porem o fato é que funciona. Eu utilizei essa função no meu código e funcionou como o esperado.
Excluirfernado uma grande duvida em relaçao a interrupção serial como fazer?
ResponderExcluirSofri um bocadinho aqui tentando fazer uma interrupção funcionar com mode LOW, e aí procurando ai pelo google descobri que pro ESP32 não é LOW ou HIGH, e sim ONLOW ou ONHIGH, fica aí pro pessoal que possa vir a esbarrar nisso.
ResponderExcluirObrigado pelo post!