Manual

do

Maker

.

com

ISR - Interrupções e timer com ESP32

ISR - Interrupções e timer com ESP32

Interrupções e timers são os recursos que mais gosto de utilizar, seja em MCU ou CPU. Mas nem todos os makers tem habilidade com esses recursos, às vezes por não se incomodar em gastar recursos, às vezes por não compreender onde cabe o recurso. Por essa razão, vou escrever brevemente a respeito.

Se você procura por interrupção e timer com Arduino, sugiro esse artigo. Caso deseje fazê-lo com PIC, tenho esse artigo, esse outro, esse sobre timer, esse sobre Interrupt On Change. Claro que não poderia deixar de escrever a respeito para o Raspberry Pi, como você vê nesse artigo.

Material necessário

Para esse tutorial tudo o que você precisa é de um ESP32. Repare que já escrevi uns 10 artigos consecutivos sobre ESP32 e acredito que seja material suficiente para convencê-lo do poder desse monstrinho. Se ainda não tem um, sugiro que pegue o seu na CurtoCircuito.

Timer com ESP32

Suponhamos que você deseja executar uma rotina em intervalos específicos de tempo. Como se trata de ESP32, é simples criar uma task e colocá-la em suspend através do delay. Mas existem 2 tipos de timer; por software e por hardware. Se tratando de software, já basta uma task como citado. Por hardware, podemos utilizar o seguinte exemplo:

hw_timer_t * timer = NULL;

void cb_timer(){
    static unsigned int counter = 1;
    Serial.println("cb_timer(): ");
    Serial.print(counter);
    Serial.print(": ");
    Serial.print(millis()/1000);
    Serial.println(" segundos");

    if (counter == 15){
        stopTimer();
    }
    counter++;
}

void startTimer(){
    /inicialização do timer. Parametros:
    /* 0 - seleção do timer a ser usado, de 0 a 3.
      80 - prescaler. O clock principal do ESP32 é 80MHz. Dividimos por 80 para ter 1us por tick.
    true - true para contador progressivo, false para regressivo
    */
    timer = timerBegin(0, 80, true);

    /*conecta à interrupção do timer
     - timer é a instância do hw_timer
     - endereço da função a ser chamada pelo timer
     - edge=true gera uma interrupção
    */
    timerAttachInterrupt(timer, &cb_timer, true);

    /* - o timer instanciado no inicio
       - o valor em us para 1s
       - auto-reload. true para repetir o alarme
    */
    timerAlarmWrite(timer, 1000000, true); 

    //ativa o alarme
    timerAlarmEnable(timer);
}

void stopTimer(){
    timerEnd(timer);
    timer = NULL; 
}

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

void loop(){

}

Repare que a função stopTimer foi criada apenas para demonstrar como interagir externamente com o timer, porque se você realmente quiser um single-shot, basta passar false no timerAttachInterrupt e ainda, se precisar mais de um ciclo, basta fazer um loop dentro da função cb_timer.

Interrupção com ESP32

Tem duas maneiras de verificar a ocorrência de um evento. A menos eficiente é fazendo polling. Além de o evento não ser pego no exato momento em que ocorreu, processamento é disperdiçado apenas para rodar um código que analisará se houve o evento. Assim como o timer, existe a interrupção por software e a interrupção por hardware. A interrupção por hardware acontece a partir de um evento externo, por exemplo, em um pino de GPIO.

Uma interrupção pode gerar o gatilho na borda alta ou na borda baixa do pino. para rising edge, quando o pino sai do estado lógico 0 para 1, temos o GPIO_INTR_POSEDGE. Já para falling edge, quando o pino sai do estado 1 para o estado 0, utilizamos o GPIO_NEGEDGE. Repare que a porção POS é para POSITIVE e a porção NEG é para **NEGATIVE.**Assim fica fácil decorar.

Para configurar um pino de interrupção, usa-se:

esp_err_t gpio_set_intr_type(gpio_num_t gpio_num, gpio_int_type_t intr_type);

Os parâmetros são o pino de interrupção e o tipo de interrupção. Fácil ou não? - Não tanto quanto bom seria. No ESP32 você tem até 32 slots de interrupção para cada núcleo (!), com diferentes prioridades. Você vai conseguir criar uma interrupção sim, através desse artigo, sem sofrimentos. Mas na hora de programar um projeto, pode começar a complicar a coisa quando tiver que gerenciar interrupções, filas, núcleos e tasks. Por outro lado, é mais poder do que você normalmente usa em um desktop. É simplesmente delicioso!

Primeiro passo

Você deve instalar o servio que gerencia as interrupções para os pinos de GPIO:

#define ESP_INTR_FLAG_DEFAULT 0
...
gpio_install_isr_service(ESP_INTR_FLAG_DEFAULT);

Segundo passo

A partir daí você pode especificar para cada pino em qual núcleo sua ISR (Interrupt Service Routine) será executado. Lembre-se que cada núcleo tem 32 canais de interrupção. Além disso, você pode passar parâmetros para a função, o que dá ainda mais flexibilidade de uso:

gpio_isr_handler_add(gpio_num, isr_handler, void* args);

Os parâmetros são o pinoa ISR (função para a interrupção), e um array de argumentos.

As interrupções bloqueiam o fluxo principal do programa, portanto faça com que sua interação seja a mais breve possível. Normalmente eu crio uma flag no fluxo principal e quando ocorre uma interrupção, mudo o valor dessa flag. Como estamos falando de ESP32, essa não seria a melhor forma de tratar interrupções, mas dependendo do que tem na task principal, não seria nada mal implementar dessa maneira. Mas você pode criar uma tarefa dedicada a atender a interrupção!

Terceiro passo

Vamos criar uma tarefa dedicada, oras. Mas para isso, precisamos ter um controle para que não sejam executadas múltiplas instâncias dessa tarefa. Nesse outro artigo vimos a respeito do mutex. Dessa vez vamos utilizar um semáforo binário.

SemaphoreHandle_t xSemaphore = NULL;
...
xSemaphore = xSemaphoreCreateBinary();

Vamos criar uma ISR para um botão:

void IRAM_ATTR button_ISR_handler(void* arg){
  xSemaphoreGiveFromISR(xSemaphore, NULL);
}

Se você leu o artigo sobre mutex, já deve ter imaginado que essa função apenas libera o semáforo. Desse modo, uma tarefa pode tomá-lo e seguir com sua execução.

A tarefa que nos dará feedback seria algo como:

void buttonTask(void *pvParameters){
  while (true){
    if(xSemaphoreTake(xSemaphore,portMAX_DELAY) == pdTRUE){
      Serial.println("Botao pressionado");
    }
  }
}

Essa tarefa ficaria em um loop infinito, então um single-shot viria bem a calhar. O que seria melhor, um timer com single-shot ou uma task com vTaskDelete(NULL)?

O código de exemplo do timer está completo, em breve iniciaremos algumas brincadeiras práticas, como esse artigo para mostrar audio e visualmente o funcionamento da multitask. Acompanhe!

Inscreva-se no nosso canal Manual do Maker no YouTube.

Também estamos no Instagram.

Nome do Autor

Djames Suhanko

Autor do blog "Do bit Ao Byte / Manual do Maker".

Viciado em embarcados desde 2006.
LinuxUser 158.760, desde 1997.