PTENES
MÓDULO 3.5 · PRÁCTICA

🔌 La Raspberry Pi se comunica con el ESP32

La Pi 5 ve y decide; el ESP32 hace girar los motores y se encarga de la seguridad. En este módulo conectarás ambos por tres cables (UART), crearás un protocolo de líneas de texto con checksum y te asegurarás de que el robot por sí solo si se cae la conversación.

6
Temas
Intermedio
Nivel
Práctica
Tipo
Pi 5 + ESP32
Hardware
0%
0 de 0 temas leídos · Sección 1 de 6

Contenido detallado

1

🤝 División del trabajo: el Pi piensa, el ESP32 actúa

El Raspberry Pi ejecuta Linux, que es excelente para la cámara, OpenCV y YOLO26, pero no garantiza el tiempo: una actualización en segundo plano puede retrasar tu programa cientos de milisegundos. El ESP32 ejecuta un bucle sencillo y predecible, ideal para generar PWM, leer encoders y detener los motores en el momento adecuado. La división clásica es: el Pi envía intenciones ("ve al 60 % hacia la izquierda") algunas decenas de veces por segundo, y el ESP32 ejecuta y protege.

RASPBERRY PI 5cámara y visióndecide qué hacerLinux: tiempo variable«qué» ESP32PWM y puente Hlímites y tiempo de esperabucle predecible«cómo» y «cuándo detenerse» comandos~20 por segundo respuestas tres cables: TX, RX y GND

Cómo leer: la flecha morada lleva las intenciones del Pi al ESP32; la línea cian discontinua trae la confirmación. La regla de oro está en el recuadro de la derecha: quien controla el motor es quien decide detenerlo. Si el Pi se bloquea, el ESP32 no se queda esperando.

✓ Hazlo

  • ✓El Pi envía la velocidad deseada; el ESP32 la limita y la aplica.
  • ✓El ESP32 detiene los motores si los comandos dejan de llegar.
  • ✓Sensor de colisión conectado directamente al ESP32, que frena sin preguntar.

✗ No lo hagas

  • ✗Generar el PWM del motor desde Python en el Pi.
  • ✗Enviar «enciende el motor» una vez y confiar en que llegará un «apágalo».
  • ✗Alimentar el Pi desde la misma línea que los motores, sin regulador.

Por qué aprender

Esta división aparece en casi todos los robots serios, desde las aspiradoras hasta los robots de almacén. En la Trilha 4 continúa igual: el Pi ejecuta ROS 2 y el ESP32 se comunica con él mediante micro-ROS. Lo único que cambia es el protocolo.

Conceptos clave

Alto nivel
decide (Pi)
Bajo nivel
ejecuta (ESP32)
Tiempo real
respuesta a tiempo
Intención
qué, no cómo
2

🧵 UART entre el Pi y el ESP32 (niveles de 3,3 V)

🌱 ¿Eres nuevo aquí?

UART es el puerto serial: un cable para enviar (TX), uno para recibir (RX) y GND como referencia. Ambos lados acuerdan la velocidad en baudios (bits por segundo); usamos 115200. El TX de uno se conecta al RX del otro, cruzados. El Serial Monitor que usas desde el módulo 1.3 es una UART que pasa por el cable USB.

El Pi 5 y el ESP32 trabajan con lógica de 3,3 V, así que los cables se conectan directamente, sin conversor. En el Pi, usa los pines físicos 8 (GPIO14, TXD) y 10 (GPIO15, RXD); en el ESP32, la UART2 en GPIO16 (RX) y GPIO17 (TX), dejando UART0 libre para el USB y los mensajes de arranque. Otra opción, más sencilla, es conectar el propio cable USB del ESP32 al Pi (aparece como /dev/ttyACM0 o /dev/ttyUSB0); el protocolo y el código son los mismos.

Pi 5 (40 pines) pin 8 · GPIO14 TXpin 10 · GPIO15 RXpin 6 · GND ESP32 GPIO16 · RX2GPIO17 · TX2GND El Pi transmite → el ESP32 escuchaEl ESP32 transmite → el Pi escuchareferencia común 3,3 V en ambos lados: conexión directa

Cómo leer: cada línea representa un cable. El cable morado lleva lo que transmite el Pi hasta la entrada del ESP32; el cian recorre el camino opuesto. El cable GND discontinuo es obligatorio: sin él, los dos no coinciden en qué significa 0 V y reciben caracteres cambiados.

Paso a paso en el Pi

1

Activa el puerto serial

sudo raspi-config → Interface Options → Serial Port. «¿Login shell por la serial?» No. «¿Hardware del puerto serial?» Sí. Reinicia.

2

Da permiso a tu usuario

sudo usermod -aG dialout $USER, luego cierra la sesión y vuelve a entrar. Comprueba que ls -l /dev/serial0 exista.

3

Prueba de bucle (loopback)

Antes de conectar el ESP32, coloca un jumper entre los pines 8 y 10 del Pi y ejecuta la prueba de abajo. Lo que sale por TX debe volver por RX.

terminal del Pi
python3 -m venv --system-site-packages ~/robo-venv   # solo si aún no lo creaste en el 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())"

Cómo verificar: con el jumper, la salida es b'ola\n'. Sin el jumper, aparece b'' después de 1 segundo. «Permission denied» significa que el paso 2 aún no se aplicó: cierra la sesión y vuelve a entrar.

Por qué aprender

UART es la forma más sencilla y fácil de depurar para que dos placas se comuniquen: puedes leer todo con un Serial Monitor. Probar el Pi por separado con el loopback permite distinguir un problema de cableado de uno de código.

Conceptos clave

TX → RX
cables cruzados
Baud
115200 en ambos
/dev/serial0
UART del Pi
Loopback
TX conectado a RX
3

📨 Protocolo sencillo de mensajes

Los bytes llegan sueltos por el cable; el protocolo es el acuerdo que indica dónde empieza y termina cada mensaje y cómo saber si llegó completo. Usaremos una línea de texto por mensaje, al estilo del GPS (NMEA): comienza con $, lleva el comando, un *, dos dígitos de checksum y termina con un salto de línea. El texto se puede leer en el Serial Monitor y es fácil de generar en cualquier lenguaje.

$ M,120,-80 * 5B \n iniciocuerpo: comando y valoresseparadorchecksumfin checksum = XOR de todos los bytes del cuerpo, en hexadecimal si un byte cambia durante el recorrido, cambia el XOR y se descarta el mensaje

Cómo leer: solo el cuerpo morado lleva información; los recuadros cian forman el marco. El Pi calcula el checksum, lo envía junto con el mensaje y el ESP32 lo vuelve a calcular. Si los dos números no coinciden, el ESP32 responde con un error e ignora el comando.

MensajeSentidoSignificado
$M,esq,dir*HHPi → ESP32velocidad de cada lado, de −255 a 255
$S*53Pi → ESP32detener ahora
$P*50Pi → ESP32ping: ¿estás ahí?
$OK,M*65ESP32 → Picomando aceptado
$ERR,motivo*HHESP32 → PiCHK, FORMATO, ARG, CMD, LONGA o TIMEOUT

✓ Línea de texto corta

  • ✓Puedes leerla y escribirla en el Serial Monitor.
  • ✓Cabe en un búfer de 40 bytes, sin bibliotecas adicionales.
  • ✓JSON corto ({"m":[120,-80]}) también sirve, con ArduinoJson.

✗ Evita

  • ✗Mensaje sin marcador de fin: nunca sabes dónde termina.
  • ✗Aceptar un comando sin checksum en un robot con motor.
  • ✗Mensajes largos y frecuentes: 115200 baud son cerca de 11 mil bytes por segundo.

🌱 ¿Eres nuevo aquí?

Checksum es un número calculado a partir del mensaje que viaja junto con él. El XOR compara bits: da 1 cuando son diferentes y 0 cuando son iguales. Al hacer el XOR de todos los bytes, cualquier byte cambiado modifica el resultado. No protege contra todo, pero detecta la mayoría de los ruidos del cable.

Por qué aprender

Un motor que recibe "M,1200" en vez de "M,120" por un byte duplicado es un robot que choca contra la pared. El marco, el checksum y las respuestas explícitas convierten un cable ruidoso en un canal confiable; la misma idea está presente en los protocolos industriales.

Conceptos clave

Marco
$ … \n
Checksum XOR
detecta un byte cambiado
Respuesta
OK o ERR
Rango válido
−255 a 255
4

🛡️ Lado ESP32: recibir comandos de forma segura

El sketch reúne letra por letra hasta el salto de línea, descarta las líneas demasiado largas, verifica el marco y el checksum, limita las velocidades y las aplica en el puente H TB6612FNG del módulo 1.5 con la API 3.x del PWM. La parte más importante está en las últimas líneas del loop(): si no llega ningún comando válido para el motor en 300 ms, los motores se detienen.

Objetivo: ESP32 que obedece el protocolo del tema 3 y se detiene por sí solo cuando no recibe comandos. Pines de un ESP32 DevKit; en un ESP32-S3, cámbialos por pines disponibles de tu placa.

receptor-pi.ino
// Recibe comandos del Raspberry Pi por la UART2 y mueve los motores (ESP32, paquete esp32 3.x)
// Protocolo: "$M,esq,dir*HH\n"  "$S*53\n"  "$P*50\n"   (HH = XOR del cuerpo en hexadecimal)
const int PINO_RX = 16, PINO_TX = 17;        // RX (GPIO16) recibe del pin 8 del Pi; TX (GPIO17) va al pin 10
const int PWMA = 25, AIN1 = 26, AIN2 = 27;    // TB6612FNG: motor izquierdo
const int PWMB = 33, BIN1 = 32, BIN2 = 4;     // motor derecho (los mismos pines del rover de la Ruta 1)
const int STBY = 13;                         // HIGH = puente H encendido
const unsigned long TIMEOUT_MS = 300;         // sin comando para el motor durante 300 ms: se detiene
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) {           // envía "$corpo*HH\n" al 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 recibe el pin
}
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" (sin el \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';                             // elimina el checksum: queda solo el cuerpo
  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: mensajes para ti
  Serial2.begin(115200, SERIAL_8N1, PINO_RX, PINO_TX); // UART2 con pines 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 al Pi que se (re)inició
  Serial.println("Listo: esperando comandos en la 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;                    // línea demasiado larga: descarta hasta el \n
  }
  if (andando && millis() - ultimoComando > TIMEOUT_MS) {   // parada segura
    parar();
    responde("ERR,TIMEOUT");
    Serial.println("Sin comandos: motores detenidos");
  }
}

Cómo verificar (antes de conectarlo al Pi)

  • ✓Ruedas levantadas. El Monitor serie (USB) muestra "Listo: esperando comandos en la UART2".
  • ✓Con un adaptador USB-serial de 3,3 V conectado a GPIO16/17, envía $M,150,150*4D: las ruedas giran y, 300 ms después, se detienen solas con "Sin comandos".
  • ✗Envía $M,150,150*00: la respuesta es $ERR,CHK y nada se mueve.

Por qué aprender

Este sketch es el "firmware" del robot para el resto de la ruta. Todo lo que se decida en el Pi, incluso mediante IA, pasa por este filtro: solo se acepta lo que tiene el formato correcto, el checksum correcto y valores dentro del rango. Así, un modelo que se equivoca no hace que un motor se descontrole.

Conceptos clave

Serial2.begin
baud, formato, RX, TX
Buffer limitado
nada de líneas infinitas
constrain
limita la velocidad
Timeout
300 ms y se detiene
5

🐍 Lado Pi: Python con pyserial

En el Pi, una clase Link oculta los detalles: arma el marco, calcula el checksum, lo envía, lee la respuesta y verifica su checksum. El demo al final del archivo hace que el robot avance, gire y retroceda, repitiendo el comando 20 veces por segundo, que es lo que mantiene al ESP32 en movimiento. En el módulo 3.6, el seguidor de objetos importa esta misma clase.

Objetivo: controlar el ESP32 desde el Pi con el protocolo del tema 3. Guárdalo como link_esp32.py y ejecútalo dentro del entorno virtual con python link_esp32.py. Si usas el cable USB en vez de los cables, cambia el puerto por /dev/ttyACM0 (o /dev/ttyUSB0).

link_esp32.py
#!/usr/bin/env python3
"""Enlace serial Pi -> ESP32 (protocolo del módulo 3.5). Requiere: pip install pyserial"""
import time
import serial

PORTA = "/dev/serial0"   # UART de los pines 8/10; con el cable USB usa "/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("se cayó el enlace:", e, "- intentando volver a abrir")
            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 mantiene al ESP32 en movimiento
                if r != "OK,M":
                    print("resposta:", r)
                time.sleep(0.05)
        print("detener ->", link.parar())
    except KeyboardInterrupt:
        print("interrompido")
    finally:
        link.fechar()                        # siempre envía "S" al salir

Cómo verificar

  • ✓La terminal muestra ping -> OK,P, después "adelante", "gira", "retrocede" y detener -> OK,S.
  • ✓Con las ruedas levantadas, siguen la secuencia y se detienen al final.
  • ✗ping -> None? Verifica que TX/RX estén cruzados, que GND sea común y que el ESP32 tenga el sketch del tema 4.

💡 Prueba sin robot

Aunque no tengas los motores conectados, puedes probarlo todo: deja el ESP32 solo en la protoboard y el Monitor serie USB mostrará "Sin comandos: motores detenidos" cuando interrumpas Python con Ctrl+C a mitad de un movimiento.

Por qué aprender

Separar el "modo de comunicarse con la placa" en una clase pequeña mantiene limpio el resto del código: el seguidor de objetos solo llama a link.motores(esq, dir). E o try/finally garantiza que se envíe "S" al salir, incluso si el programa falla.

Conceptos clave

pyserial
serial en Python
tiempo de espera de lectura
no se bloquea mientras espera
20 Hz
comando repetido
try/finally
se detiene al salir
6

🚨 Fallas: pérdida de enlace, reinicio, parada segura

Piensa en todo lo que puede salir mal: se suelta un cable, Python se bloquea, el Pi se reinicia, el ESP32 se reinicia por una caída de tensión cuando arranca el motor. En todos los casos, la respuesta correcta es la misma: el robot se queda detenido hasta volver a recibir un comando válido. El ESP32 ya lo hace con el tiempo de espera y comienza detenido al encenderse; el Pi detecta el reinicio mediante el mensaje OK,BOOT o mediante varios None seguidos.

encender /reinicio DETENIDO ANDANDO M válido S o 300 ms sin M M válido: renueva el plazo ERR: lo ignora y responde

Cómo leer: cada cuadro es un estado del ESP32 y cada flecha indica qué hace que cambie de estado. Observa que solo hay un camino para llegar a EN MOVIMIENTO (un comando M válido) y dos para volver a DETENIDO. Después de cualquier reinicio, el estado inicial siempre es DETENIDO.

✓ Prueba intencional

  • ✓Desconecta el cable TX del Pi con el robot en movimiento (ruedas levantadas): se detiene en 300 ms.
  • ✓Termina Python con kill -9: finally no se ejecuta, pero el timeout lo detiene.
  • ✓Presiona el botón EN del ESP32: vuelve a detenerse y avisa OK,BOOT.
  • ✓Ten un interruptor físico que corte la batería de los motores.

✗ Trampas

  • ✗Timeout largo (2 s) en un robot rápido: avanza metros «a ciegas».
  • ✗Ping que renueva el plazo de los motores: si el Pi se bloquea a mitad del proceso, seguiría avanzando.
  • ✗Reanudar por sí solo el último comando después de un reset.
  • ✗Motor y ESP32 conectados a la misma batería sin capacitor: cada arranque reinicia la placa.

Por qué aprender

Que un robot funcione cuando todo sale bien es la mitad del trabajo; la otra mitad es que se comporte bien cuando algo sale mal. Este patrón (comando con plazo de validez, estado inicial seguro, parada física independiente) es el mismo de los robots industriales y vuelve en la Trilha 5 con el brazo robótico.

Conceptos clave

Watchdog de comandos
comando con plazo
Estado seguro
detenido al encender
OK,BOOT
aviso de reset
Corte físico
interruptor en la batería

✅ Resumen del módulo

✓
El Pi decide, el ESP32 ejecuta y protege: intenciones a cerca de 20 Hz, PWM y límites en el microcontrolador.
✓
UART a 3,3 V: pines 8/10/6 del Pi a GPIO16/17/GND del ESP32, TX al RX, sin conversor.
✓
Protocolo: $corpo*HH\n con checksum XOR y respuesta OK/ERR.
✓
Firmware defensivo: búfer limitado, checksum, constrain y timeout de 300 ms.
✓
Fallar detenido: un cable suelto, Python bloqueado o un reset terminan con los motores apagados.

Para profundizar

Siguiente módulo

3.6 · Proyecto: robot que sigue un objeto. Cámara, detección, error y PID; usa este enlace para mover las ruedas.