Entendendo o Gerenciamento de Tarefas no ESP32
Marcelo V. Souza
425ms 914us
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.
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.
Tags