ESP32 - Lidando com Multiprocessamento - Parte I

Multiprocessamento no ESP32
Este post faz parte da série Multiprocessamento no ESP32. Leia também os outros posts da série:
  • ESP32 - Lidando com Multiprocessamento - Parte I

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:

 

Figura 2: Arquitetura 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:

 

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:

Figura 3: Monitor serial executando o sketch

 

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.

 

Referências

 

1 - ESP Interprocessor Call API Reference

2 - Suporte a placas com ESP32 para Arduino

Este post faz da série Multiprocessamento no ESP32. Leia também os outros posts da série:
  • ESP32 - Lidando com Multiprocessamento - Parte I
NEWSLETTER

Receba os melhores conteúdos sobre sistemas eletrônicos embarcados, dicas, tutoriais e promoções.

Obrigado! Sua inscrição foi um sucesso.

Ops, algo deu errado. Por favor tente novamente.

Licença Creative Commons Esta obra está licenciada com uma Licença Creative Commons Atribuição-CompartilhaIgual 4.0 Internacional.

Felipe Neves
Engenheiro de sistemas embarcados apaixonado pelo que faz, já trabalhou em diversos setores de tecnologia nos últimos 14 anos com destaque para Defesa, Automação, Agricultura, Wearables, Robótica e mais recentemente Semicondutores. Possui sangue maker, tendo paixão por construir e compatilhar suas próprias coisas e explorar novos sabores dentro do mundo "embedded". Atualmente trabalha como Engenheiro de Software Senior na Espressif, sim aquela do ESP32 e do ESP8266. Tem interesse em tópicos que envolvam Robótica, Controle de Movimento, DSP e Sistemas de Tempo Real.

5
Deixe um comentário

avatar
 
5 Comment threads
0 Thread replies
0 Followers
 
Most reacted comment
Hottest comment thread
5 Comment authors
Marcus CavalcanteAnibal VilelaFabio LuizRudsom De Oliveira LimaClaudio Borges Recent comment authors
  Notificações  
recentes antigos mais votados
Notificar
Marcus Cavalcante
Visitante
Marcus Cavalcante

Bom dia,
Ótimo artigo. Só me ficou a dúvida em como fazer o download das bibliotecas necessárias.

Anibal Vilela
Visitante
Anibal Vilela

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 !

Fabio Luiz
Visitante
Fabio luiz

Muito show esse tutorial muito bem explicado, ansioso para mais tutoriais sobre Rtos no ESP32

Rudsom De Oliveira Lima
Visitante
Rudsom

Excelente artigo! Obrigado

Claudio Borges
Visitante
Claudio Borges

Ótima aplicabilidade!!!
Parabéns, eis que uma boa forma de aproveitar ao máximo os recursos com multiprocessamento.