Руководства · 14 мин чтения

Робот, следующий по линии, на 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 здесь есть выбор драйвера моторов и аккумулятора, типичные правила соревнований и решения самых частых проблем.

Инфракрасный датчик отражения TCRT5000
TCRT5000: синеватый инфракрасный светодиод и чёрный фототранзистор стоят рядом в одном корпусе; модули датчиков линии строятся на этой детали.

Как работает робот, следующий по линии?

Белая поверхность отражает обратно большую часть инфракрасного света, а матовая чёрная большую часть поглощает. Датчики под роботом измеряют эту разницу и определяют, где линия относительно робота. Руля у робота нет; он поворачивает за счёт так называемого дифференциального привода: если левое колесо крутится быстрее правого, робот уходит вправо, а если медленнее — влево. Программа сотни раз в секунду повторяет один и тот же цикл: прочитать датчики, вычислить, насколько ушла линия, и соответственно поправить скорость двух моторов. Чем быстрее и стабильнее работает этот замкнутый контур, тем плавнее движется робот.

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

Датчик 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 UnoESP32Raspberry Pi Pico
Датчик 1 (крайний левый) AOA0GPIO 36GP26
Датчик 2 AOA1GPIO 39-
Датчик 3 (средний) AOA2GPIO 34GP27
Датчик 4 AOA3GPIO 35-
Датчик 5 (крайний правый) AOA4GPIO 32GP28
VCC датчиков5V3V33V3
L298N ENA (скорость левого мотора)D9GPIO 25GP15
L298N IN1, IN2D8, D7GPIO 26, 27GP14, GP13
L298N ENB (скорость правого мотора)D10GPIO 33GP10
L298N IN3, IN4D5, D4GPIO 18, 19GP11, GP12
Кнопка старта (другая ножка к GND)D2GPIO 23GP16
GNDGNDGNDGND (общий с аккумулятором и драйвером)
Минус аккумулятора, GND драйвера моторов и GND платы должны быть соединены; без общей земли моторы ведут себя непредсказуемо. Никогда не питай моторы от пина 5V платы. На ESP32 и Pico питай датчики от 3V3: выход AO модуля с питанием 5 V может подниматься до 5 V и повредить аналоговые входы на 3,3 V.

Выбор драйвера моторов и аккумулятора

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 не справляются с током моторов и быстро садятся. Сколько продержится аккумулятор, можно посчитать утилитой «Время работы от батареи».

ДрайверНапряжение моторовТок на каналПадение напряженияВердикт
L298N5–35 V2 A1,5–3 VВыносливый, но крупный и неэффективный
TB6612FNG4,5–13,5 V1,2 A (пиковый 3,2 A)Очень малоеЛучшее соотношение для робота по линии
DRV88332,7–10,8 V1,5 A (пиковый 2 A)Очень малоеМаленькие роботы с моторами N20
L9110S2,5–12 V0,8 AМалоеТолько лёгкие медленные роботы
Жёлтый DC-мотор с редуктором TT и колесо
Жёлтый TT-мотор с редуктором и колесом 65 мм: редуктор снижает скорость и повышает крутящий момент; это самый распространённый мотор в проектах роботов по линии и робот-машинок.

Быстрый пример расчёта скорости: жёлтые TT-моторы (1:48) при 6 V без нагрузки крутятся примерно на 200 об/мин. Длина окружности колеса 65 мм — π × 65 ≈ 204 мм, так что робот проезжает максимум 200 × 0,204 ≈ 41 м в минуту, то есть примерно 0,7 м в секунду. Для большинства школьных трасс этого более чем достаточно; настоящая задача — не потерять линию на такой скорости. Более быстрые роботы используют микромоторы с редуктором N20 и лёгкое шасси. По мере падения напряжения аккумулятора моторы замедляются, и настроенные значения PID «уплывают»; измерять напряжение аккумулятора через делитель напряжения и масштабировать под него скорость — продвинутое улучшение.

Шаг 1: проверяем датчики и находим порог

Прежде чем подключать моторы, выясни, что видят датчики. Скетч ниже выводит сырые аналоговые значения всех пяти датчиков через табуляцию; открой в Arduino IDE Инструменты › Плоттер по последовательному соединению (Tools › Serial Plotter) на скорости 115200 бод, и каждый датчик будет отдельной линией. Медленно проведи робота рукой поперёк линии и смотри, как меняются значения.

C++ (Arduino IDE)
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.

У модулей с цифровым выходом (DO) порог выставляется потенциометром на плате. Поверни подстроечный резистор так, чтобы светодиод модуля не горел, когда датчик над чёрной линией, и горел над белой поверхностью; положение ровно посередине между этими двумя точками самое стабильное. Настраивай на той трассе, где будешь соревноваться, и при том же освещении.

Шаг 2: простой алгоритм на if-else (3 датчика)

Для первого работающего робота нагляднее всего выбирать одно из нескольких готовых движений по состоянию трёх датчиков. Подключи выходы DO 3-канального модуля к A0 (левый), A1 (средний) и A2 (правый); аналоговые пины Uno работают и как цифровые входы. Логика показана в таблице (1 означает, что датчик видит линию):

ЛевыйСреднийПравыйЗначениеДвижение
010Линия посерединеПрямо
110Линия чуть левееЛевое колесо замедляется, плавно влево
100Линия далеко слеваЛевое колесо назад, резко влево
011Линия чуть правееПравое колесо замедляется, плавно вправо
001Линия далеко справаПравое колесо назад, резко вправо
111Перекрёсток или финишная линияПроехать прямо
000Линия потерянаПовернуть в сторону, где линия была видна последней
C++ (Arduino IDE)
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, и робот плавно уходит вправо.

C++ (Arduino IDE)
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

  1. Держи базовую скорость низкой (например, 100), а Ki и Kd поставь в ноль.
  2. Начни с маленького Kp (0.02) и повышай его шаг за шагом, пока робот не начнёт проходить повороты. Когда робот начинает быстро и равномерно колебаться вокруг линии, достигнут верхний предел Kp; оставайся чуть ниже этого значения.
  3. Начни с Kd примерно в 10 раз больше Kp и повышай, пока колебания не затухнут. Если Kd слишком велик, робот дрожит, а моторы визжат.
  4. Когда робот идёт идеально ровно на прямых и плавно в поворотах, подними базовую скорость на 10–20 и повтори шаги 2 и 3.
  5. Пробуй Ki, и то с очень малым значением (например, 0.0001), только если робот на длинных прямых всё время уходит в одну сторону.
  6. Запиши найденные значения и напряжение аккумулятора в этот момент: по мере разряда аккумулятора те же значения ведут себя иначе.
В масштабе этой статьи (положение 0–4000, PWM 0–255, цикл 5 мс) хороший начальный диапазон — 0.04–0.08 для Kp и 0.4–1.2 для Kd. Во время настройки не ставь Serial.print в цикл: каждый вывод замедляет цикл и сбивает слагаемое D. Если нужно видеть значения, выводи их не чаще раза в 200 мс.

Робот по линии на 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?

C++ (Arduino IDE)
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}
Некоторые регламенты соревнований запрещают модули Wi-Fi, Bluetooth и RF на роботе или даже платы с Wi-Fi вообще. Надёжнее всего использовать ESP32 для настройки и тренировок, а соревноваться на Arduino Uno, Nano или Pico без Wi-Fi; если сомневаешься, спроси организаторов.

Робот по линии на Raspberry Pi Pico и MicroPython

У Pico пользователю доступны всего три аналоговых входа (GP26, GP27, GP28), поэтому код ниже работает с тремя аналоговыми датчиками и вычисляет положение от 0 до 2000. Если хочешь пять датчиков, можно подключить 5-канальный модуль с цифровыми выходами к любым пяти GPIO и считать каждый датчик как 0 или 1000; взвешенное среднее становится грубее, но всё равно работает. read_u16() возвращает значение от 0 до 65535, а скорость моторов задаётся в процентах (от -100 до 100). Установка и первые шаги — в статье Что такое Raspberry Pi Pico?

MicroPython
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 на питание, укороти провода
По мере разряда аккумулятора робот ведёт себя иначеСкорость моторов падает вместе с напряжениемИзмеряй напряжение аккумулятора и масштабируй скорость или ставь полный аккумулятор перед каждым заездом
Дома работает, на соревнованиях нетОкружающий свет или глянцевый пол мешают инфракрасному датчикуЗакрой датчики чёрной юбкой, калибруй при освещении соревнований
Самый быстрый способ сузить круг проблем — поднять колёса над землёй и провести робота над линией рукой: если колёса ускоряются в правильную сторону, датчики и код в порядке, а проблема в механике или питании. Подробная диагностика проблем с перезагрузкой — в статье Arduino постоянно перезагружается.

Следующие шаги и идеи проектов

  • Робот по линии, объезжающий препятствия: добавь спереди 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
#78Средний
Premium

Определение чёрной и белой поверхности с TCRT5000

С маленьким датчиком, который служит глазом роботов, едущих по линии, можно узнать, чёрный пол или белый. TCRT5000 светит на пол невидимым светом и проверяет, сколько его отражается обратно; на белом

4.7(40)
49
Чтение отражения (приближения) с TCRT5000
#77Средний
Premium

Чтение отражения (приближения) с TCRT5000

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

4.6(33)
43
Машинка на L298N с управлением по Bluetooth
#80Продвинутый
Premium

Машинка на L298N с управлением по Bluetooth

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

5.0(38)
65
Робот, объезжающий препятствия
#81Продвинутый
Premium

Робот, объезжающий препятствия

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

5.0(38)
49
Измерение расстояния HC-SR04 (вывод в порт)
#27Новичок

Измерение расстояния HC-SR04 (вывод в порт)

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

4.6(46)
71
Управление светодиодом кнопкой (удержание)
#9Новичок

Управление светодиодом кнопкой (удержание)

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

4.8(113)
229
Плавное затухание светодиода с PWM на ESP32
#110Новичок

Плавное затухание светодиода с PWM на ESP32

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

4.8(36)
54
Плавное затухание светодиода с PWM на Pico (эффект дыхания)
#160Новичок

Плавное затухание светодиода с PWM на Pico (эффект дыхания)

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

0.0(0)
4