PTENES
MÓDULO 2.4 · PRÁTICA

🧵 Várias tarefas ao mesmo tempo

Um robô precisa ler sensores, corrigir motores, ouvir o celular e mandar dados, tudo junto. Aqui você troca o delay() por millis(), organiza o comportamento em uma máquina de estados e usa o FreeRTOS do ESP32 para dividir o trabalho entre tarefas e núcleos.

6
Tópicos
Intermediário
Nível
Prática
Tipo
ESP32
Hardware (Wokwi serve)
0%
0 de 0 tópicos lidos · Seção 1 de 6

Conteúdo detalhado

1

⏳ Por que delay() trava o robô

Na Trilha 1, delay(500) parecia inofensivo: pisca o LED e pronto. Mas, durante esses 500 ms, o processador não faz mais nada. Não lê o sensor de distância, não corrige o motor, não percebe o botão de parada. Um rover a 0,5 m/s anda 25 cm "de olhos fechados" a cada meio segundo de espera. Com um sensor e um LED isso passa despercebido; com Wi-Fi, PID e telemetria juntos, o robô começa a bater, oscilar e perder comandos.

🌱 Novo aqui?

Uma função bloqueante só devolve o controle ao programa quando termina. delay(), while esperando um botão e pulseIn() sem tempo limite são bloqueantes. Uma função não bloqueante faz um pedaço pequeno do trabalho e sai logo, deixando o resto do código rodar.

com delay() parado: não vê nadaparado: não vê nada com millis() 0 ms500 ms1000 ms

Como ler: cada barra azul é uma volta do programa em que ele lê sensores e decide. Em cima, o delay() deixa só duas fatias por segundo; o resto (vermelho) é tempo cego. Embaixo, o mesmo programa sem espera faz uma fatia curta atrás da outra e enxerga o mundo o tempo todo.

✓ Sinais de código saudável

  • ✓O loop() dá milhares de voltas por segundo.
  • ✓O botão de parada responde na hora, em qualquer momento.
  • ✓Esperas com tempo limite (pulseIn(pino, HIGH, 25000)).

✗ Sinais de código bloqueante

  • ✗O robô "demora" para reagir ao obstáculo ou ao celular.
  • ✗while (digitalRead(botao) == HIGH) {} esperando alguém.
  • ✗Comandos Wi-Fi chegam atrasados ou em rajada.

Por que aprender

O PID do módulo 2.3 só funciona se rodar em ritmo fixo, e o seguidor de linha do 2.6 precisa ler cinco sensores centenas de vezes por segundo enquanto manda telemetria. Sem tirar os bloqueios, nenhuma dessas peças funciona junto.

Conceitos-chave

Bloqueante
segura o processador
Tempo cego
ninguém olha o sensor
Latência
atraso até reagir
Tempo limite
espera com prazo
2

⏱️ millis() e tarefas sem bloqueio

millis() devolve quantos milissegundos se passaram desde que a placa ligou. Em vez de esperar, você olha o relógio: "já passaram 500 ms desde a última vez que pisquei? Então pisco e anoto a hora". Cada tarefa ganha a própria anotação de tempo e o próprio período. O loop() passa por todas, faz só o que está na hora e segue em frente.

LED · 500 mssensor · 20 msrelatório · 1 s if (agora − ultimaVez ≥ periodo) { faz; ultimaVez = agora; }

Como ler: cada linha é uma tarefa com seu ritmo. As marcas mostram quando ela realmente trabalha. Nenhuma espera pela outra: o mesmo teste da última linha, repetido com relógios diferentes, encaixa todas no mesmo loop().

Objetivo: rodar quatro tarefas no mesmo loop(), sem delay(), e medir quantas voltas o loop dá por segundo. No Wokwi: ESP32 + LED no GPIO 2 + potenciômetro no GPIO 34.

multitarefa-millis.ino
// Quatro tarefas no mesmo loop, sem delay() (ESP32, pacote esp32 3.x)
const int PINO_LED = 2;      // LED pisca a cada 500 ms
const int PINO_BOTAO = 0;    // botão BOOT da placa (LOW = apertado)
const int PINO_POT = 34;     // potenciômetro (pino só de entrada, ADC1)
unsigned long tLed = 0, tPot = 0, tSerial = 0, voltas = 0;   // voltas: desde o último relatório
bool ledLigado = false;
int leituraPot = 0, botaoAntes = HIGH;
void setup() {
  Serial.begin(115200);
  pinMode(PINO_LED, OUTPUT); pinMode(PINO_BOTAO, INPUT_PULLUP);
  Serial.println("Quatro tarefas sem delay: pronto");
}
void loop() {
  unsigned long agora = millis();
  voltas++;
  if (agora - tLed >= 500) {          // tarefa 1: piscar
    tLed = agora; ledLigado = !ledLigado; digitalWrite(PINO_LED, ledLigado);
  }
  if (agora - tPot >= 20) {           // tarefa 2: ler o sensor 50 vezes por segundo (0 a 4095)
    tPot = agora; leituraPot = analogRead(PINO_POT);
  }
  if (agora - tSerial >= 1000) {      // tarefa 3: relatório
    tSerial = agora;
    Serial.printf("pot=%d | voltas do loop no ultimo segundo=%lu\n", leituraPot, voltas);
    voltas = 0;
  }
  int botao = digitalRead(PINO_BOTAO); // tarefa 4: botão, a qualquer momento
  if (botaoAntes == HIGH && botao == LOW) Serial.println("Botao apertado");
  botaoAntes = botao;
}

Como verificar

  • ✓O LED pisca uma vez por segundo, sempre no mesmo ritmo, enquanto você gira o potenciômetro.
  • ✓O relatório mostra centenas de milhares de voltas por segundo e o "Botao apertado" aparece na hora.
  • ✗Coloque um delay(500) no fim do loop: as voltas caem para 2 e o botão passa a falhar.

💡 E quando o millis() "virar"?

O contador volta a zero depois de cerca de 49 dias. Se você sempre comparar com subtração (agora - tLed >= 500) e variáveis unsigned long, a conta continua certa mesmo na virada. Evite agora >= tLed + 500: essa forma quebra.

Por que aprender

É a técnica mais simples e funciona em qualquer placa, inclusive ESP32-C3 e C6, que têm um núcleo só. Para a maioria dos robôs da Trilha 2, um loop com millis() bem organizado já resolve.

Conceitos-chave

millis()
relógio desde o boot
Período
de quanto em quanto
Borda
o instante da mudança
unsigned long
sobrevive à virada
3

🔀 Máquina de estados de verdade

Quando o comportamento cresce, os if se misturam: "se está andando e viu parede, mas não se já está girando, a não ser que...". A máquina de estados põe ordem: o robô está sempre em um estado com nome (ESPERANDO, ANDANDO, RECUANDO, GIRANDO), cada estado sabe o que fazer com os motores e quais eventos o levam ao próximo. O tempo dentro do estado vem do millis(), então nada bloqueia.

ESPERANDO ANDANDO RECUANDO GIRANDO botão dist < 20 cm 400 ms 350 ms botão(qualquer estado)

Como ler: cada cápsula é um estado e cada seta azul é uma transição com a sua condição escrita ao lado. A seta vermelha tracejada vale para todos: o botão é a parada que funciona em qualquer estado. Desenhe isto no papel antes de escrever o switch.

Objetivo: o rover da Trilha 1 com a lógica de desvio reescrita como máquina de estados. Ligações: TB6612FNG (PWMA 25, AIN1 26, AIN2 27, PWMB 14, BIN1 16, BIN2 17, STBY no 3V3), HC-SR04 (TRIG 13, ECHO 4 por divisor de tensão para 3,3 V), botão BOOT.

rover-estados.ino
// Rover com máquina de estados (ESP32 3.x + TB6612FNG + HC-SR04)
enum Estado { ESPERANDO, ANDANDO, RECUANDO, GIRANDO };
const char *NOMES[] = { "ESPERANDO", "ANDANDO", "RECUANDO", "GIRANDO" };
Estado estado = ESPERANDO;
unsigned long inicioEstado = 0, tSom = 0;
float dist = 999;
const int PWMA = 25, AIN1 = 26, AIN2 = 27;   // motor esquerdo
const int PWMB = 14, BIN1 = 16, BIN2 = 17;   // motor direito
const int TRIG = 13, ECHO = 4, BOTAO = 0, VEL = 170;   // VEL: cruzeiro (0 a 255)

void motor(int pwm, int in1, int in2, int v) {   // v de -255 a 255; ledcWrite recebe o pino (3.x)
  digitalWrite(in1, v > 0); digitalWrite(in2, v < 0); ledcWrite(pwm, abs(v));
}
void andar(int esq, int dir) { motor(PWMA, AIN1, AIN2, esq); motor(PWMB, BIN1, BIN2, dir); }
float distanciaCm() {
  digitalWrite(TRIG, LOW); delayMicroseconds(2); digitalWrite(TRIG, HIGH); delayMicroseconds(10); digitalWrite(TRIG, LOW);
  unsigned long us = pulseIn(ECHO, HIGH, 25000);   // no máximo 25 ms de espera
  return us == 0 ? 999 : us / 58.0;                // 0 = nada à frente
}
void mudarPara(Estado novo) {
  estado = novo; inicioEstado = millis();
  Serial.printf("[%lu ms] -> %s\n", inicioEstado, NOMES[novo]);
}
bool botaoApertado() {                             // só a borda de apertar
  static int antes = HIGH;
  int agora = digitalRead(BOTAO);
  bool borda = (antes == HIGH && agora == LOW);
  antes = agora; return borda;
}
void setup() {
  Serial.begin(115200);
  pinMode(AIN1, OUTPUT); pinMode(AIN2, OUTPUT); pinMode(BIN1, OUTPUT); pinMode(BIN2, OUTPUT);
  ledcAttach(PWMA, 20000, 8); ledcAttach(PWMB, 20000, 8);   // pino, 20 kHz, 8 bits
  pinMode(TRIG, OUTPUT); pinMode(ECHO, INPUT); pinMode(BOTAO, INPUT_PULLUP);
  mudarPara(ESPERANDO);
}
void loop() {
  unsigned long noEstado = millis() - inicioEstado;
  bool botao = botaoApertado();
  if (millis() - tSom >= 60) { tSom = millis(); dist = distanciaCm(); }   // ping a cada 60 ms
  if (botao && estado != ESPERANDO) { andar(0, 0); mudarPara(ESPERANDO); return; }
  switch (estado) {
    case ESPERANDO: andar(0, 0);         if (botao) mudarPara(ANDANDO);           break;
    case ANDANDO:   andar(VEL, VEL);     if (dist < 20) mudarPara(RECUANDO);      break;
    case RECUANDO:  andar(-VEL, -VEL);   if (noEstado > 400) mudarPara(GIRANDO);  break;
    case GIRANDO:   andar(VEL, -VEL);    if (noEstado > 350) mudarPara(ANDANDO);  break;
  }
}

Como verificar (rodas no ar primeiro)

  • ✓O Serial Monitor mostra cada troca: [5123 ms] -> ANDANDO, depois RECUANDO e GIRANDO ao aproximar a mão.
  • ✓Apertar BOOT para os motores na hora, em qualquer estado.
  • ✗Rodas girando ao contrário? Troque os fios daquele motor ou inverta o sinal de v.

Dois erros comuns: um delay(400) dentro de RECUANDO (volta o tempo cego) e estados sem nome (modo = 3), que ninguém lembra o que significam um mês depois.

Por que aprender

Robôs de competição, aspiradores e o Nav2 da Trilha 4 pensam em estados. O log de trocas vira um diário do robô: quando algo der errado, você lê em que estado ele estava e o que o fez mudar.

Conceitos-chave

Estado
o que o robô está fazendo
Transição
evento que muda o estado
enum + switch
estados com nome
Tempo no estado
millis() − início
4

🧠 FreeRTOS no ESP32: tarefas e núcleos

O ESP32 já roda um pequeno sistema operacional por baixo do Arduino: o FreeRTOS. O seu loop() é uma tarefa dele, no núcleo 1; o Wi-Fi e o Bluetooth moram no núcleo 0. Você pode criar suas próprias tarefas, cada uma com um for (;;) infinito, prioridade e núcleo. Quando uma tarefa chama vTaskDelay(), ela dorme e o escalonador entrega o processador a outra. Aqui a espera não é tempo cego.

🌱 Novo aqui?

Um RTOS (sistema operacional de tempo real) garante que a tarefa mais importante rode quando precisa. Uma tarefa é uma função que roda "em paralelo" com as outras. O escalonador decide quem usa cada núcleo a cada instante, pela prioridade: número maior passa na frente.

Núcleo 0 Wi-Fi / BT tele Wi-Fi / BT dorme 500 ms Núcleo 1 ctrlctrlctrlctrl loop()loop()livre 10 ms

Como ler: o tempo corre da esquerda para a direita. No núcleo 1, a tarefa de controle (ctrl) acorda a cada 10 ms em ponto e o loop() usa as sobras. No núcleo 0, a telemetria (tele) divide espaço com o Wi-Fi. Separar assim impede que um pico de rede atrase o controle dos motores.

Parâmetro de xTaskCreatePinnedToCoreO que é
tarefaControlea função com o for (;;)
"controle"nome, aparece nas mensagens de erro
4096pilha em bytes no ESP32 (no FreeRTOS padrão é em palavras)
NULLparâmetro entregue à função
2prioridade (o loop() tem 1)
&hControle"alça" para pausar ou apagar a tarefa depois
1núcleo: 0 ou 1 (C3 e C6 só têm o 0)

Objetivo: uma tarefa de controle a 100 Hz exatos no núcleo 1 (lê o potenciômetro e ajusta o brilho do LED) e uma de telemetria no núcleo 0. Mesmo circuito do tópico 2.

freertos-duas-tarefas.ino
// Duas tarefas FreeRTOS em núcleos diferentes (ESP32 de dois núcleos, pacote esp32 3.x)
const int PINO_LED = 2, PINO_POT = 34;
TaskHandle_t hControle = NULL, hTelemetria = NULL;
volatile int brilho = 0;                  // escrito pelo controle, lido pela telemetria
volatile unsigned long ciclos = 0;
void tarefaControle(void *param) {
  TickType_t ultimo = xTaskGetTickCount();
  const TickType_t periodo = pdMS_TO_TICKS(10);   // 100 vezes por segundo, sem deriva
  for (;;) {
    brilho = analogRead(PINO_POT) / 16;           // 0..4095 vira 0..255
    ledcWrite(PINO_LED, brilho); ciclos++;
    vTaskDelayUntil(&ultimo, periodo);            // dorme até o próximo tique de 10 ms
  }
}
void tarefaTelemetria(void *param) {
  for (;;) {
    Serial.printf("nucleo %d | brilho=%d | ciclos de controle=%lu\n", xPortGetCoreID(), brilho, ciclos);
    vTaskDelay(pdMS_TO_TICKS(500));               // cede o núcleo por 500 ms
  }
}
void setup() {
  Serial.begin(115200);
  ledcAttach(PINO_LED, 5000, 8);
  // função, nome, pilha (bytes), parâmetro, prioridade, alça, núcleo
  xTaskCreatePinnedToCore(tarefaControle,   "controle",   4096, NULL, 2, &hControle,   1);
  xTaskCreatePinnedToCore(tarefaTelemetria, "telemetria", 4096, NULL, 1, &hTelemetria, 0);
  Serial.printf("setup rodou no nucleo %d\n", xPortGetCoreID());
}
void loop() { vTaskDelay(pdMS_TO_TICKS(1000)); }   // o loop() também é tarefa; aqui só descansa

Como verificar

  • ✓"setup rodou no nucleo 1" e, a cada meio segundo, "nucleo 0 | brilho=... | ciclos de controle=..."
  • ✓Os ciclos sobem cerca de 50 entre uma linha e outra (100 por segundo).

💡 "Task watchdog got triggered"

Se uma tarefa entra num for (;;) que nunca chama vTaskDelay (nem espera fila ou semáforo), ela não larga o núcleo, a tarefa ociosa não roda e o cão de guarda reinicia a placa. Toda tarefa precisa dormir ou esperar em algum ponto do laço.

Por que aprender

Com Wi-Fi, MQTT e PID juntos, o controle precisa de um relógio que a rede não atrapalhe. vTaskDelayUntil dá exatamente isso: o mesmo dt a cada ciclo, que é o que o PID do módulo 2.3 assume.

Conceitos-chave

Tarefa
função com laço próprio
Prioridade
maior passa na frente
vTaskDelayUntil
período fixo, sem deriva
Watchdog
reinicia tarefa presa
5

📬 Filas e semáforos: tarefas conversando

Duas tarefas mexendo na mesma variável ao mesmo tempo podem ler um dado pela metade. O FreeRTOS oferece dois correios seguros. A fila (queue) entrega mensagens em ordem: a tarefa do sensor coloca uma leitura, a do motor retira; se a fila está vazia, quem espera dorme sem gastar processador. O mutex é uma chave única: só quem está com ela usa o recurso (a Serial, o cartão SD, o barramento I2C) e depois devolve.

sensora cada 20 ms atuadoracorda com dado fila: 10 lugares, sai na ordem em que entrou Serial mutex: 1 chave

Como ler: embaixo, os dados andam da esquerda para a direita pela fila (os quadrados mais claros já estão ocupados). Em cima, as linhas tracejadas mostram que as duas tarefas querem a Serial, mas só quem segura a chave escreve; a outra espera a sua vez.

Objetivo: a tarefa do sensor manda leituras por uma fila; a do atuador ajusta o LED e, se ficar 100 ms sem dado, desliga tudo (parada segura). As duas escrevem na Serial protegidas por mutex.

fila-e-mutex.ino
// Fila (sensor -> atuador) e mutex (Serial compartilhada), FreeRTOS no ESP32
struct Leitura { uint32_t t_ms; int valor; };
QueueHandle_t fila;
SemaphoreHandle_t travaSerial;
const int PINO_LED = 2, PINO_POT = 34;
void logSeguro(const char *texto) {
  if (xSemaphoreTake(travaSerial, pdMS_TO_TICKS(50)) == pdTRUE) {   // pega a chave
    Serial.println(texto);
    xSemaphoreGive(travaSerial);                                     // devolve a chave
  }
}
void tarefaSensor(void *p) {
  for (;;) {
    Leitura l = { (uint32_t)millis(), analogRead(PINO_POT) };
    if (xQueueSend(fila, &l, 0) != pdTRUE) logSeguro("fila cheia: leitura descartada");
    vTaskDelay(pdMS_TO_TICKS(20));
  }
}
void tarefaAtuador(void *p) {
  Leitura l; bool alto = false; char msg[64];
  for (;;) {
    if (xQueueReceive(fila, &l, pdMS_TO_TICKS(100)) == pdTRUE) {    // dorme até chegar dado
      ledcWrite(PINO_LED, l.valor / 16);
      bool agoraAlto = l.valor > 3500;
      if (agoraAlto != alto) {                                       // avisa só na mudança
        alto = agoraAlto; snprintf(msg, sizeof msg, "%lu ms: valor %s (%d)", (unsigned long)l.t_ms, alto ? "alto" : "normal", l.valor);
        logSeguro(msg);
      }
    } else {
      ledcWrite(PINO_LED, 0);                                        // 100 ms sem dado: parada segura
      logSeguro("sem leitura: atuador parado");
    }
  }
}
void setup() {
  Serial.begin(115200);
  ledcAttach(PINO_LED, 5000, 8);
  fila =xQueueCreate(10, sizeof(Leitura));        // cabem 10 leituras
  travaSerial = xSemaphoreCreateMutex();
  xTaskCreatePinnedToCore(tarefaSensor,  "sensor",  3072, NULL, 2, NULL, 1);
  xTaskCreatePinnedToCore(tarefaAtuador, "atuador", 4096, NULL, 2, NULL, 1);
  logSeguro("fila e mutex prontos");
}
void loop() { vTaskDelay(pdMS_TO_TICKS(1000)); }

Como verificar

  • ✓O brilho do LED acompanha o potenciômetro; girar até o fim mostra "valor alto", voltar mostra "valor normal".
  • ✓Comente a linha do xQueueSend: o LED apaga e aparece "sem leitura: atuador parado". É a parada segura do módulo 2.1, agora entre tarefas.

✓ Use fila quando

  • ✓Uma tarefa produz dados e outra consome (sensor → controle, controle → log).
  • ✓Você quer que o consumidor durma até chegar algo, com prazo.

✗ Armadilhas

  • ✗Pegar o mutex e esquecer de devolver: as outras tarefas travam para sempre.
  • ✗Struct grande compartilhada sem proteção: dado lido pela metade.

Por que aprender

É assim que a telemetria do módulo 2.5 vai funcionar sem atrasar o controle: o laço de controle só coloca um pacote na fila, e outra tarefa, sem pressa, grava no cartão ou manda pela rede.

Conceitos-chave

Fila
mensagens em ordem
Mutex
uma chave por recurso
Condição de corrida
dois mexem ao mesmo tempo
Prazo de espera
detecta silêncio
6

🗂️ Organizar o código em arquivos e classes

Um sketch de 600 linhas num arquivo só vira um labirinto. A Arduino IDE 2.x aceita várias abas na mesma pasta: o .h diz o que uma peça sabe fazer, o .cpp diz como. Uma classe junta dados (os pinos do motor) e funções (girar, frear). Criar dois objetos da mesma classe, um para cada roda, elimina a repetição.

Passo a passo na Arduino IDE 2.x

1

Salve o sketch

Salve como robo-organizado. A IDE cria a pasta com o mesmo nome e o .ino dentro.

2

Crie as abas

No menu de três pontos ao lado das abas, escolha "Nova aba" e crie Motor.h e depois Motor.cpp.

3

Cole os três blocos e envie

Cada bloco abaixo vai na aba com o mesmo nome. A IDE compila tudo junto. Teste com o robô suspenso, rodas no ar.

Motor.h
// Motor.h: um motor DC num canal da ponte H TB6612FNG
// (o DRV8833 não tem pino PWM separado: adapte velocidade() para PWM nos IN1/IN2)
#pragma once
#include <Arduino.h>
class Motor {
public:
  Motor(int pinoPwm, int pinoIn1, int pinoIn2, bool invertido = false);
  void begin();                  // configura pinos e PWM (API 3.x)
  void velocidade(int v);        // -255 (ré) a 255 (frente); 0 = solto
  void freio();                  // trava a roda
  int atual() const { return _v; }
private:
  int _pwm, _in1, _in2, _v = 0;
  bool _inv;
};
Motor.cpp
#include "Motor.h"

Motor::Motor(int pinoPwm, int pinoIn1, int pinoIn2, bool invertido)
  : _pwm(pinoPwm), _in1(pinoIn1), _in2(pinoIn2), _inv(invertido) {}
void Motor::begin() {
  pinMode(_in1, OUTPUT); pinMode(_in2, OUTPUT);
  ledcAttach(_pwm, 20000, 8);    // 20 kHz (inaudível), 8 bits = 0..255
  velocidade(0);
}
void Motor::velocidade(int v) {
  v = constrain(v, -255, 255); _v = v;
  if (_inv) v = -v;              // motor montado espelhado
  digitalWrite(_in1, v > 0); digitalWrite(_in2, v < 0);
  ledcWrite(_pwm, abs(v));
}
void Motor::freio() {            // TB6612FNG: IN1 = IN2 = HIGH trava
  digitalWrite(_in1, HIGH); digitalWrite(_in2, HIGH); ledcWrite(_pwm, 255); _v = 0;
}
robo-organizado.ino
// robo-organizado.ino: o sketch principal fica curto e legível
#include "Motor.h"

Motor esquerdo(25, 26, 27);          // PWMA, AIN1, AIN2
Motor direito(14, 16, 17, true);     // PWMB, BIN1, BIN2 (montado espelhado)
const int SEQ[] = { 150, 0, -150, 0 };

void setup() {
  Serial.begin(115200);
  esquerdo.begin();
  direito.begin();
  Serial.println("Motores prontos: teste com as rodas no ar");
}

void loop() {
  for (int v : SEQ) {
    esquerdo.velocidade(v);
    direito.velocidade(v);
    Serial.printf("esq=%d dir=%d\n", esquerdo.atual(), direito.atual());
    delay(1000);   // aqui pode: é um teste de bancada, não o robô andando
  }
}

Como verificar

  • ✓As duas rodas giram para frente, param, giram para trás e param, a cada segundo, na mesma direção.
  • ✗Uma roda ao contrário? Troque o true de lado no construtor, sem mexer em fio.

Por que aprender

Código organizado é o que permite voltar ao robô depois de um mês, dividir o trabalho num grupo ou trocar a ponte H sem reescrever tudo. Uma aba por peça (Motor, BarraIR, Pid, Telemetria) e o .ino só liga as peças: é o mesmo formato das bibliotecas que você instala pela IDE. Guarde a pasta num repositório Git e faça um commit a cada ajuste que funciona.

Conceitos-chave

.h
o que a peça faz
.cpp
como ela faz
Classe
dados + funções juntos
Objeto
uma roda, um motor

✅ Resumo do módulo

✓
delay() cria tempo cego: enquanto espera, o robô não sente nem reage.
✓
millis() com subtração encaixa várias tarefas no mesmo loop, cada uma no seu ritmo.
✓
Máquina de estados: enum + switch + tempo no estado + uma parada que vale sempre.
✓
FreeRTOS: controle no núcleo 1 com vTaskDelayUntil; rede e telemetria no núcleo 0.
✓
Fila e mutex passam dados entre tarefas sem corrida; prazo na fila vira parada segura.

Para ir além

Próximo módulo

2.5 · Telemetria e registro de dados: ver o que o robô sente e decide, ao vivo e em arquivo.