Recents in Beach


Receba o meu conteúdo GRATUITAMENTE


Os profissionais sabem disso: Interrupt ISR



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:
  1. LOW : aciona a interrupção sempre que o pino estiver baixo.
  2. CHANGE: aciona a interrupção sempre que o pino muda de estado.
  3. RISING: aciona a interrupção quando o pino vai de baixo para alto (LOW > HIGH).
  4. FALLING: para acionar a interrupção quando o pino vai de alto para baixo (HIGH > LOW)
  5. 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 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);
}



Faça o download dos arquivos:



Postar um comentário

10 Comentários

  1. 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?

    ResponderExcluir
    Respostas
    1. attachInterrupt(digitalPinToInterrupt(pulsador), isr, FALLING);
      ..
      if(currentState) mudar por: if(currentState == false)

      Excluir
  2. Não entendi uma coisa no exemplo:
    - 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?

    ResponderExcluir
  3. Muito interessante o post. Parabéns, Fernando!

    ResponderExcluir
  4. Muito bom artigo! E se eu precisasse da interrupção de algum outro periférico, como A/D ou Serial, como faria?

    ResponderExcluir
  5. Quantos pinos do Esp32 podem ser usados como IRQ?

    ResponderExcluir
  6. Bom dia, adorei este artigo, não encontrei outra explicação melhor e mais detalhada, parabéns.

    Uma 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.

    ResponderExcluir
    Respostas
    1. 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.

      Excluir
  7. fernado uma grande duvida em relaçao a interrupção serial como fazer?

    ResponderExcluir
  8. Sofri 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.

    Obrigado pelo post!

    ResponderExcluir