ESP32: Aprenda a Magia do Dual Core

O ESP32 é um microcontrolador poderoso e versátil, amplamente utilizado em projetos de IoT e automação, como o galinheiro automático desenvolvido pelo PET Elétrica UFJF. Uma das características mais interessantes do ESP32 é o fato de ele possuir dois núcleos (dual core), o que permite executar tarefas diferentes de forma simultânea, otimizando o desempenho do sistema. Neste texto, vamos explorar o que isso significa, começando com um exemplo simples de como usar os dois núcleos e, em seguida, detalhando como o dual core foi aplicado no projeto do galinheiro automático.
O que é o Dual Core do ESP32?
O ESP32 possui dois núcleos de processamento, chamados Core 0 e Core 1, baseados na arquitetura Xtensa LX6. Esses núcleos podem trabalhar independentemente, o que é ideal para dividir tarefas em projetos que exigem multitarefa. O FreeRTOS, um sistema operacional de tempo real embutido no ESP32, gerencia essas tarefas, permitindo que você as atribua a um núcleo específico.
Imagine os núcleos como dois trabalhadores: cada um pode realizar uma tarefa diferente ao mesmo tempo, sem que um precise esperar o outro terminar. Isso é útil em projetos onde, por exemplo, você quer monitorar sensores enquanto controla um motor.
Exemplo Simples: Dois LEDs, Um em Cada Núcleo
Para entender como o dual core funciona, vamos começar com um exemplo básico: piscar dois LEDs, cada um controlado por um núcleo diferente.
Vamos conectar dois LEDs ao ESP32:
- LED1: Vai piscar rápido, ligado por 0,5 segundo e desligado por 0,5 segundo (1 piscada por segundo).
- LED2: Vai piscar mais devagar, ligado por 1 segundo e desligado por 1 segundo (1 piscada a cada 2 segundos).
Cada LED será controlado por um núcleo diferente, usando o FreeRTOS, que é o sistema que organiza as tarefas no ESP32. Vamos explicar o código por partes e ao final juntar tudo.
1. Definindo os Pinos dos LEDs
#define LED1 2 // LED1 conectado ao pino 2
#define LED2 4 // LED2 conectado ao pino 4
Aqui, estamos dizendo ao ESP32 em quais pinos os LEDs estão conectados. #define é como dar um apelido aos números dos pinos para facilitar a leitura.
2. Criando a Primeira Tarefa (Task1) para o LED1
O que é uma tarefa?
Uma tarefa é como um “trabalhador” que o FreeRTOS controla. Aqui, TaskLED1 é a tarefa que vai piscar o LED1.
void TaskLED1(void *pvParameters) {
pinMode(LED1, OUTPUT);
for (;;) {
digitalWrite(LED1, HIGH); // Liga o LED1
vTaskDelay(500 / portTICK_PERIOD_MS); // Espera 500ms
digitalWrite(LED1, LOW); // Desliga o LED1
vTaskDelay(500 / portTICK_PERIOD_MS); // Espera 500ms
}
}
void *pvParameters: Isso é um ponteiro, algo que o FreeRTOS exige, mas não vamos usar agora (é para passar informações extras para a tarefa, se precisar).
pinMode(LED1, OUTPUT): Configura o pino 2 como saída, porque vamos enviar energia para ligar o LED.
for(;;): Um loop infinito, ou seja, a tarefa nunca para (é normal em tarefas do FreeRTOS).
digitalWrite(LED1, HIGH): Liga o LED1 mandando energia para o pino 2.
vTaskDelay(500 / portTICK_PERIOD_MS): Espera 500 milissegundos (0,5 segundo). Usamos vTaskDelay em vez de delay() porque ele permite que o FreeRTOS gerencie outras tarefas enquanto espera.
digitalWrite(LED1, LOW): Desliga o LED1 cortando a energia.
3. Criando a Segunda Tarefa (Task2) para o LED2
Essa tarefa é quase igual à TaskLED1, mas controla o LED2 no pino 4 e usa tempos maiores (1000ms = 1 segundo).
void TaskLED2(void *pvParameters) {
pinMode(LED2, OUTPUT);
for (;;) {
digitalWrite(LED2, HIGH); // Liga o LED2
vTaskDelay(1000 / portTICK_PERIOD_MS); // Espera 1000ms
digitalWrite(LED2, LOW); // Desliga o LED2
vTaskDelay(1000 / portTICK_PERIOD_MS); // Espera 1000ms
}
}
pinMode(LED2, OUTPUT): Configura o pino 4 como saída.
digitalWrite(LED2, HIGH): Liga o LED2.
vTaskDelay(1000 / portTICK_PERIOD_MS): Espera 1 segundo.
digitalWrite(LED2, LOW): Desliga o LED2.
4. Configurando Tudo no setup()
void setup() {
Serial.begin(115200); // Inicia o monitor serial
xTaskCreatePinnedToCore ( // Cria as tarefas e fixa cada uma em um núcleo
TaskLED1, // Função da tarefa
"Task1", // Nome da tarefa
1000, // Tamanho da pilha da tarefa
NULL, // Parâmetro da tarefa
1, // Prioridade da tarefa
&Task1, // Handle da tarefa para acompanhar a tarefa criada
0); // Fixar a tarefa no núcleo 0
xTaskCreatePinnedToCore ( // Cria as tarefas e fixa cada uma em um núcleo
TaskLED2, // Função da tarefa
"Task2", // Nome da tarefa
1000, // Tamanho da pilha da tarefa
NULL, // Parâmetro da tarefa
1, // Prioridade da tarefa
&Task2, // Handle da tarefa para acompanhar a tarefa criada
1); // Fixar a tarefa no núcleo 1
}
xTaskCreatePinnedToCore: Essa função é o “chefe” que cria as tarefas e diz em qual núcleo elas vão rodar. Vamos dividir os argumentos:
- TaskLED1 ou TaskLED2: O nome da tarefa que criamos acima.
- “Task1” ou “Task2”: Um apelido para identificar a tarefa (útil para depuração).
- 1000: Quantidade de memória (em bytes) reservada para a tarefa. 1000 é suficiente para algo simples como piscar LEDs.
- NULL: Aqui iriam parâmetros extras para a tarefa, mas não precisamos agora.
- 1: Prioridade da tarefa (de 0 a 24, sendo 1 baixa). Ambas têm a mesma prioridade, então rodam normalmente.
- &Task1 ou &Task2: Um espaço para guardar um “identificador” da tarefa. (Neste código de exemplo não estamos utlizando)
- 0 ou 1: O núcleo onde a tarefa vai rodar (0 para Core 0, 1 para Core 1).
5. O loop() Vazio
void loop() {
// Nada aqui, o FreeRTOS cuida de tudo!
}
Por que está vazio?
Normalmente, em um código, você coloca o que quer repetir no loop(). Mas com o FreeRTOS, as tarefas (Task1 e Task2) cuidam de tudo. O loop() não é mais necessário, porque o FreeRTOS gerencia os dois núcleos sozinho.
Clique aqui para acessar o código completo:
#define LED1 2 // LED conectado ao pino 2
#define LED2 4 // LED conectado ao pino 4
void Task1(void *pvParameters) { // Tarefa para o Core 0
pinMode(LED1, OUTPUT);
for (;;) {
digitalWrite(LED1, HIGH);
vTaskDelay(500 / portTICK_PERIOD_MS); // 500ms ligado
digitalWrite(LED1, LOW);
vTaskDelay(500 / portTICK_PERIOD_MS); // 500ms desligado
}
}
void Task2(void *pvParameters) { // Tarefa para o Core 1
pinMode(LED2, OUTPUT);
for (;;) {
digitalWrite(LED2, HIGH);
vTaskDelay(1000 / portTICK_PERIOD_MS); // 1000ms ligado
digitalWrite(LED2, LOW);
vTaskDelay(1000 / portTICK_PERIOD_MS); // 1000ms desligado
}
}
void setup() {
Serial.begin(115200);
// Criação das tarefas, fixando cada uma em um núcleo
xTaskCreatePinnedToCore(Task1, "Task1", 1000, NULL, 1, NULL, 0); // Core 0
xTaskCreatePinnedToCore(Task2, "Task2", 1000, NULL, 1, NULL, 1); // Core 1
}
void loop() {
// Vazio, pois as tarefas são gerenciadas pelo FreeRTOS
}
Com esse exemplo, você já entende como usar os dois núcleos do ESP32 para tarefas simples. A partir daí, pode aplicar isso em projetos mais legais, como o Galinheiro Automático, que você verá na sequência deste texto.
Aplicando o Dual Core em Projetos: Galinheiro Automático
Agora, vamos analisar como o dual core foi utilizado no projeto do galinheiro automático do PET Elétrica, onde o dual core do ESP32 foi usado para criar um sistema mais avançado. O objetivo desse projeto é automatizar duas funções principais: contar os ovos coletados (usando sensores) e controlar a alimentação automática das galinhas (usando um motor de passo). O dual core permite que essas tarefas aconteçam ao mesmo tempo, sem que uma atrapalhe a outra.
Visão Geral do Projeto
No galinheiro automático:
- Core 0: Controla o motor de passo, que é responsável por liberar ração para as galinhas em intervalos regulares (a cada 1 minuto, no código).
- Core 1: Monitora dois sensores (um LDR para resetar o contador e um infravermelho para contar ovos) e exibe a quantidade de ovos em um display LCD.
Abaixo você encontra o código completo utilizado para o projeto e na sequência vamos detalhar cada parte para que você entenda o que foi feito.
Clique aqui para acessar o código completo do Galinheiro Automático:
#include <Stepper.h> // Biblioteca para controlar o motor de passo
#include <LiquidCrystal_I2C.h> // Biblioteca para o display LCD I2C
#include <esp_task_wdt.h> // Biblioteca para o Watchdog Timer do ESP32
#include <Wire.h> // Biblioteca para comunicação I2C
// Definição dos pinos dos sensores e do LCD
#define SENSOR_LDR 32 // Pino onde o sensor de luz (LDR) está conectado
#define SENSOR_INFRAVERMELHO 16 // Pino do sensor infravermelho para contar ovos
#define pinoSda 14 // Pino SDA para comunicação I2C com o LCD
#define pinoScl 27 // Pino SCL para comunicação I2C com o LCD
// Declaração dos identificadores das tarefas (handles) para o FreeRTOS
TaskHandle_t Motor_Step; // Handle da tarefa que controla o motor
TaskHandle_t Sensores_LDC; // Handle da tarefa que monitora os sensores
// Variáveis globais
volatile int contador = 0; // Contador de ovos (volátil porque é alterado por interrupção)
int contador_1 = 1; // Variável auxiliar para atualizar o LCD apenas quando necessário
unsigned int valorLdr; // Armazena o valor lido do sensor LDR
unsigned int valorSensorInfra; // Armazena o valor lido do sensor infravermelho (não usado diretamente aqui)
// Configuração do motor de passo
const int passosPorRevolucao = 2050; // Número de passos por volta completa do motor
unsigned int voltas = 1; // Quantidade de voltas que o motor dá por alimentação
// Objetos para o motor e o LCD
Stepper motorPasso(passosPorRevolucao, 19, 18, 5, 17); // Configura o motor nos pinos 19, 18, 5 e 17
LiquidCrystal_I2C lcd(0x27, 16, 2); // Configura o LCD I2C com endereço 0x27, 16 colunas e 2 linhas
// Variáveis de temporização
unsigned long tempoAnterior = 0; // Tempo da última alimentação (em milissegundos)
unsigned long tempoUltimaAcao = 0; // Tempo da última interrupção do sensor IR
const long intervalo = 60000; // Intervalo de 1 minuto (60.000ms) para a alimentação
const long intervaloEspera = 200; // Intervalo de 200ms para debounce da interrupção
// Função de configuração inicial
void setup() {
Serial.begin(115200); // Inicia a comunicação serial a 115200 baud para depuração
motorPasso.setSpeed(5); // Define a velocidade do motor de passo como 5 RPM
Wire.begin(pinoSda, pinoScl); // Inicia a comunicação I2C nos pinos SDA (14) e SCL (27)
lcd.init(); // Inicializa o display LCD
lcd.backlight(); // Liga a luz de fundo do LCD
// Configura os pinos dos sensores
pinMode(SENSOR_LDR, INPUT); // Configura o pino do LDR como entrada
pinMode(SENSOR_INFRAVERMELHO, INPUT_PULLUP); // Configura o sensor IR como entrada com resistor pull-up interno
// Configura a interrupção no sensor infravermelho
attachInterrupt(digitalPinToInterrupt(SENSOR_INFRAVERMELHO), tratarInterrupcao, FALLING); // Chama tratarInterrupcao quando o sinal cai
// Desativa o Watchdog Timer padrão do FreeRTOS
esp_task_wdt_deinit();
// Configura um novo WDT personalizado
esp_task_wdt_config_t wdt_config = {
.timeout_ms = 200000, // Timeout de 200 segundos (evita resets durante o movimento do motor)
.idle_core_mask = 0, // Não monitora núcleos ociosos
.trigger_panic = false // Não reinicia o ESP32 se o timeout for atingido
};
esp_task_wdt_init(&wdt_config); // Inicializa o WDT com as configurações acima
// Cria a tarefa do motor no Core 0
if (xTaskCreatePinnedToCore(
MotorStepCode, // Função da tarefa
"Motor_Step", // Nome da tarefa (para depuração)
4096, // Tamanho da pilha (em bytes)
NULL, // Parâmetros (não usados aqui)
1, // Prioridade (1 = baixa)
&Motor_Step, // Handle da tarefa
0) != pdPASS) { // Núcleo 0
Serial.println("Erro ao criar a tarefa Motor_Step!");
}
// Cria a tarefa dos sensores no Core 1
if (xTaskCreatePinnedToCore(
SensoresLDCCode, // Função da tarefa
"Sensores_LDC", // Nome da tarefa
4096, // Tamanho da pilha
NULL, // Parâmetros
1, // Prioridade
&Sensores_LDC, // Handle da tarefa
1) != pdPASS) { // Núcleo 1
Serial.println("Erro ao criar a tarefa Sensores_LDC!");
}
}
// Tarefa do Core 0: Controla o motor de passo para alimentação automática
void MotorStepCode(void *pvParameters) {
esp_task_wdt_add(NULL); // Adiciona esta tarefa ao Watchdog Timer
for (;;) { // Loop infinito da tarefa
unsigned long tempoAtual = millis(); // Pega o tempo atual em milissegundos
esp_task_wdt_reset(); // Reseta o WDT para indicar que a tarefa está ativa
vTaskDelay(pdMS_TO_TICKS(1000)); // Espera 1 segundo sem bloquear o núcleo
// Verifica se passou 1 minuto desde a última alimentação
if (tempoAtual - tempoAnterior >= intervalo) {
tempoAnterior = tempoAtual; // Atualiza o tempo da última alimentação
Serial.print("O motor dará "); // Mensagem de depuração
Serial.print(voltas);
Serial.println(" volta(s)");
motorPasso.step(voltas * passosPorRevolucao); // Gira o motor para liberar ração
Serial.println("Alimentação liberada!"); // Confirmação no monitor serial
}
}
}
// Tarefa do Core 1: Monitora os sensores e atualiza o LCD
void SensoresLDCCode(void *pvParameters) {
esp_task_wdt_add(NULL); // Adiciona esta tarefa ao Watchdog Timer
for (;;) { // Loop infinito da tarefa
valorLdr = analogRead(SENSOR_LDR); // Lê o valor do sensor de luz (0 a 4095)
Serial.println(contador); // Mostra o número de ovos no monitor serial
Serial.println(valorLdr); // Mostra o valor do LDR no monitor serial
vTaskDelay(pdMS_TO_TICKS(1000)); // Espera 1 segundo sem bloquear o núcleo
// Se há luz suficiente (valor < 2000), zera o contador de ovos
if (valorLdr < 2000) {
contador = 0; // Assume que os ovos foram coletados
}
// Atualiza o LCD apenas se o contador mudou
if (contador != contador_1) {
contador_1 = contador; // Sincroniza a variável auxiliar
lcd.clear(); // Limpa o display
lcd.setCursor(0, 0); // Posiciona o cursor na linha 1, coluna 1
lcd.print("Ovos coletados:"); // Mostra a mensagem fixa
lcd.setCursor(0, 1); // Posiciona o cursor na linha 2, coluna 1
lcd.print(contador); // Mostra o número de ovos
}
}
}
// Função de interrupção: Conta ovos quando o sensor infravermelho é acionado
void tratarInterrupcao() {
unsigned long tempoAtual = millis(); // Pega o tempo atual
// Debouncing: só conta se passaram pelo menos 200ms desde a última interrupção
if (tempoAtual - tempoUltimaAcao >= intervaloEspera) {
tempoUltimaAcao = tempoAtual; // Atualiza o tempo da última interrupção
contador++; // Incrementa o contador de ovos
}
// Verifica o LDR: se há luz forte (valor < 2000), zera o contador
valorLdr = analogRead(SENSOR_LDR);
if (valorLdr < 2000) {
contador = 0; // Assume que o galinheiro foi aberto e os ovos coletados
}
}
// Função principal de loop (vazia, pois o FreeRTOS gerencia as tarefas)
void loop() {
// Não é necessário código aqui, as tarefas rodam nos dois núcleos
}
1. Declarações Iniciais
Nesta parte do código, incluimos bibliotecas para motor de passo, LCD, Watchdog Timer e I2C, definimos pinos para sensores LDR e infravermelho e LCD, criamos as tarefas para o FreeRTOS, inicializamos o contador de ovos, variáveis de sensor, motor com 2050 passos por volta e LCD 16×2, e setamos as temporizações de 1 minuto para alimentação e 200ms de debounce, preparando o dual core para controlar alimentação e monitorar ovos ao mesmo tempo.
#include <Stepper.h>
#include <LiquidCrystal_I2C.h>
#include <esp_task_wdt.h>
#include <Wire.h>
#define SENSOR_LDR 32 // Pino do sensor LDR
#define SENSOR_INFRAVERMELHO 16 // Pino do sensor infravermelho
#define pinoSda 14 // Pino SDA para o LCD
#define pinoScl 27 // Pino SCL para o LCD
TaskHandle_t Motor_Step; // Identificador da tarefa do motor
TaskHandle_t Sensores_LDC; // Identificador da tarefa dos sensores
volatile int contador = 0; // Contador de ovos (volátil por causa da interrupção)
int contador_1 = 1; // Variável auxiliar para atualizar o LCD
unsigned int valorLdr, valorSensorInfra; // Leituras dos sensores
const int passosPorRevolucao = 2050; // Passos por volta do motor
unsigned int voltas = 1; // Quantas voltas o motor dá por alimentação
Stepper motorPasso(passosPorRevolucao, 19, 18, 5, 17); // Configura o motor nos pinos 19, 18, 5, 17
LiquidCrystal_I2C lcd(0x27, 16, 2); // Configura o LCD I2C
unsigned long tempoAnterior = 0; // Última vez que o motor girou
const long intervalo = 60000; // Intervalo de 1 minuto para alimentação
const long intervaloEspera = 200; // Tempo de debounce para o sensor IR
- #include: Bibliotecas para o motor de passo, LCD, Watchdog Timer e comunicação I2C.
- Pinos: Define onde os sensores e o LCD estão conectados.
- TaskHandle_t: Variáveis para identificar as tarefas no FreeRTOS.
- contador: Conta os ovos detectados pelo sensor infravermelho. É volatile porque é alterado por uma interrupção.
- passosPorRevolucao: Define quantos passos o motor precisa para uma volta completa (2050 no caso).
- voltas: Quantas voltas o motor dá para liberar ração (1 volta por vez).
- motorPasso: Configura o motor de passo nos pinos 19, 18, 5 e 17. Ele gira para abrir um dispensador de ração.
- lcd: Configura o display LCD I2C (endereço 0x27, 16 colunas, 2 linhas).
- intervalo: O motor gira a cada 60 segundos (1 minuto), simulando a alimentação periódica.
2. Configuração Inicial (setup())
Nesta parte do código, iniciamos o monitor serial a 115200 baud pra depuração, definimos a velocidade do motor de passo em 5 RPM pra liberar ração, começamos a comunicação I2C pros pinos SDA e SCL do LCD, inicializamos o LCD e ligamos sua luz de fundo, configuramos o sensor LDR como entrada e o infravermelho com pull-up interno pra evitar ruídos, adicionamos uma interrupção no sensor infravermelho pra contar ovos quando o sinal cair, desativamos o Watchdog Timer padrão, criamos um novo WDT com timeout de 200 segundos e sem reset pra não interromper o motor, e lançamos duas tarefas no FreeRTOS: uma pro motor no Core 0 e outra pros sensores no Core 1, tudo pra rodar alimentação e contagem ao mesmo tempo.
void setup() {
Serial.begin(115200); // Inicia o monitor serial
motorPasso.setSpeed(5); // Define a velocidade do motor como 5 RPM
Wire.begin(pinoSda, pinoScl); // Inicia a comunicação I2C
lcd.init(); // Inicializa o LCD
lcd.backlight(); // Liga a luz de fundo do LCD
pinMode(SENSOR_LDR, INPUT); // Configura o LDR como entrada
pinMode(SENSOR_INFRAVERMELHO, INPUT_PULLUP); // Configura o sensor IR com pull-up
attachInterrupt(digitalPinToInterrupt(SENSOR_INFRAVERMELHO), tratarInterrupcao, FALLING); // Interrupção no sensor IR
esp_task_wdt_deinit(); // Desativa o WDT padrão
esp_task_wdt_config_t wdt_config = {
.timeout_ms = 200000, // 200 segundos de timeout
.idle_core_mask = 0, // Não monitora núcleos ociosos
.trigger_panic = false // Não reinicia o ESP32
};
esp_task_wdt_init(&wdt_config); // Inicializa o WDT personalizado
xTaskCreatePinnedToCore(MotorStepCode, "Motor_Step", 4096, NULL, 1, &Motor_Step, 0); // Tarefa do motor no Core 0
xTaskCreatePinnedToCore(SensoresLDCCode, "Sensores_LDC", 4096, NULL, 1, &Sensores_LDC, 1); // Tarefa dos sensores no Core 1
}
- motorPasso.setSpeed(5): Define a velocidade do motor em 5 rotações por minuto. Isso controla o ritmo da alimentação.
- Wire.begin: Inicia a comunicação I2C para o LCD nos pinos 14 (SDA) e 27 (SCL).
- lcd.init() e lcd.backlight(): Prepara o LCD e acende sua luz de fundo.
- pinMode: Configura os sensores como entradas. O sensor infravermelho usa INPUT_PULLUP para evitar ruídos.
- attachInterrupt: Liga uma interrupção ao sensor infravermelho. Quando ele detecta algo (sinal caindo, FALLING), chama tratarInterrupcao.
- Watchdog Timer (WDT): Desativa o WDT padrão e cria um novo com 200 segundos de timeout e sem reset, para evitar que o ESP32 reinicie durante o movimento do motor.
- xTaskCreatePinnedToCore: Cria duas tarefas:
- MotorStepCode no Core 0: Controla o motor para alimentação.
- SensoresLDCCode no Core 1: Lê os sensores e atualiza o LCD.
3. Tarefa do Motor (MotorStepCode) – Alimentação Automática
Nesta parte do código, criamos a tarefa MotorStepCode que roda no Core 0, adicionamos ela ao Watchdog Timer pra ser monitorada, entramos num loop infinito onde pegamos o tempo atual com millis(), resetamos o WDT pra mostrar que a tarefa tá ativa, esperamos 1 segundo sem travar o núcleo com vTaskDelay, e verificamos se passou 1 minuto desde a última alimentação comparando o tempo atual com o anterior; se sim, atualizamos o tempo, mostramos no monitor serial quantas voltas o motor vai dar, giramos o motor de passo com o número de voltas vezes os passos por revolução pra liberar ração, e confirmamos no serial que a alimentação foi liberada.
void MotorStepCode(void *pvParameters) {
esp_task_wdt_add(NULL); // Adiciona a tarefa ao WDT
for (;;) {
unsigned long tempoAtual = millis(); // Pega o tempo atual
esp_task_wdt_reset(); // Reseta o WDT para evitar timeout
vTaskDelay(pdMS_TO_TICKS(1000)); // Espera 1 segundo entre verificações
if (tempoAtual - tempoAnterior >= intervalo) { // A cada 1 minuto
tempoAnterior = tempoAtual; // Atualiza o tempo
Serial.print("O motor dará ");
Serial.print(voltas);
Serial.println(" volta(s)");
motorPasso.step(voltas * passosPorRevolucao); // Gira o motor
Serial.println("Alimentação liberada!");
}
}
}
- esp_task_wdt_add(NULL): Registra a tarefa no WDT personalizado.
- millis(): Verifica o tempo atual em milissegundos.
- esp_task_wdt_reset(): Diz ao WDT que a tarefa está ativa, evitando problemas.
- vTaskDelay(1000): Espera 1 segundo entre cada verificação, sem bloquear o núcleo.
- if (tempoAtual – tempoAnterior >= intervalo): A cada 60 segundos (1 minuto), executa a alimentação.
- motorPasso.step(voltas * passosPorRevolucao): Gira o motor 1 volta (2050 passos), para liberar a ração.
- Serial.println: Mostra mensagens no monitor serial para depuração (ex.: “Alimentação liberada!”).
4. Tarefa dos Sensores (SensoresLDCCode) – Contagem de Ovos
Nesta parte do código, definimos a tarefa SensoresLDCCode que roda no Core 1, adicionamos ela ao Watchdog Timer pra monitoramento, entramos num loop infinito onde lemos o valor do sensor de luz LDR com analogRead, mostramos o contador de ovos e o valor do LDR no monitor serial pra depuração, esperamos 1 segundo com vTaskDelay pra não travar o núcleo, zeramos o contador de ovos se o valor do LDR for menor que 2000 indicando luz suficiente, e, se o contador mudar em relação à variável auxiliar, atualizamos essa variável, limpamos o LCD, posicionamos o cursor na linha 1 pra exibir “Ovos coletados:”, mudamos pra linha 2 e mostramos o número atual de ovos, mantendo o display sempre atualizado.
void SensoresLDCCode(void *pvParameters) {
esp_task_wdt_add(NULL); // Adiciona a tarefa ao WDT
for (;;) {
valorLdr = analogRead(SENSOR_LDR); // Lê o sensor de luz
Serial.println(contador); // Mostra o contador de ovos
Serial.println(valorLdr); // Mostra o valor do LDR
vTaskDelay(pdMS_TO_TICKS(1000)); // Espera 1 segundo
if (valorLdr < 2000) { // Se há luz (valor baixo), zera o contador
contador = 0;
}
if (contador != contador_1) { // Se o contador mudou, atualiza o LCD
contador_1 = contador; // Sincroniza a variável auxiliar
lcd.clear(); // Limpa o LCD
lcd.setCursor(0, 0); // Posiciona na linha 1
lcd.print("Ovos coletados:"); // Mostra a mensagem
lcd.setCursor(0, 1); // Posiciona na linha 2
lcd.print(contador); // Mostra o número de ovos
}
}
}
- valorLdr = analogRead(SENSOR_LDR): Lê o sensor de luz (valores de 0 a 4095). Quanto mais luz, menor o valor.
- if (valorLdr < 2000): Se há luz suficiente (ex.: recipiente dos ovos foi retirado de cima do ldr), zera o contador, assumindo que os ovos foram coletados.
- if (contador != contador_1): Só atualiza o LCD se o número de ovos mudou, evitando piscadas desnecessárias.
- lcd.clear() e lcd.print: Limpa o display e mostra “Ovos coletados:” na linha 1 e o número de ovos na linha 2.
5. Interrupção (tratarInterrupcao) – Detectando Ovos
Nesta parte do código, criamos a função tratarInterrupcao que é chamada quando o sensor infravermelho detecta um ovo, pegamos o tempo atual com millis(), verificamos se passou pelo menos 200ms desde a última ação pra evitar contagens duplicadas por ruído com o debounce, atualizamos o tempo da última ação e incrementamos o contador de ovos se o intervalo for respeitado, lemos o valor do sensor LDR com analogRead, e zeramos o contador se o valor for menor que 2000, indicando que há luz suficiente e os ovos provavelmente foram coletados, garantindo que a contagem seja precisa e reinicie quando necessário.
void tratarInterrupcao() {
unsigned long tempoAtual = millis();
if (tempoAtual - tempoUltimaAcao >= intervaloEspera) { // Debouncing
tempoUltimaAcao = tempoAtual; // Atualiza o tempo
contador++; // Incrementa o contador de ovos
}
valorLdr = analogRead(SENSOR_LDR); // Lê o LDR
if (valorLdr < 2000) { // Se há luz, zera o contador
contador = 0;
}
}
- if (tempoAtual – tempoUltimaAcao >= intervaloEspera): Usa um atraso de 200ms (debouncing) para evitar contagens duplicadas caso o ovo passe devagar pelo sensor ou algum outro motivo.
- contador++: Incrementa o contador quando um ovo passa pelo sensor IR.
- if (valorLdr < 2000): Se houver luz sob o sensor ldr, zera o contador.
6. O loop() Vazio
void loop() {
// Vazio, pois o FreeRTOS gerencia as tarefas
}
- O FreeRTOS assume o controle, rodando MotorStepCode no Core 0 e SensoresLDCCode no Core 1, sem precisar de código no loop().
Desafios ao Longo do Projeto:
Durante o andamento do projeto, encontramos um problema: o ESP32 reiniciava sempre que o motor terminava de girar, causando a perda dos dados coletados pelos sensores. O erro exibido no Serial Monitor era o seguinte:
TasE (10179) task_wdt: Task watchdog got triggered. The following tasks did not reset the watchdog in time:
E (10179) task_wdt: - IDLE0 (CPU 0)
E (10179) task_wdt: Tasks currently running:
E (10179) task_wdt: CPU 0: Motor_Step
E (10179) task_wdt: CPU 1: Sensores_LDC
Diante desse cenário, realizamos algumas pesquisas para tentar compreender o que estava ocorrendo e como poderíamos resolver o problema. Conseguimos solucionar a questão utilizando o WDT (Watchdog Timer), que vamos explicar a seguir.
O que é o Watchdog Timer (WDT)?
O Watchdog Timer é um temporizador de hardware embutido no microcontrolador que funciona como um “cão de guarda” para monitorar a execução do programa. Ele é projetado para detectar e corrigir situações em que o sistema trava ou entra em um estado inesperado, como um loop infinito ou uma falha de software. O WDT conta continuamente até um valor limite predefinido; se esse limite for atingido sem que o programa “resete” o temporizador, o WDT assume que algo deu errado e reinicia o microcontrolador automaticamente.
Pense no WDT como um alarme de segurança: você precisa dizer a ele regularmente “estou funcionando normalmente” (resetando o temporizador). Se ele não receber essa confirmação dentro do prazo, ele “late” e força um reset para tentar restaurar o funcionamento do sistema.
No ESP32, existem dois tipos principais de WDT:
- Task Watchdog Timer (TWDT): Associado ao FreeRTOS, monitora tarefas específicas para garantir que elas não fiquem bloqueadas ou inativas por muito tempo.
- Interrupt Watchdog Timer (IWDT): Um mecanismo de hardware mais básico que protege o sistema como um todo.
Para que Serve o WDT?
O WDT é útil em sistemas embarcados por vários motivos:
- Prevenir travamentos: Se uma tarefa entra em um loop infinito ou o programa para de responder (por exemplo, devido a um bug ou falha de hardware), o WDT reinicia o sistema automaticamente.
- Garantir confiabilidade: Em aplicações críticas, como automação industrial ou dispositivos médicos, o WDT evita que o sistema fique em um estado inoperante.
- Monitoramento de tarefas: No contexto do FreeRTOS, o TWDT pode monitorar tarefas individuais, garantindo que cada uma esteja ativa e funcionando dentro do tempo esperado.
No caso do ESP32, o TWDT é habilitado por padrão no FreeRTOS e configurado para reiniciar o microcontrolador se uma tarefa não “reportar” seu funcionamento dentro de um intervalo padrão (geralmente 5 segundos).
O Problema no Projeto do Galinheiro Automático
No projeto do galinheiro automático, observaramos que o ESP32 resetava completamente toda vez que o motor de passo terminava de girar. Vamos entender por quê:
- Motor de Passo e Bloqueio: O método motorPasso.step(voltas * passosPorRevolucao) no MotorStepCode executa o movimento do motor de passo de forma síncrona, ou seja, ele bloqueia a tarefa enquanto os passos são executados. Dependendo do número de voltas (voltas) e da velocidade do motor (5 RPM), essa operação pode levar vários segundos.
- TWDT Padrão: O TWDT padrão do FreeRTOS, configurado com um timeout de cerca de 5 segundos, espera que cada tarefa chame esp_task_wdt_reset() regularmente para indicar que está ativa. Durante o movimento do motor, a tarefa MotorStepCode fica “ocupada” executando os passos e não reseta o WDT. Se o tempo de execução ultrapassa o timeout padrão, o TWDT dispara e reinicia o ESP32.
Esse reset indesejado interrompia o funcionamento do galinheiro, zerando o contador de ovos e parando o monitoramento dos sensores.
Por que e Como o WDT foi Reconfigurado?
Para resolver esse problema, nós desativamos o WDT padrão e implementamos uma configuração personalizada que não reinicia o ESP32. Vamos explicar cada etapa:
1. Desativação do WDT Padrão
esp_task_wdt_deinit();
O WDT padrão, com seu timeout curto e comportamento de reiniciar o sistema, não era adequado para o projeto. A execução prolongada do motor de passo excedia o limite padrão, causando resets. Essa função desativa completamente o WDT padrão, permitindo que nós pudessemos definir um novo comportamento.
2. Configuração Personalizada do WDT
esp_task_wdt_config_t wdt_config = {
.timeout_ms = 200000, // 200 segundos de timeout
.idle_core_mask = 0, // Não monitora núcleos ociosos
.trigger_panic = false // NÃO reinicia o ESP32 em caso de timeout
};
esp_task_wdt_init(&wdt_config); // Inicializa o WDT com a configuração
- timeout_ms = 200000: Define um timeout muito maior (200 segundos, ou cerca de 3 minutos e 20 segundos), garantindo que o movimento do motor, mesmo em cenários mais longos, não dispare o WDT.
- idle_core_mask = 0: Desativa o monitoramento de núcleos ociosos, focando apenas nas tarefas ativas.
- trigger_panic = false: Essa é a mudança mais importante! Ao desativar o “pânico” (reset do sistema), o WDT não reinicia o ESP32, mesmo que o timeout seja atingido. Em vez disso, ele apenas sinaliza o problema, permitindo que o programa continue rodando.
Essa configuração torna o WDT um monitor passivo, útil para depuração (nós podemos verificar se algo está errado), mas sem interferir no funcionamento do galinheiro.
3. Integração nas Tarefas
void MotorStepCode(void *pvParameters) {
esp_task_wdt_add(NULL); // Adiciona a tarefa ao WDT
for (;;) {
esp_task_wdt_reset(); // Reseta o WDT
// Código do motor...
}
}
void SensoresLDCCode(void *pvParameters) {
esp_task_wdt_add(NULL); // Adiciona a tarefa ao WDT
for (;;) {
esp_task_wdt_reset(); // Reseta o WDT
// Código dos sensores...
}
}
- esp_task_wdt_add(NULL): Registra cada tarefa no novo WDT, para que elas sejam monitoradas.
- esp_task_wdt_reset(): Reseta o temporizador do WDT a cada iteração do loop, indicando que a tarefa está funcionando. Como o timeout foi aumentado para 200 segundos e o reset foi desativado, o sistema permanece estável mesmo durante o movimento do motor.
O Watchdog Timer é uma ferramenta poderosa para garantir a estabilidade de sistemas embarcados, mas sua configuração padrão pode ser inadequada para projetos específicos.
Conclusão
O projeto do galinheiro automático desenvolvido pelo PET Elétrica UFJF exemplifica como o dual core do ESP32 pode ser uma solução eficiente para sistemas de automação que exigem multitarefa. A divisão das tarefas entre o Core 0, responsável pelo controle do motor de passo para a alimentação automática, e o Core 1, dedicado ao monitoramento de sensores e atualização do LCD, demonstra a capacidade do microcontrolador de executar processos simultâneos sem comprometer o desempenho. O exemplo simples dos LEDs piscando em núcleos distintos introduz de forma didática o conceito de multitarefa, enquanto o galinheiro automático eleva essa aplicação a um nível prático e funcional, mostrando como o FreeRTOS gerencia os recursos do ESP32 de maneira robusta.
Além disso, o enfrentamento do desafio com o Watchdog Timer (WDT) destaca a importância de adaptar as configurações do sistema às necessidades específicas do projeto. A reconfiguração do WDT, com um timeout estendido e a desativação do reset automático, resolveu o problema dos reinícios indesejados durante o movimento do motor, garantindo a continuidade da contagem de ovos e a estabilidade do sistema. Assim, o uso do dual core, aliado a ajustes como esse, não apenas otimizou o galinheiro automático, mas também oferece um modelo valioso para outros projetos de automação, evidenciando o potencial do ESP32 em aplicações embarcadas inovadoras.
Leia Mais em:
- https://www.crescerengenharia.com/post/usando-o-dual-core-do-esp32-processamento-paralelo
- https://oseiasfarias.medium.com/como-usar-o-dual-core-do-esp32-programando-no-platformio-6e12d73cea89
- https://elcereza.com/multiprocessamento-esp32-como-usar-corretamente/
- https://embarcados.com.br/esp32-lidando-com-multiprocessamento-parte-i/
- https://embarcados.com.br/esp32-lidando-com-multiprocessamento-parte-ii/
- https://embarcados.com.br/esp32-watchdog-timer/