Робот-курьер Курс одним файлом Все курсыДля учителяПройти в программе Ученик Arduino Uno + шилд моторов L298P
Материалы ученика

Робот-курьер

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

Уровень итоговой задачи: Начальный
Уровень итоговой задачи: Средний
Уровень итоговой задачи: Продвинутый

Комплект: Arduino Uno + шилд моторов L298P · версия курса 0.2.0

Тема 1 · Урок 1.1

Собираем робота и оживляем плату

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

2 × 45 мин1–2 ученика на роботаСборка

Результат урока

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

Ты научишься

  • Собрать ходовую часть робота по инструкции и проверить каждый узел.
  • Различать перед, зад, левую и правую стороны робота.
  • Загрузить программу в Arduino и объяснить, что делают setup() и loop().
  • Проверить оба мотора, их остановку и оба датчика линии; отличать команду программы от наблюдаемого результата.

Нужно знать

Ничего — это первый урок.

Понадобится

Основа 130 × 150Кронштейн мотораКронштейн мотора (зеркальный)Мотор-редуктор × 2Колесо × 2Шаровая опораДержатель ArduinoArduino Uno R3Шилд моторов L298P (Амперка)Планка датчиковДатчик линии (аналоговый, Trema) × 2Крепление выключателяБатарейный отсек моторов с выключателемUSB-кабель для ArduinoКрепёж: винты, гайки, шайбы, стойкиОтвёртка

Зачем этот урок

Весь курс ты будешь работать с одним роботом — своим. Сегодня соберёшь его ходовую часть, установишь «мозг» робота — плату Arduino — и напишешь первую программу. К концу урока ты проверишь каждый установленный узел и запишешь, что он действительно работает.

Готовый робот-курьер: платформа на двух колёсах, плата управления, датчики спереди и захват
Так робот будет выглядеть в конце курса. Сегодня собираем основу: платформу, моторы, колёса, плату и датчики линии.
Безопасность

Пока идёт сборка, батарейный отсек отключён, а USB-кабель не подключён к компьютеру. Батарейный отсек к шилду заранее подключает взрослый. Моторы ты включаешь и выключаешь только выключателем.

1
Занятие 1 · 45 мин
Ходовая часть
1

Распределите роли

3 мин

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

Что используем и как это работает. Arduino Uno выполняет программу. Драйвер L298P по её сигналам управляет питанием моторов: вывод DIR задаёт направление, PWM — долю времени подачи питания. Мотор превращает электрическую энергию во вращение, а колесо — в движение. Два датчика отражения снизу помогают различать светлую поверхность и чёрную линию.

Как связать измерение с действием. Команда и результат — разные вещи. Надпись в мониторе означает, что программа дошла до печати; вращение проверяем глазами. На подставке проверим оба направления каждого колеса и остановку, затем сравним показания датчиков над белой и чёрной карточками. Так узнаем, как именно собран наш робот.

Вы работаете вдвоём. Один — Пилот: держит детали и отвёртку. Второй — Штурман: читает шаг вслух, подаёт детали и проверяет результат по картинке.

  1. Решите, кто начинает Пилотом.
  2. После каждой детали, которая закреплена винтами, меняйтесь ролями.
Должно получиться

Каждый знает свою роль. Штурман держит инструкцию, Пилот — инструмент.

2

Разложи детали

7 мин

Прежде чем собирать, найди каждую деталь. Так ты не перепутаешь похожие детали и сразу увидишь, если чего-то не хватает.

Основа 130 × 150
1 шт.
Кронштейн мотора
1 шт.
Кронштейн мотора (зеркальный)
1 шт.
Планка датчиков
1 шт.
Держатель Arduino
1 шт.
Крепление выключателя
1 шт.
Колесо
2 шт.
  1. Разложи печатные детали в ряд, как на карточках.
  2. Найди два кронштейна моторов. Они похожи, но зеркальные: положи их рядом и посмотри — полочки смотрят в разные стороны.
  3. Рядом положи покупные детали: Arduino, шилд моторов, два мотора, шаровую опору, два датчика линии, батарейный отсек и пакеты с крепежом.
Должно получиться

Все детали лежат перед тобой. Ты можешь показать оба зеркальных кронштейна.

Подсказка: Не хватает детали

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

3

Потренируйся на одном винте

4 мин

Почти все детали робота держатся на винтах и гайках. Соберём одно соединение на столе.

  1. Возьми винт, две шайбы и гайку из пакета «Тренировка».
  2. Надень шайбу на винт, проведи винт через две детали, надень вторую шайбу и накрути гайку пальцами.
  3. Придержи гайку пальцем и затяни винт отвёрткой. Остановись, как только детали перестали шевелиться.
Основа робота сверху: сетка отверстий, большое прямоугольное окно и маленькое квадратное
Сетка отверстий основы. Через эти отверстия будем крепить все узлы.
Должно получиться

Порядок соединения: винт → шайба → деталь → деталь → шайба → гайка. Детали не болтаются.

Как это устроено: Почему нельзя затягивать изо всех сил

Детали напечатаны из пластика. Если перетянуть винт, пластик продавится или треснет. Признак перетяжки — деталь прогибается или белеет вокруг винта.

4

Договоритесь, где перед

3 мин

Чтобы «левое колесо» всегда значило одно и то же, отметим перед робота.

  1. Положи основу плоско: маленькое квадратное окно — дальше от тебя, длинное прямоугольное — ближе.
  2. Дальняя сторона — перед. Наклей на неё бумажную стрелку «вперёд».
  3. Лево и право определяй как водитель, который сидит в роботе и смотрит по стрелке.
Должно получиться

На основе есть стрелка «вперёд». Вы оба показываете левую сторону робота одинаково.

5

Прикрепи кронштейны моторов

8 мин

Кронштейны держат моторы снизу основы.

Основа снизу с двумя закреплёнными кронштейнами, выделенными оранжевым
Вид снизу. Новые детали выделены оранжевым.
  1. Переверни основу так, чтобы стрелка осталась видна сбоку.
  2. Приложи кронштейны к нижней стороне, как на рисунке. Большие круглые отверстия смотрят наружу — туда будут выходить оси колёс.
  3. Вставь винты сверху, снизу надень шайбы и гайки. Сначала наживи все винты пальцами, потом затяни.
Должно получиться

Два кронштейна стоят друг напротив друга, большие отверстия — снаружи. Кронштейны не качаются.

Проверь себя
6

Установи моторы

8 мин

Ось мотора — вращающийся стержень, на который надевается колесо. Она должна выйти наружу через отверстие кронштейна.

Основа снизу: два мотора установлены в кронштейны, оси выходят наружу
Корпус мотора внутри, ось — снаружи.
  1. Приложи первый мотор к кронштейну изнутри. Ось проходит через большое отверстие наружу.
  2. Закрепи мотор винтами из пакета «Моторы». Держи мотор рукой, пока затягиваешь.
  3. Так же установи второй мотор.
  4. Наклей на провода метки: Л — левый мотор, П — правый. Сверься со стрелкой «вперёд».
Должно получиться

Оба мотора держатся крепко, оси смотрят влево и вправо, провода подписаны.

Подсказка: Мотор не встаёт в кронштейн

Не дави и не подпиливай. Проверь, тот ли это кронштейн: может быть, ты взял зеркальный для другой стороны.

7

Надень колёса

5 мин
  1. Совмести отверстие колеса с осью мотора. Форма отверстия должна совпасть с формой оси.
  2. Надень колесо, придерживая мотор за корпус. Не бей по колесу.
  3. Посмотри сбоку: колесо не должно касаться кронштейна или основы.
Основа с двумя колёсами, вид сбоку-сверху
Между колесом и кронштейном виден зазор.
Должно получиться

Два колеса стоят на осях и ничего не задевают.

8

Добавь заднюю опору и проверь робота

7 мин

Колёс два, поэтому роботу нужна третья точка опоры сзади — шаровая опора.

Основа с колёсами и опорой на стойках в задней части
Опора крепится снизу у задней стороны основы на стойках.
  1. Закрепи шаровую опору снизу у задней стороны основы. Стойки из пакета «Опора» подобраны по высоте.
  2. Поставь робота на стол. Он должен стоять ровно на двух колёсах и опоре.
  3. Аккуратно покачай робота. Ничего не должно болтаться.
Должно получиться

Робот стоит устойчиво и ровно. Ходовая часть готова.

Проверь себя
2
Занятие 2 · 45 мин
Электроника и первая программа
9

Установи Arduino

6 мин

Arduino — «мозг» робота. Плата лежит на держателе, чтобы её контакты снизу не касались винтов.

Держатель с платой Arduino на основе робота, выделен оранжевым
Держатель и плата в центре основы.
  1. Закрепи держатель Arduino в центре основы стойками вверх.
  2. Положи плату на стойки так, чтобы разъём USB был у края и до него можно было дотянуться.
  3. Закрепи плату винтами. Затягивай их по очереди, понемногу.
Должно получиться

Плата лежит ровно и не касается винтов основы. Разъём USB свободен.

10

Подготовь компьютер

5 мин

Программы для робота пишут в MyRobotica. В ней есть уроки и редактор кода.

  1. Подключи USB-кабель к Arduino и к компьютеру. Батарейный отсек моторов остаётся отключённым.
  2. Открой MyRobotica, курс «Робот-курьер» и урок 1.1. Открой учебный проект урока.
  3. В правой части кнопки Загрузить на робота выбери своего настоящего робота. В Подробнее о связи проверь плату Arduino Uno и её порт.
  4. В редакторе оставь пустые функции setup() и loop() и сохрани программу.
Должно получиться

На плате горит индикатор питания ON. В MyRobotica открыт учебный проект, выбран настоящий робот с платой Arduino Uno и виден его порт.

Подсказка: Не вижу порт

Отключи и снова подключи кабель. Если порт так и не появился, возможно, кабель только заряжает и не передаёт данные. Попроси другой кабель.

Программы для робота пишут в MyRobotica. В ней есть уроки и редактор кода.

  1. Подключи USB-кабель к Arduino и к компьютеру. Батарейный отсек моторов остаётся отключённым.
  2. Открой MyRobotica, курс «Робот-курьер», урок 1.1 и его учебный проект.
  3. В правой части кнопки Загрузить на робота выбери своего настоящего робота. Открой Подробнее о связи и проверь плату Arduino Uno. Отключи USB-кабель, посмотри, как изменилось состояние связи, и подключи его снова. Запиши имя порта в журнал.
  4. В редакторе оставь пустые функции setup() и loop() и сохрани программу.
Должно получиться

В MyRobotica открыт учебный проект и выбран настоящий робот с платой Arduino Uno. Имя его порта записано.

Подсказка: Порт так и не появился

Кабель может только заряжать и не передавать данные. Попроси другой кабель.

  1. Подключи USB-кабель к Arduino и к компьютеру. Батарейный отсек моторов остаётся отключённым.
  2. В MyRobotica открой курс «Робот-курьер», урок 1.1 и его учебный проект. В правой части кнопки Загрузить на робота выбери своего настоящего робота. Через Подробнее о связи проверь плату Arduino Uno и найди её порт.
  3. Сохрани программу с пустыми setup() и loop(), нажми Проверить код, а потом Загрузить на робота.
  4. Запиши в журнал: плату, порт, версию MyRobotica и чем Проверить код отличается от Загрузить на робота — что делает каждая кнопка и нужна ли для неё плата.
Должно получиться

Пустой скетч проверен и загружен. В журнале — плата, порт, версия MyRobotica и разница двух кнопок.

Подсказка: Проверить код и Загрузить на робота

Отключи кабель и нажми обе кнопки по очереди. Какая сработала без платы?

12

Надень шилд и подключи моторы

7 мин

Выводы Arduino не могут дать моторам достаточно тока. Поэтому моторы подключаются к шилду — плате, которая надевается сверху на Arduino и включает моторы по её командам.

Безопасность

Сначала отключи USB-кабель. Ничего не подключай и не отключай, пока плата под питанием.

  1. Совмести ножки шилда с гнёздами Arduino и аккуратно надави сверху. Ни одна ножка не должна остаться снаружи или согнуться.
  2. Подключи провода моторов к клеммам шилда:
Что подключаемКуда
Левый мотор
Пометь провода буквой «Л».
Клеммы канала M2 на шилде
Правый мотор
Пометь провода буквой «П».
Клеммы канала M1 на шилде
Батарейный отсек (через выключатель)взрослый
Подключает взрослый.
Клеммы питания моторов на шилде
  1. Батарейный отсек с выключателем уже подключён взрослым. Проверь, что выключатель выключен. Если провода отсека не подключены — сам их не подключай, позови взрослого.
Должно получиться

Шилд сидит на Arduino ровно. Провода моторов закреплены в клеммах и подписаны.

13

Установи датчики линии

6 мин

Датчики линии будут «смотреть» на пол и замечать чёрную линию. Ставим их спереди, поперёк робота.

  1. Закрепи планку датчиков снизу у переднего края основы на стойках.
  2. Закрепи на планке два датчика — левый и правый — чувствительной стороной вниз, к полу. Датчики на одинаковой высоте.
  3. Подключи датчики к шилду по таблице. Буквы у контактов датчика: S — сигнал, V — питание, G — земля.
Что подключаемКуда
Левый датчик · S (сигнал)A4
Левый датчик · V (питание)5V
Левый датчик · G (земля)GND
Правый датчик · S (сигнал)A5
Правый датчик · V (питание)5V
Правый датчик · G (земля)GND
  1. Уложи провода так, чтобы они не касались колёс.
Должно получиться

Два датчика смотрят вниз, их провода подключены и не свисают к колёсам.

14

Проверь все узлы робота

14 мин

Теперь проверим, что каждая часть робота работает: оба колеса и оба датчика. Программа для проверки уже открыта в редакторе справа.

Безопасность

Поставь робота на подставку так, чтобы колёса висели в воздухе и не касались стола. Пока выключатель моторов выключен, колёса крутиться не могут.

Программу писать не нужно — она готова. Ты будешь отправлять ей буквы и цифры, а она будет крутить одно колесо или показывать числа датчиков.

Готовая диагностикаL11_Diagnostics.ino.ino
// Программа проверки для робота курса (Arduino Uno + шилд L298P).// Робот стоит на подставке: колёса висят в воздухе.// Колесо крутится со скоростью 120 из 255 около 0,3 секунды.const int LEFT_DIR = 7;const int LEFT_PWM = 6;const int RIGHT_DIR = 4;const int RIGHT_PWM = 5;const int LEFT_SENSOR = A4;const int RIGHT_SENSOR = A5;const int FORWARD = HIGH;const int BACKWARD = LOW;const int TEST_PWM = 120;const unsigned long PULSE_MS = 300;bool running = false;unsigned long startedAt = 0; void reportSensors() {  Serial.print("LEFT=");  Serial.print(analogRead(LEFT_SENSOR));  Serial.print(" RIGHT=");  Serial.println(analogRead(RIGHT_SENSOR));} void beginPulse(int dirPin, int pwmPin, int direction) {  digitalWrite(dirPin, direction);  analogWrite(pwmPin, TEST_PWM);} // ===== Готовая оболочка — не меняй =====void stopMotors() {  analogWrite(LEFT_PWM, 0);  analogWrite(RIGHT_PWM, 0);  running = false;} void setup() {  pinMode(LEFT_PWM, OUTPUT);  pinMode(RIGHT_PWM, OUTPUT);  stopMotors();  pinMode(LEFT_DIR, OUTPUT);  pinMode(RIGHT_DIR, OUTPUT);  pinMode(LEFT_SENSOR, INPUT);  pinMode(RIGHT_SENSOR, INPUT);  Serial.begin(9600);  Serial.println("READY: 1=L forward, 2=L backward, 3=R forward, 4=R backward, d=sensors, s=stop");} void loop() {  if (running && millis() - startedAt >= PULSE_MS) {    stopMotors();    Serial.println("PWM=0; verify physical stop");  }  if (!Serial.available()) return;  char command = Serial.read();  if (command == 's') {    stopMotors();    Serial.println("PWM=0; verify physical stop");    return;  }  // Пока идёт импульс, новые команды не продлевают и не перезапускают его.  if (running) return;  if (command == 'd') {    reportSensors();    return;  }  if (command < '1' || command > '4') return;  // Удаляем накопленные команды: один ввод не должен запускать очередь движений.  while (Serial.available()) Serial.read();  stopMotors();  startedAt = millis();  running = true;  if (command == '1') beginPulse(LEFT_DIR, LEFT_PWM, FORWARD);  if (command == '2') beginPulse(LEFT_DIR, LEFT_PWM, BACKWARD);  if (command == '3') beginPulse(RIGHT_DIR, RIGHT_PWM, FORWARD);  if (command == '4') beginPulse(RIGHT_DIR, RIGHT_PWM, BACKWARD);  Serial.println("Observe wheel; no automatic hardware PASS");}
Как это устроено: Что делает программа
  • reportSensors печатает два числа: что видит левый и правый датчик.
  • beginPulse включает одно колесо.
  • Через 0,3 секунды колесо само останавливается: это делает stopMotors.

В программе уже готово всё, кроме двух функций в разделе «Твоя часть». Допиши их:

  • reportSensors — печатает числа левого и правого датчика с подписями;
  • beginPulse — включает выбранный мотор в нужную сторону со скоростью TEST_PWM. Второй мотор при этом выключен.

Остальную программу не меняй.

Допиши две функцииL11_Diagnostics_Gaps.ino.ino
// Программа проверки для робота курса (Arduino Uno + шилд L298P).// Робот стоит на подставке: колёса висят в воздухе.// Колесо крутится со скоростью 120 из 255 около 0,3 секунды.const int LEFT_DIR = 7;const int LEFT_PWM = 6;const int RIGHT_DIR = 4;const int RIGHT_PWM = 5;const int LEFT_SENSOR = A4;const int RIGHT_SENSOR = A5;const int FORWARD = HIGH;const int BACKWARD = LOW;const int TEST_PWM = 120;const unsigned long PULSE_MS = 300;bool running = false;unsigned long startedAt = 0; // ===== Твоя часть =====void reportSensors() {  // Цель: показать в мониторе подписанные показания левого и правого датчиков.} void beginPulse(int dirPin, int pwmPin, int direction) {  // Цель: включить только указанный мотор с указанным направлением и TEST_PWM.} // ===== Готовая оболочка — не меняй =====void stopMotors() {  analogWrite(LEFT_PWM, 0);  analogWrite(RIGHT_PWM, 0);  running = false;} void setup() {  pinMode(LEFT_PWM, OUTPUT);  pinMode(RIGHT_PWM, OUTPUT);  stopMotors();  pinMode(LEFT_DIR, OUTPUT);  pinMode(RIGHT_DIR, OUTPUT);  pinMode(LEFT_SENSOR, INPUT);  pinMode(RIGHT_SENSOR, INPUT);  Serial.begin(9600);  Serial.println("READY: 1=L forward, 2=L backward, 3=R forward, 4=R backward, d=sensors, s=stop");} void loop() {  if (running && millis() - startedAt >= PULSE_MS) {    stopMotors();    Serial.println("PWM=0; verify physical stop");  }  if (!Serial.available()) return;  char command = Serial.read();  if (command == 's') {    stopMotors();    Serial.println("PWM=0; verify physical stop");    return;  }  // Пока идёт импульс, новые команды не продлевают и не перезапускают его.  if (running) return;  if (command == 'd') {    reportSensors();    return;  }  if (command < '1' || command > '4') return;  // Удаляем накопленные команды: один ввод не должен запускать очередь движений.  while (Serial.available()) Serial.read();  stopMotors();  startedAt = millis();  running = true;  if (command == '1') beginPulse(LEFT_DIR, LEFT_PWM, FORWARD);  if (command == '2') beginPulse(LEFT_DIR, LEFT_PWM, BACKWARD);  if (command == '3') beginPulse(RIGHT_DIR, RIGHT_PWM, FORWARD);  if (command == '4') beginPulse(RIGHT_DIR, RIGHT_PWM, BACKWARD);  Serial.println("Observe wheel; no automatic hardware PASS");}
Подсказка: С чего начать

Найди в справке Arduino analogRead, Serial.print, digitalWrite и analogWrite. Сначала проверь датчики — для этого моторы не нужны.

Напиши программу сам. В заготовке есть только номера контактов и выключение моторов.

Напиши свою диагностикуL11_Diagnostics_Empty.ino.ino
// Программа проверки для робота курса (Arduino Uno + шилд L298P).// Робот стоит на подставке: колёса висят в воздухе.// Колесо крутится со скоростью 120 из 255 около 0,3 секунды.const int LEFT_DIR = 7;const int LEFT_PWM = 6;const int RIGHT_DIR = 4;const int RIGHT_PWM = 5;const int LEFT_SENSOR = A4;const int RIGHT_SENSOR = A5;const int FORWARD = HIGH;const int BACKWARD = LOW;const int TEST_PWM = 120;const unsigned long PULSE_MS = 300;bool running = false;unsigned long startedAt = 0; // Напиши диагностику по требованиям урока. При включении моторы стоят.void setup() {  pinMode(LEFT_PWM, OUTPUT);  pinMode(RIGHT_PWM, OUTPUT);  analogWrite(LEFT_PWM, 0);  analogWrite(RIGHT_PWM, 0);} void loop() {}

Что должна уметь программа:

  • работать с монитором порта на скорости 9600;
  • по букве d печатать подписанные числа обоих датчиков;
  • по 1 и 2 крутить левое колесо вперёд и назад, по 3 и 4 — правое;
  • каждый раз крутить со скоростью 120 не дольше 300 мс, потом выключать оба мотора;
  • по букве s сразу выключать оба мотора, даже во время вращения;
  • после включения и кнопки Reset моторы стоят, пока не придёт новая команда;
  • лишние нажатия не копятся в очередь и не продлевают вращение.
Подсказка: Как обойтись без delay

Запомни время начала вращения через millis(). В каждом проходе loop() проверяй, не пришла ли s и не прошло ли 300 мс.

Как проверить робота

  1. Проверь провода. Выключатель моторов выключен. Сверь провода с таблицами подключения: каждый на своём месте и не касается колёс. Колёса крутятся рукой свободно.
  2. Загрузи программу. Нажми Загрузить на робота. Внизу открой Serial Monitor со скоростью 9600. Появится строка READY — плата отвечает. Нажми кнопку Reset на плате: колёса стоят.
  3. Проверь датчики. Отправь d и прочитай два числа. Подложи под левый датчик белую бумагу и отправь d, потом чёрную — и снова d. Сделай так же с правым датчиком. На белом и чёрном числа должны заметно отличаться.
  4. Проверь колёса. Включи выключатель моторов. Отправь 1 и смотри на колёса: коротко крутится только левое колесо вперёд и само останавливается. Потом по очереди отправь 2, 3 и 4.
  5. Проверь остановку. Отправь 1 и сразу s — колесо останавливается. Выключи выключатель моторов и отправь 1 ещё раз: колёса не крутятся.
  6. Запиши результат в журнал для каждого узла из таблицы: команду, ответ в Serial Monitor и фактическое вращение или показание датчика. Если узел ещё не проверен, так и запиши: «не проверен». Если что-то не так — выключи выключатель моторов, отключи USB, открой «Если не получилось», исправь и проверь этот узел ещё раз.
Что проверяемЧто сделатьХорошо, если
ПлатаЗагрузить программу, нажать ResetЕсть READY, колёса стоят
Левое колесоОтправить 1, потом 2Крутится только левое: вперёд, потом назад
Правое колесоОтправить 3, потом 4Крутится только правое: вперёд, потом назад
Левый датчикd на белом и на чёрномЧисла заметно разные
Правый датчикd на белом и на чёрномЧисла заметно разные
Остановкаs и выключатель моторовКолесо останавливается
Если не получилось
Что видишьЧто сделать (выключатель моторов выключен)
Крутится не то колесоПоменяй местами провода моторов в клеммах M1 и M2
Колесо крутится назад вместо вперёдПоменяй местами два провода этого мотора в клемме
Колесо не крутитсяПроверь, что выключатель включён и провода плотно зажаты в клеммах
Число датчика не меняетсяПроверь провода S, V, G и высоту датчика над полом
Нет строки READYПроверь скорость 9600 и выбранный порт
Колесо крутится само после ResetПроверь, что загружена именно эта программа
Ничего не помогаетПозови взрослого и покажи, на каком пункте остановился
Должно получиться

Ты проверил каждое колесо в обе стороны, оба датчика и остановку. Результаты записаны в таблицу.

15

Запиши результат в журнал

4 мин
  1. Запиши в бортовой журнал дату, имя робота и что получилось сегодня.
  2. Отметь, какие узлы прошли проверку сразу, а какие ты исправлял.
  3. Сохрани программу проверки — она пригодится, если робот начнёт вести себя странно.
Должно получиться

Робот проверен, результаты записаны в журнал, программа проверки сохранена.

Проверь себя

Проверь себя

Выберите ответы для подготовки. Они не сохраняются и не оцениваются на сайте. Пройдите проверку знаний в программе курса или обсудите ответы с преподавателем.

● Основное

1.Что подтверждает успешная загрузка скетча?
2.Как проверить левый мотор?
3.Что проверяют у каждого датчика линии?
4.Зачем нужен шилд моторов?
5.Что отключить перед изменением проводки? Выбери все ответы.

★ Средний уровень

1.Что должна вывести функция чтения датчиков?
2.Выбран левый мотор. Что делает правый?
3.Когда настраивают выводы и выключают моторы при запуске?
4.Что доказывает PWM=0?
5.Что делать, если датчик выдаёт постоянное значение?

★★ Продвинутый уровень

1.Как обеспечить обработку s во время импульса?
2.Что делает скетч после Reset?
3.Что делать с накопленными командами движения?
4.Можно ли без энкодеров автоматически подтвердить вращение?
5.Все программные проверки прошли, но колесо не крутится. Итог?

Отвечай на вопросы своего уровня. Нужно верных ответов: Начальный — 4 из 5; Средний — 4 из 5; Продвинутый — 4 из 5. Ответы проверит преподаватель или программа курса.

Итог

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

Как подтвердить результат

Для каждого опыта сохраняй условия, версию программы, наблюдение и вывод. Сравнивая попытки, меняй одну причину за раз; неудачные попытки тоже остаются в журнале. Отметка результата относится к уже выполненным действиям, повторять их ради отметки не нужно. Если результат не получен, запиши расхождение и следующую проверяемую гипотезу. Модель, измерения настоящего робота и объяснение — разные виды доказательств.

Словарь урока

ArduinoПлата с микроконтроллером — маленьким компьютером, который выполняет твою программу.
шилдПлата, которая надевается сверху на Arduino и добавляет ей возможности. Наш шилд управляет моторами.
скетчПрограмма для Arduino.
функцияИменованный блок команд. setup и loop — функции.
светодиодМаленькая лампочка на плате. Светодиод L подключён к выводу 13.
выводКонтакт платы, которым программа может управлять или с которого может читать сигнал.
мсМиллисекунда — тысячная доля секунды. 1000 мс = 1 секунда.
Тема 2 · Урок 2.1

Моторы: едем и останавливаемся

Первые движения робота: вперёд, назад и обязательная остановка из программы.

2 × 45 мин1–2 ученика на роботаПрактика

Результат урока

Робот проезжает вперёд, останавливается и возвращается назад по программе. Движения оформлены функциями forward(), backward() и stopMotors().

Ты научишься

  • Объяснить, что задаёт контакт направления и что — контакт скорости.
  • Включить и остановить моторы командами digitalWrite и analogWrite.
  • Написать функции движения и вызвать их по порядку.
  • Объяснить, почему движение без команды остановки не заканчивается.

Нужно знать

  • 1.1 Собираем робота и оживляем плату

Понадобится

Мотор-редуктор × 2Шилд моторов L298P (Амперка)Батарейный отсек моторов с выключателемUSB-кабель для Arduino

Зачем этот урок

Робот собран, но пока стоит. Сегодня он поедет. Ты узнаешь, как плата командует моторами, и научишь робота ехать вперёд, назад и — самое важное — останавливаться по программе.

Разминка · вспомни
  1. Сколько раз выполняется setup(), а сколько — loop()?
  2. Что делает digitalWrite(вывод, HIGH)?
  3. Где на твоём роботе левый мотор?
Безопасность

Моторы проверяем на подставке: колёса не касаются стола. Программу загружаем при выключенном питании моторов. Чтобы остановить опыт, выключи питание моторов выключателем.

1
Занятие 1 · 45 мин
Один мотор, потом два
1

Подготовь робота

5 мин

Что должен делать робот. Роботу нужно начать движение, проехать участок и остановиться. Для этого научимся отдельно задавать направление и мощность каждого из двух моторов, а затем объединим команды в понятные действия.

Что используем и как это работает. Моторы подключены через драйвер L298P: вывод Arduino подаёт управляющий сигнал, а ток к мотору поступает через драйвер. DIR выбирает направление. ШИМ быстро чередует включение и выключение питания; большее значение analogWrite означает большую долю включённого времени. Это команда мощности, а не измеренная скорость в сантиметрах в секунду.

Как связать измерение с действием. Если оба колеса крутятся вперёд, робот едет; если одно слабее, он отклоняется. delay только задерживает следующую команду: мотор продолжает выполнять предыдущую. Поэтому в программе нужна отдельная остановка. Сначала наблюдаем колёса на подставке, затем проверяем направление, расстояние и остановку на поле.

  1. Поставь робота на подставку так, чтобы оба колеса висели в воздухе.
  2. Найди выключатель питания моторов и выключи его.
  3. Подключи USB-кабель.
Должно получиться

Колёса висят, питание моторов выключено, плата подключена к компьютеру.

2

Включи левый мотор

15 мин

Каждым мотором управляют два контакта. Контакт направления решает, куда крутится колесо. Контакт скорости решает, насколько сильно: число от 0 (стоит) до 255 (полная мощность). Левый мотор подключён к контактам 7 (направление) и 6 (скорость).

Левый моторL21_Left_Motor.ino.ino
const int LEFT_DIR_PIN = 7;const int LEFT_SPEED_PIN = 6;const int FORWARD = HIGH; void setup() {  pinMode(LEFT_DIR_PIN, OUTPUT);  pinMode(LEFT_SPEED_PIN, OUTPUT);   digitalWrite(LEFT_DIR_PIN, FORWARD);  analogWrite(LEFT_SPEED_PIN, 100);  delay(2000);  analogWrite(LEFT_SPEED_PIN, 0);} void loop() {}
Предскажи

Что сделает колесо после загрузки? Сколько секунд оно будет крутиться? Остановится ли оно само? Скажи напарнику до запуска.

  1. Набери программу и загрузи её при выключенном питании моторов.
  2. Включи питание моторов и нажми кнопку Reset на плате: программа начнётся заново.
  3. Сравни, что произошло, со своим прогнозом.
Должно получиться

Левое колесо крутится вперёд 2 секунды и останавливается.

Как это устроено: Что значит каждая строка
  • const int LEFT_DIR_PIN = …; — имя для номера контакта. Дальше в программе пишем имя, а не число: так понятнее и меньше ошибок.
  • digitalWrite(LEFT_DIR_PIN, FORWARD); — выбрать направление «вперёд».
  • analogWrite(LEFT_SPEED_PIN, 100); — включить мотор с силой 100 из 255.
  • analogWrite(LEFT_SPEED_PIN, 0); — остановить мотор.
  • Всё написано в setup(), поэтому выполняется один раз. loop() пустой: после остановки робот просто стоит.
Если не получилось
Что видишьЧто сделать
Колесо не крутитсяВключено ли питание моторов? Нажми Reset после включения.
Колесо крутится назадЗапиши это в журнал — на следующем шаге исправим направлением.
Крутится правое колесоПровода моторов перепутаны на шилде. Выключи питание и поменяй местами провода в клеммах M1 и M2.

Каждым мотором управляют два контакта. Контакт направления решает, куда крутится колесо. Контакт скорости решает, насколько сильно: число от 0 (стоит) до 255 (полная мощность). Левый мотор подключён к контактам 7 (направление) и 6 (скорость).

КомандаЧто делает
pinMode(контакт, OUTPUT);Разрешает плате управлять контактом
digitalWrite(контакт, FORWARD);Выбирает направление «вперёд»
analogWrite(контакт, сила);Включает мотор с силой от 0 до 255; 0 — стоп
delay(мс);Ждёт указанное число миллисекунд
  1. Создай скетч L21_Left_Motor по шаблону и напиши setup() сам: что должно произойти, сказано в комментариях.
Шаблон: левый моторL21_Left_Motor_Gaps.ino.ino
// Шаблон среднего уровня. Напиши код в разделе «Твоя часть» по целям в комментариях. const int LEFT_DIR_PIN = 7;const int LEFT_SPEED_PIN = 6;const int FORWARD = HIGH; // ===== Твоя часть: напиши сам ===== void setup() {  // Подготовь оба контакта левого мотора.  // Левое колесо крутится вперёд 2 секунды с силой 100 из 255 и останавливается само.} void loop() {}
Предскажи

Что будет, если забыть команду остановки? Запиши ответ и проверяй его только на подставке.

  1. Загрузи программу при выключенном питании моторов.
  2. Включи питание моторов и нажми кнопку Reset на плате: программа начнётся заново.
Подсказка: С чего начать

Сначала оба контакта должны стать выходами, потом — направление, потом сила.

Должно получиться

Левое колесо крутится вперёд 2 секунды и останавливается. Прогноз записан.

Если не получилось
Что видишьЧто сделать
Колесо не крутитсяНастроены ли контакты как выходы? Включено ли питание моторов? Нажми Reset.
Колесо крутится назадЗапиши это в журнал — на следующем шаге исправим направлением.
Крутится правое колесоПровода моторов перепутаны на шилде. Выключи питание и поменяй местами провода в клеммах M1 и M2.

Левый мотор подключён к контактам 7 (направление) и 6 (скорость). Направление «вперёд» на этом комплекте — уровень HIGH.

Как это устроено: Справка по командам
КомандаЧто делает
pinMode(pin, OUTPUT)Разрешает плате управлять контактом
digitalWrite(pin, HIGH) / LOWНаправление мотора
analogWrite(pin, v)Сила мотора: v от 0 (стоит) до 255
delay(ms)Ждать ms миллисекунд

Требование 1. Скетч L21_Left_Motor: левое колесо крутится вперёд ровно 2 секунды с силой из именованной переменной и останавливается само. Номера контактов — именованные константы, а не числа в командах.

Приёмка.

  1. На подставке напарник засекает по секундомеру, сколько крутится колесо.
  2. Замени уровень направления на противоположный. Что сделало колесо? Запиши, какой уровень на твоём роботе — «вперёд».
  3. Объясни в журнале, почему колесо остановилось, хотя loop() пустой.
Подсказка: Идея (1 из 2)

Один раз при включении: настроить контакты, выбрать направление, включить, подождать, выключить.

Подсказка: Подсказка к пункту 3 (2 из 2)

Где выполнилась команда остановки — в setup() или в loop()? Сколько раз?

Должно получиться

Колесо крутится 2 секунды и останавливается. В журнале — время по секундомеру, уровень «вперёд» и объяснение остановки.

3

Включи оба мотора

15 мин
  1. Создай скетч L21_Both_Motors и набери программу.
Оба мотораL21_Both_Motors.ino.ino
const int LEFT_DIR_PIN = 7;const int LEFT_SPEED_PIN = 6;const int RIGHT_DIR_PIN = 4;const int RIGHT_SPEED_PIN = 5;const int FORWARD = HIGH; int speed = 100; void setup() {  pinMode(LEFT_DIR_PIN, OUTPUT);  pinMode(LEFT_SPEED_PIN, OUTPUT);  pinMode(RIGHT_DIR_PIN, OUTPUT);  pinMode(RIGHT_SPEED_PIN, OUTPUT);   digitalWrite(LEFT_DIR_PIN, FORWARD);  digitalWrite(RIGHT_DIR_PIN, FORWARD);  analogWrite(LEFT_SPEED_PIN, speed);  analogWrite(RIGHT_SPEED_PIN, speed);  delay(1000);   analogWrite(LEFT_SPEED_PIN, 0);  analogWrite(RIGHT_SPEED_PIN, 0);} void loop() {}
  1. Загрузи и запусти на подставке.
  2. Поменяй speed на 60, потом на 200. Как меняется вращение?
Должно получиться

Оба колеса крутятся вперёд одну секунду и останавливаются. С большим speed колёса крутятся быстрее.

Как это устроено: Почему 0–255, а не километры в час

Число в analogWrite — это не скорость, а доля времени, когда мотор включён. При 255 он включён всё время, при 100 — примерно 40 % времени. Настоящая скорость зависит ещё от батареи и пола. Поэтому скорость подбирают опытом.

Правый мотор устроен так же, как левый: контакт направления 4 и контакт скорости 5. Имена всех четырёх контактов в шаблоне уже есть.

  1. Создай скетч L21_Both_Motors по шаблону и напиши setup() сам. Свою программу левого мотора используй как образец.
Шаблон: оба мотораL21_Both_Motors_Gaps.ino.ino
// Шаблон среднего уровня. Напиши код в разделе «Твоя часть» по целям в комментариях. const int LEFT_DIR_PIN = 7;const int LEFT_SPEED_PIN = 6;const int RIGHT_DIR_PIN = 4;const int RIGHT_SPEED_PIN = 5;const int FORWARD = HIGH;int speed = 100; // ===== Твоя часть: напиши сам ===== void setup() {  // Подготовь контакты обоих моторов.  // Оба колеса крутятся вперёд со скоростью speed одну секунду и останавливаются.} void loop() {}
  1. Загрузи и запусти на подставке.
  2. Поменяй speed на 60, потом на 200. Как меняется вращение?
Предскажи

Ты включаешь колёса по очереди: сначала левое, потом правое. Заметно ли, что они трогаются не одновременно? Почему?

Подсказка: Колёса ведут себя по-разному

Сравни команды для левого и правого мотора строка за строкой: для каждого нужны настройка, направление, сила и остановка.

Должно получиться

Оба колеса крутятся вперёд одну секунду и останавливаются. С большим speed колёса крутятся быстрее, прогноз записан.

Как это устроено: Почему 0–255, а не километры в час

Число в analogWrite — это не скорость, а доля времени, когда мотор включён. При 255 он включён всё время, при 100 — примерно 40 % времени. Настоящая скорость зависит ещё от батареи и пола. Поэтому скорость подбирают опытом.

Требование 2. Оба колеса крутятся вперёд ровно одну секунду со скоростью из одной именованной переменной speed и останавливаются. Правый мотор подключён к контактам 4 (направление) и 5 (скорость). Начни со своей программы для левого мотора и сохрани её как L21_Both_Motors.

Исследование: с какого числа колесо трогается. На подставке ставь speed = 20, 40, 60, 80… и запиши для каждого колеса, при каком наименьшем числе оно начинает уверенно крутиться.

speedЛевое крутится?Правое крутится?
20
40
60
Предскажи

Будут ли эти числа одинаковыми у двух колёс? А у тебя и у соседней команды?

Подсказка: Колесо гудит, но не крутится

Мотору не хватает силы сдвинуть редуктор. Такое число — ещё «мёртвая зона».

Должно получиться

Оба колеса крутятся одну секунду и останавливаются. В журнале — наименьшая скорость трогания для каждого колеса и вывод, какие числа speed брать нельзя.

4

Найди ошибку остановки

10 мин
Найди ошибку · Робот не останавливается

Соседи перенесли команды движения в loop() и убрали остановку:

void loop() {
  analogWrite(LEFT_SPEED_PIN, speed);
  analogWrite(RIGHT_SPEED_PIN, speed);
  delay(1000);
}

Они ждали, что робот проедет одну секунду. Что будет на самом деле и почему? Как исправить?

Должно получиться

Ты можешь объяснить: delay только ждёт, моторы сами не выключаются, а loop() повторяет команды снова и снова.

Найди ошибку · Робот почти не останавливается

Соседи перенесли движение в loop() и добавили остановку:

void loop() {
  analogWrite(LEFT_SPEED_PIN, speed);
  analogWrite(RIGHT_SPEED_PIN, speed);
  delay(1000);
  analogWrite(LEFT_SPEED_PIN, 0);
  analogWrite(RIGHT_SPEED_PIN, 0);
}

Они ждали: секунду едет, потом стоит. На подставке колёса крутятся почти без остановки. Почему?

  1. Запиши ответ в журнал.
  2. Предложи два разных исправления и запиши их кодом: одно — без loop(), второе — оставив движение в loop().
Подсказка: Второе исправление

Можно ли после остановки «застрять» на месте, чтобы loop() больше не повторялся? Вспомни защиту из урока 1.1… или пустой бесконечный цикл while (true) { }.

Должно получиться

Ты можешь объяснить, почему остановка длится мгновение, и в журнале два исправления.

Найди ошибку · Пустой loop(), а робот едет

У соседей loop() пустой, а движение — в setup():

void forward(int ms) {
  drive(FORWARD, speed, FORWARD, speed);
  delay(ms);
}

void setup() {
  // … настройка контактов
  forward(1000);
}

void loop() {
}

Робот должен был проехать одну секунду, а едет, пока его не выключат. Почему, если loop() пустой? Запиши ответ в журнал.

Требование. Перепиши программу так, чтобы движение осталось в loop(), но робот проехал ровно одну секунду и больше не трогался. while (true) использовать нельзя.

  1. Проверь на подставке: колёса крутятся одну секунду и стоят, пока не нажмёшь Reset.
  2. Объясни в журнале, как твоя программа «помнит», что движение уже было.
Подсказка: Идея

Нужна переменная, которая хранит «уже ехали» — bool. Её проверяют в начале loop().

Должно получиться

Программа выполняет требование, объяснение записано.

2
Занятие 2 · 45 мин
Функции движения
5

Оформи движения функциями

15 мин

Писать каждый раз по четыре строки неудобно. Соберём их в функции: forward(), backward(), stopMotors(). Внутри них — знакомые команды.

  1. Создай скетч L21_Moves и набери программу.
Функции движенияL21_Moves.ino.ino
const int LEFT_DIR_PIN = 7;const int LEFT_SPEED_PIN = 6;const int RIGHT_DIR_PIN = 4;const int RIGHT_SPEED_PIN = 5;const int FORWARD = HIGH;const int BACKWARD = (FORWARD == HIGH) ? LOW : HIGH; int speed = 100; void drive(int leftDir, int leftSpeed, int rightDir, int rightSpeed) {  digitalWrite(LEFT_DIR_PIN, leftDir);  digitalWrite(RIGHT_DIR_PIN, rightDir);  analogWrite(LEFT_SPEED_PIN, leftSpeed);  analogWrite(RIGHT_SPEED_PIN, rightSpeed);} void forward(int ms) {  drive(FORWARD, speed, FORWARD, speed);  delay(ms);} void backward(int ms) {  drive(BACKWARD, speed, BACKWARD, speed);  delay(ms);} void stopMotors() {  analogWrite(LEFT_SPEED_PIN, 0);  analogWrite(RIGHT_SPEED_PIN, 0);} void setup() {  pinMode(LEFT_DIR_PIN, OUTPUT);  pinMode(LEFT_SPEED_PIN, OUTPUT);  pinMode(RIGHT_DIR_PIN, OUTPUT);  pinMode(RIGHT_SPEED_PIN, OUTPUT);  stopMotors();  delay(2000); // время убрать руки от робота   forward(1000);  stopMotors();  delay(500);  backward(1000);  stopMotors();} void loop() {}
  1. Найди, где функции описаны и где они вызываются. Покажи напарнику.
Как это устроено: Как устроена функция с параметром

void forward(int ms) — функция с именем forward и параметром ms. Когда пишем forward(1000), внутри функции ms равно 1000. Так одна функция умеет ехать любое время.

drive(…) — вспомогательная функция: она сразу задаёт направление и скорость обоих колёс. Ей пользуются остальные функции.

Должно получиться

Программа проверяется без ошибок. Ты можешь показать описание и вызов forward.

Писать каждый раз по четыре строки неудобно. Соберём их в функции: forward(), backward(), stopMotors(). Функции drive и forward в шаблоне — образец.

  1. Создай скетч L21_Moves по шаблону. Напиши backward, stopMotors и движение в setup() сам.
Шаблон: функции движенияL21_Moves_Gaps.ino.ino
// Шаблон среднего уровня. Напиши код в разделе «Твоя часть» по целям в комментариях.// Функции drive и forward ниже — образец. const int LEFT_DIR_PIN = 7;const int LEFT_SPEED_PIN = 6;const int RIGHT_DIR_PIN = 4;const int RIGHT_SPEED_PIN = 5;const int FORWARD = HIGH;const int BACKWARD = (FORWARD == HIGH) ? LOW : HIGH;int speed = 100; // ===== Твоя часть: напиши сам ===== void backward(int ms) {  // Оба колеса крутятся назад со скоростью speed ms миллисекунд.} void stopMotors() {  // Оба мотора стоят.} void setup() {  pinMode(LEFT_DIR_PIN, OUTPUT);  pinMode(LEFT_SPEED_PIN, OUTPUT);  pinMode(RIGHT_DIR_PIN, OUTPUT);  pinMode(RIGHT_SPEED_PIN, OUTPUT);  stopMotors();  delay(2000); // время убрать руки от робота   // Вперёд 1 секунду, остановка, пауза полсекунды, назад 1 секунду, остановка.} // ===== Образец — не меняй ===== void drive(int leftDir, int leftSpeed, int rightDir, int rightSpeed) {  digitalWrite(LEFT_DIR_PIN, leftDir);  digitalWrite(RIGHT_DIR_PIN, rightDir);  analogWrite(LEFT_SPEED_PIN, leftSpeed);  analogWrite(RIGHT_SPEED_PIN, rightSpeed);} void forward(int ms) {  drive(FORWARD, speed, FORWARD, speed);  delay(ms);} void loop() {}
  1. Найди, где функции описаны и где они вызываются. Покажи напарнику.
Подсказка: Функция назад

Чем «назад» отличается от «вперёд»? В программе уже есть константа для этого направления.

Подсказка: Как вызвать функцию

Вызов — имя функции и значение параметра в скобках, в конце ;.

Как это устроено: Как устроена функция с параметром

void forward(int ms) — функция с именем forward и параметром ms. Когда пишем forward(1000), внутри функции ms равно 1000. Так одна функция умеет ехать любое время.

Должно получиться

Программа проверяется без ошибок. Ты можешь показать описание и вызов backward.

Требование 3. Программа L21_Moves построена из функций:

  • drive(leftDir, leftSpeed, rightDir, rightSpeed) — единственное место, где задаются направление и скорость колёс;
  • forward(ms) и backward(ms) — движение на заданное время, stopMotors() — остановка;
  • направление «назад» не записано числом, а получено из FORWARD: на другом комплекте FORWARD может быть другим;
  • после включения есть 2 секунды, чтобы убрать руки;
  • в setup(): вперёд 1 секунду, стоп, пауза полсекунды, назад 1 секунду, стоп.
Подсказка: Идея (1 из 3)

Сначала одна общая функция, которая ставит направление и скорость обоих колёс. Остальные функции только вызывают её с нужными значениями.

Подсказка: План (2 из 3)
  1. drive — четыре строки из своей программы, но с параметрами вместо чисел.
  2. forward(ms) — drive вперёд и delay(ms).
  3. backward(ms) — то же назад. stopMotors() — скорость 0.
Подсказка: Фрагмент кода (3 из 3)

const int BACKWARD = (FORWARD == HIGH) ? LOW : HIGH; — прочитай как «если FORWARD равен HIGH, то LOW, иначе HIGH».

Должно получиться

Программа выполняет требование 3 и проверяется без ошибок.

6

Первый заезд по полу

15 мин
Безопасность

Освободи на полу или столе место не меньше метра вперёд и назад. Держи руку рядом с выключателем.

  1. Загрузи программу на подставке и проверь: вперёд, пауза, назад, стоп.
  2. Поставь робота на пол, включи питание моторов и нажми Reset.
  3. Отметь на полу мелом или скотчем точку старта и точку, где робот остановился.
Должно получиться

Робот проезжает вперёд, останавливается и возвращается примерно на место старта.

Если не получилось
Что видишьЧто сделать
Робот едет не прямоМоторы не одинаковые — это нормально. Запиши, куда уводит.
Робот вернулся не на стартВперёд и назад колёса крутятся чуть по-разному. Запиши разницу.
Робот поехал сразу после загрузкиТак и есть: программа стартует сама. Поэтому в setup() есть пауза 2 секунды.
  1. Загрузи программу на подставке и проверь: вперёд, пауза, назад, стоп.
  2. Поставь робота на пол, отметь старт скотчем, включи питание моторов и нажми Reset.
  3. Повтори три раза. Каждый раз отметь, где робот остановился, и измерь линейкой, на сколько сантиметров он ушёл от старта вперёд-назад и вбок.
ЗаездВперёд или назад, смВбок, смКуда уводит
1
2
3
  1. Одинаково ли робот ошибается в трёх заездах? Запиши вывод.
Должно получиться

Таблица из трёх заездов и вывод: куда и насколько уводит твоего робота.

Если не получилось
Что видишьЧто сделать
Робот едет не прямоМоторы не одинаковые — это нормально. Запиши, куда уводит.
Робот поехал сразу после загрузкиТак и есть: программа стартует сама. Поэтому в setup() есть пауза 2 секунды.

Узнай, насколько прямо едет твой робот, и попробуй это исправить одним изменением.

  1. Проверь программу на подставке, потом поставь робота на пол у начала метровой полосы скотча. Проедь вперёд 2 секунды. Измерь, на сколько сантиметров нос ушёл вбок от полосы. Повтори три раза.
  2. Предложи одно изменение, которое уменьшит увод: например, разные скорости левого и правого колеса. Запиши, почему ты ожидаешь улучшения.
  3. Сделай изменение и снова три заезда. Стало лучше? На сколько?
Увод 1, смУвод 2, смУвод 3, см
До
После
Подсказка: Колёса разные

Если робот уходит вправо, какое колесо крутится быстрее?

Должно получиться

Две серии по три заезда, одно изменение с обоснованием и вывод по числам.

7

Измени программу

10 мин
  1. Сделай так, чтобы робот проехал вперёд в два раза дальше, а назад — как раньше.
  2. Сделай «челнок»: вперёд — назад три раза подряд с паузами.
Проверь себя
  1. Сделай так, чтобы робот проехал вперёд в два раза дальше, а назад — как раньше.
  2. Сделай «челнок» — вперёд, стоп, назад, стоп три раза подряд, — написав вызовы forward, stopMotors, backward только один раз. Нужен цикл for из урока 1.1.
Предскажи

Вернётся ли робот после трёх челноков точно на старт? Запиши прогноз, потом отметь старт и финиш на полу.

Подсказка: Как устроен for

for (int i = 0; i < 3; i++) { … } повторяет команды в скобках три раза.

Должно получиться

Робот выполняет оба задания. В журнале — прогноз и насколько робот ушёл от старта после трёх челноков.

Проверь себя

Требование 4. Функция shuttle(times, ms) делает «челнок» times раз: вперёд ms миллисекунд, стоп, назад, стоп. Вызовы движений в ней написаны один раз.

Исследование: сколько проезжает робот.

  1. Три раза запусти forward(1000) при speed = 100 и измерь путь линейкой в миллиметрах. Повтори при speed = 200.
  2. Посчитай, сколько миллиметров в секунду проезжает робот при каждой скорости. Во сколько раз отличаются результаты? Вдвое ли, как отличаются числа speed?
  3. Подбери время backward так, чтобы после одного челнока робот вставал не дальше 2 см от старта. Запиши, почему вперёд и назад время получилось разным.
speedПуть 1, ммПуть 2, ммПуть 3, мммм за секунду
100
200
Подсказка: Пути разные при одной программе

Это нормально: колёса проскальзывают, батарея садится. Поэтому меряем три раза и берём среднее.

Должно получиться

shuttle работает. В журнале — таблица путей, скорость в мм/с для двух speed, вывод про связь числа speed и настоящей скорости и подобранное время возврата.

Проверь себя
Основное · Вперёд, стоп, назад

Робот проезжает вперёд 1 секунду, останавливается и возвращается назад.

Средний уровень · Челнок без копирования

Сделай «челнок» из трёх повторов, написав вызовы forward, stopMotors, backward только один раз. Подсказка: цикл for из урока 1.1.

Продвинутый уровень · Возврат на старт

Измерь, сколько миллиметров робот проезжает за секунду, и подбери время назад так, чтобы после челнока он вставал не дальше 2 см от старта. Запиши, почему время вперёд и назад разное.

Проверь себя

Выберите ответы для подготовки. Они не сохраняются и не оцениваются на сайте. Пройдите проверку знаний в программе курса или обсудите ответы с преподавателем.

● Основное

1.Что задаёт число в команде analogWrite(LEFT_SPEED_PIN, 100)?
2.Как сделать, чтобы левое колесо крутилось назад?
3.Что сделает робот?
forward(1000);delay(500);
4.Зачем описывать движения функциями forward(), backward(), stopMotors()?
5.Почему в setup() перед движением стоит delay(2000)?

★ Средний уровень

1.Нужно повторить «вперёд — стоп — назад» три раза, написав вызовы функций только один раз. Что подойдёт?
2.Скорость уменьшили со 200 до 100, а время езды вперёд осталось 1 секунда. Что изменится?
3.В программе забыли остановить правый мотор. Что сделает робот после секунды движения?
4.Вызвали forward(1000), а сразу за ним — backward(1000) без stopMotors() между ними. Что получится?
5.После трёх челноков робот оказался в 6 см от старта, а после одного — в 2 см. Почему ошибка растёт?

★★ Продвинутый уровень

1.Робот едет «прямо», но плавно уходит вправо. Что проверить первым?
2.При speed = 30 колесо гудит, но не крутится. Что это значит для программы?
3.Почему направление «назад» получают из FORWARD, а не пишут числом?
const int BACKWARD = (FORWARD == HIGH) ? LOW : HIGH;
4.При speed = 100 робот за 1 секунду проехал 180, 200 и 190 мм. Какую скорость записать в журнал?
5.В чём ошибка функции?
void shuttle(int times, int ms) {  for (int i = 0; i < times; i++) {    forward(ms);    backward(ms);  }  stopMotors();}

Отвечай на вопросы своего уровня. Нужно верных ответов: Начальный — 4 из 5; Средний — 4 из 5; Продвинутый — 4 из 5. Ответы проверит преподаватель или программа курса.

Итог

Робот ездит по твоей программе. Главное: analogWrite включает мотор, и он крутится, пока другая команда его не остановит. Функции делают программу короче и понятнее. В следующем уроке робот научится поворачивать.

Как подтвердить результат

Для каждого опыта сохраняй условия, версию программы, наблюдение и вывод. Сравнивая попытки, меняй одну причину за раз; неудачные попытки тоже остаются в журнале. Отметка результата относится к уже выполненным действиям, повторять их ради отметки не нужно. Если результат не получен, запиши расхождение и следующую проверяемую гипотезу. Модель, измерения настоящего робота и объяснение — разные виды доказательств.

Словарь урока

ШИМЧастое включение и выключение питания мотора. Число от 0 до 255 задаёт, какую долю времени мотор включён.
направлениеСостояние контакта направления: одно значение крутит колесо вперёд, другое — назад.
константаЧисло с именем, которое не меняется во время работы программы, например номер контакта.
функцияИменованный блок команд. Его можно вызвать много раз, написав имя и скобки.
параметрЧисло, которое передают функции в скобках, например время движения.
Тема 2 · Урок 2.2

Повороты и развороты

Робот поворачивает на месте на 90° и разворачивается на 180°. Время поворота подбираешь сам.

2 × 45 мин1–2 ученика на роботаПрактика

Результат урока

Робот поворачивает на 90° налево и направо и разворачивается на 180°. Время поворотов подобрано и записано в бортовой журнал, повороты оформлены функциями.

Ты научишься

  • Объяснить, почему робот крутится на месте, когда колёса вращаются в разные стороны.
  • Подобрать время поворота опытом и проверить его повторением.
  • Написать функции turnLeft(), turnRight() и turnAround().
  • Предсказать, куда приедет робот по последовательности движений.

Нужно знать

  • 2.1 Моторы: едем и останавливаемся

Понадобится

Мотор-редуктор × 2Шилд моторов L298P (Амперка)Батарейный отсек моторов с выключателем

Зачем этот урок

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

Разминка · вспомни
  1. Какая команда задаёт направление вращения колеса?
  2. Что будет, если включить моторы и не написать stopMotors()?
  3. Чем описание функции отличается от её вызова?
1
Занятие 1 · 45 мин
Поворот на месте
1

Колёса в разные стороны

10 мин

Что должен делать робот. Курьеру нужно менять направление: повернуть к ответвлению и развернуться для обратной поездки. Поворот получим теми же двумя моторами, без отдельного рулевого механизма.

Что используем и как это работает. Когда колёса проходят разные расстояния, корпус поворачивается. Если одно колесо вращается вперёд, а другое назад, робот разворачивается примерно вокруг середины между колёсами. Функция поворота объединяет направления колёс, время вращения и остановку.

Как связать измерение с действием. В этом уроке угол задаём временем работы моторов. Это приближение: заряд батареи, покрытие и проскальзывание меняют результат. Измерим получившийся угол, подберём время и повторим опыт. Имя turnLeft само по себе не гарантирует 90 градусов — это должна подтвердить проверка.

Предскажи

Левое колесо крутится назад, правое — вперёд с той же скоростью. Что сделает робот? Покажи руками.

  1. Набери скетч L22_Turn_Test и загрузи его при выключенном питании моторов.
Проба поворотаL22_Turn_Test.ino.ino
const int LEFT_DIR_PIN = 7;const int LEFT_SPEED_PIN = 6;const int RIGHT_DIR_PIN = 4;const int RIGHT_SPEED_PIN = 5;const int FORWARD = HIGH;const int BACKWARD = (FORWARD == HIGH) ? LOW : HIGH; int turnSpeed = 100;int turnMs = 400; // подбираем: время поворота на 90° void drive(int leftDir, int leftSpeed, int rightDir, int rightSpeed) {  digitalWrite(LEFT_DIR_PIN, leftDir);  digitalWrite(RIGHT_DIR_PIN, rightDir);  analogWrite(LEFT_SPEED_PIN, leftSpeed);  analogWrite(RIGHT_SPEED_PIN, rightSpeed);} void stopMotors() {  analogWrite(LEFT_SPEED_PIN, 0);  analogWrite(RIGHT_SPEED_PIN, 0);} void setupMotors() {  pinMode(LEFT_DIR_PIN, OUTPUT);  pinMode(LEFT_SPEED_PIN, OUTPUT);  pinMode(RIGHT_DIR_PIN, OUTPUT);  pinMode(RIGHT_SPEED_PIN, OUTPUT);  stopMotors();} void setup() {  setupMotors();  delay(2000);   // Левое колесо назад, правое вперёд: робот крутится на месте налево.  drive(BACKWARD, turnSpeed, FORWARD, turnSpeed);  delay(turnMs);  stopMotors();} void loop() {}
  1. Поставь робота на пол, на лист бумаги. Прочерти карандашом линию вдоль его носа.
  2. Включи питание моторов и нажми Reset.
Должно получиться

Робот крутится на месте налево и останавливается. Нос повернулся, но, скорее всего, не ровно на 90°.

Как это устроено: Почему робот крутится на месте

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

Предскажи

Робот должен повернуть налево на месте. Какое колесо должно крутиться вперёд, а какое назад? Запиши до запуска.

  1. Создай скетч L22_Turn_Test по шаблону. Функции drive и stopMotors из урока 2.1 уже есть. Напиши поворот в setup() по своему прогнозу.
Шаблон: проба поворотаL22_Turn_Test_Gaps.ino.ino
// Шаблон среднего уровня. Напиши код в разделе «Твоя часть» по целям в комментариях. const int LEFT_DIR_PIN = 7;const int LEFT_SPEED_PIN = 6;const int RIGHT_DIR_PIN = 4;const int RIGHT_SPEED_PIN = 5;const int FORWARD = HIGH;const int BACKWARD = (FORWARD == HIGH) ? LOW : HIGH;int turnSpeed = 100;int turnMs = 400; // подбираем: время поворота на 90° // ===== Твоя часть: напиши сам ===== void setup() {  setupMotors();  delay(2000);   // Робот крутится на месте налево turnMs миллисекунд и останавливается.} // ===== Готовое из урока 2.1 — не меняй ===== void drive(int leftDir, int leftSpeed, int rightDir, int rightSpeed) {  digitalWrite(LEFT_DIR_PIN, leftDir);  digitalWrite(RIGHT_DIR_PIN, rightDir);  analogWrite(LEFT_SPEED_PIN, leftSpeed);  analogWrite(RIGHT_SPEED_PIN, rightSpeed);} void stopMotors() {  analogWrite(LEFT_SPEED_PIN, 0);  analogWrite(RIGHT_SPEED_PIN, 0);} void setupMotors() {  pinMode(LEFT_DIR_PIN, OUTPUT);  pinMode(LEFT_SPEED_PIN, OUTPUT);  pinMode(RIGHT_DIR_PIN, OUTPUT);  pinMode(RIGHT_SPEED_PIN, OUTPUT);  stopMotors();} void loop() {}
  1. Поставь робота на пол, на лист бумаги. Прочерти карандашом линию вдоль его носа. Включи питание моторов и нажми Reset.
  2. Измени программу так, чтобы робот повернул направо. Проверь.
Подсказка: Робот едет, а не крутится

Если оба колеса крутятся в одну сторону, робот едет. Для поворота на месте нужны разные направления.

Должно получиться

Робот крутится на месте налево, а после твоего изменения — направо. Прогноз проверен.

Требование. В своей программе L21_Moves из урока 2.1 сделай пробу поворота: колёса крутятся в разные стороны с одной скоростью turnSpeed в течение turnMs, потом робот останавливается. Сохрани как L22_Turn_Test.

Исследование: вокруг чего крутится робот.

  1. Положи робота на лист бумаги и обведи карандашом оба колеса.
  2. Запусти поворот и обведи колёса снова. Найди точку, вокруг которой повернулся робот.
  3. Совпадает ли она с серединой между колёсами? Если нет — насколько сантиметров и почему?
Подсказка: Как найти точку поворота

Соедини старое и новое положение каждого колеса. Точка поворота лежит там, где эти положения одинаково далеко.

Должно получиться

Проба поворота работает. В журнале — рисунок, точка поворота и объяснение, почему она там.

2

Подбери время поворота на 90°

20 мин

На поворот 90° нужно какое-то время в миллисекундах. Узнать его можно только опытом.

  1. Начерти на листе две линии под прямым углом (угольник или край тетради).
  2. Поставь робота носом вдоль первой линии. Запусти программу.
  3. Если робот не довернул — увеличь turnMs, если перевернул — уменьши. Меняй понемногу: на 20–50 мс.
  4. Когда поворот получился, повтори его три раза. Запиши время в журнал.
ПопыткаturnMsЧто получилось
1400
2
3
Должно получиться

Робот три раза подряд поворачивает примерно на 90°. Время записано в журнале.

Подсказка: Каждый раз поворачивает по-разному

Проверь, что батарея заряжена: от разряженной моторы крутятся медленнее. Пол должен быть ровным и не скользким. Небольшая разница между попытками — нормально.

На поворот 90° нужно какое-то время в миллисекундах. Узнать его можно только опытом. Как подбирать — реши сам, но меняй только turnMs и записывай каждую попытку.

  1. Начерти на листе две линии под прямым углом. Поставь робота носом вдоль первой.
  2. Подбирай turnMs, пока робот не повернёт на 90°. В журнале — не меньше четырёх попыток и объяснение, почему ты выбрал следующее число.
  3. Когда поворот получился, повтори его три раза.
ПопыткаturnMsЧто получилосьЧто поменять дальше и почему
1
2
3
4
Подсказка: С какого числа начать

Посмотри, сколько повернулся робот в пробе с turnMs = 400. Больше 90° или меньше?

Подсказка: Каждый раз поворачивает по-разному

Проверь, что батарея заряжена, а пол ровный и не скользкий. Небольшая разница между попытками — нормально.

Должно получиться

Робот три раза подряд поворачивает примерно на 90°. В журнале четыре попытки с объяснением и итоговое время.

Подбери время поворота методом и узнай, насколько ему можно доверять.

  1. Найди одно время, при котором робот явно не доворачивает, и одно, при котором явно перекручивает. Пробуй середину между ними и сужай промежуток, пока ошибка не станет меньше 5°.
  2. С найденным временем сделай пять поворотов подряд. Для каждого отметь на листе, куда смотрит нос, и измерь транспортиром ошибку в градусах.
  3. Повтори пункт 2 при другой turnSpeed (например, 160). При какой скорости повороты точнее и одинаковее?
  4. Повернёт ли робот направо за то же время? Проверь и запиши.
turnSpeedturnMsОшибки 5 поворотов, °Самая большая ошибка, °
100
160
Подсказка: Как измерить ошибку без транспортира

Сравни отметку носа с линией 90° на расстоянии 10 см от центра поворота: 1 см в сторону — это примерно 6°.

Должно получиться

В журнале — способ поиска, время для двух скоростей, ошибки пяти поворотов и вывод: какую turnSpeed брать и одинаково ли время налево и направо.

3

Разворот на 180°

15 мин
Предскажи

Если на 90° нужно, например, 400 мс, сколько нужно на 180°? Запиши свой прогноз.

  1. Поставь в turnMs своё время, умноженное на 2. Проверь разворот.
  2. Подправь число, если нужно. Запиши время разворота в журнал.
Должно получиться

Робот разворачивается и смотрит в обратную сторону. Время разворота записано.

Как это устроено: Почему ровно в два раза не всегда получается

В начале поворота моторам нужно время, чтобы разогнаться, а в конце робот немного проскальзывает по инерции. Поэтому время на 180° может быть чуть меньше двух поворотов на 90°. Опыт важнее расчёта.

Предскажи

Сколько миллисекунд нужно на разворот на 180°, если ты знаешь своё время на 90°? Запиши прогноз.

  1. Проверь прогноз на полу и подправь число, если нужно. Запиши время разворота.
  2. Ответь в журнале: получилось ровно вдвое больше, чем на 90°? Если нет, как ты это объяснишь?
Подсказка: Почему не ровно вдвое

Подумай, что происходит в начале поворота, когда колёса только трогаются, и в конце, когда моторы уже выключены.

Должно получиться

Робот разворачивается и смотрит в обратную сторону. Время разворота и объяснение записаны.

  1. Предскажи время разворота по своему времени на 90° и проверь на полу. Подбери точное время тем же методом, что и для 90°.
  2. Сравни: во сколько раз время на 180° больше времени на 90°? Объясни в журнале, откуда берётся разница. Назови две причины.
  3. Выведи правило: сколько миллисекунд нужно на один градус при развороте и при повороте на 90°? Какое из чисел точнее подойдёт для поворота на 45°? Проверь.
Должно получиться

Время разворота подобрано. В журнале — сравнение с удвоенным поворотом на 90°, две причины разницы и проверка правила на 45°.

2
Занятие 2 · 45 мин
Функции поворотов
4

Функции поворотов

12 мин
  1. Создай скетч L22_Turns. Впиши свои времена из журнала в turn90Ms и turn180Ms.
Функции поворотовL22_Turns.ino.ino
const int LEFT_DIR_PIN = 7;const int LEFT_SPEED_PIN = 6;const int RIGHT_DIR_PIN = 4;const int RIGHT_SPEED_PIN = 5;const int FORWARD = HIGH;const int BACKWARD = (FORWARD == HIGH) ? LOW : HIGH; int speed = 100;int turnSpeed = 100;int turn90Ms = 400;  // впиши своё время из журналаint turn180Ms = 800; // впиши своё время из журнала void drive(int leftDir, int leftSpeed, int rightDir, int rightSpeed) {  digitalWrite(LEFT_DIR_PIN, leftDir);  digitalWrite(RIGHT_DIR_PIN, rightDir);  analogWrite(LEFT_SPEED_PIN, leftSpeed);  analogWrite(RIGHT_SPEED_PIN, rightSpeed);} void stopMotors() {  analogWrite(LEFT_SPEED_PIN, 0);  analogWrite(RIGHT_SPEED_PIN, 0);} void setupMotors() {  pinMode(LEFT_DIR_PIN, OUTPUT);  pinMode(LEFT_SPEED_PIN, OUTPUT);  pinMode(RIGHT_DIR_PIN, OUTPUT);  pinMode(RIGHT_SPEED_PIN, OUTPUT);  stopMotors();} void forward(int ms) {  drive(FORWARD, speed, FORWARD, speed);  delay(ms);  stopMotors();} void turnLeft() {  drive(BACKWARD, turnSpeed, FORWARD, turnSpeed);  delay(turn90Ms);  stopMotors();} void turnRight() {  drive(FORWARD, turnSpeed, BACKWARD, turnSpeed);  delay(turn90Ms);  stopMotors();} void turnAround() {  drive(BACKWARD, turnSpeed, FORWARD, turnSpeed);  delay(turn180Ms);  stopMotors();} void setup() {  setupMotors();  delay(2000);   for (int i = 0; i < 4; i++) {    forward(1000);    delay(300);    turnLeft();    delay(300);  }} void loop() {}
  1. Найди разницу между turnLeft() и turnRight(). Что поменялось местами?
Должно получиться

Программа проверяется без ошибок. Ты можешь объяснить, чем отличаются функции поворота.

  1. Создай скетч L22_Turns по шаблону. Впиши свои времена из журнала в turn90Ms и turn180Ms.
  2. Напиши turnLeft, turnRight, turnAround и квадрат в setup() сам. Функция forward из урока 2.1 уже есть — пользуйся ей как образцом.
Шаблон: функции поворотовL22_Turns_Gaps.ino.ino
// Шаблон среднего уровня. Напиши код в разделе «Твоя часть» по целям в комментариях. const int LEFT_DIR_PIN = 7;const int LEFT_SPEED_PIN = 6;const int RIGHT_DIR_PIN = 4;const int RIGHT_SPEED_PIN = 5;const int FORWARD = HIGH;const int BACKWARD = (FORWARD == HIGH) ? LOW : HIGH;int speed = 100;int turnSpeed = 100;int turn90Ms = 400;  // впиши своё время из журналаint turn180Ms = 800; // впиши своё время из журнала // ===== Твоя часть: напиши сам ===== void turnLeft() {  // Поворот на месте налево на 90° по времени turn90Ms; в конце колёса стоят.} void turnRight() {  // Поворот на месте направо на 90° по времени turn90Ms; в конце колёса стоят.} void turnAround() {  // Разворот на месте на 180° по времени turn180Ms; в конце колёса стоят.} void setup() {  setupMotors();  delay(2000);   // Квадрат: четыре раза — вперёд 1 секунду, пауза, поворот налево, пауза.} // ===== Готовое из урока 2.1 — не меняй ===== void drive(int leftDir, int leftSpeed, int rightDir, int rightSpeed) {  digitalWrite(LEFT_DIR_PIN, leftDir);  digitalWrite(RIGHT_DIR_PIN, rightDir);  analogWrite(LEFT_SPEED_PIN, leftSpeed);  analogWrite(RIGHT_SPEED_PIN, rightSpeed);} void stopMotors() {  analogWrite(LEFT_SPEED_PIN, 0);  analogWrite(RIGHT_SPEED_PIN, 0);} void setupMotors() {  pinMode(LEFT_DIR_PIN, OUTPUT);  pinMode(LEFT_SPEED_PIN, OUTPUT);  pinMode(RIGHT_DIR_PIN, OUTPUT);  pinMode(RIGHT_SPEED_PIN, OUTPUT);  stopMotors();} void forward(int ms) {  drive(FORWARD, speed, FORWARD, speed);  delay(ms);  stopMotors();} void loop() {}
  1. Нажми Проверить.
Подсказка: Чем отличаются повороты

Сравни свою пробу поворота: какие колёса и в какую сторону крутятся налево, а какие — направо? А при развороте?

Подсказка: Четыре раза

Повторить одно и то же четыре раза помогает цикл for из урока 1.1.

Должно получиться

Программа проверяется без ошибок. Ты можешь объяснить, чем отличаются функции поворота.

Требование. Продолжи свою программу L21_Moves из урока 2.1 и сохрани как L22_Turns:

  • turnLeft(), turnRight(), turnAround() — повороты на 90° и разворот по твоим временам, каждый заканчивается остановкой;
  • времена — именованные переменные в начале скетча; если в шаге подбора время налево и направо получилось разным, у каждого поворота своя переменная;
  • направление и скорость колёс задаёт только drive;
  • в setup() — квадрат для следующего шага: четыре раза «вперёд 1 секунду, налево».
Подсказка: Идея (1 из 2)

Поворот — это drive с колёсами в разные стороны, ожидание и остановка.

Подсказка: Фрагмент кода (2 из 2)

drive(BACKWARD, turnSpeed, FORWARD, turnSpeed); — так робот крутится налево. Что поменять для поворота направо?

Должно получиться

Программа выполняет требование и проверяется без ошибок.

5

Квадрат

18 мин
Предскажи

Программа повторяет 4 раза: «вперёд 1 секунду, налево». Где окажется робот в конце и куда будет смотреть? Нарисуй путь в журнале.

  1. Отметь старт скотчем. Запусти программу на полу.
  2. Сравни, где робот остановился, с прогнозом.
  3. Насколько робот не доехал до старта? Запиши.
Должно получиться

Робот проезжает квадрат и возвращается близко к старту, носом в ту же сторону.

Найди ошибку · Квадрат превратился в линию

У соседей робот едет вперёд, крутится, но квадрат не получается: он ездит туда-обратно. В их turnLeft() стоит delay(turn180Ms). Что не так?

Предскажи

Программа повторяет 4 раза: «вперёд 1 секунду, налево». Нарисуй путь в журнале. Где окажется робот в конце и куда будет смотреть?

  1. Отметь старт скотчем. Запусти программу на полу три раза.
  2. Каждый раз измерь, на сколько сантиметров робот не доехал до старта, и на сколько градусов примерно повёрнут его нос.
  3. Объясни в журнале, почему ошибка в конце больше, чем ошибка одного поворота.
Должно получиться

Три заезда с замерами и объяснение накопления ошибки.

Найди ошибку · Квадрат превратился в линию

У соседей робот едет вперёд, крутится, но квадрат не получается: он ездит туда-обратно. В их turnLeft() стоит delay(turn180Ms). Что не так?

  1. Запусти квадрат три раза. Каждый раз измерь, насколько робот промахнулся мимо старта (в сантиметрах) и насколько повёрнут нос (в градусах).
  2. Оцени ошибку одного поворота: раздели ошибку направления в конце на четыре. Совпадает ли она с ошибкой, которую ты мерил при подборе времени?
  3. Что сильнее портит квадрат: ошибка поворотов или увод на прямой? Как это проверить отдельным опытом? Проведи его и запиши вывод.
Найди ошибку · Квадрат превратился в линию

У соседей робот едет вперёд, крутится, но квадрат не получается: он ездит туда-обратно. В их turnLeft() стоит delay(turn180Ms). Что не так?

Должно получиться

Замеры трёх квадратов, оценка ошибки одного поворота и опыт, который показал главную причину промаха.

6

Задания по уровням

15 мин
Основное · Поворот и разворот

Робот поворачивает налево и направо на 90° и разворачивается на 180° по твоим временам.

Средний уровень · Разворот обратно на линию

Наклей на пол полоску изоленты. Поставь робота на неё, сделай разворот на 180°. Робот должен снова стоять вдоль полоски, а нос — уходить от неё не дальше её ширины. Так разворот будет работать в итоговой задаче.

Продвинутый уровень · Поворот на любой угол

Напиши функцию turn(int degrees): положительный угол — налево, отрицательный — направо. Время считай как degrees × msPerDegree, где msPerDegree = turn90Ms / 90. Проверь на 45°, 90° и 180°. Где точность хуже всего и почему?

Проверь себя

Проверь себя

Выберите ответы для подготовки. Они не сохраняются и не оцениваются на сайте. Пройдите проверку знаний в программе курса или обсудите ответы с преподавателем.

● Основное

1.Левое колесо крутится вперёд, правое — назад с той же скоростью. Что делает робот?
2.Робот не доворачивает до 90°. Что изменить?
3.Почему время поворота подбирают опытом, а не берут из учебника?
4.Программа: 4 раза «вперёд, налево». Где окажется робот?
5.Батарея разрядилась. Что, скорее всего, случится с поворотами по времени?

★ Средний уровень

1.Разворот на 180° длится 900 мс, но робот немного недоворачивает. Что сделать?
2.Зачем проверять разворот на полоске изоленты?
3.При turnMs = 400 робот повернул на 70°. Какое число попробовать следующим?
4.Почему разворот на 180° может длиться не ровно вдвое дольше поворота на 90°?
5.Чем turnRight() отличается от turnLeft()?

★★ Продвинутый уровень

1.turn90Ms = 600. Сколько миллисекунд займёт turn(45), если msPerDegree = turn90Ms / 90?
2.Как функция turn(int degrees) понимает, куда поворачивать?
3.Ошибки пяти поворотов: при turnSpeed = 100 — +4°, −6°, +5°, −3°, +2°; при 160 — +1°, −2°, +2°, 0°, −1°. Какую скорость выбрать?
4.При 300 мс робот явно не доворачивает, при 500 мс — явно перекручивает. Что попробовать дальше?
5.turn90Ms = 400. Для 45° по коэффициенту из 90° вышло 200 мс, а робот повернул только на 35°. Почему?

Отвечай на вопросы своего уровня. Нужно верных ответов: Начальный — 4 из 5; Средний — 4 из 5; Продвинутый — 4 из 5. Ответы проверит преподаватель или программа курса.

Итог

Робот поворачивает по времени, которое ты подобрал опытом. Это простой способ, но он зависит от батареи и пола. В уроке 4.2 робот научится поворачивать точнее — по датчикам: крутиться, пока не найдёт новую линию.

Как подтвердить результат

Для каждого опыта сохраняй условия, версию программы, наблюдение и вывод. Сравнивая попытки, меняй одну причину за раз; неудачные попытки тоже остаются в журнале. Отметка результата относится к уже выполненным действиям, повторять их ради отметки не нужно. Если результат не получен, запиши расхождение и следующую проверяемую гипотезу. Модель, измерения настоящего робота и объяснение — разные виды доказательств.

Словарь урока

поворот на местеКолёса крутятся в разные стороны, и робот вращается вокруг середины между колёсами.
калибровкаПодбор числа по опыту: например, сколько миллисекунд нужно на поворот 90°.
погрешностьНасколько результат отличается от нужного: например, робот повернул на 85° вместо 90°.
Тема 3 · Урок 3.1

Датчики линии и первое следование

Робот «смотрит» на пол, отличает чёрное от белого и сам держится линии.

2 × 45 мин1–2 ученика на роботаПрактика

Результат урока

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

Ты научишься

  • Прочитать показания датчика командой analogRead и вывести их в монитор порта.
  • Различать строки Serial Monitor и каналы телеметрии с временем и единицами; сравнивать сохранённые показания модели и настоящего робота.
  • Измерить показания на белом и чёрном и объяснить, почему у каждого датчика они свои.
  • Записать правило «показание ближе к чёрному — значит, под датчиком линия».
  • Составить два правила поворота и проверить их сначала на подставке, затем на дорожке.

Нужно знать

  • 1.1 Собираем робота и оживляем плату
  • 2.1 Моторы: едем и останавливаемся

Понадобится

Датчик линии (аналоговый, Trema) × 2USB-кабель для Arduino

Зачем этот урок

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

Разминка · вспомни
  1. Какая команда задаёт скорость мотора и какие числа она принимает?
  2. Что делает delay(500)?
  3. Какое колесо надо остановить, чтобы робот повернул налево?
Предскажи

Как робот может «увидеть» чёрную линию, если у него нет камеры? Обсудите с напарником и запишите догадку в бортовой журнал. В конце урока проверите.

Безопасность

Пока меняешь провода, USB-кабель отключён, питание моторов выключено. Опыты с моторами — сначала на подставке: колёса не касаются стола.

1
Занятие 1 · 45 мин
Датчики и их показания
1

Подготовь дорожку и робота

5 мин

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

Что используем и как это работает. В датчике есть излучатель и приёмник отражённого света. Поверхности отражают свет по-разному, а приёмник превращает отражённый свет в электрический сигнал. analogRead считывает его как число. Это не готовый ответ «чёрное»: сначала измеряем белое и чёрное для каждого датчика при рабочей высоте.

Как связать измерение с действием. Программа сравнивает чтение с измеренным порогом и выбирает действие колёс. Над линией слева и справа должны получиться разные команды коррекции. Проверим четыре сочетания карточек на подставке, затем движение по линии. Одинаковые показания обоих датчиков ещё не доказывают, что робот стоит правильно: оба могут оказаться и на белом, и на чёрном.

Датчику нужна понятная картинка: светлый фон и тёмная линия.

  1. Возьми светлое матовое поле с чёрной линией (или наклей чёрную матовую изоленту на белый лист). Для начала хватит прямой с одним плавным изгибом.
  2. Подготовь две карточки: белую и чёрную.
  3. Найди на роботе левый и правый датчики. Они смотрят вниз и стоят по обе стороны от линии, когда робот стоит на ней.
Должно получиться

Дорожка и карточки готовы. Ты можешь показать левый и правый датчик.

Датчику нужна понятная картинка: светлый фон и тёмная линия. Для заданий среднего уровня на дорожке понадобится и перекрёсток.

  1. Возьми светлое матовое поле с чёрной линией или наклей чёрную матовую изоленту на белый лист: прямая, плавный изгиб и один перекрёсток.
  2. Подготовь две карточки: белую и чёрную.
  3. Найди на роботе левый и правый датчики. Поставь робота на линию и проверь: линия проходит между датчиками и не касается ни одного из них?
Должно получиться

Дорожка с перекрёстком и карточки готовы. Линия помещается между датчиками.

Подготовь дорожку для своего уровня и проверь, подходит ли она твоему роботу.

  1. Сделай дорожку: прямая, плавный изгиб, крутая дуга и перекрёсток. Подготовь белую и чёрную карточки.
  2. Измерь линейкой ширину линии и расстояние между датчиками (между серединами окошек).
  3. Поставь робота на линию. Сколько миллиметров остаётся от края линии до каждого датчика? Запиши в журнал: что будет, если линия шире расстояния между датчиками?
Должно получиться

Дорожка готова, в журнале — ширина линии, расстояние между датчиками, запас с каждой стороны и ответ на вопрос.

2

Посмотри, что видит датчик

12 мин

Датчик светит на пол и меряет, сколько света вернулось. Плата превращает это в число.

Чтобы увидеть число, плата будет отправлять его на компьютер, а мы откроем монитор порта.

  1. Создай скетч L31_Read_Sensors и набери программу. Левый датчик подключён к A4, правый — к A5.
Показания двух датчиковL31_Read_Sensors.ino.ino
const int LEFT_SENSOR_PIN = A4;const int RIGHT_SENSOR_PIN = A5; void setup() {  Serial.begin(9600);} void loop() {  int leftValue = analogRead(LEFT_SENSOR_PIN);  int rightValue = analogRead(RIGHT_SENSOR_PIN);   Serial.print("L=");  Serial.print(leftValue);  Serial.print("  R=");  Serial.println(rightValue);   delay(200);}
  1. Загрузи программу. Открой Инструменты → Монитор порта и выбери скорость 9600.
  2. Поднеси под левый датчик белую карточку, затем чёрную. Держи карточку на той же высоте, на которой датчик стоит над полом.
Должно получиться

В мониторе бегут строки вида L=… R=…. Когда меняешь карточку под датчиком, его число заметно меняется.

Как это устроено: Что значит каждая строка
  • analogRead(LEFT_SENSOR_PIN) — прочитать показание датчика: число от 0 до 1023.
  • int leftValue = … — сохранить число в переменную leftValue. int — целое число.
  • Serial.begin(9600) — начать связь с компьютером. 9600 — скорость, такая же должна быть выбрана в мониторе порта.
  • Serial.print печатает в той же строке, Serial.println — печатает и переходит на новую.
  • Текст в кавычках печатается как есть, а имя переменной без кавычек заменяется её числом.
Если не получилось
Что видишьЧто сделать
Вместо чисел непонятные символыВ мониторе порта выбери 9600.
Число не меняетсяПроверь провод сигнала S и номер входа в программе.
Меняется не тот датчикПровода левого и правого датчиков перепутаны. Поменяй их местами или номера в программе.

Датчик светит на пол и меряет, сколько света вернулось. Плата превращает это в число, а чтобы увидеть его, программа отправляет число на компьютер.

КомандаЧто делает
analogRead(контакт)Показание датчика: число от 0 до 1023
Serial.begin(9600);Включает связь с компьютером, один раз
Serial.print(x);Печатает в той же строке
Serial.println(x);Печатает и переходит на новую строку
  1. Создай скетч L31_Read_Sensors по шаблону и напиши обе части сам. Левый датчик подключён к A4, правый — к A5.
Шаблон: показания двух датчиковL31_Read_Sensors_Gaps.ino.ino
// Шаблон среднего уровня. Напиши код в разделе «Твоя часть» по целям в комментариях. const int LEFT_SENSOR_PIN = A4;const int RIGHT_SENSOR_PIN = A5; // ===== Твоя часть: напиши сам ===== void setup() {  // Включи связь с компьютером на той же скорости, что в мониторе порта: 9600.} void loop() {  // Пять раз в секунду печатай одну строку: L=число левого датчика  R=число правого.}
  1. Загрузи программу и открой монитор порта со скоростью 9600.
  2. Поднеси под левый датчик белую карточку, затем чёрную. Держи карточку на той же высоте, на которой датчик стоит над полом.
Предскажи

Что напечатает монитор, если имя переменной в Serial.print взять в кавычки?

Подсказка: Текст и число в одной строке

Текст в кавычках печатается как есть, а имя переменной без кавычек заменяется её числом.

Должно получиться

В мониторе бегут строки вида L=… R=…. Когда меняешь карточку под датчиком, его число заметно меняется.

Если не получилось
Что видишьЧто сделать
Вместо чисел непонятные символыСкорость в мониторе порта и в программе должна совпадать: 9600.
Все числа в одной длинной строкеНет перехода на новую строку.
Меняется не тот датчикПровода левого и правого датчиков перепутаны.

Датчик светит на пол и меряет, сколько света вернулось. Левый датчик подключён к A4, правый — к A5.

Как это устроено: Справка по командам
КомандаЧто делает
analogRead(pin)Показание датчика: число от 0 до 1023
Serial.begin(9600)Включает связь с компьютером, один раз в setup()
Serial.print(x) / Serial.println(x)Печатает в монитор порта; println — с переходом строки
millis()Сколько миллисекунд прошло с включения платы

Требование. Скетч L31_Read_Sensors пять раз в секунду печатает строку t=… L=… R=…, где t — время в миллисекундах с включения.

Приёмка.

  1. По числам t в мониторе проверь, что строки идут примерно каждые 200 мс. На сколько миллисекунд отличается промежуток от 200 и почему?
  2. Поднеси под левый датчик белую, затем чёрную карточку на рабочей высоте. Запиши, на сколько меняется число.
Подсказка: Почему промежуток не ровно 200

Сколько времени занимают сами чтение и печать, кроме delay(200)?

Должно получиться

Программа выполняет требование. В журнале — реальный промежуток между строками с объяснением и изменение числа на белом и чёрном.

3

Проверь оба датчика

8 мин
Предскажи

Будут ли числа левого и правого датчиков одинаковыми над одним и тем же белым полем?

  1. Поставь робота на белое поле. Запомни числа L и R.
  2. Подведи чёрную карточку только под левый датчик, потом только под правый.
  3. Сдвинь робота так, чтобы линия была точно между датчиками. Какие числа сейчас?
Должно получиться

Ты знаешь, какое число у каждого датчика на белом и как оно меняется на чёрном. Когда линия между датчиками, оба показывают «белое».

Предскажи

Будут ли числа левого и правого датчиков одинаковыми над одним и тем же белым полем?

  1. Заполни таблицу для четырёх положений робота:
ПоложениеLR
Оба над белым
Чёрное только под левым
Чёрное только под правым
Линия точно между датчиками
  1. Почему в последней строке оба числа «белые», хотя робот стоит на линии? Запиши.
Должно получиться

Таблица заполнена, прогноз проверен, ответ про положение «между датчиками» записан.

Сними профиль датчиков: как меняются числа, пока линия проходит под роботом поперёк.

  1. Положи робота поперёк линии так, чтобы линия была далеко справа от правого датчика.
  2. Сдвигай робота влево шагами по 5 мм, пока линия не уйдёт за левый датчик. На каждом шаге записывай L и R.
Сдвиг, ммLR
0
5
…
  1. Нарисуй в журнале два графика: L и R от сдвига. Найди по графику, где линия ровно между датчиками и на сколько миллиметров можно уйти в сторону, пока датчик её не заметит.
Должно получиться

Таблица и два графика. В журнале — ширина «слепой зоны» между датчиками в миллиметрах.

4

Измерь белое и чёрное

12 мин

Датчики не одинаковые: даже над одним полем их числа разные. Поэтому каждому датчику нужны свои измерения. Такая настройка по своим измерениям называется калибровкой.

  1. Поставь робота на белое поле. Посмотри на несколько строк и запиши типичное число левого датчика в бортовой журнал: «Левый, белое».
  2. Подведи под левый датчик чёрную линию. Запиши «Левый, чёрное».
  3. Повтори для правого датчика.
  4. Проверь себя: повтори каждое измерение ещё раз. Числа должны быть близки к записанным.
ДатчикБелоеЧёрное
Левый
Правый
Должно получиться

В журнале четыре числа. Белое и чёрное у каждого датчика заметно отличаются.

Подсказка: Белое и чёрное почти одинаковые

Датчик может стоять слишком высоко или линия блестит. Опусти датчики ближе к полу и проверь линию в другом месте поля, где нет бликов.

Как это устроено: Почему у одних датчиков чёрное — большое число, а у других — маленькое

Это зависит от устройства датчика. Одни выдают тем большее число, чем больше света вернулось, другие — наоборот. Поэтому мы не запоминаем «чёрное — это много», а измеряем свои числа.

Датчики не одинаковые: даже над одним полем их числа разные. Поэтому каждому датчику нужны свои измерения — калибровка.

  1. Для каждого датчика запиши по три показания на белом и три на чёрном, в разных местах дорожки.
  2. Выбери типичное число для каждой клетки и запиши, почему именно его.
ДатчикБелое: 3 числаБелоеЧёрное: 3 числаЧёрное
Левый
Правый
Подсказка: Числа в разных местах разные

Освещение и поле неодинаковые. Берут число, близкое к остальным, а не самое удачное.

Должно получиться

В журнале таблица с повторами и четыре выбранных числа. Белое и чёрное заметно отличаются.

Откалибруй датчики и докажи, что калибровке можно доверять.

  1. Для каждого датчика запиши по 10 показаний на белом и 10 на чёрном. Найди наименьшее и наибольшее в каждой серии — это разброс.
  2. Сравни разброс с разницей белого и чёрного. Во сколько раз разница больше разброса?
  3. Повтори белое у окна и в тёмном углу класса. Меняются ли числа сильнее разброса?
  4. Запиши вывод: где можно доверять калибровке и когда её нужно повторить.
ДатчикБелое: мин–максЧёрное: мин–максРазница типичных
Левый
Правый
Должно получиться

Таблица разбросов, сравнение с разницей и вывод, когда калибровку повторять.

2
Занятие 2 · 45 мин
Робот едет по линии
5

Научи программу замечать линию

10 мин

Как программе решить, что под датчиком линия? Сравним новое показание с нашими двумя измерениями: если число ближе к чёрному, чем к белому, — под датчиком линия.

Пример: белое — 100, чёрное — 800. Пришло число 650. До чёрного 150, до белого 550. Ближе к чёрному — значит, линия.

  1. Создай скетч L31_Line_Detect. Впиши свои четыре числа вместо нулей.
Программа замечает линиюL31_Line_Detect.ino.ino
const int LEFT_SENSOR_PIN = A4;const int RIGHT_SENSOR_PIN = A5; // Впиши свои измерения из бортового журнала.const int LEFT_WHITE = 0;const int LEFT_BLACK = 0;const int RIGHT_WHITE = 0;const int RIGHT_BLACK = 0; void setup() {  Serial.begin(9600);} void loop() {  int leftValue = analogRead(LEFT_SENSOR_PIN);  int rightValue = analogRead(RIGHT_SENSOR_PIN);   bool leftOnLine = abs(leftValue - LEFT_BLACK) < abs(leftValue - LEFT_WHITE);  bool rightOnLine = abs(rightValue - RIGHT_BLACK) < abs(rightValue - RIGHT_WHITE);   Serial.print("L=");  Serial.print(leftValue);  Serial.print("  R=");  Serial.print(rightValue);   if (leftOnLine) {    Serial.print("  слева линия");  }  if (rightOnLine) {    Serial.print("  справа линия");  }   Serial.println();  delay(200);}
  1. Загрузи программу, открой монитор порта и подводи чёрную карточку под датчики по очереди.
Должно получиться

Когда под левым датчиком чёрное, в строке появляется «слева линия», под правым — «справа линия».

Найди ошибку · Почему сообщение не появляется

Команда соседей написала так:

bool leftOnLine = abs(leftValue - LEFT_BLACK) > abs(leftValue - LEFT_WHITE);

Под левым датчиком чёрная карточка, а «слева линия» не печатается. Найди ошибку и объясни, что программа проверяет сейчас.

Как это устроено: Что такое abs и bool
  • abs(число) убирает знак: abs(-150) равно 150. Так мы получаем расстояние между двумя числами, не думая, какое из них больше.
  • bool — переменная, в которой хранится не число, а ответ «да» или «нет» (true или false).
  • if (leftOnLine) { … } — если ответ «да», выполнить команды в скобках.

Программе нужно правило: когда считать, что под датчиком линия? Сравни новое показание с двумя измерениями: если оно ближе к чёрному, чем к белому, — под датчиком линия.

Предскажи

Белое — 100, чёрное — 800, пришло число 650. Линия под датчиком или нет? Посчитай до запуска: сколько от 650 до чёрного и сколько до белого?

  1. Создай скетч L31_Line_Detect по шаблону и впиши свои четыре числа вместо нулей.
  2. Напиши loop() сам: решение «линия или нет» для каждого датчика и печать.
Шаблон: программа замечает линиюL31_Line_Detect_Gaps.ino.ino
// Шаблон среднего уровня. Напиши код в разделе «Твоя часть» по целям в комментариях. const int LEFT_SENSOR_PIN = A4;const int RIGHT_SENSOR_PIN = A5; // Впиши свои измерения из бортового журнала.const int LEFT_WHITE = 0;const int LEFT_BLACK = 0;const int RIGHT_WHITE = 0;const int RIGHT_BLACK = 0; // ===== Твоя часть: напиши сам ===== void loop() {  int leftValue = analogRead(LEFT_SENSOR_PIN);  int rightValue = analogRead(RIGHT_SENSOR_PIN);   // Для каждого датчика реши, линия ли под ним (правило «ближе к чёрному»): leftOnLine и rightOnLine.  // Печатай строку L=… R=…, а если под датчиком линия — допиши «слева линия» или «справа линия».  // Пять строк в секунду.} // ===== Готовое из прошлого шага — не меняй ===== void setup() {  Serial.begin(9600);}
  1. Загрузи программу, открой монитор порта и подводи чёрную карточку под датчики по очереди.
  2. Ответь в журнале: что начнёт печатать программа, если в ней перепутать белое и чёрное местами? Почему?
Подсказка: Как измерить расстояние между числами

От 650 до 800 — это 800 - 650. Но программа заранее не знает, какое число больше. Команда abs(…) убирает знак: abs(650 - 800) равно 150.

Подсказка: Да или нет

Ответ «да или нет» хранит тип bool. Команды в if (…) { … } выполняются, только если ответ «да».

Должно получиться

«слева линия» и «справа линия» появляются, только когда под датчиком чёрное. Прогноз про 650 сошёлся, ответ про перепутанные числа записан.

Найди ошибку · «Слева линия» на белом поле

Команда соседей написала так:

bool leftOnLine = abs(leftValue - LEFT_BLACK) < abs(leftValue - RIGHT_WHITE);

Иногда «слева линия» печатается, когда под левым датчиком белое поле. Найди ошибку и объясни, почему она проявляется не всегда.

Требование 1. Функция onLine(value, white, black) отвечает true, когда под датчиком линия:

  • показание ближе к чёрному измерению, чем к белому;
  • работает и когда у датчика чёрное — большое число, и когда маленькое;
  • внутри нет готовых чисел-порогов: только value, white и black.
Предскажи

Почему нельзя просто написать value > 500? Назови два случая, когда такое правило ошибётся.

Приёмка. Напиши onLine в тестовом скетче, загрузи его и открой монитор порта (9600). Тест проверяет восемь случаев для двух видов датчиков, в том числе показания за пределами твоих измерений.

Тест функции onLineL31_OnLine_Test.ino.ino
// Проверка функции onLine. Напиши функцию здесь, загрузи и открой монитор порта.// Когда все строки PASS, перенеси onLine в свою программу. // Требование 1: true, если показание ближе к чёрному, чем к белому.bool onLine(int value, int white, int black) {  return false;} int failed = 0; void expect(int value, int white, int black, bool expected) {  bool actual = onLine(value, white, black);  Serial.print(actual == expected ? "PASS " : "FAIL ");  Serial.print("onLine(");  Serial.print(value);  Serial.print(", ");  Serial.print(white);  Serial.print(", ");  Serial.print(black);  Serial.print(") = ");  Serial.print(actual ? "true" : "false");  Serial.print(", ожидаем ");  Serial.println(expected ? "true" : "false");  if (actual != expected) {    failed++;  }} void setup() {  Serial.begin(9600);   // Датчик, у которого чёрное — большое число, а белое — маленькое.  expect(780, 120, 780, true);  expect(120, 120, 780, false);  expect(600, 120, 780, true);  expect(300, 120, 780, false);  // За пределами измерений.  expect(900, 120, 780, true);  expect(50, 120, 780, false);  // Датчик, у которого наоборот: белое — большое число, чёрное — маленькое.  expect(300, 900, 120, true);  expect(700, 900, 120, false);   if (failed == 0) {    Serial.println("Все проверки пройдены");  } else {    Serial.print("Ошибок: ");    Serial.println(failed);  }} void loop() {}

Когда все строки PASS, добавь onLine в свою программу из шага «Посмотри, что видит датчик» и сохрани её как L31_Line_Detect. Пусть она печатает «слева линия» и «справа линия». Впиши свои измерения и проверь карточкой.

Подсказка: Идея (1 из 3)

Нужны два расстояния: от показания до чёрного и от показания до белого. Какое меньше?

Подсказка: План (2 из 3)
  1. Расстояние до чёрного — разница без знака.
  2. Расстояние до белого — так же.
  3. Вернуть результат сравнения двух расстояний.
Подсказка: Фрагмент кода (3 из 3)

abs(value - black) — расстояние до чёрного. Зачем здесь abs? Что будет без него у датчика, где чёрное — маленькое число?

Должно получиться

Тест показывает «Все проверки пройдены». Твоя программа печатает «слева линия» и «справа линия», только когда под датчиком чёрное.

6

Придумай, как поворачивать

7 мин

Робот едет вперёд. Линия должна оставаться между датчиками. Что делать, если линия оказалась под левым датчиком? Значит, робот уехал вправо, и ему надо повернуть налево.

Самый простой способ повернуть налево — остановить левое колесо, а правое пусть едет.

  1. Поставь выключенного робота на линию. Сдвинь его так, чтобы линия оказалась под левым датчиком. Покажи, какое колесо нужно остановить.
  2. То же — для правого датчика.
  3. Заполни таблицу в журнале:
Что видят датчикиЛевое колесоПравое колесоКуда едет робот
Оба белоевперёдвперёдпрямо
Левый — линия???
Правый — линия???
Оба — линия???
Должно получиться

Таблица заполнена. Ты можешь объяснить, почему остановка одного колеса поворачивает робота.

Робот едет вперёд, и линия должна оставаться между датчиками. Если линия оказалась под одним датчиком, робот уехал в сторону. Колесо пока умеет только ехать или стоять. Как вернуть робота к линии?

  1. Поставь выключенного робота на линию. Сдвинь его так, чтобы линия оказалась под левым датчиком. Куда робот уехал? Куда ему повернуть? Какое колесо для этого остановить?
  2. То же — для правого датчика.
  3. Заполни таблицу в журнале:
Что видят датчикиЛевое колесоПравое колесоКуда едет робот
Оба белоевперёдвперёдпрямо
Левый — линия???
Правый — линия???
Оба — линия???
  1. Объясни в журнале одной фразой, почему остановка одного колеса поворачивает робота.
Подсказка: Какое колесо останавливать

Вспомни урок 2.2: левое колесо стоит, правое едет. Вокруг какого колеса повернётся робот и в какую сторону?

Должно получиться

Таблица заполнена, объяснение записано. Ты показал на выключенном роботе оба поворота.

Робот едет вперёд, и линия должна оставаться между датчиками. Составь правила сам: что делают колёса в каждом из четырёх случаев и почему.

Что видят датчикиЛевое колесоПравое колесоКуда едет роботПочему
Оба белое
Левый — линия
Правый — линия
Оба — линия
  1. Проверь каждую строку на выключенном роботе: сдвинь его рукой и покажи, куда он должен поехать.
  2. «Оба белое» бывает в двух разных положениях робота. В каких? Может ли программа их различить? Запиши, что тогда случится с роботом.
  3. На прямой дорожке «оба — линия» почти не бывает. Где на итоговом поле это встретится и что тогда должен делать робот?
Подсказка: Два случая «оба белое»

Подумай, где может быть линия, когда ни один датчик её не видит.

Должно получиться

Таблица из четырёх строк с объяснением. В журнале записано, в каком положении программа не знает, где линия.

7

Запиши правила в программу

10 мин

Соберём всё вместе: датчики, наше правило «ближе к чёрному» и моторы из урока 2.1.

  1. Создай скетч L31_First_Line. Перенеси свои четыре измерения.
Первое движение по линииL31_First_Line.ino.ino
const int LEFT_SENSOR_PIN = A4;const int RIGHT_SENSOR_PIN = A5; // Впиши свои измерения из бортового журнала.const int LEFT_WHITE = 0;const int LEFT_BLACK = 0;const int RIGHT_WHITE = 0;const int RIGHT_BLACK = 0; const int LEFT_DIR_PIN = 7;const int LEFT_SPEED_PIN = 6;const int RIGHT_DIR_PIN = 4;const int RIGHT_SPEED_PIN = 5;const int FORWARD = HIGH; int speed = 100; // начальная скорость, подбираем на подставке void setup() {  pinMode(LEFT_DIR_PIN, OUTPUT);  pinMode(LEFT_SPEED_PIN, OUTPUT);  pinMode(RIGHT_DIR_PIN, OUTPUT);  pinMode(RIGHT_SPEED_PIN, OUTPUT);   analogWrite(LEFT_SPEED_PIN, 0);  analogWrite(RIGHT_SPEED_PIN, 0);  digitalWrite(LEFT_DIR_PIN, FORWARD);  digitalWrite(RIGHT_DIR_PIN, FORWARD);   // Без измерений робот не поедет: программа остановится здесь.  if (LEFT_WHITE == LEFT_BLACK || RIGHT_WHITE == RIGHT_BLACK) {    while (true) {    }  }   delay(2000); // время убрать руки от робота} void loop() {  int leftValue = analogRead(LEFT_SENSOR_PIN);  int rightValue = analogRead(RIGHT_SENSOR_PIN);   bool leftOnLine = abs(leftValue - LEFT_BLACK) < abs(leftValue - LEFT_WHITE);  bool rightOnLine = abs(rightValue - RIGHT_BLACK) < abs(rightValue - RIGHT_WHITE);   int leftSpeed = speed;  int rightSpeed = speed;   if (leftOnLine) {    leftSpeed = 0;  }  if (rightOnLine) {    rightSpeed = 0;  }   analogWrite(LEFT_SPEED_PIN, leftSpeed);  analogWrite(RIGHT_SPEED_PIN, rightSpeed);}
  1. Найди в loop() два правила из твоей таблицы. Объясни напарнику, что делает каждое.
  2. Нажми Проверить.
Как это устроено: Почему скорость задаётся заново в каждом повторе

В начале loop() обе скорости снова становятся равны speed. Если бы мы этого не делали, колесо, остановленное один раз, так бы и стояло, даже когда датчик снова увидит белое.

Как это устроено: Зачем строка while (true)

Если ты забыл вписать измерения, белое и чёрное равны нулю, и программа не может отличить линию. Тогда она «застревает» в пустом бесконечном цикле, и робот не поедет. Это защита от случайного старта.

Соберём всё вместе: датчики, правило «ближе к чёрному» и моторы из урока 2.1. Колёса будут слушаться правил из твоей таблицы.

  1. Создай скетч L31_First_Line по шаблону. Впиши свои четыре измерения. Настройка моторов и защита уже готовы.
  2. Напиши loop() сам: перенеси свои условия из L31_Line_Detect, задай колёсам скорости по своей таблице правил и отправь их на моторы.
Шаблон: первое движение по линииL31_First_Line_Gaps.ino.ino
// Шаблон среднего уровня. Напиши код в разделе «Твоя часть» по целям в комментариях. const int LEFT_SENSOR_PIN = A4;const int RIGHT_SENSOR_PIN = A5; // Впиши свои измерения из бортового журнала.const int LEFT_WHITE = 0;const int LEFT_BLACK = 0;const int RIGHT_WHITE = 0;const int RIGHT_BLACK = 0;const int LEFT_DIR_PIN = 7;const int LEFT_SPEED_PIN = 6;const int RIGHT_DIR_PIN = 4;const int RIGHT_SPEED_PIN = 5;const int FORWARD = HIGH;int speed = 100; // начальная скорость, подбираем на подставке // ===== Твоя часть: напиши сам ===== void loop() {  int leftValue = analogRead(LEFT_SENSOR_PIN);  int rightValue = analogRead(RIGHT_SENSOR_PIN);   // Реши, линия ли под каждым датчиком, — перенеси свои условия из L31_Line_Detect.  // Колёса выполняют твою таблицу правил: линия под датчиком — его колесо стоит.  // Колесо, которое правило остановило, снова едет, когда его датчик видит белое.} // ===== Готовое: настройка и защита — не меняй ===== void setup() {  pinMode(LEFT_DIR_PIN, OUTPUT);  pinMode(LEFT_SPEED_PIN, OUTPUT);  pinMode(RIGHT_DIR_PIN, OUTPUT);  pinMode(RIGHT_SPEED_PIN, OUTPUT);   analogWrite(LEFT_SPEED_PIN, 0);  analogWrite(RIGHT_SPEED_PIN, 0);  digitalWrite(LEFT_DIR_PIN, FORWARD);  digitalWrite(RIGHT_DIR_PIN, FORWARD);   // Без измерений робот не поедет: программа остановится здесь.  if (LEFT_WHITE == LEFT_BLACK || RIGHT_WHITE == RIGHT_BLACK) {    while (true) {    }  }   delay(2000); // время убрать руки от робота}
Предскажи

Что случится, если задать скорость колёс один раз в setup(), а не в начале каждого повтора loop()?

  1. Нажми Проверить.
Подсказка: Колесо остановилось и больше не едет

Когда правило снова разрешает колесу ехать? Откуда в каждом повторе берётся его скорость?

Подсказка: Правило колеса

if (проверка) { … } выполняет команды в скобках, только если проверка верна. Сверься со своей таблицей: какую переменную проверить и какую скорость поменять?

Должно получиться

loop() написан, Проверить проходит без ошибок, ответ на прогноз записан в журнал.

Как это устроено: Зачем строка while (true)

Если ты забыл вписать измерения, белое и чёрное равны нулю, и программа не может отличить линию. Тогда она «застревает» в пустом бесконечном цикле, и робот не поедет. Это защита от случайного старта.

Возьми каркас: в нём контакты, моторы и функция drive из урока 2.1. Перенеси в него свои измерения и функцию onLine и сохрани как L31_First_Line. Эта программа понадобится тебе в уроке 3.2.

Каркас движения по линииL31_First_Line_Skeleton.ino.ino
// Каркас движения по линии. Требования к программе — в тексте урока.const int LEFT_SENSOR_PIN = A4;const int RIGHT_SENSOR_PIN = A5; // Впиши свои измерения из бортового журнала.const int LEFT_WHITE = 0;const int LEFT_BLACK = 0;const int RIGHT_WHITE = 0;const int RIGHT_BLACK = 0; const int LEFT_DIR_PIN = 7;const int LEFT_SPEED_PIN = 6;const int RIGHT_DIR_PIN = 4;const int RIGHT_SPEED_PIN = 5;const int FORWARD = HIGH; void drive(int leftDir, int leftSpeed, int rightDir, int rightSpeed) {  digitalWrite(LEFT_DIR_PIN, leftDir);  digitalWrite(RIGHT_DIR_PIN, rightDir);  analogWrite(LEFT_SPEED_PIN, leftSpeed);  analogWrite(RIGHT_SPEED_PIN, rightSpeed);} // Требование 1: перенеси функцию onLine из тестового скетча.bool onLine(int value, int white, int black) {  return false;} void setup() {  pinMode(LEFT_DIR_PIN, OUTPUT);  pinMode(LEFT_SPEED_PIN, OUTPUT);  pinMode(RIGHT_DIR_PIN, OUTPUT);  pinMode(RIGHT_SPEED_PIN, OUTPUT);  drive(FORWARD, 0, FORWARD, 0);   // Требование 3: защита от запуска без измерений и время убрать руки.} void loop() {  // Требование 2: прочитать датчики, решить по своей таблице правил, задать скорости.  drive(FORWARD, 0, FORWARD, 0);}

Требование 2. Колёса выполняют твою таблицу правил:

  • в каждом повторе программа заново читает оба датчика и решает заново: колесо, остановленное один раз, снова едет, когда его датчик видит белое;
  • решение принимает твоя функция onLine;
  • скорость — одна именованная переменная speed в начале скетча.

Требование 3. Робот безопасен:

  • без измерений белого и чёрного колёса стоят;
  • после включения есть 2 секунды, чтобы убрать руки.

Приёмка — на подставке в следующем шаге: робот выполняет каждую строку твоей таблицы правил, а без измерений колёса не крутятся.

Предскажи

Что сделает робот, если скорости обоих колёс задать один раз в setup()? Запиши ответ до проверки.

Подсказка: Идея (1 из 3)

Каждый повтор: прочитать датчики → решить → отправить скорости на моторы. Решение начинается с «оба едут», а правила его меняют.

Подсказка: План (2 из 3)
  1. Прочитать оба датчика.
  2. Спросить onLine для каждого.
  3. Обе скорости — speed.
  4. Правила из таблицы меняют скорости.
  5. Отправить скорости в drive.
Подсказка: Фрагмент кода (3 из 3)

if (onLine(leftValue, LEFT_WHITE, LEFT_BLACK)) { leftSpeed = 0; } — а для правого датчика? А строка «оба — линия» из твоей таблицы?

Должно получиться

Программа выполняет требования 2 и 3, Проверить проходит без ошибок.

8

Проверь на подставке

6 мин
Безопасность

Загружай программу при выключенном питании моторов. Колёса не касаются стола.

  1. Поставь робота на подставку. Загрузи программу.
  2. Включи питание моторов. Оба колеса должны крутиться вперёд.
  3. Подведи чёрную карточку под левый датчик: левое колесо остановится. Убери карточку: колесо снова поедет. Проверь правый датчик.
  4. Выключи питание моторов.
Должно получиться

Каждое колесо останавливается, когда под его датчиком чёрное, и снова едет на белом.

Если не получилось
Что видишьЧто сделать
Колёса не крутятсяПроверь, что вписаны измерения и включено питание моторов.
Колесо крутится назадПроверь FORWARD и подключение мотора.
Останавливается не то колесоПерепутаны датчики или моторы. Проверь по монитору порта и по меткам Л/П.
  1. Поставь робота на подставку, загрузи программу, включи питание моторов.
  2. Проверь все четыре строки своей таблицы правил: подставляй карточки так, чтобы датчики видели нужное. Записывай, что делают колёса.
Что видят датчикиЖдалУвидел
Оба белое
Левый — линия
Правый — линия
Оба — линия
  1. Проверь свой прогноз про скорость, заданную в setup(). Выключи питание моторов.
Должно получиться

Таблица заполнена, все строки совпали с правилами. Прогноз проверен.

Если не получилось
Что видишьЧто сделать
Колёса не крутятсяНаписан ли loop()? Вписаны ли измерения? Включено ли питание моторов?
Останавливается не то колесоПерепутаны датчики или моторы. Проверь по монитору порта и по меткам Л/П.

Приёмка требований 2–3 на подставке.

  1. Загрузи программу с нулями вместо измерений. Колёса не должны крутиться. Впиши измерения и загрузи снова.
  2. Проверь каждую строку своей таблицы правил карточками.
  3. Засеки, через сколько секунд после включения трогаются колёса.
ПроверкаЖдалУвидел
Без измерений
Оба белое
Левый — линия
Правый — линия
Оба — линия
Пауза на старте
Должно получиться

Все проверки совпали с требованиями. Если нет — исправлено и проверено снова.

9

Первый заезд по линии

8 мин
  1. Выключи питание моторов, отключи USB-кабель.
  2. Поставь робота на прямой участок: линия между датчиками.
  3. Включи питание и наблюдай. Будь готов выключить питание, если робот уходит с дорожки.
  4. Попробуй проехать изгиб. Запиши в журнал, что получилось.
Должно получиться

Робот едет вдоль линии и проходит плавный изгиб. Он двигается рывками — это нормально для такого способа.

  1. Выключи питание моторов, отключи USB-кабель.
  2. Поставь робота на прямой участок: линия между датчиками. Включи питание. Будь готов выключить его, если робот уходит с дорожки.
  3. Пройди дорожку три раза при speed = 100, потом три раза при 80 и при 130. Записывай, сколько раз робот прошёл изгиб.
speedПрошёл из 3Что видно
80
100
130
  1. Почему робот едет рывками? Запиши одной фразой.
Должно получиться

Таблица заездов при трёх скоростях и объяснение рывков.

  1. Выключи питание моторов, отключи USB-кабель. Будь готов выключить питание, если робот уходит с дорожки.
  2. Найди наибольшую speed, при которой робот проходит плавный изгиб три раза из трёх. Начни с 80 и поднимай шагами по 20.
  3. На наибольшей скорости, которую робот не выдержал, посмотри, где он теряет линию. Объясни в журнале причину через твою таблицу правил.
  4. Запиши, что нужно изменить в правилах, чтобы робот держал линию быстрее. Проверишь это в уроке 3.2.
Должно получиться

Наибольшая надёжная скорость, место и причина схода, идея улучшения правил.

Основное · Прямая и плавный изгиб

Робот проходит прямую и плавный изгиб по двум правилам и не теряет линию.

Средний уровень · Едем через перекрёсток

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

Продвинутый уровень · Не стоп, а замедление

Вместо остановки колеса замедляй его: например, до трети от speed. Сравни на одном изгибе два варианта — «стоп» и «замедление». Какой плавнее? При какой скорости каждый вариант ещё держит линию? Запиши результаты в журнал: это первый шаг к П-регулятору, который понадобится на крутых дугах продвинутого поля.

Проверь себя

Выберите ответы для подготовки. Они не сохраняются и не оцениваются на сайте. Пройдите проверку знаний в программе курса или обсудите ответы с преподавателем.

● Основное

1.Почему мы измеряем белое и чёрное для каждого датчика отдельно?
2.Белое — 100, чёрное — 800. Датчик показывает 300. Что под датчиком?
3.Робот едет вперёд, линия оказалась под правым датчиком. Что сделает наша программа?
4.Что напечатает этот код, если leftValue равно 512?
Serial.print("L=");Serial.println(leftValue);
5.Зачем в начале loop() снова писать leftSpeed = speed и rightSpeed = speed?

★ Средний уровень

1.На среднем поле оба датчика видят чёрное. Что должен делать робот по заданию ★?
2.Какое условие означает перекрёсток?
3.У датчика белое — 900, чёрное — 120. Он показывает 300. Что решит условие «ближе к чёрному, чем к белому»?
4.В программе перепутали LEFT_WHITE и LEFT_BLACK местами. Что будет печатать программа?
5.Почему после остановки левого колеса робот поворачивает налево?

★★ Продвинутый уровень

1.speed = 150. По заданию ★★ колесо не останавливается, а замедляется до трети скорости. Какое значение получит колесо?
2.Чем замедление колеса лучше остановки?
3.Тест onLine не проходит для датчика «наоборот» (white = 900, black = 120). Почему?
bool onLine(int value, int white, int black) {  return value > (white + black) / 2;}
4.Оба датчика видят белое. В каких двух положениях может быть робот?
5.Что будет делать робот с этой программой?
void setup() {  // … настройка моторов  leftSpeed = speed;  rightSpeed = speed;} void loop() {  readSensors();  if (onLine(leftValue, LEFT_WHITE, LEFT_BLACK)) {    leftSpeed = 0;  }  drive(FORWARD, leftSpeed, FORWARD, rightSpeed);}

Отвечай на вопросы своего уровня. Нужно верных ответов: Начальный — 4 из 5; Средний — 4 из 5; Продвинутый — 4 из 5. Ответы проверит преподаватель или программа курса.

Итог

Робот научился сам выбирать, какое колесо остановить, по показаниям двух датчиков. Мы не угадывали числа, а измерили их на своём поле. Рывки при движении — цена простого правила «едет или стоит». В уроке 3.2 робот научится поворачивать плавно: тем сильнее, чем дальше линия ушла в сторону.

Вернись к своей догадке из начала урока. Подтвердилась ли она?

Как подтвердить результат

Для каждого опыта сохраняй условия, версию программы, наблюдение и вывод. Сравнивая попытки, меняй одну причину за раз; неудачные попытки тоже остаются в журнале. Отметка результата относится к уже выполненным действиям, повторять их ради отметки не нужно. Если результат не получен, запиши расхождение и следующую проверяемую гипотезу. Модель, измерения настоящего робота и объяснение — разные виды доказательств.

Словарь урока

датчик линииДатчик, который светит на пол и измеряет, сколько света отразилось. Белое отражает много, чёрное — мало.
показаниеЧисло, которое программа получает от датчика. У нашей платы это число от 0 до 1023.
монитор портаПанель Serial Monitor в MyRobotica, где видны сообщения, которые плата отправляет на компьютер.
калибровкаНастройка программы по измерениям своего робота и своего поля.
переменнаяИменованное место в памяти, где программа хранит число.
условиеПроверка if: команды в фигурных скобках выполняются, только если проверка верна.
телеметрияПередача именованных измерений с временем и единицами, чтобы строить графики и сохранять наблюдения.
АЦПАналого-цифровое преобразование: напряжение на входе платы превращается в число от 0 до 1023.
Тема 3 · Урок 3.2

П-регулятор: плавно и быстро

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

2 × 45 мин1–2 ученика на роботаПрактика

Результат урока

Робот плавно едет по линии с П-регулятором и проходит дугу своего уровня. Коэффициент и скорость подобраны и записаны в журнал.

Ты научишься

  • Перевести показания двух датчиков в общую шкалу от 0 до 100.
  • Посчитать ошибку как разницу показаний и объяснить её знак.
  • Объяснить, как поправка меняет скорости колёс.
  • Подобрать коэффициент и скорость по наблюдениям на дорожке.

Нужно знать

  • 3.1 Датчики линии и первое следование
  • 2.1 Моторы: едем и останавливаемся

Понадобится

Датчик линии (аналоговый, Trema) × 2USB-кабель для Arduino

Зачем этот урок

В уроке 3.1 робот ехал по линии рывками: колесо либо крутилось, либо стояло. На крутых дугах итоговой задачи так не проехать. Сегодня робот научится поворачивать чуть-чуть, когда линия ушла немного, и сильно, когда ушла далеко. Так работают автопилоты и роботы-курьеры.

Разминка · вспомни
  1. Как программа из урока 3.1 решала, что под датчиком линия?
  2. Почему робот в уроке 3.1 ехал рывками?
  3. Что делает функция drive()?
1
Занятие 1 · 45 мин
Ошибка вместо «да или нет»
1

Плавно — это как?

8 мин

Что должен делать робот. Роботу нужно двигаться по линии плавнее: при маленьком отклонении чуть поправлять курс, а при большом — сильнее. Используем те же датчики отражения и моторы, добавив П-регулятор — правило расчёта поправки.

Что используем и как это работает. Сначала переводим измерения каждого датчика в общую шкалу по его белому и чёрному. Разность уровней называется ошибкой. Коэффициент kp умножает ошибку: чем больше отклонение, тем больше поправка. Одному колесу прибавляем её к базовой команде, у другого вычитаем. Новое измерение позволяет снова скорректировать движение — это обратная связь.

Как связать измерение с действием. Слишком малая поправка не успевает вернуть робота, слишком большая вызывает колебания. Меняем один параметр за раз и сравниваем траекторию и показания на одинаковой трассе. Нулевая ошибка означает равные показания, но не всегда правильное положение: оба датчика могут потерять линию.

Представь, что ты ведёшь велосипед по линии. Если линия ушла чуть влево, ты чуть поворачиваешь руль. Если сильно — поворачиваешь сильнее. Ты не бросаешь руль из крайнего положения в крайнее.

  1. Встань с напарником: один — «робот», второй держит линию (шнур или полоску бумаги) и плавно смещает её влево и вправо.
  2. «Робот» поворачивает корпус тем сильнее, чем дальше линия от середины.

Если ты один: положи на стол изогнутый шнур и веди вдоль него ладонью — это «робот». Поворачивай ладонь тем сильнее, чем дальше шнур ушёл от её середины.

Должно получиться

Ты можешь объяснить: сила поворота зависит от того, насколько далеко ушла линия.

Представь, что ты ведёшь велосипед по линии: чуть ушла — чуть поворачиваешь руль, сильно ушла — сильнее. Программа из урока 3.1 так не умеет: она знает только «линия есть» или «нет».

  1. Запусти программу из урока 3.1 на дуге 350 мм и посмотри, как едет робот.
  2. Запиши в журнал одной фразой: чего не хватает программе, чтобы поворачивать плавно?
Должно получиться

Ты можешь объяснить: сила поворота должна зависеть от того, насколько далеко ушла линия.

Исследуй проблему, прежде чем решать.

  1. Запусти свою программу из урока 3.1 на дуге 350 мм, потом на дуге 125 мм. Запиши, как едет робот: рывки, виляние, сход с линии.
  2. Объясни в журнале, какую информацию с датчиков программа 3.1 выбрасывает и почему из-за этого робот едет рывками.
  3. До того как читать дальше, придумай своё правило: как скорости колёс должны зависеть от показаний датчиков. Запиши словами или формулой. В конце урока сравни его с П-регулятором.
Подсказка: Если не знаешь, с чего начать

Программа 3.1 сравнивает показание с порогом и получает «да или нет». А что, если использовать само число?

Должно получиться

В журнале — наблюдение на двух дугах, объяснение причины рывков и твоё правило управления.

2

Общая шкала для двух датчиков

15 мин

У каждого датчика свои числа белого и чёрного. Чтобы сравнивать их, переведём оба в одну шкалу: чёрное — 0, белое — 100, край линии — где-то посередине.

  1. Создай скетч L32_Error. Перенеси из журнала свои измерения белого и чёрного.
Шкала и ошибкаL32_Error.ino.ino
// Наблюдение в MyRobotica включается сборщиком; обычный Arduino работает без библиотеки.#include <math.h>#if defined(ROBOT_TELEMETRY_BUILD_ID)#include <RobotTelemetry.h>robot_telemetry::TelemetryStream courseTelemetry;#endif const int LEFT_SENSOR_PIN = A4;const int RIGHT_SENSOR_PIN = A5; // Измерения белого и чёрного из журнала (урок 3.1).const int LEFT_WHITE = 0;const int LEFT_BLACK = 0;const int RIGHT_WHITE = 0;const int RIGHT_BLACK = 0; // Переводит показание в шкалу: чёрное — 0, белое — 100.int level(int value, int white, int black) {  long result = (long)(value - black) * 100 / (white - black);  return constrain(result, 0, 100);} void setup() {  courseTelemetryBegin();  if (LEFT_WHITE == LEFT_BLACK || RIGHT_WHITE == RIGHT_BLACK) {    Serial.println("Впиши измерения белого и чёрного");    while (true) {      courseTelemetryPoll();    }  }} void loop() {  int leftRaw = analogRead(LEFT_SENSOR_PIN);  int rightRaw = analogRead(RIGHT_SENSOR_PIN);  int leftLevel = level(leftRaw, LEFT_WHITE, LEFT_BLACK);  int rightLevel = level(rightRaw, RIGHT_WHITE, RIGHT_BLACK);  int error = leftLevel - rightLevel;   courseTelemetryReport(leftRaw, rightRaw, error, NAN);  Serial.print("L=");  Serial.print(leftLevel);  Serial.print("  R=");  Serial.print(rightLevel);  Serial.print("  ошибка=");  Serial.println(error);  courseWait(200);} // ===== Готовое: передача наблюдений — не меняй =====void courseTelemetryBegin() {#if defined(ROBOT_TELEMETRY_BUILD_ID)  using namespace robot_telemetry::wire;  courseTelemetry.begin((1UL << Line0) | (1UL << Line1) |                        (1UL << LineError) | (1UL << ControlOutput));#else  Serial.begin(9600);#endif} void courseTelemetryPoll() {#if defined(ROBOT_TELEMETRY_BUILD_ID)  courseTelemetry.update();#endif} // NAN означает «ещё не вычислено», а не нулевую ошибку или поправку.void courseTelemetryReport(int leftRaw, int rightRaw, float error, float output) {#if defined(ROBOT_TELEMETRY_BUILD_ID)  using namespace robot_telemetry::wire;  courseTelemetry.setUnsigned(Line0, leftRaw);  courseTelemetry.setUnsigned(Line1, rightRaw);  if (isnan(error)) courseTelemetry.setMissing(LineError);  else courseTelemetry.setFloat(LineError, error);  if (isnan(output)) courseTelemetry.setMissing(ControlOutput);  else courseTelemetry.setFloat(ControlOutput, output);  courseTelemetry.update();#endif} void courseWait(unsigned long durationMs) {  unsigned long started = millis();  while (millis() - started < durationMs) {    courseTelemetryPoll();    delay(1);  }}
  1. Загрузи, открой монитор порта. Проверь: на белом оба числа около 100, на чёрном — около 0.
  2. Медленно проведи край линии под левым датчиком. Как меняется число?
Должно получиться

Оба датчика показывают около 100 на белом и около 0 на чёрном, на краю линии — промежуточные числа.

Как это устроено: Как работает функция level

value - black — насколько показание отличается от чёрного. white - black — весь путь от чёрного до белого. Их отношение, умноженное на 100, — «сколько процентов пути до белого». Формула работает, даже если у датчика чёрное — большое число, а белое — маленькое. constrain(…, 0, 100) обрезает случайные выбросы за границы шкалы.

У датчиков разные числа белого и чёрного, поэтому сравнивать их напрямую нельзя. Нужна общая шкала: чёрное — 0, белое — 100. Показание на шкале — это «сколько процентов пути от чёрного до белого» оно прошло.

  1. Создай скетч L32_Error по шаблону, перенеси измерения из журнала.
  2. Напиши функцию level сам. Ошибку в loop() пока не трогай.
Шаблон: шкала и ошибкаL32_Error_Gaps.ino.ino
// Наблюдение в MyRobotica включается сборщиком; обычный Arduino работает без библиотеки.#include <math.h>#if defined(ROBOT_TELEMETRY_BUILD_ID)#include <RobotTelemetry.h>robot_telemetry::TelemetryStream courseTelemetry;#endif // Шаблон среднего уровня. Напиши код в разделе «Твоя часть» по целям в комментариях. const int LEFT_SENSOR_PIN = A4;const int RIGHT_SENSOR_PIN = A5; // Измерения белого и чёрного из журнала (урок 3.1).const int LEFT_WHITE = 0;const int LEFT_BLACK = 0;const int RIGHT_WHITE = 0;const int RIGHT_BLACK = 0; // ===== Твоя часть: напиши сам ===== int level(int value, int white, int black) {  // Шкала: чёрное — 0, белое — 100, между ними — по прямой.  // Работает и когда у датчика чёрное — большое число, и когда маленькое; не выходит за 0…100.  return 0;} void loop() {  int leftRaw = analogRead(LEFT_SENSOR_PIN);  int rightRaw = analogRead(RIGHT_SENSOR_PIN);  int leftLevel = level(leftRaw, LEFT_WHITE, LEFT_BLACK);  int rightLevel = level(rightRaw, RIGHT_WHITE, RIGHT_BLACK);   // Ошибка: насколько и в какую сторону линия ушла от середины; линия посередине — 0.  float error = NAN; // Пока ошибка не вычислена, график показывает «нет данных».   courseTelemetryReport(leftRaw, rightRaw, error, NAN);  Serial.print("L=");  Serial.print(leftLevel);  Serial.print("  R=");  Serial.print(rightLevel);  Serial.print("  ошибка=");  Serial.println(error);  courseWait(200);} // ===== Готовое: настройка и защита — не меняй ===== void setup() {  courseTelemetryBegin();  if (LEFT_WHITE == LEFT_BLACK || RIGHT_WHITE == RIGHT_BLACK) {    Serial.println("Впиши измерения белого и чёрного");    while (true) {      courseTelemetryPoll();    }  }} // ===== Готовое: передача наблюдений — не меняй =====void courseTelemetryBegin() {#if defined(ROBOT_TELEMETRY_BUILD_ID)  using namespace robot_telemetry::wire;  courseTelemetry.begin((1UL << Line0) | (1UL << Line1) |                        (1UL << LineError) | (1UL << ControlOutput));#else  Serial.begin(9600);#endif} void courseTelemetryPoll() {#if defined(ROBOT_TELEMETRY_BUILD_ID)  courseTelemetry.update();#endif} // NAN означает «ещё не вычислено», а не нулевую ошибку или поправку.void courseTelemetryReport(int leftRaw, int rightRaw, float error, float output) {#if defined(ROBOT_TELEMETRY_BUILD_ID)  using namespace robot_telemetry::wire;  courseTelemetry.setUnsigned(Line0, leftRaw);  courseTelemetry.setUnsigned(Line1, rightRaw);  if (isnan(error)) courseTelemetry.setMissing(LineError);  else courseTelemetry.setFloat(LineError, error);  if (isnan(output)) courseTelemetry.setMissing(ControlOutput);  else courseTelemetry.setFloat(ControlOutput, output);  courseTelemetry.update();#endif} void courseWait(unsigned long durationMs) {  unsigned long started = millis();  while (millis() - started < durationMs) {    courseTelemetryPoll();    delay(1);  }}
Предскажи

У датчика белое — 120, чёрное — 780. Какое число шкалы получится для показания 450? Посчитай до запуска.

  1. Загрузи и проверь на белом, чёрном и краю линии.
  2. Подними робота на 3–4 см над столом: датчику почти нечего отразить. Какое «сырое» число получается, что показывает шкала и какая часть функции за это отвечает?
  3. Ответь в журнале: какого типа число ты взял для расчёта и почему не обычный int?
Подсказка: С чего начать формулу

Путь от чёрного до белого — white - black. Пройденная часть — value - black. Осталось превратить их отношение в проценты.

Подсказка: Шкала скачет у белого

На Arduino Uno int хранит числа только до 32 767. Посчитай, сколько выйдет (value - black) * 100 для показания 50 и чёрного 780.

Подсказка: Шкала уходит за 0…100

constrain(x, a, b) возвращает x, но не меньше a и не больше b.

Должно получиться

На белом около 100, на чёрном около 0, для показания 450 из примера — 50. Шкала не уходит за 0…100, и ты знаешь почему. В журнале — ответ про тип числа.

Ты не начинаешь с чистого листа: работай в своей программе из урока 3.1. В ней уже есть контакты, измерения белого и чёрного и защита от запуска без измерений. Сохрани её копию как L32_P_Line — за урок ты превратишь её в П-регулятор, шаг за шагом, по требованиям.

Подсказка: Нет своей программы из урока 3.1?

Возьми каркас: в нём только контакты, управление моторами и защита. Всё остальное напишешь сам.

Каркас П-регулятораL32_P_Line_Skeleton.ino.ino
// Наблюдение в MyRobotica включается сборщиком; обычный Arduino работает без библиотеки.#include <math.h>#if defined(ROBOT_TELEMETRY_BUILD_ID)#include <RobotTelemetry.h>robot_telemetry::TelemetryStream courseTelemetry;#endif // Каркас П-регулятора. Требования к программе — в тексте урока, шаг за шагом.const int LEFT_SENSOR_PIN = A4;const int RIGHT_SENSOR_PIN = A5; // Измерения белого и чёрного из журнала (урок 3.1).const int LEFT_WHITE = 0;const int LEFT_BLACK = 0;const int RIGHT_WHITE = 0;const int RIGHT_BLACK = 0; const int LEFT_DIR_PIN = 7;const int LEFT_SPEED_PIN = 6;const int RIGHT_DIR_PIN = 4;const int RIGHT_SPEED_PIN = 5;const int FORWARD = HIGH; void drive(int leftDir, int leftSpeed, int rightDir, int rightSpeed) {  digitalWrite(LEFT_DIR_PIN, leftDir);  digitalWrite(RIGHT_DIR_PIN, rightDir);  analogWrite(LEFT_SPEED_PIN, leftSpeed);  analogWrite(RIGHT_SPEED_PIN, rightSpeed);} // Требование 1: шкала «чёрное — 0, белое — 100».int level(int value, int white, int black) {  return 0;} void setup() {  courseTelemetryBegin();  pinMode(LEFT_DIR_PIN, OUTPUT);  pinMode(LEFT_SPEED_PIN, OUTPUT);  pinMode(RIGHT_DIR_PIN, OUTPUT);  pinMode(RIGHT_SPEED_PIN, OUTPUT);  drive(FORWARD, 0, FORWARD, 0);   // Без измерений робот не поедет. Эту защиту не удаляй.  if (LEFT_WHITE == LEFT_BLACK || RIGHT_WHITE == RIGHT_BLACK) {    while (true) {      courseTelemetryPoll();    }  }  courseWait(2000);} void loop() {  float observedError = NAN;  float observedOutput = NAN;  // Требования 2 и 3: ошибка, поправка и скорости колёс.  // Для графиков сохрани свои результаты в observedError и observedOutput.  drive(FORWARD, 0, FORWARD, 0);  courseTelemetryReport(analogRead(LEFT_SENSOR_PIN), analogRead(RIGHT_SENSOR_PIN),                        observedError, observedOutput);} // ===== Готовое: передача наблюдений — не меняй =====void courseTelemetryBegin() {#if defined(ROBOT_TELEMETRY_BUILD_ID)  using namespace robot_telemetry::wire;  courseTelemetry.begin((1UL << Line0) | (1UL << Line1) |                        (1UL << LineError) | (1UL << ControlOutput));#else  Serial.begin(9600);#endif} void courseTelemetryPoll() {#if defined(ROBOT_TELEMETRY_BUILD_ID)  courseTelemetry.update();#endif} // NAN означает «ещё не вычислено», а не нулевую ошибку или поправку.void courseTelemetryReport(int leftRaw, int rightRaw, float error, float output) {#if defined(ROBOT_TELEMETRY_BUILD_ID)  using namespace robot_telemetry::wire;  courseTelemetry.setUnsigned(Line0, leftRaw);  courseTelemetry.setUnsigned(Line1, rightRaw);  if (isnan(error)) courseTelemetry.setMissing(LineError);  else courseTelemetry.setFloat(LineError, error);  if (isnan(output)) courseTelemetry.setMissing(ControlOutput);  else courseTelemetry.setFloat(ControlOutput, output);  courseTelemetry.update();#endif} void courseWait(unsigned long durationMs) {  unsigned long started = millis();  while (millis() - started < durationMs) {    courseTelemetryPoll();    delay(1);  }}
Как это устроено: Справка по командам
Команда или типЧто делает
analogRead(pin)Показание датчика: 0…1023
analogWrite(pin, v)Скорость мотора: v от 0 до 255
digitalWrite(pin, HIGH) / LOWНаправление мотора
constrain(x, a, b)x, но не меньше a и не больше b
Serial.begin(9600)Включает монитор порта, пишут один раз в setup()
Serial.print(x) / Serial.println(x)Вывод в монитор порта; println — с переводом строки
int на Arduino UnoЦелые от −32 768 до 32 767
long на Arduino UnoЦелые от −2 147 483 648 до 2 147 483 647
/ с целыми числамиДробная часть отбрасывается: 7 / 2 даёт 3

Требование 1. Функция level(value, white, black) переводит показание в шкалу 0…100:

  • чёрное — 0, белое — 100, между ними — по прямой;
  • работает и когда у датчика чёрное — большое число, а белое — маленькое;
  • никогда не выходит за 0…100;
  • не переполняется на Arduino Uno.

Приёмка. Напиши level в тестовом скетче, загрузи его и открой монитор порта. Тест проверяет восемь случаев, в том числе датчик «наоборот» и показания за пределами шкалы. Когда все строки PASS, перенеси функцию в свою программу и проверь шкалу обоих датчиков на дорожке.

Тест функции levelL32_Level_Test.ino.ino
// Проверка функции level. Напиши функцию здесь, загрузи и открой монитор порта.// Когда все строки PASS, перенеси level в свою программу. // Требование 1: шкала «чёрное — 0, белое — 100».int level(int value, int white, int black) {  return 0;} int failed = 0; void expect(int value, int white, int black, int expected) {  int actual = level(value, white, black);  Serial.print(actual == expected ? "PASS " : "FAIL ");  Serial.print("level(");  Serial.print(value);  Serial.print(", ");  Serial.print(white);  Serial.print(", ");  Serial.print(black);  Serial.print(") = ");  Serial.print(actual);  Serial.print(", ожидаем ");  Serial.println(expected);  if (actual != expected) {    failed++;  }} void setup() {  Serial.begin(9600);   // Датчик, у которого чёрное — большое число, а белое — маленькое.  expect(780, 120, 780, 0);  expect(120, 120, 780, 100);  expect(450, 120, 780, 50);  // За пределами шкалы.  expect(900, 120, 780, 0);  expect(50, 120, 780, 100);  // Датчик, у которого наоборот: белое — большое число, чёрное — маленькое.  expect(120, 900, 120, 0);  expect(510, 900, 120, 50);  expect(900, 900, 120, 100);   if (failed == 0) {    Serial.println("Все проверки пройдены");  } else {    Serial.print("Ошибок: ");    Serial.println(failed);  }} void loop() {}
Подсказка: Идея (1 из 3)

Шкала — это доля пути от чёрного до белого. Весь путь — разница белого и чёрного.

Подсказка: План (2 из 3)
  1. Насколько показание ушло от чёрного?
  2. Раздели на длину всего пути и умножь на 100.
  3. Обрежь результат границами шкалы.
Подсказка: Фрагмент кода (3 из 3)

long result = (long)(value - black) * 100 / (white - black); — почему здесь long и почему умножение стоит раньше деления?

Должно получиться

Тест показывает «Все проверки пройдены». На дорожке шкала показывает около 0 на чёрном и около 100 на белом.

3

Считаем ошибку

15 мин

Ошибка — разница показаний: error = leftLevel - rightLevel. Когда линия ровно посередине, оба датчика видят одинаково и ошибка равна нулю.

Предскажи

Линия ушла под левый датчик. Какой знак будет у ошибки — плюс или минус? А если под правый?

  1. Посмотри на строку с ошибкой в мониторе порта.
  2. Подвинь линию под левый датчик, потом под правый. Запиши в журнал знак и примерные числа.
Где линияleftLevelrightLevelОшибка
Посередине
Под левым
Под правым
Должно получиться

Линия слева — ошибка отрицательная, справа — положительная, посередине — около нуля.

Ошибка показывает, насколько и в какую сторону линия ушла от середины. Считаем её как разницу: левое показание минус правое.

  1. Напиши расчёт ошибки в loop() сам и загрузи скетч.
Предскажи

Линия ушла под левый датчик. Какой знак будет у ошибки? Почему?

  1. Проверь прогноз: подвинь линию под левый датчик, потом под правый. Запиши в журнал.
Где линияleftLevelrightLevelОшибка
Посередине
Под левым
Под правым
  1. Ответь в журнале: в какую сторону должен повернуть робот, если ошибка отрицательная?
Подсказка: Знак ошибки

Чёрная линия — 0 на шкале. Если линия под левым датчиком, какое из двух чисел меньше?

Должно получиться

Знаки в таблице совпали с прогнозом, и ты можешь объяснить, куда поворачивать при каждом знаке.

Требование 2. Программа считает ошибку — число со знаком: насколько и в какую сторону линия ушла от середины. Знак выбираешь ты, но потом поправка должна поворачивать робота к линии.

  1. Добавь расчёт ошибки и временный вывод leftLevel, rightLevel, ошибки в монитор порта.
  2. Заполни таблицу и запиши в журнал своё правило знака одной фразой.
Где линияleftLevelrightLevelОшибка
Посередине
Под левым
Под правым
  1. Разберись с краем: линия ушла за пределы обоих датчиков. Что покажет ошибка и что тогда сделает робот? Запиши, чем это опасно на крутой дуге.
Подсказка: Идея (1 из 2)

Если линия ровно посередине, оба датчика видят одно и то же — ошибка должна быть ноль.

Подсказка: Проверка знака (2 из 2)

Выбери: left - right или right - left. Запиши, какое колесо должно ускориться, когда линия слева, — это понадобится в требовании 3.

Должно получиться

Таблица заполнена, правило знака записано. Ты знаешь, что видит регулятор, когда линия ушла за оба датчика.

2
Занятие 2 · 45 мин
П-регулятор на дорожке
4

От ошибки к скоростям

10 мин

Поправка — ошибка, умноженная на коэффициент: correction = kp * error. К левому колесу поправку прибавляем, у правого — отнимаем.

  1. Создай скетч L32_P_Line, перенеси измерения.
П-регуляторL32_P_Line.ino.ino
// Наблюдение в MyRobotica включается сборщиком; обычный Arduino работает без библиотеки.#include <math.h>#if defined(ROBOT_TELEMETRY_BUILD_ID)#include <RobotTelemetry.h>robot_telemetry::TelemetryStream courseTelemetry;#endif const int LEFT_SENSOR_PIN = A4;const int RIGHT_SENSOR_PIN = A5; // Измерения белого и чёрного из журнала (урок 3.1).const int LEFT_WHITE = 0;const int LEFT_BLACK = 0;const int RIGHT_WHITE = 0;const int RIGHT_BLACK = 0; // Переводит показание в шкалу: чёрное — 0, белое — 100.int level(int value, int white, int black) {  long result = (long)(value - black) * 100 / (white - black);  return constrain(result, 0, 100);} const int LEFT_DIR_PIN = 7;const int LEFT_SPEED_PIN = 6;const int RIGHT_DIR_PIN = 4;const int RIGHT_SPEED_PIN = 5;const int FORWARD = HIGH; float kp = 0.5;     // сила поворота: подбираем на дорожкеint baseSpeed = 100; // скорость на прямой void drive(int leftDir, int leftSpeed, int rightDir, int rightSpeed) {  digitalWrite(LEFT_DIR_PIN, leftDir);  digitalWrite(RIGHT_DIR_PIN, rightDir);  analogWrite(LEFT_SPEED_PIN, leftSpeed);  analogWrite(RIGHT_SPEED_PIN, rightSpeed);} void setup() {  courseTelemetryBegin();  pinMode(LEFT_DIR_PIN, OUTPUT);  pinMode(LEFT_SPEED_PIN, OUTPUT);  pinMode(RIGHT_DIR_PIN, OUTPUT);  pinMode(RIGHT_SPEED_PIN, OUTPUT);  drive(FORWARD, 0, FORWARD, 0);   // Без измерений робот не поедет.  if (LEFT_WHITE == LEFT_BLACK || RIGHT_WHITE == RIGHT_BLACK) {    while (true) {      courseTelemetryPoll();    }  }  courseWait(2000);} void loop() {  int leftRaw = analogRead(LEFT_SENSOR_PIN);  int rightRaw = analogRead(RIGHT_SENSOR_PIN);  int leftLevel = level(leftRaw, LEFT_WHITE, LEFT_BLACK);  int rightLevel = level(rightRaw, RIGHT_WHITE, RIGHT_BLACK);   int error = leftLevel - rightLevel;  int correction = kp * error;   int leftSpeed = constrain(baseSpeed + correction, 0, 255);  int rightSpeed = constrain(baseSpeed - correction, 0, 255);  drive(FORWARD, leftSpeed, FORWARD, rightSpeed);  courseTelemetryReport(leftRaw, rightRaw, error, correction);} // ===== Готовое: передача наблюдений — не меняй =====void courseTelemetryBegin() {#if defined(ROBOT_TELEMETRY_BUILD_ID)  using namespace robot_telemetry::wire;  courseTelemetry.begin((1UL << Line0) | (1UL << Line1) |                        (1UL << LineError) | (1UL << ControlOutput));#else  Serial.begin(9600);#endif} void courseTelemetryPoll() {#if defined(ROBOT_TELEMETRY_BUILD_ID)  courseTelemetry.update();#endif} // NAN означает «ещё не вычислено», а не нулевую ошибку или поправку.void courseTelemetryReport(int leftRaw, int rightRaw, float error, float output) {#if defined(ROBOT_TELEMETRY_BUILD_ID)  using namespace robot_telemetry::wire;  courseTelemetry.setUnsigned(Line0, leftRaw);  courseTelemetry.setUnsigned(Line1, rightRaw);  if (isnan(error)) courseTelemetry.setMissing(LineError);  else courseTelemetry.setFloat(LineError, error);  if (isnan(output)) courseTelemetry.setMissing(ControlOutput);  else courseTelemetry.setFloat(ControlOutput, output);  courseTelemetry.update();#endif} void courseWait(unsigned long durationMs) {  unsigned long started = millis();  while (millis() - started < durationMs) {    courseTelemetryPoll();    delay(1);  }}
  1. Посчитай на бумаге: baseSpeed = 100, kp = 0.5, ошибка −40. Какие скорости получат колёса?
Должно получиться

Ты посчитал: поправка −20, левое колесо 80, правое 120 — робот поворачивает налево, к линии.

Как это устроено: Зачем constrain

При большой ошибке скорость может получиться −30 или 300. Мотор понимает только 0…255. constrain(число, 0, 255) ставит границы: меньше нуля станет 0, больше 255 — 255.

Правило П-регулятора: поправка пропорциональна ошибке. Чем дальше ушла линия, тем сильнее одно колесо ускоряется, а другое замедляется. Насколько сильно — задаёт kp.

  1. Создай скетч L32_P_Line по шаблону, перенеси измерения и свою функцию level.
  2. Напиши loop() сам: ошибка, поправка, скорости колёс в пределах 0…255 и команда моторам.
Шаблон: П-регуляторL32_P_Line_Gaps.ino.ino
// Наблюдение в MyRobotica включается сборщиком; обычный Arduino работает без библиотеки.#include <math.h>#if defined(ROBOT_TELEMETRY_BUILD_ID)#include <RobotTelemetry.h>robot_telemetry::TelemetryStream courseTelemetry;#endif // Шаблон среднего уровня. Напиши код в разделе «Твоя часть» по целям в комментариях. const int LEFT_SENSOR_PIN = A4;const int RIGHT_SENSOR_PIN = A5; // Измерения белого и чёрного из журнала (урок 3.1).const int LEFT_WHITE = 0;const int LEFT_BLACK = 0;const int RIGHT_WHITE = 0;const int RIGHT_BLACK = 0;const int LEFT_DIR_PIN = 7;const int LEFT_SPEED_PIN = 6;const int RIGHT_DIR_PIN = 4;const int RIGHT_SPEED_PIN = 5;const int FORWARD = HIGH;float kp = 0.5;     // сила поворота: подбираем на дорожкеint baseSpeed = 100; // скорость на прямой // ===== Твоя часть: напиши сам ===== int level(int value, int white, int black) {  // Перенеси свою функцию level из L32_Error.  return 0;} void loop() {  float observedError = NAN;  float observedOutput = NAN;  int leftRaw = analogRead(LEFT_SENSOR_PIN);  int rightRaw = analogRead(RIGHT_SENSOR_PIN);  int leftLevel = level(leftRaw, LEFT_WHITE, LEFT_BLACK);  int rightLevel = level(rightRaw, RIGHT_WHITE, RIGHT_BLACK);   // Ошибка — как в L32_Error.  // Поправка пропорциональна ошибке, коэффициент kp.  // Робот поворачивает к линии тем сильнее, чем больше ошибка; скорость каждого колеса — в пределах 0…255.  // Отправь скорости на моторы.  // Сохрани вычисленные ошибку и поправку в observedError и observedOutput для графиков.  courseTelemetryReport(leftRaw, rightRaw, observedError, observedOutput);} // ===== Готовое: моторы, настройка и защита — не меняй ===== void drive(int leftDir, int leftSpeed, int rightDir, int rightSpeed) {  digitalWrite(LEFT_DIR_PIN, leftDir);  digitalWrite(RIGHT_DIR_PIN, rightDir);  analogWrite(LEFT_SPEED_PIN, leftSpeed);  analogWrite(RIGHT_SPEED_PIN, rightSpeed);} void setup() {  courseTelemetryBegin();  pinMode(LEFT_DIR_PIN, OUTPUT);  pinMode(LEFT_SPEED_PIN, OUTPUT);  pinMode(RIGHT_DIR_PIN, OUTPUT);  pinMode(RIGHT_SPEED_PIN, OUTPUT);  drive(FORWARD, 0, FORWARD, 0);   // Без измерений робот не поедет.  if (LEFT_WHITE == LEFT_BLACK || RIGHT_WHITE == RIGHT_BLACK) {    while (true) {      courseTelemetryPoll();    }  }  courseWait(2000);} // ===== Готовое: передача наблюдений — не меняй =====void courseTelemetryBegin() {#if defined(ROBOT_TELEMETRY_BUILD_ID)  using namespace robot_telemetry::wire;  courseTelemetry.begin((1UL << Line0) | (1UL << Line1) |                        (1UL << LineError) | (1UL << ControlOutput));#else  Serial.begin(9600);#endif} void courseTelemetryPoll() {#if defined(ROBOT_TELEMETRY_BUILD_ID)  courseTelemetry.update();#endif} // NAN означает «ещё не вычислено», а не нулевую ошибку или поправку.void courseTelemetryReport(int leftRaw, int rightRaw, float error, float output) {#if defined(ROBOT_TELEMETRY_BUILD_ID)  using namespace robot_telemetry::wire;  courseTelemetry.setUnsigned(Line0, leftRaw);  courseTelemetry.setUnsigned(Line1, rightRaw);  if (isnan(error)) courseTelemetry.setMissing(LineError);  else courseTelemetry.setFloat(LineError, error);  if (isnan(output)) courseTelemetry.setMissing(ControlOutput);  else courseTelemetry.setFloat(ControlOutput, output);  courseTelemetry.update();#endif} void courseWait(unsigned long durationMs) {  unsigned long started = millis();  while (millis() - started < durationMs) {    courseTelemetryPoll();    delay(1);  }}
  1. Посчитай на бумаге: baseSpeed = 100, kp = 0.5, ошибка −40. Какие скорости получат колёса и куда повернёт робот? На подставке в следующем шаге проверишь это на колёсах.
Предскажи

Что будет делать робот, если поставить kp = 0? А если сделать kp отрицательным?

Подсказка: Какое колесо ускорять

Линия слева — ошибка отрицательная. Какое колесо должно ехать быстрее, чтобы робот повернул к линии? Согласуй знак поправки с этим.

Подсказка: Границы скорости

Мотор понимает только 0…255. Какая команда из урока не даёт числу выйти за границы?

Должно получиться

loop() написан. Для примера: поправка −20, левое колесо 80, правое 120 — робот поворачивает налево, к линии. Прогноз про kp = 0 записан — проверишь на подставке.

Требование 3. Робот едет вперёд со скоростью baseSpeed и поворачивает к линии тем сильнее, чем больше ошибка:

  • поправка пропорциональна ошибке, коэффициент kp;
  • kp и baseSpeed — именованные переменные в начале скетча, их подбирают на дорожке;
  • скорости колёс всегда в пределах 0…255;
  • без измерений белого и чёрного колёса стоят (защита из урока 3.1 остаётся).

Приёмка на бумаге. Посчитай скорости колёс для своей программы:

baseSpeedkpОшибкаЛевоеПравое
1000.5−40
2000.860
2001.0100

В последней строке одно колесо упирается в границу. Насколько слабее робот повернёт, чем «хотел»? Запиши в журнал, что можно с этим сделать.

Подсказка: Идея (1 из 3)

Одно колесо получает базовую скорость плюс поправку, другое — минус поправку.

Подсказка: План (2 из 3)
  1. Ошибка из требования 2.
  2. Поправка = коэффициент × ошибка.
  3. Левое и правое колесо: база ± поправка, каждое — в границах 0…255.
  4. Отправить скорости на моторы.
Подсказка: Фрагмент кода (3 из 3)

int leftSpeed = constrain(baseSpeed + correction, 0, 255); — а что для правого? Проверь знак по своему правилу из требования 2.

Должно получиться

Программа выполняет требование 3, таблица посчитана, ограничение записано в журнал.

5

Проверь на подставке

7 мин
  1. Поставь робота на подставку, загрузи программу, включи питание моторов.
  2. Подведи линию под левый датчик: левое колесо замедлится, правое ускорится.
  3. Подведи под правый — наоборот. Линия посередине — колёса крутятся одинаково.
Должно получиться

Колёса меняют скорость плавно, а не «стоп — едет».

Если не получилось
Что видишьЧто сделать
Колёса не крутятсяВписаны ли измерения? Включено ли питание моторов?
Ускоряется не то колесоПроверь знак ошибки по монитору порта и формулы скоростей
  1. Поставь робота на подставку, загрузи программу, включи питание моторов.
  2. Подведи линию под левый датчик, потом под правый, потом посередине. Запиши, какое колесо ускоряется и какое замедляется.
  3. Проверь свой прогноз про kp = 0: поставь kp = 0, загрузи и подвигай линию. Что делают колёса? Верни свой kp.
Где линияЛевое колесоПравое колесоСовпало с прогнозом?
Под левым
Под правым
Посередине
Должно получиться

Колёса меняют скорость плавно. Таблица заполнена, прогноз про kp = 0 проверен.

Если не получилось
Что видишьЧто сделать
Колёса не крутятсяНаписан ли loop() с командой моторам? Вписаны ли измерения?
Ускоряется не то колесоПроверь знак ошибки по монитору порта и формулы скоростей

Приёмка требования 3 на подставке.

  1. Временно выведи в монитор порта ошибку, поправку и обе скорости колёс.
  2. Подводи линию под левый датчик, правый и посередине. Совпадает ли направление поправки с твоим правилом знака из требования 2?
  3. Найди положение линии, при котором одно колесо упирается в 0 или 255. Сравни скорости в мониторе с тем, что ты посчитал в таблице требования 3.
  4. Отключи измерения (нули) — колёса стоят? Верни измерения.
Должно получиться

Направление поправки, упор в границу и защита проверены по монитору порта. Расхождения с таблицей объяснены.

6

Подбери коэффициент

18 мин

Коэффициент kp подбирают на дорожке. Меняй одно число за раз и смотри, что изменилось.

  1. Поставь робота на дорожку с дугой. Попробуй kp = 0.2, 0.5, 0.8 при одной скорости.
  2. Запиши в журнал, как робот ведёт себя при каждом значении.
  3. Выбери лучший kp и попробуй увеличить baseSpeed.
kpbaseSpeedЧто видно
0.2100
0.5100
0.8100
Как это устроено: Как понять, куда менять kp
  • Робот плавно уходит с дуги наружу — поправка слишком слабая, увеличь kp.
  • Робот виляет из стороны в сторону даже на прямой — поправка слишком сильная, уменьши kp.
  • Хороший kp — робот держится линии на дуге и почти не виляет на прямой.
Должно получиться

Подобраны kp и baseSpeed, при которых робот проходит дугу. Числа записаны в журнал.

Подбери kp сам. Правило одно: меняй одно число за раз.

  1. Начни с kp = 0.5 при baseSpeed = 100. Затем попробуй одно явно меньшее и одно явно большее значение и сузь диапазон.
  2. Запиши в журнал не меньше четырёх попыток.
kpbaseSpeedЧто видноЧто поменять дальше и почему
0.5100
100
100
100
  1. С лучшим kp подними baseSpeed. Что пришлось изменить, чтобы робот снова держал дугу?
Подсказка: Как понять, куда менять kp

Робот уходит с дуги наружу — поправка слабая. Робот виляет даже на прямой — поправка сильная.

Должно получиться

В журнале четыре попытки с объяснением каждого шага и пара kp — baseSpeed, на которой робот проходит дугу.

Подбери kp и скорость методом, а не наугад, и докажи выбор записями.

  1. При baseSpeed = 100 начни с kp = 0.1 и удваивай: 0.1, 0.2, 0.4, 0.8… Найди наименьший kp, при котором робот начинает вилять на прямой.
  2. Рабочий kp возьми примерно вдвое меньше. Проверь его на дуге своего уровня.
  3. Поднимай baseSpeed шагами по 20. Для каждой скорости — три заезда. Запомни последнюю скорость, на которой робот прошёл все три.
  4. Запиши в журнал таблицу и вывод: почему именно эта пара, что ограничивает скорость.
  5. Вернись к правилу, которое ты придумал в первом шаге. Чем оно похоже на П-регулятор и чем отличается? Что бы ты в нём изменил теперь?
kpbaseSpeedЗаезды (из 3)Что видно
Подсказка: Робот виляет при любом kp

Проверь, не упирается ли колесо в 255 или 0 на дуге — вспомни последнюю строку таблицы из требования 3.

Должно получиться

Таблица с методом подбора, пара kp — baseSpeed, проверенная тремя заездами, вывод с обоснованием и сравнение своего правила с П-регулятором.

7

Задания по уровням

10 мин
Основное · Дуга начального поля

Пройти дугу радиусом 350 мм, как на начальном поле итоговой задачи, не теряя линию.

Средний уровень · Дуга на скорости

Пройти дугу радиусом 250 мм, как на среднем поле, на скорости не ниже той, что подобрана для прямой. Если не получается — что важнее увеличить: kp или уменьшить скорость?

Продвинутый уровень · Крутая S-связка

Пройти две дуги радиусом 125 мм подряд в разные стороны, как на продвинутом поле. Начни с пары kp — скорость, выбранной в шаге подбора. Если она не удерживает S-связку, измени один параметр и дополни существующую таблицу результатом; заново переписывать удачные и неудачные попытки не нужно.

Проверь себя

Проверь себя

Выберите ответы для подготовки. Они не сохраняются и не оцениваются на сайте. Пройдите проверку знаний в программе курса или обсудите ответы с преподавателем.

● Основное

1.Левый датчик показывает 20, правый — 100. Чему равна ошибка error = leftLevel - rightLevel?
2.baseSpeed = 100, kp = 0.5, ошибка = 40. Какие скорости у колёс?
3.Робот сильно виляет даже на прямой. Что сделать с kp?
4.Зачем переводить показания датчиков в шкалу 0–100?
5.Что делает constrain(baseSpeed + correction, 0, 255)?

★ Средний уровень

1.На дуге радиусом 250 мм робот на скорости съезжает наружу. Что попробовать?
2.Почему на дуге меньшего радиуса нужна более сильная поправка?
3.Линия ушла под левый датчик, ошибка error = leftLevel - rightLevel отрицательная. Какое колесо должно ускориться, чтобы робот вернулся к линии?
4.Почему в функции level результат считают в long, а не в int?
5.Команда поменяла сразу kp и baseSpeed, и робот стал проходить дугу. Что не так с этим опытом?

★★ Продвинутый уровень

1.На чёрном и у края линии шкала верна, но для показания 50 при white = 120 и black = 780 функция возвращает неверное число. В чём ошибка?
int level(int value, int white, int black) {  int result = (value - black) * 100 / (white - black);  return constrain(result, 0, 100);}
2.Что вернёт эта функция для показания 450 при white = 120 и black = 780?
int level(int value, int white, int black) {  long result = (long)(value - black) / (white - black) * 100;  return constrain(result, 0, 100);}
3.Ошибку посчитали как rightLevel - leftLevel, а поправку по-прежнему прибавляют к левому колесу. Линия ушла под левый датчик. Что сделает робот?
4.На крутой дуге линия ушла за пределы обоих датчиков. Что сделает П-регулятор из урока?
5.baseSpeed = 200, kp = 1.0, ошибка = 100, скорости ограничены 0…255. Что будет с поворотом?

Отвечай на вопросы своего уровня. Нужно верных ответов: Начальный — 4 из 5; Средний — 4 из 5; Продвинутый — 4 из 5. Ответы проверит преподаватель или программа курса.

Итог

Робот поворачивает пропорционально ошибке — это П-регулятор. Он плавнее правила «стоп — едет» и проходит крутые дуги. Дальше робот научится находить перекрёстки на линии.

Как подтвердить результат

Для каждого опыта сохраняй условия, версию программы, наблюдение и вывод. Сравнивая попытки, меняй одну причину за раз; неудачные попытки тоже остаются в журнале. Отметка результата относится к уже выполненным действиям, повторять их ради отметки не нужно. Если результат не получен, запиши расхождение и следующую проверяемую гипотезу. Модель, измерения настоящего робота и объяснение — разные виды доказательств.

Словарь урока

ошибкаНасколько линия ушла от середины: разница показаний левого и правого датчиков.
поправкаНа сколько изменить скорости колёс, чтобы вернуться к линии.
коэффициентЧисло kp: во сколько раз поправка больше или меньше ошибки.
П-регуляторПравило «поправка пропорциональна ошибке»: больше ушли — сильнее поворачиваем.
constrainКоманда, которая не даёт числу выйти за границы, например за 0…255.
Тема 4 · Урок 4.1

Находим и считаем перекрёстки

Робот замечает перекрёсток, останавливается на нём и не считает один перекрёсток дважды.

2 × 45 мин1–2 ученика на роботаПрактика

Результат урока

Робот едет по линии с П-регулятором и останавливается на нужном по счёту перекрёстке. Каждый перекрёсток считается один раз.

Ты научишься

  • Описать, что показывают датчики на перекрёстке.
  • Записать условие перекрёстка и объяснить выбор порога.
  • Объяснить, почему один перекрёсток может посчитаться много раз, и исправить это.
  • Остановить робота на перекрёстке с заданным номером.

Нужно знать

  • 3.2 П-регулятор: плавно и быстро

Понадобится

Датчик линии (аналоговый, Trema) × 2

Зачем этот урок

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

Разминка · вспомни
  1. Чему равна ошибка П-регулятора, когда линия ровно посередине?
  2. Что показывает датчик по шкале 0–100 на чёрном?
  3. Что делает constrain?
1
Занятие 1 · 45 мин
Перекрёсток глазами датчиков
1

Что видят датчики на перекрёстке

12 мин

Что должен делать робот. Курьер должен заметить перекрёсток, доехать до него и при необходимости остановиться. Отдельный датчик перекрёстка не нужен: используем оба датчика линии и их показания вместе.

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

Как связать измерение с действием. Сначала проверим признаки на неподвижном роботе, затем въезд и выезд с полосы. Для счёта нужно отделить обнаружение перекрёстка от проезда за его границу. Иначе быстрый цикл насчитает одну и ту же полосу много раз. Другая широкая тёмная область тоже может дать такой сигнал: датчики не знают географию поля.

Предскажи

Робот подъезжает к перекрёстку. Что покажут оба датчика по шкале 0–100, когда он окажется на поперечной линии?

  1. Загрузи скетч L32_Error из урока 3.2.
  2. Медленно провези робота руками по тренировочной дорожке через перекрёсток. Смотри на монитор порта.
  3. Запиши в журнал показания: на линии посередине, на перекрёстке, после перекрёстка.
Должно получиться

На перекрёстке оба датчика показывают почти 0: под ними чёрная поперечная линия.

Предскажи

Робот подъезжает к перекрёстку. Что покажут оба датчика по шкале 0–100, когда он окажется на поперечной линии? А на изгибе линии?

  1. Загрузи скетч со шкалой из урока 3.2 (L32_Error или свой шаблон).
  2. Медленно провези робота руками по дорожке через изгиб и через перекрёсток. Смотри на монитор порта.
  3. Заполни таблицу и ответь: по какому признаку перекрёсток можно отличить от изгиба?
ПоложениеleftLevelrightLevel
Линия посередине
На изгибе
На перекрёстке
После перекрёстка
Должно получиться

Таблица заполнена, признак перекрёстка записан словами.

Сколько показаний робот успевает получить, пока едет по поперечной линии?

  1. Добавь в свою программу из урока 3.2 счётчик повторов loop() и раз в секунду печатай, сколько повторов прошло. Запиши число повторов в секунду на рабочей скорости.
  2. Возьми свою скорость в мм/с из урока 2.1 или измерь её заново.
  3. Посчитай: сколько повторов придётся на поперечную линию шириной 19 мм?
  4. Провези робота руками через перекрёсток и сравни показания со своей таблицей положений.
Подсказка: Как посчитать

Время на линии — ширина линии, делённая на скорость. Повторов — это время, умноженное на число повторов в секунду.

Должно получиться

В журнале — повторы в секунду, скорость, расчёт числа повторов на линии и показания на перекрёстке.

2

Условие перекрёстка

10 мин

Запишем правило: перекрёсток — когда оба датчика видят чёрное. «Чёрное» — показание меньше порога 30.

bool onCrossing() {
  return leftLevel < 30 && rightLevel < 30;
}

&& читается как «и»: условие верно, только если верны обе части.

Подсказка: Почему 30, а не 0

Даже на чёрном показание редко ровно 0, а при движении робот видит край линии. Порог 30 надёжно отделяет «чёрное» от «края», но его можно подобрать по своим измерениям.

Должно получиться

Ты можешь объяснить, почему на изгибе линии условие не срабатывает, а на перекрёстке — срабатывает.

Сформулируй правило сам по своим записям из прошлого шага: какие показания бывают только на перекрёстке и никогда — на прямой или изгибе?

  1. Запиши правило словами в журнал: что должно быть у левого датчика и что у правого.
  2. Выбери порог «чёрного» по своим измерениям. Объясни, почему не 0 и почему не 50.
  3. Запиши условие на языке Arduino. Понадобится знак «и»: &&.
Предскажи

Кто-то написал условие через || («или»). Где робот тогда остановится по ошибке?

Подсказка: Как записать «и»

a < 30 && b < 30 верно, только если верны обе части.

Должно получиться

В журнале — правило словами, порог с объяснением, условие и ответ про ||.

Требование 1. Функция onCrossing() отвечает true на поперечной линии и false во всех остальных положениях робота.

Проверь условие на бумаге, прежде чем писать код. Для каждого положения запиши показания левого и правого датчика по шкале 0–100 и ответ функции:

ПоложениеleftLevelrightLevelonCrossing()
Линия посередине
Линия под одним датчиком на крутой дуге 125 мм
Линия ушла за оба датчика
Поперечная линия
  1. Выбери порог по своим измерениям и обоснуй его: какой запас до самого тёмного показания вне перекрёстка?
  2. Найди положение, в котором твоё условие может ошибиться. Что тогда сделает робот?
Подсказка: Где искать ошибку

Провези робота руками по дуге 125 мм из урока 3.2: насколько тёмным становится «край» линии под датчиком?

Должно получиться

Таблица заполнена по измерениям, порог обоснован запасом, опасное положение и его последствие записаны.

3

Стоп на перекрёстке

20 мин
  1. Создай скетч L41_Crossing_Stop. Перенеси измерения, kp и baseSpeed из журнала.
Стоп на перекрёсткеL41_Crossing_Stop.ino.ino
const int LEFT_SENSOR_PIN = A4;const int RIGHT_SENSOR_PIN = A5;const int LEFT_DIR_PIN = 7;const int LEFT_SPEED_PIN = 6;const int RIGHT_DIR_PIN = 4;const int RIGHT_SPEED_PIN = 5;const int FORWARD = HIGH;const int BACKWARD = (FORWARD == HIGH) ? LOW : HIGH; // Измерения и настройки из журнала (уроки 3.1 и 3.2).const int LEFT_WHITE = 0;const int LEFT_BLACK = 0;const int RIGHT_WHITE = 0;const int RIGHT_BLACK = 0;float kp = 0.5;int baseSpeed = 100; int leftLevel = 100;int rightLevel = 100; int level(int value, int white, int black) {  long result = (long)(value - black) * 100 / (white - black);  return constrain(result, 0, 100);} void readSensors() {  leftLevel = level(analogRead(LEFT_SENSOR_PIN), LEFT_WHITE, LEFT_BLACK);  rightLevel = level(analogRead(RIGHT_SENSOR_PIN), RIGHT_WHITE, RIGHT_BLACK);} void drive(int leftDir, int leftSpeed, int rightDir, int rightSpeed) {  digitalWrite(LEFT_DIR_PIN, leftDir);  digitalWrite(RIGHT_DIR_PIN, rightDir);  analogWrite(LEFT_SPEED_PIN, leftSpeed);  analogWrite(RIGHT_SPEED_PIN, rightSpeed);} void stopMotors() {  analogWrite(LEFT_SPEED_PIN, 0);  analogWrite(RIGHT_SPEED_PIN, 0);} void followLineStep() {  int error = leftLevel - rightLevel;  int correction = kp * error;  drive(FORWARD, constrain(baseSpeed + correction, 0, 255),        FORWARD, constrain(baseSpeed - correction, 0, 255));} void setupRobot() {  pinMode(LEFT_DIR_PIN, OUTPUT);  pinMode(LEFT_SPEED_PIN, OUTPUT);  pinMode(RIGHT_DIR_PIN, OUTPUT);  pinMode(RIGHT_SPEED_PIN, OUTPUT);  stopMotors();  // Без измерений робот не поедет.  if (LEFT_WHITE == LEFT_BLACK || RIGHT_WHITE == RIGHT_BLACK) {    while (true) {    }  }  delay(2000);} // Перекрёсток: оба датчика видят чёрное.bool onCrossing() {  return leftLevel < 30 && rightLevel < 30;} // Ехать по линии, пока не найдём перекрёсток.void followLineToCrossing() {  while (true) {    readSensors();    if (onCrossing()) {      return;    }    followLineStep();  }} void setup() {  setupRobot();  followLineToCrossing();  stopMotors();} void loop() {}
  1. Найди функцию followLineToCrossing(). Объясни напарнику, как из неё выходит программа.
  2. Поставь робота на линию перед перекрёстком и запусти.
Должно получиться

Робот едет по линии и останавливается на первом перекрёстке.

Как это устроено: Как работает followLineToCrossing

while (true) — повторять всегда. Внутри: прочитать датчики; если перекрёсток — return, то есть выйти из функции; иначе сделать шаг П-регулятора. После выхода программа продолжается со строки stopMotors(); в setup().

Если не получилось
Что видишьЧто сделать
Робот проезжает перекрёстокСнизь baseSpeed или подними порог до 40
Робот останавливается на изгибеПорог слишком высокий, попробуй 20
Робот не едетВписаны ли измерения из журнала?
  1. Создай скетч L41_Crossing_Stop по шаблону. Перенеси измерения, kp и baseSpeed из журнала. Функции линии из урока 3.2 уже готовы.
  2. Напиши сам onCrossing() по своему правилу из прошлого шага, followLineToCrossing() и движение в setup(). Пока onCrossing() не написана, она всегда отвечает «перекрёсток», и робот не трогается.
Шаблон: стоп на перекрёсткеL41_Crossing_Stop_Gaps.ino.ino
// Шаблон среднего уровня. Напиши код в разделе «Твоя часть» по целям в комментариях. const int LEFT_SENSOR_PIN = A4;const int RIGHT_SENSOR_PIN = A5;const int LEFT_DIR_PIN = 7;const int LEFT_SPEED_PIN = 6;const int RIGHT_DIR_PIN = 4;const int RIGHT_SPEED_PIN = 5;const int FORWARD = HIGH;const int BACKWARD = (FORWARD == HIGH) ? LOW : HIGH; // Измерения и настройки из журнала (уроки 3.1 и 3.2).const int LEFT_WHITE = 0;const int LEFT_BLACK = 0;const int RIGHT_WHITE = 0;const int RIGHT_BLACK = 0;float kp = 0.5;int baseSpeed = 100;int leftLevel = 100;int rightLevel = 100; // ===== Твоя часть: напиши сам ===== bool onCrossing() {  // true — только когда робот на поперечной линии; на прямой и на изгибе — false.  return true;} void followLineToCrossing() {  // Ехать по линии с П-регулятором, пока не найдёшь перекрёсток; на перекрёстке функция заканчивается.} void setup() {  setupRobot();   // Доехать по линии до первого перекрёстка и остановиться.} // ===== Готовое из уроков 3.1–3.2 — не меняй ===== int level(int value, int white, int black) {  long result = (long)(value - black) * 100 / (white - black);  return constrain(result, 0, 100);} void readSensors() {  leftLevel = level(analogRead(LEFT_SENSOR_PIN), LEFT_WHITE, LEFT_BLACK);  rightLevel = level(analogRead(RIGHT_SENSOR_PIN), RIGHT_WHITE, RIGHT_BLACK);} void drive(int leftDir, int leftSpeed, int rightDir, int rightSpeed) {  digitalWrite(LEFT_DIR_PIN, leftDir);  digitalWrite(RIGHT_DIR_PIN, rightDir);  analogWrite(LEFT_SPEED_PIN, leftSpeed);  analogWrite(RIGHT_SPEED_PIN, rightSpeed);} void stopMotors() {  analogWrite(LEFT_SPEED_PIN, 0);  analogWrite(RIGHT_SPEED_PIN, 0);} void followLineStep() {  int error = leftLevel - rightLevel;  int correction = kp * error;  drive(FORWARD, constrain(baseSpeed + correction, 0, 255),        FORWARD, constrain(baseSpeed - correction, 0, 255));} void setupRobot() {  pinMode(LEFT_DIR_PIN, OUTPUT);  pinMode(LEFT_SPEED_PIN, OUTPUT);  pinMode(RIGHT_DIR_PIN, OUTPUT);  pinMode(RIGHT_SPEED_PIN, OUTPUT);  stopMotors();  // Без измерений робот не поедет.  if (LEFT_WHITE == LEFT_BLACK || RIGHT_WHITE == RIGHT_BLACK) {    while (true) {    }  }  delay(2000);} void loop() {}
Предскажи

Что сделает робот, если после поиска перекрёстка забыть остановить моторы?

  1. Поставь робота на линию перед перекрёстком и запусти.
Подсказка: Ехать, пока…

Функция повторяет одно и то же — прочитать датчики, сделать шаг по линии — и заканчивается, когда найден перекрёсток. Какой цикл повторяет «всегда», и как выйти из функции посреди него?

Подсказка: Как выйти из функции

return внутри функции сразу заканчивает её. Программа продолжается со следующей строки в setup().

Должно получиться

Робот едет по линии и останавливается на первом перекрёстке. Прогноз записан.

Если не получилось
Что видишьЧто сделать
Робот проезжает перекрёстокСнизь baseSpeed или подними порог
Робот останавливается на изгибеПорог слишком высокий
Робот не едетВписаны ли измерения? Написана ли onCrossing()?

Требование 2. Продолжи свою программу L32_P_Line из урока 3.2 и сохрани как L41_Crossing_Stop:

  • шаг П-регулятора вынесен в функцию followLineStep(), чтение датчиков — в readSensors(); П-регулятор при этом работает как раньше;
  • функция followLineToCrossing() ведёт робота по линии и заканчивается, как только onCrossing() вернул true;
  • в setup() робот доезжает до первого перекрёстка и останавливается;
  • защита без измерений и пауза на старте остаются.

Приёмка. Робот останавливается на перекрёстке пять раз из пяти и ни разу на дуге своего уровня.

Подсказка: Идея (1 из 3)

Функция «едет, пока…» — это бесконечный цикл, из которого есть выход по условию.

Подсказка: План (2 из 3)
  1. Повторять всегда: прочитать датчики.
  2. Перекрёсток — выйти из функции.
  3. Иначе — шаг П-регулятора.
Подсказка: Фрагмент кода (3 из 3)

while (true) { readSensors(); if (onCrossing()) { return; } … }

Должно получиться

Программа выполняет требования 1–2, пять остановок из пяти записаны в журнал.

2
Занятие 2 · 45 мин
Считаем перекрёстки
4

Найди ошибку счёта

8 мин
Найди ошибку · Робот насчитал 40 перекрёстков

Соседи хотели считать перекрёстки так:

void loop() {
  readSensors();
  if (onCrossing()) {
    count = count + 1;
  }
  followLineStep();
}

Робот проехал один перекрёсток, а count стал 40. Почему?

Должно получиться

Ты можешь объяснить: пока робот едет по широкой чёрной полосе, условие верно много раз подряд, и каждый раз счётчик растёт.

Найди ошибку · Пауза не помогла

Соседи заметили двойной счёт и добавили паузу после счёта:

void loop() {
  readSensors();
  if (onCrossing()) {
    count = count + 1;
    delay(100);
  }
  followLineStep();
}

Робот проехал один перекрёсток, а count стал 3. Почему пауза не помогла? Чем она ещё опасна для езды по линии?

  1. Запиши ответ в журнал.
  2. Предложи исправление словами: что должна сделать программа после того, как нашла перекрёсток, прежде чем искать следующий?
Должно получиться

Ты можешь объяснить, почему пауза не помогает, и предложить исправление.

Найди ошибку · Счёт по моменту появления, а всё равно 40

Соседи решили считать только момент, когда перекрёсток появился:

bool was = false;

void loop() {
  readSensors();
  bool now = onCrossing();
  if (now && !was) {
    count = count + 1;
  }
  followLineStep();
}

Робот проехал один перекрёсток, а count всё равно стал 40. Где ошибка?

  1. Найди ошибку и запиши исправленный код в журнал.
  2. Сравни 40 со своим расчётом числа повторов на линии из прошлого шага. Похоже?
  3. Чем счёт по моменту появления хуже или лучше проезда перекрёстка, который будет в следующем шаге?
Подсказка: Что хранит was

Когда в этой программе меняется значение was?

Должно получиться

Ошибка найдена, исправление записано кодом, число 40 объяснено расчётом, сравнение двух способов записано.

5

Проехать перекрёсток

10 мин

Чтобы посчитать перекрёсток один раз, его нужно проехать: ехать прямо, пока датчики не съедут с чёрного.

void passCrossing() {
  drive(FORWARD, baseSpeed, FORWARD, baseSpeed);
  do {
    readSensors();
  } while (onCrossing());
  delay(100);
}

do { … } while (условие) — сделать и повторять, пока условие верно. Короткая пауза в конце уводит датчики подальше от края поперечной линии.

Должно получиться

Ты можешь объяснить, когда закончится цикл в passCrossing().

Чтобы посчитать перекрёсток один раз, его нужно проехать: ехать прямо, пока датчики не съедут с чёрного. Напиши функцию passCrossing() в журнале:

  1. Включи оба мотора вперёд со скоростью baseSpeed.
  2. Читай датчики, пока робот на перекрёстке.
  3. После этого подожди 100 мс.

do { … } while (условие); — сделать и повторять, пока условие верно.

Подсказка: Почему do, а не while

Датчики нужно прочитать хотя бы один раз, прежде чем проверять условие.

Подсказка: Зачем пауза в конце

Сразу после чёрного датчики видят край поперечной линии. Что будет, если в этот момент снова искать перекрёсток?

Должно получиться

Функция записана в журнал. Ты можешь объяснить, когда закончится цикл и зачем пауза.

Требование 3. Функция passCrossing() проезжает перекрёсток прямо и заканчивается, когда датчики уже не на поперечной линии и не на её краю.

  1. Напиши функцию в своей программе.
  2. Исследуй паузу после схода с чёрного: 0, 50, 100, 200 мс. Для каждой запусти робота через два перекрёстка подряд (в setup() — followLineToCrossing(), passCrossing(), followLineToCrossing(), стоп) и отметь, на каком перекрёстке он остановился.
  3. Запиши, какая пауза нужна и от чего она зависит. Что будет, если поднять baseSpeed?
Пауза, мсГде остановилсяЧто видно
0
50
100
200
Подсказка: Идея (1 из 2)

Ехать прямо и повторять чтение датчиков, пока условие перекрёстка верно.

Подсказка: Фрагмент кода (2 из 2)

do { readSensors(); } while (onCrossing()); — почему здесь do, а не while?

Должно получиться

passCrossing() работает, таблица паузы заполнена, вывод о связи паузы и скорости записан.

6

Стоп на нужном перекрёстке

17 мин
  1. Создай скетч L41_Count. В TARGET укажи, на каком перекрёстке остановиться.
Счёт перекрёстковL41_Count.ino.ino
const int LEFT_SENSOR_PIN = A4;const int RIGHT_SENSOR_PIN = A5;const int LEFT_DIR_PIN = 7;const int LEFT_SPEED_PIN = 6;const int RIGHT_DIR_PIN = 4;const int RIGHT_SPEED_PIN = 5;const int FORWARD = HIGH;const int BACKWARD = (FORWARD == HIGH) ? LOW : HIGH; // Измерения и настройки из журнала (уроки 3.1 и 3.2).const int LEFT_WHITE = 0;const int LEFT_BLACK = 0;const int RIGHT_WHITE = 0;const int RIGHT_BLACK = 0;float kp = 0.5;int baseSpeed = 100; int leftLevel = 100;int rightLevel = 100; int level(int value, int white, int black) {  long result = (long)(value - black) * 100 / (white - black);  return constrain(result, 0, 100);} void readSensors() {  leftLevel = level(analogRead(LEFT_SENSOR_PIN), LEFT_WHITE, LEFT_BLACK);  rightLevel = level(analogRead(RIGHT_SENSOR_PIN), RIGHT_WHITE, RIGHT_BLACK);} void drive(int leftDir, int leftSpeed, int rightDir, int rightSpeed) {  digitalWrite(LEFT_DIR_PIN, leftDir);  digitalWrite(RIGHT_DIR_PIN, rightDir);  analogWrite(LEFT_SPEED_PIN, leftSpeed);  analogWrite(RIGHT_SPEED_PIN, rightSpeed);} void stopMotors() {  analogWrite(LEFT_SPEED_PIN, 0);  analogWrite(RIGHT_SPEED_PIN, 0);} void followLineStep() {  int error = leftLevel - rightLevel;  int correction = kp * error;  drive(FORWARD, constrain(baseSpeed + correction, 0, 255),        FORWARD, constrain(baseSpeed - correction, 0, 255));} void setupRobot() {  pinMode(LEFT_DIR_PIN, OUTPUT);  pinMode(LEFT_SPEED_PIN, OUTPUT);  pinMode(RIGHT_DIR_PIN, OUTPUT);  pinMode(RIGHT_SPEED_PIN, OUTPUT);  stopMotors();  // Без измерений робот не поедет.  if (LEFT_WHITE == LEFT_BLACK || RIGHT_WHITE == RIGHT_BLACK) {    while (true) {    }  }  delay(2000);} // Перекрёсток: оба датчика видят чёрное.bool onCrossing() {  return leftLevel < 30 && rightLevel < 30;} // Ехать по линии, пока не найдём перекрёсток.void followLineToCrossing() {  while (true) {    readSensors();    if (onCrossing()) {      return;    }    followLineStep();  }} // Проехать перекрёсток прямо, пока датчики не съедут с чёрного.void passCrossing() {  drive(FORWARD, baseSpeed, FORWARD, baseSpeed);  do {    readSensors();  } while (onCrossing());  delay(100);} const int TARGET = 1; // на каком перекрёстке остановиться void setup() {  setupRobot();  for (int i = 1; i < TARGET; i++) {    followLineToCrossing();    passCrossing();  }  followLineToCrossing();  stopMotors();} void loop() {}
Предскажи

TARGET = 2. Сколько раз выполнится цикл for? Что робот сделает на первом перекрёстке?

  1. Проверь на дорожке с тремя перекрёстками: TARGET = 1, 2, 3.
Должно получиться

Робот останавливается ровно на перекрёстке с номером TARGET.

  1. Создай скетч L41_Count по шаблону. В TARGET укажи, на каком перекрёстке остановиться.
  2. Напиши сам passCrossing() — по своей записи в журнале — и движение в setup(). Пока твоя часть не написана, робот не трогается.
Шаблон: счёт перекрёстковL41_Count_Gaps.ino.ino
// Шаблон среднего уровня. Напиши код в разделе «Твоя часть» по целям в комментариях. const int LEFT_SENSOR_PIN = A4;const int RIGHT_SENSOR_PIN = A5;const int LEFT_DIR_PIN = 7;const int LEFT_SPEED_PIN = 6;const int RIGHT_DIR_PIN = 4;const int RIGHT_SPEED_PIN = 5;const int FORWARD = HIGH;const int BACKWARD = (FORWARD == HIGH) ? LOW : HIGH; // Измерения и настройки из журнала (уроки 3.1 и 3.2).const int LEFT_WHITE = 0;const int LEFT_BLACK = 0;const int RIGHT_WHITE = 0;const int RIGHT_BLACK = 0;float kp = 0.5;int baseSpeed = 100;int leftLevel = 100;int rightLevel = 100;const int TARGET = 1; // на каком перекрёстке остановиться // ===== Твоя часть: напиши сам ===== void passCrossing() {  // Проехать перекрёсток прямо. Функция заканчивается, когда датчики уже не на поперечной линии и не на её краю.} void setup() {  setupRobot();   // Перекрёстки до TARGET — найти и проехать прямо; на перекрёстке номер TARGET остановиться.} // ===== Готовое из уроков 3.2 и 4.1 — не меняй ===== int level(int value, int white, int black) {  long result = (long)(value - black) * 100 / (white - black);  return constrain(result, 0, 100);} void readSensors() {  leftLevel = level(analogRead(LEFT_SENSOR_PIN), LEFT_WHITE, LEFT_BLACK);  rightLevel = level(analogRead(RIGHT_SENSOR_PIN), RIGHT_WHITE, RIGHT_BLACK);} void drive(int leftDir, int leftSpeed, int rightDir, int rightSpeed) {  digitalWrite(LEFT_DIR_PIN, leftDir);  digitalWrite(RIGHT_DIR_PIN, rightDir);  analogWrite(LEFT_SPEED_PIN, leftSpeed);  analogWrite(RIGHT_SPEED_PIN, rightSpeed);} void stopMotors() {  analogWrite(LEFT_SPEED_PIN, 0);  analogWrite(RIGHT_SPEED_PIN, 0);} void followLineStep() {  int error = leftLevel - rightLevel;  int correction = kp * error;  drive(FORWARD, constrain(baseSpeed + correction, 0, 255),        FORWARD, constrain(baseSpeed - correction, 0, 255));} void setupRobot() {  pinMode(LEFT_DIR_PIN, OUTPUT);  pinMode(LEFT_SPEED_PIN, OUTPUT);  pinMode(RIGHT_DIR_PIN, OUTPUT);  pinMode(RIGHT_SPEED_PIN, OUTPUT);  stopMotors();  // Без измерений робот не поедет.  if (LEFT_WHITE == LEFT_BLACK || RIGHT_WHITE == RIGHT_BLACK) {    while (true) {    }  }  delay(2000);} // Перекрёсток: оба датчика видят чёрное.bool onCrossing() {  return leftLevel < 30 && rightLevel < 30;} // Ехать по линии, пока не найдём перекрёсток.void followLineToCrossing() {  while (true) {    readSensors();    if (onCrossing()) {      return;    }    followLineStep();  }} void loop() {}
Предскажи

TARGET = 3. Сколько раз робот должен проехать перекрёсток прямо?

  1. Проверь на дорожке с тремя перекрёстками: TARGET = 1, 2, 3.
Подсказка: Повторить нужное число раз

Цикл for из урока 1.1 повторяет команды заданное число раз. Сколько перекрёстков нужно проехать перед тем, на котором остановиться?

Должно получиться

Робот останавливается ровно на перекрёстке с номером TARGET.

Требование 4. Функция goToCrossing(n) ведёт робота по линии, проезжает прямо перекрёстки до n-го и останавливается на n-м. Сохрани программу как L41_Count.

Приёмка. Дорожка с тремя перекрёстками, n = 1, 2, 3, по три запуска на каждое.

nЗапуск 1Запуск 2Запуск 3
1
2
3
Предскажи

Что должна сделать функция при n = 0? Реши сам, запиши и проверь, что программа делает именно это.

Подсказка: План (1 из 2)

n - 1 раз: найти перекрёсток и проехать его. Потом найти последний и остановиться.

Подсказка: Фрагмент кода (2 из 2)

for (int i = 1; i < n; i++) { … } — сколько раз выполнится цикл при n = 1?

Должно получиться

Девять запусков без ошибки в таблице, поведение при n = 0 выбрано и проверено.

7

Задания по уровням

10 мин
Основное · Первый перекрёсток

Доехать по линии до первого перекрёстка и остановиться на нём.

Средний уровень · Третий перекрёсток

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

Продвинутый уровень · Без остановок

Сделай так, чтобы робот проезжал все перекрёстки без остановки и не терял линию после них. Подсказка: в loop() — если перекрёсток, passCrossing(), иначе шаг П-регулятора.

Проверь себя

Проверь себя

Выберите ответы для подготовки. Они не сохраняются и не оцениваются на сайте. Пройдите проверку знаний в программе курса или обсудите ответы с преподавателем.

● Основное

1.По какому признаку программа узнаёт перекрёсток?
2.Что вернёт выражение leftLevel < 30 && rightLevel < 30, если leftLevel = 10, rightLevel = 80?
3.Почему счётчик перекрёстков растёт много раз на одном перекрёстке?
4.Что делает return внутри followLineToCrossing()?
5.TARGET = 3. Сколько перекрёстков робот проедет прямо до остановки?

★ Средний уровень

1.Счётчик crossings начинается с 0 и увеличивается на каждом перекрёстке. При каком значении остановиться на третьем перекрёстке?
2.Условие перекрёстка записали через «или»: leftLevel < 30 || rightLevel < 30. Где робот остановится?
3.Почему порог «чёрного» — 30, а не 0?
4.В шаблоне onCrossing() пока возвращает true. Что делает робот после запуска?
5.Зачем в passCrossing() цикл do … while, а не while?

★★ Продвинутый уровень

1.Что должна делать функция passCrossing()?
2.Что помогает не потерять линию сразу после перекрёстка? Выбери все верные ответы.
3.Без паузы после схода с поперечной линии робот иногда останавливается сразу за перекрёстком. Почему?
4.Вызвали goToCrossing(2). На каком перекрёстке остановится робот?
void goToCrossing(int n) {  for (int i = 0; i < n; i++) {    followLineToCrossing();    passCrossing();  }  followLineToCrossing();  stopMotors();}
5.На крутой дуге робот иногда останавливается, будто на перекрёстке. Что проверить первым?

Отвечай на вопросы своего уровня. Нужно верных ответов: Начальный — 4 из 5; Средний — 4 из 5; Продвинутый — 4 из 5. Ответы проверит преподаватель или программа курса.

Итог

Робот находит перекрёстки и считает их без повторов. Следующий шаг — сворачивать на перекрёстке в нужную сторону и проходить маршрут.

Как подтвердить результат

Для каждого опыта сохраняй условия, версию программы, наблюдение и вывод. Сравнивая попытки, меняй одну причину за раз; неудачные попытки тоже остаются в журнале. Отметка результата относится к уже выполненным действиям, повторять их ради отметки не нужно. Если результат не получен, запиши расхождение и следующую проверяемую гипотезу. Модель, измерения настоящего робота и объяснение — разные виды доказательств.

Словарь урока

перекрёстокМесто, где линию пересекает другая линия. Оба датчика одновременно видят чёрное.
порогЧисло для сравнения: показание меньше 30 по шкале 0–100 считаем чёрным.
цикл whileПовторяет команды, пока условие верно.
returnНемедленно завершает функцию и возвращает программу туда, откуда функцию вызвали.
Тема 4 · Урок 4.2

Повороты на перекрёстке и маршрут

Робот сворачивает с перекрёстка на нужную линию и проходит маршрут из нескольких решений.

2 × 45 мин1–2 ученика на роботаПрактика

Результат урока

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

Ты научишься

  • Объяснить, почему поворот по датчикам надёжнее поворота по времени.
  • Подобрать выезд на центр перекрёстка, чтобы после поворота робот оказался на линии.
  • Записать маршрут последовательностью решений «налево / направо / прямо».
  • Проверить маршрут на поле и найти, на каком решении он ломается.

Нужно знать

  • 4.1 Находим и считаем перекрёстки
  • 2.2 Повороты и развороты

Понадобится

Датчик линии (аналоговый, Trema) × 2

Зачем этот урок

Робот находит перекрёсток. Теперь он должен свернуть — к грузу, к месту доставки или дальше по дороге. В уроке 2.2 повороты были по времени и зависели от батареи. Сегодня робот будет поворачивать по датчикам: крутиться, пока не найдёт новую линию.

Разминка · вспомни
  1. Почему поворот по времени получается разным при разряженной батарее?
  2. Что делает passCrossing()?
  3. Как робот понимает, что под ним перекрёсток?
1
Занятие 1 · 45 мин
Поворот по датчикам
1

Как повернуть точно

8 мин

Что должен делать робот. Роботу нужно пройти несколько перекрёстков и выбрать нужное направление на каждом. Используем датчики линии для обнаружения событий и заранее записанный план маршрута для выбора действия.

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

Как связать измерение с действием. Проверяем маршрут по участкам: отмечаем, какой перекрёсток найден и какое действие выполнено. Если робот не обнаружил полосу или повернул не туда, следующие команды будут выполняться уже в другом месте. Поэтому сначала проверим каждую функцию, затем соберём целый маршрут.

Предскажи

Робот стоит на перекрёстке и крутится налево. Как он может сам понять, что повернул достаточно и уже смотрит вдоль новой линии?

  1. Обсудите с напарником. Поставьте робота на перекрёсток и медленно поверните руками.
  2. Следите за левым датчиком: когда он окажется над новой линией?
Должно получиться

Ты можешь объяснить идею: крутиться, пока датчик со стороны поворота не увидит новую линию.

Предскажи

Робот стоит на перекрёстке и крутится налево. Как он может сам понять, что повернул достаточно и уже смотрит вдоль новой линии?

  1. Поставь робота на центр перекрёстка и медленно поверни руками налево, потом направо.
  2. Для каждого поворота запиши: какой датчик первым оказался над новой линией и какой датчик проходит над старой линией в начале поворота.
ПоворотПервым видит новую линиюСначала проходит над старой
Налево
Направо
Должно получиться

Таблица заполнена. Ты можешь объяснить идею: крутиться, пока датчик со стороны поворота не увидит новую линию.

Проверь идею «крутиться до новой линии» измерением, прежде чем её программировать.

  1. Положи под робота лист с транспортиром или линиями через 10°. Поставь робота на центр перекрёстка.
  2. Медленно поворачивай руками налево. Запиши угол, на котором левый датчик впервые видит новую линию, и угол, на котором робот смотрит точно вдоль неё.
  3. Повтори направо.
  4. Совпадают ли углы? Что тогда сделает робот, который останавливается в момент, когда датчик увидел линию? Как это исправить?
Должно получиться

В журнале — два угла для каждого поворота и вывод о том, где остановится робот.

2

Выезд на центр перекрёстка

12 мин

Датчики стоят впереди колёс. Когда датчики заметили перекрёсток, колёса ещё до него не доехали. Если повернуть сразу, робот окажется сбоку от новой линии. Поэтому сначала надо проехать вперёд так, чтобы колёса встали над центром перекрёстка.

  1. Поставь робота так, чтобы датчики были на поперечной линии.
  2. Измерь линейкой, сколько ещё ехать колёсам до центра перекрёстка. Запиши в журнал.
  3. Время этого проезда — centerMs — подберём на следующем шаге.
Должно получиться

Ты знаешь, почему перед поворотом нужен выезд на центр.

Датчики стоят впереди колёс. Когда датчики заметили перекрёсток, колёса ещё до него не доехали.

Предскажи

Робот повернул на месте сразу, как только датчики увидели перекрёсток. Где окажется робот относительно новой линии? Покажи на дорожке руками.

  1. Поставь робота датчиками на поперечную линию и поверни его руками на месте на 90°. Где оказались датчики — над новой линией или сбоку?
  2. Измерь линейкой, сколько ещё ехать колёсам до центра перекрёстка. Запиши в журнал.
  3. Время этого проезда — centerMs — подберёшь на следующем шаге. Как ты поймёшь, что время слишком маленькое, а как — что слишком большое? Запиши.
Должно получиться

Прогноз проверен руками, расстояние записано, признаки «мало» и «много» для centerMs записаны.

Датчики стоят впереди колёс, поэтому перед поворотом робот выезжает на центр перекрёстка. Рассчитай время этого выезда, а не подбирай наугад.

  1. Измерь расстояние от датчиков до оси колёс в миллиметрах.
  2. Возьми из журнала урока 2.1 свою скорость в миллиметрах в секунду при близкой baseSpeed. Нет таблицы — измерь: проезд 1 секунду, путь линейкой, три раза.
  3. Посчитай centerMs и запиши расчёт. На следующем шаге сравни его с подобранным на дорожке.
  4. Запиши, почему выезд по времени — слабое место и от чего его точность зависит.
Должно получиться

В журнале — расстояние, скорость, расчёт centerMs и две причины, по которым выезд по времени может ошибаться.

3

Поворот на перекрёстке

22 мин
  1. Создай скетч L42_Turn_At_Crossing. Перенеси измерения, kp и baseSpeed.
Поворот на перекрёсткеL42_Turn_At_Crossing.ino.ino
const int LEFT_SENSOR_PIN = A4;const int RIGHT_SENSOR_PIN = A5;const int LEFT_DIR_PIN = 7;const int LEFT_SPEED_PIN = 6;const int RIGHT_DIR_PIN = 4;const int RIGHT_SPEED_PIN = 5;const int FORWARD = HIGH;const int BACKWARD = (FORWARD == HIGH) ? LOW : HIGH; // Измерения и настройки из журнала (уроки 3.1 и 3.2).const int LEFT_WHITE = 0;const int LEFT_BLACK = 0;const int RIGHT_WHITE = 0;const int RIGHT_BLACK = 0;float kp = 0.5;int baseSpeed = 100; int leftLevel = 100;int rightLevel = 100; int level(int value, int white, int black) {  long result = (long)(value - black) * 100 / (white - black);  return constrain(result, 0, 100);} void readSensors() {  leftLevel = level(analogRead(LEFT_SENSOR_PIN), LEFT_WHITE, LEFT_BLACK);  rightLevel = level(analogRead(RIGHT_SENSOR_PIN), RIGHT_WHITE, RIGHT_BLACK);} void drive(int leftDir, int leftSpeed, int rightDir, int rightSpeed) {  digitalWrite(LEFT_DIR_PIN, leftDir);  digitalWrite(RIGHT_DIR_PIN, rightDir);  analogWrite(LEFT_SPEED_PIN, leftSpeed);  analogWrite(RIGHT_SPEED_PIN, rightSpeed);} void stopMotors() {  analogWrite(LEFT_SPEED_PIN, 0);  analogWrite(RIGHT_SPEED_PIN, 0);} void followLineStep() {  int error = leftLevel - rightLevel;  int correction = kp * error;  drive(FORWARD, constrain(baseSpeed + correction, 0, 255),        FORWARD, constrain(baseSpeed - correction, 0, 255));} void setupRobot() {  pinMode(LEFT_DIR_PIN, OUTPUT);  pinMode(LEFT_SPEED_PIN, OUTPUT);  pinMode(RIGHT_DIR_PIN, OUTPUT);  pinMode(RIGHT_SPEED_PIN, OUTPUT);  stopMotors();  // Без измерений робот не поедет.  if (LEFT_WHITE == LEFT_BLACK || RIGHT_WHITE == RIGHT_BLACK) {    while (true) {    }  }  delay(2000);} // Перекрёсток: оба датчика видят чёрное.bool onCrossing() {  return leftLevel < 30 && rightLevel < 30;} // Ехать по линии, пока не найдём перекрёсток.void followLineToCrossing() {  while (true) {    readSensors();    if (onCrossing()) {      return;    }    followLineStep();  }} // Проехать перекрёсток прямо, пока датчики не съедут с чёрного.void passCrossing() {  drive(FORWARD, baseSpeed, FORWARD, baseSpeed);  do {    readSensors();  } while (onCrossing());  delay(100);} int turnSpeed = 100;int centerMs = 250; // подбираем: проехать от датчиков до центра перекрёсткаint leaveMs = 200;  // время, чтобы датчик ушёл со старой линии // Выехать на центр перекрёстка: датчики стоят впереди колёс.void toCenter() {  drive(FORWARD, baseSpeed, FORWARD, baseSpeed);  delay(centerMs);  stopMotors();} // Крутиться на месте, пока датчик со стороны поворота не найдёт новую линию.void turnUntilLine(bool left) {  if (left) {    drive(BACKWARD, turnSpeed, FORWARD, turnSpeed);  } else {    drive(FORWARD, turnSpeed, BACKWARD, turnSpeed);  }  delay(leaveMs);  do {    readSensors();  } while ((left ? leftLevel : rightLevel) > 30);  stopMotors();} void turnLeftAtCrossing() {  toCenter();  turnUntilLine(true);} void turnRightAtCrossing() {  toCenter();  turnUntilLine(false);} // Развернуться на линии: крутиться, пока линия снова не окажется под датчиком.void turnAroundOnLine() {  turnUntilLine(true);} // Ехать по линии заданное время.void followLineFor(unsigned long ms) {  unsigned long start = millis();  while (millis() - start < ms) {    readSensors();    followLineStep();  }  stopMotors();} void setup() {  setupRobot();  followLineToCrossing();  turnLeftAtCrossing();  followLineFor(1500);  turnAroundOnLine();  followLineToCrossing();  stopMotors();} void loop() {}
  1. Разберите функцию turnUntilLine. Зачем в ней delay(leaveMs) перед поиском линии?
  2. Запусти на дорожке с перекрёстком. Подбери centerMs: после поворота робот должен уверенно поехать по новой линии.
Должно получиться

Робот доезжает до перекрёстка, поворачивает налево и едет по новой линии.

Как это устроено: Зачем leaveMs

В начале поворота датчик ещё над старой линией и сразу «нашёл бы» её. Поэтому первые leaveMs миллисекунд робот просто крутится, не глядя на датчики, — чтобы датчик ушёл со старой линии. Потом ищем новую.

Если не получилось
Что видишьЧто сделать
После поворота робот сбоку от линииПодбери centerMs
Робот крутится бесконечноДатчик не видит линию: проверь порог 30 и измерения
Робот останавливается сразуУвеличь leaveMs
  1. Создай скетч L42_Turn_At_Crossing по шаблону. Перенеси измерения, kp и baseSpeed. Функции линии и перекрёстков уже готовы.
  2. Напиши сам выезд на центр toCenter(), поворот до линии turnUntilLine() и повороты с перекрёстка налево и направо. setup() ниже проверяет их на дорожке.
Шаблон: поворот на перекрёсткеL42_Turn_At_Crossing_Gaps.ino.ino
// Шаблон среднего уровня. Напиши код в разделе «Твоя часть» по целям в комментариях.// setup() ниже — проверка твоих функций на дорожке. const int LEFT_SENSOR_PIN = A4;const int RIGHT_SENSOR_PIN = A5;const int LEFT_DIR_PIN = 7;const int LEFT_SPEED_PIN = 6;const int RIGHT_DIR_PIN = 4;const int RIGHT_SPEED_PIN = 5;const int FORWARD = HIGH;const int BACKWARD = (FORWARD == HIGH) ? LOW : HIGH; // Измерения и настройки из журнала (уроки 3.1 и 3.2).const int LEFT_WHITE = 0;const int LEFT_BLACK = 0;const int RIGHT_WHITE = 0;const int RIGHT_BLACK = 0;float kp = 0.5;int baseSpeed = 100;int leftLevel = 100;int rightLevel = 100;int turnSpeed = 100;int centerMs = 250; // подбираем: проехать от датчиков до центра перекрёсткаint leaveMs = 200;  // время, чтобы датчик ушёл со старой линии // ===== Твоя часть: напиши сам ===== void toCenter() {  // Колёса встают над центром перекрёстка: проехать вперёд centerMs и остановиться.} void turnUntilLine(bool left) {  // Крутиться на месте (left = true — налево) до новой линии; в конце колёса стоят.  // Пока датчик ещё над старой линией, она не должна считаться новой — для этого есть leaveMs.} void turnLeftAtCrossing() {  // Повернуть с перекрёстка налево на новую линию.} void turnRightAtCrossing() {  // Повернуть с перекрёстка направо на новую линию.} // ===== Готовое из уроков 3.2–4.1 и проверка — не меняй ===== int level(int value, int white, int black) {  long result = (long)(value - black) * 100 / (white - black);  return constrain(result, 0, 100);} void readSensors() {  leftLevel = level(analogRead(LEFT_SENSOR_PIN), LEFT_WHITE, LEFT_BLACK);  rightLevel = level(analogRead(RIGHT_SENSOR_PIN), RIGHT_WHITE, RIGHT_BLACK);} void drive(int leftDir, int leftSpeed, int rightDir, int rightSpeed) {  digitalWrite(LEFT_DIR_PIN, leftDir);  digitalWrite(RIGHT_DIR_PIN, rightDir);  analogWrite(LEFT_SPEED_PIN, leftSpeed);  analogWrite(RIGHT_SPEED_PIN, rightSpeed);} void stopMotors() {  analogWrite(LEFT_SPEED_PIN, 0);  analogWrite(RIGHT_SPEED_PIN, 0);} void followLineStep() {  int error = leftLevel - rightLevel;  int correction = kp * error;  drive(FORWARD, constrain(baseSpeed + correction, 0, 255),        FORWARD, constrain(baseSpeed - correction, 0, 255));} void setupRobot() {  pinMode(LEFT_DIR_PIN, OUTPUT);  pinMode(LEFT_SPEED_PIN, OUTPUT);  pinMode(RIGHT_DIR_PIN, OUTPUT);  pinMode(RIGHT_SPEED_PIN, OUTPUT);  stopMotors();  // Без измерений робот не поедет.  if (LEFT_WHITE == LEFT_BLACK || RIGHT_WHITE == RIGHT_BLACK) {    while (true) {    }  }  delay(2000);} // Перекрёсток: оба датчика видят чёрное.bool onCrossing() {  return leftLevel < 30 && rightLevel < 30;} // Ехать по линии, пока не найдём перекрёсток.void followLineToCrossing() {  while (true) {    readSensors();    if (onCrossing()) {      return;    }    followLineStep();  }} // Проехать перекрёсток прямо, пока датчики не съедут с чёрного.void passCrossing() {  drive(FORWARD, baseSpeed, FORWARD, baseSpeed);  do {    readSensors();  } while (onCrossing());  delay(100);} // Развернуться на линии: крутиться, пока линия снова не окажется под датчиком.void turnAroundOnLine() {  turnUntilLine(true);} // Ехать по линии заданное время.void followLineFor(unsigned long ms) {  unsigned long start = millis();  while (millis() - start < ms) {    readSensors();    followLineStep();  }  stopMotors();} void setup() {  setupRobot();  followLineToCrossing();  turnLeftAtCrossing();  followLineFor(1500);  turnAroundOnLine();  followLineToCrossing();  stopMotors();} void loop() {}
Предскажи

Что случится, если начать искать новую линию сразу, не выждав leaveMs?

  1. Запусти на дорожке с перекрёстком. Подбери centerMs по признакам, которые записал в прошлом шаге.
Подсказка: Крутиться до линии

Это похоже на passCrossing() из урока 4.1: повторять чтение датчиков, пока условие верно. Какое условие значит «линии под датчиком ещё нет»?

Подсказка: Какой датчик смотреть

Поворачиваем налево — новую линию первым увидит левый датчик.

Должно получиться

Робот доезжает до перекрёстка, поворачивает налево и едет по новой линии. Прогноз записан.

Если не получилось
Что видишьЧто сделать
После поворота робот сбоку от линииПодбери centerMs
Робот крутится бесконечноДатчик не видит линию: проверь условие поиска и измерения
Робот останавливается сразуУвеличь leaveMs

Требование 1. Продолжи свою программу L41_Count и сохрани как L42_Turn_At_Crossing. Функция turnUntilLine(left) поворачивает робота на месте до новой линии:

  • сначала робот крутится, не глядя на датчики, пока датчик не уйдёт со старой линии;
  • потом — пока датчик со стороны поворота не увидит линию;
  • если линия не нашлась за 3 секунды, робот останавливается: на поле без защиты он будет крутиться бесконечно.

Требование 2. turnLeftAtCrossing() и turnRightAtCrossing(): выезд на центр по твоему centerMs, затем поворот до линии.

Приёмка. По пять поворотов налево и направо: после каждого робот уезжает по новой линии. Отдельно проверь защиту: подними робота над столом во время поворота.

Подсказка: Идея (1 из 3)

Поиск линии — цикл «читать датчики, пока линии нет». Для защиты запомни время начала поиска.

Подсказка: План (2 из 3)
  1. Включить поворот, подождать leaveMs.
  2. Запомнить millis().
  3. Повторять чтение, пока линии нет и прошло меньше 3 секунд.
  4. Остановить моторы.
Подсказка: Фрагмент кода (3 из 3)

while ((left ? leftLevel : rightLevel) > 30 && millis() - start < 3000) — прочитай условие словами.

Должно получиться

Программа выполняет требования 1–2, десять поворотов и проверка защиты записаны. Подобранный centerMs сравнён с расчётом.

2
Занятие 2 · 45 мин
Маршрут из решений
4

Разворот на линии

10 мин

В конце ответвления, у груза или места доставки, роботу нужно развернуться и вернуться к перекрёстку. Разворот — тот же поворот по датчикам, только линия теперь сзади.

  1. В скетче найди вызов turnAroundOnLine(). Проверь на дорожке: проехать по ответвлению, развернуться, вернуться к перекрёстку.
Должно получиться

Робот разворачивается, находит ту же линию и возвращается к перекрёстку.

В конце ответвления роботу нужно развернуться и вернуться к перекрёстку. Разворот — тот же поворот по датчикам, только линия теперь сзади.

  1. Найди в скетче функцию turnAroundOnLine(). Почему в ней нет выезда на центр, как в повороте на перекрёстке? Запиши ответ.
  2. Проверь на дорожке три раза: проехать по ответвлению, развернуться, вернуться к перекрёстку.
Должно получиться

Робот три раза разворачивается и возвращается к перекрёстку. Ответ про выезд на центр записан.

Требование 3. turnAroundOnLine() разворачивает робота на линии и останавливается, когда линия снова под датчиком. Защита 3 секунды из требования 1 работает и здесь.

Приёмка. Пять разворотов на конце ответвления: после каждого робот возвращается к перекрёстку. Для каждого измерь, насколько нос смотрит мимо линии сразу после разворота.

РазворотВернулся?Нос мимо линии, мм
1

Если нос каждый раз смотрит в одну сторону, предложи и проверь одно исправление.

Должно получиться

Пять разворотов в таблице, исправление систематической ошибки проверено.

5

Маршрут по порядку

22 мин

Маршрут — это решения на перекрёстках по порядку. Робот не видит, какой это перекрёсток: он просто выполняет следующее решение.

  1. Создай скетч L42_Route. Начерти в журнале тренировочную дорожку и путь робота.
МаршрутL42_Route.ino.ino
const int LEFT_SENSOR_PIN = A4;const int RIGHT_SENSOR_PIN = A5;const int LEFT_DIR_PIN = 7;const int LEFT_SPEED_PIN = 6;const int RIGHT_DIR_PIN = 4;const int RIGHT_SPEED_PIN = 5;const int FORWARD = HIGH;const int BACKWARD = (FORWARD == HIGH) ? LOW : HIGH; // Измерения и настройки из журнала (уроки 3.1 и 3.2).const int LEFT_WHITE = 0;const int LEFT_BLACK = 0;const int RIGHT_WHITE = 0;const int RIGHT_BLACK = 0;float kp = 0.5;int baseSpeed = 100; int leftLevel = 100;int rightLevel = 100; int level(int value, int white, int black) {  long result = (long)(value - black) * 100 / (white - black);  return constrain(result, 0, 100);} void readSensors() {  leftLevel = level(analogRead(LEFT_SENSOR_PIN), LEFT_WHITE, LEFT_BLACK);  rightLevel = level(analogRead(RIGHT_SENSOR_PIN), RIGHT_WHITE, RIGHT_BLACK);} void drive(int leftDir, int leftSpeed, int rightDir, int rightSpeed) {  digitalWrite(LEFT_DIR_PIN, leftDir);  digitalWrite(RIGHT_DIR_PIN, rightDir);  analogWrite(LEFT_SPEED_PIN, leftSpeed);  analogWrite(RIGHT_SPEED_PIN, rightSpeed);} void stopMotors() {  analogWrite(LEFT_SPEED_PIN, 0);  analogWrite(RIGHT_SPEED_PIN, 0);} void followLineStep() {  int error = leftLevel - rightLevel;  int correction = kp * error;  drive(FORWARD, constrain(baseSpeed + correction, 0, 255),        FORWARD, constrain(baseSpeed - correction, 0, 255));} void setupRobot() {  pinMode(LEFT_DIR_PIN, OUTPUT);  pinMode(LEFT_SPEED_PIN, OUTPUT);  pinMode(RIGHT_DIR_PIN, OUTPUT);  pinMode(RIGHT_SPEED_PIN, OUTPUT);  stopMotors();  // Без измерений робот не поедет.  if (LEFT_WHITE == LEFT_BLACK || RIGHT_WHITE == RIGHT_BLACK) {    while (true) {    }  }  delay(2000);} // Перекрёсток: оба датчика видят чёрное.bool onCrossing() {  return leftLevel < 30 && rightLevel < 30;} // Ехать по линии, пока не найдём перекрёсток.void followLineToCrossing() {  while (true) {    readSensors();    if (onCrossing()) {      return;    }    followLineStep();  }} // Проехать перекрёсток прямо, пока датчики не съедут с чёрного.void passCrossing() {  drive(FORWARD, baseSpeed, FORWARD, baseSpeed);  do {    readSensors();  } while (onCrossing());  delay(100);} int turnSpeed = 100;int centerMs = 250; // подбираем: проехать от датчиков до центра перекрёсткаint leaveMs = 200;  // время, чтобы датчик ушёл со старой линии // Выехать на центр перекрёстка: датчики стоят впереди колёс.void toCenter() {  drive(FORWARD, baseSpeed, FORWARD, baseSpeed);  delay(centerMs);  stopMotors();} // Крутиться на месте, пока датчик со стороны поворота не найдёт новую линию.void turnUntilLine(bool left) {  if (left) {    drive(BACKWARD, turnSpeed, FORWARD, turnSpeed);  } else {    drive(FORWARD, turnSpeed, BACKWARD, turnSpeed);  }  delay(leaveMs);  do {    readSensors();  } while ((left ? leftLevel : rightLevel) > 30);  stopMotors();} void turnLeftAtCrossing() {  toCenter();  turnUntilLine(true);} void turnRightAtCrossing() {  toCenter();  turnUntilLine(false);} // Развернуться на линии: крутиться, пока линия снова не окажется под датчиком.void turnAroundOnLine() {  turnUntilLine(true);} // Ехать по линии заданное время.void followLineFor(unsigned long ms) {  unsigned long start = millis();  while (millis() - start < ms) {    readSensors();    followLineStep();  }  stopMotors();} // Маршрут: решения на перекрёстках по порядку.void setup() {  setupRobot();   followLineToCrossing();  turnLeftAtCrossing();   // решение 1: налево   followLineToCrossing();  passCrossing();         // решение 2: прямо   followLineToCrossing();  turnRightAtCrossing();  // решение 3: направо   followLineToCrossing();  stopMotors();           // конец маршрута} void loop() {}
Предскажи

По программе: налево, прямо, направо. Нарисуй, где робот остановится на дорожке.

  1. Запусти. Если маршрут сломался — на каком решении? Почему?
Должно получиться

Робот проходит маршрут из трёх решений и останавливается там, где ты предсказал.

Найди ошибку · Робот поворачивает не на том перекрёстке

У соседей после turnLeftAtCrossing() сразу стоит turnRightAtCrossing(), без followLineToCrossing() между ними. Что сделает робот?

Маршрут — это решения на перекрёстках по порядку. Робот не видит, какой это перекрёсток: он просто выполняет следующее решение.

  1. Начерти в журнале тренировочную дорожку и путь: налево, прямо, направо.
  2. Создай скетч L42_Route по шаблону и напиши маршрут в setup() сам из готовых функций урока.
Шаблон: маршрутL42_Route_Gaps.ino.ino
// Шаблон среднего уровня. Напиши код в разделе «Твоя часть» по целям в комментариях. const int LEFT_SENSOR_PIN = A4;const int RIGHT_SENSOR_PIN = A5;const int LEFT_DIR_PIN = 7;const int LEFT_SPEED_PIN = 6;const int RIGHT_DIR_PIN = 4;const int RIGHT_SPEED_PIN = 5;const int FORWARD = HIGH;const int BACKWARD = (FORWARD == HIGH) ? LOW : HIGH; // Измерения и настройки из журнала (уроки 3.1 и 3.2).const int LEFT_WHITE = 0;const int LEFT_BLACK = 0;const int RIGHT_WHITE = 0;const int RIGHT_BLACK = 0;float kp = 0.5;int baseSpeed = 100;int leftLevel = 100;int rightLevel = 100;int turnSpeed = 100;int centerMs = 250; // подбираем: проехать от датчиков до центра перекрёсткаint leaveMs = 200;  // время, чтобы датчик ушёл со старой линии // ===== Твоя часть: напиши сам ===== void setup() {  setupRobot();   // Маршрут из журнала: на каждом перекрёстке — своё решение по порядку.  // В конце остановиться на следующем перекрёстке.} // ===== Готовое из уроков 3.2–4.2 — не меняй ===== int level(int value, int white, int black) {  long result = (long)(value - black) * 100 / (white - black);  return constrain(result, 0, 100);} void readSensors() {  leftLevel = level(analogRead(LEFT_SENSOR_PIN), LEFT_WHITE, LEFT_BLACK);  rightLevel = level(analogRead(RIGHT_SENSOR_PIN), RIGHT_WHITE, RIGHT_BLACK);} void drive(int leftDir, int leftSpeed, int rightDir, int rightSpeed) {  digitalWrite(LEFT_DIR_PIN, leftDir);  digitalWrite(RIGHT_DIR_PIN, rightDir);  analogWrite(LEFT_SPEED_PIN, leftSpeed);  analogWrite(RIGHT_SPEED_PIN, rightSpeed);} void stopMotors() {  analogWrite(LEFT_SPEED_PIN, 0);  analogWrite(RIGHT_SPEED_PIN, 0);} void followLineStep() {  int error = leftLevel - rightLevel;  int correction = kp * error;  drive(FORWARD, constrain(baseSpeed + correction, 0, 255),        FORWARD, constrain(baseSpeed - correction, 0, 255));} void setupRobot() {  pinMode(LEFT_DIR_PIN, OUTPUT);  pinMode(LEFT_SPEED_PIN, OUTPUT);  pinMode(RIGHT_DIR_PIN, OUTPUT);  pinMode(RIGHT_SPEED_PIN, OUTPUT);  stopMotors();  // Без измерений робот не поедет.  if (LEFT_WHITE == LEFT_BLACK || RIGHT_WHITE == RIGHT_BLACK) {    while (true) {    }  }  delay(2000);} // Перекрёсток: оба датчика видят чёрное.bool onCrossing() {  return leftLevel < 30 && rightLevel < 30;} // Ехать по линии, пока не найдём перекрёсток.void followLineToCrossing() {  while (true) {    readSensors();    if (onCrossing()) {      return;    }    followLineStep();  }} // Проехать перекрёсток прямо, пока датчики не съедут с чёрного.void passCrossing() {  drive(FORWARD, baseSpeed, FORWARD, baseSpeed);  do {    readSensors();  } while (onCrossing());  delay(100);} // Выехать на центр перекрёстка: датчики стоят впереди колёс.void toCenter() {  drive(FORWARD, baseSpeed, FORWARD, baseSpeed);  delay(centerMs);  stopMotors();} // Крутиться на месте, пока датчик со стороны поворота не найдёт новую линию.void turnUntilLine(bool left) {  if (left) {    drive(BACKWARD, turnSpeed, FORWARD, turnSpeed);  } else {    drive(FORWARD, turnSpeed, BACKWARD, turnSpeed);  }  delay(leaveMs);  do {    readSensors();  } while ((left ? leftLevel : rightLevel) > 30);  stopMotors();} void turnLeftAtCrossing() {  toCenter();  turnUntilLine(true);} void turnRightAtCrossing() {  toCenter();  turnUntilLine(false);} // Развернуться на линии: крутиться, пока линия снова не окажется под датчиком.void turnAroundOnLine() {  turnUntilLine(true);} // Ехать по линии заданное время.void followLineFor(unsigned long ms) {  unsigned long start = millis();  while (millis() - start < ms) {    readSensors();    followLineStep();  }  stopMotors();} void loop() {}
Предскажи

Нарисуй, где робот остановится на дорожке.

  1. Запусти. Если маршрут сломался — на каком решении? Почему?
Подсказка: Одно решение

Перед каждым решением робот должен доехать до перекрёстка.

Должно получиться

Робот проходит маршрут из трёх решений и останавливается там, где ты предсказал.

Найди ошибку · Робот поворачивает не на том перекрёстке

У соседей после turnLeftAtCrossing() сразу стоит turnRightAtCrossing(), без followLineToCrossing() между ними. Что сделает робот?

Требование 4. Маршрут планируется до программы и проверяется по частям.

  1. Выбери на тренировочном поле путь из четырёх решений или больше, в котором есть налево, направо и прямо. Если в классе разложено поле итоговой задачи, возьми путь от старта до одной зоны доставки. Запиши его таблицей:
№ПерекрёстокРешениеКуда смотрит робот после
1
  1. Запиши маршрут в программу L42_Route из своих функций.
  2. Если маршрут сломался, не переписывай всё: запусти только первые решения, потом половину, и найди первое решение, на котором робот ошибается. Запиши, как искал.
Предскажи

На каком решении маршрут скорее всего сломается и почему? Запиши до первого запуска.

Подсказка: Как проверять по частям

Временно поставь stopMotors(); и return; после нужного решения в setup().

Должно получиться

Таблица маршрута, программа по ней, прогноз и запись поиска ошибки. Робот проходит маршрут на тренировочной дорожке.

6

Задания по уровням

13 мин
Основное · Поворот и разворот

Повернуть с перекрёстка налево и направо и уехать по новой линии; развернуться на линии и вернуться.

Средний уровень · Четыре решения

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

Продвинутый уровень · Маршрут массивом

Запиши решения массивом: char route[] = {'L', 'S', 'R', 'L'};. Напиши функцию decide(d), которая выполняет одно решение, и цикл, который проходит весь массив. Составь маршрут по схеме поля так, чтобы для нового маршрута менялась только строка с массивом.

Проверь себя

Проверь себя

Выберите ответы для подготовки. Они не сохраняются и не оцениваются на сайте. Пройдите проверку знаний в программе курса или обсудите ответы с преподавателем.

● Основное

1.Почему поворот «до линии» надёжнее поворота по времени?
2.Зачем перед поворотом проехать немного вперёд (centerMs)?
3.Что случится, если в turnUntilLine убрать delay(leaveMs)?
4.Маршрут: налево, прямо, направо. Что выполнит робот на втором перекрёстке?
5.char route[] = {'L', 'S', 'R'}; Чему равно route[2]?

★ Средний уровень

1.Маршрут: налево, прямо, направо, налево. Сколько решений на перекрёстках выполнит робот?
2.Зачем маршрут сначала записывают в журнал, а потом в программу?
3.После поворота робот оказался сбоку от новой линии — со стороны, откуда приехал. Что поменять?
4.Для какого поворота подходит этот поиск линии?
do {  readSensors();} while (leftLevel > 30);
5.Почему каждое решение маршрута — это две строки: followLineToCrossing() и действие?

★★ Продвинутый уровень

1.Какое решение выполнится третьим?
char route[] = {'L', 'S', 'R', 'L'};
2.Что нужно изменить в программе с массивом, чтобы пройти новый маршрут?
3.От датчиков до оси колёс 60 мм, скорость робота 200 мм/с. Каким должен быть centerMs по расчёту?
4.Робота подняли над столом во время поворота. Что произойдёт?
unsigned long start = millis();do {  readSensors();} while ((left ? leftLevel : rightLevel) > 30 && millis() - start < 3000);stopMotors();
5.Маршрут из восьми решений ломается. Как быстрее найти сломанное решение?

Отвечай на вопросы своего уровня. Нужно верных ответов: Начальный — 4 из 5; Средний — 4 из 5; Продвинутый — 4 из 5. Ответы проверит преподаватель или программа курса.

Итог

Робот сворачивает по датчикам и проходит маршрут из решений. Для итоговой задачи осталось научиться видеть груз и брать его захватом.

Как подтвердить результат

Для каждого опыта сохраняй условия, версию программы, наблюдение и вывод. Сравнивая попытки, меняй одну причину за раз; неудачные попытки тоже остаются в журнале. Отметка результата относится к уже выполненным действиям, повторять их ради отметки не нужно. Если результат не получен, запиши расхождение и следующую проверяемую гипотезу. Модель, измерения настоящего робота и объяснение — разные виды доказательств.

Словарь урока

маршрутПоследовательность решений на перекрёстках: налево, направо или прямо.
центр перекрёсткаТочка, где пересекаются линии. Для поворота на месте над ней должны оказаться колёса.
массивСписок значений под одним именем: route[0], route[1] и так далее.
millis()Сколько миллисекунд прошло с включения платы. Позволяет ехать заданное время, не останавливая программу.
Тема 5 · Урок 5.1

Ультразвук: видим груз

Робот измеряет расстояние звуком и останавливается перед грузом, не путая «нет эха» с «груз рядом».

2 × 45 мин1–2 ученика на роботаПрактика

Результат урока

Робот едет по линии и останавливается на заданном расстоянии от груза. Если эхо не вернулось, программа не принимает это за «груз рядом».

Ты научишься

  • Объяснить, как ультразвуковой датчик измеряет расстояние.
  • Вывести расстояние в монитор порта и проверить его линейкой.
  • Отличить отсутствие эха от маленького расстояния.
  • Остановить робота перед грузом по измерению, а не по времени.

Нужно знать

  • 3.2 П-регулятор: плавно и быстро

Понадобится

Ультразвуковой датчик расстоянияДатчик линии (аналоговый, Trema) × 2

Зачем этот урок

Груз стоит в конце ответвления. Робот должен подъехать к ней и остановиться ровно так, чтобы захват мог её взять. Если ехать «по времени», робот то не доедет, то врежется. Сегодня он научится видеть груз ультразвуком — так же, как летучая мышь видит препятствия.

Разминка · вспомни
  1. Как робот останавливается на перекрёстке в программе из урока 4.1?
  2. Что делает break в цикле? (Если не помнишь — разберём сегодня.)
  3. Какие две команды нужны, чтобы робот ехал по линии с П-регулятором?
1
Занятие 1 · 45 мин
Измеряем расстояние
1

Как работает ультразвук

8 мин

Что должен делать робот. Курьеру нужно обнаружить предмет впереди, оценить расстояние до него и остановиться перед грузом. Датчики линии смотрят вниз и эту задачу не решают. Добавим ультразвуковой датчик: он поможет измерить расстояние, но не отличит груз от стены.

Что используем и как это работает. Передатчик датчика излучает короткую серию ультразвуковых колебаний, которые человек не слышит. Звук отражается от предмета, а приёмник улавливает эхо. Arduino запускает измерение импульсом на TRIG и измеряет длительность сигнала ECHO. Это электрический сигнал о времени измерения, а не сам звук. Путь звука двойной: до предмета и обратно. Поэтому расстояние равно скорости звука, умноженной на время и делённой на два.

Как связать измерение с действием. В учебной программе время в микросекундах делится на 58, чтобы получить приблизительные сантиметры: около 29 мкс на сантиметр в одну сторону и столько же обратно. Например, 580 мкс соответствует примерно 10 см. Целочисленный результат отбрасывает дробную часть; температура и отражающая поверхность тоже влияют на измерение. Сравниваем его с линейкой от передней поверхности датчика.

Если измерение не получилось. Наклонная поверхность может отразить звук в сторону, мягкая — поглотить, а слишком близкий или далёкий предмет — не дать пригодного эха. Поэтому «нет эха» означает «расстояние неизвестно», а не ноль и не свободный путь. В коде для этого есть отдельное значение −1. Программа принимает решение об остановке только после проверки измерения. Базовый пример при −1 продолжает движение — это ограничение учебного алгоритма, которое разберём в задании на потерю эха.

Датчик посылает короткий щелчок ультразвука и слушает эхо. Чем дальше предмет, тем дольше звук летит туда и обратно. По времени полёта считаем расстояние.

Предскажи

Звук пролетает 1 см примерно за 29 микросекунд. Предмет в 10 см. Сколько времени пройдёт до возвращения эха? (Не забудь: туда и обратно.)

Должно получиться

Ты можешь объяснить: время эха → расстояние. До предмета в 10 см и обратно — около 580 мкс.

Датчик посылает короткий щелчок ультразвука и слушает эхо. Чем дальше предмет, тем дольше звук летит туда и обратно.

Предскажи

Звук пролетает 1 см примерно за 29 микросекунд. Сколько времени пройдёт до возвращения эха, если предмет в 10 см? А в 30 см?

  1. Запиши расчёт в журнал.
  2. Объясни, почему время нужно умножать на два.
Должно получиться

Расчёт для 10 и 30 см записан с объяснением про «туда и обратно».

Датчик посылает короткий щелчок ультразвука и слушает эхо. По времени полёта считают расстояние.

  1. Звук пролетает 1 см примерно за 29 мкс. Выведи формулу: расстояние в сантиметрах по времени эха в микросекундах.
  2. Скорость звука зависит от температуры. При 0 °C звук пролетает 1 см примерно за 30 мкс. Груз стоит в 50 см. Сколько покажет датчик зимой в холодном коридоре, если программа считает по 29 мкс? Важна ли эта ошибка для остановки в 5 см?
Должно получиться

Формула и расчёт ошибки записаны. Ты можешь объяснить, когда ошибкой можно пренебречь.

2

Расстояние в мониторе порта

20 мин
  1. Посмотри номера контактов TRIG и ECHO на карточке у робота. Карточки нет — проследи провода от выводов TRIG и ECHO датчика до платы и прочитай номера у гнёзд. Запиши в журнал.
  2. Создай скетч L51_Distance и впиши номера контактов вместо -1.
Расстояние в мониторе портаL51_Distance.ino.ino
// Контакты ультразвукового датчика: впиши номера из таблицы проводки своего робота.const int TRIG_PIN = -1;const int ECHO_PIN = -1; // Расстояние в сантиметрах или -1, если эхо не вернулось.int readDistanceCm() {  digitalWrite(TRIG_PIN, LOW);  delayMicroseconds(2);  digitalWrite(TRIG_PIN, HIGH);  delayMicroseconds(10);  digitalWrite(TRIG_PIN, LOW);  unsigned long echoUs = pulseIn(ECHO_PIN, HIGH, 25000);  if (echoUs == 0) {    return -1;  }  return echoUs / 58;} void setupSonar() {  // Без контактов датчика программа дальше не пойдёт.  if (TRIG_PIN < 0 || ECHO_PIN < 0) {    while (true) {    }  }  pinMode(TRIG_PIN, OUTPUT);  pinMode(ECHO_PIN, INPUT);} void setup() {  Serial.begin(9600);  setupSonar();} void loop() {  int distance = readDistanceCm();  if (distance < 0) {    Serial.println("нет эха");  } else {    Serial.print(distance);    Serial.println(" см");  }  delay(200);}
  1. Загрузи, открой монитор порта. Ставь груз на 5, 10, 20 и 30 см по линейке. Запиши показания.
По линейкеПоказание датчика
5 см
10 см
20 см
30 см
Должно получиться

Показания датчика близки к линейке, разница — 1–2 см.

Как это устроено: Как работает readDistanceCm
  • На TRIG подаём короткий импульс 10 мкс — датчик щёлкает.
  • pulseIn(ECHO_PIN, HIGH, 25000) ждёт эхо не дольше 25 000 мкс и возвращает время в микросекундах. Если эхо не вернулось — возвращает 0.
  • Делим время на 58: это 29 мкс на сантиметр туда и 29 обратно.
  • Если эха нет, функция возвращает −1: «измерения нет».
Если не получилось
Что видишьЧто сделать
Программа ничего не печатаетВписаны ли номера TRIG и ECHO вместо −1?
Всегда «нет эха»Перепутаны TRIG и ECHO или датчик не получает питание
Числа прыгаютГруз должна стоять прямо напротив датчика
  1. Посмотри номера контактов TRIG и ECHO на карточке у робота. Карточки нет — проследи провода от выводов TRIG и ECHO датчика до платы и прочитай номера у гнёзд. Запиши в журнал.
  2. Создай скетч L51_Distance по шаблону и впиши номера контактов вместо -1.
  3. Напиши функцию readDistanceCm() сам. Печать в мониторе порта уже готова. Пока функция не написана, монитор печатает «нет эха».
КомандаЧто делает
digitalWrite(TRIG_PIN, HIGH); … LOWЩелчок: TRIG в HIGH на 10 мкс, перед этим коротко LOW
delayMicroseconds(мкс);Ждёт указанное число микросекунд
pulseIn(ECHO_PIN, HIGH, 25000)Время эха в микросекундах; 0 — эха нет за 25 000 мкс
Шаблон: расстояниеL51_Distance_Gaps.ino.ino
// Шаблон среднего уровня. Напиши код в разделе «Твоя часть» по целям в комментариях. // Контакты ультразвукового датчика: впиши номера из таблицы проводки своего робота.const int TRIG_PIN = -1;const int ECHO_PIN = -1; // ===== Твоя часть: напиши сам ===== int readDistanceCm() {  // Щёлкнуть датчиком и вернуть расстояние до груза в сантиметрах.  // Если эхо не вернулось за 25 000 мкс, вернуть -1.  return -1;} // ===== Готовое: настройка датчика и печать — не меняй ===== void setupSonar() {  // Без контактов датчика программа дальше не пойдёт.  if (TRIG_PIN < 0 || ECHO_PIN < 0) {    while (true) {    }  }  pinMode(TRIG_PIN, OUTPUT);  pinMode(ECHO_PIN, INPUT);} void setup() {  Serial.begin(9600);  setupSonar();} void loop() {  int distance = readDistanceCm();  if (distance < 0) {    Serial.println("нет эха");  } else {    Serial.print(distance);    Serial.println(" см");  }  delay(200);}
Предскажи

Эхо вернулось через 1160 мкс. Сколько сантиметров до груза?

  1. Загрузи, открой монитор порта. Ставь груз на 5, 10, 20 и 30 см по линейке. Запиши показания.
По линейкеПоказание датчика
5 см
10 см
20 см
30 см
Подсказка: На что делить

Звук летит туда и обратно: 29 мкс на сантиметр в одну сторону и 29 — в другую. Сколько микросекунд приходится на один сантиметр расстояния до груза?

Подсказка: Время эха — большое число

Время может быть больше, чем помещается в int. Для него берут unsigned long.

Должно получиться

Показания близки к линейке, разница — 1–2 см. Прогноз для 1160 мкс сошёлся.

Если не получилось
Что видишьЧто сделать
Программа ничего не печатаетВписаны ли номера TRIG и ECHO вместо −1?
Всегда «нет эха»Есть ли щелчок перед ожиданием эха? Не перепутаны ли TRIG и ECHO?
Числа прыгаютГруз должен стоять прямо напротив датчика
  1. Посмотри номера контактов TRIG и ECHO на карточке у робота. Карточки нет — проследи провода от выводов TRIG и ECHO датчика до платы и прочитай номера у гнёзд. Запиши в журнал.

Требование 1. Функция readDistanceCm() возвращает расстояние в сантиметрах или −1, если эха нет. Как работает датчик:

  • на TRIG подаётся импульс HIGH длиной 10 мкс (перед ним — короткий LOW);
  • датчик держит ECHO в HIGH столько микросекунд, сколько звук летел туда и обратно;
  • звук пролетает 1 см примерно за 29 мкс;
  • ждать эхо дольше 25 000 мкс не нужно: команда pulseIn(pin, HIGH, timeoutUs) вернёт 0.

Без номеров контактов программа не должна работать с датчиком: пусть она остановится. Напиши скетч L51_Distance, который печатает расстояние или «нет эха».

Приёмка. Таблица по линейке и ответы в журнале.

По линейкеПоказаниеОшибка, см
5 см
10 см
20 см
30 см
  1. Какое самое большое расстояние может измерить функция при ожидании 25 000 мкс?
  2. Растёт ли ошибка с расстоянием? Чем ты это объяснишь?
Подсказка: Идея (1 из 3)

Щёлкнуть, измерить длину импульса эха, перевести микросекунды в сантиметры.

Подсказка: План (2 из 3)
  1. TRIG: LOW 2 мкс, HIGH 10 мкс, LOW.
  2. pulseIn на ECHO с ожиданием 25 000 мкс.
  3. Ноль — нет эха. Иначе — время делить на 58.
Подсказка: Фрагмент кода (3 из 3)

unsigned long echoUs = pulseIn(ECHO_PIN, HIGH, 25000); — почему unsigned long, а не int?

Должно получиться

Функция выполняет требование 1, таблица с ошибками заполнена, ответы записаны.

3

«Нет эха» — не «рядом»

15 мин
  1. Направь датчик в пустоту или на мягкую ткань под углом. Что печатает монитор?
  2. Поднеси ладонь вплотную к датчику, ближе 2 см. Что печатает?
Найди ошибку · Робот закрыл захват в пустоте

Соседи написали остановку так: if (readDistanceCm() <= 5) { stopMotors(); }. Робот иногда останавливается посреди пустой дороги. Почему? Что вернёт функция, если эха нет?

Должно получиться

Ты можешь объяснить, почему условие остановки должно проверять, что расстояние больше нуля.

  1. Проверь датчик в трёх положениях и запиши, что печатает монитор:
ПоложениеЧто печатает
В пустоту
На мягкую ткань под углом
Ладонь ближе 2 см
Найди ошибку · Робот закрыл захват в пустоте

Соседи написали остановку так: if (readDistanceCm() <= 5) { stopMotors(); }. Робот иногда останавливается посреди пустой дороги. Почему? Что вернёт функция, если эха нет?

  1. Запиши правильное условие остановки.
Должно получиться

Таблица заполнена, ошибка соседей объяснена, правильное условие записано.

Найди границы, за которыми датчику нельзя верить.

  1. Поставь груз в 20 см и поворачивай его боком к датчику. С какого угла пропадает эхо?
  2. Придвигай груз к датчику. С какого расстояния показания перестают совпадать с линейкой?
  3. Направь датчик на мягкую ткань. Что печатает монитор?
Найди ошибку · Робот закрыл захват в пустоте

Соседи написали остановку так: if (readDistanceCm() <= 5) { stopMotors(); }. Робот иногда останавливается посреди пустой дороги. Почему?

  1. Запиши, какие из найденных границ опасны для доставки и как программа должна себя вести в каждом случае.
Должно получиться

В журнале — угол потери эха, ближняя граница, поведение на ткани и правила для программы.

2
Занятие 2 · 45 мин
Стоп перед грузом
4

Едем к грузу и останавливаемся

17 мин
  1. Создай скетч L51_Stop_At_Can. Перенеси измерения, kp, baseSpeed и контакты датчика.
Стоп перед грузомL51_Stop_At_Can.ino.ino
const int LEFT_SENSOR_PIN = A4;const int RIGHT_SENSOR_PIN = A5;const int LEFT_DIR_PIN = 7;const int LEFT_SPEED_PIN = 6;const int RIGHT_DIR_PIN = 4;const int RIGHT_SPEED_PIN = 5;const int FORWARD = HIGH;const int BACKWARD = (FORWARD == HIGH) ? LOW : HIGH; // Измерения и настройки из журнала (уроки 3.1 и 3.2).const int LEFT_WHITE = 0;const int LEFT_BLACK = 0;const int RIGHT_WHITE = 0;const int RIGHT_BLACK = 0;float kp = 0.5;int baseSpeed = 100; int leftLevel = 100;int rightLevel = 100; int level(int value, int white, int black) {  long result = (long)(value - black) * 100 / (white - black);  return constrain(result, 0, 100);} void readSensors() {  leftLevel = level(analogRead(LEFT_SENSOR_PIN), LEFT_WHITE, LEFT_BLACK);  rightLevel = level(analogRead(RIGHT_SENSOR_PIN), RIGHT_WHITE, RIGHT_BLACK);} void drive(int leftDir, int leftSpeed, int rightDir, int rightSpeed) {  digitalWrite(LEFT_DIR_PIN, leftDir);  digitalWrite(RIGHT_DIR_PIN, rightDir);  analogWrite(LEFT_SPEED_PIN, leftSpeed);  analogWrite(RIGHT_SPEED_PIN, rightSpeed);} void stopMotors() {  analogWrite(LEFT_SPEED_PIN, 0);  analogWrite(RIGHT_SPEED_PIN, 0);} void followLineStep() {  int error = leftLevel - rightLevel;  int correction = kp * error;  drive(FORWARD, constrain(baseSpeed + correction, 0, 255),        FORWARD, constrain(baseSpeed - correction, 0, 255));} void setupRobot() {  pinMode(LEFT_DIR_PIN, OUTPUT);  pinMode(LEFT_SPEED_PIN, OUTPUT);  pinMode(RIGHT_DIR_PIN, OUTPUT);  pinMode(RIGHT_SPEED_PIN, OUTPUT);  stopMotors();  // Без измерений робот не поедет.  if (LEFT_WHITE == LEFT_BLACK || RIGHT_WHITE == RIGHT_BLACK) {    while (true) {    }  }  delay(2000);} // Контакты ультразвукового датчика: впиши номера из таблицы проводки своего робота.const int TRIG_PIN = -1;const int ECHO_PIN = -1; // Расстояние в сантиметрах или -1, если эхо не вернулось.int readDistanceCm() {  digitalWrite(TRIG_PIN, LOW);  delayMicroseconds(2);  digitalWrite(TRIG_PIN, HIGH);  delayMicroseconds(10);  digitalWrite(TRIG_PIN, LOW);  unsigned long echoUs = pulseIn(ECHO_PIN, HIGH, 25000);  if (echoUs == 0) {    return -1;  }  return echoUs / 58;} void setupSonar() {  // Без контактов датчика программа дальше не пойдёт.  if (TRIG_PIN < 0 || ECHO_PIN < 0) {    while (true) {    }  }  pinMode(TRIG_PIN, OUTPUT);  pinMode(ECHO_PIN, INPUT);} const int STOP_CM = 5; // на каком расстоянии от груза остановиться void setup() {  setupSonar();  setupRobot();   while (true) {    int distance = readDistanceCm();    if (distance > 0 && distance <= STOP_CM) {      break;    }    readSensors();    followLineStep();  }  stopMotors();} void loop() {}
  1. Поставь груз в конце линии. Запусти робота с другого конца.
Должно получиться

Робот едет по линии и останавливается перед грузом, не касаясь его.

Как это устроено: Что делает break

break немедленно выходит из цикла while. После цикла программа выполняет stopMotors(). Так «стоп по расстоянию» оказывается в одном месте программы.

  1. Создай скетч L51_Stop_At_Can по шаблону. Перенеси измерения, kp, baseSpeed, контакты датчика и замени readDistanceCm своей функцией.
  2. Напиши движение в setup() сам: робот едет по линии и останавливается перед грузом. Помни шаг «Нет эха — не рядом».
Шаблон: стоп перед грузомL51_Stop_At_Can_Gaps.ino.ino
// Шаблон среднего уровня. Напиши код в разделе «Твоя часть» по целям в комментариях.// Функцию readDistanceCm ниже замени своей из L51_Distance. const int LEFT_SENSOR_PIN = A4;const int RIGHT_SENSOR_PIN = A5;const int LEFT_DIR_PIN = 7;const int LEFT_SPEED_PIN = 6;const int RIGHT_DIR_PIN = 4;const int RIGHT_SPEED_PIN = 5;const int FORWARD = HIGH;const int BACKWARD = (FORWARD == HIGH) ? LOW : HIGH; // Измерения и настройки из журнала (уроки 3.1 и 3.2).const int LEFT_WHITE = 0;const int LEFT_BLACK = 0;const int RIGHT_WHITE = 0;const int RIGHT_BLACK = 0;float kp = 0.5;int baseSpeed = 100;int leftLevel = 100;int rightLevel = 100; // Контакты ультразвукового датчика: впиши номера из таблицы проводки своего робота.const int TRIG_PIN = -1;const int ECHO_PIN = -1;const int STOP_CM = 5; // на каком расстоянии от груза остановиться // ===== Твоя часть: напиши сам ===== void setup() {  setupSonar();  setupRobot();   // Ехать по линии и остановиться, когда груз не дальше STOP_CM.  // «Нет эха» — не «груз рядом»: тогда робот едет дальше.} // ===== Готовое из уроков 3.2 и 5.1 — не меняй ===== int level(int value, int white, int black) {  long result = (long)(value - black) * 100 / (white - black);  return constrain(result, 0, 100);} void readSensors() {  leftLevel = level(analogRead(LEFT_SENSOR_PIN), LEFT_WHITE, LEFT_BLACK);  rightLevel = level(analogRead(RIGHT_SENSOR_PIN), RIGHT_WHITE, RIGHT_BLACK);} void drive(int leftDir, int leftSpeed, int rightDir, int rightSpeed) {  digitalWrite(LEFT_DIR_PIN, leftDir);  digitalWrite(RIGHT_DIR_PIN, rightDir);  analogWrite(LEFT_SPEED_PIN, leftSpeed);  analogWrite(RIGHT_SPEED_PIN, rightSpeed);} void stopMotors() {  analogWrite(LEFT_SPEED_PIN, 0);  analogWrite(RIGHT_SPEED_PIN, 0);} void followLineStep() {  int error = leftLevel - rightLevel;  int correction = kp * error;  drive(FORWARD, constrain(baseSpeed + correction, 0, 255),        FORWARD, constrain(baseSpeed - correction, 0, 255));} void setupRobot() {  pinMode(LEFT_DIR_PIN, OUTPUT);  pinMode(LEFT_SPEED_PIN, OUTPUT);  pinMode(RIGHT_DIR_PIN, OUTPUT);  pinMode(RIGHT_SPEED_PIN, OUTPUT);  stopMotors();  // Без измерений робот не поедет.  if (LEFT_WHITE == LEFT_BLACK || RIGHT_WHITE == RIGHT_BLACK) {    while (true) {    }  }  delay(2000);} // Расстояние в сантиметрах или -1, если эхо не вернулось.int readDistanceCm() {  digitalWrite(TRIG_PIN, LOW);  delayMicroseconds(2);  digitalWrite(TRIG_PIN, HIGH);  delayMicroseconds(10);  digitalWrite(TRIG_PIN, LOW);  unsigned long echoUs = pulseIn(ECHO_PIN, HIGH, 25000);  if (echoUs == 0) {    return -1;  }  return echoUs / 58;} void setupSonar() {  // Без контактов датчика программа дальше не пойдёт.  if (TRIG_PIN < 0 || ECHO_PIN < 0) {    while (true) {    }  }  pinMode(TRIG_PIN, OUTPUT);  pinMode(ECHO_PIN, INPUT);} void loop() {}
  1. Поставь груз в конце линии. Запусти робота с другого конца.
Подсказка: Ехать, пока…

Как в followLineToCrossing() урока 4.1: в цикле измерить расстояние, решить, пора ли останавливаться, иначе сделать шаг по линии.

Подсказка: Две проверки

Остановиться можно, только если измерение есть и груз близко. Какое число вернёт твоя функция, если эха нет?

Как это устроено: Как выйти из цикла

break немедленно выходит из цикла while. Программа продолжается после цикла.

Должно получиться

Робот едет по линии и останавливается перед грузом, не касаясь его, и не останавливается посреди пустой дороги.

Требование 2. Продолжи свою программу L42_Turn_At_Crossing и сохрани как L51_Stop_At_Can. Функция followLineToCan(stopCm) ведёт робота по линии и останавливает его, когда груз не дальше stopCm:

  • «нет эха» никогда не считается «груз рядом»;
  • после остановки моторы выключены;
  • функции линии и перекрёстков из прошлых уроков не меняются.

Приёмка. Груз в конце линии — робот останавливается, не касаясь груза. Груза нет — робот доезжает до конца линии, не останавливаясь посреди дороги (будь готов выключить питание).

Подсказка: Идея (1 из 2)

Это followLineToCrossing() из урока 4.1, только выход — по расстоянию.

Подсказка: Фрагмент кода (2 из 2)

if (distance > 0 && distance <= stopCm) { return; } — почему здесь return, а не break?

Должно получиться

Программа выполняет требование 2, обе проверки приёмки записаны.

5

Точная остановка

15 мин
  1. Измерь линейкой, на каком расстоянии от груза остановился робот.
  2. Подбери STOP_CM и baseSpeed, чтобы робот останавливался там, где удобно захвату.
  3. Повтори три раза. Запиши в журнал STOP_CM и фактическое расстояние.
Подсказка: Робот останавливается дальше или ближе, чем STOP_CM

Робот не останавливается мгновенно: он ещё немного катится. Чем выше скорость, тем дальше. Можно перед грузом ехать медленнее или поставить STOP_CM чуть больше.

Должно получиться

Робот три раза подряд останавливается на нужном расстоянии от груза.

Подбери STOP_CM и baseSpeed так, чтобы робот останавливался там, где удобно захвату. Меняй одно число за раз.

  1. Реши, на каком расстоянии от груза роботу нужно стоять. Если захват уже стоит, измерь у него; если нет — выбери расстояние, при котором робот не касается груза, и уточни его на уроке 5.2. Запиши.
  2. Запусти, измерь линейкой, где робот остановился. Запиши в таблицу и реши, что менять.
STOP_CMbaseSpeedГде остановился, смЧто поменять и почему
  1. Когда получилось, повтори три раза.
Подсказка: Робот останавливается ближе, чем STOP_CM

Робот не останавливается мгновенно: он ещё немного катится. Чем выше скорость, тем дальше.

Должно получиться

Робот три раза подряд останавливается на нужном расстоянии. В журнале — таблица попыток с объяснением.

Добейся остановки на нужном расстоянии ±1 см три раза из трёх на своей рабочей скорости — той, на которой робот проходит линию.

  1. Измерь выбег: при baseSpeed 80, 120 и своей рабочей поставь STOP_CM = 15 и измерь, на сколько сантиметров робот прокатился после команды стоп.
  2. Выбери способ точной остановки и обоснуй его таблицей: поправить STOP_CM на выбег или ехать медленнее в последние сантиметры перед грузом.
  3. Проверь три раза.
baseSpeedSTOP_CMГде остановился, смВыбег, см
8015
12015
15
Подсказка: Медленно в конце

Два порога: дальше первого — рабочая скорость, между первым и вторым — медленная, у второго — стоп.

Должно получиться

Таблица выбега, выбранный способ с обоснованием и три остановки ±1 см записаны.

6

Задания по уровням

13 мин
Основное · Стоп в 5 см

Остановиться в 5 см от груза, стоящего в конце линии.

Средний уровень · Повтор при отсутствии эха

Если эха нет, измерь ещё раз. Если эха нет пять раз подряд — останови робота: что-то не так с датчиком. Так робот не уедет с поля, потеряв груз.

Продвинутый уровень · Медиана из трёх

Делай три измерения и бери среднее по величине — медиану. Одно случайное неверное число тогда не испортит остановку. Сравни в мониторе порта сырые измерения и медиану.

Проверь себя

Проверь себя

Выберите ответы для подготовки. Они не сохраняются и не оцениваются на сайте. Пройдите проверку знаний в программе курса или обсудите ответы с преподавателем.

● Основное

1.Как ультразвуковой датчик узнаёт расстояние?
2.readDistanceCm() вернула −1. Что это значит?
3.Какое условие остановки правильное?
4.Робот всегда останавливается на 3 см ближе, чем STOP_CM. Почему?
5.Три измерения: 12, 85, 13. Какая медиана?

★ Средний уровень

1.Эха нет пять раз подряд. Что делает робот по заданию ★?
2.Измерение вернуло «нет эха». Что программа делает сначала?
3.Эхо вернулось через 1160 мкс. Сколько сантиметров до груза?
4.Почему время эха делят на 58, а не на 29?
5.На высокой скорости робот останавливается ближе к грузу, чем на низкой. Почему?

★★ Продвинутый уровень

1.pulseIn ждёт эхо не дольше 25 000 мкс. Какое наибольшее расстояние может вернуть функция?
2.Почему для остановки берут медиану трёх измерений, а не среднее арифметическое?
3.Где ошибка, опасная для робота?
int readDistanceCm() {  digitalWrite(TRIG_PIN, HIGH);  delayMicroseconds(10);  digitalWrite(TRIG_PIN, LOW);  unsigned long echoUs = pulseIn(ECHO_PIN, HIGH, 25000);  return echoUs / 58;}
4.Выбег при рабочей скорости — 3 см. Роботу нужно стоять в 5 см от груза. Какой STOP_CM поставить?
5.Сравнение с линейкой: 5 → 6, 10 → 11, 20 → 21, 30 → 31 см. Что это значит?

Отвечай на вопросы своего уровня. Нужно верных ответов: Начальный — 4 из 5; Средний — 4 из 5; Продвинутый — 4 из 5. Ответы проверит преподаватель или программа курса.

Итог

Робот видит груз и останавливается перед ним по измерению. Осталось научиться его брать — в следующем уроке.

Как подтвердить результат

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

Словарь урока

ультразвукЗвук такой высоты, что человек его не слышит. Датчик посылает щелчок и ждёт эхо.
эхоЗвук, отразившийся от предмета и вернувшийся к датчику.
pulseInКоманда, которая измеряет, сколько микросекунд длился сигнал на входе.
мксМикросекунда — миллионная доля секунды.
медианаСреднее по величине из нескольких чисел: из 4, 25 и 5 это 5.
Тема 5 · Урок 5.2

Захват: берём и ставим

Робот подъезжает к грузу, берёт его захватом, увозит и ставит в нужном месте.

2 × 45 мин1–2 ученика на роботаПрактика

Результат урока

Робот подъезжает к грузу по ультразвуку, берёт её, разворачивается, везёт по линии и ставит на другом конце. Действия оформлены функциями pickUp() и putDown().

Ты научишься

  • Объяснить, как сервопривод поворачивается на заданный угол.
  • Найти углы «открыто» и «закрыто» для своего захвата опытом.
  • Соблюдать порядок: остановиться — закрыть; открыть — подождать — отъехать.
  • Собрать перевозку груза из функций прошлых уроков.

Нужно знать

  • 5.1 Ультразвук: видим груз
  • 4.2 Повороты на перекрёстке и маршрут

Понадобится

Захват с сервоприводомУльтразвуковой датчик расстоянияДатчик линии (аналоговый, Trema) × 2

Зачем этот урок

Робот умеет доезжать до груза. Остался последний навык курьера — взять груз, довезти и аккуратно поставить. Сегодня робот соберёт всё вместе: линия, ультразвук и захват.

Разминка · вспомни
  1. Какое условие остановки перед грузом правильное и почему?
  2. Как робот разворачивается на линии?
  3. Что делает followLineFor(1500)?
Безопасность

Не держи пальцы в захвате, когда включено питание. Сервопривод может резко повернуться при загрузке программы.

1
Занятие 1 · 45 мин
Сервопривод и захват
1

Как работает сервопривод

8 мин

Что должен делать робот. Курьер должен взять груз, удержать его при перевозке и отпустить в нужном месте. Для этого добавим механический захват с сервоприводом. Ультразвук помогает подъехать, но сам ничего не захватывает.

Что используем и как это работает. Внутри сервопривода есть мотор, редуктор, датчик положения и управляющая схема. По управляющим импульсам он получает заданное положение вала и старается его удержать. Библиотека Servo формирует эти импульсы; gripper.write задаёт положение, а не скорость вращения. Рычаг превращает поворот вала в движение губок захвата.

Как связать измерение с действием. Углы «открыто» и «закрыто» подбираем для своей механики небольшими изменениями; чужие числа могут упереть механизм в ограничитель. Сначала останавливаем колёса, затем закрываем захват и даём ему время. Команда закрытия не доказывает, что груз удерживается: это проверяем наблюдением. Подключение и питание сверяем с подтверждённой схемой комплекта.

Обычный мотор крутится, пока включён. Сервопривод поворачивается на заданный угол и держит его. Внутри у него мотор, шестерёнки и датчик положения. Командуем углом в градусах: gripper.write(90).

Предскажи

Что сделает сервопривод после gripper.write(90), а потом gripper.write(40)?

Должно получиться

Ты можешь объяснить, чем сервопривод отличается от мотора колеса.

Обычный мотор крутится, пока включён. Сервопривод поворачивается на заданный угол и держит его. Командуем углом в градусах: gripper.write(90).

Предскажи

Что сделает сервопривод после gripper.write(90), а потом gripper.write(40)? А мотор колеса после analogWrite(…, 90)?

  1. Запиши в журнал: чем команда сервоприводу отличается от команды мотору колеса — что задаёт число и когда движение заканчивается.
Должно получиться

Сравнение сервопривода и мотора колеса записано.

Сервопривод поворачивается на заданный угол и держит его: внутри мотор, шестерёнки и датчик положения.

  1. Посмотри контакт сервопривода на карточке у робота и впиши его в скетч пробы угла.
  2. Сделай опыт: gripper.write(40), пауза, gripper.write(90), пауза. Пробуй паузы 100, 300 и 600 мс. При какой паузе захват успевает дойти до угла?
  3. Запиши, сколько примерно времени нужно сервоприводу на 50° и почему программа должна ждать после команды.
Должно получиться

В журнале — опыт с паузами и наименьшая пауза, при которой захват доходит до угла.

2

Найди углы «открыто» и «закрыто»

22 мин

У каждого захвата свои углы. Найдём их опытом, как время поворота в уроке 2.2.

  1. Посмотри контакт сервопривода на карточке у робота и впиши его вместо -1.
Проба углаL52_Servo_Test.ino.ino
#include <Servo.h> // Контакт сервопривода захвата: впиши номер из таблицы проводки своего робота.const int SERVO_PIN = -1; int angle = 90; // меняй понемногу, чтобы найти «открыто» и «закрыто» Servo gripper; void setup() {  if (SERVO_PIN < 0) {    while (true) {    }  }  gripper.attach(SERVO_PIN);  gripper.write(angle);} void loop() {}
  1. Начни с angle = 90. Загрузи. Меняй угол по 10°: найди положение, где захват открыт так, что груз свободно входит.
  2. Найди угол, где захват держит груз крепко, но не давит изо всех сил.
  3. Запиши оба угла в журнал.
Подсказка: Сервопривод гудит или дрожит

Захват упёрся в груз или в собственный упор и не может дойти до угла. Отступи на 5–10° от этого положения: сервопривод не должен работать в упор.

Должно получиться

В журнале записаны углы «открыто» и «закрыто», при которых груз свободно входит и надёжно держится.

У каждого захвата свои углы. Найди их опытом — способ выбери сам, но меняй только angle и записывай каждую попытку.

КомандаЧто делает
gripper.attach(контакт);Связывает сервопривод с контактом
gripper.write(угол);Поворачивает на угол 0…180 и держит его
  1. Создай скетч L52_Servo_Test по шаблону. Посмотри контакт сервопривода на карточке у робота и впиши его вместо -1.
  2. Допиши setup() сам: захват должен повернуться на angle.
Шаблон: проба углаL52_Servo_Test_Gaps.ino.ino
// Шаблон среднего уровня. Напиши код в разделе «Твоя часть» по целям в комментариях. #include <Servo.h> // Контакт сервопривода захвата: впиши номер из таблицы проводки своего робота.const int SERVO_PIN = -1;int angle = 90; // меняй понемногу, чтобы найти «открыто» и «закрыто» Servo gripper; // ===== Твоя часть: напиши сам ===== void setup() {  if (SERVO_PIN < 0) {    while (true) {    }  }   // Захват поворачивается на угол angle и держит его.} void loop() {}
  1. Найди угол «открыто»: груз свободно входит в захват.
  2. Найди угол «закрыто»: груз держится крепко, а сервопривод не гудит.
  3. Проверь «закрыто»: подними робота с грузом и слегка наклони. Груз не выпадает?
ПопыткаangleЧто видноЧто поменять и почему
190
2
3
4
Подсказка: Сервопривод гудит или дрожит

Захват упёрся и не может дойти до угла. Что нужно сделать с углом?

Должно получиться

В журнале — попытки с объяснением, углы «открыто» и «закрыто» и проверка наклоном.

Посмотри контакт сервопривода на карточке у робота.

Требование 1. Скетч L52_Servo_Test для подбора углов: угол вводится числом в монитор порта, и захват поворачивается сразу, без новой загрузки программы.

  • Принимаются только числа от 0 до 180; на другое число программа отвечает, что так нельзя, и захват не двигается.
  • После каждого поворота программа печатает, какой угол сейчас.
  • Без номера контакта программа не трогает сервопривод.
Как это устроено: Справка по командам
КомандаЧто делает
#include <Servo.h>, Servo gripper;Подключить библиотеку и создать сервопривод
gripper.attach(pin)Связать сервопривод с контактом
gripper.write(angle)Повернуть на угол 0…180
Serial.available()Сколько символов пришло из монитора порта
Serial.parseInt()Прочитать из пришедших символов целое число

Требование 2. Углы «открыто» и «закрыто» найдены с запасом и проверены:

  • «открыто» — груз входит в захват, даже если робот остановился на 1 см левее или правее середины;
  • «закрыто» — найди угол, где захват упирается в груз, и отступи от него так, чтобы сервопривод не гудел; запиши, на сколько градусов отступил;
  • груз не выпадает, когда робота с грузом наклоняют на 30° вперёд и назад.

Требование 3. Функция moveGripper(target) поворачивает захват к углу плавно — шагами по 2° — а не одним рывком. Сравни, как ведёт себя груз при резком и плавном закрытии.

Подсказка: Идея (1 из 2)

Помни текущий угол в переменной. Пока он не равен цели, сдвигай его на шаг в сторону цели и делай короткую паузу.

Подсказка: Фрагмент кода (2 из 2)

currentAngle += (target > currentAngle) ? 2 : -2; — что будет, если цель отличается на нечётное число градусов?

Подсказка: В мониторе порта ничего не происходит

Проверь скорость 9600 и что монитор отправляет строку по Enter.

Должно получиться

Скетч подбора углов выполняет требование 1. В журнале — углы, запас «закрыто» в градусах, проверка наклоном и сравнение резкого и плавного закрытия. moveGripper работает.

3

Порядок действий важен

15 мин

Сопоставь функции pickUp() и putDown() в итоговом скетче с правилом порядка (следующее занятие):

  • Взять: сначала остановиться, потом закрыть захват.
  • Поставить: открыть захват, подождать, пока он раскроется, и только потом отъехать.
Найди ошибку · Груз уезжает с роботом

У соседей putDown() такая: gripper.write(OPEN_ANGLE); и сразу drive(BACKWARD, …). Груз то падает, то уезжает вместе с роботом. Почему? Как исправить?

Должно получиться

Ты можешь объяснить, зачем пауза после открытия захвата.

  1. До того как смотреть код, запиши в журнал по шагам: как роботу правильно взять груз и как поставить. Что должно случиться раньше: остановка или захват? Открытие или отъезд?
  2. Сравни свой порядок с правилом:
  • Взять: сначала остановиться, потом закрыть захват.
  • Поставить: открыть захват, подождать, пока он раскроется, и только потом отъехать.
Найди ошибку · Груз уезжает с роботом

У соседей putDown() такая: gripper.write(OPEN_ANGLE); и сразу drive(BACKWARD, …). Груз то падает, то уезжает вместе с роботом. Почему? Как исправить?

Должно получиться

Твой порядок записан и сверен с правилом, ошибка соседей объяснена.

Найди ошибку · Груз уезжает с роботом

У соседей putDown() такая: gripper.write(OPEN_ANGLE); и сразу drive(BACKWARD, …). Груз то падает, то уезжает вместе с роботом. Почему?

  1. Запиши правила порядка для «взять» и «поставить».
  2. Найди опытом наименьшую паузу после открытия, при которой груз остаётся на месте: 100, 200, 400, 600 мс, по три раза каждую.
  3. Какую паузу ты возьмёшь в программу и почему не самую короткую из удачных?
Должно получиться

Правила записаны, таблица опыта и выбранная пауза с обоснованием.

2
Занятие 2 · 45 мин
Перевозка груза
4

Взять и поставить

20 мин
  1. Создай скетч L52_Pick_Place. Перенеси измерения, kp, baseSpeed, контакты датчика и сервопривода, свои углы.
Взять и поставитьL52_Pick_Place.ino.ino
#include <Servo.h> const int LEFT_SENSOR_PIN = A4;const int RIGHT_SENSOR_PIN = A5;const int LEFT_DIR_PIN = 7;const int LEFT_SPEED_PIN = 6;const int RIGHT_DIR_PIN = 4;const int RIGHT_SPEED_PIN = 5;const int FORWARD = HIGH;const int BACKWARD = (FORWARD == HIGH) ? LOW : HIGH; // Измерения и настройки из журнала (уроки 3.1 и 3.2).const int LEFT_WHITE = 0;const int LEFT_BLACK = 0;const int RIGHT_WHITE = 0;const int RIGHT_BLACK = 0;float kp = 0.5;int baseSpeed = 100; int leftLevel = 100;int rightLevel = 100; int level(int value, int white, int black) {  long result = (long)(value - black) * 100 / (white - black);  return constrain(result, 0, 100);} void readSensors() {  leftLevel = level(analogRead(LEFT_SENSOR_PIN), LEFT_WHITE, LEFT_BLACK);  rightLevel = level(analogRead(RIGHT_SENSOR_PIN), RIGHT_WHITE, RIGHT_BLACK);} void drive(int leftDir, int leftSpeed, int rightDir, int rightSpeed) {  digitalWrite(LEFT_DIR_PIN, leftDir);  digitalWrite(RIGHT_DIR_PIN, rightDir);  analogWrite(LEFT_SPEED_PIN, leftSpeed);  analogWrite(RIGHT_SPEED_PIN, rightSpeed);} void stopMotors() {  analogWrite(LEFT_SPEED_PIN, 0);  analogWrite(RIGHT_SPEED_PIN, 0);} void followLineStep() {  int error = leftLevel - rightLevel;  int correction = kp * error;  drive(FORWARD, constrain(baseSpeed + correction, 0, 255),        FORWARD, constrain(baseSpeed - correction, 0, 255));} void setupRobot() {  pinMode(LEFT_DIR_PIN, OUTPUT);  pinMode(LEFT_SPEED_PIN, OUTPUT);  pinMode(RIGHT_DIR_PIN, OUTPUT);  pinMode(RIGHT_SPEED_PIN, OUTPUT);  stopMotors();  // Без измерений робот не поедет.  if (LEFT_WHITE == LEFT_BLACK || RIGHT_WHITE == RIGHT_BLACK) {    while (true) {    }  }  delay(2000);} // Контакты ультразвукового датчика: впиши номера из таблицы проводки своего робота.const int TRIG_PIN = -1;const int ECHO_PIN = -1; // Расстояние в сантиметрах или -1, если эхо не вернулось.int readDistanceCm() {  digitalWrite(TRIG_PIN, LOW);  delayMicroseconds(2);  digitalWrite(TRIG_PIN, HIGH);  delayMicroseconds(10);  digitalWrite(TRIG_PIN, LOW);  unsigned long echoUs = pulseIn(ECHO_PIN, HIGH, 25000);  if (echoUs == 0) {    return -1;  }  return echoUs / 58;} void setupSonar() {  // Без контактов датчика программа дальше не пойдёт.  if (TRIG_PIN < 0 || ECHO_PIN < 0) {    while (true) {    }  }  pinMode(TRIG_PIN, OUTPUT);  pinMode(ECHO_PIN, INPUT);} int turnSpeed = 100;int centerMs = 250; // подбираем: проехать от датчиков до центра перекрёсткаint leaveMs = 200;  // время, чтобы датчик ушёл со старой линии // Выехать на центр перекрёстка: датчики стоят впереди колёс.void toCenter() {  drive(FORWARD, baseSpeed, FORWARD, baseSpeed);  delay(centerMs);  stopMotors();} // Крутиться на месте, пока датчик со стороны поворота не найдёт новую линию.void turnUntilLine(bool left) {  if (left) {    drive(BACKWARD, turnSpeed, FORWARD, turnSpeed);  } else {    drive(FORWARD, turnSpeed, BACKWARD, turnSpeed);  }  delay(leaveMs);  do {    readSensors();  } while ((left ? leftLevel : rightLevel) > 30);  stopMotors();} void turnLeftAtCrossing() {  toCenter();  turnUntilLine(true);} void turnRightAtCrossing() {  toCenter();  turnUntilLine(false);} // Развернуться на линии: крутиться, пока линия снова не окажется под датчиком.void turnAroundOnLine() {  turnUntilLine(true);} // Ехать по линии заданное время.void followLineFor(unsigned long ms) {  unsigned long start = millis();  while (millis() - start < ms) {    readSensors();    followLineStep();  }  stopMotors();} // Сервопривод захвата: впиши контакт и свои углы из журнала.const int SERVO_PIN = -1;const int OPEN_ANGLE = 90;const int CLOSED_ANGLE = 40;const int STOP_CM = 5;Servo gripper; void setupGripper() {  // Без контакта сервопривода программа дальше не пойдёт.  if (SERVO_PIN < 0) {    while (true) {    }  }  gripper.attach(SERVO_PIN);  gripper.write(OPEN_ANGLE);  delay(500);} // Ехать по линии и остановиться перед грузом по ультразвуку.void approachCan() {  while (true) {    int distance = readDistanceCm();    if (distance > 0 && distance <= STOP_CM) {      break;    }    readSensors();    followLineStep();  }  stopMotors();} // Сначала остановиться, потом закрыть захват.void pickUp() {  stopMotors();  gripper.write(CLOSED_ANGLE);  delay(600);} // Открыть захват, подождать и только потом отъехать назад.void putDown() {  stopMotors();  gripper.write(OPEN_ANGLE);  delay(600);  drive(BACKWARD, baseSpeed, BACKWARD, baseSpeed);  delay(300);  stopMotors();} const unsigned long CARRY_MS = 1500; // сколько ехать по линии с грузом void setup() {  setupSonar();  setupGripper();  setupRobot();   approachCan();  pickUp();  turnAroundOnLine();  followLineFor(CARRY_MS);  putDown();} void loop() {}
  1. Поставь груз в конце линии, робота — на другом конце. Запусти.
Предскажи

Прочитай setup() и расскажи по шагам, что сделает робот.

Должно получиться

Робот подъезжает, берёт груз, разворачивается, везёт его по линии и ставит.

  1. Создай скетч L52_Pick_Place по шаблону. Перенеси измерения, kp, baseSpeed, контакты датчика и сервопривода, свои углы. Езда по линии, перекрёстки и подъезд к грузу уже готовы.
  2. Напиши сам подготовку захвата setupGripper(), pickUp(), putDown() и доставку в setup() — по своим правилам порядка из прошлого шага.
Шаблон: взять и поставитьL52_Pick_Place_Gaps.ino.ino
// Шаблон среднего уровня. Напиши код в разделе «Твоя часть» по целям в комментариях. #include <Servo.h>const int LEFT_SENSOR_PIN = A4;const int RIGHT_SENSOR_PIN = A5;const int LEFT_DIR_PIN = 7;const int LEFT_SPEED_PIN = 6;const int RIGHT_DIR_PIN = 4;const int RIGHT_SPEED_PIN = 5;const int FORWARD = HIGH;const int BACKWARD = (FORWARD == HIGH) ? LOW : HIGH; // Измерения и настройки из журнала (уроки 3.1 и 3.2).const int LEFT_WHITE = 0;const int LEFT_BLACK = 0;const int RIGHT_WHITE = 0;const int RIGHT_BLACK = 0;float kp = 0.5;int baseSpeed = 100;int leftLevel = 100;int rightLevel = 100; // Контакты ультразвукового датчика: впиши номера из таблицы проводки своего робота.const int TRIG_PIN = -1;const int ECHO_PIN = -1;int turnSpeed = 100;int centerMs = 250; // подбираем: проехать от датчиков до центра перекрёсткаint leaveMs = 200;  // время, чтобы датчик ушёл со старой линии // Сервопривод захвата: впиши контакт и свои углы из журнала.const int SERVO_PIN = -1;const int OPEN_ANGLE = 90;const int CLOSED_ANGLE = 40;const int STOP_CM = 5; Servo gripper;const unsigned long CARRY_MS = 1500; // сколько ехать по линии с грузом // ===== Твоя часть: напиши сам ===== void setupGripper() {  // Без контакта сервопривода программа дальше не идёт.  // Захват подключён и открыт; дай ему время раскрыться.} void pickUp() {  // Взять груз: груз оказывается в захвате и не выталкивается.} void putDown() {  // Поставить груз: груз остаётся на месте, когда робот отъезжает назад на 300 мс; в конце колёса стоят.} void setup() {  setupSonar();  setupGripper();  setupRobot();   // Доставка: подъехать к грузу, взять, развернуться на линии, проехать с грузом CARRY_MS, поставить.} // ===== Готовое из уроков 3.2–5.1 — не меняй ===== int level(int value, int white, int black) {  long result = (long)(value - black) * 100 / (white - black);  return constrain(result, 0, 100);} void readSensors() {  leftLevel = level(analogRead(LEFT_SENSOR_PIN), LEFT_WHITE, LEFT_BLACK);  rightLevel = level(analogRead(RIGHT_SENSOR_PIN), RIGHT_WHITE, RIGHT_BLACK);} void drive(int leftDir, int leftSpeed, int rightDir, int rightSpeed) {  digitalWrite(LEFT_DIR_PIN, leftDir);  digitalWrite(RIGHT_DIR_PIN, rightDir);  analogWrite(LEFT_SPEED_PIN, leftSpeed);  analogWrite(RIGHT_SPEED_PIN, rightSpeed);} void stopMotors() {  analogWrite(LEFT_SPEED_PIN, 0);  analogWrite(RIGHT_SPEED_PIN, 0);} void followLineStep() {  int error = leftLevel - rightLevel;  int correction = kp * error;  drive(FORWARD, constrain(baseSpeed + correction, 0, 255),        FORWARD, constrain(baseSpeed - correction, 0, 255));} void setupRobot() {  pinMode(LEFT_DIR_PIN, OUTPUT);  pinMode(LEFT_SPEED_PIN, OUTPUT);  pinMode(RIGHT_DIR_PIN, OUTPUT);  pinMode(RIGHT_SPEED_PIN, OUTPUT);  stopMotors();  // Без измерений робот не поедет.  if (LEFT_WHITE == LEFT_BLACK || RIGHT_WHITE == RIGHT_BLACK) {    while (true) {    }  }  delay(2000);} // Расстояние в сантиметрах или -1, если эхо не вернулось.int readDistanceCm() {  digitalWrite(TRIG_PIN, LOW);  delayMicroseconds(2);  digitalWrite(TRIG_PIN, HIGH);  delayMicroseconds(10);  digitalWrite(TRIG_PIN, LOW);  unsigned long echoUs = pulseIn(ECHO_PIN, HIGH, 25000);  if (echoUs == 0) {    return -1;  }  return echoUs / 58;} void setupSonar() {  // Без контактов датчика программа дальше не пойдёт.  if (TRIG_PIN < 0 || ECHO_PIN < 0) {    while (true) {    }  }  pinMode(TRIG_PIN, OUTPUT);  pinMode(ECHO_PIN, INPUT);} // Выехать на центр перекрёстка: датчики стоят впереди колёс.void toCenter() {  drive(FORWARD, baseSpeed, FORWARD, baseSpeed);  delay(centerMs);  stopMotors();} // Крутиться на месте, пока датчик со стороны поворота не найдёт новую линию.void turnUntilLine(bool left) {  if (left) {    drive(BACKWARD, turnSpeed, FORWARD, turnSpeed);  } else {    drive(FORWARD, turnSpeed, BACKWARD, turnSpeed);  }  delay(leaveMs);  do {    readSensors();  } while ((left ? leftLevel : rightLevel) > 30);  stopMotors();} void turnLeftAtCrossing() {  toCenter();  turnUntilLine(true);} void turnRightAtCrossing() {  toCenter();  turnUntilLine(false);} // Развернуться на линии: крутиться, пока линия снова не окажется под датчиком.void turnAroundOnLine() {  turnUntilLine(true);} // Ехать по линии заданное время.void followLineFor(unsigned long ms) {  unsigned long start = millis();  while (millis() - start < ms) {    readSensors();    followLineStep();  }  stopMotors();} // Ехать по линии и остановиться перед грузом по ультразвуку.void approachCan() {  while (true) {    int distance = readDistanceCm();    if (distance > 0 && distance <= STOP_CM) {      break;    }    readSensors();    followLineStep();  }  stopMotors();} void loop() {}
Предскажи

Прочитай свой setup() и расскажи напарнику по шагам, что сделает робот.

  1. Поставь груз в конце линии, робота — на другом конце. Запусти.
Подсказка: Сколько ждать

Захвату нужно время, чтобы дойти до угла. Попробуй паузу 300 и 600 мс: успевает ли он?

Подсказка: Без контакта

Защиту без контакта ты уже видел в скетче пробы угла.

Должно получиться

Робот подъезжает, берёт груз, разворачивается, везёт его по линии и ставит.

Требование 4. Продолжи свою программу L51_Stop_At_Can и сохрани как L52_Pick_Place. Робот выполняет доставку по линии:

  • pickUp() и putDown() соблюдают порядок: остановиться — закрыть; открыть — подождать — отъехать;
  • захват двигается твоей функцией moveGripper;
  • без контакта сервопривода программа не начинает движение;
  • в setup(): подъехать к грузу, взять, развернуться на линии, проехать с грузом, поставить.

Приёмка. Груз в конце линии, робот — на другом конце: доставка без помощи рук.

Подсказка: Идея (1 из 2)

Каждое действие доставки — уже готовая функция из прошлых уроков или этого урока. Осталось соединить их по порядку.

Подсказка: План (2 из 2)
  1. followLineToCan(STOP_CM).
  2. pickUp().
  3. turnAroundOnLine().
  4. Проехать по линии CARRY_MS.
  5. putDown().
Должно получиться

Программа выполняет требования 1–4, доставка проходит без помощи рук.

5

Надёжность

12 мин
  1. Повтори перевозку три раза подряд. Где робот ошибается чаще всего?
  2. Подбери STOP_CM так, чтобы груз оказывался в середине захвата.
  3. Подбери CARRY_MS, чтобы груз ставился в нужном месте.
Должно получиться

Три перевозки подряд без помощи рук. Значения записаны в журнал.

  1. Повтори перевозку пять раз подряд. Каждую неудачу запиши: на каком этапе и что случилось.
ЗаездПодъездЗахватПеревозкаПостановкаЧто случилось
1
2
3
4
5
  1. Найди этап, где ошибок больше всего. Какое одно число поменять, чтобы это исправить? Поменяй и повтори три раза.
Подсказка: Какие числа есть

STOP_CM — где робот встаёт перед грузом, CARRY_MS — где ставит, углы захвата — как держит.

Должно получиться

Таблица из пяти заездов, исправленный этап и три перевозки подряд без помощи рук.

Измерь надёжность доставки и улучши её по данным.

  1. Сделай 10 доставок подряд и отметь для каждой, на каком этапе она сорвалась: подъезд, захват, разворот, перевозка, постановка.
  2. Посчитай, сколько доставок из 10 прошли. Какой этап даёт больше всего срывов?
  3. Исправь только этот этап и снова сделай 10 доставок. Стало лучше? На сколько?
СерияУспешно из 10ПодъездЗахватРазворотПеревозкаПостановка
До
После
Подсказка: Если срывов нет

Усложни условия: поставь груз на 1 см в сторону от линии или подними baseSpeed. Где сорвётся первым?

Должно получиться

Две серии по 10 доставок, этап с больше всего срывов, что изменено и насколько выросла надёжность.

6

Задания по уровням

13 мин
Основное · Перевозка по линии

Взять груз с конца линии и поставить его на другом конце той же линии.

Средний уровень · Через перекрёсток

Груз стоит на одной ветке, место доставки — на другой. Перевези груз через перекрёсток, используя функции урока 4.2.

Продвинутый уровень · Функция deliver

Напиши функцию deliver(маршрут до груза, маршрут до места, время до места), которая выполняет всю доставку. Маршруты передавай строками решений, например "LS". Тогда в итоговой задаче доставка будет одной строкой программы.

Проверь себя

Проверь себя

Выберите ответы для подготовки. Они не сохраняются и не оцениваются на сайте. Пройдите проверку знаний в программе курса или обсудите ответы с преподавателем.

● Основное

1.Чем сервопривод отличается от мотора колеса?
2.В каком порядке правильно взять груз?
3.Зачем в putDown() пауза после открытия захвата?
4.Сервопривод гудит, захват дрожит у груза. Что сделать?
5.Почему углы захвата подбирают опытом?

★ Средний уровень

1.Груз стоит на одной ветке, место доставки — на другой. Что нужно добавить к перевозке по линии?
2.В каком порядке выполняется доставка?
3.Функция pickUp() осталась пустой. Что сделает робот у груза?
4.При проверке наклоном груз выпадает из захвата. Что поменять?
5.Три заезда из пяти сорвались на постановке: груз ставится мимо места. Какое одно число менять первым?

★★ Продвинутый уровень

1.Что в этом вызове означает строка "LS"?
deliver("LS", "R", 1500);
2.Зачем оформлять доставку функцией deliver()?
3.currentAngle = 90, target = 41. Что произойдёт?
void moveGripper(int target) {  while (currentAngle != target) {    currentAngle += (target > currentAngle) ? 2 : -2;    gripper.write(currentAngle);    delay(15);  }}
4.Серия из 10 доставок: срывов на подъезде — 0, на захвате — 1, на развороте — 4, при перевозке — 1, на постановке — 0. Что улучшать первым?
5.После исправления разворота успешных доставок стало 7 из 10 вместо 4. Как убедиться, что помогло именно исправление?

Отвечай на вопросы своего уровня. Нужно верных ответов: Начальный — 4 из 5; Средний — 4 из 5; Продвинутый — 4 из 5. Ответы проверит преподаватель или программа курса.

Итог

У робота есть все умения итоговой задачи: ехать, поворачивать, держаться линии, находить перекрёстки, видеть груз и перевозить его. Дальше — итоговый проект.

Как подтвердить результат

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

Словарь урока

сервоприводМотор, который поворачивается на заданный угол и держит его.
уголПоложение рычага сервопривода в градусах, обычно от 0 до 180.
библиотекаГотовый набор команд, который подключают строкой #include. Servo — библиотека для сервоприводов.
Тема 6 · Урок 6.1

Робот-курьер: план и программа

Итоговая задача, часть 1: выбираешь уровень, планируешь маршрут и собираешь программу из проверенных частей.

2 × 45 мин1–2 ученика на роботаПроект

Результат урока

Выбран уровень итоговой задачи, записан и проверен план маршрута. Программа проходит весь маршрут хотя бы в пробном заезде, работающие версии сохранены.

Ты научишься

  • Выбрать уровень итоговой задачи по своим выполненным заданиям ●, ★ и ★★.
  • Разбить задачу на участки и записать план маршрута до написания кода.
  • Собрать программу из функций прошлых уроков и проверять её по частям.
  • Сохранять рабочую версию после каждой проверенной части.

Нужно знать

  • 2.1 Моторы: едем и останавливаемся
  • 2.2 Повороты и развороты
  • 3.1 Датчики линии и первое следование
  • 3.2 П-регулятор: плавно и быстро
  • 4.1 Находим и считаем перекрёстки
  • 4.2 Повороты на перекрёстке и маршрут
  • 5.1 Ультразвук: видим груз
  • 5.2 Захват: берём и ставим

Понадобится

Ультразвуковой датчик расстоянияЗахват с сервоприводомДатчик линии (аналоговый, Trema) × 2

Твоя задача

Ты — инженер службы доставки. Такие роботы-курьеры уже ездят по тротуарам и складам: сами находят дорогу, забирают груз и привозят его по адресу. Твой робот должен сам выехать со старта, перевезти два груза на новые места и остановиться в финише. Во время заезда ему никто не помогает: всё решает твоя программа.

Новых команд здесь нет. Всё нужное ты уже делал: ездил и поворачивал, держался линии с П-регулятором, находил перекрёстки, видел груз ультразвуком и работал захватом. Сейчас нужно собрать эти умения в одну программу. В уроке 6.2 — зачётные заезды.

Разминка · вспомни
  1. Чем setup() отличается от loop() и где должен выполняться маршрут, чтобы после финиша робот стоял?
  2. Как программа понимает, что под роботом перекрёсток?
  3. Что делать, если ультразвук не получил эхо?
1
Занятие 1 · 45 мин
Уровень и план
1

Выбери уровень

6 мин

Что должен делать робот. Теперь роботу нужно доставить два груза и закончить поездку на финише. Нового датчика не добавляем: объединяем уже проверенные возможности — движение, поиск линии и перекрёстков, подъезд по ультразвуку и захват.

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

Как связать измерение с действием. Перед сборкой маршрута проверь каждую функцию своим коротким опытом. Пустая функция в заготовке не выполняет действие, даже если программа успешно собирается. После последнего участка нужна явная остановка; повторение маршрута в loop без решения автора запустило бы доставку заново.

Задача есть в трёх уровнях. Правила и критерии оценки у них общие, а поля разные. Уровень выбирают по тому, какие задания уроков ты уже выполнил: задания ★ готовили к среднему полю, ★★ — к продвинутому.

Уровень итоговой задачиКакие задания уроков нужны
Начальный
S-образная линия, два перекрёстка, порядок действий известен заранее.
● Основное 1.12.12.23.13.24.14.25.15.2
Средний
Сеть дорог, четыре перекрёстка, проезд прямо и повторные встречи с перекрёстками.
● Основное 1.12.12.23.13.24.14.25.15.2
★ Средний уровень 1.12.12.23.13.24.14.25.15.2
Продвинутый
Шесть перекрёстков, крутые S-дуги, маршрут по карточке составляешь сам.
● Основное 1.12.12.23.13.24.14.25.15.2
★ Средний уровень 1.12.12.23.13.24.14.25.15.2
★★ Продвинутый уровень 1.12.12.23.13.24.14.25.15.2
  1. Открой бортовой журнал и посмотри, какие задания ●, ★ и ★★ у тебя получились.
  2. Выбери уровень сам и отметь его ниже. Если сомневаешься, начни с начального: после зачёта можно попробовать следующий.
Должно получиться

Уровень выбран и записан в журнал.

Задача есть в трёх уровнях. Правила и критерии оценки у них общие, а поля разные. Поле выбирают по тому, какие задания уроков ты уже выполнил.

Уровень итоговой задачиКакие задания уроков нужны
Начальный
S-образная линия, два перекрёстка, порядок действий известен заранее.
● Основное 1.12.12.23.13.24.14.25.15.2
Средний
Сеть дорог, четыре перекрёстка, проезд прямо и повторные встречи с перекрёстками.
● Основное 1.12.12.23.13.24.14.25.15.2
★ Средний уровень 1.12.12.23.13.24.14.25.15.2
Продвинутый
Шесть перекрёстков, крутые S-дуги, маршрут по карточке составляешь сам.
● Основное 1.12.12.23.13.24.14.25.15.2
★ Средний уровень 1.12.12.23.13.24.14.25.15.2
★★ Продвинутый уровень 1.12.12.23.13.24.14.25.15.2
  1. Открой бортовой журнал. Выпиши, какие задания ★ у тебя получились и в каких уроках.
  2. Сравни со списком, который нужен для среднего поля. Чего не хватает?
  3. Выбери поле сам. Если выбираешь среднее, но какого-то задания ★ нет, запиши, как и когда ты его доделаешь.
Должно получиться

Поле выбрано, в журнале — список выполненных заданий ★ и план для недостающих.

Задача есть в трёх уровнях. Правила и критерии оценки у них общие, а поля разные.

Уровень итоговой задачиКакие задания уроков нужны
Начальный
S-образная линия, два перекрёстка, порядок действий известен заранее.
● Основное 1.12.12.23.13.24.14.25.15.2
Средний
Сеть дорог, четыре перекрёстка, проезд прямо и повторные встречи с перекрёстками.
● Основное 1.12.12.23.13.24.14.25.15.2
★ Средний уровень 1.12.12.23.13.24.14.25.15.2
Продвинутый
Шесть перекрёстков, крутые S-дуги, маршрут по карточке составляешь сам.
● Основное 1.12.12.23.13.24.14.25.15.2
★ Средний уровень 1.12.12.23.13.24.14.25.15.2
★★ Продвинутый уровень 1.12.12.23.13.24.14.25.15.2
  1. Выпиши из журнала задания ★★, которые у тебя получились, и числа надёжности из уроков: ошибку поворота, сколько раз робот прошёл крутую дугу, точность остановки у груза, надёжность доставки из урока 5.2.
  2. Для продвинутого поля назови два самых слабых умения по этим числам.
  3. Выбери поле сам и обоснуй выбор числами. Запиши, что ты сделаешь со слабыми умениями до зачёта.
Должно получиться

Поле выбрано и обосновано числами из журнала, два слабых умения и план по ним записаны.

2

Разберись в задании своего уровня

8 мин
Начальное поле: старт справа вверху, S-образная линия с двумя перекрёстками, у ответвлений грузы и места доставки, финиш слева внизу
Начальное поле.
  1. Выехать из зоны старта влево и найти линию.
  2. По линии пройти левую дугу и доехать до первого перекрёстка.
  3. Перевезти груз 1 с верхнего конца ответвления на нижний — в место 1.
  4. Доехать до второго перекрёстка и перевезти груз 2 с нижнего конца на верхний — в место 2.
  5. Пройти правую дугу, выехать с конца линии в зону финиша и остановиться.

У старта и финиша есть короткие участки без линии: там робот едет прямо по твоим командам.

Среднее поле: сеть дорог с четырьмя перекрёстками А, Б, В, Г, старт слева, финиш справа внизу, два груза и два места доставки
Среднее поле. Перекрёстки отмечены буквами А, Б, В, Г.

Груз 1 → место 1, груз 2 → место 2, затем финиш. Маршрут задан по перекрёсткам:

УчастокЧерез перекрёстки
Старт → груз 1А → В → Б
Груз 1 → место 1Б → В
Место 1 → груз 2В → Г
Груз 2 → место 2Г
Место 2 → финишГ

Куда поворачивать на каждом перекрёстке — налево, направо или прямо, — определяешь ты. Через некоторые перекрёстки робот проезжает несколько раз. Ветки, которых нет в маршруте, не используются.

Продвинутое поле: шесть перекрёстков А–Е, крутая S-образная связка справа, позиции 1–6 на концах линий, старт слева внизу, финиш вверху
Продвинутое поле. Перекрёстки А–Е, конечные позиции 1–6. На схеме — расстановка для карточки 1.

Банки стоят в позициях 1 и 5. Перед планированием вытяни карточку из стопки: на ней написано, в каком порядке и куда их перевезти.

КарточкаПервая доставкаВторая доставка
11 → 45 → 3
21 → 35 → 4
35 → 31 → 4
45 → 41 → 3

Готового маршрута нет: путь по перекрёсткам ты выбираешь сам, до запуска. Правильных путей несколько. Справа на поле — крутая S-образная связка.

Проверь, что твой робот проходит S-образную связку с грузом.

  1. Покажи пальцем на схеме: старт, оба груза, оба места доставки, финиш.
  2. Прочитай критерии оценки в конце урока. За что дают больше всего баллов?
Должно получиться

Ты можешь пересказать задание своего уровня своими словами.

3

Проверь умения по отдельности

10 мин
Безопасность

После загрузки старой программы робот может сразу поехать. Загружай программы при выключенном питании моторов, первый запуск — на подставке.

Если хотя бы одно умение не работает, вся программа не поедет. Проверь каждое коротким опытом.

  1. Оба колеса крутятся вперёд и назад; моторы стоят, когда выключены (урок 2.1).
  2. Робот поворачивает на 90° и разворачивается на 180° (урок 2.2).
  3. Робот едет по линии с П-регулятором и проходит дуги своего поля (урок 3.2).
  4. Робот находит перекрёсток и поворачивает с него на нужную линию (уроки 4.1, 4.2).
  5. Ультразвук видит груз на нужном расстоянии (урок 5.1).
  6. Захват берёт, держит и отпускает груз (урок 5.2).
Должно получиться

Все умения проверены. То, что не работает, ты сначала чинишь по разделу «Если не получилось» своего урока.

Если хотя бы одно умение не работает, вся программа не поедет. Проверь каждое три раза коротким опытом.

УмениеУрокПрошло из 3
Колёса вперёд, назад и стоп2.1
Поворот 90° и разворот 180°2.2
Линия и дуги своего поля3.2
Перекрёсток и поворот с него4.1, 4.2
Остановка перед грузом5.1
Захват берёт и отпускает5.2
Должно получиться

Таблица заполнена. Умения, которые прошли меньше трёх раз из трёх, ты чинишь первыми.

Проверь умения по своим числам: для каждого запиши, какой результат ты считаешь надёжным, и проверь пять раз.

УмениеМой критерийПрошло из 5
Прямо: увод на 1 мне больше … см
Поворот 90°ошибка не больше …°
Крутая дуга своего полябез схода
Перекрёсток и поворот с негона новую линию
Остановка у груза… ± 1 см
Захватгруз не выпадает при развороте

Умения, которые не прошли пять раз из пяти, отметь — с них начнёшь работу.

Должно получиться

Критерии записаны числами, у каждого умения — результат из пяти проб.

4

Составь план маршрута

16 мин

Хороший инженер сначала планирует, потом пишет код. Самое сложное — повороты на обратном пути: «налево» и «направо» считаются от носа робота, а не от тебя.

УчастокЧто делает роботКакая функция
Старт → линияЕдет прямо до линииdriveStraight()
…
  1. Заполни таблицу плана в бортовом журнале. Одна строка — один участок.
  2. Для каждого перекрёстка запиши: откуда приехал, куда повернул, что делаешь дальше.
  3. Проведи выключенного робота по плану руками. Куда смотрит его нос после каждого разворота?
  4. Запиши, где скорее всего будет ошибка и как ты это проверишь.

В плане семь участков: выезд, линия до перекрёстка 1, груз 1, место 1, перекрёсток 2, груз 2 и место 2, финиш.

В плане есть повороты налево, направо и проезд прямо. Каждая встреча с перекрёстком записана отдельной строкой — даже если это тот же перекрёсток. Программа не видит букв: она знает только, какое по счёту решение сейчас выполняет.

План составлен по своей карточке. Для каждой доставки ты выбрал путь по буквам перекрёстков и можешь объяснить, почему именно его. Маршрут записан так, чтобы для другой карточки менялись только решения на перекрёстках.

Подсказка: Робот развернулся — а что теперь «налево»?

Встань и повернись так же, как робот. Твоя левая рука показывает его «налево».

Должно получиться

План записан. Напарник прошёл по нему с выключенным роботом и не нашёл ошибок.

2
Занятие 2 · 45 мин
Программа по частям
5

Подготовь заготовку

6 мин

Программу собираем из функций. Функция — действие с понятным именем, например turnLeft(). Когда маршрут записан вызовами функций, его легко сверить с планом.

Заготовка с функциямиL61_Courier.ino.ino
// Робот-курьер: моя программа итоговой задачи.// Это заготовка, а не решение. Наполни функции своим проверенным кодом// из прошлых уроков и проверяй каждую отдельно, прежде чем собирать маршрут. // --- Моторы (урок 2.1) ---const int LEFT_DIR_PIN = 7;const int LEFT_SPEED_PIN = 6;const int RIGHT_DIR_PIN = 4;const int RIGHT_SPEED_PIN = 5;const int FORWARD = HIGH; // --- Датчики линии (уроки 3.1 и 3.2) ---const int LEFT_SENSOR_PIN = A4;const int RIGHT_SENSOR_PIN = A5;// Перенеси сюда свои измерения белого и чёрного и коэффициент П-регулятора. // --- Ультразвук (урок 5.1) и захват (урок 5.2) ---// Перенеси сюда контакты и настройки из своих проверенных скетчей. void stopMotors() {  // Остановить оба мотора (урок 2.1).} void driveStraight() {  // Проехать прямо проверенное время (урок 2.1), например въезд в зону финиша.} void turnLeft() {  // Повернуть налево на 90° (урок 2.2) или с перекрёстка до линии (урок 4.2).} void turnRight() {  // Повернуть направо.} void turnAround() {  // Развернуться на 180° (урок 2.2).} void followLineToCrossing() {  // Ехать по линии с П-регулятором до перекрёстка (уроки 3.2, 4.1).  // После перекрёстка сначала съедь с широкой чёрной области.} void approachCan() {  // Ехать по линии и остановиться перед грузом по ультразвуку (урок 5.1).} void pickUp() {  // Остановить моторы и закрыть захват (урок 5.2).} void putDown() {  // Открыть захват и отпустить груз (урок 5.2).} void setup() {  // Подготовь выводы, датчики и захват. Убедись, что моторы стоят.   // Здесь — твой маршрут: вызывай функции в порядке своего плана.   stopMotors();} void loop() {  // Задание выполняется один раз в setup(). После финиша робот стоит.}
  1. Открой заготовку и сохрани её под своим именем.
  2. Перенеси в функции свой проверенный код из прошлых уроков.
Как это устроено: Зачем делить программу на функции

Если поворот работает неправильно, исправлять нужно одно место — саму функцию, а не каждый поворот на маршруте. А маршрут в setup() читается почти как план из журнала.

Программу собираем из функций. Когда маршрут записан вызовами функций, его легко сверить с планом.

Заготовка: маршрут по плануL61_Courier_Plan.ino.ino
// Робот-курьер, средний уровень: маршрут по плану из журнала.// Функций здесь нет: перенеси свои проверенные функции из уроков 2.1–5.2 в раздел ниже.// Маршрут в setup() разбит на участки плана. В каждый участок впиши вызовы функций. // --- Контакты, измерения и настройки из своих скетчей --- // --- Мои функции (перенеси и проверь каждую отдельно) ---// Нужны: остановка моторов, прямой проезд, линия до перекрёстка, проезд перекрёстка// прямо, поворот налево и направо на перекрёстке, разворот на линии, подъезд к грузу,// взять груз, поставить груз. void setup() {  // Подготовь выводы, датчики и захват. Моторы стоят.   // Участок 1. Старт → линия:   // Участок 2. Линия → груз 1:   // Участок 3. Груз 1 → место 1:   // Участок 4. Место 1 → груз 2:   // Участок 5. Груз 2 → место 2:   // Участок 6. Место 2 → финиш, стоп:} void loop() {  // Задание выполняется один раз в setup(). После финиша робот стоит.}
  1. Открой заготовку и сохрани её под своим именем. Готовых функций в ней нет.
  2. Перенеси свои проверенные функции из уроков 2.1–5.2. Проверь по списку в заготовке, что ни одной не забыл.
  3. Перед каждой строкой плана в журнале запиши, какая функция её выполняет. Если для строки нет функции — напиши её и проверь отдельно.
Должно получиться

Все нужные функции перенесены и проверены, у каждой строки плана есть своя функция.

Требование 1. Программа собрана из твоих проверенных программ уроков 4.2–5.2, а не из заготовки:

  • действия — функции: линия до перекрёстка, решения на перекрёстке, подъезд к грузу, взять, поставить, разворот;
  • маршрут — данные, отдельные от действий: для другой карточки меняются только строки маршрута, функции не трогаются;
  • у каждой функции, которая чего-то ждёт, есть защита от бесконечного ожидания.

Своей программы из урока 5.2 нет — возьми у преподавателя решение урока 5.2 как основу; маршрут и защиты пишешь сам.

  1. Запиши в журнал список функций своей программы: что делает каждая и в каком уроке проверена.
  2. Отметь функции без защиты от бесконечного ожидания и добавь защиту.
Должно получиться

Список функций с уроком проверки, маршрут отделён от действий, защиты добавлены.

Продвинутый уровень · Маршрут массивом

Запиши решения на перекрёстках массивом, например char route[] = {'L', 'S', 'R'};, и выполняй их по очереди в цикле, как в уроке 4.2. Тогда для новой карточки достаточно поменять одну строку.

6

Часть 1: от старта до первого груза

12 мин
  1. В setup() вызови функции первого участка: выезд, линия, перекрёсток, поворот, подъезд к грузу.
  2. Проверь на поле только эту часть.
  3. Работает — сохрани копию: «рабочая версия 1».
Должно получиться

Робот выезжает со старта, доезжает по линии до груза 1 и останавливается перед ним.

Если не получилось
Что видишьЧто проверить
Робот не находит линию после стартаСмотрит ли нос робота вдоль линии? Хватает ли длины выезда?
Робот останавливается на дугеДуга — не перекрёсток. Проверь условие поиска перекрёстка.
Робот проезжает перекрёстокСкорость велика или условие перекрёстка не срабатывает.
После поворота робот теряет линиюПроверь, что поворот заканчивается, когда датчик нашёл новую линию.
  1. Запиши в журнал вызовы функций первого участка по плану: выезд, линия, перекрёстки, повороты, подъезд к грузу. Потом перенеси их в setup().
  2. Проверь на поле только эту часть три раза. Робот должен каждый раз остановиться перед грузом 1.
  3. Работает — сохрани копию: «рабочая версия 1».
Подсказка: Где искать ошибку

Сравни, что сделал робот, со строкой плана. На какой строке разошлось? Проверь эту одну функцию отдельно.

Должно получиться

Робот три раза подряд выезжает со старта и останавливается перед грузом 1.

Приёмка части 1. Робот от старта до груза 1: три раза из трёх, в том числе когда его поставили в зоне старта на 2 см левее или правее и чуть повёрнутым.

  1. Запиши маршрут первой части в данные программы и проверь.
  2. Каждую неудачу запиши: какая функция не сработала и почему.
  3. Работает — сохрани «рабочую версию 1».
ЗапускКак стоял на стартеРезультатЧто сорвалось
1Ровно
22 см левее
32 см правее, повёрнут
Должно получиться

Три запуска из трёх с разной постановкой на старте, неудачи разобраны.

7

Часть 2: первая доставка

12 мин
  1. Допиши: захват, разворот, путь к месту 1, отпускание, возвращение к дороге.
  2. Проверь только эту часть: поставь робота перед грузом 1 и запусти.
  3. Работает — сохрани «рабочую версию 2».
Подсказка: Когда закрывать захват?

Сначала останови моторы, потом закрывай захват. Подъезд заканчивай по измерению ультразвука. Если эха нет, это не значит «груз совсем рядом».

Подсказка: Груз уезжает вместе с роботом

Отъезжай только после того, как захват полностью открылся.

Должно получиться

Груз 1 стоит в месте 1, захват открыт, робот на дороге и смотрит туда, куда нужно дальше.

  1. Запиши в журнал вызовы функций первой доставки: захват, разворот, путь к месту 1, отпускание, возвращение к дороге. Потом допиши их в программу.
  2. Проверь только эту часть три раза: ставь робота перед грузом 1 и запускай.
  3. Работает — сохрани «рабочую версию 2».
Подсказка: Порядок с захватом

Вспомни правило урока 5.2: остановиться — закрыть; открыть — подождать — отъехать.

Должно получиться

Три раза подряд груз 1 стоит в месте 1, робот на дороге и смотрит туда, куда нужно дальше.

Приёмка части 2. Первая доставка три раза из трёх, в том числе когда груз сдвинут на 1 см в сторону от линии.

  1. Допиши в данные маршрута первую доставку и проверь её отдельно: ставь робота перед грузом 1.
  2. Как робот узнаёт место отпускания? Запиши, чем это место определяется в твоей программе и насколько точно.
  3. Работает — сохрани «рабочую версию 2».
Должно получиться

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

8

Часть 3: вторая доставка и финиш

15 мин
  1. Допиши вторую доставку и путь к финишу. Груз 2 стоит в другом месте, поэтому повороты будут другими — сверься с планом.
  2. В конце — прямой въезд в финиш и остановка. После финиша робот должен стоять.
  3. Запусти весь маршрут от старта. Сохрани «рабочую версию 3».
Подсказка: Как понять, что пора въезжать в финиш

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

Должно получиться

Робот проходит весь маршрут хотя бы в одной пробе. Все рабочие версии сохранены.

Проверь себя
  1. Допиши вторую доставку и путь к финишу по плану. Груз 2 стоит в другом месте, поэтому повороты будут другими. Каждую встречу с перекрёстком — отдельной строкой.
  2. В конце — прямой въезд в финиш и остановка. После финиша робот должен стоять.
  3. Запусти весь маршрут от старта. Если сорвалось — запусти с места срыва только оставшуюся часть. Сохрани «рабочую версию 3».
Подсказка: Как понять, что пора въезжать в финиш

Чёрной метки финиша на поле нет. Используй последний перекрёсток и проверенное время прямого движения.

Должно получиться

Робот проходит весь маршрут хотя бы в одной пробе. Все рабочие версии сохранены.

Проверь себя
  1. Допиши вторую доставку и путь к финишу. После финиша робот стоит.
  2. Запусти весь маршрут от старта. Сохрани «рабочую версию 3».
  3. Проверь отделение маршрута от действий: вытяни другую карточку, составь для неё маршрут и поменяй только данные маршрута. Робот проходит новый маршрут хотя бы один раз.
  4. Запиши, какие части маршрута ненадёжны и что ты проверишь в уроке 6.2.
Должно получиться

Маршрут своей карточки пройден целиком, маршрут второй карточки пройден после замены только данных. Список ненадёжных частей записан.

Проверь себя

Критерии оценки

Зачётные заезды пройдут в уроке 6.2 по этим критериям.

Как оценивается заезд

Максимум 100Зачёт от 70
КритерийБаллыМакс.
Прямой выезд со старта
5 — Сам выехал вперёд из зоны старта.
10 — Доехал до линии и продолжил задание без подталкивания.
10
Движение по линии
10 — Терял линию, но сам возвращался на неё и продолжал маршрут.
20 — Прошёл все участки линии и дуги по датчикам, не теряя линию.
20
Выбор дороги на перекрёстках
5 — За каждое из четырёх решений на перекрёстках, которые совпали с планом.
20
Груз 1: обнаружение
5 — Остановился перед грузом по ультразвуку.
5
Груз 1: захват
5 — Взял груз захватом и увёз, а не вытолкнул.
5
Груз 1: доставка
5 — Груз стоит и частично находится в нужной зоне.
10 — Груз стоит целиком в нужной зоне, захват отпущен.
10
Груз 2: обнаружение
5 — Остановился перед грузом по ультразвуку.
5
Груз 2: захват
5 — Взял груз захватом и увёз, а не вытолкнул.
5
Груз 2: доставка
5 — Груз стоит и частично находится в нужной зоне.
10 — Груз стоит целиком в нужной зоне, захват отпущен.
10
Прямой въезд и остановка
5 — После линии сам въехал вперёд в зону финиша.
10 — Остановился внутри зоны и стоит с выключенными моторами не меньше 3 секунд.
10

Обязательные условия зачёта

  • Команда объясняет свою программу: показывает прямое движение, расчёт П-регулятора, остановку по ультразвуку, управление захватом и строку, которая завершает поездку.
  • Изгибы пройдены по показаниям датчиков линии с П-регулятором.
  • Подъезд к грузу заканчивается по измерению ультразвука, а не только по времени.
  • После старта робота никто не трогает и не отправляет ему команд.

Правила

  • Скорость не оценивается: нет бонуса за быстрый заезд.
  • Отрицательных штрафов нет. Невыполненное действие просто не получает баллов.
  • Груз в чужой зоне или доставленная не по порядку — 0 за её доставку.
  • После помощи руками автономный заезд окончен. Продолжать можно для тренировки, но баллы не добавляются.
  • Каждая попытка оценивается отдельно. Лучшие части разных попыток не складываются.
  • Аварийная остановка ради безопасности не штрафуется.

Проверь себя

Выберите ответы для подготовки. Они не сохраняются и не оцениваются на сайте. Пройдите проверку знаний в программе курса или обсудите ответы с преподавателем.

● Основное

1.Робот ехал на восток и развернулся на 180°. Теперь ему нужно повернуть на север. Куда он поворачивает?
2.Робот нашёл перекрёсток и повернул. Почему перед поиском следующего перекрёстка нужно сначала съехать с чёрной области?
3.Поворот налево оформлен функцией turnLeft() и вызывается четыре раза. Поворот работает неправильно. Сколько мест нужно исправить?
4.Зачем собирать программу по частям и сохранять копию после каждой работающей части?
5.На среднем поле робот проезжает перекрёсток В несколько раз. Как программа понимает, что делать на нём в этот раз?

★ Средний уровень

1.На среднем поле одни и те же перекрёстки встречаются несколько раз. Что поможет не перепутать решения?
2.У строки плана «проехать перекрёсток Б прямо» нет своей функции. Что сделать до сборки маршрута?
3.Часть 1 сработала один раз из трёх. Можно ли переходить к части 2?
4.Весь маршрут сорвался в части 3. Как быстрее проверить исправление?
5.Почему маршрут записывают в setup(), а не в loop()?

★★ Продвинутый уровень

1.Карточка маршрута: налево, направо, прямо. Какая запись верна?
2.Для новой карточки пришлось менять функцию turnLeftAtCrossing(). Что это говорит о программе?
3.Функция подъезда ждёт груз, а груз забыли поставить. Что должно уберечь робота?
4.Часть 1 проходит, если робот стоит на старте ровно, и срывается, если он на 2 см левее. Где слабое место?
5.Маршрут по карточке из четырёх решений обрывается раньше. Почему?
char route[] = {'L', 'S', 'R', 'L'};for (int i = 0; i < 3; i++) {  followLineToCrossing();  decide(route[i]);}

Отвечай на вопросы своего уровня. Нужно верных ответов: Начальный — 4 из 5; Средний — 4 из 5; Продвинутый — 4 из 5. Ответы проверит преподаватель или программа курса.

Итог

У тебя есть план и программа, которая проходит маршрут. В уроке 6.2 ты сделаешь её надёжной и проведёшь зачётные заезды.

Как подтвердить результат

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

Словарь урока

функцияИменованный блок команд, который можно вызывать много раз: например, turnLeft().
план маршрутаТаблица участков пути: что делает робот и какая функция за это отвечает.
рабочая версияКопия программы, в которой всё написанное уже проверено на поле.
Тема 6 · Урок 6.2

Робот-курьер: зачётные заезды

Итоговая задача, часть 2: делаешь программу надёжной, проводишь зачётные заезды и разбираешь результат.

2 × 45 мин1–2 ученика на роботаПроект

Результат урока

Проведены две зачётные попытки, каждая оценена по листу. Команда объясняет свою программу и рассказывает, какую ошибку нашла и как исправила.

Ты научишься

  • Находить причину ошибки по наблюдению и менять за раз одну вещь.
  • Проверять, что программа работает при немного других условиях старта.
  • Провести автономный заезд по правилам и оценить его по критериям.
  • Объяснить устройство своей программы и следующий шаг улучшения.

Нужно знать

  • 6.1 Робот-курьер: план и программа

Понадобится

Ультразвуковой датчик расстоянияЗахват с сервоприводомДатчик линии (аналоговый, Trema) × 2

Зачем этот урок

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

Разминка · вспомни
  1. Какой участок твоего плана в прошлый раз работал ненадёжно?
  2. Почему после помощи руками в зачётной попытке баллы продолжения не засчитываются?
1
Занятие 1 · 45 мин
Отладка и надёжность
1

Пробный заезд целиком

8 мин

Что должен делать робот. Задача робота — повторяемо выполнить доставку по своему плану. Задача команды — доказать это наблюдениями и объяснить, почему программа выбирает каждое действие. Используем готового робота, поле, журнал и свою проверенную программу.

Что используем и как это работает. Во время заезда работает цепочка: датчик даёт сигнал, программа проверяет условие, исполнитель выполняет команду, новое измерение показывает изменение. Ошибка может возникнуть на любом звене. Отладка начинается с первого места, где наблюдение расходится с прогнозом: например, датчик увидел полосу, но условие её не распознало.

Как связать измерение с действием. Записывай ожидаемое действие и фактический результат, меняй одну причину за раз и повторяй тот же участок. Разовый успех не доказывает повторяемость. Успешная сборка доказывает допустимость программы для компилятора, а доставка и остановка подтверждаются только наблюдением самого заезда.

  1. Поставь грузы на места, робота — в зону старта.
  2. Запусти весь маршрут и не вмешивайся.
  3. Запиши в журнал, на каком участке плана робот впервые сделал не то, что нужно.
Должно получиться

Ты знаешь, какой участок работает ненадёжно, — или что весь маршрут прошёл.

  1. Поставь грузы на места, робота — в зону старта.
  2. Запусти весь маршрут и не вмешивайся.
  3. Отметь в своём плане из урока 6.1 каждую строку: выполнена или нет. Первая невыполненная строка — участок, с которого начнёшь отладку.
Должно получиться

План размечен по пробному заезду, участок для отладки выбран.

  1. Проведи два пробных заезда подряд без исправлений между ними.
  2. Для каждого запиши: на каком этапе первый срыв (выезд, линия, перекрёсток, подъезд, захват, разворот, постановка, финиш) и сколько секунд длился заезд.
  3. Срыв в одном месте в обоих заездах или в разных? Что это говорит о причине?
Должно получиться

Два заезда в журнале с этапом срыва и временем, вывод о том, случайная ошибка или постоянная.

2

Найди и исправь ошибку

25 мин

Отладка идёт по кругу из четырёх шагов. Главное правило: меняй одну вещь за раз, иначе не поймёшь, что помогло.

ШагЧто делаешьПример
НаблюдаюОписываю, что робот сделалРобот проехал мимо груза 2
ПричинаПредлагаю одну возможную причинуНе досчитал перекрёсток: проехал его, не заметив
Одно изменениеМеняю одну вещьСнижаю скорость перед перекрёстком
ПроверкаЗапускаю только этот участокРобот останавливается на перекрёстке 3 раза из 3
  1. Заполни такую таблицу в журнале для своей ошибки.
  2. Проверяй исправление на своём участке, а не всем маршрутом: так быстрее.
  3. Когда участок работает три раза подряд, проверь весь маршрут и сохрани новую рабочую версию.
Подсказка: Не понимаю, что делает программа

Временно добавь вывод в монитор порта: какое решение маршрута выполняется и что видят датчики. Перед зачётом этот вывод можно оставить — он не мешает.

Должно получиться

Ошибка найдена и исправлена, в журнале записан путь к причине.

Отладка идёт по кругу: наблюдаю → причина → одно изменение → проверка. Меняй одну вещь за раз, иначе не поймёшь, что помогло.

НаблюдаюПричинаОдно изменениеПроверка
  1. Заполни таблицу в журнале для своей ошибки. Если первая причина не подтвердилась, добавь строку со следующей.
  2. Проверяй исправление только на своём участке: поставь робота перед ним.
  3. Когда участок работает три раза подряд, проверь весь маршрут и сохрани новую рабочую версию.
Подсказка: Не знаю, на каком участке ошибка

Запусти первую половину маршрута. Сработала — ищи во второй. Так за два-три запуска найдёшь участок.

Должно получиться

Ошибка найдена и исправлена, в журнале — строки круга отладки.

Отлаживай по доказательствам, а не по догадке.

  1. Добавь в программу диагностику: номер выполняемого решения маршрута и показания датчиков в момент решения выводятся в монитор порта. Подключи USB-кабель и запусти робота с ним, придерживая кабель; или зажигай светодиод L на каждом решении.
  2. Для своей ошибки запиши гипотезу и опыт, который может её опровергнуть, — до того, как менять код.
  3. Проведи опыт. Гипотеза не подтвердилась — запиши новую. В журнале должно быть не меньше двух кругов, если первый не дал ответа.
  4. Исправление засчитывается, когда участок проходит пять раз из пяти.
ГипотезаОпытЧто показалВывод
Должно получиться

Диагностика работает, в журнале — гипотезы, опыты и выводы, участок проходит пять раз из пяти.

3

Проверь надёжность

12 мин

На зачёте условия будут чуть другими: робот поставлен на старт не идеально, батарея немного разряжена. Проверь заранее.

  1. Поставь робота на старт чуть левее, потом чуть правее. Маршрут проходит?
  2. Если робот стал ехать медленнее, чем на пробах, попроси преподавателя проверить батарею. После замены или зарядки заново подбери время прямых участков и поворотов.
  3. Проведи весь маршрут два раза подряд без исправлений между запусками.

Два полных маршрута подряд с разных положений на старте.

Все решения на перекрёстках верны в двух заездах подряд.

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

Должно получиться

Программа проходит маршрут два раза подряд. Рабочая версия для зачёта сохранена.

2
Занятие 2 · 45 мин
Зачёт и разбор
4

Зачётная попытка 1

10 мин
  1. Расставь грузы на исходные места по схеме своего поля.
  2. Перед стартом договоритесь, за какими четырьмя решениями на перекрёстках следить.
  3. Один запуск. Дальше робот едет сам: нельзя трогать его, поправлять грузы или отправлять команды.
  4. Если нужна помощь, останови робота. Безопасность важнее баллов.
Должно получиться

Попытка записана в лист оценки.

5

Исправь, если нужно

8 мин

Между попытками можно исправлять программу. Используй круг отладки: наблюдение → причина → одно изменение → проверка участка.

Должно получиться

Исправление проверено на участке и сохранено — или ты оставил программу без изменений.

6

Зачётная попытка 2

10 мин

Проведи вторую попытку по тем же правилам. Каждая попытка оценивается отдельно: лучшие части разных попыток не складываются.

Должно получиться

Обе попытки записаны в лист оценки.

7

Разбор результата

12 мин
  1. Сначала оцени свою попытку по листу оценки сам, потом сверь оценку с преподавателем.
  2. Покажи в своей программе: прямое движение, расчёт П-регулятора, остановку по ультразвуку, управление захватом и строку, которая завершает поездку.
  3. Расскажи об одной ошибке, которую ты нашёл и исправил. Как ты понял, в чём причина?
  4. Запиши в журнал: какое умение у твоего робота работает лучше всего, а какое ты бы улучшил дальше.
  5. Сохрани итоговый скетч и лист оценки.
Проверь себя
  1. Сначала оцени свою попытку по листу оценки сам, потом сверь оценку с преподавателем.
  2. Покажи в своей программе: прямое движение, расчёт П-регулятора, остановку по ультразвуку, управление захватом и строку, которая завершает поездку.
  3. Расскажи об одной ошибке, которую ты нашёл и исправил, по строкам круга отладки.
  4. Объясни одно своё решение: почему на каком-то перекрёстке или участке ты сделал именно так. Что было бы при другом решении?
  5. Запиши в журнал: какое умение у твоего робота работает лучше всего, а какое ты бы улучшил дальше и как.
  6. Сохрани итоговый скетч и лист оценки.
Проверь себя
  1. Сначала оцени свою попытку по листу оценки сам, потом сверь оценку с преподавателем.
  2. Покажи в своей программе: прямое движение, расчёт П-регулятора, остановку по ультразвуку, управление захватом и строку, которая завершает поездку.
  3. Напиши короткий инженерный отчёт в журнал: - надёжность по своим данным: сколько проб каждого участка прошло; - главная ошибка, гипотезы и опыты, которыми ты её нашёл; - самый большой оставшийся риск и одно изменение, которое бы его уменьшило, — и как ты проверил бы, что оно помогло.
  4. Расскажи отчёт классу за две минуты.
  5. Сохрани итоговый скетч, лист оценки и отчёт.
Проверь себя

Критерии оценки

Как оценивается заезд

Максимум 100Зачёт от 70
КритерийБаллыМакс.
Прямой выезд со старта
5 — Сам выехал вперёд из зоны старта.
10 — Доехал до линии и продолжил задание без подталкивания.
10
Движение по линии
10 — Терял линию, но сам возвращался на неё и продолжал маршрут.
20 — Прошёл все участки линии и дуги по датчикам, не теряя линию.
20
Выбор дороги на перекрёстках
5 — За каждое из четырёх решений на перекрёстках, которые совпали с планом.
20
Груз 1: обнаружение
5 — Остановился перед грузом по ультразвуку.
5
Груз 1: захват
5 — Взял груз захватом и увёз, а не вытолкнул.
5
Груз 1: доставка
5 — Груз стоит и частично находится в нужной зоне.
10 — Груз стоит целиком в нужной зоне, захват отпущен.
10
Груз 2: обнаружение
5 — Остановился перед грузом по ультразвуку.
5
Груз 2: захват
5 — Взял груз захватом и увёз, а не вытолкнул.
5
Груз 2: доставка
5 — Груз стоит и частично находится в нужной зоне.
10 — Груз стоит целиком в нужной зоне, захват отпущен.
10
Прямой въезд и остановка
5 — После линии сам въехал вперёд в зону финиша.
10 — Остановился внутри зоны и стоит с выключенными моторами не меньше 3 секунд.
10

Обязательные условия зачёта

  • Команда объясняет свою программу: показывает прямое движение, расчёт П-регулятора, остановку по ультразвуку, управление захватом и строку, которая завершает поездку.
  • Изгибы пройдены по показаниям датчиков линии с П-регулятором.
  • Подъезд к грузу заканчивается по измерению ультразвука, а не только по времени.
  • После старта робота никто не трогает и не отправляет ему команд.

Правила

  • Скорость не оценивается: нет бонуса за быстрый заезд.
  • Отрицательных штрафов нет. Невыполненное действие просто не получает баллов.
  • Груз в чужой зоне или доставленная не по порядку — 0 за её доставку.
  • После помощи руками автономный заезд окончен. Продолжать можно для тренировки, но баллы не добавляются.
  • Каждая попытка оценивается отдельно. Лучшие части разных попыток не складываются.
  • Аварийная остановка ради безопасности не штрафуется.

Проверь себя

Выберите ответы для подготовки. Они не сохраняются и не оцениваются на сайте. Пройдите проверку знаний в программе курса или обсудите ответы с преподавателем.

● Основное

1.Оба датчика линии показывают «белое». Что это значит?
2.Ультразвуковой датчик не получил эхо. Как поступить программе?
3.Робот проехал мимо груза. Что делаешь первым?
4.Во время зачётной попытки робот сбился, и ты поставил его обратно на линию. Что с попыткой?
5.После финиша робот снова поехал. В чём вероятная причина?

★ Средний уровень

1.На среднем поле робот выбрал на перекрёстке не то решение. Где искать причину сначала?
2.Зачем перед зачётной попыткой делать тренировочный заезд?
3.Робот проехал мимо груза 2. Какая запись в круге отладки — «причина», а не «наблюдение»?
4.Не знаешь, на каком участке ошибка. Как найти его за 2–3 запуска?
5.Почему между попытками меняют только одну вещь?

★★ Продвинутый уровень

1.На крутой S-дуге робот теряет линию только со свежезаряженной батареей. Что вероятнее всего?
2.Что записать в журнал после неудачного заезда? Выбери все верные ответы.
3.Гипотеза: «робот пропускает перекрёсток В из-за скорости». Какой опыт может её опровергнуть?
4.Диагностика печатает номер решения на каждом перекрёстке. Робот повернул не там, а последним напечатано «решение 4», хотя по плану это перекрёсток 5. Что это значит?
5.В отчёте: «участок 3 прошёл 4 раза из 10». Какой вывод верен?

Отвечай на вопросы своего уровня. Нужно верных ответов: Начальный — 4 из 5; Средний — 4 из 5; Продвинутый — 4 из 5. Ответы проверит преподаватель или программа курса.

Итог

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

Как подтвердить результат

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

Словарь урока

отладкаПоиск и исправление ошибки: наблюдаем, находим причину, меняем одну вещь и проверяем снова.
автономный заездЗаезд, в котором робот после старта едет сам: никто его не трогает и не отправляет команд.
надёжностьПрограмма работает не один раз случайно, а каждый раз — даже если робот поставлен на старт чуть иначе.
Тема 7 · Урок Ф.1

Энкодеры: считаем обороты и точное расстояние

Робот считает обороты колёс и проезжает отмеренное расстояние, а не «сколько успеет за время».

2 × 45 мин1–2 ученика на роботаПрактика

Результат урока

Программа считает обороты колёс энкодерами и проезжает отмеренное расстояние. Число импульсов на оборот измерено, точность проверена линейкой. Не нужен для итоговой задачи.

Ты научишься

  • Объяснить, как энкодер превращает оборот колеса в импульсы.
  • Измерить число импульсов на оборот колеса.
  • Пересчитать расстояние в импульсы через длину окружности колеса.
  • Проехать отмеренное расстояние и оценить точность линейкой.

Нужно знать

  • 2.1 Моторы: едем и останавливаемся
  • 3.2 П-регулятор: плавно и быстро

Понадобится

Колесо × 2Мотор-редуктор × 2USB-кабель для Arduino

Зачем этот урок

В уроке 2.1 робот ехал «по времени»: моторы включались на две секунды. Но сколько он проедет за две секунды, зависит от заряда батарей, пола и веса. Энкодер считает обороты колеса, поэтому программа может выбирать момент остановки по вращению колёс, а не только по времени. Точность пути всё равно нужно проверить линейкой.

Для урока нужен дополнительный модуль: энкодер на каждое колесо. В основной комплект курса он не входит; без него урок не провести. Факультатив не нужен для итоговой задачи.

Разминка · вспомни
  1. Как в уроке 2.1 робот проезжал заданный участок?
  2. Почему при тех же двух секундах робот с севшими батареями проезжает меньше?
  3. Что такое П-регулятор из урока 3.2 в одной фразе?
1
Занятие 1 · 45 мин
Считаем импульсы
1

Почему время — плохая линейка

8 мин

Что должен делать робот. Роботу нужно проехать заданное расстояние точнее, чем по секундомеру. Для этого понадобятся дополнительные энкодеры: в базовом комплекте их нет. Они измеряют вращение колёс, а не расстояние до препятствия.

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

Как связать измерение с действием. Измерим импульсы на оборот и используем диаметр своего колеса. На AVR чтение многобайтного счётчика прерывание может разорвать посередине, поэтому копируем его при кратко выключенных прерываниях. Проверим результат линейкой: при проскальзывании колесо вращается, а корпус проходит меньше, поэтому энкодер не даёт абсолютной координаты.

  1. Вспомни программу из урока 2.1, которая едет вперёд по времени. Запусти её дважды: на полной скорости и на скорости вдвое меньше. Время одинаковое.
  2. Отметь, где робот остановился оба раза. Измерь линейкой оба пути и запиши в журнал.
Должно получиться

Ты видишь: при одном и том же времени робот проезжает разное расстояние.

  1. Запусти программу «по времени» из урока 2.1 трижды: скорость 100, 150 и 200 при одном времени. Измерь пути линейкой.
  2. Ответь в журнале: что, кроме скорости в программе, меняет путь за одно и то же время? Назови не меньше трёх причин.
Должно получиться

Три пути измерены, причины записаны.

  1. Измерь пути «по времени» при скорости 100, 150 и 200. Построй в журнале таблицу «скорость — путь».
  2. Оцени: можно ли по этой таблице пересчитать время для 50 см на скорости 120? Насколько ты доверяешь такому пересчёту завтра, на другом заряде батарей? Объясни.
  3. Запиши, что нужно измерять во время езды, чтобы путь не зависел от этих причин.
Должно получиться

Таблица, оценка пересчёта и ответ — в журнале.

2

Как устроен энкодер

12 мин

Энкодер — это диск с прорезями на оси колеса и датчик-«вилка». Внутри вилки с одной стороны светит светодиод, с другой — приёмник. Когда мимо проходит прорезь, свет попадает на приёмник, и датчик посылает импульс. Один оборот колеса — столько импульсов, сколько прорезей на диске.

  1. Посчитай прорези на диске энкодера. Запиши в журнал.
  2. Найди в таблице проводки своего робота, к каким контактам подключены энкодеры. Запиши номера.
Безопасность

Не подключай энкодеры при включённом питании. Если в таблице проводки контакты энкодеров не указаны — подключение делает преподаватель.

Должно получиться

В журнале — число прорезей и номера контактов левого и правого энкодеров.

Как это устроено: Что такое прерывание

Импульсы приходят быстро — десятки в секунду. Если программа занята чем-то ещё, она может их пропустить. Поэтому счёт делает прерывание: на каждый импульс Arduino сама откладывает основную программу, прибавляет 1 к счётчику и возвращается обратно. На Arduino Uno так умеют только контакты 2 и 3.

Энкодер — диск с прорезями на оси колеса и оптический датчик щели: прорезь проходит мимо датчика — датчик даёт импульс. Считать импульсы будет прерывание: Arduino сама вызывает короткую функцию на каждый импульс, что бы ни делала основная программа.

  1. Посчитай прорези на диске, запиши номера контактов энкодеров из таблицы проводки.
Предскажи

Колесо диаметром 45 мм, на диске 20 прорезей. Сколько миллиметров проезжает колесо между двумя импульсами? Посчитай до запуска.

  1. Ответь в журнале: почему счётчик импульсов нельзя увеличивать в loop() после delay(200)?
Безопасность

Не подключай энкодеры при включённом питании. Контакты не указаны в таблице — подключает преподаватель.

Подсказка: Сколько проезжает колесо за оборот

За один оборот колесо проходит длину своей окружности: π × диаметр.

Должно получиться

Число прорезей, контакты, прогноз «мм на импульс» и ответ про delay — в журнале.

  1. Посчитай прорези, запиши контакты энкодеров из таблицы проводки.
  2. Оцени частоту импульсов: колесо 45 мм, 20 прорезей, робот едет 0,3 м/с. Сколько импульсов в секунду даёт каждый энкодер?
  3. Объясни в журнале, почему счёт делают прерыванием и почему на Arduino Uno для этого подходят только контакты 2 и 3.
Безопасность

Не подключай энкодеры при включённом питании. Контакты не указаны в таблице — подключает преподаватель.

Подсказка: Частота импульсов

Сначала — сколько оборотов в секунду делает колесо при 300 мм/с.

Должно получиться

Оценка частоты и объяснение — в журнале.

3

Импульсов на один оборот

22 мин
  1. Создай скетч LF1_Count. Впиши номера контактов энкодеров.
Счёт импульсовLF1_Count.ino.ino
// Контакты энкодеров: впиши номера из таблицы проводки своего робота.// На Arduino Uno счёт по прерыванию работает только на контактах 2 и 3.const int LEFT_ENCODER_PIN = -1;const int RIGHT_ENCODER_PIN = -1; // Счётчики импульсов. volatile: их меняет прерывание.volatile long leftTicks = 0;volatile long rightTicks = 0; // Arduino вызывает её сама на каждый импульс левого энкодера.void countLeft() {  leftTicks++;} // То же для правого энкодера.void countRight() {  rightTicks++;} void setupEncoders() {  // Без контактов энкодеров программа дальше не пойдёт.  if (LEFT_ENCODER_PIN < 0 || RIGHT_ENCODER_PIN < 0) {    Serial.println("Впиши контакты энкодеров");    while (true) {    }  }  pinMode(LEFT_ENCODER_PIN, INPUT);  pinMode(RIGHT_ENCODER_PIN, INPUT);  attachInterrupt(digitalPinToInterrupt(LEFT_ENCODER_PIN), countLeft, RISING);  attachInterrupt(digitalPinToInterrupt(RIGHT_ENCODER_PIN), countRight, RISING);} void setup() {  Serial.begin(9600);  setupEncoders();} void loop() {  // Читаем оба счётчика, пока прерывания выключены: число не изменится посередине чтения.  noInterrupts();  long left = leftTicks;  long right = rightTicks;  interrupts();   Serial.print("Левое: ");  Serial.print(left);  Serial.print("  правое: ");  Serial.println(right);  delay(200);}
  1. Загрузи и открой монитор порта. Поверни колесо рукой: число должно расти.
  2. Сделай мелом или маркером метку на колесе. Прокрути колесо рукой ровно 10 оборотов по метке. Раздели число импульсов на 10 — это импульсов на оборот.
  3. Повтори для второго колеса. Запиши оба числа в журнал.
Должно получиться

В журнале — импульсов на оборот для каждого колеса. Обычно это число прорезей на диске.

Если не получилось
Что видишьЧто сделать
«Впиши контакты энкодеров»Впиши номера контактов вместо −1
Число не растётПроверь провода и что диск проходит через щель датчика
Число растёт, когда колесо стоитКолесо остановилось на краю прорези: сдвинь его чуть-чуть
  1. Создай скетч LF1_Count по шаблону. Впиши контакты, напиши свою часть.
Шаблон: счёт импульсовLF1_Count_Gaps.ino.ino
// Шаблон среднего уровня. Напиши код в разделе «Твоя часть» по целям в комментариях. // Контакты энкодеров: впиши номера из таблицы проводки своего робота.// На Arduino Uno счёт по прерыванию работает только на контактах 2 и 3.const int LEFT_ENCODER_PIN = -1;const int RIGHT_ENCODER_PIN = -1; // Счётчики импульсов. volatile: их меняет прерывание.volatile long leftTicks = 0;volatile long rightTicks = 0; // ===== Твоя часть: напиши сам ===== void countLeft() {  // Каждый импульс левого энкодера увеличивает его счётчик на 1.} void countRight() {  // То же для правого энкодера.} void loop() {  long left = 0;  long right = 0;  // Скопируй оба счётчика в left и right так, чтобы прерывание не изменило их посередине чтения.   Serial.print("Левое: ");  Serial.print(left);  Serial.print("  правое: ");  Serial.println(right);  delay(200);} // ===== Готовое: подключение энкодеров — не меняй ===== void setupEncoders() {  // Без контактов энкодеров программа дальше не пойдёт.  if (LEFT_ENCODER_PIN < 0 || RIGHT_ENCODER_PIN < 0) {    Serial.println("Впиши контакты энкодеров");    while (true) {    }  }  pinMode(LEFT_ENCODER_PIN, INPUT);  pinMode(RIGHT_ENCODER_PIN, INPUT);  attachInterrupt(digitalPinToInterrupt(LEFT_ENCODER_PIN), countLeft, RISING);  attachInterrupt(digitalPinToInterrupt(RIGHT_ENCODER_PIN), countRight, RISING);} void setup() {  Serial.begin(9600);  setupEncoders();}
  1. Прокрути каждое колесо рукой ровно 10 оборотов по метке и посчитай импульсы на оборот.
  2. Сравни с числом прорезей. Совпало? Если нет — найди причину.
Подсказка: Как не испортить число при чтении

Счётчик — long, это 4 байта, а Arduino читает их по одному. Прерывание посередине даст смесь старого и нового числа. Есть пара команд, которая на время выключает прерывания.

Должно получиться

Счёт работает, импульсы на оборот обоих колёс записаны и сравнены с числом прорезей.

Требование 1. Программа считает импульсы каждого колеса прерыванием и печатает оба счётчика в монитор порта; счётчики читаются так, что прерывание не портит число. Без вписанных контактов программа не идёт дальше.

  1. Напиши программу сам.
Как это устроено: Справка по командам
КомандаЧто делает
attachInterrupt(digitalPinToInterrupt(pin), f, RISING)Вызывать функцию f на каждый фронт импульса на контакте pin
volatile long xПеременная, которую меняет прерывание
noInterrupts() / interrupts()Выключить и снова включить прерывания
  1. Измерь импульсы на оборот: прокрути колесо 10 оборотов по метке, потом 20 оборотов. Насколько различаются два результата? Какой точнее и почему?
  2. Найди положение колеса, при котором счёт растёт без движения. Объясни в журнале, что происходит в датчике.
Подсказка: Идея (1 из 2)

Функции прерывания должны быть короткими: только увеличить счётчик.

Подсказка: Счёт без движения (2 из 2)

На краю прорези свет попадает на приёмник «наполовину», и сигнал дрожит около порога.

Должно получиться

Программа выполняет требование 1, импульсы на оборот измерены дважды, объяснение дрожания записано.

2
Занятие 2 · 45 мин
Точное расстояние
4

Миллиметры в импульсы

15 мин

За один оборот колесо проезжает длину окружности: π × диаметр. Диаметр колеса — 45 мм. Чтобы проехать 500 мм, нужно 500 / (π × 45) оборотов, а импульсов — в TICKS_PER_REV раз больше.

  1. Посчитай на бумаге: сколько импульсов на 500 мм, если на оборот 20 импульсов?
  2. Создай скетч LF1_Distance, впиши контакты и своё число импульсов на оборот.
Точное расстояниеLF1_Distance.ino.ino
// Контакты энкодеров: впиши номера из таблицы проводки своего робота.// На Arduino Uno счёт по прерыванию работает только на контактах 2 и 3.const int LEFT_ENCODER_PIN = -1;const int RIGHT_ENCODER_PIN = -1; const int LEFT_DIR_PIN = 7;const int LEFT_SPEED_PIN = 6;const int RIGHT_DIR_PIN = 4;const int RIGHT_SPEED_PIN = 5;const int FORWARD = HIGH; const int TICKS_PER_REV = 0; // импульсов на оборот колеса: измерь и впишиconst int WHEEL_DIAMETER_MM = 45;const int DISTANCE_MM = 500; // сколько проехатьint speed = 100; volatile long leftTicks = 0;volatile long rightTicks = 0; void countLeft() {  leftTicks++;} void countRight() {  rightTicks++;} // Сколько импульсов нужно, чтобы проехать mm миллиметров.long ticksFor(int mm, int ticksPerRev, int diameterMm) {  float revolutions = mm / (PI * diameterMm);  return (long)(revolutions * ticksPerRev + 0.5);} // Сколько импульсов в среднем насчитали оба колеса.long averageTicks() {  noInterrupts();  long sum = leftTicks + rightTicks;  interrupts();  return sum / 2;} void drive(int leftDir, int leftSpeed, int rightDir, int rightSpeed) {  digitalWrite(LEFT_DIR_PIN, leftDir);  digitalWrite(RIGHT_DIR_PIN, rightDir);  analogWrite(LEFT_SPEED_PIN, leftSpeed);  analogWrite(RIGHT_SPEED_PIN, rightSpeed);} void setupEncoders() {  // Без контактов энкодеров программа дальше не пойдёт.  if (LEFT_ENCODER_PIN < 0 || RIGHT_ENCODER_PIN < 0) {    Serial.println("Впиши контакты энкодеров");    while (true) {    }  }  pinMode(LEFT_ENCODER_PIN, INPUT);  pinMode(RIGHT_ENCODER_PIN, INPUT);  attachInterrupt(digitalPinToInterrupt(LEFT_ENCODER_PIN), countLeft, RISING);  attachInterrupt(digitalPinToInterrupt(RIGHT_ENCODER_PIN), countRight, RISING);} void setup() {  Serial.begin(9600);  pinMode(LEFT_DIR_PIN, OUTPUT);  pinMode(LEFT_SPEED_PIN, OUTPUT);  pinMode(RIGHT_DIR_PIN, OUTPUT);  pinMode(RIGHT_SPEED_PIN, OUTPUT);  drive(FORWARD, 0, FORWARD, 0);  setupEncoders();   // Без числа импульсов на оборот робот не поедет.  if (TICKS_PER_REV <= 0) {    Serial.println("Впиши число импульсов на оборот");    while (true) {    }  }  delay(2000); // время убрать руки от робота   long target = ticksFor(DISTANCE_MM, TICKS_PER_REV, WHEEL_DIAMETER_MM);  drive(FORWARD, speed, FORWARD, speed);  while (averageTicks() < target) {  }  drive(FORWARD, 0, FORWARD, 0);  delay(1000); // робот докатывается   Serial.print("Цель, импульсов: ");  Serial.println(target);  Serial.print("Насчитано после остановки: ");  Serial.println(averageTicks());} void loop() {}
  1. Поставь робота на пол у метки старта, включи питание моторов. Робот проедет и остановится. Измерь путь линейкой от метки до той же точки корпуса.
Должно получиться

Ты посчитал: около 71 импульса. Робот проехал примерно 50 см, путь измерен.

  1. Создай скетч LF1_Distance по шаблону, впиши контакты и импульсы на оборот.
  2. Напиши ticksFor и averageTicks по целям.
Шаблон: точное расстояниеLF1_Distance_Gaps.ino.ino
// Шаблон среднего уровня. Напиши код в разделе «Твоя часть» по целям в комментариях. // Контакты энкодеров: впиши номера из таблицы проводки своего робота.// На Arduino Uno счёт по прерыванию работает только на контактах 2 и 3.const int LEFT_ENCODER_PIN = -1;const int RIGHT_ENCODER_PIN = -1; const int LEFT_DIR_PIN = 7;const int LEFT_SPEED_PIN = 6;const int RIGHT_DIR_PIN = 4;const int RIGHT_SPEED_PIN = 5;const int FORWARD = HIGH; const int TICKS_PER_REV = 0; // импульсов на оборот колеса: измерь и впишиconst int WHEEL_DIAMETER_MM = 45;const int DISTANCE_MM = 500; // сколько проехатьint speed = 100; volatile long leftTicks = 0;volatile long rightTicks = 0; // ===== Твоя часть: напиши сам ===== long ticksFor(int mm, int ticksPerRev, int diameterMm) {  // Сколько оборотов колеса нужно на mm миллиметров: один оборот — длина окружности колеса.  // Сколько это импульсов, округлённо до целого. Число PI в Arduino уже есть.  return 0;} long averageTicks() {  // Среднее двух счётчиков. Читай их так, чтобы прерывание не изменило число посередине.  return 0;} // ===== Готовое: энкодеры, моторы и поездка — не меняй ===== void countLeft() {  leftTicks++;} void countRight() {  rightTicks++;} void drive(int leftDir, int leftSpeed, int rightDir, int rightSpeed) {  digitalWrite(LEFT_DIR_PIN, leftDir);  digitalWrite(RIGHT_DIR_PIN, rightDir);  analogWrite(LEFT_SPEED_PIN, leftSpeed);  analogWrite(RIGHT_SPEED_PIN, rightSpeed);} void setupEncoders() {  // Без контактов энкодеров программа дальше не пойдёт.  if (LEFT_ENCODER_PIN < 0 || RIGHT_ENCODER_PIN < 0) {    Serial.println("Впиши контакты энкодеров");    while (true) {    }  }  pinMode(LEFT_ENCODER_PIN, INPUT);  pinMode(RIGHT_ENCODER_PIN, INPUT);  attachInterrupt(digitalPinToInterrupt(LEFT_ENCODER_PIN), countLeft, RISING);  attachInterrupt(digitalPinToInterrupt(RIGHT_ENCODER_PIN), countRight, RISING);} void setup() {  Serial.begin(9600);  pinMode(LEFT_DIR_PIN, OUTPUT);  pinMode(LEFT_SPEED_PIN, OUTPUT);  pinMode(RIGHT_DIR_PIN, OUTPUT);  pinMode(RIGHT_SPEED_PIN, OUTPUT);  drive(FORWARD, 0, FORWARD, 0);  setupEncoders();   // Без числа импульсов на оборот робот не поедет.  if (TICKS_PER_REV <= 0) {    Serial.println("Впиши число импульсов на оборот");    while (true) {    }  }  delay(2000); // время убрать руки от робота   long target = ticksFor(DISTANCE_MM, TICKS_PER_REV, WHEEL_DIAMETER_MM);  drive(FORWARD, speed, FORWARD, speed);  while (averageTicks() < target) {  }  drive(FORWARD, 0, FORWARD, 0);  delay(1000); // робот докатывается   Serial.print("Цель, импульсов: ");  Serial.println(target);  Serial.print("Насчитано после остановки: ");  Serial.println(averageTicks());} void loop() {}
Предскажи

Сколько импульсов вернёт ticksFor(500, 20, 45)? Посчитай до запуска.

  1. Запусти поездку от метки, измерь путь линейкой. Сравни с монитором порта: насколько «насчитано после остановки» больше цели?
Подсказка: Округление до целого

Если прибавить 0.5 и отбросить дробную часть, получится округление до ближайшего целого.

Должно получиться

Прогноз совпал: 71. Робот проехал около 50 см, перекат после остановки виден в мониторе порта.

Требование 2. Функция long ticksFor(int mm, int ticksPerRev, int diameterMm) переводит миллиметры в импульсы с округлением до ближайшего целого и не переполняется на Arduino Uno.

Приёмка. Напиши функцию в тестовом скетче. Когда все строки PASS, перенеси её в свою программу.

Тест функции ticksForLF1_Ticks_Test.ino.ino
// Проверка функции ticksFor. Напиши функцию здесь, загрузи и открой монитор порта.// Когда все строки PASS, перенеси ticksFor в свою программу. // Требование 2: сколько импульсов нужно, чтобы проехать mm миллиметров (округлённо).long ticksFor(int mm, int ticksPerRev, int diameterMm) {  return 0;} int failed = 0; void expect(int mm, int ticksPerRev, int diameterMm, long expected) {  long actual = ticksFor(mm, ticksPerRev, diameterMm);  Serial.print(actual == expected ? "PASS " : "FAIL ");  Serial.print("ticksFor(");  Serial.print(mm);  Serial.print(", ");  Serial.print(ticksPerRev);  Serial.print(", ");  Serial.print(diameterMm);  Serial.print(") = ");  Serial.print(actual);  Serial.print(", ожидаем ");  Serial.println(expected);  if (actual != expected) {    failed++;  }} void setup() {  Serial.begin(9600);   expect(0, 20, 45, 0);  expect(141, 20, 45, 20);   // почти ровно один оборот  expect(500, 20, 45, 71);   // 70.7 округляется вверх  expect(1000, 20, 65, 98);  // другое колесо  expect(1000, 40, 45, 283);  expect(2000, 40, 45, 566); // 2000 × 40 не помещается в int   if (failed == 0) {    Serial.println("Все проверки пройдены");  } else {    Serial.print("Ошибок: ");    Serial.println(failed);  }} void loop() {}

Требование 3. Программа едет вперёд, пока среднее двух счётчиков не достигнет цели, останавливается и печатает цель и насчитанное после остановки. Без вписанного числа импульсов на оборот робот стоит.

  1. Выполни требования 2 и 3, проедь 500 мм от метки и измерь путь.
Подсказка: Идея (1 из 2)

Импульсы = обороты × импульсы на оборот, обороты = путь / длина окружности.

Подсказка: Тест на 2000 мм не проходит (2 из 2)

2000 * 40 — 80 000, больше, чем вмещает int. Считай в float или long до умножения.

Должно получиться

Тест пройден, поездка на 500 мм сделана и измерена.

5

Почему робот проезжает дальше

15 мин

После команды «стоп» робот ещё немного катится — как велосипед, если перестать крутить педали.

  1. Сделай три заезда на 500 мм. Запиши пути в журнал.
  2. Уменьши speed до 70 и сделай ещё три заезда. Стало точнее?
speedЗаезд 1Заезд 2Заезд 3
100
70
Должно получиться

Таблица заполнена. Ты видишь, на какой скорости робот точнее.

  1. Сделай три заезда на 500 мм. Для каждого запиши путь линейкой и «перекат» в импульсах из монитора порта.
  2. Добавь в программу поправку: останавливайся на OVERSHOOT импульсов раньше цели, где OVERSHOOT — средний перекат из трёх заездов. Сделай ещё три заезда.
  3. Ответь в журнале: будет ли эта поправка верна на другой скорости? Почему?
Должно получиться

Шесть заездов в журнале, поправка проверена, ответ про скорость записан.

Требование 4. За SLOW_TICKS импульсов до цели робот сбавляет скорость, чтобы меньше катиться после остановки.

  1. Выполни требование 4 и подбери SLOW_TICKS и пониженную скорость.
  2. Сравни три заезда без замедления и три с замедлением: среднюю ошибку и разброс.
  3. Объясни в журнале, чем замедление лучше поправки «остановиться раньше на N импульсов».
Должно получиться

Требование 4 выполнено, сравнение двух способов и вывод — в журнале.

6

Задания по уровням

12 мин
Основное · Ровно 50 см

Проехать 500 мм с ошибкой не больше 2 см в трёх заездах из трёх.

Средний уровень · Прямо на метр

Проехать 1 м, выравнивая колёса по разнице импульсов: если левое насчитало больше — замедлить левое и ускорить правое, как П-регулятор из урока 3.2. Робот уходит от прямой не больше чем на 3 см.

Продвинутый уровень · Ровно 90 градусов

Повернуть на месте на 90°: колёса крутятся в разные стороны, каждое проходит четверть окружности диаметром в колею робота — 90 мм. Посчитать импульсы, проверить угол угольником. Ошибка — не больше 10°.

Проверь себя

Проверь себя

Выберите ответы для подготовки. Они не сохраняются и не оцениваются на сайте. Пройдите проверку знаний в программе курса или обсудите ответы с преподавателем.

● Основное

1.Что считает энкодер?
2.На диске 20 прорезей, на каждую — один импульс. Сколько импульсов за 3 оборота колеса?
3.Какой путь проходит колесо диаметром 45 мм за один оборот?
4.Почему езда «по времени» неточна?
5.Почему после остановки робот проезжает чуть больше цели?

★ Средний уровень

1.20 импульсов на оборот, колесо 45 мм. Сколько импульсов нужно на 500 мм?
2.Зачем счётчик импульсов объявлен как volatile long?
3.За одно время левое колесо насчитало 300 импульсов, правое — 270. Куда уводит робота?
4.Как точнее измерить число импульсов на оборот?
5.Почему поправка «остановиться на 5 импульсов раньше» перестаёт работать на другой скорости?

★★ Продвинутый уровень

1.Робот поворачивает на месте на 90°, колея 90 мм. Какой путь проходит каждое колесо?
2.Функция верна на 500 мм, но для 2000 мм и 40 импульсов на оборот даёт неверное число. В чём ошибка?
long ticksFor(int mm, int ticksPerRev, int diameterMm) {  int ticks = mm * ticksPerRev / (3.14 * diameterMm);  return ticks;}
3.Колесо стоит на краю прорези, а счётчик растёт. Что происходит?
4.Почему цель сравнивают со средним двух счётчиков, а не с одним левым?
5.Счётчик long читают в loop() без noInterrupts(). Что может случиться?

Отвечай на вопросы своего уровня. Нужно верных ответов: Начальный — 4 из 5; Средний — 4 из 5; Продвинутый — 4 из 5. Ответы проверит преподаватель или программа курса.

Итог

Энкодер превращает оборот колеса в импульсы, а импульсы — в пройденный путь. Робот едет не «сколько успеет», а сколько нужно, при любом заряде батарей.

Как подтвердить результат

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

Словарь урока

энкодерДатчик оборотов: диск с прорезями на оси колеса и датчик, который даёт импульс на каждую прорезь.
импульсКороткий сигнал энкодера: прорезь прошла мимо датчика.
прерываниеArduino откладывает основную программу и сразу выполняет короткую функцию, когда на контакт пришёл импульс.
volatileПометка переменной, которую меняет прерывание: программа каждый раз читает её заново.
длина окружностиПуть колеса за один оборот: π × диаметр.
Тема 7 · Урок Ф.2

Регуляторы: релейный, П, ПД и ПИД на одной трассе

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

2 × 45 мин1–2 ученика на роботаПрактика

Результат урока

Команда сравнила релейный, П-, ПД- и ПИД-регуляторы на одной трассе и одной скорости по средней ошибке, наибольшей ошибке и числу переходов через линию и объяснила результат. Не нужен для итоговой задачи.

Ты научишься

  • Провести честное сравнение: одна трасса, одна скорость, одно изменение за раз, три заезда.
  • Объяснить, чем релейный регулятор отличается от П-регулятора.
  • Объяснить, на что реагируют Д- и И-части регулятора.
  • Сделать вывод о регуляторах по измерениям, а не по впечатлению.

Нужно знать

  • 3.1 Датчики линии и первое следование
  • 3.2 П-регулятор: плавно и быстро

Понадобится

Датчик линии (аналоговый, Trema) × 2USB-кабель для Arduino

Зачем этот урок

В курсе робот ездил по линии с двумя регуляторами: релейным в уроке 3.1 («колесо едет или стоит») и П-регулятором в уроке 3.2 («чем дальше линия, тем сильнее поворот»). Инженеры используют ещё два: ПД и ПИД. Какой из четырёх лучше? Сегодня ответим не «на глаз», а по числам: робот сам измерит, насколько точно проехал, а команда сравнит все четыре регулятора на одной трассе и одной скорости.

Факультатив не нужен для итоговой задачи. Он нужен тем, кто хочет понять, как устроены настоящие автопилоты, и проверить это опытом.

Разминка · вспомни
  1. Что делает колесо в программе урока 3.1, когда под его датчиком линия?
  2. Как в уроке 3.2 считали ошибку и поправку?
  3. Почему при подборе kp меняли одно число за раз?
1
Занятие 1 · 45 мин
Измеряем, а не на глаз
1

Как сравнить честно

8 мин

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

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

Как связать измерение с действием. Проводим заезды с одинаковой базовой командой и на одном поле, сравниваем отклонения и колебания. Больший коэффициент не означает лучший результат. Проверка формулы на числах помогает найти ошибку кода, но не заменяет испытания на трассе; коэффициенты теста не являются настройками вашего робота.

Чтобы сравнить регуляторы, всё остальное должно быть одинаковым: одна дорожка, одно место старта, одна скорость, одни и те же измерения белого и чёрного. Меняется только регулятор. Каждый регулятор едет три раза: один заезд может оказаться случайным.

  1. Выберите трассу сравнения: прямой участок и дуга 250 мм из урока 3.2. Отметьте карандашом место старта.
  2. Перерисуй в журнал таблицу — её вы заполните к концу урока.
РегуляторЗаездСредняя ошибкаНаибольшая ошибкаПереходов через линиюСошёл с линии?
Релейный1, 2, 3
П1, 2, 3
ПД1, 2, 3
ПИД1, 2, 3
Предскажи

Какой регулятор, по-твоему, проедет точнее всех, а какой — хуже всех? Запиши в журнал.

Должно получиться

Трасса и старт отмечены, таблица перерисована, прогноз записан.

Сравнение честное, только если меняется одно: регулятор. Трасса, старт, скорость, измерения и длина заезда — одни и те же. Каждый регулятор — три заезда.

  1. Выберите трассу сравнения: прямой участок и дуга 250 мм из урока 3.2. Отметьте старт.
  2. Запиши в журнал список того, что должно оставаться одинаковым во всех заездах. Найди не меньше пяти пунктов.
  3. Перерисуй таблицу сравнения.
РегуляторЗаездСредняя ошибкаНаибольшая ошибкаПереходов через линиюСошёл с линии?
Релейный1, 2, 3
П1, 2, 3
ПД1, 2, 3
ПИД1, 2, 3
Предскажи

Расставь четыре регулятора от лучшего к худшему по средней ошибке и объясни порядок одной фразой.

Должно получиться

Список одинаковых условий, таблица и прогноз с объяснением — в журнале.

Сначала спланируй опыт, потом пиши код.

  1. Выберите трассу сравнения: прямой участок и дуга 250 мм из урока 3.2. Отметьте старт.
  2. Запиши в журнал план опыта: - что меняется (одно); - что остаётся одинаковым (не меньше пяти пунктов); - что измеряем и почему этих чисел достаточно, чтобы назвать лучший регулятор; - сколько заездов делаем и какое число берём из трёх, если один заезд сильно отличается.
  3. Запиши прогноз: порядок четырёх регуляторов по средней ошибке.
Подсказка: Какое число брать из трёх заездов

Если один заезд из трёх сильно отличается, среднее арифметическое «утянет» его за собой. Есть число, которое на один случайный заезд не реагирует.

Должно получиться

В журнале план опыта и прогноз. Ты знаешь, какое число из трёх заездов пойдёт в таблицу.

2

Робот измеряет сам

15 мин

Программа ведёт робота заданное время RUN_MS, а потом останавливается и сохраняет итоги заезда в память Arduino. Память не стирается, когда выключаешь питание. В начале каждого запуска программа печатает в монитор порта итоги прошлого заезда.

Что печатает роботЧто значит
Средняя ошибкаНасколько в среднем линия уходила от середины. Чем меньше, тем точнее
Наибольшая ошибкаСамый сильный уход от линии за заезд
Переходов через линиюСколько раз линия перескочила с одной стороны на другую: виляние
  1. Создай скетч LF2_Compare, перенеси измерения из журнала и свой kp из урока 3.2.
Четыре регулятора и измерение заездаLF2_Compare.ino.ino
// Наблюдение в MyRobotica включается сборщиком; обычный Arduino работает без библиотеки.#include <math.h>struct Result; // Тип итогов до прототипов, которые добавляет Arduino.#if defined(ROBOT_TELEMETRY_BUILD_ID)#include <RobotTelemetry.h>robot_telemetry::TelemetryStream courseTelemetry;#endif #include <EEPROM.h> const int LEFT_SENSOR_PIN = A4;const int RIGHT_SENSOR_PIN = A5; // Измерения белого и чёрного из журнала (урок 3.1).const int LEFT_WHITE = 0;const int LEFT_BLACK = 0;const int RIGHT_WHITE = 0;const int RIGHT_BLACK = 0; const int LEFT_DIR_PIN = 7;const int LEFT_SPEED_PIN = 6;const int RIGHT_DIR_PIN = 4;const int RIGHT_SPEED_PIN = 5;const int FORWARD = HIGH; // Регулятор заезда: 0 — релейный, 1 — П, 2 — ПД, 3 — ПИД.const int MODE = 1;int baseSpeed = 100; // одна скорость для всех регуляторовfloat kp = 0.5;      // твой kp из урока 3.2float kd = 0;        // Д: реакция на то, как быстро уходит линияfloat ki = 0;        // И: реакция на ошибку, которая копитсяconst long SUM_LIMIT = 3000;       // предел накопленной ошибкиconst unsigned long RUN_MS = 6000; // длина заезда: робот должен остановиться до конца дорожкиconst unsigned long STEP_MS = 10;  // шаг регулятора: 100 раз в секунду int lastError = 0;long errorSum = 0; // Итоги заезда: хранятся в памяти Arduino и после выключения.struct Result {  int marker;  int mode;  int baseSpeed;  float kp;  float kd;  float ki;  long steps;  long sumAbs;  int maxAbs;  int crossings;}; // Переводит показание в шкалу: чёрное — 0, белое — 100.int level(int value, int white, int black) {  long result = (long)(value - black) * 100 / (white - black);  return constrain(result, 0, 100);} void drive(int leftDir, int leftSpeed, int rightDir, int rightSpeed) {  digitalWrite(LEFT_DIR_PIN, leftDir);  digitalWrite(RIGHT_DIR_PIN, rightDir);  analogWrite(LEFT_SPEED_PIN, leftSpeed);  analogWrite(RIGHT_SPEED_PIN, rightSpeed);} // Поправка П-, ПД- или ПИД-регулятора по ошибке этого шага.int controller(int mode, int error) {  float correction = kp * error;  if (mode >= 2) {    correction += kd * (error - lastError);  }  if (mode == 3) {    errorSum = constrain(errorSum + error, -SUM_LIMIT, SUM_LIMIT);    correction += ki * errorSum;  }  lastError = error;  return correction;} // ===== Измерение заезда ===== const int MARKER = 2610; // признак того, что в памяти записан заездResult run;int lastSide = 0;unsigned long startTime;unsigned long nextStep; // Добавляет ошибку шага к итогам заезда.void record(int error) {  run.steps++;  run.sumAbs += abs(error);  if (abs(error) > run.maxAbs) {    run.maxAbs = abs(error);  }  int side = 0;  if (error > 10) {    side = 1;  }  if (error < -10) {    side = -1;  }  if (side != 0 && lastSide != 0 && side != lastSide) {    run.crossings++;  }  if (side != 0) {    lastSide = side;  }} void printResult(Result r) {  const char *names[] = {"релейный", "П", "ПД", "ПИД"};  Serial.print("Регулятор: ");  Serial.print(names[r.mode]);  Serial.print("  скорость=");  Serial.print(r.baseSpeed);  Serial.print("  kp=");  Serial.print(r.kp);  Serial.print("  kd=");  Serial.print(r.kd);  Serial.print("  ki=");  Serial.println(r.ki, 3);  Serial.print("Средняя ошибка: ");  Serial.println(r.steps > 0 ? (float)r.sumAbs / r.steps : 0, 1);  Serial.print("Наибольшая ошибка: ");  Serial.println(r.maxAbs);  Serial.print("Переходов через линию: ");  Serial.println(r.crossings);} // Ждёт, пока робот стоит на старте: оба датчика видят белое 1 секунду подряд.void waitForStart() {  Serial.println("Поставь робота на старт: линия между датчиками");  unsigned long whiteSince = millis();  while (millis() - whiteSince < 1000) {    courseTelemetryPoll();    int leftLevel = level(analogRead(LEFT_SENSOR_PIN), LEFT_WHITE, LEFT_BLACK);    int rightLevel = level(analogRead(RIGHT_SENSOR_PIN), RIGHT_WHITE, RIGHT_BLACK);    if (leftLevel < 70 || rightLevel < 70) {      whiteSince = millis();    }  }} void setup() {  courseTelemetryBegin();  pinMode(LEFT_DIR_PIN, OUTPUT);  pinMode(LEFT_SPEED_PIN, OUTPUT);  pinMode(RIGHT_DIR_PIN, OUTPUT);  pinMode(RIGHT_SPEED_PIN, OUTPUT);  drive(FORWARD, 0, FORWARD, 0);   Result last;  EEPROM.get(0, last);  if (last.marker == MARKER && last.mode >= 0 && last.mode <= 3) {    Serial.println("Прошлый заезд:");    printResult(last);  } else {    Serial.println("Записанных заездов нет");  }   // Без измерений робот не поедет.  if (LEFT_WHITE == LEFT_BLACK || RIGHT_WHITE == RIGHT_BLACK) {    Serial.println("Впиши измерения белого и чёрного");    while (true) {      courseTelemetryPoll();    }  }  waitForStart();  courseWait(2000); // время убрать руки от робота   run = {MARKER, MODE, baseSpeed, kp, kd, ki, 0, 0, 0, 0};  startTime = millis();  nextStep = startTime;} void loop() {  if (millis() - startTime >= RUN_MS) {    drive(FORWARD, 0, FORWARD, 0);    EEPROM.put(0, run);    Serial.println("Заезд окончен:");    printResult(run);    courseTelemetryReport(analogRead(LEFT_SENSOR_PIN), analogRead(RIGHT_SENSOR_PIN), NAN, NAN);    while (true) {      courseTelemetryPoll();    }  }  while (millis() < nextStep) {    courseTelemetryPoll();  }  nextStep += STEP_MS;   int leftRaw = analogRead(LEFT_SENSOR_PIN);  int rightRaw = analogRead(RIGHT_SENSOR_PIN);  int leftLevel = level(leftRaw, LEFT_WHITE, LEFT_BLACK);  int rightLevel = level(rightRaw, RIGHT_WHITE, RIGHT_BLACK);  int error = leftLevel - rightLevel;  record(error);   float observedOutput = NAN; // у релейного алгоритма нет пропорциональной поправки  int leftSpeed;  int rightSpeed;  if (MODE == 0) {    // Релейный, как в уроке 3.1: колесо над линией стоит.    leftSpeed = leftLevel < 50 ? 0 : baseSpeed;    rightSpeed = rightLevel < 50 ? 0 : baseSpeed;  } else {    int correction = controller(MODE, error);    observedOutput = correction;    leftSpeed = constrain(baseSpeed + correction, 0, 255);    rightSpeed = constrain(baseSpeed - correction, 0, 255);  }  drive(FORWARD, leftSpeed, FORWARD, rightSpeed);  courseTelemetryReport(leftRaw, rightRaw, error, observedOutput);} // ===== Готовое: передача наблюдений — не меняй =====void courseTelemetryBegin() {#if defined(ROBOT_TELEMETRY_BUILD_ID)  using namespace robot_telemetry::wire;  courseTelemetry.begin((1UL << Line0) | (1UL << Line1) |                        (1UL << LineError) | (1UL << ControlOutput));#else  Serial.begin(9600);#endif} void courseTelemetryPoll() {#if defined(ROBOT_TELEMETRY_BUILD_ID)  courseTelemetry.update();#endif} // NAN означает «ещё не вычислено», а не нулевую ошибку или поправку.void courseTelemetryReport(int leftRaw, int rightRaw, float error, float output) {#if defined(ROBOT_TELEMETRY_BUILD_ID)  using namespace robot_telemetry::wire;  courseTelemetry.setUnsigned(Line0, leftRaw);  courseTelemetry.setUnsigned(Line1, rightRaw);  if (isnan(error)) courseTelemetry.setMissing(LineError);  else courseTelemetry.setFloat(LineError, error);  if (isnan(output)) courseTelemetry.setMissing(ControlOutput);  else courseTelemetry.setFloat(ControlOutput, output);  courseTelemetry.update();#endif} void courseWait(unsigned long durationMs) {  unsigned long started = millis();  while (millis() - started < durationMs) {    courseTelemetryPoll();    delay(1);  }}
  1. Поставь MODE = 1 (П-регулятор), загрузи программу.
  2. Поставь робота на старт: линия между датчиками. Через секунду программа увидит старт, ещё через две — робот поедет.
  3. Робот должен останавливаться до конца дорожки. Если доезжает до края — уменьши RUN_MS. Подобрал — больше RUN_MS не меняй.
Безопасность

Подключай кабель, держа робота в руке или на подставке. Если поставить его на белый стол, программа примет стол за старт и робот поедет.

  1. Возьми робота в руки, подключи кабель и открой монитор порта: появятся итоги прошлого заезда.
Должно получиться

RUN_MS подобрано, робот останавливается до конца дорожки, в мониторе порта видны итоги заезда.

Как это устроено: Почему робот ждёт старта

Когда открываешь монитор порта, Arduino перезапускается. Если бы робот сразу ехал, он уехал бы со стола и записал бы «заезд» поверх настоящего. Поэтому программа ждёт, пока оба датчика целую секунду видят белое: так бывает, когда робот стоит на старте.

Программа ведёт робота заданное время RUN_MS, останавливает его и сохраняет итоги заезда в память, которая не стирается без питания. При следующем запуске итоги печатаются в монитор порта. Измерение заезда в шаблоне готово. Твоя часть — функция controller: сегодня в ней П-регулятор, на втором занятии добавишь Д и И.

  1. Создай скетч LF2_Compare по шаблону, перенеси измерения и свой kp.
  2. Напиши в controller только П-часть — как в уроке 3.2.
Шаблон: функция регулятораLF2_Compare_Gaps.ino.ino
// Наблюдение в MyRobotica включается сборщиком; обычный Arduino работает без библиотеки.#include <math.h>struct Result; // Тип итогов до прототипов, которые добавляет Arduino.#if defined(ROBOT_TELEMETRY_BUILD_ID)#include <RobotTelemetry.h>robot_telemetry::TelemetryStream courseTelemetry;#endif // Шаблон среднего уровня. Напиши код в разделе «Твоя часть» по целям в комментариях. #include <EEPROM.h> const int LEFT_SENSOR_PIN = A4;const int RIGHT_SENSOR_PIN = A5; // Измерения белого и чёрного из журнала (урок 3.1).const int LEFT_WHITE = 0;const int LEFT_BLACK = 0;const int RIGHT_WHITE = 0;const int RIGHT_BLACK = 0; const int LEFT_DIR_PIN = 7;const int LEFT_SPEED_PIN = 6;const int RIGHT_DIR_PIN = 4;const int RIGHT_SPEED_PIN = 5;const int FORWARD = HIGH; // Регулятор заезда: 0 — релейный, 1 — П, 2 — ПД, 3 — ПИД.const int MODE = 1;int baseSpeed = 100; // одна скорость для всех регуляторовfloat kp = 0.5;      // твой kp из урока 3.2float kd = 0;        // Д: реакция на то, как быстро уходит линияfloat ki = 0;        // И: реакция на ошибку, которая копитсяconst long SUM_LIMIT = 3000;       // предел накопленной ошибкиconst unsigned long RUN_MS = 6000; // длина заезда: робот должен остановиться до конца дорожкиconst unsigned long STEP_MS = 10;  // шаг регулятора: 100 раз в секунду int lastError = 0;long errorSum = 0; // Итоги заезда: хранятся в памяти Arduino и после выключения.struct Result {  int marker;  int mode;  int baseSpeed;  float kp;  float kd;  float ki;  long steps;  long sumAbs;  int maxAbs;  int crossings;}; // ===== Твоя часть: напиши сам ===== // Поправка П-, ПД- или ПИД-регулятора по ошибке этого шага.int controller(int mode, int error) {  // Занятие 1. П: поправка пропорциональна ошибке, коэффициент kp.  // Занятие 2. Д, режимы 2 и 3: прибавь kd, умноженный на то, насколько ошибка изменилась с прошлого шага.  // Занятие 2. И, только режим 3: копи ошибку в errorSum, не выпуская сумму за ±SUM_LIMIT, и прибавь ki × errorSum.  // В конце запомни ошибку этого шага в lastError: она нужна Д на следующем шаге.  return 0;} // ===== Готовое: шкала, моторы, измерение заезда и запуск — не меняй ===== // Переводит показание в шкалу: чёрное — 0, белое — 100.int level(int value, int white, int black) {  long result = (long)(value - black) * 100 / (white - black);  return constrain(result, 0, 100);} void drive(int leftDir, int leftSpeed, int rightDir, int rightSpeed) {  digitalWrite(LEFT_DIR_PIN, leftDir);  digitalWrite(RIGHT_DIR_PIN, rightDir);  analogWrite(LEFT_SPEED_PIN, leftSpeed);  analogWrite(RIGHT_SPEED_PIN, rightSpeed);} const int MARKER = 2610; // признак того, что в памяти записан заездResult run;int lastSide = 0;unsigned long startTime;unsigned long nextStep; // Добавляет ошибку шага к итогам заезда.void record(int error) {  run.steps++;  run.sumAbs += abs(error);  if (abs(error) > run.maxAbs) {    run.maxAbs = abs(error);  }  int side = 0;  if (error > 10) {    side = 1;  }  if (error < -10) {    side = -1;  }  if (side != 0 && lastSide != 0 && side != lastSide) {    run.crossings++;  }  if (side != 0) {    lastSide = side;  }} void printResult(Result r) {  const char *names[] = {"релейный", "П", "ПД", "ПИД"};  Serial.print("Регулятор: ");  Serial.print(names[r.mode]);  Serial.print("  скорость=");  Serial.print(r.baseSpeed);  Serial.print("  kp=");  Serial.print(r.kp);  Serial.print("  kd=");  Serial.print(r.kd);  Serial.print("  ki=");  Serial.println(r.ki, 3);  Serial.print("Средняя ошибка: ");  Serial.println(r.steps > 0 ? (float)r.sumAbs / r.steps : 0, 1);  Serial.print("Наибольшая ошибка: ");  Serial.println(r.maxAbs);  Serial.print("Переходов через линию: ");  Serial.println(r.crossings);} // Ждёт, пока робот стоит на старте: оба датчика видят белое 1 секунду подряд.void waitForStart() {  Serial.println("Поставь робота на старт: линия между датчиками");  unsigned long whiteSince = millis();  while (millis() - whiteSince < 1000) {    courseTelemetryPoll();    int leftLevel = level(analogRead(LEFT_SENSOR_PIN), LEFT_WHITE, LEFT_BLACK);    int rightLevel = level(analogRead(RIGHT_SENSOR_PIN), RIGHT_WHITE, RIGHT_BLACK);    if (leftLevel < 70 || rightLevel < 70) {      whiteSince = millis();    }  }} void setup() {  courseTelemetryBegin();  pinMode(LEFT_DIR_PIN, OUTPUT);  pinMode(LEFT_SPEED_PIN, OUTPUT);  pinMode(RIGHT_DIR_PIN, OUTPUT);  pinMode(RIGHT_SPEED_PIN, OUTPUT);  drive(FORWARD, 0, FORWARD, 0);   Result last;  EEPROM.get(0, last);  if (last.marker == MARKER && last.mode >= 0 && last.mode <= 3) {    Serial.println("Прошлый заезд:");    printResult(last);  } else {    Serial.println("Записанных заездов нет");  }   // Без измерений робот не поедет.  if (LEFT_WHITE == LEFT_BLACK || RIGHT_WHITE == RIGHT_BLACK) {    Serial.println("Впиши измерения белого и чёрного");    while (true) {      courseTelemetryPoll();    }  }  waitForStart();  courseWait(2000); // время убрать руки от робота   run = {MARKER, MODE, baseSpeed, kp, kd, ki, 0, 0, 0, 0};  startTime = millis();  nextStep = startTime;} void loop() {  if (millis() - startTime >= RUN_MS) {    drive(FORWARD, 0, FORWARD, 0);    EEPROM.put(0, run);    Serial.println("Заезд окончен:");    printResult(run);    courseTelemetryReport(analogRead(LEFT_SENSOR_PIN), analogRead(RIGHT_SENSOR_PIN), NAN, NAN);    while (true) {      courseTelemetryPoll();    }  }  while (millis() < nextStep) {    courseTelemetryPoll();  }  nextStep += STEP_MS;   int leftRaw = analogRead(LEFT_SENSOR_PIN);  int rightRaw = analogRead(RIGHT_SENSOR_PIN);  int leftLevel = level(leftRaw, LEFT_WHITE, LEFT_BLACK);  int rightLevel = level(rightRaw, RIGHT_WHITE, RIGHT_BLACK);  int error = leftLevel - rightLevel;  record(error);   float observedOutput = NAN; // у релейного алгоритма нет пропорциональной поправки  int leftSpeed;  int rightSpeed;  if (MODE == 0) {    // Релейный, как в уроке 3.1: колесо над линией стоит.    leftSpeed = leftLevel < 50 ? 0 : baseSpeed;    rightSpeed = rightLevel < 50 ? 0 : baseSpeed;  } else {    int correction = controller(MODE, error);    observedOutput = correction;    leftSpeed = constrain(baseSpeed + correction, 0, 255);    rightSpeed = constrain(baseSpeed - correction, 0, 255);  }  drive(FORWARD, leftSpeed, FORWARD, rightSpeed);  courseTelemetryReport(leftRaw, rightRaw, error, observedOutput);} // ===== Готовое: передача наблюдений — не меняй =====void courseTelemetryBegin() {#if defined(ROBOT_TELEMETRY_BUILD_ID)  using namespace robot_telemetry::wire;  courseTelemetry.begin((1UL << Line0) | (1UL << Line1) |                        (1UL << LineError) | (1UL << ControlOutput));#else  Serial.begin(9600);#endif} void courseTelemetryPoll() {#if defined(ROBOT_TELEMETRY_BUILD_ID)  courseTelemetry.update();#endif} // NAN означает «ещё не вычислено», а не нулевую ошибку или поправку.void courseTelemetryReport(int leftRaw, int rightRaw, float error, float output) {#if defined(ROBOT_TELEMETRY_BUILD_ID)  using namespace robot_telemetry::wire;  courseTelemetry.setUnsigned(Line0, leftRaw);  courseTelemetry.setUnsigned(Line1, rightRaw);  if (isnan(error)) courseTelemetry.setMissing(LineError);  else courseTelemetry.setFloat(LineError, error);  if (isnan(output)) courseTelemetry.setMissing(ControlOutput);  else courseTelemetry.setFloat(ControlOutput, output);  courseTelemetry.update();#endif} void courseWait(unsigned long durationMs) {  unsigned long started = millis();  while (millis() - started < durationMs) {    courseTelemetryPoll();    delay(1);  }}
  1. Поставь MODE = 1, загрузи, поставь робота на старт. Подбери RUN_MS так, чтобы робот останавливался до конца дорожки, и больше его не меняй.
  2. Найди в готовой части функцию record и ответь в журнале: когда она добавляет переход через линию и зачем там порог 10?
Безопасность

Подключай кабель, держа робота в руке или на подставке: на белом столе программа примет стол за старт.

Подсказка: Робот не трогается со старта

Программа ждёт, пока оба датчика целую секунду видят белое (больше 70 по шкале). Посмотри шкалу датчиков на старте.

Должно получиться

controller возвращает П-поправку, RUN_MS подобрано, в журнале — ответ про record.

Каркас ниже берёт на себя измерение: итоги заезда копятся в record(error), сохраняются в память после RUN_MS и печатаются при следующем запуске. Остальное напишешь сам.

Каркас: измерение заездаLF2_Compare_Skeleton.ino.ino
// Наблюдение в MyRobotica включается сборщиком; обычный Arduino работает без библиотеки.#include <math.h>struct Result; // Тип итогов до прототипов, которые добавляет Arduino.#if defined(ROBOT_TELEMETRY_BUILD_ID)#include <RobotTelemetry.h>robot_telemetry::TelemetryStream courseTelemetry;#endif // Каркас для сравнения регуляторов. Измерение заезда и запуск готовы;// регуляторы напиши сам по требованиям из текста урока.#include <EEPROM.h> const int LEFT_SENSOR_PIN = A4;const int RIGHT_SENSOR_PIN = A5; // Измерения белого и чёрного из журнала (урок 3.1).const int LEFT_WHITE = 0;const int LEFT_BLACK = 0;const int RIGHT_WHITE = 0;const int RIGHT_BLACK = 0; const int LEFT_DIR_PIN = 7;const int LEFT_SPEED_PIN = 6;const int RIGHT_DIR_PIN = 4;const int RIGHT_SPEED_PIN = 5;const int FORWARD = HIGH; // Регулятор заезда: 0 — релейный, 1 — П, 2 — ПД, 3 — ПИД.const int MODE = 1;int baseSpeed = 100; // одна скорость для всех регуляторовfloat kp = 0.5;      // твой kp из урока 3.2float kd = 0;        // Д: реакция на то, как быстро уходит линияfloat ki = 0;        // И: реакция на ошибку, которая копитсяconst long SUM_LIMIT = 3000;       // предел накопленной ошибкиconst unsigned long RUN_MS = 6000; // длина заезда: робот должен остановиться до конца дорожкиconst unsigned long STEP_MS = 10;  // шаг регулятора: 100 раз в секунду int lastError = 0;long errorSum = 0; // Итоги заезда: хранятся в памяти Arduino и после выключения.struct Result {  int marker;  int mode;  int baseSpeed;  float kp;  float kd;  float ki;  long steps;  long sumAbs;  int maxAbs;  int crossings;};// Требование 2: здесь будет int controller(int mode, int error). // Перенеси свою функцию level из урока 3.2.int level(int value, int white, int black) {  return 0;} void drive(int leftDir, int leftSpeed, int rightDir, int rightSpeed) {  digitalWrite(LEFT_DIR_PIN, leftDir);  digitalWrite(RIGHT_DIR_PIN, rightDir);  analogWrite(LEFT_SPEED_PIN, leftSpeed);  analogWrite(RIGHT_SPEED_PIN, rightSpeed);} // ===== Измерение заезда ===== const int MARKER = 2610; // признак того, что в памяти записан заездResult run;int lastSide = 0;unsigned long startTime;unsigned long nextStep; // Добавляет ошибку шага к итогам заезда.void record(int error) {  run.steps++;  run.sumAbs += abs(error);  if (abs(error) > run.maxAbs) {    run.maxAbs = abs(error);  }  int side = 0;  if (error > 10) {    side = 1;  }  if (error < -10) {    side = -1;  }  if (side != 0 && lastSide != 0 && side != lastSide) {    run.crossings++;  }  if (side != 0) {    lastSide = side;  }} void printResult(Result r) {  const char *names[] = {"релейный", "П", "ПД", "ПИД"};  Serial.print("Регулятор: ");  Serial.print(names[r.mode]);  Serial.print("  скорость=");  Serial.print(r.baseSpeed);  Serial.print("  kp=");  Serial.print(r.kp);  Serial.print("  kd=");  Serial.print(r.kd);  Serial.print("  ki=");  Serial.println(r.ki, 3);  Serial.print("Средняя ошибка: ");  Serial.println(r.steps > 0 ? (float)r.sumAbs / r.steps : 0, 1);  Serial.print("Наибольшая ошибка: ");  Serial.println(r.maxAbs);  Serial.print("Переходов через линию: ");  Serial.println(r.crossings);} // Ждёт, пока робот стоит на старте: оба датчика видят белое 1 секунду подряд.void waitForStart() {  Serial.println("Поставь робота на старт: линия между датчиками");  unsigned long whiteSince = millis();  while (millis() - whiteSince < 1000) {    courseTelemetryPoll();    int leftLevel = level(analogRead(LEFT_SENSOR_PIN), LEFT_WHITE, LEFT_BLACK);    int rightLevel = level(analogRead(RIGHT_SENSOR_PIN), RIGHT_WHITE, RIGHT_BLACK);    if (leftLevel < 70 || rightLevel < 70) {      whiteSince = millis();    }  }} void setup() {  courseTelemetryBegin();  pinMode(LEFT_DIR_PIN, OUTPUT);  pinMode(LEFT_SPEED_PIN, OUTPUT);  pinMode(RIGHT_DIR_PIN, OUTPUT);  pinMode(RIGHT_SPEED_PIN, OUTPUT);  drive(FORWARD, 0, FORWARD, 0);   Result last;  EEPROM.get(0, last);  if (last.marker == MARKER && last.mode >= 0 && last.mode <= 3) {    Serial.println("Прошлый заезд:");    printResult(last);  } else {    Serial.println("Записанных заездов нет");  }   // Без измерений робот не поедет.  if (LEFT_WHITE == LEFT_BLACK || RIGHT_WHITE == RIGHT_BLACK) {    Serial.println("Впиши измерения белого и чёрного");    while (true) {      courseTelemetryPoll();    }  }  waitForStart();  courseWait(2000); // время убрать руки от робота   run = {MARKER, MODE, baseSpeed, kp, kd, ki, 0, 0, 0, 0};  startTime = millis();  nextStep = startTime;} void loop() {  if (millis() - startTime >= RUN_MS) {    drive(FORWARD, 0, FORWARD, 0);    EEPROM.put(0, run);    Serial.println("Заезд окончен:");    printResult(run);    courseTelemetryReport(analogRead(LEFT_SENSOR_PIN), analogRead(RIGHT_SENSOR_PIN), NAN, NAN);    while (true) {      courseTelemetryPoll();    }  }  while (millis() < nextStep) {    courseTelemetryPoll();  }  nextStep += STEP_MS;   float observedError = NAN;  float observedOutput = NAN;  // Требования 1–3: ошибка, record(error), регулятор по MODE и скорости колёс.  // Для графиков сохрани свои результаты в observedError и observedOutput.  drive(FORWARD, 0, FORWARD, 0);  courseTelemetryReport(analogRead(LEFT_SENSOR_PIN), analogRead(RIGHT_SENSOR_PIN),                        observedError, observedOutput);} // ===== Готовое: передача наблюдений — не меняй =====void courseTelemetryBegin() {#if defined(ROBOT_TELEMETRY_BUILD_ID)  using namespace robot_telemetry::wire;  courseTelemetry.begin((1UL << Line0) | (1UL << Line1) |                        (1UL << LineError) | (1UL << ControlOutput));#else  Serial.begin(9600);#endif} void courseTelemetryPoll() {#if defined(ROBOT_TELEMETRY_BUILD_ID)  courseTelemetry.update();#endif} // NAN означает «ещё не вычислено», а не нулевую ошибку или поправку.void courseTelemetryReport(int leftRaw, int rightRaw, float error, float output) {#if defined(ROBOT_TELEMETRY_BUILD_ID)  using namespace robot_telemetry::wire;  courseTelemetry.setUnsigned(Line0, leftRaw);  courseTelemetry.setUnsigned(Line1, rightRaw);  if (isnan(error)) courseTelemetry.setMissing(LineError);  else courseTelemetry.setFloat(LineError, error);  if (isnan(output)) courseTelemetry.setMissing(ControlOutput);  else courseTelemetry.setFloat(ControlOutput, output);  courseTelemetry.update();#endif} void courseWait(unsigned long durationMs) {  unsigned long started = millis();  while (millis() - started < durationMs) {    courseTelemetryPoll();    delay(1);  }}

Требование 1. Одна константа MODE выбирает регулятор: 0 — релейный, 1 — П, 2 — ПД, 3 — ПИД. Всё остальное во всех режимах одинаково.

  • Релейный — по правилу урока 3.1, но на шкале 0…100: колесо, под датчиком которого линия, стоит, другое едет со скоростью baseSpeed.
  • П, ПД и ПИД получают поправку из функции controller(mode, error) и делят её между колёсами, как в уроке 3.2.
  • Каждый шаг ошибка передаётся в record(error) — во всех четырёх режимах.

Сегодня напиши level, ошибку, релейный режим и П-часть controller. Д и И — на втором занятии.

  1. Перенеси level и ошибку из урока 3.2.
  2. Выполни требование 1 для режимов 0 и 1. Подбери RUN_MS так, чтобы робот останавливался до конца дорожки.
  3. Найди в record порог 10. Запиши в журнал, что изменится в числе переходов, если порог убрать, и почему.
Безопасность

Подключай кабель, держа робота в руке или на подставке: на белом столе программа примет стол за старт.

Подсказка: Робот не трогается со старта

waitForStart ждёт белого по шкале level. Пока level возвращает 0, робот не стартует никогда — это защита каркаса.

Должно получиться

Режимы 0 и 1 работают, RUN_MS подобрано, в журнале — ответ про порог.

3

Релейный против П

20 мин
  1. Поставь MODE = 0 — релейный регулятор из урока 3.1. Сделай три заезда. После каждого подключи кабель и перепиши итоги в таблицу.
  2. Поставь MODE = 1 — П-регулятор. Ещё три заезда.
  3. Сравни строки. Какой регулятор вилял больше? У какого меньше средняя ошибка?
Если не получилось
Что видишьЧто сделать
«Записанных заездов нет» после заездаЗаезд не доехал до конца RUN_MS: питание отключили раньше. Повтори
Робот поехал прямо со столаТы подключил кабель, когда робот стоял на белом. Держи его в руке
Релейный сходит с дугиЭто тоже результат: запиши «да» в «Сошёл с линии?»
Должно получиться

В таблице по три заезда релейного и П-регулятора. Ты можешь сказать, какой точнее и по какому числу это видно.

  1. Сделай по три заезда с MODE = 0 и MODE = 1. Итоги — в таблицу.
  2. Для каждого регулятора выбери из трёх средних ошибок среднюю по величине — она и пойдёт в сравнение.
  3. Ответь в журнале: почему у релейного больше переходов через линию? Объясни через то, какие скорости получают колёса.
Подсказка: Почему средняя по величине

Если один заезд случайно вышел плохим, среднее арифметическое изменится сильно, а средняя по величине — нет.

Должно получиться

Таблица для двух регуляторов заполнена, выбраны средние по величине, объяснение переходов записано.

  1. Проведи опыт по своему плану для режимов 0 и 1.
  2. Сравни не только средние ошибки, но и разброс трёх заездов: насколько повторяется результат каждого регулятора?
  3. Запиши в журнал объяснение через устройство регулятора: почему у релейного больше переходов и наибольшая ошибка, хотя оба видят одни и те же датчики.
Должно получиться

Данные двух регуляторов, разброс и объяснение — в журнале. Прогноз из первого шага проверен для первой пары.

2
Занятие 2 · 45 мин
ПД, ПИД и общий итог
4

ПД: тормозим поворот заранее

14 мин

П-регулятор поворачивает, пока линия не вернулась на середину, и часто проскакивает её: робот виляет. Д-часть смотрит, как быстро меняется ошибка. Если линия быстро возвращается к середине, Д-часть заранее ослабляет поворот — как велосипедист, который начинает выпрямлять руль ещё до того, как выехал на линию.

Функция регулятораLF2_Compare.ino.ino
// Наблюдение в MyRobotica включается сборщиком; обычный Arduino работает без библиотеки.#include <math.h>struct Result; // Тип итогов до прототипов, которые добавляет Arduino.#if defined(ROBOT_TELEMETRY_BUILD_ID)#include <RobotTelemetry.h>robot_telemetry::TelemetryStream courseTelemetry;#endif #include <EEPROM.h> const int LEFT_SENSOR_PIN = A4;const int RIGHT_SENSOR_PIN = A5; // Измерения белого и чёрного из журнала (урок 3.1).const int LEFT_WHITE = 0;const int LEFT_BLACK = 0;const int RIGHT_WHITE = 0;const int RIGHT_BLACK = 0; const int LEFT_DIR_PIN = 7;const int LEFT_SPEED_PIN = 6;const int RIGHT_DIR_PIN = 4;const int RIGHT_SPEED_PIN = 5;const int FORWARD = HIGH; // Регулятор заезда: 0 — релейный, 1 — П, 2 — ПД, 3 — ПИД.const int MODE = 1;int baseSpeed = 100; // одна скорость для всех регуляторовfloat kp = 0.5;      // твой kp из урока 3.2float kd = 0;        // Д: реакция на то, как быстро уходит линияfloat ki = 0;        // И: реакция на ошибку, которая копитсяconst long SUM_LIMIT = 3000;       // предел накопленной ошибкиconst unsigned long RUN_MS = 6000; // длина заезда: робот должен остановиться до конца дорожкиconst unsigned long STEP_MS = 10;  // шаг регулятора: 100 раз в секунду int lastError = 0;long errorSum = 0; // Итоги заезда: хранятся в памяти Arduino и после выключения.struct Result {  int marker;  int mode;  int baseSpeed;  float kp;  float kd;  float ki;  long steps;  long sumAbs;  int maxAbs;  int crossings;}; // Переводит показание в шкалу: чёрное — 0, белое — 100.int level(int value, int white, int black) {  long result = (long)(value - black) * 100 / (white - black);  return constrain(result, 0, 100);} void drive(int leftDir, int leftSpeed, int rightDir, int rightSpeed) {  digitalWrite(LEFT_DIR_PIN, leftDir);  digitalWrite(RIGHT_DIR_PIN, rightDir);  analogWrite(LEFT_SPEED_PIN, leftSpeed);  analogWrite(RIGHT_SPEED_PIN, rightSpeed);} // Поправка П-, ПД- или ПИД-регулятора по ошибке этого шага.int controller(int mode, int error) {  float correction = kp * error;  if (mode >= 2) {    correction += kd * (error - lastError);  }  if (mode == 3) {    errorSum = constrain(errorSum + error, -SUM_LIMIT, SUM_LIMIT);    correction += ki * errorSum;  }  lastError = error;  return correction;} // ===== Измерение заезда ===== const int MARKER = 2610; // признак того, что в памяти записан заездResult run;int lastSide = 0;unsigned long startTime;unsigned long nextStep; // Добавляет ошибку шага к итогам заезда.void record(int error) {  run.steps++;  run.sumAbs += abs(error);  if (abs(error) > run.maxAbs) {    run.maxAbs = abs(error);  }  int side = 0;  if (error > 10) {    side = 1;  }  if (error < -10) {    side = -1;  }  if (side != 0 && lastSide != 0 && side != lastSide) {    run.crossings++;  }  if (side != 0) {    lastSide = side;  }} void printResult(Result r) {  const char *names[] = {"релейный", "П", "ПД", "ПИД"};  Serial.print("Регулятор: ");  Serial.print(names[r.mode]);  Serial.print("  скорость=");  Serial.print(r.baseSpeed);  Serial.print("  kp=");  Serial.print(r.kp);  Serial.print("  kd=");  Serial.print(r.kd);  Serial.print("  ki=");  Serial.println(r.ki, 3);  Serial.print("Средняя ошибка: ");  Serial.println(r.steps > 0 ? (float)r.sumAbs / r.steps : 0, 1);  Serial.print("Наибольшая ошибка: ");  Serial.println(r.maxAbs);  Serial.print("Переходов через линию: ");  Serial.println(r.crossings);} // Ждёт, пока робот стоит на старте: оба датчика видят белое 1 секунду подряд.void waitForStart() {  Serial.println("Поставь робота на старт: линия между датчиками");  unsigned long whiteSince = millis();  while (millis() - whiteSince < 1000) {    courseTelemetryPoll();    int leftLevel = level(analogRead(LEFT_SENSOR_PIN), LEFT_WHITE, LEFT_BLACK);    int rightLevel = level(analogRead(RIGHT_SENSOR_PIN), RIGHT_WHITE, RIGHT_BLACK);    if (leftLevel < 70 || rightLevel < 70) {      whiteSince = millis();    }  }} void setup() {  courseTelemetryBegin();  pinMode(LEFT_DIR_PIN, OUTPUT);  pinMode(LEFT_SPEED_PIN, OUTPUT);  pinMode(RIGHT_DIR_PIN, OUTPUT);  pinMode(RIGHT_SPEED_PIN, OUTPUT);  drive(FORWARD, 0, FORWARD, 0);   Result last;  EEPROM.get(0, last);  if (last.marker == MARKER && last.mode >= 0 && last.mode <= 3) {    Serial.println("Прошлый заезд:");    printResult(last);  } else {    Serial.println("Записанных заездов нет");  }   // Без измерений робот не поедет.  if (LEFT_WHITE == LEFT_BLACK || RIGHT_WHITE == RIGHT_BLACK) {    Serial.println("Впиши измерения белого и чёрного");    while (true) {      courseTelemetryPoll();    }  }  waitForStart();  courseWait(2000); // время убрать руки от робота   run = {MARKER, MODE, baseSpeed, kp, kd, ki, 0, 0, 0, 0};  startTime = millis();  nextStep = startTime;} void loop() {  if (millis() - startTime >= RUN_MS) {    drive(FORWARD, 0, FORWARD, 0);    EEPROM.put(0, run);    Serial.println("Заезд окончен:");    printResult(run);    courseTelemetryReport(analogRead(LEFT_SENSOR_PIN), analogRead(RIGHT_SENSOR_PIN), NAN, NAN);    while (true) {      courseTelemetryPoll();    }  }  while (millis() < nextStep) {    courseTelemetryPoll();  }  nextStep += STEP_MS;   int leftRaw = analogRead(LEFT_SENSOR_PIN);  int rightRaw = analogRead(RIGHT_SENSOR_PIN);  int leftLevel = level(leftRaw, LEFT_WHITE, LEFT_BLACK);  int rightLevel = level(rightRaw, RIGHT_WHITE, RIGHT_BLACK);  int error = leftLevel - rightLevel;  record(error);   float observedOutput = NAN; // у релейного алгоритма нет пропорциональной поправки  int leftSpeed;  int rightSpeed;  if (MODE == 0) {    // Релейный, как в уроке 3.1: колесо над линией стоит.    leftSpeed = leftLevel < 50 ? 0 : baseSpeed;    rightSpeed = rightLevel < 50 ? 0 : baseSpeed;  } else {    int correction = controller(MODE, error);    observedOutput = correction;    leftSpeed = constrain(baseSpeed + correction, 0, 255);    rightSpeed = constrain(baseSpeed - correction, 0, 255);  }  drive(FORWARD, leftSpeed, FORWARD, rightSpeed);  courseTelemetryReport(leftRaw, rightRaw, error, observedOutput);} // ===== Готовое: передача наблюдений — не меняй =====void courseTelemetryBegin() {#if defined(ROBOT_TELEMETRY_BUILD_ID)  using namespace robot_telemetry::wire;  courseTelemetry.begin((1UL << Line0) | (1UL << Line1) |                        (1UL << LineError) | (1UL << ControlOutput));#else  Serial.begin(9600);#endif} void courseTelemetryPoll() {#if defined(ROBOT_TELEMETRY_BUILD_ID)  courseTelemetry.update();#endif} // NAN означает «ещё не вычислено», а не нулевую ошибку или поправку.void courseTelemetryReport(int leftRaw, int rightRaw, float error, float output) {#if defined(ROBOT_TELEMETRY_BUILD_ID)  using namespace robot_telemetry::wire;  courseTelemetry.setUnsigned(Line0, leftRaw);  courseTelemetry.setUnsigned(Line1, rightRaw);  if (isnan(error)) courseTelemetry.setMissing(LineError);  else courseTelemetry.setFloat(LineError, error);  if (isnan(output)) courseTelemetry.setMissing(ControlOutput);  else courseTelemetry.setFloat(ControlOutput, output);  courseTelemetry.update();#endif} void courseWait(unsigned long durationMs) {  unsigned long started = millis();  while (millis() - started < durationMs) {    courseTelemetryPoll();    delay(1);  }}

Поправка ПД: kp × ошибка + kd × (ошибка сейчас − ошибка на прошлом шаге).

  1. Посчитай на бумаге: kp = 0.5, kd = 4, на прошлом шаге ошибка была 10, сейчас 25.
  2. Поставь MODE = 2 и попробуй kd = 5, 10 и 20 — по одному заезду на дорожке. Выбери kd, при котором робот меньше всего виляет.
  3. С выбранным kd сделай три заезда и запиши итоги в таблицу.
Должно получиться

Ты посчитал поправку: 12,5 + 60 = 72,5, программа отбросит дробную часть — 72. Выбран kd, в таблице три заезда ПД.

Как это устроено: Почему kd такой большой

Регулятор считает 100 раз в секунду. За одну сотую секунды ошибка меняется всего на несколько единиц, поэтому множитель kd нужен больше, чем kp.

П-регулятор часто проскакивает линию: поправка не ослабевает, пока ошибка не станет нулём. Д-часть реагирует на то, как быстро меняется ошибка, и гасит поворот заранее.

  1. Допиши Д-часть в controller по цели в шаблоне.
Предскажи

kp = 0.5, kd = 4, на прошлом шаге ошибка 10, сейчас 25. Какую поправку вернёт controller для MODE = 2? Посчитай до запуска.

  1. Поставь MODE = 2. Подбери kd: начни с 2.5 и удваивай, по одному заезду. Запиши, при каком kd робот начал дёргаться на прямой, и возьми предыдущее значение.
  2. С выбранным kd — три заезда в таблицу.
Подсказка: Где взять прошлую ошибку

Она уже лежит в lastError, если ты запоминаешь её в конце функции.

Подсказка: Робот дёргается даже при маленьком kd

Проверь знак: Д-часть прибавляется, а изменение ошибки — «сейчас минус раньше».

Должно получиться

Прогноз совпал: 72. Д-часть написана, kd подобран удвоением, три заезда записаны.

Требование 2. Функция int controller(int mode, int error):

  • режим 1 — только П-часть;
  • режимы 2 и 3 — плюс Д-часть: kd, умноженный на изменение ошибки с прошлого шага;
  • режим 3 — плюс И-часть: ошибки копятся в errorSum, сумма не выходит за ±SUM_LIMIT, поправка ki × errorSum; в других режимах сумма не копится;
  • прошлая ошибка обновляется на каждом шаге во всех режимах.

Приёмка. Напиши controller в тестовом скетче, загрузи и открой монитор порта. Когда все строки PASS, перенеси функцию в свою программу. Сегодня тест проверяет и И-часть — значит, её тоже нужно написать.

Тест функции controllerLF2_Controller_Test.ino.ino
// Проверка функции controller. Напиши функцию здесь, загрузи и открой монитор порта.// Когда все строки PASS, перенеси controller в свою программу.// Коэффициенты в тесте свои, удобные для счёта; на дорожке будут твои. float kp = 0.5;float kd = 2.0;float ki = 0.25;const long SUM_LIMIT = 200; int lastError = 0;long errorSum = 0; // Требование 2: режим 1 — П, 2 — ПД, 3 — ПИД.int controller(int mode, int error) {  return 0;} int failed = 0; void reset() {  lastError = 0;  errorSum = 0;} void expect(const char *name, int actual, int expected) {  Serial.print(actual == expected ? "PASS " : "FAIL ");  Serial.print(name);  Serial.print(": получили ");  Serial.print(actual);  Serial.print(", ожидаем ");  Serial.println(expected);  if (actual != expected) {    failed++;  }} void setup() {  Serial.begin(9600);   reset();  expect("П, ошибка 40", controller(1, 40), 20);  expect("П, ошибка -40", controller(1, -40), -20);   reset();  expect("ПД, первый шаг 10", controller(2, 10), 25);  expect("ПД, ошибка выросла до 20", controller(2, 20), 30);  expect("ПД, ошибка осталась 20", controller(2, 20), 10);   reset();  expect("ПИД, первый шаг 10", controller(3, 10), 27);  expect("ПИД, снова 10", controller(3, 10), 10);   // Накопленная ошибка не выходит за SUM_LIMIT.  reset();  int last = 0;  for (int i = 0; i < 5; i++) {    last = controller(3, 100);  }  expect("ПИД, пять шагов по 100", last, 100);  reset();  for (int i = 0; i < 5; i++) {    last = controller(3, -100);  }  expect("ПИД, пять шагов по -100", last, -100);   // В режиме П ошибка не копится, но прошлая ошибка запоминается.  reset();  controller(1, 100);  controller(1, 100);  controller(1, 100);  expect("ПИД после трёх шагов П", controller(3, 100), 75);   if (failed == 0) {    Serial.println("Все проверки пройдены");  } else {    Serial.print("Ошибок: ");    Serial.println(failed);  }} void loop() {}

Затем подбери kd методом: начни с 2.5 и удваивай до первого дёргания на прямой, возьми предыдущее значение. Три заезда — в таблицу.

Подсказка: Идея (1 из 3)

Скорость ухода линии — разница между ошибкой сейчас и ошибкой на прошлом шаге.

Подсказка: План (2 из 3)
  1. П-часть.
  2. Если режим 2 или 3 — прибавить Д-часть.
  3. Если режим 3 — обновить сумму, ограничить её, прибавить И-часть.
  4. Запомнить ошибку.
Подсказка: Фрагмент кода (3 из 3)

errorSum = constrain(errorSum + error, -SUM_LIMIT, SUM_LIMIT); — почему тест «пять шагов по 100» без этой строки даёт 175?

Должно получиться

Тест: «Все проверки пройдены». kd подобран методом, три заезда ПД записаны.

5

ПИД: что даёт накопленная ошибка

11 мин

И-часть складывает ошибки шаг за шагом. Если линия долго остаётся с одной стороны — например, на длинной дуге, — сумма растёт, и робот поворачивает всё сильнее. Чтобы сумма не росла бесконечно, её ограничивает SUM_LIMIT.

  1. Оставь свой kd, поставь MODE = 3 и попробуй ki = 0.005, 0.01 и 0.02 — по одному заезду.
  2. Выбери лучший ki и сделай три заезда в таблицу.
Предскажи

Будет ли ПИД точнее ПД на нашей трассе? Запиши прогноз до заездов.

Должно получиться

В таблице три заезда ПИД. Прогноз проверен.

И-часть копит ошибку. Пока линия долго с одной стороны, сумма растёт и усиливает поворот. Опасность — сумма продолжает тянуть в ту же сторону и после дуги.

  1. Допиши И-часть в controller по цели в шаблоне.
  2. Поставь MODE = 3, подбери ki: 0.005, 0.01, 0.02. Три заезда с лучшим — в таблицу.
  3. Временно поставь SUM_LIMIT = 30000 и проедь один раз. Что изменилось после дуги? Верни 3000.
Подсказка: Как не выпустить сумму за предел

Та же команда, что держит скорость колёс в 0…255, подойдёт и для суммы.

Должно получиться

И-часть написана, ki подобран, три заезда записаны. Ты видел, что делает робот без предела суммы.

  1. Поставь MODE = 3, подбери ki методом: от 0.005, удваивая. Три заезда с лучшим — в таблицу.
  2. Опыт с пределом: один заезд с SUM_LIMIT = 30000. Опиши, что происходит после дуги, и объясни через значение errorSum.
  3. Ответь в журнале: в каких условиях И-часть даёт выигрыш и есть ли они на нашей трассе?
Подсказка: Когда И-часть полезна

Подумай о долгой ошибке одного знака: длинная дуга одного радиуса или робот, у которого одно колесо всегда крутится медленнее другого.

Должно получиться

ki подобран, опыт с пределом описан, условия пользы И-части записаны.

6

Сравнение всех четырёх

12 мин
  1. Выпиши для каждого регулятора среднюю ошибку из трёх заездов — среднюю по величине.
  2. Расставь регуляторы от лучшего к худшему.
МестоРегуляторСредняя ошибкаПереходов через линию
1
2
3
4
  1. Сравни с прогнозом из начала урока. Что совпало, что нет?
Должно получиться

Четыре регулятора расставлены по местам, прогноз проверен.

  1. Составь итоговую таблицу: регулятор, средняя по величине средняя ошибка, наибольшая ошибка, переходы, сходы.
  2. Назови лучший регулятор для этой трассы и скорости и обоснуй двумя числами.
  3. Ответь: оправдала ли И-часть своё усложнение? Почему?
  4. Какой регулятор ты взял бы для итоговой задачи и почему?
Должно получиться

Итоговая таблица, обоснованный вывод и ответы — в журнале.

  1. Составь итоговую таблицу всех четырёх регуляторов с разбросом трёх заездов.
  2. Напиши вывод из трёх частей: что показали числа, чем это объясняется в устройстве регуляторов, что можно было сделать нечестно и как ты это исключил.
  3. Сравни результат с прогнозом и планом опыта из первого шага.
Должно получиться

Итоговая таблица и вывод из трёх частей — в журнале.

7

Задания по уровням

8 мин
Основное · Все четыре регулятора

Заполнить таблицу сравнения всех четырёх регуляторов на одной скорости, по три заезда, и назвать лучший по средней ошибке.

Средний уровень · ПД лучше П

Подобрать kd, при котором ПД проходит трассу с меньшей средней ошибкой, чем П, и объяснить по числам почему.

Продвинутый уровень · Предельная скорость

Найти для каждого из четырёх регуляторов наибольшую baseSpeed, на которой он проходит трассу три раза из трёх: повышать шагами по 20. Составить рейтинг по скорости и сравнить его с рейтингом по средней ошибке.

Проверь себя

Проверь себя

Выберите ответы для подготовки. Они не сохраняются и не оцениваются на сайте. Пройдите проверку знаний в программе курса или обсудите ответы с преподавателем.

● Основное

1.Почему все регуляторы сравнивают на одной трассе и одной скорости?
2.Что делает релейный регулятор из урока 3.1 с колесом, под датчиком которого линия?
3.Чем П-регулятор отличается от релейного?
4.Что показывает «средняя ошибка» в итогах заезда?
5.На что реагирует Д-часть регулятора?

★ Средний уровень

1.kp = 0.5, kd = 4. На прошлом шаге ошибка была 10, сейчас 25. Какую поправку вернёт ПД-регулятор (дробная часть отбрасывается)?
2.Почему ПД меньше виляет на выходе из дуги, чем П?
3.Зачем ограничивать сумму ошибок errorSum значением SUM_LIMIT?
4.Три заезда ПИД дали среднюю ошибку 8, 9 и 21. Какое число взять в сравнение?
5.Зачем регулятор считает с постоянным шагом STEP_MS?

★★ Продвинутый уровень

1.С этой функцией ПД ведёт себя как П с увеличенным kp. Почему?
int controller(int mode, int error) {  float correction = kp * error;  if (mode >= 2) {    correction += kd * (error - lastError);  }  return correction;}
2.В ПИД убрали ограничение суммы. После длинной дуги влево робот на прямой долго уходит влево за линию. Почему?
3.Средняя ошибка и переходы: релейный 30 и 40, П 14 и 6, ПД 9 и 2, ПИД 10 и 3. Какой вывод честный?
4.Почему на трассе из дуг в разные стороны И-часть почти не помогает?
5.В программе написали correction -= kd * (error - lastError). Что будет с ПД?

Отвечай на вопросы своего уровня. Нужно верных ответов: Начальный — 4 из 5; Средний — 4 из 5; Продвинутый — 4 из 5. Ответы проверит преподаватель или программа курса.

Итог

Релейный регулятор знает только «да или нет», П-регулятор — «насколько», Д-часть — «как быстро», И-часть — «как долго». Больше частей не значит лучше: решают измерения на твоей трассе и скорости.

Как подтвердить результат

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

Словарь урока

релейный регуляторПравило «всё или ничего»: колесо либо едет, либо стоит, как в уроке 3.1.
П-частьПоправка, пропорциональная ошибке: kp × ошибка. Регулятор из урока 3.2.
Д-частьПоправка на то, как быстро меняется ошибка: kd × (ошибка сейчас − ошибка на прошлом шаге).
И-частьПоправка на ошибку, накопленную за время: ki × сумма ошибок.
средняя ошибкаНасколько в среднем линия уходила от середины за заезд, без учёта стороны. Чем меньше, тем точнее робот едет.
шаг регулятораПостоянный промежуток между расчётами поправки, здесь 10 мс.
Печатный лист команды

Бортовой журнал робота

Записывайте сюда всё, что измерили и решили. Эти значения понадобятся в следующих уроках.

Имя робота
Команда
КомплектArduino Uno + шилд моторов L298P

Выполненные уровни заданий

Отмечай, какие задания получились. По этим отметкам в уроке 6.1 выбирается уровень итоговой задачи.

Урок● Основное★★★
2.2 Повороты
3.1 Датчики линии
3.2 П-регулятор
4.1 Перекрёстки
4.2 Маршрут
5.1 Ультразвук
5.2 Захват

Урок 1.1 · Сборка

Дата
Что получилось
Что было трудно

Урок 2.1 · Моторы

Скорость speed, при которой оба колеса уверенно трогаютсяКуда уводит робота при движении прямо

Урок 2.2 · Повороты

ПоворотВремя, мсПроверено три раза
90°
180°

Урок 3.1 · Датчики линии

Догадка в начале урока: как робот видит линию?

________________________________________________

ДатчикПодключён кБелоеЧёрное
ЛевыйA4
ПравыйA5
Что видят датчикиЛевое колесоПравое колесоКуда едет робот
Оба белоевперёдвперёдпрямо
Левый — линия
Правый — линия
Оба — линия
Скорость speedПрошёл изгиб?Что заметили

Урок 3.2 · П-регулятор

kpbaseSpeedЧто видно

Выбрано: kp = ____ , baseSpeed = ____

Урок 4.1–4.2 · Перекрёстки и маршрут

Порог перекрёсткаcenterMsleaveMs

Урок 5.1–5.2 · Ультразвук и захват

TRIGECHOSTOP_CMСервоприводУгол «открыто»Угол «закрыто»

Урок 6.1 · План итоговой задачи

Уровень поля: начальный / средний / продвинутый. Карточка (для продвинутого): ____

УчастокЧто делает роботКакая функция

Где скорее всего будет ошибка и как я это проверю:

________________________________________________

Урок 6.2 · Отладка и зачёт

НаблюдаюПричинаОдно изменениеПроверка
ПопыткаБаллыЧто исправили после неё
1
2

Что у робота работает лучше всего и что я улучшу дальше:

________________________________________________