Robot seguidor de línea con Arduino: sensores TCRT5000 y PID
Robot seguidor de línea con Arduino: barra TCRT5000, colocación y calibración de sensores, código if-else y PID, ajuste de Kp/Kd, driver, batería y reglas.
Un robot seguidor de línea es un vehículo autónomo que detecta una línea negra (o blanca) en el suelo con sensores infrarrojos y ajusta sin parar la velocidad de sus dos ruedas para mantenerse sobre ella. Es uno de los primeros proyectos de robótica de verdad que se hacen con Arduino, porque reúne en un solo proyecto la lectura de sensores, la toma de decisiones y el control de motores. Los vehículos autónomos (AGV) que transportan material en las fábricas funcionan con la misma idea, y el seguidor de línea es una de las categorías más concurridas en las competiciones escolares de robótica.
En esta guía aprenderás cómo funciona el sensor TCRT5000, en qué se diferencian las barras de 2, 3 y 5 sensores, a qué altura y con qué separación montar los sensores y cómo calibrar el umbral. Primero harás que el robot avance con un código if-else sencillo y después pasarás a una versión con control PID que sigue la línea sin zigzaguear ni salirse de la pista en las curvas. Además del código para Arduino Uno, ESP32 y Raspberry Pi Pico, encontrarás cómo elegir el driver de motores y la batería, las reglas típicas de competición y la solución a los problemas más habituales.

¿Cómo funciona un robot seguidor de línea?
Una superficie blanca refleja la mayor parte de la luz infrarroja, mientras que una superficie negra mate absorbe casi toda. Los sensores situados bajo el robot miden esa diferencia y así saben dónde está la línea respecto al robot. El robot no tiene dirección: gira con lo que se llama tracción diferencial. Si la rueda izquierda gira más rápido que la derecha, el robot se curva hacia la derecha, y si gira más despacio, hacia la izquierda. El programa repite el mismo bucle cientos de veces por segundo: leer los sensores, calcular cuánto se ha desviado la línea y corregir la velocidad de los dos motores. Cuanto más rápido y estable sea este lazo cerrado, más fluido irá el robot.
| Pieza | Recomendado | Nota |
|---|---|---|
| Placa de control | Arduino Uno o Nano | El Nano cabe mejor en un chasis pequeño; también sirven un ESP32 o una Pico |
| Sensor de línea | Barra TCRT5000 de 3 o 5 canales, QTR-8A | Con PID da mejor resultado una barra de salida analógica |
| Driver de motores | L298N o TB6612FNG | El TB6612FNG es más pequeño y eficiente |
| Motores | 2 motores DC con reductora (TT 1:48 o N20) | Los dos motores deben ser del mismo modelo y velocidad |
| Ruedas | 2 ruedas motrices y 1 rueda loca | Las ruedas de 65 mm de diámetro son las más comunes |
| Alimentación | 2 baterías Li-ion 18650 (7,4 V) e interruptor | Una pila de 9 V no da suficiente corriente para los motores |
| Otros | Chasis, pulsador, cables jumper | Un pulsador facilita la calibración y el arranque |




El sensor TCRT5000: barras de 2, 3 y 5 sensores
El TCRT5000 está formado por un LED infrarrojo (950 nm) y un fototransistor colocados uno junto al otro en el mismo encapsulado. Cuanta más luz del LED se refleja en el suelo y llega al fototransistor, más conduce el transistor. El sensor funciona mejor a unos 2,5 mm de distancia; por encima de 1 cm la diferencia entre negro y blanco cae rápidamente. Los módulos listos para usar llevan un comparador LM393: en la mayoría, el pin DO da HIGH sobre negro y LOW sobre blanco según el umbral ajustado con el potenciómetro, y el LED de la placa se enciende sobre superficie blanca. El pin AO da el valor analógico en bruto: alto sobre negro, bajo sobre blanco. Tienes todos los detalles del sensor en Sensor infrarrojo TCRT5000.
| Sensores | Colocación | Algoritmo | Cuándo elegirlo |
|---|---|---|---|
| 2 | A ambos lados de la línea; los dos ven blanco normalmente | if-else | Primer intento, pista ancha con curvas suaves |
| 3 | El central sobre la línea y los laterales en sus bordes | if-else o PID sencillo | Proyectos escolares, competiciones de nivel básico |
| 5 | Fila con separación uniforme, el central sobre la línea | PID (media ponderada) | Seguimiento rápido y suave |
| 8 | Fila ancha (como la QTR-8A) | PID y detección de cruces | Pistas de competición con curvas cerradas y cruces |
Colocación de los sensores: altura, separación y distancia al eje
Por muy bueno que sea tu código, si los sensores están mal colocados el robot no irá bien. Monta la barra de sensores delante del eje de las ruedas, paralela al suelo y simétrica respecto al centro del robot. Estas medidas son un buen punto de partida:
- Altura: Deja 3–5 mm entre la cara inferior del sensor y el suelo. Pololu indica para su barra QTR-8A una distancia óptima de 3 mm y una máxima recomendada de 6 mm. Más arriba, la diferencia entre negro y blanco se reduce y el robot pierde la línea.
- Separación: En las barras analógicas, la distancia entre dos sensores debe estar entre la mitad y dos tercios del ancho de la línea; para una línea de 20 mm van bien 10–13 mm. Así la línea suele quedar bajo dos sensores y la posición se calcula de forma suave.
- Disposición digital de 3 sensores: El sensor central va justo encima de la línea y los laterales justo fuera de sus bordes; para una línea de 20 mm, unos 18–22 mm de centro a centro.
- Distancia al eje: Coloca los sensores 4–8 cm por delante del eje de las ruedas. Si están demasiado cerca, el robot detecta las curvas tarde; si están demasiado lejos, un pequeño giro mueve mucho los sensores y el robot tiembla.
- Luz ambiente: La luz del sol y las lámparas halógenas también contienen infrarrojo. Cubrir los sensores con una falda o cinta negra hace que las lecturas sean mucho más estables.
Conexiones: Arduino Uno, ESP32 y Pico
La tabla siguiente coincide exactamente con el código de prueba de sensores y los códigos PID; las conexiones del ejemplo if-else de 3 sensores se explican en su propia sección. Numera los sensores de izquierda a derecha mirando el robot desde atrás: el sensor de más a la izquierda es el número 1. Como driver de motores usamos un L298N; la función de cada pin y la lógica del puente H se explican en detalle en Control de motor DC con L298N. La Pico solo tiene tres entradas analógicas, así que en ella usamos los sensores 1, 3 y 5.
| Conexión | Arduino Uno | ESP32 | Raspberry Pi Pico |
|---|---|---|---|
| Sensor 1 (el de la izquierda) AO | A0 | GPIO 36 | GP26 |
| Sensor 2 AO | A1 | GPIO 39 | - |
| Sensor 3 (centro) AO | A2 | GPIO 34 | GP27 |
| Sensor 4 AO | A3 | GPIO 35 | - |
| Sensor 5 (el de la derecha) AO | A4 | GPIO 32 | GP28 |
| VCC de los sensores | 5V | 3V3 | 3V3 |
| L298N ENA (velocidad motor izquierdo) | D9 | GPIO 25 | GP15 |
| L298N IN1, IN2 | D8, D7 | GPIO 26, 27 | GP14, GP13 |
| L298N ENB (velocidad motor derecho) | D10 | GPIO 33 | GP10 |
| L298N IN3, IN4 | D5, D4 | GPIO 18, 19 | GP11, GP12 |
| Pulsador de arranque (otra pata a GND) | D2 | GPIO 23 | GP16 |
| GND | GND | GND | GND (común con batería y driver) |
Cómo elegir el driver de motores y la batería
El L298N es barato y resistente, pero sus transistores bipolares se comen entre 1,5 y 3 V: de un pack de 7,4 V a los motores solo les llegan unos 5–5,5 V. En robots pequeños y rápidos funciona mejor un TB6612FNG o un DRV8833 con MOSFET. Con el TB6612FNG el código no cambia: conectas los cables de ENA, IN1 e IN2 a PWMA, AIN1 y AIN2 (los del canal B a PWMB, BIN1 y BIN2) y el pin STBY a VCC. El DRV8833 no tiene un pin de velocidad (enable) aparte: la señal PWM va directamente a los pines IN, así que tendrás que adaptar la función drive(). Para alimentarlo, lo más práctico son dos baterías Li-ion 18650 en serie: 8,4 V cargadas, 7,4 V nominales, y entregan corriente alta sin problemas. Las pilas de 9 V no aguantan la corriente de los motores y se agotan enseguida. Puedes calcular cuánto durará tu batería con la herramienta Duración de la batería.
| Driver | Tensión del motor | Corriente por canal | Caída de tensión | Valoración |
|---|---|---|---|---|
| L298N | 5–35 V | 2 A | 1,5–3 V | Resistente, pero grande y poco eficiente |
| TB6612FNG | 4,5–13,5 V | 1,2 A (pico 3,2 A) | Muy baja | La mejor relación calidad-precio para un seguidor |
| DRV8833 | 2,7–10,8 V | 1,5 A (pico 2 A) | Muy baja | Robots pequeños con motores N20 |
| L9110S | 2,5–12 V | 0,8 A | Baja | Solo robots ligeros y lentos |

Un ejemplo de velocidad: los motores amarillos TT (1:48) giran a unas 200 rpm sin carga a 6 V. Una rueda de 65 mm tiene una circunferencia de π × 65 ≈ 204 mm, así que el robot puede ir como máximo a 200 × 0,204 ≈ 41 m por minuto, unos 0,7 m por segundo. Es más que suficiente para la mayoría de pistas escolares; lo difícil es no perder la línea a esa velocidad. Los robots más rápidos usan micromotores N20 con reductora y un chasis ligero. Al bajar la tensión de la batería los motores se frenan y los valores PID que ajustaste dejan de servir; medir la tensión con un divisor de tensión y escalar la velocidad en función de ella es una mejora de nivel avanzado.
Paso 1: Prueba los sensores y encuentra el umbral
Antes de conectar los motores, averigua qué ven los sensores. El código siguiente imprime el valor analógico en bruto de los cinco sensores separado por tabuladores; si abres en el IDE de Arduino Herramientas > Serial Plotter a 115200 baudios verás cada sensor como una línea distinta. Pasa el robot despacio por encima de la línea con la mano y observa cómo cambian los valores.
1// 5 sensörün ham değerlerini okur / reads the raw values of 5 sensors
2const int N = 5;
3const int SENSOR[N] = {A0, A1, A2, A3, A4}; // A0 en solda / A0 is the leftmost
4
5void setup() {
6 Serial.begin(115200);
7}
8
9void loop() {
10 for (int i = 0; i < N; i++) {
11 Serial.print(analogRead(SENSOR[i]));
12 Serial.print(i < N - 1 ? '\t' : '\n'); // Serial Plotter için sekme / tabs for the Serial Plotter
13 }
14 delay(50);
15}A 5 V, un módulo TCRT5000 típico da entre 30 y 200 sobre blanco y entre 600 y 1000 sobre la línea negra, aunque estas cifras dependen de la altura del sensor, del material del suelo y de la luz ambiente. El umbral más sencillo es: umbral = (valor en blanco + valor en negro) / 2. Por ejemplo, si lees 80 en blanco y 820 en negro, el umbral es 450. Como cada sensor lee un poco distinto, el código PID lo hará de forma automática: durante la calibración guarda el valor mínimo y máximo de cada sensor y luego escala cada lectura entre 0 (blanco) y 1000 (negro) con (lectura − mín) × 1000 / (máx − mín). Así el umbral queda en 500 para todos los sensores.
Paso 2: Algoritmo if-else sencillo (3 sensores)
Para un primer robot que funcione, lo más claro es elegir entre unos cuantos movimientos fijos según el estado de los tres sensores. Conecta las salidas DO de un módulo de 3 canales a A0 (izquierda), A1 (centro) y A2 (derecha); los pines analógicos del Uno también sirven como entradas digitales. La lógica es la de esta tabla (1 significa que el sensor ve la línea):
| Izq. | Centro | Der. | Significado | Movimiento |
|---|---|---|---|---|
| 0 | 1 | 0 | Línea en el centro | Recto |
| 1 | 1 | 0 | Línea un poco a la izquierda | La rueda izquierda frena, giro suave a la izquierda |
| 1 | 0 | 0 | Línea muy a la izquierda | La rueda izquierda va hacia atrás, giro brusco a la izquierda |
| 0 | 1 | 1 | Línea un poco a la derecha | La rueda derecha frena, giro suave a la derecha |
| 0 | 0 | 1 | Línea muy a la derecha | La rueda derecha va hacia atrás, giro brusco a la derecha |
| 1 | 1 | 1 | Cruce o línea de meta | Seguir recto |
| 0 | 0 | 0 | Línea perdida | Girar hacia el último lado visto |
1// 3 sensörlü basit çizgi izleyen / simple 3-sensor line follower
2const int ENA = 9, IN1 = 8, IN2 = 7; // sol motor / left motor
3const int ENB = 10, IN3 = 5, IN4 = 4; // sağ motor / right motor
4const int S_LEFT = A0, S_MID = A1, S_RIGHT = A2; // sensörlerin DO çıkışları / sensor DO outputs
5const bool WHITE_LINE = false; // siyah zemin + beyaz çizgi ise true / true for a white line on black
6
7const int SPEED = 150; // düz gidiş hızı / straight-line speed
8const int SOFT = 80; // hafif dönüşte iç teker / inner wheel on a soft turn
9const int BACK = -90; // sert dönüşte iç teker geri / inner wheel reverses on a sharp turn
10int lastSide = 0; // -1 sol, 1 sağ / -1 left, 1 right
11
12void drive(int left, int right) { // her biri -255 … 255 / each -255 … 255
13 left = constrain(left, -255, 255);
14 right = constrain(right, -255, 255);
15 digitalWrite(IN1, left >= 0); digitalWrite(IN2, left < 0);
16 digitalWrite(IN3, right >= 0); digitalWrite(IN4, right < 0);
17 analogWrite(ENA, abs(left));
18 analogWrite(ENB, abs(right));
19}
20
21void setup() {
22 int outputs[] = {ENA, IN1, IN2, ENB, IN3, IN4};
23 for (int p : outputs) pinMode(p, OUTPUT);
24 pinMode(S_LEFT, INPUT);
25 pinMode(S_MID, INPUT);
26 pinMode(S_RIGHT, INPUT);
27}
28
29void loop() {
30 bool l = digitalRead(S_LEFT) != WHITE_LINE; // true = sensör çizgide / true = sensor on the line
31 bool m = digitalRead(S_MID) != WHITE_LINE;
32 bool r = digitalRead(S_RIGHT) != WHITE_LINE;
33
34 if (m && !l && !r) { // tam ortada: düz git / centered: go straight
35 drive(SPEED, SPEED);
36 } else if (l && r) { // kavşak ya da bitiş çizgisi: düz geç / junction or finish line: go straight
37 drive(SPEED, SPEED);
38 } else if (l && m) { // çizgi biraz solda / line slightly to the left
39 drive(SOFT, SPEED); lastSide = -1;
40 } else if (r && m) { // çizgi biraz sağda / line slightly to the right
41 drive(SPEED, SOFT); lastSide = 1;
42 } else if (l) { // çizgi çok solda / line far to the left
43 drive(BACK, SPEED); lastSide = -1;
44 } else if (r) { // çizgi çok sağda / line far to the right
45 drive(SPEED, BACK); lastSide = 1;
46 } else { // çizgi kayıp: son görülen tarafa dön / line lost: turn to the last seen side
47 if (lastSide < 0) drive(BACK, SPEED);
48 else if (lastSide > 0) drive(SPEED, BACK);
49 else drive(SOFT, SOFT);
50 }
51}La variable lastSide recuerda en qué lado vio el robot la línea por última vez; si en una curva cerrada los tres sensores pierden la línea, el robot gira hacia ese lado para recuperarla en lugar de seguir recto sin rumbo. Con dos sensores la lógica es todavía más sencilla: los sensores van a ambos lados de la línea, el robot sigue recto mientras los dos ven blanco, gira a la izquierda cuando el izquierdo ve negro y a la derecha cuando lo ve el derecho. El punto débil de este método es que los movimientos van a saltos: el robot solo cambia entre unas pocas velocidades fijas, así que zigzaguea un poco incluso en las rectas y se sale en las curvas en cuanto subes la velocidad.
Paso 3: Seguimiento suave con PID
El PID aplica una corrección proporcional a lo que el robot se ha desviado de la línea. Primero se calcula la posición de la línea con los cinco sensores mediante una media ponderada: el valor de cada sensor (0–1000) se multiplica por su propia posición (0, 1000, 2000, 3000, 4000), se suma todo y se divide entre la suma de los valores. El resultado es un número entre 0 y 4000, que vale 2000 cuando la línea está justo en el centro. Por ejemplo, si los sensores leen 0, 0, 300, 900, 0, la posición es (300 × 2000 + 900 × 3000) / 1200 = 2750. El error = posición − 2000: un valor positivo indica que la línea está a la derecha y uno negativo, a la izquierda.
La corrección es la suma de tres términos: corrección = Kp × error + Ki × suma de errores + Kd × (error − error anterior). El término P gira con más fuerza cuanto mayor es la desviación. El término D mira lo rápido que cambia el error y frena cuando el robot se acerca a la línea, amortiguando la oscilación. El término I corrige pequeños errores que se mantienen mucho tiempo hacia un lado, pero en un seguidor de línea casi siempre se deja a cero. Después, el motor izquierdo gira a velocidad base + corrección y el derecho a velocidad base − corrección. Por ejemplo, con un error de 800 y Kp = 0,06 la corrección es 48; con velocidad base 140, el motor izquierdo va a 188, el derecho a 92 y el robot gira suavemente a la derecha.
1// PID çizgi izleyen: Arduino Uno + 5 analog sensör + L298N
2// PID line follower: Arduino Uno + 5 analog sensors + L298N
3const int ENA = 9, IN1 = 8, IN2 = 7; // sol motor / left motor
4const int ENB = 10, IN3 = 5, IN4 = 4; // sağ motor / right motor
5const int BUTTON = 2; // diğer bacağı GND'ye / other leg to GND
6const int N = 5;
7const int SENSOR[N] = {A0, A1, A2, A3, A4}; // A0 en solda / A0 is the leftmost
8const bool WHITE_LINE = false; // siyah zemin + beyaz çizgi ise true / true for a white line on black
9const int CENTER = (N - 1) * 500; // orta konum: 2000 / center position: 2000
10
11float Kp = 0.06, Ki = 0.0, Kd = 0.6; // başlangıç değerleri / starting values
12int baseSpeed = 140; // düzlük hızı (0-255) / straight-line speed (0-255)
13const unsigned long PERIOD_MS = 5; // sabit döngü süresi / fixed loop period
14
15int minVal[N], maxVal[N];
16int lastPosition = CENTER;
17float integral = 0, lastError = 0;
18unsigned long lastTime = 0;
19
20void drive(int left, int right) { // her biri -255 … 255 / each -255 … 255
21 left = constrain(left, -255, 255);
22 right = constrain(right, -255, 255);
23 digitalWrite(IN1, left >= 0); digitalWrite(IN2, left < 0);
24 digitalWrite(IN3, right >= 0); digitalWrite(IN4, right < 0);
25 analogWrite(ENA, abs(left));
26 analogWrite(ENB, abs(right));
27}
28
29void waitButton() {
30 while (digitalRead(BUTTON) == HIGH) {} // basılmasını bekle / wait for a press
31 delay(30);
32 while (digitalRead(BUTTON) == LOW) {} // bırakılmasını bekle / wait for the release
33 delay(30);
34}
35
36void calibrate() { // 5 sn: robotu çizginin üstünde sağa sola gezdir / 5 s: sweep the robot across the line
37 for (int i = 0; i < N; i++) { minVal[i] = 1023; maxVal[i] = 0; }
38 digitalWrite(LED_BUILTIN, HIGH);
39 unsigned long start = millis();
40 while (millis() - start < 5000) {
41 for (int i = 0; i < N; i++) {
42 int v = analogRead(SENSOR[i]);
43 if (v < minVal[i]) minVal[i] = v;
44 if (v > maxVal[i]) maxVal[i] = v;
45 }
46 }
47 digitalWrite(LED_BUILTIN, LOW);
48}
49
50int readPosition() { // 0 (en sol) … 4000 (en sağ) / 0 (leftmost) … 4000 (rightmost)
51 long sum = 0, weighted = 0;
52 bool onLine = false;
53 for (int i = 0; i < N; i++) {
54 int range = max(maxVal[i] - minVal[i], 1);
55 long v = (long)(analogRead(SENSOR[i]) - minVal[i]) * 1000 / range;
56 v = constrain(v, 0L, 1000L); // 0 = beyaz, 1000 = siyah / 0 = white, 1000 = black
57 if (WHITE_LINE) v = 1000 - v;
58 if (v > 500) onLine = true; // eşik: kalibrasyonun ortası / threshold: middle of the calibration
59 if (v > 50) { // küçük gürültüyü yok say / ignore small noise
60 weighted += v * (i * 1000L);
61 sum += v;
62 }
63 }
64 if (!onLine) { // çizgi kayıp: son görülen tarafa dön / line lost: turn to the last seen side
65 return (lastPosition < CENTER) ? 0 : (N - 1) * 1000;
66 }
67 lastPosition = weighted / sum;
68 return lastPosition;
69}
70
71void setup() {
72 int outputs[] = {ENA, IN1, IN2, ENB, IN3, IN4, LED_BUILTIN};
73 for (int p : outputs) pinMode(p, OUTPUT);
74 pinMode(BUTTON, INPUT_PULLUP);
75 drive(0, 0);
76 waitButton(); // 1. basış: kalibrasyon / 1st press: calibration
77 calibrate();
78 waitButton(); // 2. basış: yarış / 2nd press: race
79 lastTime = millis();
80}
81
82void loop() {
83 if (millis() - lastTime < PERIOD_MS) return;
84 lastTime += PERIOD_MS;
85
86 float error = readPosition() - CENTER; // artı: çizgi sağda / positive: line is to the right
87 integral = constrain(integral + error, -20000.0f, 20000.0f); // birikmeyi sınırla / anti-windup
88 float derivative = error - lastError;
89 lastError = error;
90
91 int correction = (int)(Kp * error + Ki * integral + Kd * derivative);
92 drive(baseSpeed + correction, baseSpeed - correction);
93}Después de cargar el código, pulsa el botón una vez: el LED de la placa se enciende durante 5 segundos. En ese tiempo, desliza el robot con la mano a izquierda y derecha sobre la línea varias veces para que cada sensor vea tanto el negro como el blanco. Cuando el LED se apague, pon el robot sobre la línea y vuelve a pulsar el botón. El bucle se ejecuta cada 5 ms gracias a millis(); un periodo fijo hace que el término D tenga siempre la misma escala. Si la línea desaparece por completo, readPosition() devuelve 0 o 4000 según el último lado en que se vio y el robot la busca con el giro más cerrado. Para conectar el pulsador consulta Cómo conectar un pulsador a Arduino, y para temporizar sin delay, Arduino millis() y delay().
Cómo ajustar Kp y Kd
- Mantén baja la velocidad base (por ejemplo, 100) y pon Ki y Kd a cero.
- Empieza con un Kp pequeño (0,02) y súbelo poco a poco hasta que el robot tome las curvas. Cuando el robot empiece a oscilar rápido y de forma regular alrededor de la línea, habrás llegado al límite superior de Kp; quédate un poco por debajo.
- Empieza Kd en unas 10 veces Kp y súbelo hasta que desaparezca la oscilación. Si Kd es demasiado grande, el robot tiembla y los motores zumban.
- Cuando el robot vaya totalmente recto en las rectas y fluido en las curvas, sube la velocidad base de 10 en 10 o de 20 en 20 y repite los pasos 2 y 3.
- Prueba Ki, con un valor muy pequeño (como 0,0001), solo si el robot se desvía siempre hacia un lado en rectas largas.
- Anota los valores que encuentres y la tensión de la batería en ese momento; al descargarse, los mismos valores se comportan de otra manera.
Seguidor de línea con ESP32: ajusta el PID desde el móvil
La gran ventaja del ESP32 es que puedes cambiar los valores del PID desde el móvil sin volver a cargar el código cada vez. La lógica es la misma que en la versión para Uno; las diferencias son: la entrada analógica del ESP32 es de 12 bits (0–4095), los sensores deben ir en pines ADC1, es decir, GPIO 32–39 (los pines ADC2 no se pueden leer con el Wi-Fi activo), y los sensores se alimentan a 3,3 V. analogWrite() funciona en las versiones 2.x y 3.x del core de Arduino para ESP32. Instala en tu móvil Android una app como Serial Bluetooth Terminal, conéctate al dispositivo "CizgiRobot" y envía órdenes como p0.07, d0.9 o s160; el robot usa el nuevo valor al instante y te lo devuelve. El Bluetooth clásico solo existe en el ESP32 original (no en el S3 ni en el C3) y los iPhone no admiten este tipo de conexión serie. Si el ESP32 es nuevo para ti, lee primero ¿Qué es el ESP32?.
1// ESP32 PID çizgi izleyen + Bluetooth ile canlı ayar
2// ESP32 PID line follower + live tuning over Bluetooth
3#include "BluetoothSerial.h"
4
5BluetoothSerial SerialBT;
6
7const int ENA = 25, IN1 = 26, IN2 = 27; // sol motor / left motor
8const int ENB = 33, IN3 = 18, IN4 = 19; // sağ motor / right motor
9const int BUTTON = 23; // diğer bacağı GND'ye / other leg to GND
10const int LED_PIN = 2; // DevKit üzerindeki LED / onboard LED on the DevKit
11const int N = 5;
12const int SENSOR[N] = {36, 39, 34, 35, 32}; // yalnız ADC1 pinleri / ADC1 pins only
13const bool WHITE_LINE = false; // siyah zemin + beyaz çizgi ise true / true for a white line on black
14const int CENTER = (N - 1) * 500;
15
16float Kp = 0.06, Ki = 0.0, Kd = 0.6;
17int baseSpeed = 140;
18const unsigned long PERIOD_MS = 5;
19
20int minVal[N], maxVal[N];
21int lastPosition = CENTER;
22float integral = 0, lastError = 0;
23unsigned long lastTime = 0;
24
25void drive(int left, int right) { // her biri -255 … 255 / each -255 … 255
26 left = constrain(left, -255, 255);
27 right = constrain(right, -255, 255);
28 digitalWrite(IN1, left >= 0); digitalWrite(IN2, left < 0);
29 digitalWrite(IN3, right >= 0); digitalWrite(IN4, right < 0);
30 analogWrite(ENA, abs(left));
31 analogWrite(ENB, abs(right));
32}
33
34void waitButton() {
35 while (digitalRead(BUTTON) == HIGH) {}
36 delay(30);
37 while (digitalRead(BUTTON) == LOW) {}
38 delay(30);
39}
40
41void calibrate() { // ESP32 ADC 12 bit: 0-4095 / ESP32 ADC is 12-bit: 0-4095
42 for (int i = 0; i < N; i++) { minVal[i] = 4095; maxVal[i] = 0; }
43 digitalWrite(LED_PIN, HIGH);
44 unsigned long start = millis();
45 while (millis() - start < 5000) {
46 for (int i = 0; i < N; i++) {
47 int v = analogRead(SENSOR[i]);
48 if (v < minVal[i]) minVal[i] = v;
49 if (v > maxVal[i]) maxVal[i] = v;
50 }
51 }
52 digitalWrite(LED_PIN, LOW);
53}
54
55int readPosition() {
56 long sum = 0, weighted = 0;
57 bool onLine = false;
58 for (int i = 0; i < N; i++) {
59 int range = max(maxVal[i] - minVal[i], 1);
60 long v = (long)(analogRead(SENSOR[i]) - minVal[i]) * 1000 / range;
61 v = constrain(v, 0L, 1000L);
62 if (WHITE_LINE) v = 1000 - v;
63 if (v > 500) onLine = true;
64 if (v > 50) {
65 weighted += v * (i * 1000L);
66 sum += v;
67 }
68 }
69 if (!onLine) {
70 return (lastPosition < CENTER) ? 0 : (N - 1) * 1000;
71 }
72 lastPosition = weighted / sum;
73 return lastPosition;
74}
75
76void readTuning() { // telefondan "p0.07", "d0.9", "s160" gibi komutlar / commands like "p0.07", "d0.9", "s160" from the phone
77 if (!SerialBT.available()) return;
78 char cmd = SerialBT.read();
79 if (cmd != 'p' && cmd != 'i' && cmd != 'd' && cmd != 's') return; // satır sonlarını atla / skip line endings
80 float value = SerialBT.parseFloat();
81 if (cmd == 'p') Kp = value;
82 if (cmd == 'i') { Ki = value; integral = 0; }
83 if (cmd == 'd') Kd = value;
84 if (cmd == 's') baseSpeed = (int)value;
85 SerialBT.printf("Kp=%.3f Ki=%.4f Kd=%.3f speed=%d\n", Kp, Ki, Kd, baseSpeed);
86}
87
88void setup() {
89 int outputs[] = {ENA, IN1, IN2, ENB, IN3, IN4, LED_PIN};
90 for (int p : outputs) pinMode(p, OUTPUT);
91 pinMode(BUTTON, INPUT_PULLUP);
92 drive(0, 0);
93 SerialBT.begin("CizgiRobot"); // telefonda görünecek ad / name shown on the phone
94 SerialBT.setTimeout(20); // parseFloat robotu bekletmesin / keep parseFloat from blocking
95 waitButton();
96 calibrate();
97 waitButton();
98 lastTime = millis();
99}
100
101void loop() {
102 readTuning();
103 if (millis() - lastTime < PERIOD_MS) return;
104 lastTime += PERIOD_MS;
105
106 float error = readPosition() - CENTER;
107 integral = constrain(integral + error, -20000.0f, 20000.0f);
108 float derivative = error - lastError;
109 lastError = error;
110
111 int correction = (int)(Kp * error + Ki * integral + Kd * derivative);
112 drive(baseSpeed + correction, baseSpeed - correction);
113}Seguidor de línea con Raspberry Pi Pico y MicroPython
La Pico solo tiene tres entradas analógicas disponibles (GP26, GP27, GP28), así que el código siguiente funciona con tres sensores analógicos y calcula la posición entre 0 y 2000. Si quieres usar cinco sensores, puedes conectar un módulo de 5 canales con salidas digitales a cinco GPIO cualesquiera y contar cada sensor como 0 o 1000; la media ponderada es más tosca, pero sigue funcionando. read_u16() devuelve valores de 0 a 65535 y la velocidad del motor se da en porcentaje (de -100 a 100). Para la instalación y los primeros pasos, consulta ¿Qué es la Raspberry Pi Pico?.
1from machine import Pin, PWM, ADC
2import time
3
4ena = PWM(Pin(15)); ena.freq(1000) # sol motor hızı / left motor speed
5in1 = Pin(14, Pin.OUT); in2 = Pin(13, Pin.OUT)
6enb = PWM(Pin(10)); enb.freq(1000) # sağ motor hızı / right motor speed
7in3 = Pin(11, Pin.OUT); in4 = Pin(12, Pin.OUT)
8button = Pin(16, Pin.IN, Pin.PULL_UP) # diğer bacağı GND'ye / other leg to GND
9led = Pin("LED", Pin.OUT)
10sensors = [ADC(26), ADC(27), ADC(28)] # sol, orta, sağ / left, middle, right
11WHITE_LINE = False # siyah zemin + beyaz çizgi ise True / True for a white line on black
12
13N = len(sensors)
14CENTER = (N - 1) * 500 # orta konum: 1000 / center position: 1000
15Kp, Ki, Kd = 0.05, 0.0, 0.4
16BASE = 45 # taban hız (%) / base speed (%)
17min_val = [65535] * N
18max_val = [0] * N
19last_pos = CENTER
20
21def drive(left, right): # her biri -100 … 100 / each -100 … 100
22 left = max(-100, min(100, left))
23 right = max(-100, min(100, right))
24 in1.value(left >= 0); in2.value(left < 0)
25 in3.value(right >= 0); in4.value(right < 0)
26 ena.duty_u16(int(abs(left) * 655.35))
27 enb.duty_u16(int(abs(right) * 655.35))
28
29def wait_button():
30 while button.value(): # basılmasını bekle / wait for a press
31 time.sleep_ms(10)
32 time.sleep_ms(30)
33 while not button.value(): # bırakılmasını bekle / wait for the release
34 time.sleep_ms(10)
35 time.sleep_ms(30)
36
37def calibrate(): # 5 sn sağa sola gezdir / sweep for 5 s
38 led.on()
39 start = time.ticks_ms()
40 while time.ticks_diff(time.ticks_ms(), start) < 5000:
41 for i, s in enumerate(sensors):
42 v = s.read_u16()
43 min_val[i] = min(min_val[i], v)
44 max_val[i] = max(max_val[i], v)
45 led.off()
46
47def read_position(): # 0 (sol) … 2000 (sağ) / 0 (left) … 2000 (right)
48 global last_pos
49 total = weighted = 0
50 on_line = False
51 for i, s in enumerate(sensors):
52 rng = max(1, max_val[i] - min_val[i])
53 v = max(0, min(1000, (s.read_u16() - min_val[i]) * 1000 // rng))
54 if WHITE_LINE:
55 v = 1000 - v
56 if v > 500:
57 on_line = True
58 if v > 50:
59 weighted += v * i * 1000
60 total += v
61 if not on_line: # çizgi kayıp / line lost
62 return 0 if last_pos < CENTER else (N - 1) * 1000
63 last_pos = weighted // total
64 return last_pos
65
66drive(0, 0)
67wait_button() # 1. basış: kalibrasyon / 1st press: calibration
68calibrate()
69wait_button() # 2. basış: yarış / 2nd press: race
70integral = 0
71last_error = 0
72while True:
73 t0 = time.ticks_ms()
74 error = read_position() - CENTER
75 integral = max(-10000, min(10000, integral + error))
76 correction = Kp * error + Ki * integral + Kd * (error - last_error)
77 last_error = error
78 drive(BASE + correction, BASE - correction)
79 while time.ticks_diff(time.ticks_ms(), t0) < 5: # 5 ms sabit döngü / fixed 5 ms loop
80 passReglas de competición y pista
Las reglas de las competiciones de seguidores de línea cambian según el organizador y el año, pero la idea básica es la misma: el robot debe completar la pista en el menor tiempo posible sin salirse de la línea. Por ejemplo, en el reglamento 2024 de nivel básico de seguidor de línea de la competición nacional de robótica MEB de Turquía, la pista es una línea blanca de 20 ± 2 mm sobre fondo negro. El robot puede medir como máximo 280 × 180 mm con las ruedas y pesar como máximo 1 kg con las baterías; la tensión de trabajo no puede superar 12 V y debe usar dos motores DC de 6–12 V con reductora en L y engranajes de plástico, ruedas de 65 mm de diámetro como máximo y una barra de 8 sensores como máximo. La pista tiene un puente con 13 grados de pendiente y un tramo cuadrado en el que atajar por el medio cuesta 20 segundos de penalización; si el cuerpo del robot queda totalmente sobre el fondo negro se considera fuera de pista, y si se queda parado sobre la línea más de 30 segundos queda descalificado. Otras competiciones escolares y regionales usan lo contrario, una línea negra de 20 ± 2 mm sobre fondo blanco, y piden robots más pequeños de 500 gramos como máximo. En resumen: lee siempre el reglamento de ese año antes de diseñar tu robot.
- Pista invertida: Para una línea blanca sobre fondo negro, pon a true la variable WHITE_LINE de los códigos. Si los colores cambian dentro de la pista, puedes añadir una comprobación que cambie de modo cuando los sensores exteriores empiecen a ver negro y el central blanco.
- Curvas de 90 grados: Baja la velocidad base antes de la curva o deja que la rueda interior gire hacia atrás; en los códigos de este artículo la rueda interior ya va hacia atrás cuando la corrección supera la velocidad base.
- Cruces: Cuando todos los sensores ven la línea a la vez, sigue recto; si necesitas elegir dirección, monta una máquina de estados que cuente los cruces.
- Líneas discontinuas: Cuando la línea desaparezca un momento, prueba a seguir recto 100–200 ms en lugar de girar enseguida.
- Rampa y puente: En la pendiente cambia la distancia entre el sensor y el suelo; mantén la barra de sensores baja y cerca de las ruedas, y reduce un poco la velocidad en la rampa.
- Pista de entrenamiento: Pega cinta aislante negra mate de 19 mm sobre cartulina blanca mate o cartón pluma; en los primeros intentos no hagas curvas con un radio menor de 15 cm.
Problemas frecuentes y soluciones
| Problema | Causa probable | Solución |
|---|---|---|
| El robot zigzaguea en las rectas | Kp demasiado alto, Kd demasiado bajo o demasiada velocidad | Baja Kp un 20 % aproximadamente, sube Kd; si hace falta, baja la velocidad base |
| Se sale de la pista en las curvas | Velocidad alta, barra de sensores estrecha o mala reacción al perder la línea | Baja la velocidad base, deja que la rueda interior vaya hacia atrás y gira hacia el último lado visto |
| El robot siempre tira hacia un lado | Los motores giran a distinta velocidad o la barra no está centrada | Centra los sensores y reduce el motor más rápido con un pequeño factor (por ejemplo, 0,95) |
| Los sensores ven negro o blanco en todas partes | Sin calibrar, sensor demasiado alto o trimmer mal ajustado | Baja la altura a 3–5 mm y repite la calibración sobre la pista |
| El Arduino se reinicia al arrancar los motores | La tensión de la batería cae de golpe o la masa común es deficiente | Alimenta la placa por separado o añade un condensador de 470 µF a la alimentación, acorta los cables |
| El robot cambia de comportamiento al bajar la batería | La velocidad del motor baja con la tensión | Mide la tensión de la batería y escala la velocidad, o pon una batería cargada antes de cada carrera |
| Funciona en casa, pero no en la competición | La luz ambiente o un suelo brillante alteran el infrarrojo | Tapa los sensores con una falda negra y calibra con la luz de la competición |
Siguientes pasos e ideas de proyectos
- Seguidor que esquiva obstáculos: Añade un HC-SR04 delante para que el robot rodee los obstáculos de la pista; la lógica de decisión está en Robot evita obstáculos.
- Modo Bluetooth: Pasa del seguimiento de línea al control con el móvil pulsando un botón; se añade fácilmente con el módulo Bluetooth HC-05.
- Cronómetro de vuelta: Un robot que detecta la línea de salida y muestra el tiempo por vuelta en una pantalla OLED.
- Resolvedor de laberintos: Un robot que cuenta y memoriza los cruces y en la segunda vuelta sigue el camino más corto.
- Robot de almacén: Un pequeño modelo de AGV que se detiene en estaciones de la línea y suelta una carga con un servo.
- Una base sólida: Si aún no has montado el chasis, los motores y la batería, empieza por la guía Cómo hacer un coche robot con Arduino.
Un robot seguidor de línea enseña sensores, algoritmos, motores y alimentación a la vez, y va un poco más rápido con cada ronda de ajustes. Primero haz que el robot avance con la versión if-else sencilla, después pasa al PID y ajusta los valores con paciencia. Si quieres saber cómo regula el PWM la velocidad del motor, ¿Qué es el PWM? es una buena continuación. En la app BlueGrays encontrarás paso a paso, con esquemas y código, los proyectos de detección de superficie blanca y negra con TCRT5000, el coche controlado por Bluetooth con L298N y el robot que esquiva obstáculos.
Preguntas frecuentes
¿Cuántos sensores necesita un robot seguidor de línea?
Con 2 sensores basta para un robot que funcione, pero con 3 y un código if-else sencillo el resultado es mucho más estable. Para un seguimiento rápido y suave se recomiendan 5 u 8 sensores analógicos con PID. Muchas competiciones permiten como máximo 8 sensores.
¿Por qué mi seguidor de línea zigzaguea?
La causa más habitual es un Kp demasiado alto y un Kd demasiado bajo: el robot corrige en exceso cada desviación y cruza al otro lado de la línea. Bajar un poco Kp, subir Kd, reducir la velocidad base y adelantar los sensores 4–8 cm respecto a las ruedas reduce la oscilación. Con un código if-else sencillo es normal un ligero zigzag, porque el robot solo cambia entre unas pocas velocidades fijas.
¿Se puede hacer un seguidor de línea sin PID?
Sí. Con un código if-else sencillo y 2 o 3 sensores el robot sigue la línea, y para proyectos escolares suele ser suficiente. Pero al subir la velocidad pierde la línea en las curvas y oscila en las rectas. Si buscas un buen puesto en una competición, tendrás que pasar al PID.
¿Qué batería usar en un robot seguidor de línea?
Lo más práctico son dos baterías Li-ion 18650 en serie: dan 7,4 V nominales y entregan sin problema la corriente alta que piden los motores. En robots ligeros también se usa una LiPo 2S. Una pila de 9 V no puede dar la corriente de los motores y se agota enseguida.
¿Cómo cambia el código para una línea blanca sobre fondo negro?
Basta con invertir los valores de los sensores: en los códigos de este artículo, al poner WHITE_LINE a true el valor normalizado pasa a ser 1000 − valor y el robot sigue la línea blanca. En los módulos digitales la misma variable invierte la lectura de DO. Aun así, debes calibrar sobre la pista en la que vas a competir.
¿A qué altura del suelo debe ir el sensor TCRT5000?
El TCRT5000 funciona mejor a unos 2,5 mm; en la práctica, dejar 3–5 mm entre el sensor y el suelo da buenos resultados. Por encima de 1 cm la diferencia entre negro y blanco se reduce mucho y el robot pierde la línea. En pistas con rampas, montar la barra de sensores cerca de las ruedas reduce el cambio de distancia.
Proyectos para hacer después de leer esto
Ver todos los proyectos
TCRT5000 Detección de superficie blanca y negra
Proyecto que detecta el color del suelo con el sensor TCRT5000 y enciende el LED rojo en suelo negro y el LED verde en suelo blanco. Enseña la lógica de detección del umbral de color que constituye la

Medición de distancia con TCRT5000
Proyecto que mide la cantidad de luz reflejada por la superficie con el sensor infrarrojo TCRT5000 y la muestra como un valor analógico en la pantalla LCD I2C. Enseña los fundamentos de la detección ó

Coche controlado por Bluetooth con L298N
Proyecto de coche robot 4WD (cuatro motores, tracción en las cuatro ruedas) que se puede controlar desde el teléfono con Arduino y el módulo Bluetooth HC-05. Un total de cuatro ruedas se accionan simu

Robot para evitar obstáculos
¿Qué le parece construir un vehículo inteligente que encuentre su propio camino? El Robot Evitador de Obstáculos explora constantemente su entorno gracias al sensor ultrasónico que lleva delante. Cuan

HC-SR04 Medición de distancia (pantalla en serie)
Sensor ultrasónico HC-SR04 para medir distancias y mostrar el resultado en cm en el monitor serie. Enseña el principio del cálculo de distancias con ondas sonoras.

Encender un LED con pulsador (Push)
Proyecto de control instantáneo (push) que enciende el LED al pulsar el botón y lo apaga al soltarlo. Enseña el uso de la lectura de botones y la expresión condicional con digitalRead.

ESP32 LED Fade con PWM
Cambiar el brillo del LED suavemente de 0 a max, de max a 0. Controlamos PWM con analogWrite() como en Arduino - la misma sintaxis con ESP32 Arduino Core 3.0+ funciona sin problemas. Pin definición + pinMode + analogWrite es suficiente.

Raspberry Pi Pico PWM LED Fade (Efecto Respiración, GP14)
¡El LED ya no solo parpadea — respira! 🌬️ Con el **PWM** (modulación por ancho de pulso) del Raspberry Pi Pico, el brillo sube y baja lentamente. Aprende la clase machine.PWM, duty_u16() (brillo de 16 bits 0–65535), la frecuencia PWM, bucles range() y salida tipo analógica. Base para mezcla de color RGB, control de motores, servos y buzzer.

