O seu ESP32 tem dois cérebros. Você sabia disso? Dentro do chip existe um processador de duplo núcleo chamado Xtensa LX6, rodando a 240MHz cada. E o mais surpreendente: na grande maioria dos projetos Arduino, apenas um deles é usado. O outro fica ocioso, esperando um trabalho que nunca chega.
Neste projeto, vamos acordar os dois núcleos e colocá-los a trabalhar em paralelo para resolver um dos problemas mais pesados da computação: a simulação de física de colisões com complexidade O(N²). Quanto mais objetos adicionamos ao sistema, mais o processador sofre — e é exatamente essa pressão que vamos usar para provar, num benchmark ao vivo no navegador, a diferença brutal entre rodar tudo num núcleo só, versus dividir o trabalho entre os dois.
Como funciona esse sistema?
Imagine dois operários especializados dentro de uma fábrica. O primeiro operário é um matemático: ele não para de calcular colisões, física, velocidades e posições de centenas de objetos ao mesmo tempo. Ele é tão focado que não pode parar nem por um segundo para atender o telefone.
O segundo operário é o atendente de rede: ele fica de olho nas conexões WebSocket, recebe comandos do navegador (como “mude para Dual-Core” ou “adicione mais partículas”) e envia o resultado das posições calculadas de volta ao painel web a cada 33ms.
No Single-Core, esses dois operários são na verdade a mesma pessoa. Ela precisa parar o cálculo, atender a rede, depois voltar para o cálculo — e isso cria um gargalo enorme. Quanto mais partículas, mais tempo ela gasta com matemática antes de conseguir falar com o navegador. A animação gagueja, o pass/s despenca.
No Dual-Core, cada operário tem seu próprio cérebro (núcleo). O matemático calcula sem parar. O atendente envia os dados continuamente. Os dois trabalham ao mesmo tempo, em paralelo. O único momento em que precisam se coordenar é quando o atendente precisa “fotografar” as posições para enviar — e aí o sistema usa um Semáforo Mutex como um crachá de acesso: apenas um por vez pode tocar na memória compartilhada.

Materiais Necessários
Single-Core vs. Dual-Core: o gargalo que você vai ver ao vivo
Antes de mergulhar no código, é fundamental entender por que o gargalo acontece e por que o Dual-Core resolve tão dramaticamente.

A física de colisões usada neste projeto tem complexidade O(N²), onde N é o número de partículas. Isso significa que com 150 partículas, o processador precisa checar até 11.175 pares de colisão por ciclo. Com 300 partículas, esse número quadruplica para quase 45.000 verificações. O custo computacional explode rapidamente.
No modo Single-Core, física e rede dividem o mesmo núcleo (Core 0). A tarefa de rede precisa interromper os ciclos de física para processar o handshake WebSocket, montar e enviar o frame binário. Esse tempo de overhead rouba fatias do orçamento dos 2ms e reduz quantos cálculos cabem por segundo. Quanto mais pesado o O(N²), maior a fatia consumida — e o pass/s despenca.
No modo Dual-Core, a tarefa de física fica fixada exclusivamente no Core 1 com prioridade máxima. Ela nunca disputa processador com a rede. Já a tarefa de rede fica no Core 0 e transmite em ritmo constante sem jamais bloquear o cálculo. Os dois núcleos correm em paralelo — e a diferença no pass/s que você verá na tela é a prova real dessa vantagem arquitetural.
Entendendo as Bibliotecas
Todas as bibliotecas usadas neste projeto já vêm instaladas com o pacote ESP32 para Arduino IDE, sem precisar instalar nada extra:
- WiFi.h — Gerencia a conexão Wi-Fi do chip. Documentação oficial ESP32 Arduino
- WebServer.h — Sobe um servidor HTTP simples para servir o painel HTML/CSS/JS pela porta 80.
- ESPmDNS.h — Registra um endereço amigável na rede local, permitindo acessar o painel via `http://esp32.local` sem digitar o IP.
- mbedtls/sha1.h e mbedtls/base64.h — Biblioteca de criptografia já embutida no ESP32. Usamos para implementar o handshake WebSocket nativo (o protocolo WebSocket exige uma troca de chaves SHA1/Base64 no momento da conexão), eliminando a necessidade de qualquer biblioteca externa de WebSocket.
Como acessar o painel pelo navegador
Ao gravar o firmware e conectar o ESP32 à sua rede Wi-Fi, o chip registrará o endereço mDNS `esp32` na rede. Isso significa que qualquer dispositivo na mesma rede Wi-Fi pode acessar o painel digitando no navegador:
http://esp32.local
Caso o mDNS não funcione no seu sistema operacional (isso acontece em algumas versões do Windows sem o serviço Bonjour instalado), você pode verificar o IP real pelo Monitor Serial da Arduino IDE logo após a inicialização e acessar diretamente pelo IP (ex: `http://192.168.1.45`).
Segurança das credenciais: Nunca suba seu código para repositórios públicos com as variáveis `ssid` e `password` preenchidas. Se precisar compartilhar o código, substitua os valores por `”SEUWIFI”` e `”SUASENHA”` antes de publicar.
Análise Técnica da Arquitetura
Criando e fixando as tarefas FreeRTOS nos núcleos
O coração do projeto está na função `setup()`, onde criamos as duas tarefas e as fixamos em núcleos específicos com `xTaskCreatePinnedToCore()`:
// Tarefa de física: fixada no Core 1 (APP_CPU), prioridade alta
xTaskCreatePinnedToCore(vTaskCalculoMatematico, "Fisica", 8192, NULL, 2, &taskMathHandle, 1);
// Tarefa de rede: fixada no Core 0 (PRO_CPU), prioridade menor
xTaskCreatePinnedToCore(vTaskTransmissaoRede, "Rede", 8192, NULL, 1, &taskNetHandle, 0);
O último parâmetro é o número do núcleo. O `1` fixa a tarefa no Core 1 (também chamado de APPCPU), que é o núcleo “livre” — o Core 0 (PROCPU) já é usado pelo próprio Wi-Fi stack do ESP32 internamente. Separar as responsabilidades assim garante que a física não dispute processador com a pilha de rede.
O Semáforo Mutex: a fechadura que evita o Kernel Panic
As duas tarefas precisam acessar o array `particulas[]` ao mesmo tempo — uma para atualizar as posições, outra para lê-las antes de enviar. Se as duas tentarem mexer nessa memória simultaneamente, ocorre uma condição de corrida (race condition) e o chip pode reiniciar com um Kernel Panic.
O Mutex (Mutual Exclusion) é a solução: funciona como uma fechadura com uma única chave. Antes de qualquer tarefa tocar nas partículas, ela precisa “pegar a chave” (`xSemaphoreTake`). Enquanto uma tarefa segura a chave, a outra fica pausada aguardando. Ao terminar, devolve a chave (`xSemaphoreGive`) para a próxima poder entrar:
// Antes de mexer nas partículas, pede a "chave"
if (xSemaphoreTake(mutexFisica, portMAX_DELAY) == pdTRUE) {
// ... código que acessa o array particulas[] ...
// Devolve a "chave" ao terminar
xSemaphoreGive(mutexFisica);
}
O motor de física O(N²): detectando colisões entre todos os pares
A função `atualizarFisica()` implementa dois estágios por ciclo:
1. Movimento com tela infinita (Wrap-around): Cada partícula se move pela sua velocidade. Ao sair por uma borda do canvas, reaparece do lado oposto — como um videogame clássico. Isso garante que nenhuma partícula some do sistema.
2. Detecção e resolução de colisão O(N²): Para cada par único de partículas `(i, j)`, calculamos a distância quadrada entre elas. Se essa distância for menor que a soma dos raios, houve colisão. Resolvemos empurrando as partículas para fora uma da outra e trocando as componentes de velocidade ao longo do eixo de colisão (física newtoniana de colisão elástica):
// Percorre todos os pares únicos (i, j) — complexidade O(N²)
for (int i = 0; i < ativos; i++) {
for (int j = i + 1; j < ativos; j++) {
float dx = particulas[j].x - particulas[i].x;
float dy = particulas[j].y - particulas[i].y;
float distSq = dx * dx + dy * dy;
float raioSoma = particulas[i].raio + particulas[j].raio;
if (distSq < raioSoma * raioSoma && distSq > 0.0001f) {
// Separação por overlap e troca de velocidades
float dist = sqrtf(distSq);
float overlap = 0.5f * (raioSoma - dist);
float nx = dx / dist;
float ny = dy / dist;
particulas[i].x -= nx * overlap;
particulas[j].x += nx * overlap;
float p = (nx * (particulas[i].vx - particulas[j].vx)) +
(ny * (particulas[i].vy - particulas[j].vy));
particulas[i].vx -= p * nx;
particulas[j].vx += p * nx;
}
}
}
WebSocket nativo sem bibliotecas externas
Em vez de usar a biblioteca AsyncWebServer ou WebSockets, implementamos o handshake WebSocket do zero usando apenas `mbedtls` (SHA1 + Base64). Isso torna o firmware mais leve e sem dependências externas.
O protocolo WebSocket exige que, na primeira conexão HTTP (chamada de Upgrade Request), o servidor calcule uma chave de aceitação: pega o valor do cabeçalho `Sec-WebSocket-Key` enviado pelo navegador, concatena com a string mágica `”258EAFA5-E914-47DA-95CA-C5AB0DC85B11″`, aplica SHA1 e codifica em Base64. O navegador verifica esse valor para confirmar que está falando com um servidor WebSocket legítimo.
Depois que a conexão é estabelecida, a transmissão usa frames binários compactos: 2 bytes para o contador SPS e 4 bytes por partícula (X e Y como `uint16_t`). Com 150 partículas, cada frame tem apenas 602 bytes — extremamente eficiente para o Wi-Fi do ESP32.
O dashboard: Canvas fullscreen e controles interativos
O painel HTML é servido diretamente do firmware como um literal de string raw (`R”rawliteral(…)”`). O JavaScript abre um WebSocket para a porta 81, recebe os frames binários e:
- Extrai os primeiros 2 bytes para atualizar o contador pass/s no HUD.
- Para cada partícula, extrai suas coordenadas X e Y e desenha num “ fullscreen como um ponto luminoso.
- Além dos pontos, desenha linhas de conexão entre partículas que estão próximas (distância < 83px), criando o efeito de malha tecnológica/constelação animada.
Os botões 1 Núcleo (Single) e 2 Núcleos (Dual) enviam comandos de volta para o ESP32 via WebSocket (`MODO:SINGLE` e `MODO:DUAL`). O slider de Densidade envia `COUNT:N` para ajustar quantas partículas são simuladas em tempo real, sem precisar reiniciar o firmware.
Código Completo
/**
* @brief Benchmark Multithread ESP32: Malha Tecnológica (Constelação) O(N^2).
* Focado 100% em processamento matemático. Tela Infinita.
* @note Acesso via: http://esp32.local
* @author Abraão da Silva
*/
#include <WiFi.h>
#include <WebServer.h>
#include <ESPmDNS.h>
#include <mbedtls/sha1.h>
#include <mbedtls/base64.h>
const char* ssid = "SEU_WIFI";
const char* password = "SUA_SENHA";
WebServer httpServer(80);
WiFiServer wsServer(81);
WiFiClient wsClient;
// ============================================================================
// CONFIGURAÇÕES DA SIMULAÇÃO
// ============================================================================
const int CANVAS_WIDTH = 1200;
const int CANVAS_HEIGHT = 800;
const int MAX_PARTICLES = 300;
struct Particula {
float x, y;
float vx, vy;
float raio;
};
Particula particulas[MAX_PARTICLES];
volatile int numParticulasAtivas = 150;
enum ModoExecucao { MODO_SINGLE_CORE, MODO_DUAL_CORE };
volatile ModoExecucao modoAtual = MODO_DUAL_CORE;
SemaphoreHandle_t mutexFisica;
TaskHandle_t taskMathHandle = NULL;
TaskHandle_t taskNetHandle = NULL;
volatile uint32_t spsAtual = 0;
volatile uint32_t contadorSps = 0;
uint32_t tempoUltimaMetrica = 0;
// ============================================================================
// MOTOR MATEMÁTICO: física de colisão e movimentação das partículas
// ============================================================================
void atualizarFisica() {
// Tenta adquirir o mutex antes de mexer nos dados compartilhados
if (xSemaphoreTake(mutexFisica, portMAX_DELAY) == pdTRUE) {
int ativos = numParticulasAtivas;
// Move cada partícula pela sua velocidade
for (int i = 0; i < ativos; i++) {
particulas[i].x += particulas[i].vx;
particulas[i].y += particulas[i].vy;
// Efeito Tela Infinita: sai por um lado, entra pelo outro
if (particulas[i].x < 0) particulas[i].x += CANVAS_WIDTH;
else if (particulas[i].x > CANVAS_WIDTH) particulas[i].x -= CANVAS_WIDTH;
if (particulas[i].y < 0) particulas[i].y += CANVAS_HEIGHT;
else if (particulas[i].y > CANVAS_HEIGHT) particulas[i].y -= CANVAS_HEIGHT;
// Detecção e resolução de colisão O(N²) entre todos os pares
for (int j = i + 1; j < ativos; j++) {
float dx = particulas[j].x - particulas[i].x;
float dy = particulas[j].y - particulas[i].y;
float distSq = dx * dx + dy * dy;
float raioSoma = particulas[i].raio + particulas[j].raio;
if (distSq < raioSoma * raioSoma && distSq > 0.0001f) {
float dist = sqrtf(distSq);
float overlap = 0.5f * (raioSoma - dist);
float nx = dx / dist;
float ny = dy / dist;
// Separa as partículas para evitar sobreposição
particulas[i].x -= nx * overlap;
particulas[i].y -= ny * overlap;
particulas[j].x += nx * overlap;
particulas[j].y += ny * overlap;
// Troca de momento (colisão elástica)
float kx = particulas[i].vx - particulas[j].vx;
float ky = particulas[i].vy - particulas[j].vy;
float p = nx * kx + ny * ky;
particulas[i].vx -= p * nx;
particulas[i].vy -= p * ny;
particulas[j].vx += p * nx;
particulas[j].vy += p * ny;
}
}
}
// Atualiza o contador de passos por segundo (SPS)
contadorSps++;
uint32_t agora = millis();
if (agora - tempoUltimaMetrica >= 1000) {
spsAtual = contadorSps;
contadorSps = 0;
tempoUltimaMetrica = agora;
}
xSemaphoreGive(mutexFisica); // Libera o mutex
}
}
// ============================================================================
// TAREFA DO CORE 1: Cálculo matemático contínuo
// ============================================================================
void vTaskCalculoMatematico(void* pvParameters) {
while (true) {
// No modo Dual-Core, esta tarefa é a responsável pelo cálculo físico no Core 1
if (modoAtual == MODO_DUAL_CORE) {
atualizarFisica();
}
// Mesmo intervalo de respiro (2ms) em AMBOS os modos — benchmark justo
vTaskDelay(pdMS_TO_TICKS(2));
}
}
// ============================================================================
// TAREFA DO CORE 0: Rede — WebSocket, HTTP e envio de telemetria
// ============================================================================
void vTaskTransmissaoRede(void* pvParameters) {
uint8_t bufferRede[1500];
while (true) {
// Aguarda conexão WebSocket se não houver cliente
if (!wsClient || !wsClient.connected()) {
WiFiClient novo = wsServer.available();
if (novo) {
String req = "";
while (novo.connected() && novo.available()) {
req += (char)novo.read();
if (req.endsWith("\r\n\r\n")) break;
}
// Realiza o handshake WebSocket (SHA1 + Base64 da chave)
int k = req.indexOf("Sec-WebSocket-Key: ");
if (k != -1) {
String wsKey = req.substring(k + 19, req.indexOf("\r\n", k));
wsKey.trim();
String mk = wsKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
unsigned char hash[20];
mbedtls_sha1((const unsigned char*)mk.c_str(), mk.length(), hash);
unsigned char b64[64]; size_t outL = 0;
mbedtls_base64_encode(b64, sizeof(b64), &outL, hash, 20);
b64[outL] = '\0';
novo.print("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: " + String((char*)b64) + "\r\n\r\n");
wsClient = novo;
}
}
vTaskDelay(pdMS_TO_TICKS(50));
continue;
}
// Lê comandos do navegador (MODO e COUNT)
if (wsClient.available()) {
uint8_t h[2]; wsClient.read(h, 2);
size_t len = h[1] & 0x7F;
uint8_t mask[4] = {0}; if (h[1] & 0x80) wsClient.read(mask, 4);
if (len > 0) {
uint8_t p[32] = {0}; wsClient.read(p, len);
for (size_t i = 0; i < len; i++) p[i] ^= mask[i % 4];
String cmd = String((char*)p);
if (cmd.startsWith("MODO:SINGLE")) modoAtual = MODO_SINGLE_CORE;
else if (cmd.startsWith("MODO:DUAL")) modoAtual = MODO_DUAL_CORE;
else if (cmd.startsWith("COUNT:")) numParticulasAtivas = cmd.substring(6).toInt();
}
}
if (modoAtual == MODO_SINGLE_CORE) {
// Preenche a janela de 33ms com ciclos de física usando o mesmo intervalo do Dual-Core (2ms)
// O gargalo é natural: o overhead do WebSocket consome parte desse tempo compartilhado
uint32_t tempoLimite = millis() + 33;
while ((int32_t)(tempoLimite - millis()) > 2) {
atualizarFisica();
vTaskDelay(pdMS_TO_TICKS(2));
}
}
// Lê os dados com o mutex e monta o frame binário para envio
if (xSemaphoreTake(mutexFisica, pdMS_TO_TICKS(20)) == pdTRUE) {
int idx = 0;
// Primeiros 2 bytes: valor do SPS
bufferRede[idx++] = (spsAtual >> 8) & 0xFF;
bufferRede[idx++] = spsAtual & 0xFF;
// 4 bytes por partícula: X (uint16) + Y (uint16)
int ativos = numParticulasAtivas;
for (int i = 0; i < ativos; i++) {
uint16_t px = (uint16_t)particulas[i].x;
uint16_t py = (uint16_t)particulas[i].y;
bufferRede[idx++] = (px >> 8) & 0xFF;
bufferRede[idx++] = px & 0xFF;
bufferRede[idx++] = (py >> 8) & 0xFF;
bufferRede[idx++] = py & 0xFF;
}
xSemaphoreGive(mutexFisica);
// Envia o frame WebSocket binário
uint8_t header[4];
header[0] = 0x82; // FIN=1, opcode=2 (binário)
if (idx <= 125) {
header[1] = idx;
wsClient.write(header, 2);
} else {
header[1] = 126;
header[2] = (idx >> 8) & 0xFF;
header[3] = idx & 0xFF;
wsClient.write(header, 4);
}
wsClient.write(bufferRede, idx);
}
vTaskDelay(pdMS_TO_TICKS(33)); // ~30 FPS de transmissão
}
}
// ============================================================================
// SERVIDOR HTTP: entrega o dashboard HTML/CSS/JS ao navegador
// ============================================================================
void handleRoot() {
// O HTML completo está embutido como string literal raw no firmware
String html = R"rawliteral(
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Benchmark ESP32</title>
<style>
body { font-family: 'Segoe UI', sans-serif; margin: 0; padding: 0; background: #020617; color: #e2e8f0; overflow: hidden; }
canvas { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; z-index: -1; }
.ui-container { position: absolute; top: 20px; left: 50%; transform: translateX(-50%); width: 90%; max-width: 800px; background: rgba(15,23,42,0.7); backdrop-filter: blur(8px); border: 1px solid rgba(51,65,85,0.5); border-radius: 12px; padding: 20px; box-shadow: 0 10px 30px rgba(0,0,0,0.5); z-index: 10; }
.hud { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; flex-wrap: wrap; gap: 10px; }
.metrics { font-size: 1.2rem; background: rgba(30,41,59,0.8); padding: 10px 20px; border-radius: 8px; border: 1px solid #334155; }
.metrics strong { color: #38bdf8; font-size: 1.6rem; }
.controls { display: flex; gap: 15px; align-items: center; flex-wrap: wrap; }
button { background: rgba(30,41,59,0.8); color: white; border: 1px solid #334155; padding: 10px 20px; border-radius: 6px; cursor: pointer; transition: 0.2s; font-weight: 600; }
button:hover { background: #334155; }
button.active { background: #38bdf8; border-color: #0284c7; color: #0f172a; }
.slider-container { display: flex; align-items: center; gap: 10px; margin-left: auto; background: rgba(30,41,59,0.8); padding: 8px 15px; border-radius: 6px; border: 1px solid #334155; }
</style>
</head>
<body>
<canvas id="cvs" width="1200" height="800"></canvas>
<div class="ui-container">
<div class="hud">
<div>
<h2 style="margin:0; color:#fff;">Estresse FPU O(N²)</h2>
<span style="color:#94a3b8;">Teste direto de colisão e malha dinâmica</span>
</div>
<div class="metrics">Cálculos da CPU: <strong id="valSps">0</strong> pass/s</div>
</div>
<div class="controls">
<button id="b1" onclick="setMode('SINGLE')">1 Núcleo (Single)</button>
<button id="b2" class="active" onclick="setMode('DUAL')">2 Núcleos (Dual)</button>
<div class="slider-container">
Densidade: <strong id="valCount" style="color:#38bdf8; min-width:30px;">150</strong>
<input type="range" min="10" max="300" value="150" oninput="changeCount(this.value)">
</div>
</div>
</div>
<script>
const canvas = document.getElementById('cvs');
const ctx = canvas.getContext('2d');
let ws;
function initWS() {
ws = new WebSocket('ws://' + window.location.hostname + ':81/');
ws.binaryType = 'arraybuffer';
ws.onmessage = (e) => {
if (!(e.data instanceof ArrayBuffer)) return;
let buf = new Uint8Array(e.data);
if (buf.length < 2) return;
// Atualiza o contador de pass/s
document.getElementById('valSps').innerText = (buf[0] << 8) | buf[1];
// Limpa o canvas e redesenha
ctx.clearRect(0, 0, 1200, 800);
let particulas = [];
let num = (buf.length - 2) / 4;
// Extrai posições de cada partícula
for (let i = 0; i < num; i++) {
let o = 2 + i * 4;
particulas.push({ x: (buf[o] << 8) | buf[o+1], y: (buf[o+2] << 8) | buf[o+3] });
}
// Desenha linhas de conexão entre partículas próximas (malha/constelação)
ctx.lineWidth = 1;
for (let i = 0; i < num; i++) {
for (let j = i + 1; j < num; j++) {
let dx = particulas[i].x - particulas[j].x;
let dy = particulas[i].y - particulas[j].y;
let distSq = dx*dx + dy*dy;
if (distSq < 7000) {
let alpha = 1.0 - (Math.sqrt(distSq) / 83.6);
ctx.strokeStyle = `rgba(56, 189, 248, ${alpha})`;
ctx.beginPath(); ctx.moveTo(particulas[i].x, particulas[i].y);
ctx.lineTo(particulas[j].x, particulas[j].y); ctx.stroke();
}
}
}
// Desenha os nós (pontos brilhantes)
ctx.fillStyle = '#bae6fd';
for (let i = 0; i < num; i++) {
ctx.beginPath(); ctx.arc(particulas[i].x, particulas[i].y, 3.5, 0, Math.PI * 2); ctx.fill();
}
};
ws.onclose = () => setTimeout(initWS, 1000);
}
window.setMode = (m) => {
document.getElementById('b1').className = m === 'SINGLE' ? 'active' : '';
document.getElementById('b2').className = m === 'DUAL' ? 'active' : '';
if (ws) ws.send('MODO:' + m);
};
window.changeCount = (v) => { document.getElementById('valCount').innerText = v; if (ws) ws.send('COUNT:' + v); };
window.onload = initWS;
</script>
</body>
</html>
)rawliteral";
httpServer.send(200, "text/html", html);
}
// ============================================================================
// SETUP: inicializa hardware, rede, mDNS e cria as tarefas FreeRTOS
// ============================================================================
void setup() {
Serial.begin(115200);
// Inicializa as partículas com posições e velocidades aleatórias
for (int i = 0; i < MAX_PARTICLES; i++) {
particulas[i].x = random(0, CANVAS_WIDTH);
particulas[i].y = random(0, CANVAS_HEIGHT);
particulas[i].vx = (random(-40, 40)) / 10.0f;
particulas[i].vy = (random(-40, 40)) / 10.0f;
particulas[i].raio = 4.0f;
}
// Cria o mutex antes de qualquer tarefa que possa acessar os dados
mutexFisica = xSemaphoreCreateMutex();
// Conecta ao Wi-Fi
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) delay(500);
Serial.print("Conectado! IP: "); Serial.println(WiFi.localIP());
// Registra o endereço mDNS amigável
if (MDNS.begin("esp32")) Serial.println("MDNS: http://esp32.local");
// Inicia o servidor HTTP e o servidor WebSocket
httpServer.on("/", handleRoot);
httpServer.begin();
wsServer.begin();
// Cria as tarefas e as fixa nos núcleos específicos
xTaskCreatePinnedToCore(vTaskCalculoMatematico, "Fisica", 8192, NULL, 2, &taskMathHandle, 1);
xTaskCreatePinnedToCore(vTaskTransmissaoRede, "Rede", 8192, NULL, 1, &taskNetHandle, 0);
}
void loop() {
httpServer.handleClient(); // Atende requisições HTTP
vTaskDelay(pdMS_TO_TICKS(10));
}
Conclusão e Desafios
Você acaba de construir um benchmark interativo que prova, com números ao vivo, que o processamento paralelo importa. O que o código demonstra é uma verdade fundamental da computação moderna: tarefas independentes que competem pelo mesmo recurso criam gargalos. Separar responsabilidades por núcleo, proteger a memória compartilhada com um mutex e deixar cada tarefa trabalhar em seu ritmo é a essência do design multithread robusto — e o ESP32 faz isso com FreeRTOS de forma nativa.
Desafios para ir além:
- Adicione um terceiro gráfico ao HUD que exiba a temperatura interna do chip (use `temperatureRead()`) para ver como a carga computacional aquece os núcleos em tempo real.
- Implemente cores diferentes para as partículas baseadas na sua velocidade (partículas mais rápidas = mais quentes = mais vermelhas).
- Experimente aumentar o slider até 300 partículas e compare os números de Single vs. Dual. O efeito câmera lenta do Single-Core fica ainda mais dramático — e o Dual-Core sustenta uma performance surpreendentemente superior.
Demonstração em Vídeo
No vídeo abaixo, você pode acompanhar o benchmark ao vivo: partindo do modo Single-Core com movimento lento, ao aumentarmos progressivamente o número de partículas em colisão e mudar para o modo Dual-Core, o contador de pass/s aumenta e a malha passa a se mover muito mais rápido — exatamente o gargalo que o processamento paralelo elimina.




