Contenido detallado
🔁 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.
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
🏎️ 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.
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 respuesta | Se pasa del objetivo | Error restante |
|---|---|---|---|
| Kp | más rápida | más | disminuye, no desaparece |
| Ki | un poco más rápida | más | desaparece |
| Kd | cambia poco | menos | no 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
💻 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().
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 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
🎛️ 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
Pon I y D en cero
Empieza con i0 e d0. Robot suspendido, con las ruedas en el aire.
Sube P hasta que casi oscile
Duplica Kp hasta que la respuesta empiece a oscilar; entonces vuelve a la mitad de ese valor.
Agrega I poco a poco
Sube Ki hasta que el error restante desaparezca en menos de un segundo, sin oscilar demasiado.
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.
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.
| Controlador | Kp | Ki | Kd |
|---|---|---|---|
| P | 0,5 · Ku | 0 | 0 |
| PI | 0,45 · Ku | 0,54 · Ku / Tu | 0 |
| PID | 0,6 · Ku | 1,2 · Ku / Tu | 0,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
🧯 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.
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.
| Problema | Línea del código (tema 3) | Qué hace |
|---|---|---|
| Saturación | saidaMin / saidaMax | limita la salida al rango que acepta el motor |
| Windup | if (erro < 0) integral = novaIntegral | en saturación, solo acepta la integral si ayuda a salir de ella |
| Tirón al cambiar el objetivo | -(medida - medidaAnt) / dt | derivada de la medición, no del error: cambiar el objetivo no genera un pico |
| Ruido en D | dFiltrada += 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
⚖️ 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.
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 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
✅ Resumen del módulo
Para profundizar
- Improving the Beginner's PID (Brett Beauregard) — la serie que inspiró la biblioteca PID de Arduino
- Control Tutorials for MATLAB and Simulink: PID — efecto de cada ganancia con gráficos (Univ. de Michigan)
- Serial Plotter de Arduino IDE 2 — cómo graficar varias variables
- LEDC (PWM) en arduino-esp32 — ledcAttach y ledcWrite en la 3.x
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.