Robô seguidor de linha com Arduino: sensores TCRT5000 e PID
Monte um robô seguidor de linha com Arduino: sensores TCRT5000, posicionamento e calibração, código if-else e PID, ajuste de Kp/Kd, driver de motor, bateria e regras.
Um robô seguidor de linha é um veículo autônomo que detecta uma linha preta (ou branca) no chão com sensores infravermelhos e fica ajustando a velocidade das suas duas rodas para continuar sobre ela. É um dos primeiros projetos de robô de verdade que as pessoas montam com Arduino, porque junta leitura de sensores, tomada de decisão e controle de motores em um único projeto. Os carrinhos autônomos (AGVs) que movimentam materiais nas fábricas funcionam com a mesma ideia, e seguir linha é uma das categorias mais disputadas nas competições escolares de robótica.
Neste guia você vai aprender como o sensor TCRT5000 funciona, como diferem as barras de 2, 3 e 5 sensores, a que altura e espaçamento montar os sensores e como calibrar o limite. Primeiro você coloca o robô para andar com um sketch if-else simples e depois passa para uma versão com controle PID, que segue a linha sem zigue-zague e sem sair da pista nas curvas. Junto com código que funciona para Arduino Uno, ESP32 e Raspberry Pi Pico, você também encontra como escolher o driver de motor e a bateria, regras típicas de competição e soluções para os problemas mais comuns.

Como funciona um robô seguidor de linha?
Uma superfície branca reflete de volta a maior parte da luz infravermelha, enquanto uma superfície preta fosca absorve a maior parte dela. Os sensores embaixo do robô medem essa diferença para descobrir onde a linha está em relação ao robô. O robô não tem direção; ele faz curvas com o que se chama de tração diferencial: se a roda esquerda gira mais rápido que a direita, o robô vira para a direita, e se gira mais devagar, vira para a esquerda. O programa executa o mesmo laço centenas de vezes por segundo: lê os sensores, calcula quanto a linha se desviou e corrige a velocidade dos dois motores de acordo. Quanto mais rápido e estável esse laço fechado roda, mais suave o robô se move.
| Peça | Recomendado | Observação |
|---|---|---|
| Placa controladora | Arduino Uno ou Nano | O Nano cabe mais fácil em um chassi pequeno; um ESP32 ou um Pico também servem |
| Sensor de linha | Barra TCRT5000 de 3 ou 5 canais, QTR-8A | Uma barra com saídas analógicas dá melhores resultados com PID |
| Driver de motor | L298N ou TB6612FNG | O TB6612FNG é menor e mais eficiente |
| Motores | 2 motores CC com redução (TT 1:48 ou N20) | Os dois motores devem ser do mesmo modelo e velocidade |
| Rodas | 2 rodas de tração e 1 roda boba | Rodas de 65 mm são as mais comuns |
| Alimentação | 2 × células Li-ion 18650 (7,4 V) e uma chave | Uma bateria de 9 V (PP3) é fraca demais para motores |
| Outros | Chassi, botão, jumpers | Um botão facilita a calibração e a largada |




O sensor TCRT5000: barras de 2, 3 e 5 sensores
O TCRT5000 é formado por um LED infravermelho (950 nm) e um fototransistor lado a lado no mesmo invólucro. Quanto mais luz do LED bate no chão e chega ao fototransistor, mais o transistor conduz. O sensor funciona melhor a uma distância de cerca de 2,5 mm; acima de 1 cm, a diferença entre preto e branco cai rápido. Os módulos prontos trazem um comparador LM393: na maioria dos módulos, o pino DO fornece HIGH sobre o preto e LOW sobre o branco, conforme o limite ajustado no potenciômetro, e o LED da placa acende sobre uma superfície branca. O pino AO dá o valor analógico bruto: alto sobre o preto, baixo sobre o branco. Você encontra todos os detalhes do sensor em Sensor infravermelho TCRT5000.
| Sensores | Posicionamento | Algoritmo | Quando escolher |
|---|---|---|---|
| 2 | Um de cada lado da linha; os dois normalmente veem branco | if-else | Primeira tentativa, pista larga com curvas suaves |
| 3 | O do meio sobre a linha, os de fora nas bordas dela | if-else ou PID simples | Projetos escolares, competições de nível iniciante |
| 5 | Fileira com espaçamento igual, o do meio sobre a linha | PID (média ponderada) | Seguir linha rápido e suave |
| 8 | Fileira larga (como a QTR-8A) | PID mais detecção de cruzamentos | Pistas de competição com curvas fechadas e cruzamentos |
Posicionamento dos sensores: altura, espaçamento e distância à frente
Por melhor que seja o seu código, o robô não anda bem se os sensores estiverem no lugar errado. Monte a barra de sensores à frente do eixo das rodas, paralela ao chão e simétrica em relação à linha central do robô. Estas medidas são um bom ponto de partida:
- Altura: deixe 3–5 mm entre a parte de baixo do sensor e o chão. A Pololu indica 3 mm como a distância ideal e 6 mm como a máxima recomendada para a sua barra QTR-8A. Mais alto que isso, a diferença entre preto e branco diminui e o robô perde a linha.
- Espaçamento: em barras analógicas, mantenha a distância entre dois sensores entre metade e dois terços da largura da linha; 10–13 mm funciona bem para uma linha de 20 mm. Assim a linha normalmente fica sob dois sensores e a posição é calculada de forma suave.
- Disposição digital com 3 sensores: coloque o sensor do meio bem sobre a linha e os laterais logo fora das bordas dela; para uma linha de 20 mm, isso dá cerca de 18–22 mm de centro a centro.
- Distância à frente: coloque os sensores 4–8 cm à frente do eixo das rodas. Perto demais e o robô percebe as curvas tarde; longe demais e até uma pequena virada desloca muito os sensores, fazendo o robô balançar.
- Luz ambiente: a luz do sol e as lâmpadas halógenas também têm luz infravermelha. Proteger os sensores com uma saia preta ou fita deixa as leituras bem mais estáveis.
Ligação: Arduino Uno, ESP32 e Pico
A tabela abaixo corresponde exatamente aos sketches de teste dos sensores e de PID; a ligação do exemplo if-else com 3 sensores está descrita na seção dele. Numere os sensores da esquerda para a direita, olhando o robô por trás: o sensor mais à esquerda é o número 1. Usamos um L298N como driver de motor; o que cada pino faz e como a ponte H funciona estão explicados em detalhe em Controle de motor CC com L298N. O Pico só tem três entradas analógicas, então nele usamos os sensores 1, 3 e 5.
| Ligação | Arduino Uno | ESP32 | Raspberry Pi Pico |
|---|---|---|---|
| AO do sensor 1 (mais à esquerda) | A0 | GPIO 36 | GP26 |
| AO do sensor 2 | A1 | GPIO 39 | - |
| AO do sensor 3 (meio) | A2 | GPIO 34 | GP27 |
| AO do sensor 4 | A3 | GPIO 35 | - |
| AO do sensor 5 (mais à direita) | A4 | GPIO 32 | GP28 |
| VCC dos sensores | 5V | 3V3 | 3V3 |
| ENA do L298N (velocidade do motor esquerdo) | D9 | GPIO 25 | GP15 |
| IN1, IN2 do L298N | D8, D7 | GPIO 26, 27 | GP14, GP13 |
| ENB do L298N (velocidade do motor direito) | D10 | GPIO 33 | GP10 |
| IN3, IN4 do L298N | D5, D4 | GPIO 18, 19 | GP11, GP12 |
| Botão de largada (outra perna no GND) | D2 | GPIO 23 | GP16 |
| GND | GND | GND | GND (comum com a bateria e o driver) |
Como escolher o driver de motor e a bateria
O L298N é barato e resistente, mas os seus transistores bipolares engolem 1,5–3 V: de um pacote de baterias de 7,4 V, só cerca de 5–5,5 V chegam aos motores. Em robôs pequenos e rápidos, um TB6612FNG ou um DRV8833, baseados em MOSFET, funcionam melhor. Com o TB6612FNG o código continua o mesmo: você liga as conexões ENA, IN1 e IN2 a PWMA, AIN1 e AIN2 (PWMB, BIN1 e BIN2 no canal B) e liga o pino STBY ao VCC. O DRV8833 não tem pino separado de velocidade (enable); o sinal PWM vai direto aos pinos IN, então você vai precisar adaptar a função drive(). Para a alimentação, a escolha mais prática são duas células Li-ion 18650 em série: 8,4 V cheias, 7,4 V nominais, e elas fornecem corrente alta com facilidade. Baterias de 9 V (PP3) não aguentam a corrente dos motores e descarregam rápido. Você pode calcular quanto tempo a sua bateria vai durar com a ferramenta Duração da bateria.
| Driver | Tensão dos motores | Corrente por canal | Queda de tensão | Veredito |
|---|---|---|---|---|
| L298N | 5–35 V | 2 A | 1,5–3 V | Resistente, mas grande e ineficiente |
| TB6612FNG | 4,5–13,5 V | 1,2 A (pico de 3,2 A) | Muito baixa | Melhor custo-benefício para um seguidor de linha |
| DRV8833 | 2,7–10,8 V | 1,5 A (pico de 2 A) | Muito baixa | Robôs pequenos com motores N20 |
| L9110S | 2,5–12 V | 0,8 A | Baixa | Só robôs leves e lentos |

Um exemplo rápido de velocidade: motores TT amarelos (1:48) giram a cerca de 200 rpm sem carga em 6 V. Uma roda de 65 mm tem circunferência de π × 65 ≈ 204 mm, então o robô percorre no máximo 200 × 0,204 ≈ 41 m por minuto, ou cerca de 0,7 m por segundo. Isso é mais que suficiente para a maioria das pistas escolares; o verdadeiro desafio é não perder a linha nessa velocidade. Robôs mais rápidos usam micromotores N20 com redução e um chassi leve. Conforme a tensão da bateria cai, os motores ficam mais lentos e os seus valores de PID ajustados mudam de comportamento; medir a tensão da bateria com um divisor de tensão e escalar a velocidade de acordo é uma melhoria avançada.
Passo 1: teste os sensores e encontre o limite
Antes de ligar os motores, descubra o que os sensores veem. O sketch abaixo mostra o valor analógico bruto dos cinco sensores separados por tabulação; abra Ferramentas > Plotter Serial na Arduino IDE em 115200 baud e você vai ver cada sensor como uma linha própria. Passe o robô devagar sobre a linha com a mão e observe como os valores mudam.
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}Em 5 V, um módulo TCRT5000 típico lê 30–200 em uma superfície branca e 600–1000 em uma linha preta, mas esses números mudam com a altura do sensor, o material do chão e a luz ambiente. O limite mais simples é limite = (valor do branco + valor do preto) / 2. Por exemplo, se você lê 80 no branco e 820 no preto, o limite é 450. Como cada sensor lê um pouco diferente, o sketch de PID faz isso automaticamente: durante a calibração, ele registra o menor e o maior valor de cada sensor e depois escala cada leitura para entre 0 (branco) e 1000 (preto) com (leitura − mín) × 1000 / (máx − mín). Isso deixa o limite em 500 para todos os sensores.
Passo 2: um algoritmo if-else simples (3 sensores)
Para um primeiro robô funcionando, a abordagem mais clara é escolher entre alguns movimentos prontos com base no estado de três sensores. Ligue as saídas DO de um módulo de 3 canais ao A0 (esquerda), ao A1 (meio) e ao A2 (direita); os pinos analógicos do Uno também funcionam como entradas digitais. A lógica está nesta tabela (1 significa que o sensor vê a linha):
| Esquerda | Meio | Direita | Significado | Movimento |
|---|---|---|---|---|
| 0 | 1 | 0 | Linha no meio | Seguir reto |
| 1 | 1 | 0 | Linha um pouco à esquerda | Roda esquerda desacelera, curva suave à esquerda |
| 1 | 0 | 0 | Linha bem à esquerda | Roda esquerda dá ré, curva fechada à esquerda |
| 0 | 1 | 1 | Linha um pouco à direita | Roda direita desacelera, curva suave à direita |
| 0 | 0 | 1 | Linha bem à direita | Roda direita dá ré, curva fechada à direita |
| 1 | 1 | 1 | Cruzamento ou linha de chegada | Atravessar reto |
| 0 | 0 | 0 | Linha perdida | Virar para o ú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}A variável lastSide lembra de que lado o robô viu a linha por último; se os três sensores perderem a linha em uma curva fechada, o robô vira para esse lado para encontrá-la de novo, em vez de seguir reto sem rumo. Com dois sensores a mesma lógica é ainda mais simples: os sensores ficam um de cada lado da linha, o robô segue reto enquanto os dois veem branco, vira à esquerda quando o esquerdo vê preto e vira à direita quando é o direito. A fraqueza desse método é que os movimentos vêm em degraus: o robô só alterna entre algumas velocidades fixas, então faz um leve zigue-zague até nas retas e sai da pista nas curvas quando você aumenta a velocidade.
Passo 3: seguir linha de forma suave com PID
O PID aplica uma correção proporcional ao quanto o robô se desviou da linha. Primeiro, a posição da linha é calculada a partir dos cinco sensores como uma média ponderada: o valor de 0–1000 de cada sensor é multiplicado pela sua posição (0, 1000, 2000, 3000, 4000), os resultados são somados e divididos pela soma dos valores. O resultado é um número entre 0 e 4000, que vale 2000 quando a linha está exatamente no meio. Por exemplo, se os sensores leem 0, 0, 300, 900, 0, a posição é (300 × 2000 + 900 × 3000) / 1200 = 2750. O erro = posição − 2000: um valor positivo significa que a linha está à direita, um negativo, que está à esquerda.
A correção é a soma de três termos: correção = Kp × erro + Ki × soma dos erros + Kd × (erro − erro anterior). O termo P vira com mais força quanto maior o desvio. O termo D olha a rapidez com que o erro muda e freia conforme o robô se aproxima da linha, amortecendo a oscilação. O termo I corrige erros pequenos que ficam muito tempo de um lado só, mas em um seguidor de linha normalmente fica em zero. Depois, o motor esquerdo roda a velocidade base + correção e o direito a velocidade base − correção. Por exemplo, com um erro de 800 e Kp = 0.06, a correção é 48; com velocidade base de 140, o motor esquerdo roda a 188 e o direito a 92, e o robô faz uma curva suave para a direita.
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}Depois de carregar, aperte o botão uma vez: o LED da placa acende por 5 segundos. Nesse tempo, deslize o robô com a mão algumas vezes para a esquerda e para a direita sobre a linha, para que cada sensor veja tanto o preto quanto o branco. Quando o LED apagar, coloque o robô sobre a linha e aperte o botão de novo. O laço roda a cada 5 ms usando millis(); um período fixo mantém o termo D sempre na mesma escala. Se a linha sumir completamente, readPosition() retorna 0 ou 4000, conforme o último lado em que foi vista, e o robô a procura com a curva mais fechada. Para a ligação do botão, veja Como ligar um botão ao Arduino, e para temporizar sem delay, veja millis() e delay() no Arduino.
Como ajustar Kp e Kd
- Mantenha a velocidade base baixa (100, por exemplo) e coloque Ki e Kd em zero.
- Comece com um Kp pequeno (0.02) e aumente aos poucos até o robô conseguir fazer as curvas. Quando o robô começar a oscilar rápido e de forma regular em torno da linha, você chegou ao limite superior do Kp; fique um pouco abaixo desse valor.
- Comece o Kd com cerca de 10 vezes o Kp e aumente até a oscilação sumir. Se o Kd ficar grande demais, o robô treme e os motores chiam.
- Quando o robô andar perfeitamente reto nas retas e suave nas curvas, aumente a velocidade base em 10–20 e repita os passos 2 e 3.
- Só experimente o Ki, com um valor bem pequeno (como 0.0001), se o robô continuar puxando para um lado em retas longas.
- Anote os valores que encontrar e a tensão da bateria naquele momento; conforme a bateria descarrega, os mesmos valores se comportam de outro jeito.
Seguidor de linha com ESP32: ajuste o PID pelo celular
A grande vantagem do ESP32 é que você pode mudar os valores do PID pelo celular, sem carregar o código de novo toda vez. A lógica é a mesma da versão do Uno; as diferenças são: a entrada analógica do ESP32 é de 12 bits (0–4095), os sensores precisam ir em pinos do ADC1, GPIO 32–39 (os pinos do ADC2 não podem ser lidos com o Wi-Fi ligado), e os sensores são alimentados com 3,3 V. analogWrite() funciona nas versões 2.x e 3.x do core Arduino do ESP32. Instale um app como o Serial Bluetooth Terminal no seu celular Android, conecte ao dispositivo “CizgiRobot” e envie comandos como p0.07, d0.9 ou s160; o robô usa o novo valor na hora e o devolve como confirmação. O Bluetooth clássico só existe no ESP32 original (não no S3 nem no C3), e iPhones não suportam esse tipo de conexão serial. Se o ESP32 é novidade para você, leia antes O que é o 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 linha com Raspberry Pi Pico e MicroPython
O Pico só tem três entradas analógicas disponíveis para o usuário (GP26, GP27, GP28), então o código abaixo trabalha com três sensores analógicos e calcula a posição entre 0 e 2000. Se quiser cinco sensores, você pode ligar um módulo de 5 canais com saídas digitais a quaisquer cinco GPIOs e contar cada sensor como 0 ou 1000; a média ponderada fica mais grosseira, mas ainda funciona. read_u16() retorna um valor de 0 a 65535, e a velocidade dos motores é dada em porcentagem (de -100 a 100). Para a configuração e os primeiros passos, veja O que é o 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 passRegras de competição e a pista
As regras das competições de seguidor de linha variam conforme o organizador e o ano, mas a ideia básica é a mesma: o robô precisa completar a pista no menor tempo sem sair da linha. Por exemplo, no regulamento de 2024 da categoria seguidor de linha de nível iniciante da Competição Nacional de Robôs do MEB, na Turquia, a pista é uma linha branca de 20 ± 2 mm sobre fundo preto. O robô pode medir no máximo 280 × 180 mm, incluindo as rodas, e pesar no máximo 1 kg, incluindo as baterias; a tensão de operação não pode passar de 12 V, e ele precisa usar dois motores CC de 6–12 V com redução em L e engrenagens de plástico, rodas de no máximo 65 mm de diâmetro e uma barra de até 8 sensores. A pista tem uma ponte com inclinação de 13 graus e um trecho quadrado em que cortar caminho pelo meio custa uma penalidade de 20 segundos; um robô cujo corpo fique totalmente sobre o fundo preto conta como fora da pista, e um que fique parado sobre a linha por mais de 30 segundos é desclassificado. Outras competições escolares e regionais usam o contrário, uma linha preta de 20 ± 2 mm sobre fundo branco, e pedem robôs menores, de no máximo 500 gramas. Resumindo, leia sempre o regulamento daquele ano antes de projetar o seu robô.
- Pista invertida: para uma linha branca sobre fundo preto, coloque a variável WHITE_LINE em true nos sketches. Se as cores se invertem no meio da pista, você pode acrescentar uma verificação que troca de modo quando os sensores de fora começam a ver preto e o do meio, branco.
- Curvas de 90 graus: diminua a velocidade base antes da curva ou deixe a roda interna dar ré; nos sketches deste artigo, a roda interna já dá ré quando a correção é maior que a velocidade base.
- Cruzamentos: quando todos os sensores veem a linha ao mesmo tempo, atravesse reto; se precisar escolher uma direção, monte uma máquina de estados que conta os cruzamentos.
- Linhas tracejadas: quando a linha some por um instante, experimente seguir reto por 100–200 ms em vez de virar na hora.
- Rampas e pontes: em uma inclinação, a distância entre o sensor e o chão muda; mantenha a barra de sensores baixa e perto das rodas e reduza um pouco a velocidade na rampa.
- Pista de treino: cole fita isolante preta fosca de 19 mm em cartolina branca fosca ou placa de espuma; nas primeiras tentativas, não faça curvas com raio menor que 15 cm.
Problemas comuns e soluções
| Problema | Causa provável | Solução |
|---|---|---|
| O robô faz zigue-zague nas retas | Kp alto demais, Kd baixo demais ou velocidade alta demais | Diminua o Kp em cerca de 20%, aumente o Kd; diminua a velocidade base, se precisar |
| Sai da pista nas curvas | Velocidade alta demais, barra de sensores estreita demais ou reação errada quando a linha se perde | Diminua a velocidade base, deixe a roda interna dar ré, vire para o último lado visto quando a linha se perder |
| O robô sempre puxa para um lado | Os motores giram em velocidades diferentes ou a barra de sensores não está centralizada | Centralize os sensores, reduza o motor mais rápido com um pequeno fator (0.95, por exemplo) |
| Os sensores veem preto ou branco em todo lugar | Sem calibração, sensor alto demais ou trimpot não ajustado | Baixe a altura para 3–5 mm, repita a calibração na pista |
| O Arduino reinicia quando os motores ligam | A tensão da bateria cai por um instante ou o GND comum está ruim | Alimente a placa separadamente ou acrescente um capacitor de 470 µF à alimentação, encurte os fios |
| O robô muda de comportamento conforme a bateria descarrega | A velocidade dos motores cai junto com a tensão | Meça a tensão da bateria e escale a velocidade, ou coloque uma bateria cheia antes de cada corrida |
| Funciona em casa, falha no local da competição | A luz ambiente ou um piso brilhante atrapalham o infravermelho | Proteja os sensores com uma saia preta, calibre sob a iluminação da competição |
Próximos passos e ideias de projetos
- Seguidor de linha que desvia de obstáculos: acrescente um HC-SR04 na frente para o robô contornar obstáculos na pista; a lógica de decisão está em Robô que desvia de obstáculos.
- Modo Bluetooth: alterne entre seguir linha e controle pelo celular com um botão; é fácil de acrescentar com o módulo Bluetooth HC-05.
- Cronômetro de volta: um robô que detecta a linha de largada e mostra o tempo da volta em um display OLED.
- Resolvedor de labirinto: um robô que conta e memoriza os cruzamentos e segue o caminho mais curto na segunda volta.
- Robô de armazém: uma pequena maquete de AGV que para em estações ao longo da linha e solta uma carga com um servo.
- Uma base sólida: se você ainda não montou o chassi, os motores e a bateria, comece pelo guia Carrinho robô com Arduino.
Um robô seguidor de linha ensina sensores, algoritmos, motores e alimentação de uma vez só, e fica um pouco mais rápido a cada rodada de ajustes. Coloque o robô para andar primeiro com a versão if-else simples, depois passe para o PID e ajuste os valores com paciência. Se tem curiosidade sobre como o PWM define a velocidade dos motores, O que é PWM? é uma boa próxima leitura. No app BlueGrays você encontra passo a passo, com esquemas do circuito e código, os projetos de detecção de superfície preta e branca com TCRT5000, do carrinho L298N controlado por Bluetooth e do robô que desvia de obstáculos.
Perguntas frequentes
Quantos sensores um robô seguidor de linha precisa?
Dois sensores bastam para um robô funcionar, mas três dão resultados bem mais estáveis com um sketch if-else simples. Para seguir a linha de forma suave e rápida, recomendam-se 5 ou 8 sensores analógicos com PID. Muitas competições permitem no máximo 8 sensores.
Por que o meu robô seguidor de linha faz zigue-zague?
O motivo mais comum é um Kp alto demais e um Kd baixo demais: o robô corrige demais cada desvio e cruza para o outro lado da linha. Diminuir um pouco o Kp, aumentar o Kd, reduzir a velocidade base e colocar os sensores 4–8 cm à frente das rodas reduzem a oscilação. Com código if-else simples, um leve zigue-zague é normal, porque o robô alterna entre algumas velocidades fixas.
Dá para montar um seguidor de linha sem PID?
Sim. Com um sketch if-else simples e 2 ou 3 sensores, o robô segue a linha, e isso muitas vezes basta para projetos escolares. Conforme a velocidade aumenta, porém, o robô perde a linha nas curvas e balança nas retas. Se você quer uma boa colocação em uma competição, vai precisar passar para o PID.
Qual é a melhor bateria para um robô seguidor de linha?
A opção mais prática são duas células Li-ion 18650 em série: elas dão 7,4 V nominais e fornecem com facilidade a corrente alta que os motores puxam. Robôs leves também usam um pacote LiPo 2S. Uma bateria de 9 V (PP3) não fornece a corrente dos motores e descarrega rápido.
Como mudo o código para uma linha branca sobre fundo preto?
Basta inverter os valores dos sensores: nos sketches deste artigo, colocar WHITE_LINE em true faz o valor normalizado virar 1000 − valor, então o robô segue a linha branca. Em módulos digitais, a mesma variável inverte a leitura do DO. Mesmo assim, faça a calibração na pista em que vai competir.
A que altura do chão o sensor TCRT5000 deve ficar?
O TCRT5000 funciona melhor a cerca de 2,5 mm; na prática, deixar 3–5 mm entre o sensor e o chão dá bons resultados. Acima de 1 cm, a diferença entre preto e branco diminui muito e o robô perde a linha. Em pistas com rampas, manter a barra de sensores perto das rodas reduz a variação da distância.
Projetos para montar depois desta leitura
Ver todos os projetos
Detecção de superfícies pretas e brancas com TCRT5000
Com o pequeno sensor que funciona como o olho dos robôs seguidores de linha, você identifica se o piso é preto ou branco. O TCRT5000 emite luz invisível em direção ao piso e verifica quanto dela é ref

Leitura de reflexão (proximidade) com TCRT5000
Com um pequeno sensor que emite luz invisível e mede quanto dela é refletido, você detecta a superfície à frente dele. Quanto mais o sensor se aproxima de um objeto ou de uma superfície clara, mais lu

Carrinho controlado por Bluetooth com L298N
Transforme seu celular em um controle remoto e dirija seu próprio carrinho robô. O Arduino recebe os comandos de um aplicativo de controle remoto no celular por meio de um módulo Bluetooth e aciona as

Robô que desvia de obstáculos
Que tal construir um veículo inteligente que encontra seu próprio caminho? O robô que desvia de obstáculos monitora o ambiente constantemente com o sensor ultrassônico na parte da frente. Ao encontrar

Medição de distância com HC-SR04 (Monitor Serial)
O sensor HC-SR04 funciona como um morcego que se orienta no escuro: emite um som que você não consegue ouvir, escuta o eco e mede a distância até o objeto à sua frente. O Arduino escreve essa distânci

Controle de LED com botão (momentâneo)
É como uma campainha: a luz fica acesa enquanto você pressiona o botão e apaga quando você solta. O Arduino monitora o botão continuamente; enquanto ele está pressionado, o LED fica aceso e apaga assi

Variação gradual do brilho do LED com PWM no ESP32
Varie suavemente o brilho do LED de 0 ao máximo e do máximo a 0. Controlamos o PWM com analogWrite(), como no Arduino. A mesma sintaxe funciona sem problemas com ESP32 Arduino Core 3.0+. Basta definir o pino e usar pinMode e analogWrite.

Raspberry Pi Pico: variação de brilho do LED com PWM (efeito de respiração)
O LED não só pisca — ele respira! Com o PWM (modulação por largura de pulso) do Raspberry Pi Pico, o brilho do LED aumenta e diminui lentamente, como a luz de um MacBook em repouso. Aprenda a usar a classe machine.PWM, duty_u16() (brilho de 16 bits, de 0–65535), frequência PWM (freq(1000) — 1.000 ciclos de ligar/desligar por segundo, sem cintilação visível), laços com passos usando range() (crescentes e decrescentes) e saída semelhante à analógica em um pino digital.
Mais artigos
App BlueGrays
Monte este circuito passo a passo pelo celular
Esquema do circuito, código e lista de componentes em uma só tela. Mais de 200 projetos com Arduino, ESP32 e Pico prontos para montar.
Escaneie com seu celular
Aponte a câmera para o código para abrir a loja de aplicativos do seu celular.
ou abrir a loja

