Conteúdo detalhado
🤝 Divisão de trabalho: Pi pensa, ESP32 age
O Raspberry Pi roda Linux, que é ótimo para câmera, OpenCV e YOLO26, mas não garante o tempo: uma atualização em segundo plano pode atrasar o seu programa em centenas de milissegundos. O ESP32 roda um laço simples e previsível, ideal para gerar PWM, ler encoders e parar os motores na hora certa. A divisão clássica é: o Pi manda intenções ("vá a 60% para a esquerda") algumas dezenas de vezes por segundo, e o ESP32 executa e protege.
Como ler: a seta roxa leva intenções do Pi ao ESP32; a tracejada ciano traz a confirmação. A regra de ouro está na caixa da direita: quem toca no motor é quem decide parar. Se o Pi travar, o ESP32 não fica esperando.
✓ Faça
- ✓Pi manda velocidade desejada; ESP32 limita e aplica.
- ✓ESP32 para os motores se os comandos pararem de chegar.
- ✓Sensor de colisão ligado direto no ESP32, que freia sem perguntar.
✗ Não faça
- ✗Gerar o PWM do motor pelo Python no Pi.
- ✗Mandar "liga o motor" uma vez e confiar que um "desliga" vai chegar.
- ✗Alimentar o Pi pela mesma linha que os motores, sem regulador.
Por que aprender
Essa divisão aparece em quase todo robô sério, do aspirador ao robô de armazém. Na Trilha 4 ela continua igual: o Pi roda o ROS 2 e o ESP32 conversa com ele pelo micro-ROS. O que muda é só o protocolo.
Conceitos-chave
🧵 UART entre Pi e ESP32 (níveis 3,3 V)
🌱 Novo aqui?
UART é a porta serial: um fio para mandar (TX), um para receber (RX) e o GND como referência. Os dois lados combinam a velocidade em baud (bits por segundo); usamos 115200. O TX de um vai no RX do outro, cruzado. O Serial Monitor que você usa desde o módulo 1.3 é uma UART passando pelo cabo USB.
O Pi 5 e o ESP32 trabalham com lógica de 3,3 V, então os fios ligam direto, sem conversor. No Pi use os pinos físicos 8 (GPIO14, TXD) e 10 (GPIO15, RXD); no ESP32, a UART2 nos GPIO16 (RX) e GPIO17 (TX), deixando a UART0 livre para o USB e as mensagens de boot. Outro caminho, mais simples, é o próprio cabo USB do ESP32 no Pi (aparece como /dev/ttyACM0 ou /dev/ttyUSB0); o protocolo e o código são os mesmos.
Como ler: cada linha é um fio. O fio roxo leva o que o Pi transmite até a entrada do ESP32; o ciano faz o caminho oposto. O GND tracejado é obrigatório: sem ele os dois não concordam sobre o que é 0 V e chegam letras trocadas.
Passo a passo no Pi
Ligue a porta serial
sudo raspi-config → Interface Options → Serial Port. "Login shell pela serial?" Não. "Hardware da porta serial?" Sim. Reinicie.
Dê permissão ao seu usuário
sudo usermod -aG dialout $USER, depois saia e entre de novo. Confira que ls -l /dev/serial0 existe.
Teste de laço (loopback)
Antes de ligar o ESP32, faça um jumper entre os pinos 8 e 10 do Pi e rode o teste abaixo. O que sai pelo TX tem que voltar pelo RX.
python3 -m venv --system-site-packages ~/robo-venv # só se ainda não criou no módulo 3.1
source ~/robo-venv/bin/activate
pip install pyserial
python -c "import serial; s=serial.Serial('/dev/serial0',115200,timeout=1); s.write(b'ola\n'); print(s.readline())"
Como verificar: com o jumper, a saída é b'ola\n'. Sem o jumper, sai b'' depois de 1 segundo. "Permission denied" significa que o passo 2 ainda não valeu: saia e entre de novo na sessão.
Por que aprender
A UART é o jeito mais simples e mais depurável de fazer duas placas conversarem: você pode ler tudo com um Serial Monitor. Testar o Pi sozinho com o loopback separa problema de fiação de problema de código.
Conceitos-chave
📨 Protocolo simples de mensagens
Os bytes chegam soltos pelo fio; o protocolo é o combinado que diz onde começa e termina cada mensagem e como saber se ela chegou inteira. Vamos usar uma linha de texto por mensagem, no estilo do GPS (NMEA): começa com $, traz o comando, um *, dois dígitos de checksum e termina com quebra de linha. Texto é legível no Serial Monitor e fácil de gerar em qualquer linguagem.
Como ler: só o corpo roxo carrega informação; as caixas ciano são moldura. O checksum é calculado pelo Pi, enviado junto e recalculado pelo ESP32. Se os dois números não baterem, o ESP32 responde erro e ignora o comando.
| Mensagem | Sentido | Significado |
|---|---|---|
| $M,esq,dir*HH | Pi → ESP32 | velocidade de cada lado, de −255 a 255 |
| $S*53 | Pi → ESP32 | parar agora |
| $P*50 | Pi → ESP32 | ping: você está aí? |
| $OK,M*65 | ESP32 → Pi | comando aceito |
| $ERR,motivo*HH | ESP32 → Pi | CHK, FORMATO, ARG, CMD, LONGA ou TIMEOUT |
✓ Linha de texto curta
- ✓Você lê e digita no Serial Monitor.
- ✓Cabe num buffer de 40 bytes, sem biblioteca extra.
- ✓JSON curto ({"m":[120,-80]}) também serve, com ArduinoJson.
✗ Evite
- ✗Mensagem sem marcador de fim: você nunca sabe onde ela acaba.
- ✗Aceitar comando sem checksum num robô com motor.
- ✗Mensagens longas e frequentes: 115200 baud são cerca de 11 mil bytes por segundo.
🌱 Novo aqui?
Checksum é um número calculado a partir da mensagem que viaja junto com ela. O XOR compara bits: dá 1 quando são diferentes e 0 quando são iguais. Fazendo o XOR de todos os bytes, qualquer byte trocado muda o resultado. Não protege contra tudo, mas pega a maioria dos ruídos de fio.
Por que aprender
Um motor que recebe "M,1200" em vez de "M,120" por causa de um byte duplicado é um robô batendo na parede. Moldura, checksum e respostas explícitas transformam um fio ruidoso num canal confiável, e a mesma ideia está nos protocolos industriais.
Conceitos-chave
🛡️ Lado ESP32: receber comandos com segurança
O sketch junta letra por letra até a quebra de linha, descarta linhas longas demais, confere moldura e checksum, limita as velocidades e aplica na ponte H TB6612FNG do módulo 1.5 com a API 3.x do PWM. A peça mais importante são as últimas linhas do loop(): se nenhum comando de motor válido chegar em 300 ms, os motores param.
Objetivo: ESP32 que obedece ao protocolo do tópico 3 e para sozinho sem comandos. Pinos de um ESP32 DevKit; num ESP32-S3, troque por pinos livres da sua placa.
// Recebe comandos do Raspberry Pi pela UART2 e move os motores (ESP32, pacote esp32 3.x)
// Protocolo: "$M,esq,dir*HH\n" "$S*53\n" "$P*50\n" (HH = XOR do corpo em hexadecimal)
const int PINO_RX = 16, PINO_TX = 17; // RX (GPIO16) recebe do pino 8 do Pi; TX (GPIO17) vai ao pino 10
const int PWMA = 25, AIN1 = 26, AIN2 = 27; // TB6612FNG: motor esquerdo
const int PWMB = 33, BIN1 = 32, BIN2 = 4; // motor direito (mesmos pinos do rover da Trilha 1)
const int STBY = 13; // HIGH = ponte H ligada
const unsigned long TIMEOUT_MS = 300; // sem comando de motor por 300 ms: para
const int MAX_LINHA = 40;
char linha[MAX_LINHA + 1];
int tam = 0;
bool descartando = false, andando = false;
unsigned long ultimoComando = 0;
uint8_t xorDe(const char *s, int n) { uint8_t x = 0; for (int i = 0; i < n; i++) x ^= (uint8_t)s[i]; return x; }
void responde(const char *corpo) { // envia "$corpo*HH\n" ao Pi
char buf[56];
snprintf(buf, sizeof(buf), "$%s*%02X\n", corpo, xorDe(corpo, strlen(corpo)));
Serial2.print(buf);
}
void motor(int pwm, int in1, int in2, int v) {
v = constrain(v, -255, 255);
digitalWrite(in1, v > 0 ? HIGH : LOW);
digitalWrite(in2, v < 0 ? HIGH : LOW);
ledcWrite(pwm, abs(v)); // API 3.x: ledcWrite recebe o pino
}
void mover(int esq, int dir) { motor(PWMA, AIN1, AIN2, esq); motor(PWMB, BIN1, BIN2, dir); }
void parar() { mover(0, 0); andando = false; }
void trata(char *l) { // l = "$M,120,-80*5B" (sem o \n)
char *estrela = strrchr(l, '*');
if (l[0] != '$' || estrela == NULL || strlen(estrela) != 3) { responde("ERR,FORMATO"); return; }
char *corpo = l + 1;
int n = estrela - corpo;
if (xorDe(corpo, n) != (uint8_t)strtol(estrela + 1, NULL, 16)) { responde("ERR,CHK"); return; }
*estrela = '\0'; // corta o checksum: sobra só o corpo
if (corpo[0] == 'M' && corpo[1] == ',') {
int esq, dir;
if (sscanf(corpo + 2, "%d,%d", &esq, &dir) != 2) { responde("ERR,ARG"); return; }
mover(esq, dir); andando = true; ultimoComando = millis();
responde("OK,M");
} else if (strcmp(corpo, "S") == 0) { parar(); responde("OK,S"); }
else if (strcmp(corpo, "P") == 0) { responde("OK,P"); }
else responde("ERR,CMD");
}
void setup() {
Serial.begin(115200); // USB: mensagens para você
Serial2.begin(115200, SERIAL_8N1, PINO_RX, PINO_TX); // UART2 com pinos explícitos
int saidas[] = { AIN1, AIN2, BIN1, BIN2, STBY };
for (int p : saidas) pinMode(p, OUTPUT);
ledcAttach(PWMA, 20000, 8); // 20 kHz, 8 bits (0 a 255)
ledcAttach(PWMB, 20000, 8);
parar();
digitalWrite(STBY, HIGH);
responde("OK,BOOT"); // avisa o Pi que (re)iniciou
Serial.println("Pronto: esperando comandos na UART2");
}
void loop() {
while (Serial2.available()) {
char c = Serial2.read();
if (c == '\r') continue;
if (c == '\n') {
linha[tam] = '\0';
if (descartando) responde("ERR,LONGA"); else if (tam > 0) trata(linha);
tam = 0; descartando = false;
} else if (tam < MAX_LINHA) linha[tam++] = c;
else descartando = true; // linha grande demais: joga fora até o \n
}
if (andando && millis() - ultimoComando > TIMEOUT_MS) { // parada segura
parar();
responde("ERR,TIMEOUT");
Serial.println("Sem comandos: motores parados");
}
}
Como verificar (antes de ligar no Pi)
- ✓Rodas no ar. O Serial Monitor (USB) mostra "Pronto: esperando comandos na UART2".
- ✓Com um adaptador USB-serial de 3,3 V ligado nos GPIO16/17, envie $M,150,150*4D: as rodas giram e, 300 ms depois, param sozinhas com "Sem comandos".
- ✗Envie $M,150,150*00: a resposta é $ERR,CHK e nada se mexe.
Por que aprender
Este sketch é o "firmware" do robô para o resto da trilha. Tudo o que for decidido no Pi, inclusive por IA, passa por este filtro: só entra o que tem formato certo, checksum certo e valores dentro da faixa. É assim que um modelo que erra não vira um motor descontrolado.
Conceitos-chave
🐍 Lado Pi: Python com pyserial
No Pi, uma classe Link esconde os detalhes: monta a moldura, calcula o checksum, envia, lê a resposta e confere o checksum dela. O demo no fim do arquivo manda o robô para a frente, gira e dá ré, repetindo o comando 20 vezes por segundo, que é o que mantém o ESP32 andando. No módulo 3.6 o seguidor de objetos importa esta mesma classe.
Objetivo: controlar o ESP32 pelo Pi com o protocolo do tópico 3. Salve como link_esp32.py e rode dentro do ambiente virtual com python link_esp32.py. Se usar o cabo USB em vez dos fios, troque a porta por /dev/ttyACM0 (ou /dev/ttyUSB0).
#!/usr/bin/env python3
"""Link serial Pi -> ESP32 (protocolo do módulo 3.5). Requer: pip install pyserial"""
import time
import serial
PORTA = "/dev/serial0" # UART dos pinos 8/10; pelo cabo USB use "/dev/ttyACM0"
BAUD = 115200
def checksum(corpo):
x = 0
for b in corpo.encode("ascii"):
x ^= b
return f"{x:02X}"
class Link:
def __init__(self, porta=PORTA, baud=BAUD):
self.porta, self.baud, self.ser = porta, baud, None
self.abrir()
def abrir(self):
self.ser = serial.Serial(self.porta, self.baud, timeout=0.05)
self.ser.reset_input_buffer()
def ler_resposta(self):
bruto = self.ser.readline().decode("ascii", errors="replace").strip()
if not bruto.startswith("$") or "*" not in bruto:
return None
corpo, chk = bruto[1:].rsplit("*", 1)
return corpo if chk.upper() == checksum(corpo) else None
def enviar(self, corpo):
try:
self.ser.write(f"${corpo}*{checksum(corpo)}\n".encode("ascii"))
return self.ler_resposta()
except (serial.SerialException, OSError) as e:
print("link caiu:", e, "- tentando reabrir")
time.sleep(0.5)
try:
self.ser.close()
self.abrir()
except (serial.SerialException, OSError):
pass
return None
def motores(self, esq, drt):
esq = max(-255, min(255, int(esq)))
drt = max(-255, min(255, int(drt)))
return self.enviar(f"M,{esq},{drt}")
def parar(self):
return self.enviar("S")
def ping(self):
return self.enviar("P")
def fechar(self):
try:
self.parar()
finally:
self.ser.close()
if __name__ == "__main__":
link = Link()
try:
print("ping ->", link.ping())
for nome, esq, drt, segundos in [("frente", 150, 150, 1.0), ("gira", 120, -120, 0.8), ("re", -150, -150, 1.0)]:
print(nome)
fim = time.monotonic() + segundos
while time.monotonic() < fim:
r = link.motores(esq, drt) # repetir a 20 Hz mantém o ESP32 andando
if r != "OK,M":
print("resposta:", r)
time.sleep(0.05)
print("parar ->", link.parar())
except KeyboardInterrupt:
print("interrompido")
finally:
link.fechar() # sempre manda "S" ao sair
Como verificar
- ✓O terminal mostra ping -> OK,P, depois "frente", "gira", "re" e parar -> OK,S.
- ✓Com as rodas no ar, elas seguem a sequência e param no fim.
- ✗ping -> None? Confira TX/RX cruzados, GND comum e se o ESP32 está com o sketch do tópico 4.
💡 Teste sem robô
Sem motores ligados, você ainda pode testar tudo: o ESP32 só na protoboard e o Serial Monitor do USB mostrando "Sem comandos: motores parados" quando você interrompe o Python com Ctrl+C no meio de um movimento.
Por que aprender
Separar o "como falar com a placa" numa classe pequena deixa o resto do código limpo: o seguidor de objetos só chama link.motores(esq, dir). E o try/finally garante o "S" na saída mesmo quando o programa quebra.
Conceitos-chave
🚨 Falhas: perda de link, reset, parada segura
Pense em tudo o que pode dar errado: um fio solta, o Python trava, o Pi reinicia, o ESP32 reinicia por queda de tensão quando o motor arranca. Em todos os casos a resposta certa é a mesma: o robô fica parado até receber de novo um comando válido. O ESP32 já faz isso com o timeout e começa parado ao ligar; o Pi percebe o reset pela mensagem OK,BOOT ou pelos None seguidos.
Como ler: cada caixa é um estado do ESP32 e cada seta é o que faz mudar de estado. Repare que só existe um caminho para ANDANDO (um comando M válido) e dois para voltar a PARADO. O estado inicial, depois de qualquer reset, é sempre PARADO.
✓ Teste de propósito
- ✓Puxe o fio TX do Pi com o robô andando (rodas no ar): para em 300 ms.
- ✓Mate o Python com kill -9: o finally não roda, mas o timeout para.
- ✓Aperte o botão EN do ESP32: ele volta parado e avisa OK,BOOT.
- ✓Tenha uma chave física que corta a bateria dos motores.
✗ Armadilhas
- ✗Timeout longo (2 s) num robô rápido: ele anda metros "às cegas".
- ✗Ping renovando o prazo dos motores: o Pi travado no meio continuaria andando.
- ✗Retomar o último comando sozinho depois de um reset.
- ✗Motor e ESP32 na mesma bateria sem capacitor: cada arranque reinicia a placa.
Por que aprender
Um robô que funciona quando tudo dá certo é metade do trabalho; a outra metade é ele se comportar bem quando algo dá errado. Esse padrão (comando com prazo de validade, estado inicial seguro, parada física independente) é o mesmo dos robôs industriais e volta na Trilha 5 com o braço robótico.
Conceitos-chave
✅ Resumo do módulo
Para ir além
- Arduino-ESP32: Serial (UART) — pinos e parâmetros de Serial2.begin
- Documentação do pyserial — Serial, timeout, readline
- UARTs no Raspberry Pi — serial0, raspi-config e config.txt
- pinout.xyz — mapa dos 40 pinos do Pi
Próximo módulo
3.6 · Projeto: robô que segue um objeto. Câmera, detecção, erro e PID, usando este link para mover as rodas.