Multitarefa em ESP32 com Arduino e FreeRTOS

Tempo de leitura: 5 minutes

Até agora, não é nenhum segredo que o ESP32 é meu chip favorito para fazer dispositivos IoT. Eles são pequenos, poderosos, têm uma tonelada de recursos integrados e são relativamente fáceis de programar.

No entanto, ao usá-lo junto com o Arduino, todo o seu código é executado em um único núcleo. Isso parece um desperdício, então vamos mudar isso usando o FreeRTOS para agendar tarefas em ambos os núcleos.

 

Por quê?

Existem vários casos de uso para querer multitarefa em um microcontrolador. Por exemplo: você pode ter um microcontrolador que lê um sensor de temperatura, mostra-o em um LCD e o envia para a nuvem.

Você pode fazer todos os três de forma síncrona, um após o outro. Mas e se você estiver usando uma tela e-ink que leva alguns segundos para atualizar?

Felizmente, a implementação do Arduino para o ESP32 inclui a possibilidade de agendar tarefas com o FreeRTOS. Eles podem ser executados em um único núcleo, muitos núcleos e você pode até definir o que é mais importante e deve receber tratamento preferencial.

 

Criação de tarefas

Para agendar uma tarefa, você deve fazer duas coisas: criar uma função que contenha o código que deseja executar e, em seguida, criar uma tarefa que chame essa função.

Digamos que eu queira acender e apagar um LED continuamente.

Primeiro, vou definir o pino ao qual o LED está conectado e definir seu modo para OUTPUT. Coisas muito comuns do Arduino:

const int led1 = 2; // Pino do LED

void setup(){
  pinMode(led1, OUTPUT);
}

A seguir, criarei uma função que se tornará a base da tarefa. Eu uso digitalWrite() para ligar e desligar o LED e uso vTaskDelay (em vez de delay()) para pausar a tarefa 500ms entre os estados alterados:

void toggleLED(void * parameter){
  for(;;){ // Loop infinito

    // Ligue o LED
    digitalWrite(led1, HIGH);

    // Ligue o LED
    vTaskDelay(500 / portTICK_PERIOD_MS);

    // Desligue o LED
    digitalWrite(led1, LOW);

    // Pause a tarefa novamente por 500ms
    vTaskDelay(500 / portTICK_PERIOD_MS);
  }
}

Essa é sua primeira tarefa! Algumas coisas a serem observadas:

Sim, criamos um loop for infinito (;;) e isso pode parecer um pouco estranho. Como podemos realizar várias tarefas se escrevermos uma tarefa que dura para sempre? O truque é vTaskDelay, que diz ao planejador que essa tarefa não deve ser executada por um determinado período. O agendador pausará o loop for e executará outras tarefas (se houver).

Por último, mas não menos importante, temos que informar o planejador sobre nossa tarefa. Podemos fazer isso na função setup():

void setup() {
  xTaskCreate(
    toggleLED,    // Função que deve ser chamada
    "Toggle LED",   // Nome da tarefa (para depuração)
    1000,            // Nome da tarefa (para depuração)
    NULL,            // Parâmetro para passar
    1,               // Prioridade da tarefa
    NULL             // Identificador de tarefa
  );
}

É isso aí! Quer piscar outro LED em um intervalo diferente? Basta criar outra tarefa e sentar enquanto o planejador cuida da execução de ambas.

Criação de uma tarefa única

Você também pode criar tarefas que são executadas apenas uma vez. Por exemplo, meu monitor de energia cria uma tarefa para fazer upload de dados para a nuvem quando tiver leituras suficientes.

Tarefas únicas não precisam de um loop for sem fim; em vez disso, tem a seguinte aparência:

void uploadToAWS(void * parameter){
    // Implemente sua lógica personalizada aqui

    // Quando terminar, chame vTaskDelete. Não se esqueça disso!
    vTaskDelete(NULL);
}

Isso se parece com uma função C ++ normal, exceto para o vTaskDelete(). Depois de chamá-lo, o FreeRTOS sabe que a tarefa foi concluída e não deve ser reprogramada. (Nota: não se esqueça de chamar esta função, ou isso fará com que o watchdog reinicie o ESP32).

xTaskCreate(
    uploadToAWS,    // Função que deve ser chamada
    "Upload to AWS",  // Nome da tarefa (para depuração)
    1000,            // Tamanho da pilha (bytes)
    NULL,            // Parâmetro para passar
    1,               // Prioridade da tarefa
    NULL             // Identificador de tarefa
);

 

 

Escolha em qual núcleo executar

Quando você usa xTaskCreate(), o agendador é livre para escolher em qual núcleo ele executa sua tarefa. Na minha opinião, esta é a solução mais flexível (você nunca sabe quando um chip IoT quad-core pode aparecer, certo?)

No entanto, é possível fixar uma tarefa em um núcleo específico com xTaskCreatePinnedToCore. É como xTaskCreate e leva um parâmetro adicional, o núcleo no qual você deseja executar a tarefa:

xTaskCreatePinnedToCore(
    uploadToAWS,      // Função que deve ser chamada
    "Upload to AWS",    // Nome da tarefa (para depuração)
    1000,               // Tamanho da pilha (bytes)
    NULL,               // Parâmetro para passar
    1,                  // Prioridade da tarefa
    NULL,               // Identificador de tarefa
    0,         // Core em que você deseja executar a tarefa (0 ou 1)
);

 

Verifique em qual núcleo você está executando

A maioria das placas ESP32 tem processadores dual-core, então como saber em qual núcleo sua tarefa está sendo executada?

Basta chamar xPortGetCoreID() de dentro de sua tarefa:

void exampleTask(void * parameter){
  Serial.print("A tarefa está sendo executada em:");
  Serial.println(xPortGetCoreID());
  vTaskDelay(100 / portTICK_PERIOD_MS);
}

Quando você tiver tarefas suficientes, o planejador começará a despachá-las para ambos os núcleos.

 

Parando tarefas

Agora, e se você adicionou uma tarefa ao agendador, mas deseja interrompê-la? Duas opções: você exclui a tarefa de dentro dela mesma ou usa um identificador de tarefa. Terminar uma tarefa de dentro já foi discutido antes (use vTaskDelete).

Para interromper uma tarefa de outro lugar (como outra tarefa ou seu loop principal), temos que armazenar um identificador de tarefa:

// Este TaskHandle permitirá
TaskHandle_t task1Handle = NULL;

void task1(void * parameter){
  // sua lógica de tarefa
}

xTaskCreate(
    task1,
    "Task 1",
    1000,
    NULL,
    1,
    task1Handle            // Identificador de tarefa
);

 

Bastava definir o identificador e passá-lo como último parâmetro de xTaskCreate. Agora podemos eliminá-lo com vTaskDelete:

void anotherTask(void * parameter){
  // Mata a tarefa 1 se ela estiver em execução
  if(task1Handle != NULL) {
    vTaskDelete(task1Handle);
  }
}

Prioridade da tarefa

Ao criar tarefas, devemos dar-lhe uma prioridade. É o quinto parâmetro de xTaskCreate. As prioridades são importantes quando duas ou mais tarefas estão competindo por tempo de CPU. Quando isso acontecer, o planejador executará primeiro a tarefa de prioridade mais alta. Faz sentido!

No FreeRTOS, um número de prioridade mais alta significa que uma tarefa é mais importante. Achei isso um tanto contra-intuitivo porque para mim uma “prioridade 1” parece mais importante do que uma “prioridade 2”, mas isso sou só eu.

Quando duas tarefas compartilham a mesma prioridade, o FreeRTOS compartilhará o tempo de processamento disponível entre elas.

Cada tarefa pode ter uma prioridade entre 0 e 24. O limite superior é definido por configMAX_PRIORITIES no arquivo FreeRTOSConfig.h.

Eu uso isso para diferenciar as tarefas primárias das secundárias. Leve meu medidor de energia para casa – a tarefa de maior prioridade é medir a eletricidade (prioridade 3). Atualizar a exibição ou sincronizar o tempo com um servidor NTP não é tão crítico para sua funcionalidade principal (prioridade 2).

Você não precisa de tarefas para realizar várias tarefas ao mesmo tempo

Apenas uma observação rápida antes de encerrar este post: você não precisa do FreeRTOS ou de um microcontrolador multicore para fazer várias coisas ao mesmo tempo.

Existem muitos tutoriais online sobre como você pode usar millis() para realizar a mesma coisa em sua função loop(). Outra solução seria usar um agendador de tarefas Arduino como o TaskScheduler. Ele roda em qualquer placa compatível com o Arduino, incluindo aquelas que não têm um processador multicore.

Segue o Site completo de TaskSheduler (para Arduino, Esp32, Esp8266, STM32)

E se desejar mais informações sobre FreeRTOS veja no Tutorial da Embarcados (Link)

Mas isso está além do escopo deste artigo. Vou ficar com o ESP32 por enquanto!

Feliz multitarefa!