Multiprocessamento - ESP32
Multiprocessamento com ESP32
As vezes é necessário que façamos o uso de sistemas multicores para as mais diversas finalidades, como por exemplo, fazer verificações de dados pela Serial, I2C ou sensores, enquanto o outro processador (Core) faz uma outra atividade em que não possa ser interrompida ou seja indesejado esse tipo de ação. Aprenderemos o que é multiprocessamento e usaremos o incrível ESP32 que tem ao todo três cores, para criar dois LOOP(), permitindo que você rode dois códigos ao mesmo tempo!
Lembrando: O terceiro core do ESP32 é um ULP, que é apenas programado em Assembly. Também é possível programa-lo para as mais diversas finalidades. Será mostrado apenas a programação do core principal.
Para conhecer mais sobre o ESP32 você pode conferir nosso tutorial: Conhecendo o ESP32
[toc]
Computação paralela - Multiprocessamento
Computação paralela é um assunto extremamente complicado, porem podemos simplificar de um método prático:
Você tem um supermercado com um caixa e em horários de pico, o caixa não é rápido suficiente para atender os clientes, então é necessário contratar outro funcionário que fique em outro caixa, fazendo assim que a velocidade de espera seja até 50% menor. (será atendido o dobro de clientes em relação a apenas um caixa)
Um sistema computacional paralelo, permite realizar diversos cálculos (tarefas, algoritmos, etc) simultaneamente. Há diversas maneiras de se paralelizar um código que é normalmente escrito em sequência: em bit, instrução, de dado ou de tarefa. O método mais simples é por exemplo dividir um FOR() pela metade a atribuir cada metade em cores diferentes, diminuindo o tempo de execução em até 50%. Apesar de parecer ser extremamente útil, há inúmeros problemas relacionados, como condição de corrida: o acesso simultâneo da mesma variável pode gerar erros em cálculos ou se o próprio calculo for dependentes de outros resultados, a paralelização disto é inconveniente, uma vez que utilizar semáforos para sincronizar as tarefas (exclusão mutua), pode ser mais lento que o simples código em sequencial. A questão à se pensar com uso de semáforos para sincronia de processos, deve ser analisada com o grau de granulação, uma vez que o uso excessivo de semáforos, pode deixar o processo mais lento que o código sequencial (single core).
Mãos à obra
Componentes necessários
- 1x ESP32
- Arduino IDE
Código do projeto
int tempo;//Variavel que armazena o tempo. void setup() { Serial.begin(115200);//Inicia a comunicaçao serial pinMode(2, OUTPUT);//Define o led Onboard como saída Serial.printf("\nsetup() em core: %d", xPortGetCoreID());//Mostra no monitor em qual core o setup() foi chamado xTaskCreatePinnedToCore(loop2, "loop2", 8192, NULL, 1, NULL, 0);//Cria a tarefa "loop2()" com prioridade 1, atribuída ao core 0 delay(1); } void loop()//O loop() sempre será atribuído ao core 1 automaticamente pelo sistema, com prioridade 1 { Serial.printf("\n Tempo corrido: %d", tempo++); delay(1000);//Mantem o processador 1 em estado ocioso por 1seg } void loop2(void*z)//Atribuímos o loop2 ao core 0, com prioridade 1 { Serial.printf("\nloop2() em core: %d", xPortGetCoreID());//Mostra no monitor em qual core o loop2() foi chamado while (1)//Pisca o led infinitamente { digitalWrite(2, !digitalRead(2)); delay(100); } }
Colocando para funcionar
Podemos observar tanto no Serial Monitor quanto no ESP32, o funcionamento esperado do código. Enquanto o core 1 faz a contagem do tempo e espera 1seg para repetir a mensagem (o que o deixa em IDLE [travado]), o core 0 pisca o led, mostrando que um não interfere no outro.
Entendendo a fundo
Software
-Função xTaskCreatePinnedToCore()
xTaskCreatePinnedToCore(loop2, "loop2", 8192, NULL, 1, NULL, 0);
Esta função cria uma tarefa e atribuí a um especifico processador. O FreeRTOS pode definir automaticamente em qual core a tarefa será rodada, para isto, use xTaskCreate() (Mais informações no site FreeRTOS).
Neste caso, criamos a tarefa loop2, com "tamanho" de 8192 Bytes (words), nenhum parâmetro, prioridade 1 e atribuída ao core 0.
Vamos esclarecer os parâmetros em ordem (da esquerda à direita):
xTaskCreatePinnedToCore(pxTaskCode, pcName, usStackDepth, pvParameters, uxPriority, pxCreatedTask, xCoreID)
pxTaskCode: Ponteiro para a tarefa, apenas o nome da tarefa. loop2.
pcName: O nome (String) da tarefa (entre aspas), é usado para facilitar o debug. "loop2".
usStackDepth: O tamanho de Stack reservada à tarefa, mais informações clique aqui. 8192.
pvParameters: O valor a ser passado para a tarefa no momento de criação. Nossa tarefa não precisa de parâmetros, então: NULL.
uxPriority: A prioridade da tarefa. É comum se usar 1 para tarefas simples, já que funções de delay (IDLE) tem prioridade 0. 1.
Se duas tarefas com mesma prioridade estiverem na fila, a primeira da fila irá ser executada e depois a próxima. Caso uma tarefa com prioridade 1 esteja na fila, porém uma tarefa com prioridade 2 também, será executado a tarefa com prioridade 2 e depois as outras.
pxCreatedTask: Valor opcional caso seja necessário manipulação das tarefas (Handle). NULL.
xCoreID: Atribuí a tarefa a um core especifico. 0.
Observações:
- Se você por exemplo criar dois loops, ao chamar uma subrotina (função), ela será executada no core em que foi chamada.
- Caso você não crie uma tarefa "infinita", será necessário deletar a tarefa com xTaskDelete().
- No caso do loop2() criado, é necessário o uso de pelo menos delay(1) dentro do loop, para que o Task Watchdog não seja ativado. Há maneiras de contornar isso, mas precisa fazer alterações no BootLoader.
-Função xPortGetCoreID()
xPortGetCoreID()
Esta função retorna o core em que a tarefa esta sendo executada.
Fechamento
A computação paralela se mostra útil quando necessário alto poder computacional ou monitoramento de itens específicos. Este assunto é gigantesco, complicado e intrigante, foi mostrado apenas o básico sobre o assunto; se você quer se aprofundar mais, veja os PDFs que foram citados no começo.
Dúvidas? Sugestões? Críticas? Comente abaixo!