arduino-robot-equilibrista-pid

Cómo crear un robot equilibrista con Arduino y PID

  • 9 min

Un robot autoequilibrado es un péndulo invertido que desplaza sus ruedas para mantener el cuerpo vertical.

El concepto es simple: mantener un palo vertical sobre dos ruedas. Es el clásico problema del Péndulo Invertido. Sin embargo, la física es cruel. Es un sistema intrínsecamente inestable; si el Arduino se duerme una fracción de segundo, el robot se cae.

Para lograr que se mantenga en pie, el código debe medir la inclinación, calcular la corrección y actuar sobre los motores con un periodo estable. En este ejemplo usaremos un ciclo aproximado de 100 Hz.

Este proyecto introduce dos pilares de la ingeniería moderna:

  1. Fusión de Sensores: Combinar acelerómetro y giroscopio para obtener un ángulo fiable.
  2. Control PID: El algoritmo matemático que mantiene la estabilidad.

Cómo estimamos la inclinación

Necesitamos un sensor IMU (Unidad de Medición Inercial), concretamente el famoso MPU6050. Este chip nos da dos datos:

  • Acelerómetro: Mide la gravedad. Nos dice dónde está el “abajo”. Problema: Es muy ruidoso y sensible a las vibraciones del motor.
  • Giroscopio: Mide la velocidad angular (a qué velocidad giramos). Problema: Tiene “drift” (deriva). Con el tiempo, el error se acumula y cree que está girando cuando está quieto.

Filtro complementario

Para obtener un ángulo limpio, mezclamos ambos. Confiamos en el giroscopio para los cambios rápidos y en el acelerómetro para la estabilidad a largo plazo.

Controlador PID

Una vez sabemos el ángulo, ¿cuánto aceleramos?

  • P (Proporcional): “Me caigo un poco, acelero un poco. Me caigo mucho, acelero mucho”.
  • I (Integral): “Llevo rato un poco inclinado y no me enderezo, voy a meter más potencia para compensar”.
  • D (Derivativo): “¡Cuidado! Me estoy cayendo muy rápido, voy a hacer fuerza en contra para frenar la caída”.

Componentes

  • [1x|blue] Arduino Uno | O Nano
  • [1x|orange] Módulo MPU6050 | Acelerómetro + Giroscopio {I2C}
  • [2x|green] Motores DC con reductora | Pololu o N20 recomendados {TT Motors funcionan}
  • [1x|red] Driver de motores | L298N para la prueba; TB6612FNG reduce pérdidas
  • [1x|red] Batería protegida | Tensión compatible con driver y motores
  • [Varios|gray] Chasis Vertical | Rígido, sensor bien fijo

Los motores TT funcionan, pero tienen bastante holgura. Para obtener una respuesta más repetible, usa motores con reductora de menor juego y, si es posible, encoders.

Esquema de conexión

El montaje debe ser rígido. Si el sensor MPU6050 vibra suelto respecto al chasis, el robot nunca se equilibrará.

Módulo MPU6050

El MPU6050 se comunica por I2C. Muchos módulos admiten 5V porque incorporan regulador, pero el chip desnudo trabaja a 3.3V.

[ { “from”: “Módulo MPU6050 VCC”, “to”: “5V Arduino”, “color”: “red”, “note”: “Solo si el módulo admite 5V” }, { “from”: “MPU6050 GND”, “to”: “GND Arduino”, “color”: “black” }, { “from”: “MPU6050 SDA”, “to”: “Pin A4”, “color”: “blue”, “note”: “Bus I2C” }, { “from”: “MPU6050 SCL”, “to”: “Pin A5”, “color”: “purple”, “note”: “Bus I2C” } ]

Driver L298N

El driver recibe dos señales de dirección y una señal PWM por motor.

[ { “from”: “L298N ENA (Velocidad A)”, “to”: “Pin 9 (PWM)”, “color”: “green” }, { “from”: “L298N IN1 / IN2 (Dir A)”, “to”: “Pin 4 / Pin 5”, “color”: “orange” }, { “from”: “L298N ENB (Velocidad B)”, “to”: “Pin 10 (PWM)”, “color”: “green” }, { “from”: “L298N IN3 / IN4 (Dir B)”, “to”: “Pin 6 / Pin 7”, “color”: “orange” } ]

Alimentación de motores

Los motores se alimentan desde la batería a través del driver. La referencia de GND debe ser común con Arduino.

[ { “from”: “L298N Alimentación 12V/Bat”, “to”: “Driver”, “color”: “red”, “note”: “GND compartido con Arduino” }, { “from”: “GND común”, “to”: “GND baterías + Arduino”, “color”: “black” } ]

El código

Este código es complejo. Vamos a leer el MPU6050 “a pelo” (accediendo a los registros) para tener máximo control y velocidad, y luego aplicaremos el PID.

Usaremos la librería Wire, incluida con el núcleo de Arduino.

Variables y constantes

#include <Wire.h>

// --- Configuración PID (Ajustar estos valores es LA CLAVE) ---
double Kp = 20.0; 
double Ki = 0.8;  
double Kd = 0.5;  

// --- Pines del L298N ---
const int ENA = 9;
const int IN1 = 4;
const int IN2 = 5;
const int IN3 = 6;
const int IN4 = 7;
const int ENB = 10;

// Variables del IMU
long accelX, accelY, accelZ;
float gForceX, gForceY, gForceZ;
long gyroX, gyroY, gyroZ;
float rotX, rotY, rotZ;

float anguloGiro = 0, anguloAccel = 0, anguloActual = 0;
float error, errorAnterior = 0, sumaError = 0;

// Variables de tiempo para el PID
unsigned long tiempoActual, tiempoAnterior;
float dt;

// Punto de equilibrio (SetPoint)
// Ajustar si el robot tiende a irse hacia un lado mecánicamente
float setPoint = 0; 
Copied!

setup e inicialización del MPU

Hay que despertar al MPU6050 en el setup().

void setup() {
  Serial.begin(9600);
  
  // Configurar Pines Motor
  pinMode(ENA, OUTPUT); pinMode(IN1, OUTPUT); pinMode(IN2, OUTPUT);
  pinMode(ENB, OUTPUT); pinMode(IN3, OUTPUT); pinMode(IN4, OUTPUT);
  
  // Iniciar I2C y MPU6050
  Wire.begin();
  Wire.beginTransmission(0x68);
  Wire.write(0x6B); // Registro de Power Management
  Wire.write(0);    // Despertar el MPU
  Wire.endTransmission(true);
  
  tiempoAnterior = millis();
}
Copied!

Bucle de control

void loop() {
  tiempoActual = millis();
  dt = (tiempoActual - tiempoAnterior) / 1000.0; // Tiempo en segundos
  
  // Si el bucle corre muy rápido, el dt puede ser 0. Forzamos un mínimo.
  if(dt < 0.01) return; 
  tiempoAnterior = tiempoActual;

  // 1. LEER DATOS (Función auxiliar definida abajo)
  leerMPU();

  // 2. FILTRO COMPLEMENTARIO
  // Ángulo del acelerómetro (Trigonometría básica)
  anguloAccel = atan2(accelY, accelZ) * 180 / PI;
  
  // Ángulo del giroscopio (Integración: velocidad * tiempo)
  // El 0.98 y 0.02 son los pesos del filtro.
  anguloActual = 0.98 * (anguloActual + rotX * dt) + 0.02 * anguloAccel;

  // 3. CÁLCULO PID
  error = setPoint - anguloActual;
  sumaError += error * dt;            // Término Integral
  // Limitamos la integral para evitar "windup" (que se acumule infinito)
  sumaError = constrain(sumaError, -200, 200); 
  
  float tasaError = (error - errorAnterior) / dt; // Término Derivativo
  
  // Salida del PID
  float salida = (Kp * error) + (Ki * sumaError) + (Kd * tasaError);
  
  errorAnterior = error;

  // 4. MOVER MOTORES
  if (abs(anguloActual) > 45) {
    sumaError = 0;
    errorAnterior = 0;
    detenerMotores();
    return;
  }

  moverMotores(salida);
}
Copied!

Funciones auxiliares

Aquí leemos los registros crudos (Raw) y controlamos el puente H.

void leerMPU() {
  Wire.beginTransmission(0x68);
  Wire.write(0x3B); // Registro inicial de datos del acelerómetro
  Wire.endTransmission(false);
  Wire.requestFrom(0x68, 6, true); 
  
  // Los valores vienen en dos bytes, hay que unirlos
  accelX = (Wire.read() << 8 | Wire.read());
  accelY = (Wire.read() << 8 | Wire.read());
  accelZ = (Wire.read() << 8 | Wire.read());

  // Ahora leemos giroscopio (Registro 0x43)
  Wire.beginTransmission(0x68);
  Wire.write(0x43);
  Wire.endTransmission(false);
  Wire.requestFrom(0x68, 6, true);
  
  gyroX = (Wire.read() << 8 | Wire.read());
  gyroY = (Wire.read() << 8 | Wire.read());
  gyroZ = (Wire.read() << 8 | Wire.read());
  
  // Convertir a grados/segundo (Divisor 131.0 es estándar para rango default)
  rotX = gyroX / 131.0;
}

void moverMotores(float velocidad) {
  // velocidad será un valor que puede ir de -infinito a +infinito
  // Mapeamos a PWM (0-255)
  
  int pwm = constrain(abs(velocidad), 0, 255);
  
  // Zona muerta: Los motores DC baratos no arrancan con PWM < 30-40
  // Si hay señal pero es baja, forzamos un mínimo para vencer la fricción
  if (pwm < 40 && pwm > 5) pwm = 40; 

  if (velocidad > 0) {
    // Hacia ADELANTE (Ajustar IN1/IN2 según vuestro cableado)
    digitalWrite(IN1, HIGH); digitalWrite(IN2, LOW);
    digitalWrite(IN3, HIGH); digitalWrite(IN4, LOW);
  } else {
    // Hacia ATRÁS
    digitalWrite(IN1, LOW); digitalWrite(IN2, HIGH);
    digitalWrite(IN3, LOW); digitalWrite(IN4, HIGH);
  }
  
  analogWrite(ENA, pwm);
  analogWrite(ENB, pwm);
}

void detenerMotores() {
  analogWrite(ENA, 0);
  analogWrite(ENB, 0);
}
Copied!

Ajuste del PID

Si copias el código y lo enciendes, el robot se caerá. Te lo garantizo. Cada robot tiene un peso, una altura y unos motores distintos. Necesitas ajustar Kp, Ki y Kd.

Haz las primeras pruebas con el robot sujeto en un soporte y las ruedas libres, lejos de dedos y cables. Comprueba antes que el signo de la corrección sea el correcto. Después puedes ajustar los términos con incrementos pequeños:

  1. Pon Ki = 0 y Kd = 0.
  2. Sube Kp poco a poco.
  • Si es bajo, el robot cae lento (no tiene fuerza para levantarse).
  • Sube hasta que el robot empiece a oscilar frenéticamente sobre su eje.
  1. Una vez oscila, empieza a subir Kd.
  • El Kd actúa como un amortiguador. Deberías ver que las oscilaciones se calman y el robot se queda más quieto.
  1. Si el robot se mantiene pero se va desplazando lentamente hacia un lado hasta caerse, sube un poco el Ki. El Ki corrige ese error constante.

Análisis de problemas

  • El robot sale disparado en una dirección: La lógica de los motores está invertida respecto a la inclinación. Si se inclina adelante, las ruedas deben acelerar hacia adelante (para meterse debajo del centro de gravedad), no hacia atrás. Invierte los pines IN.
  • Zona Muerta: Si el robot hace un ruido agudo (el PWM del motor) pero no se mueve hasta que ya está muy inclinado, tienes que subir el valor mínimo de PWM en la función moverMotores.