- ESP32 – Lidando com Multiprocessamento – Parte I
- ESP32 – Lidando com Multiprocessamento – Parte II
Olá caro leitores. Já tem um tempo que tenho utilizado o conhecido ESP32 em uma gama de projetos em que ele não seja comumente empregado como: Controle de movimento e unidade de sensores, e a cada interação com esse simpático chip tenho encontrado algumas funcionalidades dentro do IDF, o framework principal de desenvolvimento da Espressif, que me chamaram muito a atenção, além de claro ter facilitado muito a minha vida.
Nesse meu primeiro texto de 2020, gostaria de apresentar para vocês um recurso que talvez poucos tenham ouvido falar e nem usem pois:
- O IDF é uma base de código bem extensa e um pouco complexa;
- O uso do ESP32 pelo Arduino é a forma mais popular de desenvolver ainda que o port do Arduino não ofereça tudo que está disponível no IDF.
Esse recurso é o multiprocessamento, para quem não se recorda, o ESP32 conta com dois núcleos físicos XTENSA LX6 (o popular dual-core), cada um deles rodando a modestos 240MHz. Isso pode não chamar a atenção inicialmente, porém o IDF oferece suporte a multiprocessamento de forma transparente ao usuário, estando ele desenvolvendo dentro do Arduino ou fora dele.
Para deixar as coisas mais simples iremos rodar os exemplos no Arduino IDE, mas antes vamos revisitar um pouco sobre multiprocessamento, prometo que é rapidinho.
Multiprocessamento Simétrico ou Assimétrico?
O conceito de multiprocessamento, como o nome sugere, situa na capacidade de rodar um determinado programa em uma CPU que possui mais de 1 núcleo físico, ou seja imagine que, no caso do ESP32 que exista não um mas dois processadores, onde podemos balancear quais partes da firmware devem rodar entre os núcleos. De forma similar o sistema operacional do seu smartphone, composto de vários processos contidos em um aplicativo, pode delegar em qual núcleo físico um determinado processo vai rodar. Dessa mesma forma ESP32 pode criar tasks, que são parecidas com um processo, de forma que o agendamento das tasks não compartilhar apenas um núcleo entre elas, agora o sistema operacional do ESP32 pode fazer isso dividindo as tasks entre dois (ou mais) núcleos!
O multiprocessamento se divide ainda em dois grandes grupos, sendo o primeiro deles o assimétrico. Imagine que tenhamos um ESP32 com dois núcleos, porém cada um deles acessando apenas uma determinada área de memoria fisicamente separadas, ou seja esses núcleos fictícios consomem instruções de localidades diferentes, e podem trocar informações através de uma área de memória compartilhada, nesse caso teríamos duas instancias de firmware cada uma compilada de uma forma diferente. A grande vantagem desse tipo de arquitetura é que nesse caso os ESP32 poderiam ter núcleos diferentes sem qualquer relação, já que um núcleo jamais vai executar uma instrução que pertence a área de código do outro. A figura abaixo mostra bem esse conceito, embora não seja um ESP32:
Na figura 1 temos um caso clássico, dois núcleos ARM, sendo um deles um Cortex-A e outro um Cortex-M, observe que eles são bem diferentes entre si, a começar por não serem binariamente compatíveis, embora isso evidencie o fato de se tratar de uma arquitetura assimétrica, arquiteturas com núcleos binariamente compatíveis também podem ser consideradas assimétricas desde que cada núcleo consuma isoladamente sua própria instância de firmware.
O ESP32 opera na segunda categoria, a arquitetura simétrica onde, como o nome sugere, temos dois núcleos idênticos (primeiro requisito para ser simétrica) e além disso os dois núcleos compartilham tudo, desde memórias, periféricos e consomem a mesma instancia de firmware, basicamente isso significa que embora cada núcleo possa estar executando um pedaço diferente da área de código, esses pedaços pertencem ao mesmo binário da firmware, tendo, dessa forma dois processos fisicos sendo compartilhados entre a firmware e não apenas um como estamos habituados nos microcontroladores mais comuns.
O suporte ao multiprocessamento simétrico no ESP32
Abaixo temos a arquitetura simplificada do ESP32:
Percebam que os dois núcleos possuem codinomes, são eles o PRO_CPU e APP_CPU, usarei essa notação daqui em diante para facilitar a identificação, mas em princípio:
- PRO_CPU é o núcleo padrão, quando ESP32 é inicializado, apenas esse núcleo consome instruções da memória de programa;
- APP_CPU é o núcleo secundário, inicia desabilitado, mas uma vez ativo ele começa a consumir instruções a partir do valor inicial colocado no seu contador de programa, o conhecido PC.
No ESP32, ao utilizar o IDF ou o Arduino (pra quem não sabe a bibliotecas do Arduino para o ESP32 é construída em cima do IDF), ambos os núcleos são inicializados e colocados para rodar muito antes do programa chegar na parte da aplicação, o fato é que o sistema operacional interno do ESP32 no momento em que o programa alcança a função main(), setup() ou loop(), PRO_CPU e APP_CPU estão a disposição e prontas para rodar.
Por padrão a função loop() roda na APP_CPU, e para quem ja está familiarizado com o FreeRTOS do ESP32 talvez já tenha criado alguma task, que por padrão tem afinidade inicial com a PRO_CPU (nos bastidores o FreeRTOS modificado do ESP32 pode executar um balanceamento de carga jogando algumas tarefas para a PRO_CPU ou APP_CPU sem controle do usuário).
Mas então imagina agora que você teve a ideia: “E se eu mover funções da minha aplicação para executarem somente na PRO_CPU terei mais processamento livre?”. Sim você acertou e nessa primeira parte vamos explicar como delegar uma função para ser executada na PRO_CPU enquanto a APP_CPU cuida da função loop(), utilizando a IPC (Inter-Processor Call) API.
Delegando funções para a PRO_CPU com o IPC
Vamos direto pro código, como comentado antes, podemos usar componentes do IDF diretamente do Arduino, apenas incluindo os arquivos necessários, se esqueça de instalar o suporte para o Arduino do ESP32 adicionando o link 2 ao final do artigo no seu gerenciador de boards para busca e instalação de suporte.
E agora sim, vamos para o nosso primeiro exemplo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
#include <Arduino.h> #include <freertos/FreeRTOS.h> #include <freertos/task.h> #include <esp_ipc.h> void setup(){ Serial.begin(115200); } void LoopOnProCpu(void *arg) { (void)arg; Serial.print("This loop runs on PRO_CPU which id is:"); Serial.println(xPortGetCoreID()); Serial.println(); Serial.println(); } void loop(){ //Default loop runs on APP_CPU Serial.print("This loop runs on APP_CPU which id is:"); Serial.println(xPortGetCoreID()); Serial.println(); Serial.println(); //Execute LoopOnAppCpu on PRO_CPU esp_ipc_call(PRO_CPU_NUM, LoopOnProCpu, NULL); } |
Esse exemplo super simples mostra como executar uma função em qualquer que seja o núcleo desejado, o usuário que tiver algum ESP32 na mesa pode criar um sketch Arduino com esse código e já gravar nele. Vamos explicar o que ocorre, primeiro de tudo é importante reforçar que todo o IDF está acessível mesmo pelo Arduino, vejam que apenas inclui os arquivos do FreeRTOS e a API de IPC do ESP32 diretamente no sketch, isso vale para qualquer componente do IDF core. Com os arquivos devidamente incluídos temos as habituais funções setup() e loop() que como dissemos antes, elas rodam na APP_CPU, em setup(), nada de muito novo além de inicializar o monitor serial, é dentro de loop que vemos algo bem interessante.
A função xPortGetCoreID() retorna o número do núcleo onde aquela função está executando, então em loop, basicamente mostramos no console que estamos rodando loop da da APP_CPU ou seja no ID número 1, agora reparem que após essa mensagem temos uma função nova sendo chamada.
A função esp_ipc_call(), chamada ao final de loop, recebe três parâmetros, o primeiro é o ID do núcleo, podendo ser o PRO_CPU ou APP_CPU, o segundo parâmetro é a função que desejamos executar, o terceiro é um argumento que pode ter um formato definido pelo usuário (sendo do tipo void*) caso desejemos passar uma informação para essa função. É essa chamada que provoca que a execução da função LoopOnProCpu() que faz exatamente o que loop faz, ou seja imprime o ID do núcleo na CPU, carregue esse sketch no seu ESP32 e abra o monitor serial, você deve ver algo do tipo na tela:
Um ponto interessante a se destacar é que a execução da função em outro core é assíncrona ou seja, uma vez que o IPC seja chamado a função passada executa imediatamente no outro core, podendo terminar sua execução antes ou depois da função que a chamou, podendo ser entendido como uma aplicação rodando em paralelo. Adicionalmente a API IPC oferece a função esp_ipc_call_blocking() cuja a funcionalidade é idêntica, porém que chama essa função aguarda que a função do IPC finalize antes de prosseguir com sua execução, sendo interessante quando o usuário deseja sincronizar processos em dois cores diferentes. Tenha em mente que as funções delegadas dessa forma devem ser do tipo Run-To-Completion , ou seja elas devem ter um ponto de retorno, diferentemente de uma task ou da função main() elas não devem conter loops infinitos como while(1).
Conclusão
Esse artigo visou demonstrar que o ESP32 pode oferecer muito mais do que a aparência mostra, uma dessas ofertas está no multicore simétrico, perfeito para dividir o processamento entre ou dois núcleos permitindo o desenvolvimento de aplicações mais complexas ou a delegação de responsabilidade, os nomes APP_CPU e PRO_CPU não tem esse nome a toa ja que derivam de Application CPU e Protocol CPU respectivamente, onde um núcleo dedica-se a aplicação enquanto outro processa e encaminha o processamento de comunicações sem que um núcleo penalize o outro por usa natureza de processamento. Fique ligado pois na parte II iremos apresentar uma forma de contornar a limitação das funções serem Run-To-Completion permitindo que você usuário execute o que quiser do IDF no núcleo que desejar. Muito obrigado pela sua leitura e até a próxima.
Excelente artigo. Conteúdo mais aprofundado do que costumamos ver. Parabéns.
Apenas para checar meu entendimento, o nome LoopOnProCPU não seria muito apropriado, pois neste caso não podemos ter um loop correto?
A princípio achei que seria um loop paralelo ao void loop principal, mas no texto informar que deve ser uma função que tem um ponto de retorno.
Fiquei um pouco confuso.
Obrigado.
Em primeiro, Felipe Neves, parabéns pelo artigo, que é muito bem escrito e fácil de compreender. Sabe, estou nas trilhas de estudar o ESP32+IDF, principalmente para usar com M5Stick-C e M5Stack. Estou implementando cada uma das chamadas de HW em IDF — mas ficam muitas dúvidas (e algumas delas talvez sejam sobre o próprio FreeRTOS que não consegui entender). Por exemplo: Task Notification e Binary Semaphore. Imagine criar diferentes tarefas: – Exibir informações no LCD; – Monitorar botões e seus deboucing, número de cliques e tempo do clique – WI-FI – Bluetooth – SPIFFS (configuração em JSON) – RTC –… Leia mais »
Olá Nelio,
estou começando os estudos sobre o idf e lendo a sua pergunta, acredito que você pode usar as ferramentas de mailbox e eventgroups do freeRTOS.
O mailbox funciona como uma Queue, porém, os dados não são apagados quando são lidos pelas tasks. E o eventgroups executa uma task assim que certas condições do seu programa foram atingidas, essas condições são programadas por você
Espero ter dado uma luz para o seu projeto.
Eng. Ricardo Salmazo
Bom dia gostaria de saber se com este recursos se pode correr duas funçoes ao mesmo tempo. Por exemplo no nucleo fazer leitura dos sensores e o outro por exemplo receber dados da serial
Oi Jorge, obrigado pelo comentário, sim é totalmente possível, recomendo nesse seu caso que leia a parte II do artigo, que pode te dar uma idéia de como implementar isso da melhor forma.
Bom dia,
Ótimo artigo. Só me ficou a dúvida em como fazer o download das bibliotecas necessárias.
Marcus obrigado pelo comentário, basicamente é só instalar o suporte ao Arduino do ESP32 usando o gerenciador de placas, de uma olhada no link número 2 na seção de referências, creio que ela possa ser o que você precisa.
Gostei muito do artigo. O ESP32 tem muito mais recurso do que eu jamais sonhei precisar, mas é muito bom saber como usar as funções nativas dele dentro da IDE Arduino.
No dia em que lançarem uma placa de ESP32 que já traga embutida algumas interfaces convertidas para 5V, será o fim do mundo !
Obrigado pela leitura Anibal, fique ligado nos próximos artigos 🙂
Muito show esse tutorial muito bem explicado, ansioso para mais tutoriais sobre Rtos no ESP32
Obrigado pela leitura Fábio, teremos mais artigos a caminho, a segunda parte ja está disponível no site.
Excelente artigo! Obrigado
Rudsom, eu que agredeço pela leitura.
Ótima aplicabilidade!!!
Parabéns, eis que uma boa forma de aproveitar ao máximo os recursos com multiprocessamento.
Obrigado pela leitura Claudio, da uma olhada depois na parte 2, você vai gostar bastante.