PTENES
MÓDULO 2.3 · FUNDAMENTO

🎯 Control PID en la práctica

El PID es la fórmula que hace que un robot llegue al objetivo sin pasarse y se quede allí, incluso con la batería baja o una pendiente en el camino. Entenderás cada letra con un auto en la pista, escribirás el PID en código, lo probarás en un motor simulado sin hardware, ajustarás las ganancias y terminarás con las dos ruedas del rover girando a la misma velocidad.

6
Temas
Intermedio
Nivel
Fundamentos
Tipo
Wokwi + rover
Hardware
0%
0 de 0 temas leídos · Sección 1 de 6

Contenido detallado

1

🔁 Por qué encender y apagar no basta

El control más simple es el de encendido y apagado: por debajo del objetivo, motor al máximo; por encima, motor apagado. Funciona en una plancha, que se calienta despacio. En un robot, que reacciona rápido, el resultado es un vaivén sin fin: la rueda se pasa del objetivo, frena, queda por debajo y vuelve a acelerar al máximo. El seguidor de línea «baila» en la pista y pierde las curvas. Hace falta una respuesta proporcional: corregir poco cuando el error es pequeño y mucho cuando es grande.

🌱 ¿Eres nuevo aquí?

O El objetivo (setpoint) es el valor que quieres, como 150 RPM. La medición es lo que indica el sensor que está ocurriendo. El error es el objetivo menos la medición. La salida es lo que el controlador envía al actuador; en este caso, el PWM del motor. Lazo cerrado Es este ciclo en el que la medición vuelve para corregir la salida.

encendido y apagadoPIDoscila para siemprese pasa un poco y se estabilizaEl objetivo

Cómo leer: la línea ámbar es el objetivo en los dos gráficos. A la izquierda, la velocidad alcanza el objetivo y se pasa de inmediato, porque el motor solo conoce «todo» o «nada». A la derecha, el PID reduce la fuerza a medida que se acerca: se pasa un poco del objetivo y luego se mantiene en él.

✓ El encendido y apagado sirve para

  • ✓Sistemas lentos: calentador, bomba de tanque de agua.
  • ✓Decisiones de estado: detenerse al ver un obstáculo.

✗ Necesitas PID para

  • ✗Velocidad de las ruedas, dirección en un seguidor de línea.
  • ✗Equilibrio, posición de un brazo, giro exacto de 90°.

Por qué aprender

El PID está en el control de velocidad de un auto, en el dron, en la impresora 3D y en el brazo de la Trilha 5. Entender por qué falla el encendido y apagado es entender el problema que resuelve el PID.

Conceptos clave

Objetivo
lo que quieres
Error
objetivo − medición
Oscilación
vaivén alrededor del objetivo
Lazo cerrado
la medición corrige la salida
2

🏎️ P, I y D explicados con un auto en la pista

Imagina que conduces y quieres mantenerte en el centro del carril. La P (proporcional) consiste en girar el volante según la distancia al centro: si estás lejos, giras mucho; si estás cerca, poco. Si usas solo esta parte, zigzagueas. La D (derivativa) observa la velocidad con la que te acercas al centro y afloja el volante antes de que te pases: amortigua el zigzagueo. La I (integral) percibe que, con viento lateral, siempre quedas un poco a la derecha y va sumando ese error hasta compensarlo.

🌱 ¿Eres nuevo aquí?

A integral del error es la suma del error a lo largo del tiempo: un error pequeño que no desaparece se convierte en una suma grande. La derivada del error es la rapidez con que cambia. Las ganancias Kp, Ki y Kd son los tres números que ajustas: cuánto peso darle a cada parte. Salida = Kp·error + Ki·suma + Kd·variación.

El objetivosolo P: queda por debajoP + I: llega, oscilaP + I + D: llega y se estabilizatiempo →

Cómo leer: las tres curvas responden al mismo pedido de «acelera hasta el objetivo». La gris (solo P) se detiene antes: queda un error fijo. La cian (P + I) elimina ese error, pero la suma se pasa del objetivo y oscila. La azul (PID) usa D para frenar al llegar y se estabiliza antes.

Aumentar…Velocidad de respuestaSe pasa del objetivoError restante
Kpmás rápidamásdisminuye, no desaparece
Kiun poco más rápidamásdesaparece
Kdcambia pocomenosno cambia (amplifica el ruido)

Por qué aprender

Quien entiende el papel de cada letra ajusta el PID mirando el gráfico: «se pasó demasiado, más D»; «quedó por debajo, más I». Quien no lo entiende cambia números al azar.

Conceptos clave

P
reacciona al error actual
I
corrige el error persistente
D
frena al llegar
Overshoot
cuánto se pasa del objetivo
3

💻 PID en código (bucle de tiempo fijo)

En código, el PID es una función que se llama siempre en el mismo intervalo, por ejemplo, cada 10 ms. El intervalo fijo importa: la integral suma error × tiempo y la derivada divide entre el tiempo; si el intervalo varía, las ganancias cambian su efecto por sí solas. Por eso el bucle usa micros() y marca un «próximo instante», en lugar de delay().

El objetivo −PID motorencoder errorPWMruedavelocidad medida · cada 10 ms

Cómo leer: sigue las flechas azules de izquierda a derecha: objetivo, error, PID, PWM, motor. La flecha cian discontinua es la medición que regresa. En el código de abajo, el «motor + encoder» es una simulación de dos líneas para que juegues con las ganancias sin hardware; en el tema 6 se convierte en el motor real.

Objetivo: ejecutar un PID completo a 100 Hz contra un motor simulado, con el objetivo alternando entre 120 y 220 RPM cada 4 s y una «subida» que frena la rueda a partir de los 12 s. Cambias las ganancias escribiéndolas en el Serial Monitor (p. ej.: p3, i5, d0.05). Funciona en Wokwi con solo un ESP32.

pid-simulado.ino
// PID contra un motor simulado: entiende las ganancias sin hardware (paquete esp32 3.x)
struct PID {
  float kp = 1.0, ki = 0.0, kd = 0.0;          // empieza solo con P
  float saidaMin = 0, saidaMax = 255;           // límites del PWM
  float integral = 0, dFiltrada = 0, medidaAnt = 0;
  bool primeira = true;
  float calcula(float alvo, float medida, float dt) {
    float erro = alvo - medida;
    if (primeira) { medidaAnt = medida; primeira = false; }
    float dBruta = -(medida - medidaAnt) / dt;   // derivada de la MEDICIÓN: sin tirón cuando cambia el objetivo
    medidaAnt = medida;
    dFiltrada += 0.2 * (dBruta - dFiltrada);       // filtro pasa-bajas en la derivada
    float novaIntegral = integral + ki * erro * dt;
    float saida = kp * erro + novaIntegral + kd * dFiltrada;
    if (saida > saidaMax) { saida = saidaMax; if (erro < 0) integral = novaIntegral; }  // anti-windup
    else if (saida < saidaMin) { saida = saidaMin; if (erro > 0) integral = novaIntegral; }
    else integral = novaIntegral;
    return saida;
  }
};

PID pid;
const unsigned long PERIODO_US = 10000;       // 10 ms = 100 Hz
const float DT = PERIODO_US / 1e6;
float vel = 0;                                 // velocidad simulada, en RPM

void motorSimulado(float pwm) {                  // motor de primer orden: tiende a 1,2 x PWM en ~0,3 s
  float carga = (millis() > 12000) ? 40 : 0;    // a partir de los 12 s, una subida frena la rueda
  vel += ((1.2 * pwm - carga) - vel) * DT / 0.3;
}

void setup() {
  Serial.begin(115200);
  Serial.setTimeout(20);
  Serial.println("Escribe p, i o d seguido del valor. Ej.: p3");
}

void loop() {
  static unsigned long proximo = micros();
  if ((long)(micros() - proximo) < 0) return;  // todavía no llegó el siguiente instante
  proximo += PERIODO_US;
  if (Serial.available()) {
    char g = Serial.read();
    float v = Serial.parseFloat();
    if (g == 'p') pid.kp = v; else if (g == 'i') pid.ki = v; else if (g == 'd') pid.kd = v;
  }
  float alvo = ((millis() / 4000) % 2) ? 220 : 120;
  float pwm = pid.calcula(alvo, vel, DT);
  motorSimulado(pwm);
  static int n = 0;
  if (++n % 5 == 0) Serial.printf("alvo:%.0f,vel:%.1f,pwm:%.0f\n", alvo, vel, pwm);
}

Cómo verificar

  • ✓En el Serial Plotter, con solo P = 1: «vel» queda muy por debajo de «alvo» (error restante).
  • ✓Escribe i5: «vel» se acerca al objetivo; después de 12 s, la carga reduce la velocidad y la I la recupera.
  • ✓Escribe p8: la respuesta se vuelve nerviosa y supera el objetivo. Vuelve a p2.

Por qué aprender

Esta estructura de PID es la que vas a copiar para el seguidor de línea del 2.6, para el giro del robot que sigue objetos en el 3.6 y para cualquier cosa que necesite «llegar y quedarse». Probar primero con una simulación permite separar los errores de código de los errores de montaje.

Conceptos clave

Período fijo
dt siempre igual
micros()
reloj sin bloqueos
Planta simulada
probar sin motor
Serial Plotter
ver la respuesta
4

🎛️ Ajuste paso a paso (Ziegler-Nichols simplificado y manual)

Ajustar es elegir Kp, Ki y Kd para tu robot. No existe un número universal: depende del motor, la rueda, el peso y la batería. Hay dos caminos que se complementan. El método Ziegler-Nichols da un punto de partida a partir de una sola prueba; el ajuste manual, observando el Serial Plotter, permite hacer el ajuste fino.

Ajuste manual, en el orden correcto

1

Pon I y D en cero

Empieza con i0 e d0. Robot suspendido, con las ruedas en el aire.

2

Sube P hasta que casi oscile

Duplica Kp hasta que la respuesta empiece a oscilar; entonces vuelve a la mitad de ese valor.

3

Agrega I poco a poco

Sube Ki hasta que el error restante desaparezca en menos de un segundo, sin oscilar demasiado.

4

Usa D solo si hace falta

Si supera mucho el objetivo, un poco de Kd lo amortigua. Para la velocidad de la rueda, muchas veces basta con PI.

5

Prueba en el suelo y anota

Con el robot en el suelo, el peso cambia la respuesta; vuelve a ajustar y anota las ganancias finales en el código.

Ziegler-Nichols simplificado: con I y D en cero, sube Kp hasta que la velocidad oscile a un ritmo constante, sin aumentar ni disminuir. Ese es el ganancia crítica Ku. Mide el tiempo de una oscilación completa en el Serial Plotter: ese es el período Tu, en segundos. La tabla da las ganancias iniciales.

ControladorKpKiKd
P0,5 · Ku00
PI0,45 · Ku0,54 · Ku / Tu0
PID0,6 · Ku1,2 · Ku / Tu0,075 · Ku · Tu

💡 Ziegler-Nichols es agresivo

Las ganancias de la tabla suelen producir una respuesta rápida que supera bastante el objetivo. Úsalas como punto de partida y reduce Kp y Ki entre un 20 y un 30 % si el robot se comporta de forma nerviosa. El cálculo sirve para empezar, no para evitar las pruebas.

✓ Un ajuste que funciona

  • ✓Cambiar una ganancia a la vez y observar el gráfico.
  • ✓Cambiar en pasos grandes (duplicar, dividir entre 2).
  • ✓Ajustar con la batería cargada, como en el uso real.

✗ Un ajuste que vuelve loco al robot

  • ✗Cambiar las tres ganancias al mismo tiempo.
  • ✗Ajustar «a ojo», sin Serial Plotter.
  • ✗Copiar las ganancias de un robot diferente al tuyo.

Por qué aprender

Un PID mal ajustado es peor que no tener PID: oscila, consume batería y calienta el motor. Con un método, el ajuste de un seguidor de línea pasa de una tarde de prueba y error a unas cuantas pruebas organizadas.

Conceptos clave

Ku
ganancia que provoca oscilación
Tu
período de oscilación
P → I → D
el orden del ajuste
Una a la vez
para conocer el efecto
5

🧯 Windup, saturación y filtro en la derivada

El PID «de libro» tiene tres trampas en el mundo real, y el código del tema 3 ya las resuelve. Saturación: el PWM no supera 255; pedir 400 da el mismo resultado. Windup: mientras la salida está atascada en el máximo, la integral sigue sumando error; cuando el robot finalmente llega, esa suma enorme hace que supere mucho el objetivo. Ruido en la derivada: la D amplifica cada pequeño salto de la medición y hace vibrar el motor.

rueda atascada:la integral crecesin anti-windup: se pasa muchocon anti-windup: llega y se quedaEl objetivotiempo → (la rueda se suelta al final de la franja roja)

Cómo leer: en la franja roja, la rueda está sujeta con la mano: velocidad cero, salida al máximo. Cuando la sueltas, la curva roja (sin protección) se dispara muy por encima del objetivo, por la integral acumulada. La azul (con protección) no acumuló nada mientras estaba saturada y llega sin sobrepasarse.

ProblemaLínea del código (tema 3)Qué hace
SaturaciónsaidaMin / saidaMaxlimita la salida al rango que acepta el motor
Windupif (erro < 0) integral = novaIntegralen saturación, solo acepta la integral si ayuda a salir de ella
Tirón al cambiar el objetivo-(medida - medidaAnt) / dtderivada de la medición, no del error: cambiar el objetivo no genera un pico
Ruido en DdFiltrada += 0.2 * (...)promedio móvil exponencial: suaviza antes de multiplicar por Kd

🌱 ¿Eres nuevo aquí?

Un filtro pasa-bajas deja pasar el cambio lento y corta el rápido, como el promedio del 1.4. La forma f += a * (novo - f) lo hace con una sola línea: con a = 0.2; cada nueva lectura acerca el valor filtrado solo un 20 % hacia ella. Menor a = más suave y más retrasado.

⚠️ Prueba el windup con cuidado

Para ver el efecto en el rover real, sujeta la rueda solo con el motor pequeño (N20 o TT) y durante pocos segundos: un motor bloqueado consume la corriente máxima y calienta el motor y el driver. Nunca lo hagas con motores más grandes.

Por qué aprender

La diferencia entre un PID que funciona en la simulación y uno que funciona en el robot está en estas cuatro líneas. Las bibliotecas PID listas para usar hacen lo mismo; ahora sabes qué buscar en ellas.

Conceptos clave

Saturación
límite físico de la salida
Anti-windup
integral bajo control
D en su justa medida
sin pico al cambiar el objetivo
Paso bajo
elimina el ruido
6

⚖️ Misma velocidad en las dos ruedas con PID

Dos motores «iguales» nunca son iguales: con el mismo PWM, uno gira más que el otro y el rover se desvía hacia un lado. La solución es asignar un PID a cada rueda, ambos con el mismo objetivo en RPM y cada uno midiendo su propio encoder. El PID de la rueda más débil simplemente envía más PWM hasta que ambas alcanzan la velocidad objetivo.

El objetivo150 RPM PID izq.PID der. motor izq.motor der. enc. izq.enc. der.

Cómo leer: el mismo objetivo ámbar alimenta dos lazos que no se conocen entre sí. Cada línea cian discontinua es la señal del encoder de una rueda que vuelve a su propio PID. Si la rueda derecha es más débil, solo el lazo de abajo aumenta el PWM.

Objetivo: mantener las dos ruedas a las RPM que escribas en el Serial Monitor (p. ej., 150; 0 para detenerse), con un PI en cada una a 50 Hz. Los pines del driver son iguales a los del 2.1 y los encoders, a los del 2.2.

dos-rodas-pid.ino
// Dos ruedas a la misma velocidad: un PID por rueda (TB6612FNG + encoders, paquete esp32 3.x)
const int PWMA = 25, AIN1 = 26, AIN2 = 27, PWMB = 14, BIN1 = 16, BIN2 = 17;   // STBY en 3V3
const int ENC_E_A = 18, ENC_E_B = 19, ENC_D_A = 34, ENC_D_B = 35;
const float PULSOS_VOLTA = 1400;            // el valor medido en el 2.2
const unsigned long PERIODO_US = 20000;     // 20 ms = 50 Hz
const float DT = PERIODO_US / 1e6;
volatile long pulsosE = 0, pulsosD = 0;

void IRAM_ATTR isrE() { if (digitalRead(ENC_E_A) == digitalRead(ENC_E_B)) pulsosE++; else pulsosE--; }
void IRAM_ATTR isrD() { if (digitalRead(ENC_D_A) == digitalRead(ENC_D_B)) pulsosD--; else pulsosD++; }

struct PID {
  float kp = 0.8, ki = 4.0, kd = 0.0, saidaMin = 0, saidaMax = 255;   // punto de partida: ajústalo
  float integral = 0, dFiltrada = 0, medidaAnt = 0;
  float calcula(float alvo, float medida, float dt) {
    float erro = alvo - medida;
    dFiltrada += 0.2 * (-(medida - medidaAnt) / dt - dFiltrada);
    medidaAnt = medida;
    float novaIntegral = integral + ki * erro * dt;
    float saida = kp * erro + novaIntegral + kd * dFiltrada;
    if (saida > saidaMax) { saida = saidaMax; if (erro < 0) integral = novaIntegral; }
    else if (saida < saidaMin) { saida = saidaMin; if (erro > 0) integral = novaIntegral; }
    else integral = novaIntegral;
    return saida;
  }
  void zera() { integral = 0; dFiltrada = 0; }
};

PID pidE, pidD;
float alvoRPM = 0, rpmE = 0, rpmD = 0;

void frente(int pwm, int in1, int in2, int v) { digitalWrite(in1, v > 0); digitalWrite(in2, LOW); ledcWrite(pwm, v); }

void setup() {
  Serial.begin(115200);
  Serial.setTimeout(20);
  int saidas[] = {AIN1, AIN2, BIN1, BIN2};
  for (int p : saidas) pinMode(p, OUTPUT);
  ledcAttach(PWMA, 20000, 8);
  ledcAttach(PWMB, 20000, 8);
  pinMode(ENC_E_A, INPUT_PULLUP); pinMode(ENC_E_B, INPUT_PULLUP);
  pinMode(ENC_D_A, INPUT); pinMode(ENC_D_B, INPUT);
  attachInterrupt(digitalPinToInterrupt(ENC_E_A), isrE, CHANGE);
  attachInterrupt(digitalPinToInterrupt(ENC_D_A), isrD, CHANGE);
  Serial.println("Escribe el objetivo en RPM (p. ej., 150). 0 para detenerse.");
}

void loop() {
  static unsigned long proximo = micros();
  static long antE = 0, antD = 0;
  if ((long)(micros() - proximo) < 0) return;
  proximo += PERIODO_US;
  if (Serial.available()) alvoRPM = constrain(Serial.parseFloat(), 0, 250);
  noInterrupts(); long e = pulsosE, d = pulsosD; interrupts();
  rpmE = 0.7 * rpmE + 0.3 * ((e - antE) / PULSOS_VOLTA / DT * 60);   // filtro ligero en la medición
  rpmD = 0.7 * rpmD + 0.3 * ((d - antD) / PULSOS_VOLTA / DT * 60);
  antE = e; antD = d;
  if (alvoRPM < 1) {                                        // detener de verdad y limpiar la integral
    frente(PWMA, AIN1, AIN2, 0); frente(PWMB, BIN1, BIN2, 0);
    pidE.zera(); pidD.zera();
  } else {
    frente(PWMA, AIN1, AIN2, (int)pidE.calcula(alvoRPM, rpmE, DT));
    frente(PWMB, BIN1, BIN2, (int)pidD.calcula(alvoRPM, rpmD, DT));
  }
  static int n = 0;
  if (++n % 5 == 0) Serial.printf("alvo:%.0f,esq:%.1f,dir:%.1f\n", alvoRPM, rpmE, rpmD);
}

Cómo verificar

  • ✓Con las ruedas en el aire, escribe 150: en el Serial Plotter, «esq» y «dir» suben y se encuentran en 150 en menos de un segundo.
  • ✓Toca suavemente una rueda con el dedo: baja un poco y vuelve sola a 150.
  • ✓En el suelo, con 150 en ambas ruedas, el rover avanza en línea recta durante 2 m (compáralo con un PWM fijo).
  • ✗¿Una rueda se dispara al máximo y no vuelve? El encoder cuenta al revés: el PID «ve» que la rueda va hacia atrás y acelera aún más.

Por qué aprender

Avanzar en línea recta parece trivial hasta que lo intentas. Con la velocidad controlada en las dos ruedas, la odometría del 2.2 es más confiable, el seguidor de línea del 2.6 tiene una base estable para sumar la corrección de dirección y el comando /cmd_vel de ROS 2 en la Trilha 4 se convierte exactamente en esto: un objetivo de velocidad por rueda.

Conceptos clave

Un PID por rueda
lazos independientes
RPM medidas
pulsos ÷ tiempo
PI es suficiente
para la velocidad de la rueda
Poner la integral en cero
al detenerse

✅ Resumen del módulo

✓
Encendido/apagado oscila: los sistemas rápidos necesitan una corrección proporcional al error.
✓
P reacciona, I insiste, D frena: salida = Kp·error + Ki·suma + Kd·variación.
✓
Lazo de tiempo fijo: micros() y el siguiente instante, nunca delay() en el control.
✓
Ajuste con método: P, luego I y después D; Ziegler-Nichols como punto de partida.
✓
PID real: saturación, anti-windup, derivada de la medición filtrada, un lazo por rueda.

Para profundizar

Siguiente módulo

2.4 · Varias tareas al mismo tiempo: millis(), máquina de estados y FreeRTOS para que el robot lea, decida y controle sin bloquearse.