Conteúdo detalhado
⏳ 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.
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
⏱️ 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.
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.
// 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
🔀 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.
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 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
🧠 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.
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 xTaskCreatePinnedToCore | O que é |
|---|---|
| tarefaControle | a função com o for (;;) |
| "controle" | nome, aparece nas mensagens de erro |
| 4096 | pilha em bytes no ESP32 (no FreeRTOS padrão é em palavras) |
| NULL | parâmetro entregue à função |
| 2 | prioridade (o loop() tem 1) |
| &hControle | "alça" para pausar ou apagar a tarefa depois |
| 1 | nú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.
// 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
📬 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.
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 (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
🗂️ 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
Salve o sketch
Salve como robo-organizado. A IDE cria a pasta com o mesmo nome e o .ino dentro.
Crie as abas
No menu de três pontos ao lado das abas, escolha "Nova aba" e crie Motor.h e depois Motor.cpp.
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: 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;
};
#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: 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
✅ Resumo do módulo
Para ir além
- FreeRTOS no ESP-IDF (Espressif) — tarefas, núcleos e diferenças do FreeRTOS padrão
- Filas no FreeRTOS — documentação oficial do kernel
- Arduino-ESP32 (Espressif) — referência do pacote 3.x
- millis() na referência Arduino
- Documentação do Wokwi — todos os exemplos deste módulo rodam lá
Próximo módulo
2.5 · Telemetria e registro de dados: ver o que o robô sente e decide, ao vivo e em arquivo.