ESP32: Aprenda a Magia do Dual Core

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

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 *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).

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()

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

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:

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:

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

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

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

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

  • 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

  • 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:

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:

  1. Task Watchdog Timer (TWDT): Associado ao FreeRTOS, monitora tarefas específicas para garantir que elas não fiquem bloqueadas ou inativas por muito tempo.
  2. 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

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

  • 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

  • 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:

Wylker Alves