Manual

do

Maker

.

com

Selecionar uma CPU para executar tasks com ESP32

Selecionar uma CPU para executar tasks com ESP32

No artigo anterior discorri a respeito da execução de tarefas em um núcleo do ESP32, gerenciado pelo próprio sistema operacional. Dessa vez vamos ver como selecionar o núcleo que executará a tarefa. Dessa vez vamos ver como selecionar uma CPU para executar tasks com o ESP32. Recomendo a compra na CurtoCircuito, que vem entrando gradativamente no mundo de IoT.

Porque selecionar uma CPU?

Bem, você pode dedicar uma CPU às tarefas de gerenciamento dos GPIO e recepção de mensagens por RF, WiFi, bluetooth, serial, leitura dos sensores e afins, enquanto a outra CPU fica totalmente encarregada de processar o que for recebido. Ou ainda, processar tudo na memsa CPU e deixar a outra CPU rodando um webserver. Claro que isso demandará uma boa programação, porque você precisará utilizar um recurso parecido com mutex para acessar as variáveis compartilhadas.

Estou iniciando minhas interações com o FreeRTOS, portanto não conheço nem metade dos recursos oferecidos e, conforme for aprendendo, vou escrevendo artigos que me servirão de referência para futuras aplicações, por isso o formato desses artigos serão simples e objetivos, no intuito de não deixar margem para enganos.

Em inglês "pin" pode ser prego, cravilha, alfinete e co-relacionados. Lembre-se disso se você tiver um nível de inglês fraco como o meu, pois isso auxiliará a lembrar o nome do método que atribui uma tarefa à uma determinada CPU, pois iremos atrelar, ou, "pregar" uma tarefa à uma CPU. O nome do método utilizado é xTaskCreatePinnedToCore, referenciado nesse link do ESP_IDF.

Como ver a prioridade de uma tarefa?

Como visto no post anterior, podemos ver em qual CPU uma tarefa está sendo executada, incluindo sua prioridade, mas temos também a opção de pegar apenas a prioridade de uma tarefa que sabemos onde está sendo executada. Podemos passar como parâmetro a tarefa que queremos obter informação para a função uxTaskPriorityGet(TaskHandle_t xTask), ou se quisermos obter a prioridade da atual tarefa onde estamos chamando essa função, simplesmente passar NULL como parâmetro de função.

Mais uma vez, vamos iniciar pegando a prioridade da função setup() para exemplificar. Seria algo como:

setup(){
    Serial.begin(115200);
    delay(1000);
    ...
    Serial.print("Prioridade do setup(): ");
    Serial.println(uxTaskPriorityGet(NULL));
    ...
}

Para criar um handle para uma tarefa que estejamos criando, devemos utilizar o tipo supracitado:

TaskHandle_t dobitaobyte;

Daí criamos uma nova tarefa e passamos o endereço do nosso handle como parâmetro da função da tarefa, mesmo a xTaskCreate:

xTaskCreate(bbTask,"bbTask",10000,NULL,10,&dobitaobyte);

A função xTaskCreate foi exemplificada no post anterior. Aqui só mudou a prioridade (10) e dessa vez passamos o endereço de uma variável que armazenará a prioridade (&dobitaobyte). Quando for exibir a prioridade, basta passar a variável que criamos como parâmetro da função:

setup(){
    Serial.begin(115200);
    delay(1000);
    ...
    Serial.print("Prioridade do setup(): ");
    Serial.println(uxTaskPriorityGet(dobitaobyte));
    ...
}

Já sabemos que passando NULL como parâmetro, pegamos a prioridade da tarefa em que a função está sendo chamada, portanto para pegar a prioridade da função loop() podemos simplesmente adicionar a chamada da função uxTaskPriorityGet passando NULL como parâmetro:

void loop(){
    Serial.print("prioridade do loop(): ");
    Serial.println(uxTaskPriorityGet(NULL));
    delay(1000);
    ...
}

E para completar o código, não podemos nos esquecer de criar a função da tarefa (exemplificada como bbTask):

void bbTask(void * parameter){
    delay(20000);
    vTaskDelete(NULL);
}

Já sabemos criar uma tarefa gerenciada pelo FreeRTOS, aqui temos o exemplo disso como no post passado, apenas para mostrar o recurso de consultar a prioridade de uma tarefa sem ter que reservar muitos recursos, como quando utilizando a função de debugging vTaskList, que desabilita as interrupções da CPU. Agora vamos à segunda novidade.

Selecionar a CPU na qual a tarefa será executada

Por padrão setup() e **loop()**rodam no núcleo 1, defino no arquivo main.cpp da API para o Arduino, portanto não haverá mudança de prioridade dessas funções em nenhuma execução. Se não leu o artigo anterior, sugiro que o faça para ter mais informações sobre a prioridade de uma tarefa.

Atenção ao comportamento das tarefas

Deve-se tomar muito cuidado no gerenciamento das tarefas, porque uma tarefa com maior prioridade estará em execução em um core até que finalize para que só então a CPU esteja liberada para uma tarefa de menor prioridade. Se essa tarefa estiver presa, a CPU não será liberada para a tarefa de menor prioridade e daí você começará a enfrentar problemas que pode não estar habituado. Já no caso da chamada de uma tarefa de maior prioridade, a CPU deverá ser cedida. Ainda, estando as duas CPUS livres, você poderia optar por transferir a tarefa de menor prioridade para esse núcleo livre, mas se estivesse criando as tarefas deixando o gerenciamento por conta do sistema operacional, além de precisar fazer menos tratamentos no código, ainda evitará criar problemas por conta da programação.

Em que momento então é vantajoso selecionar a CPU?

Como citado mais ao início do artigo, se você tem a pretensão de manter uma CPU exclusivamente para um determinado tipo de trabalho. Mas quando essa CPU dedicada estiver ocupada, transferir a tarefa é uma ótima opção, daí você mesmo cria a excessão à sua regra. Delicia, hum?

Lembre-se: A prioridade das funções padrão do sketch do Arduino são 1, portanto criar uma tarefa com maior prioridade pode ser um sério problema, hum? Então, o mais lógico a fazer é criar nossas tasks com prioridade 0, mas agora devemos considerar que no código principal do sketch não teremos nada travando a execução, senão as tarefas não se mostrarão vivas; mas o **loop()**do Arduino roda no núcleo 1, portanto jogando as tarefas no núcleo 0 sua execução se dará conforme as prioridades definidas por você. Abre horizontes, não?

Separe as coisas

Quando você precisar (ou quiser) gerenciar as prioridades (que podem ser chamadas a partir de diferentes funções), lembre-se de criar o handle em uma variável global, colocando-a ao início do sketch. A partir do momento que você criar um handle dentro de uma função, ela passa a ser local, de modo que estará inacessível em chamadas fora dessa função.

Selecionar uma CPU

Agora é a hora de selecionar uma CPU para executar tasks. Utilizaremos a função citada mais ao início do artigo, a xTaskCreatePinnedToCore, que recebe como parâmetro adicional (em relação ao xTaskCreate) o núcleo no qual a tarefa deve ser executada. A exemplo, criaremos uma tarefa chamada coreZero:

xTaskCreatePinnedToCore(coreZero,"coreZero",10000,NULL,0,NULL,0);

Não precisamos nos preocupar com o loop mais, que agora poderá estar travado:

void loop(){
    while(true){
        delay(100);
    }
}

Criamos então a tarefa e colocamo-la a exibir seu núcleo de execução. O código completo fica assim:

void coreZero(void * pvParameters){
    while(true){
        Serial.println(xPortGetCoreID());
        delay(1000);
    }
}
 
void setup() {
    Serial.begin(115200);
    delay(1000);
    xTaskCreatePinnedToCore(coreZero,"coreZero",10000,NULL,0,NULL,0);
    Serial.println("Tarefa criada. seguindo...");
}
 
void loop(){
    Serial.println("Entrando no loop()...");
    while (true){
        }
}

Se quiser (não) ver a tarefa travada, troque o core 0 para 1. Como a tarefa  coreZero tem prioridade menor que que a função loop(), ela nunca será executada. Está motivado a novas experiências? Porque pretendo escrever bem mais a respeito!

Inscreva-se no nosso canal Manual do Maker no YouTube.

Também estamos no Instagram.

Nome do Autor

Djames Suhanko

Autor do blog "Do bit Ao Byte / Manual do Maker".

Viciado em embarcados desde 2006.
LinuxUser 158.760, desde 1997.