Polling e interrupção com ESP32 usando um reed switch

Pra quem não conhece, um reed switch é um encapsulamento de vidro fechado hermeticamente sobre um par metálco. Seu contato é normalmente aberto e se fecha reagindo a um campo magnético, por exemplo, um imã. A vantagem em utilizar um reed switch é o fato de não haver contato físico, evitando atrito e aumentanto a durabilidade de um projeto. Nesse artigo vou mostrar como utilizar polling  e interrupção com esp32 usando um reed switch para exemplificar, uma vez que ele será utilizado no projeto do relógio cuco, que está próximo de ser concluído.

Polling

A técnica de polling é comumente utilizada em microcontroladores quando não há necessidade de muita precisão quando da ocorrência do evento ou quando a MCU tem a tarefa dedicada de tratar esse evento. A diferença em utilizá-lo no ESP32 é que podemos criar tasks para fazer polling, de modo que outras funções podem estar tratando de outras tarefas.

O polling consiste na varredura de um determinado conjunto de estados de variáveis ou pinos de IO. Em um loop, o estado dos pinos é checado constantemente e, em havendo o evento, inicia-se então o tratamento.

Interrupção

Escrevi montes de artigos mostrando tratamento de interrupção em PIC, Arduino, ESP8266, ESP32, Raspberry e sei lá onde mais. Você pode usar a caixa de pesquisa para procurar ou então ir até o menu aí em cima e clicar na categoria. Minha recomendação é que digite no google “interrupções dobitaobyte” ou “interrupção dobitaobyte” e selecione a partir dos resultados.

A interrupção é mais imediata que o polling, uma vez que independe do código que esteja sendo executado. Se ocorrer esse evento, uma ISR é executada, o evento é tratado e então o código é retomado de onde parou. No ESP32 é melhor do que isso, uma vez que o tratamento é paralelo.

Tasks e núcleos

Sempre devemos lembrar que o ESP32 possui 2 núcleos programáveis em C/C++ e um ULP (Ultra Low Power coprocessor), mas esse último, somente programável em assembly. Se estiver utilizando a API do Arduino para programá-lo, considere que as funções setup() e loop() rodam no núcleo 1. Isso significa que o núcleo 0 está totalmente disponível. Normalmente o utilizo para executar minhas tasks.

Nesse artigo explico como criar uma task. Quando criada, ela se executa no núcleo em que foi iniciada. Se desejar escolher o núcleo a executar a task, escrevi esse outro artigo. Recomendo a leitura por causa dos conceitos. E nesse artigo estou adicionando mais um recurso, que é o manipulador de tarefas. Vou mostrar como utilizar alguns recursos que só coloquei em teoria em outros artigos.

Ambiente de desenvolvimento

Estou utilizando Atom com PlatformIO  e Clang para auto-completation. Prefiro utilizar essa IDE porque posso abrir duas abas do mesmo código, para analisar partes diferentes do código, ou ainda, abrir uma aba com a biblioteca que está sendo utilizada para verificar alguma referência. Você poderá programar utilizando o ESP-IDF ou a API do Arduino, de modo que será como programar na IDE do Arduino, mas com bem mais comodidade. Nesse artigo explico como instalar tudo o que é necessário, é bem simples.

Como não poderia faltar, escolhi mais uma vez utilizar a placa de desenvolvimento para ESP32, que é realmente excepcional, permitindo prototipagem rápida e para quem pretende gravar ESP32 para colocar em placa própria, nada mais cômodo do que gravar seu programa nela antes de soldá-lo à placa definitiva. É essa placa da imagem de destaque, que você encontra no nosso parceiro CurtoCircuito. Para o relógio vou utilizar esse ESP32 , com um pouco mais de recursos que adicionei na placa verde de prototipagem:

polling e interrupção com ESP32

Se interessou, o link para ele é esse, e a compra é recomendada, pois trata-se do nosso parceiro MASUGUX. O regulador de tensão para alimentar o ESP32 (que está ali, soldado na placa de prototipagem) pode ser encontrado nesse link, também do nosso parceiro MASUGUX. A placa de prototipagem é essa.

Aplicação do reed switch

Eu precisei escrever código para esse reed switch porque preciso garantir a posição de uma das engrenagens do relógio, que me garantirá estar marcando o horário certo, em conjunção com a hora buscada na Internet através de uma consulta de minuto em minuto a um servidor NTP.

O ESP32 tem um RTC interno, utilizado para timing de processos internos e sua precisão não é boa. Usar um RTC externo aumentaria a complexidade do desenvolvimento, wiring e ocuparia mais espaço dentro do relógio cuco. Por isso optei pela utilização de hora da Internet, mas não é a melhor escolha porque devemos considerar indisponibilidade de rede. De qualquer modo, não pegará fogo na casa se o relógio perder o controle das horas, então não estou preocupado com isso.

Wiring

Não fiz nada de mais. O ESP32 é 3v3, não tolerante a 5v. Para gerar a interrupção, liguei um jumper do 3v3 no pino da placa de desenvolvimento a uma perna do reed switch. Na outra perna, liguei outro jumper e conectei ao IO14. Sem resistor, nem nada.

Código de polling e interrupção com ESP32

Fiz um código para controle de níveis de uma caixa d’água para um cliente. O código é uma estrutura funcional para ser implementado ao projeto, não é o projeto inteiro, por isso o peguei para exemplo, sem afetar a confidencialidade do projeto do cliente.

Para começar, incluí as bibliotecas para usar as funções do Arduino e também incluí a de controle do gpio do ESP32. Vou explicar mais direramente no código.

#include <Arduino.h>
#include "rom/gpio.h"
//12,27,25,32
#define ESP_INTR_FLAG_DEFAULT 0
//O pino de GPIO tem um tipo específico, por isso deve-se utilizar o
//formato pré-definido ou então fazer o define passando a macro.
#define PIN_TO_INT            GPIO_NUM_14

byte st = 0; // estado dos pinos são armazenados nesse byte

bool last_state     = false; //guarda o último estado (ON ou OFF)
bool interrupted    = false; //Usado na task da interrupção de alimentação

//0000   0001 0010     0011 0100    0101   0110  0111  1000       1001    1010    1011   1100    1101   1110  1111
//vazio  25   1def,50  50   1,2d75  2d75   1def75  75  1,2,3d100  2,3d100 3,1d100 3d100  1,2d100 2d100  1d100 100
char *messages[] = {"Vazio",
                    "25%",
                    "50% (Sensor 1 off)",
                    "50%",
                    "75% (Sensor 1 e 2 off)",
                    "75% (Sensor 2 off)",
                    "75% (Sensor 1 off)",
                    "75%",
                    "100% (Sensor 1,2,3 off)",
                    "100% (Sensor 2 e 3 off)",
                    "100% (Sensor 1 e 3 off)",
                    "100% (Sensor 3 off)",
                    "100% (Sensor 1 e 2 off)",
                    "100% (Sensor 2 off)",
                    "100% (Sensor 1 off)",
                    "100%"};

//pinos de polling dos níveis da caixa
struct pins_of_pull {
  byte QUARTER = GPIO_NUM_12;
  byte HALF    = GPIO_NUM_27;
  byte TQUART  = GPIO_NUM_25;
  byte FULL    = GPIO_NUM_32;
};

pins_of_pull polling;

//semáforo para gerenciar o acesso às tarefas.
SemaphoreHandle_t xSemaphore    = xSemaphoreCreateBinary();

//declaração da ISR
void IRAM_ATTR my_isr_handler(void* arg){
    xSemaphoreGiveFromISR(xSemaphore, pdFALSE);
    interrupted = true;
}

//habilitando as interrupções
void enableInterrupt(){
  //equivalente a pinMode() do Arduino
  gpio_set_pull_mode(PIN_TO_INT,GPIO_PULLUP_PULLDOWN);

  //sem isso, as interrupções não funcionam
  gpio_install_isr_service(ESP_INTR_FLAG_DEFAULT);

  //tipo da interrupção
  gpio_set_intr_type(PIN_TO_INT, GPIO_INTR_ANYEDGE);

  //habilita interrupção no pino X
  gpio_intr_enable(PIN_TO_INT);

  //manipulador
  gpio_isr_handler_add(PIN_TO_INT, my_isr_handler, NULL);
}

void setup(){
  //Configuração dos pinos de polling - INPUT para leitura
    pinMode(polling.QUARTER,INPUT);
    pinMode(polling.HALF,INPUT);
    pinMode(polling.TQUART,INPUT);
    pinMode(polling.FULL,INPUT);

    //O mesmo para o pino de interrupção
    gpio_set_direction(PIN_TO_INT, GPIO_MODE_INPUT);
    enableInterrupt();
    Serial.begin(115200);
}

void loop(){
    if (interrupted){
      //monitor de alimentação
      st = digitalRead(PIN_TO_INT);
      if (st == 1){
          Serial.println("Ligado");
          last_state = true;
      }
      else if (last_state){
          Serial.println("Desligado");
          last_state = false;
      }
      interrupted = false;
      gpio_intr_enable(PIN_TO_INT);
    }
    st = 0;
    st = st|digitalRead(polling.QUARTER)<<0;
    st = st|digitalRead(polling.HALF)<<1;
    st = st|digitalRead(polling.TQUART)<<2;
    st = st|digitalRead(polling.FULL)<<3;

    //Serial.println(messages[st]);
    delay(10);
}

A parte mais interessante é o tratamento do polling. Invés de fazer um monte de ‘ifs’ para saber as condições, as defini no array de char messages[]. Na função loop() fiz o deslocamento de bits ao contrário; invés de empurrar 1 bit para a respectiva posição, empurrei o valor posicional da leitura dos pinos de polling, definidos na struct pins_of_pull. Utilizando uma máscara de bits, o valores anteriormente definidos são preservados, de modo que com 4 deslocamentos consigo analisar os 16 estados possíveis dos 4 pinos!

    st = 0;
    st = st|digitalRead(polling.QUARTER)<<0;
    st = st|digitalRead(polling.HALF)<<1;
    st = st|digitalRead(polling.TQUART)<<2;
    st = st|digitalRead(polling.FULL)<<3;

Os pinos de polling configurei no modo Arduino mesmo, utilizando a função pinMode(). A interrupção está sendo utilizada apenas no pino que analisa de a alimentação externa (utilizada em outra parte do projeto) está sendo fornecida ou não. Esse tratamento é um pouco mais elaborado.

IRAM_ATTR

Criei um semáforo para controlar a interrupção, na eclaração da ISR.

//declaração da ISR
void IRAM_ATTR my_isr_handler(void* arg){
    xSemaphoreGiveFromISR(xSemaphore, pdFALSE);
    interrupted = true;
}

 

enableInterrupts()

Essa é a declaração da configuração da interrupção. Configura-se o modo do pino, instala-se o serviço de ISR, a borda de interrupção, define-se o pino a interromper  e finalmente, configura-se o manipulador.

//habilitando as interrupções
void enableInterrupt(){
  //equivalente a pinMode() do Arduino
  gpio_set_pull_mode(PIN_TO_INT,GPIO_PULLUP_PULLDOWN);

  //sem isso, as interrupções não funcionam
  gpio_install_isr_service(ESP_INTR_FLAG_DEFAULT);

  //tipo da interrupção
  gpio_set_intr_type(PIN_TO_INT, GPIO_INTR_ANYEDGE);

  //habilita interrupção no pino X
  gpio_intr_enable(PIN_TO_INT);

  //manipulador
  gpio_isr_handler_add(PIN_TO_INT, my_isr_handler, NULL);
}

O modo do pino de interrupção foi configurado para INPUT no setup, mas exclusivamente esse pino eu utilizei o modo ESP32 para configurar, já que a interrupção foi toda criada assim.

Para testar, comentei o print dos níveis e deixei exclusivamete o da interrupção (porque o delay no loop é 3s no código original e para pegar a interrupção, deixei apenas 10ms). Após subir o programa para o ESP32, fiz o teste com um pequeno imã de neodímio, que já está grudado atrás da engrenagem de segundos do relógio cuco nesse momento. Funcionou lindamente, a sensibilidade é bem alta.

polling e interrupção com ESP32

Melhorando a eficiência

Nesse artigo vimos um pouco do poder do deslocamento de bits (para ver mais, sugiro esse artigo). Também vimos como criar uma interrupção com recursos do ESP32, além dos conceitos de polling e interrupção. Com esses recursos, o código ficou reduzido e preciso. Já a interrupção não está respondendo de imediato, o que a torna ineficiente nesse caso. Como resolver? – Simples; basta criar uma task para responder à requisição. Para tal, podemos fazer o seguinte:

//cria-se um manipulador para a tarefa
TaskHandle_t extPowHandler;

//declara-se a tarefa
void externalPower(void *pvParameters){
  while (true){
    if (interrupted){
      //monitor de alimentação
      st = digitalRead(PIN_TO_INT);
      if (st == 1){
          Serial.println("Ligado");
          last_state = true;
      }
      else if (last_state){
          Serial.println("Desligado");
          last_state = false;
      }
      interrupted = false;
      gpio_intr_enable(PIN_TO_INT);
    }
    vTaskDelay(pdMS_TO_TICKS(10));
  }
}

//invoca-se a tarefa a partir do ISR
void IRAM_ATTR my_isr_handler(void* arg){
    xSemaphoreGiveFromISR(xSemaphore, pdFALSE);
    interrupted = true;
}

setup(){
    ...
    xTaskCreate(externalPower,"externalPower",10000,NULL,0,&extPowHandler);
}

Funciona bem. Só que ainda não é a maneira correta de fazê-lo. Mas vou deixar para um próximo artigo, esse já está ficando longo. Simples ou não?

 

Djames Suhanko

Djames Suhanko é Perito Forense Digital. Já atuou com deployer em sistemas de missão critica em diversos países pelo mundão. Programador Shell, Python, C, C++ e Qt, tendo contato com embarcados ( ora profissionalmente, ora por lazer ) desde 2009.