Entendendo o Gerenciamento de Tarefas no ESP32

O ESP32 é um microcontrolador poderoso e versátil, amplamente utilizado em projetos de automação e Internet das Coisas (IoT). Uma de suas características mais notáveis é a capacidade de gerenciar múltiplas tarefas simultaneamente, graças ao sistema operacional em tempo real (RTOS) integrado. Neste artigo, exploraremos em profundidade o gerenciamento de tarefas no ESP32, entendendo como ele funciona e como podemos tirar proveito dessa funcionalidade em nossos projetos.

O que é Gerenciamento de Tarefas?🔗

Antes de mergulharmos no ESP32, é fundamental compreender o conceito de gerenciamento de tarefas. Em sistemas embarcados, o gerenciamento de tarefas refere-se à capacidade do sistema de executar múltiplas operações (tarefas) de forma aparentemente simultânea. Isso é especialmente relevante em aplicações que exigem a resposta a múltiplos eventos ou a execução de várias funções paralelamente.

Sistema Operacional em Tempo Real (RTOS)🔗

O ESP32 utiliza o FreeRTOS, um sistema operacional em tempo real, para gerenciar tarefas. Um RTOS é projetado para aplicações que requerem uma resposta imediata e previsível a eventos externos. Ele fornece mecanismos para escalonamento de tarefas, gerenciamento de recursos, comunicação entre tarefas e muito mais.

Por que o FreeRTOS?

O FreeRTOS é uma escolha popular devido à sua leveza, eficiência e ampla compatibilidade. Ele permite que desenvolvedores criem aplicações complexas sem se preocupar com o gerenciamento manual de multitarefas.

Arquitetura de Tarefas no ESP32🔗

No ESP32, as tarefas são unidades independentes de execução que o FreeRTOS gerencia. Cada tarefa possui seu próprio contexto, incluindo registradores e pilha, permitindo que funcione de forma isolada das demais.

Dual-Core do ESP32

O ESP32 possui dois núcleos de processamento: CORE 0 e CORE 1. O FreeRTOS pode agendar tarefas em ambos os núcleos, permitindo uma verdadeira execução paralela de tarefas.

Imagem ilustrando a arquitetura dual-core do ESP32

Criando Tarefas no FreeRTOS🔗

Para criar uma tarefa no FreeRTOS, utilizamos a função xTaskCreate(). Esta função exige que especifiquemos o nome da função que a tarefa irá executar, nome da tarefa, tamanho da pilha, parâmetro de entrada, prioridade e um manipulador para a tarefa.

Exemplo Prático

Vamos criar duas tarefas simples que piscam LEDs em intervalos diferentes.

Configuração do Hardware

  • LED1 conectado ao GPIO 2
  • LED2 conectado ao GPIO 4

Código

#include <Arduino.h>
#define LED1 2
#define LED2 4
void setup()
{
  pinMode(LED1, OUTPUT);
  pinMode(LED2, OUTPUT);
  // Cria a primeira tarefa
  xTaskCreate(
    TaskBlinkLED1,     // Função que implementa a tarefa
    "Blink LED1",      // Nome da tarefa
    1024,              // Tamanho da pilha em palavras
    NULL,              // Parâmetro para a tarefa
    1,                 // Prioridade da tarefa
    NULL               // Manipulador da tarefa
  );
  // Cria a segunda tarefa
  xTaskCreate(
    TaskBlinkLED2,
    "Blink LED2",
    1024,
    NULL,
    1,
    NULL
  );
}
void loop()
{
  // O loop principal permanece vazio
}
// Tarefa que pisca o LED1
void TaskBlinkLED1(void *pvParameters)
{
  (void) pvParameters;
  for (;;)
  {
    digitalWrite(LED1, HIGH);
    vTaskDelay(500 / portTICK_PERIOD_MS);
    digitalWrite(LED1, LOW);
    vTaskDelay(500 / portTICK_PERIOD_MS);
  }
}
// Tarefa que pisca o LED2
void TaskBlinkLED2(void *pvParameters)
{
  (void) pvParameters;
  for (;;)
  {
    digitalWrite(LED2, HIGH);
    vTaskDelay(1000 / portTICK_PERIOD_MS);
    digitalWrite(LED2, LOW);
    vTaskDelay(1000 / portTICK_PERIOD_MS);
  }
}

Explicação do Código

  • setup(): Configuramos os pinos dos LEDs como saídas e criamos duas tarefas com xTaskCreate().
  • loop(): Deixamos vazio, pois nossas tarefas estão sendo gerenciadas pelo FreeRTOS.
  • TaskBlinkLED1 e TaskBlinkLED2: Funções que implementam as tarefas para piscar os LEDs em diferentes intervalos.

Prioridades e Escalonamento de Tarefas🔗

Cada tarefa no FreeRTOS tem uma prioridade associada. O escalonador decide qual tarefa deve ser executada com base nessas prioridades.

Definindo Prioridades

No exemplo anterior, ambas as tarefas têm prioridade 1. Podemos alterar as prioridades para controlar o comportamento do sistema.

Exemplo com Prioridades Diferentes

Vamos modificar o exemplo para dar prioridade mais alta ao TaskBlinkLED1.

// Alterando as prioridades
xTaskCreate(
  TaskBlinkLED1,
  "Blink LED1",
  1024,
  NULL,
  2,   // Prioridade aumentada
  NULL
);
xTaskCreate(
  TaskBlinkLED2,
  "Blink LED2",
  1024,
  NULL,
  1,
  NULL
);

Impacto das Prioridades

Com essa mudança, o TaskBlinkLED1 terá preferência na execução. Isso significa que, se ambas as tarefas estiverem prontas para executar, o FreeRTOS dará preferência à tarefa com maior prioridade.

Comunicação Entre Tarefas🔗

Em sistemas complexos, é comum que tarefas precisem se comunicar. O FreeRTOS fornece mecanismos como Filas (Queues), Semáforos e Mutexes para facilitar essa comunicação e sincronização.

Uso de Filas

As filas permitem a passagem de dados entre tarefas de forma segura e organizada.

Exemplo Prático

Vamos criar um exemplo onde uma tarefa produz dados e outra consome.

#include <Arduino.h>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
QueueHandle_t queue;
void setup()
{
  Serial.begin(115200);
  // Cria a fila com capacidade para 10 inteiros
  queue = xQueueCreate(10, sizeof(int));
  // Cria as tarefas de produtor e consumidor
  xTaskCreate(TaskProducer, "Producer", 1024, NULL, 2, NULL);
  xTaskCreate(TaskConsumer, "Consumer", 1024, NULL, 1, NULL);
}
void loop()
{
  // O loop principal permanece vazio
}
void TaskProducer(void *pvParameters)
{
  int count = 0;
  for (;;)
  {
    // Envia o valor de count para a fila
    xQueueSend(queue, &count, portMAX_DELAY);
    Serial.println("Produziu: " + String(count));
    count++;
    vTaskDelay(1000 / portTICK_PERIOD_MS);
  }
}
void TaskConsumer(void *pvParameters)
{
  int receivedValue;
  for (;;)
  {
    // Recebe o valor da fila
    if (xQueueReceive(queue, &receivedValue, portMAX_DELAY))
    {
      Serial.println("Consumiu: " + String(receivedValue));
    }
  }
}

Explicação do Código

  • QueueHandle_t queue: Declaramos uma variável para a fila.
  • xQueueCreate(): Criamos a fila com capacidade para 10 inteiros.
  • TaskProducer: Tarefa que produz um valor inteiro e o envia para a fila.
  • TaskConsumer: Tarefa que recebe o valor da fila e o processa (neste caso, apenas imprime).

Sincronização de Tarefas🔗

Além da comunicação, às vezes é necessário sincronizar tarefas para evitar conflitos de recursos. O FreeRTOS oferece semáforos binários e contadores, além de mutexes.

Uso de Semáforos

Semáforos podem ser usados para sinalizar eventos entre tarefas.

Exemplo Prático

Vamos usar um semáforo para controlar o acesso a um recurso compartilhado.

#include <Arduino.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
SemaphoreHandle_t semaphore;
void setup()
{
  Serial.begin(115200);
  // Cria o semáforo binário
  semaphore = xSemaphoreCreateBinary();
  // Libera o semáforo inicialmente
  xSemaphoreGive(semaphore);
  // Cria as tarefas
  xTaskCreate(TaskAcessResource, "Task1", 1024, NULL, 1, NULL);
  xTaskCreate(TaskAcessResource, "Task2", 1024, NULL, 1, NULL);
}
void loop()
{
  // O loop principal permanece vazio
}
void TaskAcessResource(void *pvParameters)
{
  for (;;)
  {
    // Tenta pegar o semáforo
    if (xSemaphoreTake(semaphore, portMAX_DELAY))
    {
      // Acessa o recurso compartilhado
      Serial.println("Tarefa acessando o recurso");
      vTaskDelay(1000 / portTICK_PERIOD_MS);
      // Libera o semáforo
      xSemaphoreGive(semaphore);
    }
    vTaskDelay(500 / portTICK_PERIOD_MS);
  }
}

Explicação do Código

  • SemaphoreHandle_t semaphore: Declaramos uma variável para o semáforo.
  • xSemaphoreCreateBinary(): Criamos um semáforo binário.
  • xSemaphoreGive(): Liberamos o semáforo inicialmente.
  • TaskAcessResource: Ambas as tarefas tentam acessar o recurso protegido pelo semáforo.

Alocação de Tarefas aos Núcleos🔗

O FreeRTOS no ESP32 permite fixar tarefas em núcleos específicos ou deixar que o sistema agende automaticamente.

Fixando Tarefas em Núcleos

Para fixar uma tarefa a um núcleo, utilizamos a função xTaskCreatePinnedToCore().

Exemplo Prático

Vamos fixar uma tarefa em cada núcleo.

// Cria a primeira tarefa no núcleo 0
xTaskCreatePinnedToCore(
  TaskBlinkLED1,
  "Blink LED1",
  1024,
  NULL,
  1,
  NULL,
  0   // Núcleo 0
);
// Cria a segunda tarefa no núcleo 1
xTaskCreatePinnedToCore(
  TaskBlinkLED2,
  "Blink LED2",
  1024,
  NULL,
  1,
  NULL,
  1   // Núcleo 1
);

Benefícios de Fixar Tarefas

  • Determinismo: Garantimos que certas tarefas sempre rodarão no mesmo núcleo.
  • Performance: Podemos otimizar o uso dos núcleos para tarefas específicas.

Considerações sobre o Consumo de Recursos🔗

Cada tarefa criada consome recursos do sistema, como memória da pilha. É importante dimensionar corretamente o tamanho da pilha de cada tarefa para evitar estouro de memória.

Tamanho da Pilha

O tamanho da pilha é especificado em palavras (normalmente 4 bytes). Ao criar uma tarefa, é essencial estimar o quanto de memória ela necessitará.

Monitoramento de Recursos

Podemos utilizar funções do FreeRTOS para monitorar o uso de memória e garantir que não haja sobrecarga.

Resumo e Boas Práticas🔗

  • Planeje as Tarefas: Defina claramente o que cada tarefa irá fazer e suas interdependências.
  • Gerencie Prioridades com Cuidado: Prioridades inadequadas podem levar a tarefas sendo preteridas ou ao inverso, monopolizando o processador.
  • Use Mecanismos de Sincronização: Semáforos, mutexes e filas são essenciais para a comunicação segura entre tarefas.
  • Dimensione Corretamente os Recursos: Ajuste o tamanho da pilha e monitore o uso de memória.
  • Teste Extensivamente: Erros em sistemas multitarefa podem ser sutis. Teste em diferentes cenários.

Conclusão🔗

O gerenciamento de tarefas no ESP32, facilitado pelo FreeRTOS, é uma poderosa ferramenta que permite desenvolver aplicações complexas e responsivas. Compreender como criar, priorizar e sincronizar tarefas é fundamental para tirar o máximo proveito do ESP32 em projetos de automação e IoT. Esperamos que este artigo tenha fornecido uma base sólida para você começar a explorar as capacidades multitarefas deste microcontrolador incrível.

Este artigo faz parte do grupo Introdução ao ESP32: O que é e como funciona
Autor: Marcelo V. Souza - Engenheiro de Sistemas e Entusiasta em IoT e Desenvolvimento de Software, com foco em inovação tecnológica.

Referências🔗

Artigos Relacionados