Робот, следующий по линии, на Arduino: датчики TCRT5000 и PID
Собери робота, следующего по линии, на Arduino: датчики TCRT5000, их размещение и калибровка, код на if-else и PID, настройка Kp/Kd, драйвер моторов, аккумулятор и правила соревнований.
Робот, следующий по линии, — автономная тележка, которая инфракрасными датчиками находит на полу чёрную (или белую) линию и постоянно подстраивает скорость двух колёс, чтобы оставаться на ней. Это один из первых настоящих робототехнических проектов на Arduino: в нём сразу есть чтение датчиков, принятие решений и управление моторами. На той же идее работают автономные тележки (AGV), развозящие материалы по заводам, а следование по линии — одна из самых многолюдных категорий на школьных соревнованиях по робототехнике.
В этом руководстве ты узнаешь, как работает датчик TCRT5000, чем отличаются линейки из 2, 3 и 5 датчиков, на какой высоте и с каким шагом их ставить и как откалибровать порог. Сначала заставишь робота ехать простым скетчем на if-else, а затем перейдёшь к версии с PID-регулятором, которая следует по линии без зигзагов и не вылетает с трассы на поворотах. Помимо рабочего кода для Arduino Uno, ESP32 и Raspberry Pi Pico здесь есть выбор драйвера моторов и аккумулятора, типичные правила соревнований и решения самых частых проблем.

Как работает робот, следующий по линии?
Белая поверхность отражает обратно большую часть инфракрасного света, а матовая чёрная большую часть поглощает. Датчики под роботом измеряют эту разницу и определяют, где линия относительно робота. Руля у робота нет; он поворачивает за счёт так называемого дифференциального привода: если левое колесо крутится быстрее правого, робот уходит вправо, а если медленнее — влево. Программа сотни раз в секунду повторяет один и тот же цикл: прочитать датчики, вычислить, насколько ушла линия, и соответственно поправить скорость двух моторов. Чем быстрее и стабильнее работает этот замкнутый контур, тем плавнее движется робот.
| Деталь | Рекомендуется | Примечание |
|---|---|---|
| Управляющая плата | Arduino Uno или Nano | Nano легче уместить на маленьком шасси; подойдут и ESP32 или Pico |
| Датчик линии | Линейка TCRT5000 на 3 или 5 каналов, QTR-8A | Линейка с аналоговыми выходами даёт лучший результат с PID |
| Драйвер моторов | L298N или TB6612FNG | TB6612FNG меньше и эффективнее |
| Моторы | 2 DC-мотора с редуктором (TT 1:48 или N20) | Оба мотора должны быть одной модели и скорости |
| Колёса | 2 ведущих колеса и 1 поворотное | Чаще всего используют колёса 65 мм |
| Питание | 2 × Li-ion 18650 (7,4 V) и выключатель | Батарейка-«крона» (PP3) на 9 V слишком слаба для моторов |
| Прочее | Шасси, кнопка, перемычки | С кнопкой проще калибровать и стартовать |




Датчик TCRT5000: линейки из 2, 3 и 5 датчиков
TCRT5000 состоит из инфракрасного светодиода (950 nm) и фототранзистора, стоящих рядом в одном корпусе. Чем больше света светодиода отражается от пола и попадает на фототранзистор, тем сильнее транзистор проводит. Лучше всего датчик работает на расстоянии около 2,5 мм; выше 1 см разница между чёрным и белым быстро падает. Готовые модули содержат компаратор LM393: на большинстве модулей пин DO выдаёт HIGH над чёрным и LOW над белым в зависимости от порога, выставленного потенциометром, а светодиод на плате загорается над белой поверхностью. Пин AO даёт сырое аналоговое значение: высокое над чёрным, низкое над белым. Все подробности о датчике — в статье Инфракрасный датчик TCRT5000.
| Датчиков | Размещение | Алгоритм | Когда выбирать |
|---|---|---|---|
| 2 | По обе стороны линии; обычно оба видят белое | Условия if-else | Первая попытка, широкая трасса с плавными поворотами |
| 3 | Средний над линией, крайние у её краёв | if-else или простой PID | Школьные проекты, соревнования начального уровня |
| 5 | Ровный ряд, средний над линией | PID (взвешенное среднее) | Быстрое и плавное следование по линии |
| 8 | Широкий ряд (как у QTR-8A) | PID плюс распознавание перекрёстков | Соревновательные трассы с резкими поворотами и перекрёстками |
Размещение датчиков: высота, шаг и вынос вперёд
Каким бы хорошим ни был код, робот не сможет хорошо ехать, если датчики стоят не там. Закрепи линейку датчиков перед осью колёс, параллельно полу и симметрично относительно средней линии робота. Хорошая отправная точка — такие размеры:
- Высота: оставь 3–5 мм между нижней стороной датчика и полом. Pololu указывает для своей линейки QTR-8A 3 мм как оптимальное и 6 мм как максимально рекомендуемое расстояние. Выше разница между чёрным и белым уменьшается, и робот теряет линию.
- Шаг: у аналоговых линеек расстояние между двумя датчиками должно быть от половины до двух третей ширины линии; для линии 20 мм хорошо подходят 10–13 мм. Тогда линия обычно оказывается под двумя датчиками, и положение вычисляется плавно.
- Цифровая схема на 3 датчика: средний датчик ставь прямо над линией, а боковые — сразу за её краями; для линии 20 мм это около 18–22 мм между центрами.
- Вынос вперёд: ставь датчики на 4–8 см впереди оси колёс. Слишком близко — робот поздно замечает повороты; слишком далеко — даже небольшой поворот сильно уводит датчики, и робот раскачивается.
- Окружающий свет: солнечный свет и галогенные лампы тоже содержат инфракрасное излучение. Если закрыть датчики чёрной юбкой или изолентой, показания станут намного стабильнее.
Подключение: Arduino Uno, ESP32 и Pico
Таблица ниже точно соответствует скетчам проверки датчиков и PID; подключение для примера на if-else с 3 датчиками описано в его собственном разделе. Нумеруй датчики слева направо, если смотреть на робота сзади: крайний левый — номер 1. В роли драйвера моторов используем L298N; что делает каждый пин и как работает H-мост, подробно объяснено в статье Управление DC-мотором через L298N. У Pico всего три аналоговых входа, поэтому на нём используем датчики 1, 3 и 5.
| Подключение | Arduino Uno | ESP32 | Raspberry Pi Pico |
|---|---|---|---|
| Датчик 1 (крайний левый) AO | A0 | GPIO 36 | GP26 |
| Датчик 2 AO | A1 | GPIO 39 | - |
| Датчик 3 (средний) AO | A2 | GPIO 34 | GP27 |
| Датчик 4 AO | A3 | GPIO 35 | - |
| Датчик 5 (крайний правый) AO | A4 | GPIO 32 | GP28 |
| VCC датчиков | 5V | 3V3 | 3V3 |
| L298N ENA (скорость левого мотора) | D9 | GPIO 25 | GP15 |
| L298N IN1, IN2 | D8, D7 | GPIO 26, 27 | GP14, GP13 |
| L298N ENB (скорость правого мотора) | D10 | GPIO 33 | GP10 |
| L298N IN3, IN4 | D5, D4 | GPIO 18, 19 | GP11, GP12 |
| Кнопка старта (другая ножка к GND) | D2 | GPIO 23 | GP16 |
| GND | GND | GND | GND (общий с аккумулятором и драйвером) |
Выбор драйвера моторов и аккумулятора
L298N дешёвый и выносливый, но его биполярные транзисторы съедают 1,5–3 V: от блока на 7,4 V до моторов доходит лишь около 5–5,5 V. На маленьких быстрых роботах лучше работают драйверы на MOSFET — TB6612FNG или DRV8833. С TB6612FNG код не меняется: подключения ENA, IN1 и IN2 переносятся на PWMA, AIN1 и AIN2 (на канале B — PWMB, BIN1 и BIN2), а пин STBY соединяется с VCC. У DRV8833 нет отдельного пина скорости (enable); сигнал PWM идёт прямо на пины IN, так что функцию drive() придётся доработать. Для питания удобнее всего два литий-ионных аккумулятора 18650 последовательно: 8,4 V при полном заряде, 7,4 V номинально, и они легко отдают большой ток. Батарейки-«кроны» на 9 V не справляются с током моторов и быстро садятся. Сколько продержится аккумулятор, можно посчитать утилитой «Время работы от батареи».
| Драйвер | Напряжение моторов | Ток на канал | Падение напряжения | Вердикт |
|---|---|---|---|---|
| L298N | 5–35 V | 2 A | 1,5–3 V | Выносливый, но крупный и неэффективный |
| TB6612FNG | 4,5–13,5 V | 1,2 A (пиковый 3,2 A) | Очень малое | Лучшее соотношение для робота по линии |
| DRV8833 | 2,7–10,8 V | 1,5 A (пиковый 2 A) | Очень малое | Маленькие роботы с моторами N20 |
| L9110S | 2,5–12 V | 0,8 A | Малое | Только лёгкие медленные роботы |

Быстрый пример расчёта скорости: жёлтые TT-моторы (1:48) при 6 V без нагрузки крутятся примерно на 200 об/мин. Длина окружности колеса 65 мм — π × 65 ≈ 204 мм, так что робот проезжает максимум 200 × 0,204 ≈ 41 м в минуту, то есть примерно 0,7 м в секунду. Для большинства школьных трасс этого более чем достаточно; настоящая задача — не потерять линию на такой скорости. Более быстрые роботы используют микромоторы с редуктором N20 и лёгкое шасси. По мере падения напряжения аккумулятора моторы замедляются, и настроенные значения PID «уплывают»; измерять напряжение аккумулятора через делитель напряжения и масштабировать под него скорость — продвинутое улучшение.
Шаг 1: проверяем датчики и находим порог
Прежде чем подключать моторы, выясни, что видят датчики. Скетч ниже выводит сырые аналоговые значения всех пяти датчиков через табуляцию; открой в Arduino IDE Инструменты › Плоттер по последовательному соединению (Tools › Serial Plotter) на скорости 115200 бод, и каждый датчик будет отдельной линией. Медленно проведи робота рукой поперёк линии и смотри, как меняются значения.
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}При 5 V типичный модуль TCRT5000 показывает 30–200 на белой поверхности и 600–1000 на чёрной линии, но эти числа меняются с высотой датчика, материалом пола и окружающим светом. Самый простой порог — порог = (значение белого + значение чёрного) / 2. Например, если на белом 80, а на чёрном 820, порог равен 450. Поскольку каждый датчик показывает чуть по-разному, скетч с PID делает это автоматически: во время калибровки он запоминает наименьшее и наибольшее значение каждого датчика, а затем масштабирует каждое показание от 0 (белое) до 1000 (чёрное) по формуле (показание − min) × 1000 / (max − min). Так порог для каждого датчика становится равным 500.
Шаг 2: простой алгоритм на if-else (3 датчика)
Для первого работающего робота нагляднее всего выбирать одно из нескольких готовых движений по состоянию трёх датчиков. Подключи выходы DO 3-канального модуля к A0 (левый), A1 (средний) и A2 (правый); аналоговые пины Uno работают и как цифровые входы. Логика показана в таблице (1 означает, что датчик видит линию):
| Левый | Средний | Правый | Значение | Движение |
|---|---|---|---|---|
| 0 | 1 | 0 | Линия посередине | Прямо |
| 1 | 1 | 0 | Линия чуть левее | Левое колесо замедляется, плавно влево |
| 1 | 0 | 0 | Линия далеко слева | Левое колесо назад, резко влево |
| 0 | 1 | 1 | Линия чуть правее | Правое колесо замедляется, плавно вправо |
| 0 | 0 | 1 | Линия далеко справа | Правое колесо назад, резко вправо |
| 1 | 1 | 1 | Перекрёсток или финишная линия | Проехать прямо |
| 0 | 0 | 0 | Линия потеряна | Повернуть в сторону, где линия была видна последней |
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}Переменная lastSide запоминает, с какой стороны робот последний раз видел линию; если на крутом повороте все три датчика теряют линию, робот поворачивает в ту сторону, чтобы снова её найти, а не уезжает прямо. С двумя датчиками та же логика ещё проще: датчики стоят по обе стороны линии, робот едет прямо, пока оба видят белое, поворачивает влево, когда левый видит чёрное, и вправо, когда это видит правый. Слабость этого метода в том, что движения ступенчатые: робот лишь переключается между несколькими фиксированными скоростями, поэтому слегка виляет даже на прямых и вылетает с трассы на поворотах, как только поднимаешь скорость.
Шаг 3: плавное следование по линии с PID
PID применяет поправку, пропорциональную тому, насколько робот ушёл от линии. Сначала положение линии вычисляется по пяти датчикам как взвешенное среднее: значение каждого датчика 0–1000 умножается на его позицию (0, 1000, 2000, 3000, 4000), результаты складываются и делятся на сумму значений. Получается число от 0 до 4000, равное 2000, когда линия ровно посередине. Например, если датчики показывают 0, 0, 300, 900, 0, положение равно (300 × 2000 + 900 × 3000) / 1200 = 2750. Ошибка = положение − 2000: положительное значение означает, что линия справа, отрицательное — что слева.
Поправка — сумма трёх слагаемых: поправка = Kp × ошибка + Ki × сумма ошибок + Kd × (ошибка − предыдущая ошибка). Слагаемое P поворачивает тем сильнее, чем больше отклонение. Слагаемое D смотрит, как быстро меняется ошибка, и тормозит, когда робот приближается к линии, гася колебания. Слагаемое I исправляет небольшие ошибки, которые долго держатся с одной стороны, но у робота по линии его обычно оставляют нулевым. Затем левый мотор крутится со скоростью базовая скорость + поправка, а правый — базовая скорость − поправка. Например, при ошибке 800 и Kp = 0.06 поправка равна 48; при базовой скорости 140 левый мотор крутится на 188, правый на 92, и робот плавно уходит вправо.
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}После загрузки нажми кнопку один раз: светодиод на плате загорится на 5 секунд. За это время несколько раз проведи робота рукой влево и вправо поперёк линии, чтобы каждый датчик увидел и чёрное, и белое. Когда светодиод погаснет, поставь робота на линию и нажми кнопку ещё раз. Цикл выполняется каждые 5 мс с помощью millis(); постоянный период держит слагаемое D всё время в одном масштабе. Если линия пропадает совсем, readPosition() возвращает 0 или 4000 в зависимости от стороны, где она была видна последней, и робот ищет её самым резким поворотом. Как подключить кнопку — в статье Как подключить кнопку к Arduino, а как отсчитывать время без delay — в статье millis() и delay() в Arduino.
Как настроить Kp и Kd
- Держи базовую скорость низкой (например, 100), а Ki и Kd поставь в ноль.
- Начни с маленького Kp (0.02) и повышай его шаг за шагом, пока робот не начнёт проходить повороты. Когда робот начинает быстро и равномерно колебаться вокруг линии, достигнут верхний предел Kp; оставайся чуть ниже этого значения.
- Начни с Kd примерно в 10 раз больше Kp и повышай, пока колебания не затухнут. Если Kd слишком велик, робот дрожит, а моторы визжат.
- Когда робот идёт идеально ровно на прямых и плавно в поворотах, подними базовую скорость на 10–20 и повтори шаги 2 и 3.
- Пробуй Ki, и то с очень малым значением (например, 0.0001), только если робот на длинных прямых всё время уходит в одну сторону.
- Запиши найденные значения и напряжение аккумулятора в этот момент: по мере разряда аккумулятора те же значения ведут себя иначе.
Робот по линии на ESP32: настраиваем PID с телефона
Большое преимущество ESP32 в том, что значения PID можно менять с телефона, не перезагружая код каждый раз. Логика та же, что у версии для Uno; отличия такие: аналоговый вход ESP32 12-битный (0–4095), датчики должны стоять на пинах ADC1, GPIO 32–39 (пины ADC2 нельзя читать при включённом Wi-Fi), а питаются датчики от 3,3 V. analogWrite() работает и в версии 2.x, и в версии 3.x ядра ESP32 Arduino. Установи на Android-телефон приложение вроде Serial Bluetooth Terminal, подключись к устройству «CizgiRobot» и отправляй команды вроде p0.07, d0.9 или s160; робот сразу использует новое значение и отправляет его обратно. Классический Bluetooth есть только у оригинальной ESP32 (не у S3 и не у C3), а iPhone такое последовательное подключение не поддерживает. Если ESP32 для тебя в новинку, сначала прочитай Что такое 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}Робот по линии на Raspberry Pi Pico и MicroPython
У Pico пользователю доступны всего три аналоговых входа (GP26, GP27, GP28), поэтому код ниже работает с тремя аналоговыми датчиками и вычисляет положение от 0 до 2000. Если хочешь пять датчиков, можно подключить 5-канальный модуль с цифровыми выходами к любым пяти GPIO и считать каждый датчик как 0 или 1000; взвешенное среднее становится грубее, но всё равно работает. read_u16() возвращает значение от 0 до 65535, а скорость моторов задаётся в процентах (от -100 до 100). Установка и первые шаги — в статье Что такое 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 passПравила соревнований и трасса
Правила соревнований роботов по линии зависят от организатора и года, но основная идея одна: робот должен пройти трассу за кратчайшее время, не сходя с линии. Например, в регламенте 2024 года для начального уровня турецкого национального соревнования роботов MEB трасса — белая линия 20 ± 2 мм на чёрном фоне. Робот может иметь размер не больше 280 × 180 мм с колёсами и весить не больше 1 кг с аккумуляторами; рабочее напряжение не должно превышать 12 V, а использовать нужно два L-образных DC-мотора на 6–12 V с пластиковыми шестернями, колёса диаметром не больше 65 мм и линейку не больше чем из 8 датчиков. На трассе есть мост с уклоном 13 градусов и квадратный участок, где срезка прямо через середину стоит 20 секунд штрафа; робот, корпус которого полностью оказался на чёрном фоне, считается сошедшим с трассы, а простоявший на линии больше 30 секунд дисквалифицируется. Другие школьные и региональные конкурсы используют обратное — чёрную линию 20 ± 2 мм на белом фоне — и требуют роботов поменьше, весом не больше 500 граммов. Коротко: всегда читай регламент текущего года, прежде чем проектировать робота.
- Инвертированная трасса: для белой линии на чёрном фоне поставь в скетчах переменную WHITE_LINE в true. Если цвета меняются местами в пределах трассы, можно добавить проверку, которая переключает режим, когда крайние датчики начинают видеть чёрное, а средний — белое.
- Повороты на 90 градусов: снижай базовую скорость перед поворотом или давай внутреннему колесу крутиться назад; в скетчах этой статьи внутреннее колесо и так идёт назад, когда поправка больше базовой скорости.
- Перекрёстки: когда все датчики сразу видят линию, проезжай прямо; если нужно выбирать направление, сделай конечный автомат, который считает перекрёстки.
- Пунктирные линии: когда линия ненадолго пропадает, попробуй ехать прямо ещё 100–200 мс, а не поворачивать сразу.
- Горки и мосты: на уклоне расстояние между датчиком и полом меняется; держи линейку датчиков низко и близко к колёсам и немного сбавляй скорость на горке.
- Тренировочная трасса: наклей матовую чёрную изоленту шириной 19 мм на белый матовый картон или пенокартон; в первых попытках не делай повороты круче радиуса 15 см.
Частые проблемы и решения
| Проблема | Вероятная причина | Решение |
|---|---|---|
| Робот виляет на прямых | Kp слишком велик, Kd слишком мал или скорость слишком высокая | Уменьши Kp примерно на 20%, увеличь Kd; при необходимости снизь базовую скорость |
| Вылетает с трассы на поворотах | Слишком высокая скорость, слишком узкая линейка датчиков или неправильная реакция на потерю линии | Снизь базовую скорость, дай внутреннему колесу крутиться назад, при потере линии поворачивай в сторону, где она была видна последней |
| Робота всегда тянет в одну сторону | Моторы крутятся с разной скоростью или линейка датчиков не по центру | Отцентруй датчики, уменьши скорость более быстрого мотора небольшим множителем (например, 0.95) |
| Датчики везде видят чёрное или белое | Нет калибровки, датчик слишком высоко или не выставлен подстроечный резистор | Опусти датчики до 3–5 мм, повтори калибровку на трассе |
| Arduino перезагружается при старте моторов | Напряжение аккумулятора на мгновение проседает или плохая общая земля | Питай плату отдельно или добавь конденсатор 470 µF на питание, укороти провода |
| По мере разряда аккумулятора робот ведёт себя иначе | Скорость моторов падает вместе с напряжением | Измеряй напряжение аккумулятора и масштабируй скорость или ставь полный аккумулятор перед каждым заездом |
| Дома работает, на соревнованиях нет | Окружающий свет или глянцевый пол мешают инфракрасному датчику | Закрой датчики чёрной юбкой, калибруй при освещении соревнований |
Следующие шаги и идеи проектов
- Робот по линии, объезжающий препятствия: добавь спереди HC-SR04, чтобы робот объезжал препятствия на трассе; логика принятия решений — в статье Робот, объезжающий препятствия.
- Режим Bluetooth: переключайся кнопкой со следования по линии на управление с телефона; это легко добавить с Bluetooth-модулем HC-05.
- Секундомер круга: робот, который распознаёт стартовую линию и показывает время круга на OLED-дисплее.
- Решатель лабиринта: робот, который считает и запоминает перекрёстки и во втором заезде едет кратчайшим путём.
- Складской робот: небольшая модель AGV, которая останавливается на станциях вдоль линии и сбрасывает груз сервоприводом.
- Надёжная основа: если шасси, моторы и аккумулятор ещё не собраны, начни с руководства Робот-машинка на Arduino.
Робот, следующий по линии, учит сразу датчикам, алгоритмам, моторам и питанию, и с каждым кругом настройки становится чуть быстрее. Сначала заставь робота ехать простой версией на if-else, затем переходи на PID и терпеливо подбирай значения. Если интересно, как PWM задаёт скорость мотора, хорошее продолжение — статья Что такое PWM? В приложении BlueGrays есть проекты распознавания чёрной и белой поверхности на TCRT5000, машинки на L298N с управлением по Bluetooth и робота, объезжающего препятствия, — пошагово, со схемами и кодом.
Частые вопросы
Сколько датчиков нужно роботу, следующему по линии?
Для работающего робота хватит двух датчиков, но три дают гораздо более стабильный результат с простым скетчем на if-else. Для плавного и быстрого следования по линии рекомендуются 5 или 8 аналоговых датчиков с PID. Многие соревнования разрешают не больше 8 датчиков.
Почему мой робот по линии виляет зигзагом?
Самая частая причина — слишком большой Kp и слишком маленький Kd: робот перекорректирует каждое отклонение и перескакивает на другую сторону линии. Колебания уменьшат небольшое снижение Kp, повышение Kd, снижение базовой скорости и вынос датчиков на 4–8 см вперёд от колёс. С простым кодом на if-else лёгкий зигзаг нормален: робот переключается между несколькими фиксированными скоростями.
Можно ли сделать робота по линии без PID?
Да. С простым скетчем на if-else и 2 или 3 датчиками робот следует по линии, и для школьных проектов этого часто достаточно. Но с ростом скорости робот теряет линию на поворотах и раскачивается на прямых. Если целишься в хорошее место на соревнованиях, придётся переходить на PID.
Какой аккумулятор лучше для робота по линии?
Самый практичный вариант — два литий-ионных аккумулятора 18650 последовательно: они дают номинальные 7,4 V и легко отдают большой ток, который потребляют моторы. Лёгкие роботы используют ещё и блок LiPo 2S. Батарейка-«крона» (PP3) на 9 V не может отдать ток моторов и быстро садится.
Как изменить код для белой линии на чёрном фоне?
Нужно лишь инвертировать значения датчиков: в скетчах этой статьи при WHITE_LINE в true нормированное значение становится 1000 − значение, и робот едет по белой линии. На цифровых модулях та же переменная инвертирует показание DO. Калибровать всё равно стоит на той трассе, где будешь соревноваться.
На какой высоте над полом ставить датчик TCRT5000?
Лучше всего TCRT5000 работает примерно на 2,5 мм; на практике хорошие результаты даёт зазор 3–5 мм между датчиком и полом. Выше 1 см разница между чёрным и белым сильно уменьшается, и робот теряет линию. На трассах с горками линейка датчиков ближе к колёсам уменьшает изменение расстояния.
Что собрать после прочтения
Все проекты
Определение чёрной и белой поверхности с TCRT5000
С маленьким датчиком, который служит глазом роботов, едущих по линии, можно узнать, чёрный пол или белый. TCRT5000 светит на пол невидимым светом и проверяет, сколько его отражается обратно; на белом

Чтение отражения (приближения) с TCRT5000
С маленьким датчиком, который посылает невидимый свет и измеряет, сколько его отражается обратно, можно определить поверхность перед ним. Чем ближе предмет или светлая поверхность, тем больше света во

Машинка на L298N с управлением по Bluetooth
Преврати телефон в пульт и управляй своей машинкой-роботом. Arduino получает команды из приложения-пульта на телефоне через Bluetooth-модуль и вращает колёса через драйвер моторов; машинка едет вперёд

Робот, объезжающий препятствия
Как насчёт того, чтобы собрать умную машину, которая сама находит дорогу? Робот, объезжающий препятствия, постоянно осматривает всё вокруг благодаря ультразвуковому датчику спереди. Встретив препятств

Измерение расстояния HC-SR04 (вывод в порт)
Датчик HC-SR04 работает совсем как летучая мышь, которая находит дорогу в темноте: он посылает звук, которого ты не слышишь, слушает эхо и измеряет, насколько далеко предмет перед ним. Arduino каждые

Управление светодиодом кнопкой (удержание)
Как дверной звонок: свет горит, пока ты нажимаешь, и гаснет, когда отпускаешь. Arduino всё время слушает кнопку; пока она нажата, светодиод горит, а как только ты убираешь палец, он гаснет. Эта просте

Плавное затухание светодиода с PWM на ESP32
Плавное изменение яркости светодиода от 0 до максимума и от максимума до 0. PWM управляется через analogWrite(), как в Arduino, — тот же синтаксис без проблем работает в ESP32 Arduino Core 3.0+. Достаточно определения пина, pinMode и analogWrite.

Плавное затухание светодиода с PWM на Pico (эффект дыхания)
Светодиод больше не просто мигает — он дышит! С помощью PWM (широтно-импульсной модуляции) Raspberry Pi Pico яркость светодиода медленно растёт и медленно падает, как огонёк спящего MacBook. Изучи класс machine.PWM, duty_u16() (16-битная яркость 0–65535), частоту PWM (freq(1000) — 1000 циклов вкл/выкл в секунду, мерцания не видно), циклы range() с шагом (вверх и вниз) и почти аналоговый выход с цифрового пина.
Другие статьи
Приложение BlueGrays
Собери эту схему шаг за шагом в телефоне
Схема подключения, код и список деталей на одном экране. 200+ проектов на Arduino, ESP32 и Pico готовы к сборке.
Наведи камеру телефона
Наведи камеру на код — откроется магазин приложений на телефоне.
или открыть магазин

