PTENES
MÓDULO 2.6 · PROYECTO

🏁 Proyecto: seguidor de línea con PID

El proyecto que cierra la Ruta 2 reúne todo: una barra de 5 sensores IR calcula dónde está la línea, un PID de dirección corrige las ruedas 200 veces por segundo, la calibración se ajusta a tu pista y la telemetría en la microSD muestra, con números, si el ajuste mejoró.

6
Temas
Intermedio
Nivel
Proyecto
Tipo
Rover + barra IR
Hardware
0%
0 de 0 temas leídos · Sección 1 de 6

Contenido detallado

1

🛤️ La pista y la barra de sensores

La pista más sencilla es cinta aislante negra de 19 mm sobre un fondo blanco mate (cartulina, lona o piso claro), con curvas de al menos 10 cm de radio al principio. En la parte delantera del rover va una barra con 5 sensores infrarrojos en línea, a una distancia de 3 a 8 mm del suelo, con una separación de 12 a 20 mm entre ellos. Con cinco sensores, el robot no solo sabe «estoy sobre la línea»: sabe cuánto se alejó de ella, y eso es lo que alimenta el PID.

🌱 ¿Eres nuevo aquí?

Cada sensor (TCRT5000 o el par de una barra QTR) tiene un LED infrarrojo que ilumina el suelo y un fototransistor que mide cuánto reflejó. El blanco refleja mucho; el negro absorbe. La salida analógica da un número entre esos extremos, lo que permite saber que la línea está «a medio camino» debajo de un sensor. Usa la salida analógica (A0), no la digital (D0).

rover S0S1S2S3S4 barra a 3–8 mmdel suelo cinta negra19 mm

Cómo leer: la franja negra es la línea; los círculos azules rellenos son los sensores que la detectan (S3 con intensidad alta, S4 parcialmente), los círculos vacíos ven el blanco. Como la línea está a la derecha del centro (S2), el robot debe girar un poco a la derecha. Las barras cian a los lados son las ruedas.

PiezaPin del ESP32Observación
Sensores S0…S4 (A0)36, 39, 34, 35, 32solo ADC1: ADC2 no funciona con el Wi-Fi activado
Motor izquierdo (TB6612FNG)PWMA 25, AIN1 26, AIN2 27STBY en 3V3, VM en la batería
Motor derechoPWMB 14, BIN1 16, BIN2 17GND común con el ESP32
Módulo microSDCS 5, SCK 18, MISO 19, MOSI 23igual al módulo 2.5
Botón y LEDBOOT (0), LED (2)ya vienen en la placa

✓ Pista que ayuda

  • ✓Fondo mate y uniforme; cinta bien estirada, sin burbujas.
  • ✓Una recta larga para medir la velocidad y un tramo con curvas.
  • ✓Barra alimentada con 3,3 V para que la salida no supere el límite del ADC.

✗ Pista que dificulta

  • ✗Piso brillante o sol directo: el infrarrojo del ambiente satura el sensor.
  • ✗Barra demasiado alta (más de 1 cm): los sensores ven una mancha borrosa.
  • ✗Sensores conectados directamente a 5 V en el pin del ESP32.

Por qué aprender

La mitad de los problemas del seguidor de línea son mecánicos y ópticos, no de código. La altura de la barra, la separación y el fondo definen lo que el software puede hacer; conviene ajustar esto antes de escribir la primera línea del PID.

Conceptos clave

Reflectancia
el blanco refleja, el negro absorbe
Salida analógica
cuánto, no solo sí/no
ADC1
GPIO 32 a 39
Altura de la barra
3 a 8 mm
2

📐 Calcular el error de la línea (promedio ponderado)

Asigna a cada sensor una «dirección»: S0 vale 0, S1 vale 1000, S2 vale 2000, S3 vale 3000, S4 vale 4000. Después de la calibración, cada lectura se convierte en un número de 0 (blanco) a 1000 (sobre la línea). La posición de la línea es el promedio de las direcciones, ponderado por las lecturas: los sensores que ven más negro acercan el promedio a su posición. El centro es 2000, así que error = posición − 2000. Error negativo: línea a la izquierda. Positivo: a la derecha.

🌱 ¿Eres nuevo aquí?

Una promedio ponderado suma cada valor multiplicado por su peso y divídelo entre la suma de los pesos. Aquí, el «valor» es la posición del sensor y el «peso» es cuánto negro detecta. Fórmula: posición = Σ(lectura × posición) ÷ Σ(lectura).

0050010000 S0 · 0S1 · 1000S2 · 2000S3 · 3000S4 · 4000 centro 2667 (500×2000 + 1000×3000) ÷ 1500 = 2667 → error = +667

Cómo leer: la altura de cada barra indica cuánto negro detecta el sensor; abajo aparece la posición de cada uno. La línea discontinua es el centro (S2); la línea naranja es la posición calculada, desplazada hacia S3, que detecta más negro. La distancia entre ambas es el error que corregirá el PID.

¿Y si ningún sensor detecta la línea? La suma de las lecturas queda casi en cero y la división deja de tener sentido. El sketch guarda hacia qué lado estaba la línea por última vez y devuelve el extremo (0 o 4000): el robot gira con fuerza hacia ese lado hasta volver a encontrarla. Si pasan 0,4 s sin detectar la línea, se detiene.

Por qué aprender

El promedio ponderado transforma cinco sensores de encendido/apagado en una regla continua de 4001 puntos. Con ella, el PID corrige poco cuando la desviación es pequeña y mucho cuando es grande, y el robot avanza suavemente en vez de zigzaguear.

Conceptos clave

Posición
0 a 4000
Error
posición − 2000
Peso
cuánto negro detecta el sensor
Último lado
para volver a encontrar la línea
3

🎯 PID de dirección sumado a la velocidad base

El PID del módulo 2.3 entra aquí con una sola tarea: decidir cuánto girar. Recibe el error y devuelve una corrección. Las dos ruedas avanzan a la velocidad base, y la corrección se suma a una y se resta de la otra: izq = base + corr, der = base − corr. Si la línea está a la derecha (error positivo), la rueda izquierda acelera y la derecha frena, y el robot gira a la derecha.

barra IRposición −2000 PIDKp, Ki, Kd izq = base + corr der = base − corr errorcorr el robot gira → la línea cambia de posición bajo la barra (200 veces por segundo)

Cómo leer: sigue de izquierda a derecha: la barra mide, el círculo resta el centro, el PID decide la corrección y esta se divide en dos sentidos (más en una rueda, menos en la otra). La flecha cian cierra el ciclo: el giro del robot cambia la siguiente lectura.

Cálculo con el ejemplo del tema 2Resultado
error = 2667 − 2000+667
P = Kp × error = 0,08 × 66753
D = Kd × (error − error anterior) = 0,6 × (667 − 600)40
corr = P + D (Ki = 0 por ahora)93
izq = 150 + 93 · der = 150 − 93243 · 57

💡 ¿El robot se aleja de la línea en vez de seguirla?

La señal está invertida en algún punto: los sensores están numerados de derecha a izquierda, el motor está conectado al revés o la línea es clara sobre un fondo oscuro. No lo «corrijas» con un Kp negativo; prueba cada parte por separado (Serial Plotter con el robot detenido, una rueda a la vez) y corrígela desde el origen.

Por qué aprender

Separar «qué tan rápido» (base) de «cuánto girar» (corrección) es la práctica habitual en todos los robots con tracción diferencial, incluido el que sigue objetos con una cámara en la Trilha 3 y el que recibe comandos de velocidad en ROS 2.

Conceptos clave

Base
velocidad en línea recta
Corrección
salida del PID
Tracción diferencial
gira por la diferencia
Saturación
PWM limitado a ±255
4

🧭 Calibración automática de los sensores

No hay dos pares de sensores iguales, y la luz de la sala lo cambia todo. Por eso, el robot aprende la pista antes de correr: gira en el sitio durante 2,5 s, pasando la barra sobre la línea y el fondo, y guarda el valor mínimo y máximo de cada sensor. Después, cada lectura se convierte en (lectura − mínimo) × 1000 ÷ (máximo − mínimo), limitada entre 0 y 1000. Así, los cinco hablan el mismo idioma.

lectura sin procesar (0 a 4095) S0S1S2S3S4 04095 normalizado 01000

Cómo leer: a la izquierda, cada barra va desde el mínimo (fondo) hasta el máximo (línea) que midió ese sensor: empiezan y terminan en lugares diferentes. A la derecha, después del cálculo, todos ocupan el mismo rango de 0 a 1000. Una barra muy corta a la izquierda indica que el sensor está demasiado alto o sucio.

Objetivo: el sketch completo del proyecto. Presiona BOOT una vez: calibra. Coloca el robot sobre la línea y vuelve a presionar: la sigue. Presiona una vez más: se detiene y cierra el registro. Si no hay tarjeta, sigue sin guardar el registro. Si tu barra da valores menores sobre el negro (mira el Serial Monitor durante la calibración), cambia LINHA_ESCURA_ALTA a false.

seguidor-pid.ino
// Seguidor de línea con PID, calibración automática y registro en microSD
// ESP32 (paquete esp32 3.x) + barra de 5 sensores IR analógicos + TB6612FNG + módulo microSD
#include <SPI.h>
#include <SD.h>
// ---------- ajusta aquí ----------
float KP = 0.08, KI = 0.0, KD = 0.6;     // ganancias del PID de dirección (punto de partida)
int BASE = 150;                           // velocidad base (0 a 255)
const bool LINHA_ESCURA_ALTA = true;      // ¿la barra lee MÁS alto sobre el negro?
// ---------- pines ----------
const int SENS[5] = { 36, 39, 34, 35, 32 };   // S0 (izquierda) a S4 (derecha), solo ADC1
const int PWMA = 25, AIN1 = 26, AIN2 = 27;    // motor izquierdo
const int PWMB = 14, BIN1 = 16, BIN2 = 17;    // motor derecho (STBY en 3V3)
const int SD_CS = 5, BOTAO = 0, LED = 2;

int minimo[5], maximo[5], ultimoLado = 1;
float erroAnt = 0, integral = 0;
bool naLinha = false, temSD = false;
unsigned long tCiclo = 0, tViuLinha = 0;
enum Estado { ESPERA, CALIBRA, PRONTO, SEGUE };
Estado estado = ESPERA;
struct Amostra { uint32_t t; int16_t pos, erro, corr, esq, dir; };
QueueHandle_t filaLog;
File arq;

void motor(int pwm, int in1, int in2, int v) {
  v = constrain(v, -255, 255);
  digitalWrite(in1, v > 0); digitalWrite(in2, v < 0); ledcWrite(pwm, abs(v));   // API 3.x
}
void andar(int esq, int dir) { motor(PWMA, AIN1, AIN2, esq); motor(PWMB, BIN1, BIN2, dir); }
bool botaoApertado() { static int antes = HIGH; int a = digitalRead(BOTAO); bool b = (antes == HIGH && a == LOW); antes = a; return b; }

void calibrar() {                            // gira en el sitio durante 2,5 s; es la única espera del programa
  for (int i = 0; i < 5; i++) { minimo[i] = 4095; maximo[i] = 0; }
  unsigned long t0 = millis();
  while (millis() - t0 < 2500) {
    bool lado = ((millis() - t0) / 625) % 2;                 // cambia el sentido cada 0,625 s
    andar(lado ? 110 : -110, lado ? -110 : 110);
    for (int i = 0; i < 5; i++) { int v = analogRead(SENS[i]); minimo[i] = min(minimo[i], v); maximo[i] = max(maximo[i], v); }
  }
  andar(0, 0);
  for (int i = 0; i < 5; i++) Serial.printf("S%d: min %d max %d\n", i, minimo[i], maximo[i]);
}

int lerPosicao() {                           // 0 (línea bajo S0) a 4000 (bajo S4); 2000 = centro
  long soma = 0, pesos = 0;
  for (int i = 0; i < 5; i++) {
    long n = constrain((long)(analogRead(SENS[i]) - minimo[i]) * 1000 / max(maximo[i] - minimo[i], 1), 0L, 1000L);
    if (!LINHA_ESCURA_ALTA) n = 1000 - n;                    // 1000 = sobre la línea
    if (n > 50) { soma += n * i * 1000; pesos += n; }         // ignora el ruido del fondo
  }
  naLinha = pesos > 300;
  if (!naLinha) return ultimoLado < 0 ? 0 : 4000;            // la perdió: gira hacia donde desapareció
  int pos = soma / pesos;
  ultimoLado = pos < 2000 ? -1 : 1;
  return pos;
}

void registrar(uint32_t t, int pos, int erro, int corr, int esq, int dir) {
  if (!temSD) return;
  Amostra a = { t, (int16_t)pos, (int16_t)erro, (int16_t)corr, (int16_t)esq, (int16_t)dir };
  xQueueSend(filaLog, &a, t <= 1 ? pdMS_TO_TICKS(50) : 0);  // t = 0 abre el archivo, t = 1 lo cierra
}

void cicloControle() {                       // se ejecuta cada 5 ms (200 Hz)
  static uint8_t n = 0;
  int pos = lerPosicao();
  if (naLinha) tViuLinha = millis();
  else if (millis() - tViuLinha > 400) { andar(0, 0); return; }   // salió de la pista: se detiene
  float erro = pos - 2000;                   // negativo: línea a la izquierda
  integral = constrain(integral + erro, -20000, 20000);        // limita el windup
  float corr = KP * erro + KI * integral + KD * (erro - erroAnt);
  erroAnt = erro;
  int esq = constrain(BASE + corr, -255, 255), dir = constrain(BASE - corr, -255, 255);
  andar(esq, dir);
  if (++n % 2 == 0) registrar(millis(), pos, erro, corr, esq, dir);                     // registro a 100 Hz
  if (n % 10 == 0) Serial.printf("erro:%.0f,corr:%.0f,esq:%d,dir:%d\n", erro, corr, esq, dir);   // Plotter a 20 Hz
}

void abrirProximo() {                        // /pid001.csv, /pid002.csv... uno por carrera
  char nome[16];
  for (int i = 1; i < 1000; i++) { snprintf(nome, sizeof nome, "/pid%03d.csv", i); if (!SD.exists(nome)) break; }
  arq = SD.open(nome, FILE_WRITE);
  arq.printf("# KP=%.3f KI=%.4f KD=%.3f BASE=%d\n", KP, KI, KD, BASE);
  arq.println("t_ms,pos,erro,corr,esq,dir");
  Serial.printf("log: %s\n", nome);
}

void tarefaLog(void *p) {                    // núcleo 0: guarda sin retrasar el control
  Amostra a; int linhas = 0;
  for (;;) {
    if (xQueueReceive(filaLog, &a, portMAX_DELAY) != pdTRUE) continue;
    if (a.t == 0) { abrirProximo(); linhas = 0; }
    else if (a.t == 1) { if (arq) arq.close(); }
    else if (arq) {
      arq.printf("%lu,%d,%d,%d,%d,%d\n", (unsigned long)a.t, a.pos, a.erro, a.corr, a.esq, a.dir);
      if (++linhas % 50 == 0) arq.flush();
    }
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(AIN1, OUTPUT); pinMode(AIN2, OUTPUT); pinMode(BIN1, OUTPUT); pinMode(BIN2, OUTPUT);
  ledcAttach(PWMA, 20000, 8); ledcAttach(PWMB, 20000, 8);
  pinMode(BOTAO, INPUT_PULLUP); pinMode(LED, OUTPUT);
  temSD = SD.begin(SD_CS);
  if (temSD) { filaLog = xQueueCreate(64, sizeof(Amostra)); xTaskCreatePinnedToCore(tarefaLog, "log", 4096, NULL, 1, NULL, 0); }
  Serial.println(temSD ? "SD ok" : "sin SD: sigue sin registro");
  Serial.println("BOOT: calibrar, luego seguir, luego detenerse");
}

void loop() {
  bool botao = botaoApertado();
  switch (estado) {
    case ESPERA:  if (botao) estado = CALIBRA; break;
    case CALIBRA: calibrar(); estado = PRONTO; Serial.println("Calibrado: colócalo sobre la línea y presiona BOOT"); break;
    case PRONTO:
      if (botao) { erroAnt = 0; integral = 0; tViuLinha = millis(); registrar(0, 0, 0, 0, 0, 0); tCiclo = micros(); estado = SEGUE; }
      break;
    case SEGUE:
      if (botao) { andar(0, 0); registrar(1, 0, 0, 0, 0, 0); estado = PRONTO; Serial.println("Parado"); break; }
      if (micros() - tCiclo >= 5000) { tCiclo += 5000; cicloControle(); }   // 200 Hz, sin delay
      break;
  }
  digitalWrite(LED, estado == SEGUE);
}

Cómo verificar

  • ✓Durante la calibración, el Serial Monitor muestra 5 líneas como «S3: min 612 max 3580», con una distancia de al menos 1000 entre el mínimo y el máximo.
  • ✓Con el robot suspendido y la línea bajo S3, el Plotter muestra un error positivo, izq mayor que der.
  • ✓Después de una carrera, la tarjeta tiene /pid001.csv con la línea # KP=... en la parte superior.
  • ✗Mínimo y máximo casi iguales: barra demasiado alta, sensor en el pin equivocado o LED IR apagado (míralo con la cámara del celular: el LED aparece morado).

Por qué aprender

Calibrar al inicio de cada recorrido permite que el mismo código funcione con distintas pistas, luces y sensores. En una competencia, esto marca la diferencia entre el robot que funcionó «en el taller» y el que funciona durante la prueba.

Conceptos clave

Mínimo/máximo
fondo y línea, por sensor
Normalizar
todos de 0 a 1000
200 Hz
ciclo fijo de 5 ms
Registro en otra tarea
cola del módulo 2.4
5

📊 Ajuste con telemetría (antes/después)

«Parece que mejoró» puede engañarte. Cada recorrido guarda un archivo con las ganancias en el encabezado, y puedes comparar dos archivos con dos números: el error RMS (el tamaño típico de la desviación; cuanto menor, mejor) y el error máximo (el peor momento, generalmente una curva). Cambia un un parámetro a la vez, en la misma pista y con la batería con la misma carga.

0 +− curvacurva antes: KP 0,12, KD 0 (RMS alto)después: KP 0,08, KD 0,6

Cómo leer: el eje horizontal muestra el tiempo del recorrido y el vertical, el error (cero = línea en el centro). La curva roja nunca se estabiliza: el robot zigzaguea incluso en la recta. La cian se mantiene cerca de cero y solo sube en las curvas, para luego volver. Ese es el patrón que buscas en el PNG.

Paso a paso para ajustar en la pista

1

Solo P, despacio

BASE 100, KI 0, KD 0. Sube KP (0,02 → 0,04 → 0,06...) hasta que el robot recorra toda la pista, aunque se balancee.

2

Usa D para amortiguar

Sube KD (0,2 → 0,4 → 0,8...) hasta que desaparezca el balanceo en la recta. Si el robot empieza a temblar rápidamente, te excediste: baja un paso.

3

Más velocidad

Sube BASE de 20 en 20. En cada incremento, vuelve a ajustar KP y KD observando el RMS.

4

Usa I solo si hace falta

Si el error promedio siempre queda de un lado (el robot «se desvía»), un KI pequeño (0,0005) lo corrige. En la mayoría de las pistas, KI = 0.

Objetivo: comparar dos recorridos con números y en un solo gráfico. Usa el mismo entorno de Python del módulo 2.5. Para ver un recorrido por separado, el analisa_log.py del 2.5 ya sirve: python analisa_log.py pid001.csv erro corr.

compara.py
# compara.py: antes x después de un ajuste del PID
# Uso: python compara.py pid001.csv pid002.csv
import sys
import pandas as pd
import matplotlib.pyplot as plt

for arquivo in sys.argv[1:]:
    ganhos = open(arquivo).readline().strip("# \n")           # línea "# KP=... KD=..."
    df = pd.read_csv(arquivo, comment="#")
    t = (df["t_ms"] - df["t_ms"].iloc[0]) / 1000
    rms = (df["erro"] ** 2).mean() ** 0.5
    satur = ((df["esq"].abs() >= 255) | (df["dir"].abs() >= 255)).mean() * 100
    print(f"{arquivo}: {ganhos} | RMS {rms:.0f} | máx {df['erro'].abs().max()} | volta {t.iloc[-1]:.2f} s | saturado {satur:.0f}%")
    plt.plot(t, df["erro"], label=f"{arquivo} ({ganhos})")

plt.axhline(0, color="gray", lw=0.8)
plt.xlabel("tiempo (s)"); plt.ylabel("erro"); plt.legend(); plt.grid(alpha=0.3)
plt.savefig("comparacao.png", dpi=120)
plt.show()

Cómo verificar

  • ✓Una línea por archivo, con las ganancias y el RMS: un buen ajuste tiene un RMS menor y la curva se mantiene más cerca de cero en comparacao.png.
  • ✓Un valor de «saturado» superior al 20% indica que BASE es demasiado alta: las ruedas ya están al límite y no queda margen para corregir.

Por qué aprender

Medir antes y después es un método de ingeniería en miniatura: hipótesis, un cambio, datos y conclusión. Estos mismos dos números sirven para ajustar el robot con cámara de la Trilha 3 y para defender el proyecto en una feria o competencia.

Conceptos clave

Error RMS
desviación típica
Error máximo
el peor momento
Un parámetro a la vez
para saber qué ayudó
Saturación
PWM atascado en el límite
6

🏆 Desafíos: curvas cerradas, cruces, cronómetro

Ahora que el robot completa la pista, los desafíos siguientes te ayudan a practicar lo que exigen las pruebas de seguimiento de línea (como la modalidad práctica de la OBR). Cada uno consiste en un pequeño cambio en el sketch; hazlos de uno en uno y compara los resultados con el registro.

↩️ Curvas cerradas

Reduce la velocidad base cuando aumenta el error: int base = BASE - abs(erro) * 0.04; y usa base en lugar de BASE. Recta rápida, curva segura.

✚ Cruces

Cuando los cinco sensores superan 700 al mismo tiempo, es un cruce: mantén el último error durante 100 ms y sigue recto, sin dejar que el PID reaccione a la «línea ancha».

⏱️ Cronómetro

Una marca lateral en la salida, detectada por un sexto sensor (GPIO 33, también ADC1): guarda el millis() en cada paso y muestra el tiempo de la vuelta.

✓ Seguridad y método

  • ✓Prueba cada cambio primero con el robot suspendido y después en la pista.
  • ✓Mantén la mano cerca de BOOT durante las primeras vueltas; la parada al perder la línea es la segunda protección.
  • ✓Anota en la bitácora cada archivo de registro y qué cambió.

✗ Errores en la competencia

  • ✗Ajustar con la batería cargada y competir con ella a media carga (o al revés).
  • ✗Saltarse la calibración «porque ayer funcionó»: la iluminación del gimnasio es diferente.
  • ✗Dejar la batería LiPo cargando sola en la mesa de trabajo durante las pruebas.

💡 Cierre de la Trilha 2

Ahora tienes un robot que controlas desde el celular, que sabe medir su propio movimiento, ejecuta PID a un ritmo fijo, divide el trabajo en tareas y registra todo. El límite que queda es el sensor: cinco puntos de luz en el suelo. En la Trilha 3, el robot incorpora una cámara y una Raspberry Pi 5, y el mismo PID empieza a seguir objetos y personas.

Por qué aprender

Los pequeños desafíos con mediciones son el puente entre «funciona» y «funciona bien». El hábito de cambiar una cosa, guardar los datos y comparar es algo que llevarás a todos los proyectos de las próximas trilhas.

Conceptos clave

Velocidad base variable
reduce la velocidad en la curva
Cruce
todos detectan negro
Tiempo de vuelta
millis() entre marcas
Bitácora
registro + qué cambió

✅ Resumen del módulo y de la Trilha 2

✓
Pista y barra: fondo mate, barra a 3–8 mm, cinco sensores analógicos en ADC1.
✓
Posición mediante promedio ponderado de 0 a 4000; error = posición − 2000; conserva el último lado para volver a encontrar la línea.
✓
esq = base + corr, dir = base − corr, con el PID a 200 Hz y la salida limitada.
✓
Calibración en cada recorrido: mínimo y máximo por sensor, todo normalizado de 0 a 1000.
✓
Ajuste con números: un parámetro a la vez, RMS y error máximo, antes y después.

Para profundizar

Siguiente ruta

Ruta 3 · Visión e IA integrada. Empieza por el módulo 3.1: prepara la Raspberry Pi 5 con Linux, la «computadora que piensa» junto al ESP32 que ya dominas.