Robot seguilinea con Arduino: sensori TCRT5000 e controllo PID
Robot seguilinea con Arduino: barra di sensori TCRT5000, posizione e calibrazione, codice if-else e PID, regolazione di Kp/Kd, driver, batteria e regole.
Un robot seguilinea è un veicolo autonomo che rileva una linea nera (o bianca) sul pavimento con sensori a infrarossi e regola di continuo la velocità delle sue due ruote per restarci sopra. È uno dei primi veri progetti di robotica che si realizzano con Arduino, perché riunisce in un unico progetto la lettura dei sensori, le decisioni e il controllo dei motori. I veicoli a guida automatica (AGV) che spostano materiali nelle fabbriche funzionano con la stessa idea, e il seguilinea è una delle categorie più affollate nelle gare di robotica scolastiche.
In questa guida imparerai come funziona il sensore TCRT5000, che differenza c'è tra le barre a 2, 3 e 5 sensori, a quale altezza e con quale distanza montare i sensori e come calibrare la soglia. Prima farai muovere il robot con un semplice sketch if-else, poi passerai a una versione con controllo PID che segue la linea senza zigzagare e senza uscire di pista in curva. Oltre al codice per Arduino Uno, ESP32 e Raspberry Pi Pico, troverai come scegliere il driver dei motori e la batteria, le regole tipiche delle gare e le soluzioni ai problemi più comuni.

Come funziona un robot seguilinea?
Una superficie bianca riflette gran parte della luce infrarossa, mentre una superficie nera opaca ne assorbe quasi tutta. I sensori sotto il robot misurano questa differenza e capiscono dove si trova la linea rispetto al robot. Il robot non ha sterzo: gira con la cosiddetta trazione differenziale. Se la ruota sinistra gira più veloce della destra il robot curva a destra, se gira più piano curva a sinistra. Il programma ripete lo stesso ciclo centinaia di volte al secondo: legge i sensori, calcola di quanto si è spostata la linea e corregge la velocità dei due motori. Più questo anello chiuso è veloce e stabile, più il robot procede fluido.
| Componente | Consigliato | Nota |
|---|---|---|
| Scheda di controllo | Arduino Uno o Nano | Il Nano entra più facilmente in un telaio piccolo; vanno bene anche ESP32 o Pico |
| Sensore di linea | Barra TCRT5000 a 3 o 5 canali, QTR-8A | Con il PID una barra a uscite analogiche dà risultati migliori |
| Driver motori | L298N o TB6612FNG | Il TB6612FNG è più piccolo ed efficiente |
| Motori | 2 motoriduttori DC (TT 1:48 o N20) | I due motori devono essere dello stesso modello e con gli stessi giri |
| Ruote | 2 ruote motrici e 1 ruota pivottante | Le ruote da 65 mm di diametro sono le più diffuse |
| Alimentazione | 2 batterie Li-ion 18650 (7,4 V) e un interruttore | Una pila da 9 V è troppo debole per i motori |
| Altro | Telaio, pulsante, cavetti jumper | Un pulsante semplifica calibrazione e partenza |




Il sensore TCRT5000: barre a 2, 3 e 5 sensori
Il TCRT5000 è composto da un LED a infrarossi (950 nm) e da un fototransistor affiancati nello stesso contenitore. Più luce del LED viene riflessa dal pavimento e raggiunge il fototransistor, più il transistor conduce. Il sensore lavora al meglio a circa 2,5 mm di distanza; oltre 1 cm la differenza tra nero e bianco cala rapidamente. I moduli pronti includono un comparatore LM393: nella maggior parte dei moduli il pin DO dà HIGH sul nero e LOW sul bianco in base alla soglia regolata con il potenziometro, e il LED sulla scheda si accende sulla superficie bianca. Il pin AO fornisce il valore analogico grezzo: alto sul nero, basso sul bianco. Trovi tutti i dettagli sul sensore nell'articolo Sensore a infrarossi TCRT5000.
| Sensori | Posizione | Algoritmo | Quando sceglierlo |
|---|---|---|---|
| 2 | Ai due lati della linea; entrambi vedono normalmente il bianco | if-else | Primo tentativo, pista larga con curve dolci |
| 3 | Il centrale sulla linea, i laterali sui suoi bordi | if-else o PID semplice | Progetti scolastici, gare di livello base |
| 5 | Fila a passo uniforme, il centrale sulla linea | PID (media pesata) | Inseguimento veloce e fluido |
| 8 | Fila larga (come la QTR-8A) | PID e rilevamento degli incroci | Piste da gara con curve strette e incroci |
Posizione dei sensori: altezza, distanza e sporgenza
Per quanto buono sia il tuo codice, se i sensori sono nel posto sbagliato il robot non può andare bene. Monta la barra dei sensori davanti all'asse delle ruote, parallela al pavimento e simmetrica rispetto all'asse centrale del robot. Queste misure sono un buon punto di partenza:
- Altezza: Lascia 3–5 mm tra il lato inferiore del sensore e il pavimento. Pololu indica per la sua barra QTR-8A una distanza ottimale di 3 mm e una massima consigliata di 6 mm. Più in alto la differenza tra nero e bianco si riduce e il robot perde la linea.
- Passo: Nelle barre analogiche la distanza tra due sensori dovrebbe stare tra metà e due terzi della larghezza della linea; per una linea da 20 mm vanno bene 10–13 mm. Così la linea cade quasi sempre sotto due sensori e la posizione viene calcolata in modo fluido.
- Disposizione digitale a 3 sensori: Il sensore centrale sta proprio sopra la linea e quelli laterali appena fuori dai suoi bordi; per una linea da 20 mm circa 18–22 mm da centro a centro.
- Sporgenza: Metti i sensori 4–8 cm davanti all'asse delle ruote. Se sono troppo vicini il robot vede le curve in ritardo; se sono troppo lontani anche una piccola rotazione sposta molto i sensori e il robot oscilla.
- Luce ambiente: Anche la luce del sole e le lampade alogene contengono infrarossi. Schermare i sensori con una gonnellina o del nastro nero rende le letture molto più stabili.
Collegamenti: Arduino Uno, ESP32 e Pico
La tabella qui sotto corrisponde esattamente allo sketch di test dei sensori e agli sketch PID; i collegamenti dell'esempio if-else a 3 sensori sono spiegati nella sua sezione. Numera i sensori da sinistra a destra guardando il robot da dietro: il sensore più a sinistra è il numero 1. Come driver dei motori usiamo un L298N; la funzione dei pin e la logica del ponte H sono spiegate nel dettaglio in Controllo di un motore DC con L298N. Il Pico ha solo tre ingressi analogici, quindi lì usiamo i sensori 1, 3 e 5.
| Collegamento | Arduino Uno | ESP32 | Raspberry Pi Pico |
|---|---|---|---|
| Sensore 1 (più a sinistra) AO | A0 | GPIO 36 | GP26 |
| Sensore 2 AO | A1 | GPIO 39 | - |
| Sensore 3 (centrale) AO | A2 | GPIO 34 | GP27 |
| Sensore 4 AO | A3 | GPIO 35 | - |
| Sensore 5 (più a destra) AO | A4 | GPIO 32 | GP28 |
| VCC dei sensori | 5V | 3V3 | 3V3 |
| L298N ENA (velocità motore sinistro) | D9 | GPIO 25 | GP15 |
| L298N IN1, IN2 | D8, D7 | GPIO 26, 27 | GP14, GP13 |
| L298N ENB (velocità motore destro) | D10 | GPIO 33 | GP10 |
| L298N IN3, IN4 | D5, D4 | GPIO 18, 19 | GP11, GP12 |
| Pulsante di avvio (altro piedino a GND) | D2 | GPIO 23 | GP16 |
| GND | GND | GND | GND (in comune con batteria e driver) |
Scegliere driver motori e batteria
L'L298N è economico e robusto, ma i suoi transistor bipolari si mangiano 1,5–3 V: da un pacco da 7,4 V ai motori arrivano solo 5–5,5 V circa. Nei robot piccoli e veloci rende meglio un TB6612FNG o un DRV8833 a MOSFET. Con il TB6612FNG il codice resta uguale: colleghi i fili di ENA, IN1 e IN2 a PWMA, AIN1 e AIN2 (quelli del canale B a PWMB, BIN1 e BIN2) e il pin STBY a VCC. Il DRV8833 non ha un pin di velocità (enable) separato: il segnale PWM va direttamente sui pin IN, quindi devi adattare la funzione drive(). Per l'alimentazione la scelta più pratica sono due batterie Li-ion 18650 in serie: 8,4 V a piena carica, 7,4 V nominali, e forniscono correnti elevate senza fatica. Le pile da 9 V non reggono la corrente dei motori e si scaricano in fretta. Puoi calcolare quanto durerà la tua batteria con lo strumento Durata batteria.
| Driver | Tensione motore | Corrente per canale | Caduta di tensione | Giudizio |
|---|---|---|---|---|
| L298N | 5–35 V | 2 A | 1,5–3 V | Robusto ma ingombrante e poco efficiente |
| TB6612FNG | 4,5–13,5 V | 1,2 A (picco 3,2 A) | Molto bassa | Il miglior rapporto qualità-prezzo per un seguilinea |
| DRV8833 | 2,7–10,8 V | 1,5 A (picco 2 A) | Molto bassa | Robot piccoli con motori N20 |
| L9110S | 2,5–12 V | 0,8 A | Bassa | Solo robot leggeri e lenti |

Un esempio di velocità: i motori gialli TT (1:48) girano a circa 200 giri/min a vuoto a 6 V. Una ruota da 65 mm ha una circonferenza di π × 65 ≈ 204 mm, quindi il robot può andare al massimo a 200 × 0,204 ≈ 41 m al minuto, circa 0,7 m al secondo. È più che sufficiente per la maggior parte delle piste scolastiche; la vera sfida è non perdere la linea a quella velocità. I robot più veloci usano micromotoriduttori N20 e un telaio leggero. Quando la tensione della batteria cala, i motori rallentano e i valori PID che hai regolato non vanno più bene; misurare la tensione con un partitore di tensione e scalare la velocità di conseguenza è un miglioramento di livello avanzato.
Passo 1: prova i sensori e trova la soglia
Prima di collegare i motori, scopri cosa vedono i sensori. Lo sketch qui sotto stampa il valore analogico grezzo dei cinque sensori separato da tabulazioni; aprendo nell'IDE Arduino Strumenti > Plotter seriale a 115200 baud vedrai ogni sensore come una linea separata. Fai passare lentamente il robot sopra la linea a mano e guarda come cambiano i valori.
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 tipico modulo TCRT5000 legge 30–200 sulla superficie bianca e 600–1000 sulla linea nera, ma questi numeri cambiano con l'altezza del sensore, il materiale del pavimento e la luce ambiente. La soglia più semplice è: soglia = (valore sul bianco + valore sul nero) / 2. Per esempio, se leggi 80 sul bianco e 820 sul nero, la soglia è 450. Poiché ogni sensore legge in modo un po' diverso, lo sketch PID lo farà da solo: durante la calibrazione memorizza il valore minimo e massimo di ogni sensore, poi riporta ogni lettura tra 0 (bianco) e 1000 (nero) con (lettura − min) × 1000 / (max − min). Così la soglia diventa 500 per tutti i sensori.
Passo 2: semplice algoritmo if-else (3 sensori)
Per un primo robot funzionante, la via più chiara è scegliere tra poche mosse prefissate in base allo stato dei tre sensori. Collega le uscite DO di un modulo a 3 canali ad A0 (sinistra), A1 (centro) e A2 (destra); i pin analogici della Uno funzionano anche come ingressi digitali. La logica è quella di questa tabella (1 significa che il sensore vede la linea):
| Sinistra | Centro | Destra | Significato | Movimento |
|---|---|---|---|---|
| 0 | 1 | 0 | Linea al centro | Dritto |
| 1 | 1 | 0 | Linea un po' a sinistra | La ruota sinistra rallenta, curva dolce a sinistra |
| 1 | 0 | 0 | Linea molto a sinistra | La ruota sinistra va indietro, curva stretta a sinistra |
| 0 | 1 | 1 | Linea un po' a destra | La ruota destra rallenta, curva dolce a destra |
| 0 | 0 | 1 | Linea molto a destra | La ruota destra va indietro, curva stretta a destra |
| 1 | 1 | 1 | Incrocio o linea d'arrivo | Tirare dritto |
| 0 | 0 | 0 | Linea persa | Girare verso l'ultimo lato 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 variabile lastSide ricorda da quale lato il robot ha visto la linea l'ultima volta; se in una curva stretta tutti e tre i sensori perdono la linea, il robot gira da quella parte per ritrovarla invece di andare dritto a caso. Con due sensori la stessa logica è ancora più semplice: i sensori stanno ai due lati della linea, il robot va dritto finché entrambi vedono bianco, gira a sinistra quando il sinistro vede nero e a destra quando lo vede il destro. Il punto debole di questo metodo è che i movimenti sono a scatti: il robot passa solo tra poche velocità fisse, quindi zigzaga un po' anche sui rettilinei ed esce di pista in curva appena aumenti la velocità.
Passo 3: inseguimento fluido con il PID
Il PID applica una correzione proporzionale a quanto il robot si è allontanato dalla linea. Prima si calcola la posizione della linea con i cinque sensori tramite una media pesata: il valore di ogni sensore (0–1000) viene moltiplicato per la sua posizione (0, 1000, 2000, 3000, 4000), si somma tutto e si divide per la somma dei valori. Il risultato è un numero tra 0 e 4000, che vale 2000 quando la linea è esattamente al centro. Per esempio, se i sensori leggono 0, 0, 300, 900, 0, la posizione è (300 × 2000 + 900 × 3000) / 1200 = 2750. L'errore = posizione − 2000: un valore positivo indica che la linea è a destra, uno negativo che è a sinistra.
La correzione è la somma di tre termini: correzione = Kp × errore + Ki × somma degli errori + Kd × (errore − errore precedente). Il termine P sterza con più decisione quanto più grande è lo scostamento. Il termine D guarda quanto velocemente cambia l'errore e frena quando il robot si riavvicina alla linea, smorzando le oscillazioni. Il termine I corregge piccoli errori che restano a lungo da una parte, ma in un seguilinea di solito si lascia a zero. Poi il motore sinistro gira a velocità base + correzione e il destro a velocità base − correzione. Per esempio, con un errore di 800 e Kp = 0,06 la correzione è 48; con velocità base 140 il motore sinistro va a 188, il destro a 92 e il robot curva dolcemente a destra.
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}Dopo il caricamento premi una volta il pulsante: il LED sulla scheda si accende per 5 secondi. In quel tempo fai scorrere a mano il robot qualche volta a destra e a sinistra sopra la linea, così ogni sensore vede sia il nero sia il bianco. Quando il LED si spegne, appoggia il robot sulla linea e premi di nuovo il pulsante. Il ciclo gira ogni 5 ms grazie a millis(); un periodo fisso mantiene il termine D sempre sulla stessa scala. Se la linea sparisce del tutto, readPosition() restituisce 0 o 4000 in base all'ultimo lato in cui è stata vista e il robot la cerca con la curva più stretta. Per collegare il pulsante guarda Come collegare un pulsante ad Arduino, per temporizzare senza delay Arduino millis() e delay().
Come regolare Kp e Kd
- Tieni bassa la velocità base (per esempio 100) e metti Ki e Kd a zero.
- Parti con un Kp piccolo (0,02) e aumentalo un po' alla volta finché il robot non riesce a prendere le curve. Quando il robot inizia a oscillare in modo rapido e regolare attorno alla linea hai raggiunto il limite superiore di Kp; resta un po' sotto quel valore.
- Parti con Kd pari a circa 10 volte Kp e aumentalo finché l'oscillazione non scompare. Se Kd è troppo grande il robot vibra e i motori fischiano.
- Quando il robot va perfettamente dritto sui rettilinei e fluido in curva, aumenta la velocità base di 10–20 e ripeti i passi 2 e 3.
- Prova Ki, con un valore molto piccolo (come 0,0001), solo se il robot scivola sempre da una parte sui rettilinei lunghi.
- Annota i valori trovati e la tensione della batteria in quel momento; man mano che la batteria si scarica, gli stessi valori si comportano diversamente.
Seguilinea con ESP32: regola il PID dal telefono
Il grande vantaggio dell'ESP32 è che puoi cambiare i valori del PID dal telefono senza ricaricare il codice ogni volta. La logica è la stessa della versione per Uno; le differenze sono: l'ingresso analogico dell'ESP32 è a 12 bit (0–4095), i sensori vanno collegati ai pin ADC1, cioè GPIO 32–39 (i pin ADC2 non si possono leggere con il Wi-Fi attivo), e i sensori si alimentano a 3,3 V. analogWrite() funziona sia nella versione 2.x sia nella 3.x del core Arduino per ESP32. Installa sul telefono Android un'app come Serial Bluetooth Terminal, collegati al dispositivo "CizgiRobot" e invia comandi come p0.07, d0.9 o s160; il robot usa subito il nuovo valore e te lo rimanda. Il Bluetooth classico esiste solo sull'ESP32 originale (non su S3 e C3) e gli iPhone non supportano questo tipo di collegamento seriale. Se l'ESP32 è nuovo per te, leggi prima Cos'è l'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}Seguilinea con Raspberry Pi Pico e MicroPython
Il Pico ha solo tre ingressi analogici disponibili (GP26, GP27, GP28), quindi il codice qui sotto lavora con tre sensori analogici e calcola la posizione tra 0 e 2000. Se vuoi usare cinque sensori puoi collegare un modulo a 5 canali con uscite digitali a cinque GPIO qualsiasi e contare ogni sensore come 0 o 1000; la media pesata è più grossolana ma funziona lo stesso. read_u16() restituisce valori da 0 a 65535 e la velocità del motore è espressa in percentuale (da -100 a 100). Per installazione e primi passi guarda Cos'è il 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 passRegole di gara e pista
Le regole delle gare per seguilinea cambiano a seconda dell'organizzatore e dell'anno, ma l'idea di base è sempre la stessa: il robot deve completare la pista nel minor tempo possibile senza uscire dalla linea. Per esempio, nel regolamento 2024 di livello base per seguilinea della gara nazionale di robotica MEB in Turchia, la pista è una linea bianca da 20 ± 2 mm su fondo nero. Il robot può misurare al massimo 280 × 180 mm ruote comprese e pesare al massimo 1 kg batteria compresa; la tensione di lavoro non può superare 12 V e sono obbligatori due motoriduttori DC da 6–12 V a forma di L con ingranaggi in plastica, ruote con diametro massimo di 65 mm e una barra con al massimo 8 sensori. In pista ci sono un ponte con pendenza di 13 gradi e un tratto quadrato dove tagliare per il centro costa 20 secondi di penalità; un robot con il corpo completamente sul fondo nero è considerato fuori pista, e uno fermo sulla linea per più di 30 secondi viene squalificato. Altre gare scolastiche e regionali usano il contrario, una linea nera da 20 ± 2 mm su fondo bianco, e chiedono robot più piccoli che non superino i 500 grammi. Insomma, leggi sempre il regolamento dell'anno prima di progettare il robot.
- Pista invertita: Per una linea bianca su fondo nero imposta a true la variabile WHITE_LINE negli sketch. Se i colori cambiano lungo la pista puoi aggiungere un controllo che cambia modalità quando i sensori esterni iniziano a vedere nero e quello centrale bianco.
- Curve a 90 gradi: Riduci la velocità base prima della curva o lascia che la ruota interna giri all'indietro; negli sketch di questo articolo la ruota interna va già indietro quando la correzione supera la velocità base.
- Incroci: Quando tutti i sensori vedono la linea insieme, tira dritto; se devi scegliere una direzione, costruisci una macchina a stati che conti gli incroci.
- Linee tratteggiate: Quando la linea scompare per poco, prova a proseguire dritto per 100–200 ms invece di girare subito.
- Rampa e ponte: In salita cambia la distanza tra sensore e pavimento; tieni la barra dei sensori bassa e vicina alle ruote, e rallenta un po' sulla rampa.
- Pista di allenamento: Incolla nastro isolante nero opaco da 19 mm su cartoncino bianco opaco o su un pannello in forex; nelle prime prove non fare curve con raggio inferiore a 15 cm.
Problemi comuni e soluzioni
| Problema | Causa probabile | Soluzione |
|---|---|---|
| Il robot zigzaga sui rettilinei | Kp troppo alto, Kd troppo basso o velocità eccessiva | Riduci Kp del 20 % circa, aumenta Kd; se serve abbassa la velocità base |
| Esce di pista in curva | Velocità alta, barra di sensori stretta o reazione sbagliata quando perde la linea | Abbassa la velocità base, lascia andare indietro la ruota interna, gira verso l'ultimo lato visto |
| Il robot tira sempre da una parte | I motori girano a velocità diverse o la barra non è centrata | Centra i sensori, rallenta il motore più veloce con un piccolo fattore (per esempio 0,95) |
| I sensori vedono nero o bianco ovunque | Calibrazione mancante, sensore troppo alto o trimmer non regolato | Abbassa l'altezza a 3–5 mm, ripeti la calibrazione sulla pista |
| Arduino si resetta alla partenza dei motori | La tensione della batteria crolla per un attimo o la massa comune è scarsa | Alimenta la scheda a parte o aggiungi un condensatore da 470 µF sull'alimentazione, accorcia i cavi |
| Il robot cambia comportamento quando la batteria cala | La velocità dei motori scende con la tensione | Misura la tensione della batteria e scala la velocità, oppure monta una batteria carica prima di ogni prova |
| A casa funziona, in gara no | La luce ambiente o un pavimento lucido disturbano l'infrarosso | Scherma i sensori con una gonnellina nera, calibra con la luce della gara |
Prossimi passi e idee per progetti
- Seguilinea che evita gli ostacoli: Aggiungi un HC-SR04 davanti perché il robot aggiri gli ostacoli sulla pista; la logica decisionale è in Robot che evita gli ostacoli.
- Modalità Bluetooth: Passa dall'inseguimento della linea al comando da telefono con un pulsante; si aggiunge facilmente con il modulo Bluetooth HC-05.
- Cronometro sul giro: Un robot che rileva la linea di partenza e mostra il tempo sul giro su un display OLED.
- Risolutore di labirinti: Un robot che conta e memorizza gli incroci e al secondo giro segue il percorso più breve.
- Robot da magazzino: Un piccolo modello di AGV che si ferma alle stazioni lungo la linea e scarica un carico con un servo.
- Una base solida: Se non hai ancora montato telaio, motori e batteria, inizia dalla guida Costruire un'auto robot con Arduino.
Un robot seguilinea insegna sensori, algoritmi, motori e alimentazione tutti insieme, e diventa un po' più veloce a ogni giro di regolazione. Prima fai muovere il robot con la versione if-else semplice, poi passa al PID e regola i valori con pazienza. Se vuoi capire come il PWM regola la velocità del motore, Cos'è il PWM? è un buon seguito. Nell'app BlueGrays trovi passo passo, con schemi e codice, i progetti di rilevamento del fondo bianco e nero con TCRT5000, l'auto comandata via Bluetooth con L298N e il robot che evita gli ostacoli.
Domande frequenti
Quanti sensori servono a un robot seguilinea?
Per un robot funzionante bastano 2 sensori, ma con 3 e un semplice codice if-else il risultato è molto più stabile. Per un inseguimento veloce e fluido si consigliano 5 o 8 sensori analogici con il PID. Molte gare ammettono al massimo 8 sensori.
Perché il mio robot seguilinea zigzaga?
La causa più comune è un Kp troppo alto e un Kd troppo basso: il robot corregge troppo ogni scostamento e finisce dall'altra parte della linea. Abbassare un po' Kp, aumentare Kd, ridurre la velocità base e portare i sensori 4–8 cm davanti alle ruote riduce l'oscillazione. Con un semplice codice if-else un leggero zigzag è normale, perché il robot passa solo tra poche velocità fisse.
Si può fare un seguilinea senza PID?
Sì. Con un semplice codice if-else e 2 o 3 sensori il robot segue la linea, e per i progetti scolastici spesso basta. Ma quando la velocità aumenta perde la linea in curva e oscilla sui rettilinei. Se punti a un buon piazzamento in gara, dovrai passare al PID.
Che batteria usare per un robot seguilinea?
La scelta più pratica sono due batterie Li-ion 18650 in serie: danno 7,4 V nominali e forniscono senza fatica la corrente elevata dei motori. Nei robot leggeri si usa anche una LiPo 2S. Una pila da 9 V non regge la corrente dei motori e si scarica in fretta.
Come cambia il codice per una linea bianca su fondo nero?
Basta invertire i valori dei sensori: negli sketch di questo articolo, impostando WHITE_LINE a true il valore normalizzato diventa 1000 − valore e il robot segue la linea bianca. Nei moduli digitali la stessa variabile inverte la lettura di DO. La calibrazione va comunque fatta sulla pista di gara.
A che altezza dal pavimento va il sensore TCRT5000?
Il TCRT5000 lavora al meglio a circa 2,5 mm; in pratica lasciare 3–5 mm tra sensore e pavimento dà buoni risultati. Oltre 1 cm la differenza tra nero e bianco diventa molto piccola e il robot perde la linea. Sulle piste con rampe, montare la barra dei sensori vicino alle ruote riduce la variazione di distanza.
Progetti da realizzare dopo questo articolo
Vedi tutti i progetti
TCRT5000 Rilevamento di superfici bianche e nere
Progetto che rileva il colore del terreno con il sensore TCRT5000 e accende il LED rosso sul terreno nero e il LED verde sul terreno bianco. Insegna la logica di rilevamento della soglia di colore che

Misurazione della distanza con TCRT5000
Progetto che misura la quantità di luce riflessa dalla superficie con il sensore a infrarossi TCRT5000 e la visualizza come valore analogico sullo schermo LCD I2C. Insegna le basi del rilevamento otti

Auto controllata via Bluetooth con L298N
Progetto di auto robot 4WD (quattro motori, quattro ruote motrici) che può essere controllata dal telefono con Arduino e il modulo HC-05 Bluetooth. Un totale di quattro ruote sono azionate simultaneam

Robot che evita gli ostacoli
Che ne dite di costruire un veicolo intelligente che trova la propria strada? L'Obstacle Avoiding Robot scruta costantemente l'ambiente circostante grazie al sensore a ultrasuoni posto davanti a lui.

HC-SR04 Misura della distanza (display seriale)
Il sensore a ultrasuoni HC-SR04 misura la distanza e visualizza il risultato in cm sul monitor seriale. Insegna il principio del calcolo della distanza con l'onda sonora.

Accendere un LED con un pulsante (Push)
Progetto di controllo istantaneo (push) che accende il LED quando il pulsante viene premuto e lo spegne quando viene rilasciato. Insegna l'uso della lettura dei pulsanti e dell'espressione condizional

Dissolvenza LED ESP32 con PWM
Modificare la luminosità del LED in modo fluido da 0 a max, da max a 0. Controlliamo il PWM con analogWrite() come in Arduino - la stessa sintassi con ESP32 Arduino Core 3.0+ funziona senza problemi. La definizione del pin + pinMode + analogWrite è sufficiente.

Raspberry Pi Pico PWM LED Fade (Effetto Respiro, GP14)
Il LED non lampeggia più — respira! 🌬️ Con il **PWM** (modulazione di larghezza d'impulso) del Raspberry Pi Pico, la luminosità sale e scende lentamente. Impara la classe machine.PWM, duty_u16() (luminosità a 16 bit 0–65535), la frequenza PWM, i cicli range() e l'uscita quasi analogica. Base per miscelazione colori RGB, controllo motori, servo e buzzer.

