Manual
do
Maker
.
com
Lembro-me do tempo que Linux era tratado como um software; as pessoas perguntavam se "dava pra instalar no Windows", porque em meados dos anos 90 era difícil compreender que existisse outra coisa que não o Windows. Hoje no mundo dos embarcados, até recentemente, o ESP8266 era tratado como um módulo WiFi para utilizar no Arduino e isso que me fez lembrar da época em que comecei a usar Linux.
Porque se não tem, vai perder a brincadeira. Sugiro que pegue o seu na CurtoCircuito.
O ESP é um processador da arquitetura Tensillica de 32 bits e pode rodar um sistema operacional de tempo real (RTOS). Ele é totalmente independente, não precisa estar atrelado a um Arduino, e ainda pode rodar diferentes firmwares como o Sming, o MicroPython e o NodeMCU. Mas o ESP32, ah, o ESP32; esse tem 2 núcleos de 240MHz, que traz além do WiFi, o bluetooth clássico e o BLE (dual mode). Tem 520KB de SRAM e 16MB de flash. Agora, de que adianta tantos recursos se não o utilizarmos corretamente? Bem, hoje eu vi um post que tratava da utilização de núcleos de forma independente, atribuindo a cada um uma respectiva tarefa. Confesso que deixei o ESP32 de lado um pouco porque sou apaixonado pelo Sming e não vi ainda a migração desse framework ocorrer para o ESP32, mas esse artigo que li me empolgou, então resolvi pesquisar um pouco mais sobre os recursos oferecidos pelo RTOS.
Fazendo uma analogia, quando usamos o delay no Arduino, estamos bloqueando o fluxo do código, que permanecerá dentro do loop desse delay até a finalização dessa parada. Isso signifca que nada mais ocorrerá; sensores não serão lidos, a serial estará bloqueada e todo o resto também. Quando utilizamos um delay no ESP32 através da implementação para utilizar na IDE do Arduino, é chamada a função vTaskDelay, que é uma função do FreeRTOS, cuja documentação de seus recursos podem ser vistas nesse link.
Claro, é muito simples utilizar a função delay, mas apenas para esclarecer como utilizar essa função, segue um exemplo:
void vTaskFunction(void * pvParameters){
const TickType_t xDelay = 500 / portTICK_PERIOD_MS; //500ms
while (true){
vToggleLED();
vTaskDelay(xDelay);
}
}
Esse é o mesmo exemplo utilizado na documentação; ou bem parecido, mas é um ótimo exemplo de blink, que servirá perfeitamente para utilizarmos no teste de tarefas paralelas.
A única coisa que varia entre o delay do Arduino é a declaração de um valor prévio para o delay. Mas o mais legal de utilizar os recursos do RTOS é a disponibilização de recursos que não temos no Arduino. Os recursos do FreeRTOS são tão amplos que não seria capaz de descrevê-los todos, por mais artigos que eu escreva.
Outra coisa que acho fantástica em utilizar uma board como o ESP32 é a possibilidade de controlar cada CPU de forma independente. É muito prazeroso contar com triggers, interrupções e tasks, tudo em uma plaquinha que cabe no bolso!
Quando utilizamos a função xTaskCreate para criar uma tarefa, não especificamos um núcleo para sua execução, isso fica por conta do FreeRTOS, que selecionará a CPU livre para atribuir a tarefa. Também temos a possiblidade de associar a tarefa a um núcleo específico, como veremos mais adiante.
Quando as tarefas estão sendo gerenciadas pelo FreeRTOS, não sabemos qual núcleo está sendo utilizado. Para obter essa informação, usamos a função xPortGetCoreID.
Como vamos utilizar a comunicação serial para observar os resultados, já temos aí uma tarefa em execução, que é o setup(), onde inicializaremos a comunicação serial. Como essa função não recebe parâmetros, devemos chamá-la dentro da tarefa em execução desse modo:
void setup(){
Serial.begin(115200);
delay(1000);
Serial.print("Executando setup() no nucleo ");
Serial.println(xPortGetCoreID());
...
}
Agora vamos iniciar uma nova tarefa com a utilização do **xTaskCreate (**do ESP32-IDF, que pode ser visto nesse link), e novamente analisaremos em que núcleo ela será executada. Nesse caso, o setup() ficaria assim:
void setup(){
Serial.begin(115200);
delay(1000);
Serial.print("Executando setup() no nucleo ");
Serial.println(xPortGetCoreID());
...
xTaskCreate(funcaoTeste,"funcaoTeste",10000,NULL,2,NULL);
delay(2000);
}
Seus parâmetros são:
função | string com o nome da tarefa | stack size | parâmetro | prioridade | handle |
funcaoTeste | "funcaoTeste" | 10000 | NULL | 2 | NULL |
O primeiro parâmetro é o ponteiro para uma função criada por você.
A string define um nome para a tarefa. Pode ser qualquer coisa, mas para debugging é melhor que seja o mesmo nome da função para facilitar as coisas.
O stack especifica o número de variáveis que podem ser tratadas, isso não é o número de bytes. Um stack de 16 bits com usStackDepth é definido cmo 100,200 bytes que serão alocados.
O parâmetro é o ponteiro que será usado para a tarefa que está sendo criada.
A prioridade é o peso da tarefa sobre as demais. Menor peso, menor prioridade, maior peso, maior prioridade.
O último parâmetro permite passar um manipulador pelo qual a tarefa criada pode ser referenciada.
Precisamos declarar a função que será utilizada pela tarefa, então:
void genericTask(void * parameter){
Serial.print("Executando tarefa no nucleo ");
Serial.println(xPortGetCoreID());
vTaskDelete(NULL);
}
Reparou no vTaskDelete(NULL)? Quando a tarefa não permanece em um loop infinito, precisamos desalocá-la da memória. Então, dentro da própria tarefa chamamos a função que a excluirá da lista.
Agora vamos colocar na função loop() apenas o print do núcleo de sua execução:
void loop(){
Serial.print("loop executando no nucleo ");
Serial.println(xPortGetCoreID());
delay(1000);
}
O código completo fica assim:
void genericTask(void * parameter){
Serial.print("Executando tarefa no nucleo ");
Serial.println(xPortGetCoreID());
vTaskDelete(NULL);
}
void setup(){
Serial.begin(115200);
delay(1000);
Serial.print("Executando setup() no nucleo ");
Serial.println(xPortGetCoreID());
...
xTaskCreate(funcaoTeste,"funcaoTeste",10000,NULL,2,NULL);
delay(2000);
}
void loop(){
Serial.print("loop executando no nucleo ");
Serial.println(xPortGetCoreID());
delay(1000);
}
Também existe uma função específica para listar as tarefas. Mas essa função não deve ser utilizada para outro fim que não debugging, porque ela desabilita as interrupções do sistema enquanto em execução. Essa função não tem retorno e recebe como parâmetro o ponteiro de um array de char, onde ela alocará o respectivo Byte da tabela ASCII denotando o estado de cada tarefa da seguinte maneira:
B | blocked |
R | Ready |
D | Deleted |
S | Suspended ou bloqueado sem um timeout |
O formato dessa função é:
void vTaskList(char *pcWriteBuffer);
E seu uso deve ser algo como:
char tasks_state[600] = {0};
...
void myTask(){
...
}
void setup(){
...
}
void loop(){
...
vTaskList(tasks_state);
...
}
A saída dessa função deve ser algo como:
A média é de 40 Bytes por tarefa e o valor que utilizei no array de char é hipotético, ajuste conforme sua necessidade.
Se quiser ver mais funcionalidades do ESP-IDF, consulte nesse link.
Jogar uma tarefa para ser executada em um núcleo específico permite que você tenha um código principal atuante sem delays ou interrupções, dando ao seu programa um comportamento parecido com threads, onde você poderá ter paralelismo, processamento assíncrono e segurança na tarefa principal - que ainda responderá às prioridades de interrupção. É muito poder ou não é?
No próximo artigo veremos como selecionar a CPU que executará uma determinada tarefa e daí cito mais algumas funcionalidades do FreeRTOS.
Inscreva-se no nosso canal Manual do Maker no YouTube.
Também estamos no Instagram.
Autor do blog "Do bit Ao Byte / Manual do Maker".
Viciado em embarcados desde 2006.
LinuxUser 158.760, desde 1997.