O ESP32 dispõe de dois núcleos: processador e cooprocessador em arquitetura Tensilica LX6, que possibilitam a construção de microprocessamentos personalizados, através da divisão de funções em núcleos distintos. Neste post, iremos explorar as principais opções para a configuração de processamento de placas ESP32, armazenamento de informações em memória RTC e metodologias que proporcionam maior economia ao circuito elétrico.

          Para concluir as atividades desse post tenha em mãos os seguintes componentes:

          Você pode compra-los clicando nos links acima ou visitando nosso site: www.curtocircuito.com.br.

 

Dual Core: Processamento Independente

          Uma das opções que tornam essa placa tão interessante encontra-se na possibilidade de separar o processamento de informações, onde se torna possível atribuir funções distintas em cada CPU. Aplicar o multiprocessamento em uma programação, requer o uso de funções Task do FreeRTOS, que refere-se a um sistema operacional em tempo real (Real Time Operation System) , que possibilita atribuir níveis de prioridade a execução de determinadas tarefas, construir pausas e depois retoma-las do último ponto.

          Para melhor entendimento, utilizaremos um exemplo em que o ESP32 irá dividir atividades em núcleos distintos:

 

Núcleo  0 - Irá contar a quantidade de vezes que um LED irá piscar;
Núcleo 1 - Responsável por realizar uma contagem crescente, em segundos;

cada núcleo será responsável pelo processamento de uma atividade distinta.


          As atividades serão separadas através de funções como xTaskCreate ou xTaskCreatePinnedToCore, ambas possuem um funcionamento similar, porém, o TaskCreate define um núcleo de trabalho automaticamente, alocando a função no core que estiver disponível no momento.

O funcionamento de ambas será definido da seguinte maneira:

 

xTaskCreatePinnedToCore(função, string, variável, parâmetros, prioridade, hadle, core);
xTaskCreate(função, string, variável, parâmetros, prioridade,hadle);

          Onde:

Função: será o indicativo da função, direcionando a leitura;
String: cria a função;
Variáveis: quantidade de variáveis a serem tratadas, com valor em bytes;
Parâmetros: executa tarefas pré-definidas;
Prioridade: será definida por ordem crescente, quanto maior mais priorizado;
Hadle: manipulador para referenciar a tarefa;
Core: Define o processador ao qual a função será executada.

          No caso dessa atividade utilizaremos o xTaskCreatePinnedToCore, e definirmos manualmente em qual núcleo a função será alocada, separando assim as atividades presente no Task e void loop em processadores diferentes. A programação abaixo irá utilizar o LED D2, que encontra-se embutido na própria placa, portanto, não haverá necessidade de construir um circuito elétrico para este exemplo. 

/* Projeto Curto Circuito – ESP32: Processamento Independente */

int tempo;     /* Variável que armazenar a contagem */
int pisca;      /* Variável que armazenar quantas vezes o LED irá piscar */
void setup()
{
  Serial.begin(115200);
  pinMode(2, OUTPUT); /*LED D2 do ESP32 */
  /* Exibe no monitor em qual núcleo o setup() estará */
  Serial.printf("\n loop() em: %d", xPortGetCoreID());
  /* Cria a tarefa "nucleo0()" com prioridade 1, atribuída ao core 0 */
  xTaskCreatePinnedToCore(nucleo0, "nucleo0", 10000, NULL, 1, NULL, 0);
  delay(1);
}
void loop()
{
  Serial.println("\nTempo em segundos:"); /* Escreve no monitor serial */
  Serial.println( tempo++);                          /* aumenta o valor da variável */
  delay(1000);                                            /* a cada 1 segundo */
}
void nucleo0(void*z)
{
  /*Mostra no monitor em qual núcleo o loop2() estará */
  Serial.printf("\n nucleo0() em: %d", xPortGetCoreID());
  while (1)
  {
    Serial.println("\nPisca LED:");      /*Escreve no monitor serial */
    Serial.println(pisca++);                 /*o valor da variável pisca*/
    digitalWrite(2, !digitalRead(2));    /*e pisca o led*/
    delay(500);                                  /*a cada meio segundo*/
  }
}

          A programação dividiu o processamento da atividade em dois, enquanto o núcleo 0 será responsável por fazer o LED piscar e demonstrar quantas vezes tal evento ocorreu, o núcleo 1, irá demonstrar apenas o tempo corrido durante o processo. O comando xPortGetCoreID(), indica o núcleo de processamento responsável pela leitura de cada void, no caso desse exemplo, a programação foi dividida entre void loop e void nucleo0.

          Segundo a função xTaskCreatePinnedToCore(nucleo0, "nucleo0", 10000, NULL, 1, NULL, 0), definimos que a leitura do núcleo 0 será direcionada para a função "nucleo0", e a prioridade na execução das tarefas será nula (NULL), ou seja, se não há preferência, então o processamento ocorrerá de forma sequencial, seguindo o fluxo de programação.

          Este tipo de exemplo poderá servir como forma de aprimorar projetos, simplificando o processamento de informações, por meio das opções de configuração dos microprocesadores personalizáveis. Suponha, por exemplo, a construção de uma estação meteorológica, onde devem ser registrados os valores presente sensor, juntamente ao horário em que tal leitura ocorreu. Ambos poderiam ser alocados em um único núcleo, porém, dividi-los servirá para focar a leitura de cada função em um processamento distinto, aliviando a quantidade de tarefas presente em um único Core.

 

ULP: Ultra Low Power

          Trata-se de um sistema operacional em ultrabaixo presente no coprocessador de placas ESP32, que possibilita a leitura de funções com um consumo de energia reduzido. Esse método de economia poderá ser aplicado de três formas distintas, Deep Sleep (sono profundo), que desliga os dois principais núcleos de processamento e mantem apenas o coprocessador, Light Sleep (sono leve), interrompe temporariamente o núcleo de processamento mantendo, e Modem Sleep (sono dos módulos), modo de hibernação das funções Wi-Fi e Bluetooth. Essas três aplicações serão utilizadas para desativar funções em excesso, que em muitos casos encontram-se em desuso, ou que operam apenas em um curto período de tempo, sem haver a real necessidade de manter-se continuamente em uso.

          Cada modo será responsável pela hibernação de determinadas funções e componentes do ESP32, como mostra a tabela apresentada abaixo:

 

Componentes afetados pelas funções de descanso do ESP32.


          O ESP32 possui a capacidade de permanecer em hibernação por muitos anos, e despertar automaticamente de forma cronometrada ou por meio de sensores. A leitura de um sensor em modo Deep Sleep, por exemplo, poderá operar por meio de um temporizador, despertando em determinados períodos apenas para coletar informações, consumindo apenas 40 mA.

 

Deep Sleeep

          O modo de leitura em Deep Sleep desliga praticamente todas as funções principais do ESP32, mantendo apenas alguns periféricos de memória e o coprocessador ULP, com um consumo de aproximadamente 150 µA. As informações presente na placa serão reiniciadas, e a única forma de manter os dados a salvo será através da construção de uma variável de armazenamento que aloque tais dados em uma memória RTC (Real Time Clock), que se trata de um módulo de trabalho que manipula informações em tempo real. Como exemplo, suponha que os dois processadores estejam em uso, e como meio de economizar, deverá entrar em descanso a cada dez segundos, retomando as atividades ao pressionar um dos sensores capacitivos. Para a construção do circuito será necessário encaixar o polo positivo  do LED no pino D13, conectar um resistor de 1K entre um dos pinos GND e o polo negativo, e encaixar um jumper no pino D4.

 

Colocar o controlador em hibernação trará maior economia para o circuito, podendo despertado a qualquer momento, por meio de sensores capacitivos.


          Assim como o exemplo citado anteriormente, o processamento será dividido entre tempo corrido e o piscar de um LED, os valores da contagem devem ser armazenados em RTC, para que possa retomar a contagem com base no último valor registrado.

/* Projeto Curto Circuito – ESP32: Deep Sleep */

#include <esp_deepsleep.h> /* Biblioteca ESP32 deep sleep */
#define Threshold 40 /*Calibra a sensibilidade do sensor Cap.*/
touch_pad_t touchPin;        /* Variável de controle.*/
RTC_DATA_ATTR int tempo;     /* Armazena os valores de tempo no RTC. */
RTC_DATA_ATTR int tempo2;    /* Armazena os valores de tempo no RTC2. */
#define led2 13 /* Define o LED2 como o pino D13 do ESP32. */

void wakeup_touchpad()
{
  touch_pad_t pin;
  touchPin = esp_sleep_get_touchpad_wakeup_status();  /* Acorda o ESP através do Touch */
}

void callback() {}            /* Função de retorno vazia */
void setup()
{
  wakeup_touchpad();
  Serial.begin(115200);       /* Taxa de transmissão 115200dBs. */
  pinMode(led2, OUTPUT);      /* LED2 Embutido no ESP32 */
  digitalWrite(led2, 0);      /* Desliga o LED2 */
  /* (Pinagem, função retorno, sensibilidade). */
  touchAttachInterrupt(T0, callback, Threshold);
  /* Mostra em qual núcleo o setup() estará */
  Serial.printf("\nsetup() em core: %d", xPortGetCoreID());
  /* Tarefa "pled()" com prioridade 1, atribuída ao core 0 */
  xTaskCreatePinnedToCore(pled, "pled", 8192, NULL, 1, NULL, 0);
  delay(1);
  esp_sleep_enable_touchpad_wakeup(); /* Habilita o Wake através do touch */
  tempo2 += 10;                       /* Atribui 10 ao valor de tempo2 */
}
void loop()
{
  if (tempo == tempo2)
  { /* Se tempo for igual a tempo2 */
    delay(1);
    esp_deep_sleep_start();          /* Esp32 entra em Deep Sleep */
  }
  Serial.printf("\n Tempo corrido: %d", tempo++);
  delay(1000);                       /* Realiza uma contagem a cada 1 segundo */
}
void pled(void*z)
{
  /* Mostra em qual núcleo o pled() será executado */
  Serial.printf("\n pled() em core: %d", xPortGetCoreID());
  while (1)
  {
    digitalWrite(led2, !digitalRead(led2)); /* Pisca o led2 */
    delay(100);
  }
}

          A função RTC_DATA_ATTR int tempo, servirá para armazenar os valores da variável "tempo" na memória RTC. O void wakeup_touchpad será utilizado na configuração de um sensor capacitivo, como meio de tirar o ESP32 do modo de hibernação. As funções touch_pad_t esp_sleep_get _touchpad_wakeup_status, irão construir variáveis de leitura  aplicadas aos pinos capacitivos, determinando que o toque irá influenciar no status da hibernação. O touchAttachInterrupt() configura o pino T0 como um interruptor no processo de despertar da placa, utilizando o valor da variável Threshold no ajuste da sensibilidade do sensor. E por fim, esp_deep_sleep_start(), inicia o modo de hibernação da placa.

 

Light vs. Deep: Leitura de Sensores

          Em alguns projetos, a aplicação do modo de hibernação intensa irá atrapalhar na coleta de algumas informações, neste caso, podemos substituir a aplicação do modo anterior por um Light Sleep, que proporciona um descanso mais suave, mantendo boa parte das funções ativas, porém, fornecendo uma economia menor em comparação com o Deep Sleep. Para compreender a diferença entre esses dois meios de hibernação, iremos propor dois exemplos, ambos irão conter a mesma função, coletar informações em um sensor de chuva e hibernar em certos períodos, utilizando a cada exemplo um tipo de descanso.

          O circuito elétrico deste exemplo utilizará um sensor de chuva comum, conectando os pinos GND e VCC nas fileiras de alimentação do Protoboard, o terminal "D" na porta digital D12,  e "A" na porta analógica VP (ou D36). A leitura será dividida entre digital e analógica, a digital será responsável por identificar quando houver contato entre o sensor e gotas de água, enquanto a analógica irá medir a intensidade da chuva, essa medida será abordada de forma decrescente, através da faixa de leitura de 0 á 4095, sendo qualquer valor acima de 2000 clima aberto ou chuviscos, e abaixo de 1000 chuva intensa.

Circuito Elétrico, conectando o sensor de chuva ao ESP32


          Light Sleep: 
A programação pausa o processamento temporariamente por 30 segundos, ou até cair uma gota de água no sensor de chuva.

/* Projeto Curto Circuito – ESP32: Sensores em Light Sleep */

#include <esp_deepsleep.h> /* Biblioteca ESP32 deepsleep */
int tempo;
int tempo2;
int val_a;

int pino_a = 36;
int pino_d = 12;
#define led2 2 /* Define o LED como o pino D2.do ESP32 */

void setup()
{
  Serial.begin(115200); /* Inicia a comunicação serial.*/
  pinMode(led2, OUTPUT);
  pinMode(pino_d, INPUT);
  pinMode(pino_a, INPUT);
  Serial.printf("\nsetup() em core: %d", xPortGetCoreID());
  xTaskCreatePinnedToCore(loop2, "loop2", 8192, NULL, 1, NULL, 0);
  delay(1);
  esp_sleep_enable_ext0_wakeup(GPIO_NUM_13, 0);
  esp_sleep_enable_timer_wakeup(30000000);
  tempo2 += 10;
}
void loop()
{
  val_a = analogRead(pino_a);

  if (tempo == tempo2)
  {
    delay(1);
    tempo2 += 10;
    esp_light_sleep_start();
  }
  Serial.printf("\n Tempo corrido: %d", tempo++);
  Serial.printf("\n Intensidade da Chuva %d", val_a);
  delay(1000);
}
void loop2(void*z)
{
  Serial.printf("\nloop2() em core: %d", xPortGetCoreID());
  while (1)
  {
    digitalWrite(led2, !digitalRead(led2));
    delay(1000);
  }
}

          Deep Sleep: Matem o sistema desligado durante 30 segundos, ou até cair uma gota de água no sensor de chuva.

/* Projeto Curto Circuito – ESP32:Sensores em Deep Sleep */

#include <esp_deepsleep.h> /* Biblioteca ESP32 Deep Sleep */

RTC_DATA_ATTR int tempo;
RTC_DATA_ATTR int tempo2;
RTC_DATA_ATTR int val_a;
int pino_d = 13;
int pino_a = 36;

#define led2 2 /* Define o LED ao pino D22.*/

void setup()
{
  Serial.begin(115200);/* Inicia a comunicação serial */
  pinMode(led2, OUTPUT);
  pinMode(pino_d, INPUT);
  pinMode(pino_a, INPUT);
  Serial.printf("\nsetup() em core: %d", xPortGetCoreID());
  xTaskCreatePinnedToCore(loop2, "loop2", 8192, NULL, 1, NULL, 0);
  esp_sleep_enable_ext0_wakeup(GPIO_NUM_13, 0);
  esp_sleep_enable_timer_wakeup(30000000);
  tempo2 += 10;
}
void loop()
{
  val_a = analogRead(pino_a);
  if (tempo == tempo2)
  {
    esp_deep_sleep_start();
  }
  Serial.printf("\n Tempo corrido: %d", tempo++);
  Serial.printf("\n Intensidade da Chuva: %d",  val_a);
  delay(1000);/* Mantem o processador 1 em estado ocioso por 1seg */
}
void loop2(void*z)
{
  Serial.printf("\nloop2() em core: %d", xPortGetCoreID());
  while (1)
  {
    digitalWrite(led2, !digitalRead(led2));
    delay(1000);
  }
}

          Ao abrir o monitor serial, e observe que a leitura no exemplo Deep Sleep, será reeniciada a cada trinta segundos, isso porque a CPU neste modo será totalmente desligada. Todos os dados seriam perdidos, se não houvesse uma referencia do último valor armazenado na memória RTC. Enquanto o exemplo Light Sleep, deixa a CPU em pausa por 30 segundos, retomando a leitura sem a necessidade de armazenamento de dados. Tais modos irão depender do tipo de controle que deseja aplicar a um projeto, em todos os meios de hibernação a comunicação com os módulos Bluetooth e Wi-Fi serão interrompidos, proporcionando maior economia no consumo de energia.

 

Despertar Sensorizado

          Outra forma de despertar o sistema de forma eficiente encontra-se na leitura dos níveis lógicos de um sensor externo, por exemplo, suponha que um sistema deva despertar ao sentir o gotejar da chuva, coletar informações, e retornar a hibernação. Na programação abaixo utilize o mesmo circuito apresentado anteriormente, com um sensor de chuva conectado nos pinos D36 e D12.

/* Projeto Curto Circuito – ESP32: Despertar com Chuva */

#include <esp_deepsleep.h> /* Biblioteca ESP32 Deep Sleep */
RTC_DATA_ATTR int tempo;
RTC_DATA_ATTR int tempo2;
int pino_d = 13;
int pino_a = 36; 
int val_a = 0;
#define led2 22 /* Define o LED ao pino D22.*/
void setup()
{
  Serial.begin(115200); /* Inicia a comunicação serial. */
  pinMode(led2, OUTPUT);
  pinMode(pino_d, INPUT);
  pinMode(pino_a, INPUT);
  Serial.printf("\nsetup() em core: %d", xPortGetCoreID());
  xTaskCreatePinnedToCore(loop2, "loop2", 8192, NULL, 1, NULL, 0);
  delay(1);
  esp_sleep_enable_ext0_wakeup(GPIO_NUM_12, 0);
  tempo2 += 10;
}

void loop()
{
  val_a = analogRead(pino_a);
  if (tempo == tempo2)
  {
    delay(1);
    tempo2 += 10;
    esp_deep_sleep_start();
  }
  Serial.printf("\n Tempo corrido: %d", tempo++);
  delay(1000);
}
void loop2(void*z)
{
  Serial.printf("\nloop2() em core: %d", xPortGetCoreID());
  while (1)
  {
    digitalWrite(led2, !digitalRead(led2));
    delay(1000);
  }
}

          Na função esp_sleep_enable_ext0_wakeup (GPIO_NUM_12, int),  o GPIO_NUM_x determina o pino de leitura, e int o nível lógico que acionará o sistema, optando entre 1 ou 0 (HIGH e LOW). Com isso, o sistema irá despertar de acordo com o nível lógico do sensor, no caso da programação, será a leitura d nível lógico do pino digital D12 (GPIO_NUM_12, 0). No caso do teste aplicado a este exemplo, o estado lógico do sensor variava em zero sempre que entrava em contato com a água.

 

Despertar Automático

          Alguns projetos necessitam de avaliações contínuas, informando a leitura de sensores em determinados intervalos. Para economizar energia podemos optar por despertar o sistema em certo intervalo de tempo, permitindo que os núcleos poupem energia e apresentando informações posteriores armazenadas em RTC. 

          Com base no exemplo demonstrado anteriormente, a programação abaixo terá a função de despertar o sistema automaticamente a cada trinta segundos.

/* Projeto Curto Circuito – ESP32: Despertador ULP */

#include <esp_deepsleep.h>  /* Biblioteca ESP32 Deep Sleep */
#include "DHT.h"
RTC_DATA_ATTR int tempo;        /* Armazena os valores de tempo no RTC. */
RTC_DATA_ATTR int tempo2;       /* Armazena os valores de tempo2 no RTC. */
#define led2 2 /* Define o LED ao pino D02. */

void setup()
{
  Serial.begin(115200);         /* Taxa de transmissão 115200dBs */
  pinMode(led2, OUTPUT);        /* LED2 OUTPUT */
  Serial.printf("\nsetup() em core: %d", xPortGetCoreID());
  /* Tarefa "pled()" com prioridade 1, atribuída ao core */
  xTaskCreatePinnedToCore(pled, "pled", 8192, NULL, 1, NULL, 0);
  delay(1);
  /* Desperta o ESP32 a cada 30 segundos */
  esp_sleep_enable_timer_wakeup(30000000);
  tempo2 += 10;
}

void loop()
{
  if (tempo == tempo2)
  {
    delay(1);
    esp_deep_sleep_start(); /* ESP32 inicia modo Deep Sleep */
  }
  Serial.printf("\n Tempo corrido: %d", tempo++);
  delay(1000);  /* Realiza uma contagem a cada 1 segundo */
}
void pled(void*z)
{
  /* Mostra em qual núcleo o pled() será executado */
  Serial.printf("\nloop2() em core: %d", xPortGetCoreID());
  while (1) {
    digitalWrite(led2, !digitalRead(led2)); /* Pisca o led2 */
    delay(1000);
  }
}

          A função esp_sleep_enable_timer_wakeup() determina um intervalo para despertar o sistema, esse valor será interpretado em microssegundos (1 segundo  = 1.000000 microssegundos). Caso encontre certa dificuldade em trabalhar com esse tipo de medida de tempo, crie uma variável com a função de realizar a conversão de micro para segundo, por exemplo:

/* Desperta o ESP32 a cada 30 segundos */
 int segundos = 30;
 esp_sleep_enable_timer_wakeup(segundos*1000000);

          O valor da variável "segundos" será multiplicado por 1.000.000, com a finalidade de tranforma-lo em segundos, tornando assim a conversão mais automática e simples. Portanto, se o programador desejar ajustar o intervalo de tempo do despertar em 60 segundos, basta escrever o valor desejado na variável.

 

Considerações Finais

          Configurar o processamento de um sistema de acordo com as necessidades existentes, solucionar problemas com o consumo de energia elétrica desligando funções em desuso, armazenar informações  e manipular como bem entender, essas são apenas algumas das principais características presente no processamento Dual-Core de placas ESP32. A flexibilidade no funcionamento dos microprocessadores permite um ajuste personalizado e adepto as exigências de qualquer projeto.