PTENES
MÓDULO 3.5 · PRÁTICA

🔌 Raspberry Pi conversa com o ESP32

O Pi 5 enxerga e decide; o ESP32 gira os motores e cuida da segurança. Neste módulo você liga os dois por três fios (UART), cria um protocolo de linhas de texto com checksum e garante que o robô para sozinho se a conversa cair.

6
Tópicos
Intermediário
Nível
Prática
Tipo
Pi 5 + ESP32
Hardware
0%
0 de 0 tópicos lidos · Seção 1 de 6

Conteúdo detalhado

1

🤝 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.

RASPBERRY PI 5câmera e visãodecide o que fazerLinux: tempo variável"o quê" ESP32PWM e ponte Hlimites e timeoutlaço previsível"como" e "quando parar" comandos~20 por segundo respostas três fios: TX, RX e GND

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

Alto nível
decide (Pi)
Baixo nível
executa (ESP32)
Tempo real
resposta no prazo
Intenção
o quê, não o como
2

🧵 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.

Pi 5 (40 pinos) pino 8 · GPIO14 TXpino 10 · GPIO15 RXpino 6 · GND ESP32 GPIO16 · RX2GPIO17 · TX2GND Pi fala → ESP32 ouveESP32 fala → Pi ouvereferência comum 3,3 V nos dois lados: ligação direta

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

1

Ligue a porta serial

sudo raspi-config → Interface Options → Serial Port. "Login shell pela serial?" Não. "Hardware da porta serial?" Sim. Reinicie.

2

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.

3

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.

terminal do Pi
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

TX → RX
fios cruzados
Baud
115200 nos dois
/dev/serial0
UART do Pi
Loopback
TX ligado no RX
3

📨 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.

$ M,120,-80 * 5B \n iníciocorpo: comando e valoresseparachecksumfim checksum = XOR de todos os bytes do corpo, em hexadecimal um byte trocado no caminho muda o XOR e a mensagem é descartada

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.

MensagemSentidoSignificado
$M,esq,dir*HHPi → ESP32velocidade de cada lado, de −255 a 255
$S*53Pi → ESP32parar agora
$P*50Pi → ESP32ping: você está aí?
$OK,M*65ESP32 → Picomando aceito
$ERR,motivo*HHESP32 → PiCHK, 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

Moldura
$ … \n
Checksum XOR
detecta byte trocado
Resposta
OK ou ERR
Faixa válida
−255 a 255
4

🛡️ 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.

receptor-pi.ino
// 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

Serial2.begin
baud, formato, RX, TX
Buffer limitado
nada de linha infinita
constrain
limita a velocidade
Timeout
300 ms e para
5

🐍 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).

link_esp32.py
#!/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

pyserial
serial no Python
timeout de leitura
não trava esperando
20 Hz
comando repetido
try/finally
para ao sair
6

🚨 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.

liga /reset PARADO ANDANDO M válido S ou 300 ms sem M M válido: renova o prazo ERR: ignora e responde

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

Watchdog de comando
comando com prazo
Estado seguro
parado ao ligar
OK,BOOT
aviso de reset
Corte físico
chave na bateria

✅ Resumo do módulo

✓
Pi decide, ESP32 executa e protege: intenções a cerca de 20 Hz, PWM e limites no microcontrolador.
✓
UART em 3,3 V: pinos 8/10/6 do Pi nos GPIO16/17/GND do ESP32, TX no RX, sem conversor.
✓
Protocolo: $corpo*HH\n com checksum XOR e resposta OK/ERR.
✓
Firmware defensivo: buffer limitado, checksum, constrain e timeout de 300 ms.
✓
Falhar parado: fio solto, Python travado ou reset terminam com motores desligados.

Para ir além

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.