Contenido detallado
🛤️ 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).
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.
| Pieza | Pin del ESP32 | Observación |
|---|---|---|
| Sensores S0…S4 (A0) | 36, 39, 34, 35, 32 | solo ADC1: ADC2 no funciona con el Wi-Fi activado |
| Motor izquierdo (TB6612FNG) | PWMA 25, AIN1 26, AIN2 27 | STBY en 3V3, VM en la batería |
| Motor derecho | PWMB 14, BIN1 16, BIN2 17 | GND común con el ESP32 |
| Módulo microSD | CS 5, SCK 18, MISO 19, MOSI 23 | igual al módulo 2.5 |
| Botón y LED | BOOT (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
📐 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).
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
🎯 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.
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 2 | Resultado |
|---|---|
| error = 2667 − 2000 | +667 |
| P = Kp × error = 0,08 × 667 | 53 |
| D = Kd × (error − error anterior) = 0,6 × (667 − 600) | 40 |
| corr = P + D (Ki = 0 por ahora) | 93 |
| izq = 150 + 93 · der = 150 − 93 | 243 · 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
🧭 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.
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 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
📊 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.
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
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.
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.
Más velocidad
Sube BASE de 20 en 20. En cada incremento, vuelve a ajustar KP y KD observando el RMS.
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: 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
🏆 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.
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.
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».
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
✅ Resumen del módulo y de la Trilha 2
Para profundizar
- Biblioteca QTR (Pololu) — la referencia clásica de la barra de sensores y del promedio ponderado
- ADC en Arduino-ESP32 — pines, atenuación y límites
- LEDC (PWM) en la API 3.x — ledcAttach y ledcWrite
- Olimpíada Brasileira de Robótica — reglas y pistas de la modalidad práctica
- Wokwi — prueba la lógica de estados y el PID con potenciómetros en lugar de los sensores
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.