Робот-курьер
Собираем колёсного робота и учим его ездить по линии, находить предметы и доставлять их.
Собираем робота и оживляем плату
Собери робота и проверь каждый установленный узел диагностическим скетчем.
Результат урока
Собранный робот: оба мотора проверены в обе стороны и останавливаются, оба датчика линии реагируют на белое и чёрное. Результаты проверки записаны в журнал.
Ты научишься
- Собрать ходовую часть робота по инструкции и проверить каждый узел.
- Различать перед, зад, левую и правую стороны робота.
- Загрузить программу в Arduino и объяснить, что делают setup() и loop().
- Проверить оба мотора, их остановку и оба датчика линии; отличать команду программы от наблюдаемого результата.
Нужно знать
Ничего — это первый урок.
Понадобится
Зачем этот урок
Весь курс ты будешь работать с одним роботом — своим. Сегодня соберёшь его ходовую часть, установишь «мозг» робота — плату Arduino — и напишешь первую программу. К концу урока ты проверишь каждый установленный узел и запишешь, что он действительно работает.

Пока идёт сборка, батарейный отсек отключён, а USB-кабель не подключён к компьютеру. Батарейный отсек к шилду заранее подключает взрослый. Моторы ты включаешь и выключаешь только выключателем.
Распределите роли
3 минЧто должен делать робот. Сначала роботу нужно правильно выполнять отдельные команды: вращать каждое колесо, останавливаться и сообщать показания датчиков. До самостоятельной поездки проверим каждый узел по очереди, чтобы искать причину ошибки в одном месте.
Что используем и как это работает. Arduino Uno выполняет программу. Драйвер L298P по её сигналам управляет питанием моторов: вывод DIR задаёт направление, PWM — долю времени подачи питания. Мотор превращает электрическую энергию во вращение, а колесо — в движение. Два датчика отражения снизу помогают различать светлую поверхность и чёрную линию.
Как связать измерение с действием. Команда и результат — разные вещи. Надпись в мониторе означает, что программа дошла до печати; вращение проверяем глазами. На подставке проверим оба направления каждого колеса и остановку, затем сравним показания датчиков над белой и чёрной карточками. Так узнаем, как именно собран наш робот.
Вы работаете вдвоём. Один — Пилот: держит детали и отвёртку. Второй — Штурман: читает шаг вслух, подаёт детали и проверяет результат по картинке.
- Решите, кто начинает Пилотом.
- После каждой детали, которая закреплена винтами, меняйтесь ролями.
Каждый знает свою роль. Штурман держит инструкцию, Пилот — инструмент.
Разложи детали
7 минПрежде чем собирать, найди каждую деталь. Так ты не перепутаешь похожие детали и сразу увидишь, если чего-то не хватает.







- Разложи печатные детали в ряд, как на карточках.
- Найди два кронштейна моторов. Они похожи, но зеркальные: положи их рядом и посмотри — полочки смотрят в разные стороны.
- Рядом положи покупные детали: Arduino, шилд моторов, два мотора, шаровую опору, два датчика линии, батарейный отсек и пакеты с крепежом.
Все детали лежат перед тобой. Ты можешь показать оба зеркальных кронштейна.
Подсказка: Не хватает детали
Не заменяй её похожей. Сверь пакет с карточками ещё раз; если детали нет — сообщи взрослому.
Потренируйся на одном винте
4 минПочти все детали робота держатся на винтах и гайках. Соберём одно соединение на столе.
- Возьми винт, две шайбы и гайку из пакета «Тренировка».
- Надень шайбу на винт, проведи винт через две детали, надень вторую шайбу и накрути гайку пальцами.
- Придержи гайку пальцем и затяни винт отвёрткой. Остановись, как только детали перестали шевелиться.

Порядок соединения: винт → шайба → деталь → деталь → шайба → гайка. Детали не болтаются.
Как это устроено: Почему нельзя затягивать изо всех сил
Детали напечатаны из пластика. Если перетянуть винт, пластик продавится или треснет. Признак перетяжки — деталь прогибается или белеет вокруг винта.
Договоритесь, где перед
3 минЧтобы «левое колесо» всегда значило одно и то же, отметим перед робота.
- Положи основу плоско: маленькое квадратное окно — дальше от тебя, длинное прямоугольное — ближе.
- Дальняя сторона — перед. Наклей на неё бумажную стрелку «вперёд».
- Лево и право определяй как водитель, который сидит в роботе и смотрит по стрелке.
На основе есть стрелка «вперёд». Вы оба показываете левую сторону робота одинаково.
Прикрепи кронштейны моторов
8 минКронштейны держат моторы снизу основы.

- Переверни основу так, чтобы стрелка осталась видна сбоку.
- Приложи кронштейны к нижней стороне, как на рисунке. Большие круглые отверстия смотрят наружу — туда будут выходить оси колёс.
- Вставь винты сверху, снизу надень шайбы и гайки. Сначала наживи все винты пальцами, потом затяни.
Два кронштейна стоят друг напротив друга, большие отверстия — снаружи. Кронштейны не качаются.
Установи моторы
8 минОсь мотора — вращающийся стержень, на который надевается колесо. Она должна выйти наружу через отверстие кронштейна.

- Приложи первый мотор к кронштейну изнутри. Ось проходит через большое отверстие наружу.
- Закрепи мотор винтами из пакета «Моторы». Держи мотор рукой, пока затягиваешь.
- Так же установи второй мотор.
- Наклей на провода метки: Л — левый мотор, П — правый. Сверься со стрелкой «вперёд».
Оба мотора держатся крепко, оси смотрят влево и вправо, провода подписаны.
Подсказка: Мотор не встаёт в кронштейн
Не дави и не подпиливай. Проверь, тот ли это кронштейн: может быть, ты взял зеркальный для другой стороны.
Надень колёса
5 мин- Совмести отверстие колеса с осью мотора. Форма отверстия должна совпасть с формой оси.
- Надень колесо, придерживая мотор за корпус. Не бей по колесу.
- Посмотри сбоку: колесо не должно касаться кронштейна или основы.

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

- Закрепи шаровую опору снизу у задней стороны основы. Стойки из пакета «Опора» подобраны по высоте.
- Поставь робота на стол. Он должен стоять ровно на двух колёсах и опоре.
- Аккуратно покачай робота. Ничего не должно болтаться.
Робот стоит устойчиво и ровно. Ходовая часть готова.
Установи Arduino
6 минArduino — «мозг» робота. Плата лежит на держателе, чтобы её контакты снизу не касались винтов.

- Закрепи держатель Arduino в центре основы стойками вверх.
- Положи плату на стойки так, чтобы разъём USB был у края и до него можно было дотянуться.
- Закрепи плату винтами. Затягивай их по очереди, понемногу.
Плата лежит ровно и не касается винтов основы. Разъём USB свободен.
Подготовь компьютер
5 минПрограммы для робота пишут в MyRobotica. В ней есть уроки и редактор кода.
- Подключи USB-кабель к Arduino и к компьютеру. Батарейный отсек моторов остаётся отключённым.
- Открой MyRobotica, курс «Робот-курьер» и урок 1.1. Открой учебный проект урока.
- В правой части кнопки Загрузить на робота выбери своего настоящего робота. В Подробнее о связи проверь плату Arduino Uno и её порт.
- В редакторе оставь пустые функции
setup()иloop()и сохрани программу.
На плате горит индикатор питания ON. В MyRobotica открыт учебный проект, выбран настоящий робот с платой Arduino Uno и виден его порт.
Подсказка: Не вижу порт
Отключи и снова подключи кабель. Если порт так и не появился, возможно, кабель только заряжает и не передаёт данные. Попроси другой кабель.
Программы для робота пишут в MyRobotica. В ней есть уроки и редактор кода.
- Подключи USB-кабель к Arduino и к компьютеру. Батарейный отсек моторов остаётся отключённым.
- Открой MyRobotica, курс «Робот-курьер», урок 1.1 и его учебный проект.
- В правой части кнопки Загрузить на робота выбери своего настоящего робота. Открой Подробнее о связи и проверь плату Arduino Uno. Отключи USB-кабель, посмотри, как изменилось состояние связи, и подключи его снова. Запиши имя порта в журнал.
- В редакторе оставь пустые функции
setup()иloop()и сохрани программу.
В MyRobotica открыт учебный проект и выбран настоящий робот с платой Arduino Uno. Имя его порта записано.
Подсказка: Порт так и не появился
Кабель может только заряжать и не передавать данные. Попроси другой кабель.
- Подключи USB-кабель к Arduino и к компьютеру. Батарейный отсек моторов остаётся отключённым.
- В MyRobotica открой курс «Робот-курьер», урок 1.1 и его учебный проект. В правой части кнопки Загрузить на робота выбери своего настоящего робота. Через Подробнее о связи проверь плату Arduino Uno и найди её порт.
- Сохрани программу с пустыми
setup()иloop(), нажми Проверить код, а потом Загрузить на робота. - Запиши в журнал: плату, порт, версию MyRobotica и чем Проверить код отличается от Загрузить на робота — что делает каждая кнопка и нужна ли для неё плата.
Пустой скетч проверен и загружен. В журнале — плата, порт, версия MyRobotica и разница двух кнопок.
Подсказка: Проверить код и Загрузить на робота
Отключи кабель и нажми обе кнопки по очереди. Какая сработала без платы?
Проверь загрузку программы
3 минЗагрузи пустой скетч и прочитай сообщение об успешной загрузке.
void setup() {} void loop() {}
Это проверка платы, USB-связи и загрузки. Она ещё ничего не говорит о моторах, шилде, датчиках и качестве механической сборки. Работоспособность проверим после подключения всех узлов.
Надень шилд и подключи моторы
7 минВыводы Arduino не могут дать моторам достаточно тока. Поэтому моторы подключаются к шилду — плате, которая надевается сверху на Arduino и включает моторы по её командам.
Сначала отключи USB-кабель. Ничего не подключай и не отключай, пока плата под питанием.
- Совмести ножки шилда с гнёздами Arduino и аккуратно надави сверху. Ни одна ножка не должна остаться снаружи или согнуться.
- Подключи провода моторов к клеммам шилда:
| Что подключаем | Куда |
|---|---|
| Левый мотор Пометь провода буквой «Л». | Клеммы канала M2 на шилде |
| Правый мотор Пометь провода буквой «П». | Клеммы канала M1 на шилде |
| Батарейный отсек (через выключатель)взрослый Подключает взрослый. | Клеммы питания моторов на шилде |
- Батарейный отсек с выключателем уже подключён взрослым. Проверь, что выключатель выключен. Если провода отсека не подключены — сам их не подключай, позови взрослого.
Шилд сидит на Arduino ровно. Провода моторов закреплены в клеммах и подписаны.
Установи датчики линии
6 минДатчики линии будут «смотреть» на пол и замечать чёрную линию. Ставим их спереди, поперёк робота.
- Закрепи планку датчиков снизу у переднего края основы на стойках.
- Закрепи на планке два датчика — левый и правый — чувствительной стороной вниз, к полу. Датчики на одинаковой высоте.
- Подключи датчики к шилду по таблице. Буквы у контактов датчика: S — сигнал, V — питание, G — земля.
| Что подключаем | Куда |
|---|---|
| Левый датчик · S (сигнал) | A4 |
| Левый датчик · V (питание) | 5V |
| Левый датчик · G (земля) | GND |
| Правый датчик · S (сигнал) | A5 |
| Правый датчик · V (питание) | 5V |
| Правый датчик · G (земля) | GND |
- Уложи провода так, чтобы они не касались колёс.
Два датчика смотрят вниз, их провода подключены и не свисают к колёсам.
Проверь все узлы робота
14 минТеперь проверим, что каждая часть робота работает: оба колеса и оба датчика. Программа для проверки уже открыта в редакторе справа.
Поставь робота на подставку так, чтобы колёса висели в воздухе и не касались стола. Пока выключатель моторов выключен, колёса крутиться не могут.
Программу писать не нужно — она готова. Ты будешь отправлять ей буквы и цифры, а она будет крутить одно колесо или показывать числа датчиков.
// Программа проверки для робота курса (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. Второй мотор при этом выключен.
Остальную программу не меняй.
// Программа проверки для робота курса (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. Сначала проверь датчики — для этого моторы не нужны.
Напиши программу сам. В заготовке есть только номера контактов и выключение моторов.
// Программа проверки для робота курса (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 мс.
Как проверить робота
- Проверь провода. Выключатель моторов выключен. Сверь провода с таблицами подключения: каждый на своём месте и не касается колёс. Колёса крутятся рукой свободно.
- Загрузи программу. Нажми Загрузить на робота. Внизу открой Serial Monitor со скоростью 9600. Появится строка
READY— плата отвечает. Нажми кнопку Reset на плате: колёса стоят. - Проверь датчики. Отправь
dи прочитай два числа. Подложи под левый датчик белую бумагу и отправьd, потом чёрную — и сноваd. Сделай так же с правым датчиком. На белом и чёрном числа должны заметно отличаться. - Проверь колёса. Включи выключатель моторов. Отправь
1и смотри на колёса: коротко крутится только левое колесо вперёд и само останавливается. Потом по очереди отправь2,3и4. - Проверь остановку. Отправь
1и сразуs— колесо останавливается. Выключи выключатель моторов и отправь1ещё раз: колёса не крутятся. - Запиши результат в журнал для каждого узла из таблицы: команду, ответ в Serial Monitor и фактическое вращение или показание датчика. Если узел ещё не проверен, так и запиши: «не проверен». Если что-то не так — выключи выключатель моторов, отключи USB, открой «Если не получилось», исправь и проверь этот узел ещё раз.
| Что проверяем | Что сделать | Хорошо, если |
|---|---|---|
| Плата | Загрузить программу, нажать Reset | Есть READY, колёса стоят |
| Левое колесо | Отправить 1, потом 2 | Крутится только левое: вперёд, потом назад |
| Правое колесо | Отправить 3, потом 4 | Крутится только правое: вперёд, потом назад |
| Левый датчик | d на белом и на чёрном | Числа заметно разные |
| Правый датчик | d на белом и на чёрном | Числа заметно разные |
| Остановка | s и выключатель моторов | Колесо останавливается |
| Что видишь | Что сделать (выключатель моторов выключен) |
|---|---|
| Крутится не то колесо | Поменяй местами провода моторов в клеммах M1 и M2 |
| Колесо крутится назад вместо вперёд | Поменяй местами два провода этого мотора в клемме |
| Колесо не крутится | Проверь, что выключатель включён и провода плотно зажаты в клеммах |
| Число датчика не меняется | Проверь провода S, V, G и высоту датчика над полом |
Нет строки READY | Проверь скорость 9600 и выбранный порт |
| Колесо крутится само после Reset | Проверь, что загружена именно эта программа |
| Ничего не помогает | Позови взрослого и покажи, на каком пункте остановился |
Ты проверил каждое колесо в обе стороны, оба датчика и остановку. Результаты записаны в таблицу.
Запиши результат в журнал
4 мин- Запиши в бортовой журнал дату, имя робота и что получилось сегодня.
- Отметь, какие узлы прошли проверку сразу, а какие ты исправлял.
- Сохрани программу проверки — она пригодится, если робот начнёт вести себя странно.
Робот проверен, результаты записаны в журнал, программа проверки сохранена.
Проверь себя
Выберите ответы для подготовки. Они не сохраняются и не оцениваются на сайте. Пройдите проверку знаний в программе курса или обсудите ответы с преподавателем.
● Основное
★ Средний уровень
★★ Продвинутый уровень
Отвечай на вопросы своего уровня. Нужно верных ответов: Начальный — 4 из 5; Средний — 4 из 5; Продвинутый — 4 из 5. Ответы проверит преподаватель или программа курса.
Итог
Ты собрал ходовую часть робота, установил плату и датчики и написал первую программу. Ты подтвердил работу установленных узлов наблюдениями. Проверка знаний обязательна отдельно. В следующем уроке перейдёшь от отдельных проверок на подставке к управляемому движению.
Как подтвердить результат
Для каждого опыта сохраняй условия, версию программы, наблюдение и вывод. Сравнивая попытки, меняй одну причину за раз; неудачные попытки тоже остаются в журнале. Отметка результата относится к уже выполненным действиям, повторять их ради отметки не нужно. Если результат не получен, запиши расхождение и следующую проверяемую гипотезу. Модель, измерения настоящего робота и объяснение — разные виды доказательств.
Словарь урока
| Arduino | Плата с микроконтроллером — маленьким компьютером, который выполняет твою программу. |
| шилд | Плата, которая надевается сверху на Arduino и добавляет ей возможности. Наш шилд управляет моторами. |
| скетч | Программа для Arduino. |
| функция | Именованный блок команд. setup и loop — функции. |
| светодиод | Маленькая лампочка на плате. Светодиод L подключён к выводу 13. |
| вывод | Контакт платы, которым программа может управлять или с которого может читать сигнал. |
| мс | Миллисекунда — тысячная доля секунды. 1000 мс = 1 секунда. |
Моторы: едем и останавливаемся
Первые движения робота: вперёд, назад и обязательная остановка из программы.
Результат урока
Робот проезжает вперёд, останавливается и возвращается назад по программе. Движения оформлены функциями forward(), backward() и stopMotors().
Ты научишься
- Объяснить, что задаёт контакт направления и что — контакт скорости.
- Включить и остановить моторы командами digitalWrite и analogWrite.
- Написать функции движения и вызвать их по порядку.
- Объяснить, почему движение без команды остановки не заканчивается.
Нужно знать
- 1.1 Собираем робота и оживляем плату
Понадобится
Зачем этот урок
Робот собран, но пока стоит. Сегодня он поедет. Ты узнаешь, как плата командует моторами, и научишь робота ехать вперёд, назад и — самое важное — останавливаться по программе.
- Сколько раз выполняется
setup(), а сколько —loop()? - Что делает
digitalWrite(вывод, HIGH)? - Где на твоём роботе левый мотор?
Моторы проверяем на подставке: колёса не касаются стола. Программу загружаем при выключенном питании моторов. Чтобы остановить опыт, выключи питание моторов выключателем.
Подготовь робота
5 минЧто должен делать робот. Роботу нужно начать движение, проехать участок и остановиться. Для этого научимся отдельно задавать направление и мощность каждого из двух моторов, а затем объединим команды в понятные действия.
Что используем и как это работает. Моторы подключены через драйвер L298P: вывод Arduino подаёт управляющий сигнал, а ток к мотору поступает через драйвер. DIR выбирает направление. ШИМ быстро чередует включение и выключение питания; большее значение analogWrite означает большую долю включённого времени. Это команда мощности, а не измеренная скорость в сантиметрах в секунду.
Как связать измерение с действием. Если оба колеса крутятся вперёд, робот едет; если одно слабее, он отклоняется. delay только задерживает следующую команду: мотор продолжает выполнять предыдущую. Поэтому в программе нужна отдельная остановка. Сначала наблюдаем колёса на подставке, затем проверяем направление, расстояние и остановку на поле.
- Поставь робота на подставку так, чтобы оба колеса висели в воздухе.
- Найди выключатель питания моторов и выключи его.
- Подключи USB-кабель.
Колёса висят, питание моторов выключено, плата подключена к компьютеру.
Включи левый мотор
15 минКаждым мотором управляют два контакта. Контакт направления решает, куда крутится колесо. Контакт скорости решает, насколько сильно: число от 0 (стоит) до 255 (полная мощность). Левый мотор подключён к контактам 7 (направление) и 6 (скорость).
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() {}
Что сделает колесо после загрузки? Сколько секунд оно будет крутиться? Остановится ли оно само? Скажи напарнику до запуска.
- Набери программу и загрузи её при выключенном питании моторов.
- Включи питание моторов и нажми кнопку Reset на плате: программа начнётся заново.
- Сравни, что произошло, со своим прогнозом.
Левое колесо крутится вперёд 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(мс); | Ждёт указанное число миллисекунд |
- Создай скетч
L21_Left_Motorпо шаблону и напишиsetup()сам: что должно произойти, сказано в комментариях.
// Шаблон среднего уровня. Напиши код в разделе «Твоя часть» по целям в комментариях. const int LEFT_DIR_PIN = 7;const int LEFT_SPEED_PIN = 6;const int FORWARD = HIGH; // ===== Твоя часть: напиши сам ===== void setup() { // Подготовь оба контакта левого мотора. // Левое колесо крутится вперёд 2 секунды с силой 100 из 255 и останавливается само.} void loop() {}
Что будет, если забыть команду остановки? Запиши ответ и проверяй его только на подставке.
- Загрузи программу при выключенном питании моторов.
- Включи питание моторов и нажми кнопку 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 секунды с силой из именованной переменной и останавливается само. Номера контактов — именованные константы, а не числа в командах.
Приёмка.
- На подставке напарник засекает по секундомеру, сколько крутится колесо.
- Замени уровень направления на противоположный. Что сделало колесо? Запиши, какой уровень на твоём роботе — «вперёд».
- Объясни в журнале, почему колесо остановилось, хотя
loop()пустой.
Подсказка: Идея (1 из 2)
Один раз при включении: настроить контакты, выбрать направление, включить, подождать, выключить.
Подсказка: Подсказка к пункту 3 (2 из 2)
Где выполнилась команда остановки — в setup() или в loop()? Сколько раз?
Колесо крутится 2 секунды и останавливается. В журнале — время по секундомеру, уровень «вперёд» и объяснение остановки.
Включи оба мотора
15 мин- Создай скетч
L21_Both_Motorsи набери программу.
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() {}
- Загрузи и запусти на подставке.
- Поменяй
speedна 60, потом на 200. Как меняется вращение?
Оба колеса крутятся вперёд одну секунду и останавливаются. С большим speed колёса крутятся быстрее.
Как это устроено: Почему 0–255, а не километры в час
Число в analogWrite — это не скорость, а доля времени, когда мотор включён. При 255 он включён всё время, при 100 — примерно 40 % времени. Настоящая скорость зависит ещё от батареи и пола. Поэтому скорость подбирают опытом.
Правый мотор устроен так же, как левый: контакт направления 4 и контакт скорости 5. Имена всех четырёх контактов в шаблоне уже есть.
- Создай скетч
L21_Both_Motorsпо шаблону и напишиsetup()сам. Свою программу левого мотора используй как образец.
// Шаблон среднего уровня. Напиши код в разделе «Твоя часть» по целям в комментариях. 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() {}
- Загрузи и запусти на подставке.
- Поменяй
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 брать нельзя.
Найди ошибку остановки
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);
}
Они ждали: секунду едет, потом стоит. На подставке колёса крутятся почти без остановки. Почему?
- Запиши ответ в журнал.
- Предложи два разных исправления и запиши их кодом: одно — без
loop(), второе — оставив движение вloop().
Подсказка: Второе исправление
Можно ли после остановки «застрять» на месте, чтобы loop() больше не повторялся? Вспомни защиту из урока 1.1… или пустой бесконечный цикл while (true) { }.
Ты можешь объяснить, почему остановка длится мгновение, и в журнале два исправления.
У соседей loop() пустой, а движение — в setup():
void forward(int ms) {
drive(FORWARD, speed, FORWARD, speed);
delay(ms);
}
void setup() {
// … настройка контактов
forward(1000);
}
void loop() {
}
Робот должен был проехать одну секунду, а едет, пока его не выключат. Почему, если loop() пустой? Запиши ответ в журнал.
Требование. Перепиши программу так, чтобы движение осталось в loop(), но робот проехал ровно одну секунду и больше не трогался. while (true) использовать нельзя.
- Проверь на подставке: колёса крутятся одну секунду и стоят, пока не нажмёшь Reset.
- Объясни в журнале, как твоя программа «помнит», что движение уже было.
Подсказка: Идея
Нужна переменная, которая хранит «уже ехали» — bool. Её проверяют в начале loop().
Программа выполняет требование, объяснение записано.
Оформи движения функциями
15 минПисать каждый раз по четыре строки неудобно. Соберём их в функции: forward(), backward(), stopMotors(). Внутри них — знакомые команды.
- Создай скетч
L21_Movesи набери программу.
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() {}
- Найди, где функции описаны и где они вызываются. Покажи напарнику.
Как это устроено: Как устроена функция с параметром
void forward(int ms) — функция с именем forward и параметром ms. Когда пишем forward(1000), внутри функции ms равно 1000. Так одна функция умеет ехать любое время.
drive(…) — вспомогательная функция: она сразу задаёт направление и скорость обоих колёс. Ей пользуются остальные функции.
Программа проверяется без ошибок. Ты можешь показать описание и вызов forward.
Писать каждый раз по четыре строки неудобно. Соберём их в функции: forward(), backward(), stopMotors(). Функции drive и forward в шаблоне — образец.
- Создай скетч
L21_Movesпо шаблону. Напишиbackward,stopMotorsи движение вsetup()сам.
// Шаблон среднего уровня. Напиши код в разделе «Твоя часть» по целям в комментариях.// Функции 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() {}
- Найди, где функции описаны и где они вызываются. Покажи напарнику.
Подсказка: Функция назад
Чем «назад» отличается от «вперёд»? В программе уже есть константа для этого направления.
Подсказка: Как вызвать функцию
Вызов — имя функции и значение параметра в скобках, в конце ;.
Как это устроено: Как устроена функция с параметром
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)
drive— четыре строки из своей программы, но с параметрами вместо чисел.forward(ms)—driveвперёд иdelay(ms).backward(ms)— то же назад.stopMotors()— скорость 0.
Подсказка: Фрагмент кода (3 из 3)
const int BACKWARD = (FORWARD == HIGH) ? LOW : HIGH; — прочитай как «если FORWARD равен HIGH, то LOW, иначе HIGH».
Программа выполняет требование 3 и проверяется без ошибок.
Первый заезд по полу
15 минОсвободи на полу или столе место не меньше метра вперёд и назад. Держи руку рядом с выключателем.
- Загрузи программу на подставке и проверь: вперёд, пауза, назад, стоп.
- Поставь робота на пол, включи питание моторов и нажми Reset.
- Отметь на полу мелом или скотчем точку старта и точку, где робот остановился.
Робот проезжает вперёд, останавливается и возвращается примерно на место старта.
| Что видишь | Что сделать |
|---|---|
| Робот едет не прямо | Моторы не одинаковые — это нормально. Запиши, куда уводит. |
| Робот вернулся не на старт | Вперёд и назад колёса крутятся чуть по-разному. Запиши разницу. |
| Робот поехал сразу после загрузки | Так и есть: программа стартует сама. Поэтому в setup() есть пауза 2 секунды. |
- Загрузи программу на подставке и проверь: вперёд, пауза, назад, стоп.
- Поставь робота на пол, отметь старт скотчем, включи питание моторов и нажми Reset.
- Повтори три раза. Каждый раз отметь, где робот остановился, и измерь линейкой, на сколько сантиметров он ушёл от старта вперёд-назад и вбок.
| Заезд | Вперёд или назад, см | Вбок, см | Куда уводит |
|---|---|---|---|
| 1 | |||
| 2 | |||
| 3 |
- Одинаково ли робот ошибается в трёх заездах? Запиши вывод.
Таблица из трёх заездов и вывод: куда и насколько уводит твоего робота.
| Что видишь | Что сделать |
|---|---|
| Робот едет не прямо | Моторы не одинаковые — это нормально. Запиши, куда уводит. |
| Робот поехал сразу после загрузки | Так и есть: программа стартует сама. Поэтому в setup() есть пауза 2 секунды. |
Узнай, насколько прямо едет твой робот, и попробуй это исправить одним изменением.
- Проверь программу на подставке, потом поставь робота на пол у начала метровой полосы скотча. Проедь вперёд 2 секунды. Измерь, на сколько сантиметров нос ушёл вбок от полосы. Повтори три раза.
- Предложи одно изменение, которое уменьшит увод: например, разные скорости левого и правого колеса. Запиши, почему ты ожидаешь улучшения.
- Сделай изменение и снова три заезда. Стало лучше? На сколько?
| Увод 1, см | Увод 2, см | Увод 3, см | |
|---|---|---|---|
| До | |||
| После |
Подсказка: Колёса разные
Если робот уходит вправо, какое колесо крутится быстрее?
Две серии по три заезда, одно изменение с обоснованием и вывод по числам.
Измени программу
10 мин- Сделай так, чтобы робот проехал вперёд в два раза дальше, а назад — как раньше.
- Сделай «челнок»: вперёд — назад три раза подряд с паузами.
- Сделай так, чтобы робот проехал вперёд в два раза дальше, а назад — как раньше.
- Сделай «челнок» — вперёд, стоп, назад, стоп три раза подряд, — написав вызовы
forward,stopMotors,backwardтолько один раз. Нужен циклforиз урока 1.1.
Вернётся ли робот после трёх челноков точно на старт? Запиши прогноз, потом отметь старт и финиш на полу.
Подсказка: Как устроен for
for (int i = 0; i < 3; i++) { … } повторяет команды в скобках три раза.
Робот выполняет оба задания. В журнале — прогноз и насколько робот ушёл от старта после трёх челноков.
Требование 4. Функция shuttle(times, ms) делает «челнок» times раз: вперёд ms миллисекунд, стоп, назад, стоп. Вызовы движений в ней написаны один раз.
Исследование: сколько проезжает робот.
- Три раза запусти
forward(1000)приspeed = 100и измерь путь линейкой в миллиметрах. Повтори приspeed = 200. - Посчитай, сколько миллиметров в секунду проезжает робот при каждой скорости. Во сколько раз отличаются результаты? Вдвое ли, как отличаются числа
speed? - Подбери время
backwardтак, чтобы после одного челнока робот вставал не дальше 2 см от старта. Запиши, почему вперёд и назад время получилось разным.
speed | Путь 1, мм | Путь 2, мм | Путь 3, мм | мм за секунду |
|---|---|---|---|---|
| 100 | ||||
| 200 |
Подсказка: Пути разные при одной программе
Это нормально: колёса проскальзывают, батарея садится. Поэтому меряем три раза и берём среднее.
shuttle работает. В журнале — таблица путей, скорость в мм/с для двух speed, вывод про связь числа speed и настоящей скорости и подобранное время возврата.
Робот проезжает вперёд 1 секунду, останавливается и возвращается назад.
Сделай «челнок» из трёх повторов, написав вызовы forward, stopMotors, backward только один раз. Подсказка: цикл for из урока 1.1.
Измерь, сколько миллиметров робот проезжает за секунду, и подбери время назад так, чтобы после челнока он вставал не дальше 2 см от старта. Запиши, почему время вперёд и назад разное.
Проверь себя
Выберите ответы для подготовки. Они не сохраняются и не оцениваются на сайте. Пройдите проверку знаний в программе курса или обсудите ответы с преподавателем.
● Основное
★ Средний уровень
★★ Продвинутый уровень
Отвечай на вопросы своего уровня. Нужно верных ответов: Начальный — 4 из 5; Средний — 4 из 5; Продвинутый — 4 из 5. Ответы проверит преподаватель или программа курса.
Итог
Робот ездит по твоей программе. Главное: analogWrite включает мотор, и он крутится, пока другая команда его не остановит. Функции делают программу короче и понятнее. В следующем уроке робот научится поворачивать.
Как подтвердить результат
Для каждого опыта сохраняй условия, версию программы, наблюдение и вывод. Сравнивая попытки, меняй одну причину за раз; неудачные попытки тоже остаются в журнале. Отметка результата относится к уже выполненным действиям, повторять их ради отметки не нужно. Если результат не получен, запиши расхождение и следующую проверяемую гипотезу. Модель, измерения настоящего робота и объяснение — разные виды доказательств.
Словарь урока
| ШИМ | Частое включение и выключение питания мотора. Число от 0 до 255 задаёт, какую долю времени мотор включён. |
| направление | Состояние контакта направления: одно значение крутит колесо вперёд, другое — назад. |
| константа | Число с именем, которое не меняется во время работы программы, например номер контакта. |
| функция | Именованный блок команд. Его можно вызвать много раз, написав имя и скобки. |
| параметр | Число, которое передают функции в скобках, например время движения. |
Повороты и развороты
Робот поворачивает на месте на 90° и разворачивается на 180°. Время поворота подбираешь сам.
Результат урока
Робот поворачивает на 90° налево и направо и разворачивается на 180°. Время поворотов подобрано и записано в бортовой журнал, повороты оформлены функциями.
Ты научишься
- Объяснить, почему робот крутится на месте, когда колёса вращаются в разные стороны.
- Подобрать время поворота опытом и проверить его повторением.
- Написать функции turnLeft(), turnRight() и turnAround().
- Предсказать, куда приедет робот по последовательности движений.
Нужно знать
- 2.1 Моторы: едем и останавливаемся
Понадобится
Зачем этот урок
Робот умеет ездить вперёд и назад. Но на поле итоговой задачи нужно сворачивать к грузам и разворачиваться. Сегодня робот научится поворачивать на месте, а ты — подбирать числа опытом, как настоящий инженер.
- Какая команда задаёт направление вращения колеса?
- Что будет, если включить моторы и не написать
stopMotors()? - Чем описание функции отличается от её вызова?
Колёса в разные стороны
10 минЧто должен делать робот. Курьеру нужно менять направление: повернуть к ответвлению и развернуться для обратной поездки. Поворот получим теми же двумя моторами, без отдельного рулевого механизма.
Что используем и как это работает. Когда колёса проходят разные расстояния, корпус поворачивается. Если одно колесо вращается вперёд, а другое назад, робот разворачивается примерно вокруг середины между колёсами. Функция поворота объединяет направления колёс, время вращения и остановку.
Как связать измерение с действием. В этом уроке угол задаём временем работы моторов. Это приближение: заряд батареи, покрытие и проскальзывание меняют результат. Измерим получившийся угол, подберём время и повторим опыт. Имя turnLeft само по себе не гарантирует 90 градусов — это должна подтвердить проверка.
Левое колесо крутится назад, правое — вперёд с той же скоростью. Что сделает робот? Покажи руками.
- Набери скетч
L22_Turn_Testи загрузи его при выключенном питании моторов.
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() {}
- Поставь робота на пол, на лист бумаги. Прочерти карандашом линию вдоль его носа.
- Включи питание моторов и нажми Reset.
Робот крутится на месте налево и останавливается. Нос повернулся, но, скорее всего, не ровно на 90°.
Как это устроено: Почему робот крутится на месте
Колёса толкают робота в разные стороны с одинаковой силой. Вперёд и назад он не едет — он вращается вокруг точки посередине между колёсами. Чем дольше включены моторы, тем больше угол поворота.
Робот должен повернуть налево на месте. Какое колесо должно крутиться вперёд, а какое назад? Запиши до запуска.
- Создай скетч
L22_Turn_Testпо шаблону. ФункцииdriveиstopMotorsиз урока 2.1 уже есть. Напиши поворот вsetup()по своему прогнозу.
// Шаблон среднего уровня. Напиши код в разделе «Твоя часть» по целям в комментариях. 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() {}
- Поставь робота на пол, на лист бумаги. Прочерти карандашом линию вдоль его носа. Включи питание моторов и нажми Reset.
- Измени программу так, чтобы робот повернул направо. Проверь.
Подсказка: Робот едет, а не крутится
Если оба колеса крутятся в одну сторону, робот едет. Для поворота на месте нужны разные направления.
Робот крутится на месте налево, а после твоего изменения — направо. Прогноз проверен.
Требование. В своей программе L21_Moves из урока 2.1 сделай пробу поворота: колёса крутятся в разные стороны с одной скоростью turnSpeed в течение turnMs, потом робот останавливается. Сохрани как L22_Turn_Test.
Исследование: вокруг чего крутится робот.
- Положи робота на лист бумаги и обведи карандашом оба колеса.
- Запусти поворот и обведи колёса снова. Найди точку, вокруг которой повернулся робот.
- Совпадает ли она с серединой между колёсами? Если нет — насколько сантиметров и почему?
Подсказка: Как найти точку поворота
Соедини старое и новое положение каждого колеса. Точка поворота лежит там, где эти положения одинаково далеко.
Проба поворота работает. В журнале — рисунок, точка поворота и объяснение, почему она там.
Подбери время поворота на 90°
20 минНа поворот 90° нужно какое-то время в миллисекундах. Узнать его можно только опытом.
- Начерти на листе две линии под прямым углом (угольник или край тетради).
- Поставь робота носом вдоль первой линии. Запусти программу.
- Если робот не довернул — увеличь
turnMs, если перевернул — уменьши. Меняй понемногу: на 20–50 мс. - Когда поворот получился, повтори его три раза. Запиши время в журнал.
| Попытка | turnMs | Что получилось |
|---|---|---|
| 1 | 400 | |
| 2 | ||
| 3 |
Робот три раза подряд поворачивает примерно на 90°. Время записано в журнале.
Подсказка: Каждый раз поворачивает по-разному
Проверь, что батарея заряжена: от разряженной моторы крутятся медленнее. Пол должен быть ровным и не скользким. Небольшая разница между попытками — нормально.
На поворот 90° нужно какое-то время в миллисекундах. Узнать его можно только опытом. Как подбирать — реши сам, но меняй только turnMs и записывай каждую попытку.
- Начерти на листе две линии под прямым углом. Поставь робота носом вдоль первой.
- Подбирай
turnMs, пока робот не повернёт на 90°. В журнале — не меньше четырёх попыток и объяснение, почему ты выбрал следующее число. - Когда поворот получился, повтори его три раза.
| Попытка | turnMs | Что получилось | Что поменять дальше и почему |
|---|---|---|---|
| 1 | |||
| 2 | |||
| 3 | |||
| 4 |
Подсказка: С какого числа начать
Посмотри, сколько повернулся робот в пробе с turnMs = 400. Больше 90° или меньше?
Подсказка: Каждый раз поворачивает по-разному
Проверь, что батарея заряжена, а пол ровный и не скользкий. Небольшая разница между попытками — нормально.
Робот три раза подряд поворачивает примерно на 90°. В журнале четыре попытки с объяснением и итоговое время.
Подбери время поворота методом и узнай, насколько ему можно доверять.
- Найди одно время, при котором робот явно не доворачивает, и одно, при котором явно перекручивает. Пробуй середину между ними и сужай промежуток, пока ошибка не станет меньше 5°.
- С найденным временем сделай пять поворотов подряд. Для каждого отметь на листе, куда смотрит нос, и измерь транспортиром ошибку в градусах.
- Повтори пункт 2 при другой
turnSpeed(например, 160). При какой скорости повороты точнее и одинаковее? - Повернёт ли робот направо за то же время? Проверь и запиши.
turnSpeed | turnMs | Ошибки 5 поворотов, ° | Самая большая ошибка, ° |
|---|---|---|---|
| 100 | |||
| 160 |
Подсказка: Как измерить ошибку без транспортира
Сравни отметку носа с линией 90° на расстоянии 10 см от центра поворота: 1 см в сторону — это примерно 6°.
В журнале — способ поиска, время для двух скоростей, ошибки пяти поворотов и вывод: какую turnSpeed брать и одинаково ли время налево и направо.
Разворот на 180°
15 минЕсли на 90° нужно, например, 400 мс, сколько нужно на 180°? Запиши свой прогноз.
- Поставь в
turnMsсвоё время, умноженное на 2. Проверь разворот. - Подправь число, если нужно. Запиши время разворота в журнал.
Робот разворачивается и смотрит в обратную сторону. Время разворота записано.
Как это устроено: Почему ровно в два раза не всегда получается
В начале поворота моторам нужно время, чтобы разогнаться, а в конце робот немного проскальзывает по инерции. Поэтому время на 180° может быть чуть меньше двух поворотов на 90°. Опыт важнее расчёта.
Сколько миллисекунд нужно на разворот на 180°, если ты знаешь своё время на 90°? Запиши прогноз.
- Проверь прогноз на полу и подправь число, если нужно. Запиши время разворота.
- Ответь в журнале: получилось ровно вдвое больше, чем на 90°? Если нет, как ты это объяснишь?
Подсказка: Почему не ровно вдвое
Подумай, что происходит в начале поворота, когда колёса только трогаются, и в конце, когда моторы уже выключены.
Робот разворачивается и смотрит в обратную сторону. Время разворота и объяснение записаны.
- Предскажи время разворота по своему времени на 90° и проверь на полу. Подбери точное время тем же методом, что и для 90°.
- Сравни: во сколько раз время на 180° больше времени на 90°? Объясни в журнале, откуда берётся разница. Назови две причины.
- Выведи правило: сколько миллисекунд нужно на один градус при развороте и при повороте на 90°? Какое из чисел точнее подойдёт для поворота на 45°? Проверь.
Время разворота подобрано. В журнале — сравнение с удвоенным поворотом на 90°, две причины разницы и проверка правила на 45°.
Функции поворотов
12 мин- Создай скетч
L22_Turns. Впиши свои времена из журнала вturn90Msиturn180Ms.
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() {}
- Найди разницу между
turnLeft()иturnRight(). Что поменялось местами?
Программа проверяется без ошибок. Ты можешь объяснить, чем отличаются функции поворота.
- Создай скетч
L22_Turnsпо шаблону. Впиши свои времена из журнала вturn90Msиturn180Ms. - Напиши
turnLeft,turnRight,turnAroundи квадрат вsetup()сам. Функцияforwardиз урока 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;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() {}
- Нажми Проверить.
Подсказка: Чем отличаются повороты
Сравни свою пробу поворота: какие колёса и в какую сторону крутятся налево, а какие — направо? А при развороте?
Подсказка: Четыре раза
Повторить одно и то же четыре раза помогает цикл 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); — так робот крутится налево. Что поменять для поворота направо?
Программа выполняет требование и проверяется без ошибок.
Квадрат
18 минПрограмма повторяет 4 раза: «вперёд 1 секунду, налево». Где окажется робот в конце и куда будет смотреть? Нарисуй путь в журнале.
- Отметь старт скотчем. Запусти программу на полу.
- Сравни, где робот остановился, с прогнозом.
- Насколько робот не доехал до старта? Запиши.
Робот проезжает квадрат и возвращается близко к старту, носом в ту же сторону.
У соседей робот едет вперёд, крутится, но квадрат не получается: он ездит туда-обратно. В их turnLeft() стоит delay(turn180Ms). Что не так?
Программа повторяет 4 раза: «вперёд 1 секунду, налево». Нарисуй путь в журнале. Где окажется робот в конце и куда будет смотреть?
- Отметь старт скотчем. Запусти программу на полу три раза.
- Каждый раз измерь, на сколько сантиметров робот не доехал до старта, и на сколько градусов примерно повёрнут его нос.
- Объясни в журнале, почему ошибка в конце больше, чем ошибка одного поворота.
Три заезда с замерами и объяснение накопления ошибки.
У соседей робот едет вперёд, крутится, но квадрат не получается: он ездит туда-обратно. В их turnLeft() стоит delay(turn180Ms). Что не так?
- Запусти квадрат три раза. Каждый раз измерь, насколько робот промахнулся мимо старта (в сантиметрах) и насколько повёрнут нос (в градусах).
- Оцени ошибку одного поворота: раздели ошибку направления в конце на четыре. Совпадает ли она с ошибкой, которую ты мерил при подборе времени?
- Что сильнее портит квадрат: ошибка поворотов или увод на прямой? Как это проверить отдельным опытом? Проведи его и запиши вывод.
У соседей робот едет вперёд, крутится, но квадрат не получается: он ездит туда-обратно. В их turnLeft() стоит delay(turn180Ms). Что не так?
Замеры трёх квадратов, оценка ошибки одного поворота и опыт, который показал главную причину промаха.
Задания по уровням
15 минРобот поворачивает налево и направо на 90° и разворачивается на 180° по твоим временам.
Наклей на пол полоску изоленты. Поставь робота на неё, сделай разворот на 180°. Робот должен снова стоять вдоль полоски, а нос — уходить от неё не дальше её ширины. Так разворот будет работать в итоговой задаче.
Напиши функцию turn(int degrees): положительный угол — налево, отрицательный — направо. Время считай как degrees × msPerDegree, где msPerDegree = turn90Ms / 90. Проверь на 45°, 90° и 180°. Где точность хуже всего и почему?
Проверь себя
Выберите ответы для подготовки. Они не сохраняются и не оцениваются на сайте. Пройдите проверку знаний в программе курса или обсудите ответы с преподавателем.
● Основное
★ Средний уровень
★★ Продвинутый уровень
Отвечай на вопросы своего уровня. Нужно верных ответов: Начальный — 4 из 5; Средний — 4 из 5; Продвинутый — 4 из 5. Ответы проверит преподаватель или программа курса.
Итог
Робот поворачивает по времени, которое ты подобрал опытом. Это простой способ, но он зависит от батареи и пола. В уроке 4.2 робот научится поворачивать точнее — по датчикам: крутиться, пока не найдёт новую линию.
Как подтвердить результат
Для каждого опыта сохраняй условия, версию программы, наблюдение и вывод. Сравнивая попытки, меняй одну причину за раз; неудачные попытки тоже остаются в журнале. Отметка результата относится к уже выполненным действиям, повторять их ради отметки не нужно. Если результат не получен, запиши расхождение и следующую проверяемую гипотезу. Модель, измерения настоящего робота и объяснение — разные виды доказательств.
Словарь урока
| поворот на месте | Колёса крутятся в разные стороны, и робот вращается вокруг середины между колёсами. |
| калибровка | Подбор числа по опыту: например, сколько миллисекунд нужно на поворот 90°. |
| погрешность | Насколько результат отличается от нужного: например, робот повернул на 85° вместо 90°. |
Датчики линии и первое следование
Робот «смотрит» на пол, отличает чёрное от белого и сам держится линии.
Результат урока
Робот проезжает по чёрной линии с плавным изгибом, поворачивая по сигналам двух датчиков. В бортовом журнале записаны показания датчиков на белом и чёрном.
Ты научишься
- Прочитать показания датчика командой analogRead и вывести их в монитор порта.
- Различать строки Serial Monitor и каналы телеметрии с временем и единицами; сравнивать сохранённые показания модели и настоящего робота.
- Измерить показания на белом и чёрном и объяснить, почему у каждого датчика они свои.
- Записать правило «показание ближе к чёрному — значит, под датчиком линия».
- Составить два правила поворота и проверить их сначала на подставке, затем на дорожке.
Нужно знать
- 1.1 Собираем робота и оживляем плату
- 2.1 Моторы: едем и останавливаемся
Понадобится
Зачем этот урок
До сих пор робот делал только то, что ты заранее записал: ехать столько-то, повернуть так-то. Сегодня он начнёт сам принимать решения. Два датчика будут смотреть на пол, и робот будет поворачивать, когда линия уходит в сторону.
- Какая команда задаёт скорость мотора и какие числа она принимает?
- Что делает
delay(500)? - Какое колесо надо остановить, чтобы робот повернул налево?
Как робот может «увидеть» чёрную линию, если у него нет камеры? Обсудите с напарником и запишите догадку в бортовой журнал. В конце урока проверите.
Пока меняешь провода, USB-кабель отключён, питание моторов выключено. Опыты с моторами — сначала на подставке: колёса не касаются стола.
Подготовь дорожку и робота
5 минЧто должен делать робот. Чтобы ехать по дорожке, роботу нужно замечать, когда линия оказалась под левым или правым датчиком. Для этого используем два направленных вниз датчика отражения; они смотрят на пол, а не ищут предметы впереди.
Что используем и как это работает. В датчике есть излучатель и приёмник отражённого света. Поверхности отражают свет по-разному, а приёмник превращает отражённый свет в электрический сигнал. analogRead считывает его как число. Это не готовый ответ «чёрное»: сначала измеряем белое и чёрное для каждого датчика при рабочей высоте.
Как связать измерение с действием. Программа сравнивает чтение с измеренным порогом и выбирает действие колёс. Над линией слева и справа должны получиться разные команды коррекции. Проверим четыре сочетания карточек на подставке, затем движение по линии. Одинаковые показания обоих датчиков ещё не доказывают, что робот стоит правильно: оба могут оказаться и на белом, и на чёрном.
Датчику нужна понятная картинка: светлый фон и тёмная линия.
- Возьми светлое матовое поле с чёрной линией (или наклей чёрную матовую изоленту на белый лист). Для начала хватит прямой с одним плавным изгибом.
- Подготовь две карточки: белую и чёрную.
- Найди на роботе левый и правый датчики. Они смотрят вниз и стоят по обе стороны от линии, когда робот стоит на ней.
Дорожка и карточки готовы. Ты можешь показать левый и правый датчик.
Датчику нужна понятная картинка: светлый фон и тёмная линия. Для заданий среднего уровня на дорожке понадобится и перекрёсток.
- Возьми светлое матовое поле с чёрной линией или наклей чёрную матовую изоленту на белый лист: прямая, плавный изгиб и один перекрёсток.
- Подготовь две карточки: белую и чёрную.
- Найди на роботе левый и правый датчики. Поставь робота на линию и проверь: линия проходит между датчиками и не касается ни одного из них?
Дорожка с перекрёстком и карточки готовы. Линия помещается между датчиками.
Подготовь дорожку для своего уровня и проверь, подходит ли она твоему роботу.
- Сделай дорожку: прямая, плавный изгиб, крутая дуга и перекрёсток. Подготовь белую и чёрную карточки.
- Измерь линейкой ширину линии и расстояние между датчиками (между серединами окошек).
- Поставь робота на линию. Сколько миллиметров остаётся от края линии до каждого датчика? Запиши в журнал: что будет, если линия шире расстояния между датчиками?
Дорожка готова, в журнале — ширина линии, расстояние между датчиками, запас с каждой стороны и ответ на вопрос.
Посмотри, что видит датчик
12 минДатчик светит на пол и меряет, сколько света вернулось. Плата превращает это в число.
Чтобы увидеть число, плата будет отправлять его на компьютер, а мы откроем монитор порта.
- Создай скетч
L31_Read_Sensorsи набери программу. Левый датчик подключён кA4, правый — кA5.
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);}
- Загрузи программу. Открой Инструменты → Монитор порта и выбери скорость 9600.
- Поднеси под левый датчик белую карточку, затем чёрную. Держи карточку на той же высоте, на которой датчик стоит над полом.
В мониторе бегут строки вида 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); | Печатает и переходит на новую строку |
- Создай скетч
L31_Read_Sensorsпо шаблону и напиши обе части сам. Левый датчик подключён кA4, правый — кA5.
// Шаблон среднего уровня. Напиши код в разделе «Твоя часть» по целям в комментариях. const int LEFT_SENSOR_PIN = A4;const int RIGHT_SENSOR_PIN = A5; // ===== Твоя часть: напиши сам ===== void setup() { // Включи связь с компьютером на той же скорости, что в мониторе порта: 9600.} void loop() { // Пять раз в секунду печатай одну строку: L=число левого датчика R=число правого.}
- Загрузи программу и открой монитор порта со скоростью 9600.
- Поднеси под левый датчик белую карточку, затем чёрную. Держи карточку на той же высоте, на которой датчик стоит над полом.
Что напечатает монитор, если имя переменной в 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 — время в миллисекундах с включения.
Приёмка.
- По числам
tв мониторе проверь, что строки идут примерно каждые 200 мс. На сколько миллисекунд отличается промежуток от 200 и почему? - Поднеси под левый датчик белую, затем чёрную карточку на рабочей высоте. Запиши, на сколько меняется число.
Подсказка: Почему промежуток не ровно 200
Сколько времени занимают сами чтение и печать, кроме delay(200)?
Программа выполняет требование. В журнале — реальный промежуток между строками с объяснением и изменение числа на белом и чёрном.
Проверь оба датчика
8 минБудут ли числа левого и правого датчиков одинаковыми над одним и тем же белым полем?
- Поставь робота на белое поле. Запомни числа L и R.
- Подведи чёрную карточку только под левый датчик, потом только под правый.
- Сдвинь робота так, чтобы линия была точно между датчиками. Какие числа сейчас?
Ты знаешь, какое число у каждого датчика на белом и как оно меняется на чёрном. Когда линия между датчиками, оба показывают «белое».
Будут ли числа левого и правого датчиков одинаковыми над одним и тем же белым полем?
- Заполни таблицу для четырёх положений робота:
| Положение | L | R |
|---|---|---|
| Оба над белым | ||
| Чёрное только под левым | ||
| Чёрное только под правым | ||
| Линия точно между датчиками |
- Почему в последней строке оба числа «белые», хотя робот стоит на линии? Запиши.
Таблица заполнена, прогноз проверен, ответ про положение «между датчиками» записан.
Сними профиль датчиков: как меняются числа, пока линия проходит под роботом поперёк.
- Положи робота поперёк линии так, чтобы линия была далеко справа от правого датчика.
- Сдвигай робота влево шагами по 5 мм, пока линия не уйдёт за левый датчик. На каждом шаге записывай L и R.
| Сдвиг, мм | L | R |
|---|---|---|
| 0 | ||
| 5 | ||
| … |
- Нарисуй в журнале два графика: L и R от сдвига. Найди по графику, где линия ровно между датчиками и на сколько миллиметров можно уйти в сторону, пока датчик её не заметит.
Таблица и два графика. В журнале — ширина «слепой зоны» между датчиками в миллиметрах.
Измерь белое и чёрное
12 минДатчики не одинаковые: даже над одним полем их числа разные. Поэтому каждому датчику нужны свои измерения. Такая настройка по своим измерениям называется калибровкой.
- Поставь робота на белое поле. Посмотри на несколько строк и запиши типичное число левого датчика в бортовой журнал: «Левый, белое».
- Подведи под левый датчик чёрную линию. Запиши «Левый, чёрное».
- Повтори для правого датчика.
- Проверь себя: повтори каждое измерение ещё раз. Числа должны быть близки к записанным.
| Датчик | Белое | Чёрное |
|---|---|---|
| Левый | ||
| Правый |
В журнале четыре числа. Белое и чёрное у каждого датчика заметно отличаются.
Подсказка: Белое и чёрное почти одинаковые
Датчик может стоять слишком высоко или линия блестит. Опусти датчики ближе к полу и проверь линию в другом месте поля, где нет бликов.
Как это устроено: Почему у одних датчиков чёрное — большое число, а у других — маленькое
Это зависит от устройства датчика. Одни выдают тем большее число, чем больше света вернулось, другие — наоборот. Поэтому мы не запоминаем «чёрное — это много», а измеряем свои числа.
Датчики не одинаковые: даже над одним полем их числа разные. Поэтому каждому датчику нужны свои измерения — калибровка.
- Для каждого датчика запиши по три показания на белом и три на чёрном, в разных местах дорожки.
- Выбери типичное число для каждой клетки и запиши, почему именно его.
| Датчик | Белое: 3 числа | Белое | Чёрное: 3 числа | Чёрное |
|---|---|---|---|---|
| Левый | ||||
| Правый |
Подсказка: Числа в разных местах разные
Освещение и поле неодинаковые. Берут число, близкое к остальным, а не самое удачное.
В журнале таблица с повторами и четыре выбранных числа. Белое и чёрное заметно отличаются.
Откалибруй датчики и докажи, что калибровке можно доверять.
- Для каждого датчика запиши по 10 показаний на белом и 10 на чёрном. Найди наименьшее и наибольшее в каждой серии — это разброс.
- Сравни разброс с разницей белого и чёрного. Во сколько раз разница больше разброса?
- Повтори белое у окна и в тёмном углу класса. Меняются ли числа сильнее разброса?
- Запиши вывод: где можно доверять калибровке и когда её нужно повторить.
| Датчик | Белое: мин–макс | Чёрное: мин–макс | Разница типичных |
|---|---|---|---|
| Левый | |||
| Правый |
Таблица разбросов, сравнение с разницей и вывод, когда калибровку повторять.
Научи программу замечать линию
10 минКак программе решить, что под датчиком линия? Сравним новое показание с нашими двумя измерениями: если число ближе к чёрному, чем к белому, — под датчиком линия.
Пример: белое — 100, чёрное — 800. Пришло число 650. До чёрного 150, до белого 550. Ближе к чёрному — значит, линия.
- Создай скетч
L31_Line_Detect. Впиши свои четыре числа вместо нулей.
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);}
- Загрузи программу, открой монитор порта и подводи чёрную карточку под датчики по очереди.
Когда под левым датчиком чёрное, в строке появляется «слева линия», под правым — «справа линия».
Команда соседей написала так:
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 до чёрного и сколько до белого?
- Создай скетч
L31_Line_Detectпо шаблону и впиши свои четыре числа вместо нулей. - Напиши
loop()сам: решение «линия или нет» для каждого датчика и печать.
// Шаблон среднего уровня. Напиши код в разделе «Твоя часть» по целям в комментариях. 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);}
- Загрузи программу, открой монитор порта и подводи чёрную карточку под датчики по очереди.
- Ответь в журнале: что начнёт печатать программа, если в ней перепутать белое и чёрное местами? Почему?
Подсказка: Как измерить расстояние между числами
От 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). Тест проверяет восемь случаев для двух видов датчиков, в том числе показания за пределами твоих измерений.
// Проверка функции 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)
- Расстояние до чёрного — разница без знака.
- Расстояние до белого — так же.
- Вернуть результат сравнения двух расстояний.
Подсказка: Фрагмент кода (3 из 3)
abs(value - black) — расстояние до чёрного. Зачем здесь abs? Что будет без него у датчика, где чёрное — маленькое число?
Тест показывает «Все проверки пройдены». Твоя программа печатает «слева линия» и «справа линия», только когда под датчиком чёрное.
Придумай, как поворачивать
7 минРобот едет вперёд. Линия должна оставаться между датчиками. Что делать, если линия оказалась под левым датчиком? Значит, робот уехал вправо, и ему надо повернуть налево.
Самый простой способ повернуть налево — остановить левое колесо, а правое пусть едет.
- Поставь выключенного робота на линию. Сдвинь его так, чтобы линия оказалась под левым датчиком. Покажи, какое колесо нужно остановить.
- То же — для правого датчика.
- Заполни таблицу в журнале:
| Что видят датчики | Левое колесо | Правое колесо | Куда едет робот |
|---|---|---|---|
| Оба белое | вперёд | вперёд | прямо |
| Левый — линия | ? | ? | ? |
| Правый — линия | ? | ? | ? |
| Оба — линия | ? | ? | ? |
Таблица заполнена. Ты можешь объяснить, почему остановка одного колеса поворачивает робота.
Робот едет вперёд, и линия должна оставаться между датчиками. Если линия оказалась под одним датчиком, робот уехал в сторону. Колесо пока умеет только ехать или стоять. Как вернуть робота к линии?
- Поставь выключенного робота на линию. Сдвинь его так, чтобы линия оказалась под левым датчиком. Куда робот уехал? Куда ему повернуть? Какое колесо для этого остановить?
- То же — для правого датчика.
- Заполни таблицу в журнале:
| Что видят датчики | Левое колесо | Правое колесо | Куда едет робот |
|---|---|---|---|
| Оба белое | вперёд | вперёд | прямо |
| Левый — линия | ? | ? | ? |
| Правый — линия | ? | ? | ? |
| Оба — линия | ? | ? | ? |
- Объясни в журнале одной фразой, почему остановка одного колеса поворачивает робота.
Подсказка: Какое колесо останавливать
Вспомни урок 2.2: левое колесо стоит, правое едет. Вокруг какого колеса повернётся робот и в какую сторону?
Таблица заполнена, объяснение записано. Ты показал на выключенном роботе оба поворота.
Робот едет вперёд, и линия должна оставаться между датчиками. Составь правила сам: что делают колёса в каждом из четырёх случаев и почему.
| Что видят датчики | Левое колесо | Правое колесо | Куда едет робот | Почему |
|---|---|---|---|---|
| Оба белое | ||||
| Левый — линия | ||||
| Правый — линия | ||||
| Оба — линия |
- Проверь каждую строку на выключенном роботе: сдвинь его рукой и покажи, куда он должен поехать.
- «Оба белое» бывает в двух разных положениях робота. В каких? Может ли программа их различить? Запиши, что тогда случится с роботом.
- На прямой дорожке «оба — линия» почти не бывает. Где на итоговом поле это встретится и что тогда должен делать робот?
Подсказка: Два случая «оба белое»
Подумай, где может быть линия, когда ни один датчик её не видит.
Таблица из четырёх строк с объяснением. В журнале записано, в каком положении программа не знает, где линия.
Запиши правила в программу
10 минСоберём всё вместе: датчики, наше правило «ближе к чёрному» и моторы из урока 2.1.
- Создай скетч
L31_First_Line. Перенеси свои четыре измерения.
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);}
- Найди в
loop()два правила из твоей таблицы. Объясни напарнику, что делает каждое. - Нажми Проверить.
Как это устроено: Почему скорость задаётся заново в каждом повторе
В начале loop() обе скорости снова становятся равны speed. Если бы мы этого не делали, колесо, остановленное один раз, так бы и стояло, даже когда датчик снова увидит белое.
Как это устроено: Зачем строка while (true)
Если ты забыл вписать измерения, белое и чёрное равны нулю, и программа не может отличить линию. Тогда она «застревает» в пустом бесконечном цикле, и робот не поедет. Это защита от случайного старта.
Соберём всё вместе: датчики, правило «ближе к чёрному» и моторы из урока 2.1. Колёса будут слушаться правил из твоей таблицы.
- Создай скетч
L31_First_Lineпо шаблону. Впиши свои четыре измерения. Настройка моторов и защита уже готовы. - Напиши
loop()сам: перенеси свои условия изL31_Line_Detect, задай колёсам скорости по своей таблице правил и отправь их на моторы.
// Шаблон среднего уровня. Напиши код в разделе «Твоя часть» по целям в комментариях. 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()?
- Нажми Проверить.
Подсказка: Колесо остановилось и больше не едет
Когда правило снова разрешает колесу ехать? Откуда в каждом повторе берётся его скорость?
Подсказка: Правило колеса
if (проверка) { … } выполняет команды в скобках, только если проверка верна. Сверься со своей таблицей: какую переменную проверить и какую скорость поменять?
loop() написан, Проверить проходит без ошибок, ответ на прогноз записан в журнал.
Как это устроено: Зачем строка while (true)
Если ты забыл вписать измерения, белое и чёрное равны нулю, и программа не может отличить линию. Тогда она «застревает» в пустом бесконечном цикле, и робот не поедет. Это защита от случайного старта.
Возьми каркас: в нём контакты, моторы и функция drive из урока 2.1. Перенеси в него свои измерения и функцию onLine и сохрани как L31_First_Line. Эта программа понадобится тебе в уроке 3.2.
// Каркас движения по линии. Требования к программе — в тексте урока.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)
- Прочитать оба датчика.
- Спросить
onLineдля каждого. - Обе скорости —
speed. - Правила из таблицы меняют скорости.
- Отправить скорости в
drive.
Подсказка: Фрагмент кода (3 из 3)
if (onLine(leftValue, LEFT_WHITE, LEFT_BLACK)) { leftSpeed = 0; } — а для правого датчика? А строка «оба — линия» из твоей таблицы?
Программа выполняет требования 2 и 3, Проверить проходит без ошибок.
Проверь на подставке
6 минЗагружай программу при выключенном питании моторов. Колёса не касаются стола.
- Поставь робота на подставку. Загрузи программу.
- Включи питание моторов. Оба колеса должны крутиться вперёд.
- Подведи чёрную карточку под левый датчик: левое колесо остановится. Убери карточку: колесо снова поедет. Проверь правый датчик.
- Выключи питание моторов.
Каждое колесо останавливается, когда под его датчиком чёрное, и снова едет на белом.
| Что видишь | Что сделать |
|---|---|
| Колёса не крутятся | Проверь, что вписаны измерения и включено питание моторов. |
| Колесо крутится назад | Проверь FORWARD и подключение мотора. |
| Останавливается не то колесо | Перепутаны датчики или моторы. Проверь по монитору порта и по меткам Л/П. |
- Поставь робота на подставку, загрузи программу, включи питание моторов.
- Проверь все четыре строки своей таблицы правил: подставляй карточки так, чтобы датчики видели нужное. Записывай, что делают колёса.
| Что видят датчики | Ждал | Увидел |
|---|---|---|
| Оба белое | ||
| Левый — линия | ||
| Правый — линия | ||
| Оба — линия |
- Проверь свой прогноз про скорость, заданную в
setup(). Выключи питание моторов.
Таблица заполнена, все строки совпали с правилами. Прогноз проверен.
| Что видишь | Что сделать |
|---|---|
| Колёса не крутятся | Написан ли loop()? Вписаны ли измерения? Включено ли питание моторов? |
| Останавливается не то колесо | Перепутаны датчики или моторы. Проверь по монитору порта и по меткам Л/П. |
Приёмка требований 2–3 на подставке.
- Загрузи программу с нулями вместо измерений. Колёса не должны крутиться. Впиши измерения и загрузи снова.
- Проверь каждую строку своей таблицы правил карточками.
- Засеки, через сколько секунд после включения трогаются колёса.
| Проверка | Ждал | Увидел |
|---|---|---|
| Без измерений | ||
| Оба белое | ||
| Левый — линия | ||
| Правый — линия | ||
| Оба — линия | ||
| Пауза на старте |
Все проверки совпали с требованиями. Если нет — исправлено и проверено снова.
Первый заезд по линии
8 мин- Выключи питание моторов, отключи USB-кабель.
- Поставь робота на прямой участок: линия между датчиками.
- Включи питание и наблюдай. Будь готов выключить питание, если робот уходит с дорожки.
- Попробуй проехать изгиб. Запиши в журнал, что получилось.
Робот едет вдоль линии и проходит плавный изгиб. Он двигается рывками — это нормально для такого способа.
- Выключи питание моторов, отключи USB-кабель.
- Поставь робота на прямой участок: линия между датчиками. Включи питание. Будь готов выключить его, если робот уходит с дорожки.
- Пройди дорожку три раза при
speed = 100, потом три раза при 80 и при 130. Записывай, сколько раз робот прошёл изгиб.
speed | Прошёл из 3 | Что видно |
|---|---|---|
| 80 | ||
| 100 | ||
| 130 |
- Почему робот едет рывками? Запиши одной фразой.
Таблица заездов при трёх скоростях и объяснение рывков.
- Выключи питание моторов, отключи USB-кабель. Будь готов выключить питание, если робот уходит с дорожки.
- Найди наибольшую
speed, при которой робот проходит плавный изгиб три раза из трёх. Начни с 80 и поднимай шагами по 20. - На наибольшей скорости, которую робот не выдержал, посмотри, где он теряет линию. Объясни в журнале причину через твою таблицу правил.
- Запиши, что нужно изменить в правилах, чтобы робот держал линию быстрее. Проверишь это в уроке 3.2.
Наибольшая надёжная скорость, место и причина схода, идея улучшения правил.
Робот проходит прямую и плавный изгиб по двум правилам и не теряет линию.
Сейчас, если оба датчика видят чёрное, робот останавливается. На среднем поле через перекрёстки нужно проезжать прямо. Измени программу так, чтобы при чёрном под обоими датчиками оба колеса ехали вперёд. Проверь на дорожке с одним перекрёстком.
Вместо остановки колеса замедляй его: например, до трети от speed. Сравни на одном изгибе два варианта — «стоп» и «замедление». Какой плавнее? При какой скорости каждый вариант ещё держит линию? Запиши результаты в журнал: это первый шаг к П-регулятору, который понадобится на крутых дугах продвинутого поля.
Проверь себя
Выберите ответы для подготовки. Они не сохраняются и не оцениваются на сайте. Пройдите проверку знаний в программе курса или обсудите ответы с преподавателем.
● Основное
★ Средний уровень
★★ Продвинутый уровень
Отвечай на вопросы своего уровня. Нужно верных ответов: Начальный — 4 из 5; Средний — 4 из 5; Продвинутый — 4 из 5. Ответы проверит преподаватель или программа курса.
Итог
Робот научился сам выбирать, какое колесо остановить, по показаниям двух датчиков. Мы не угадывали числа, а измерили их на своём поле. Рывки при движении — цена простого правила «едет или стоит». В уроке 3.2 робот научится поворачивать плавно: тем сильнее, чем дальше линия ушла в сторону.
Вернись к своей догадке из начала урока. Подтвердилась ли она?
Как подтвердить результат
Для каждого опыта сохраняй условия, версию программы, наблюдение и вывод. Сравнивая попытки, меняй одну причину за раз; неудачные попытки тоже остаются в журнале. Отметка результата относится к уже выполненным действиям, повторять их ради отметки не нужно. Если результат не получен, запиши расхождение и следующую проверяемую гипотезу. Модель, измерения настоящего робота и объяснение — разные виды доказательств.
Словарь урока
| датчик линии | Датчик, который светит на пол и измеряет, сколько света отразилось. Белое отражает много, чёрное — мало. |
| показание | Число, которое программа получает от датчика. У нашей платы это число от 0 до 1023. |
| монитор порта | Панель Serial Monitor в MyRobotica, где видны сообщения, которые плата отправляет на компьютер. |
| калибровка | Настройка программы по измерениям своего робота и своего поля. |
| переменная | Именованное место в памяти, где программа хранит число. |
| условие | Проверка if: команды в фигурных скобках выполняются, только если проверка верна. |
| телеметрия | Передача именованных измерений с временем и единицами, чтобы строить графики и сохранять наблюдения. |
| АЦП | Аналого-цифровое преобразование: напряжение на входе платы превращается в число от 0 до 1023. |
П-регулятор: плавно и быстро
Робот поворачивает тем сильнее, чем дальше ушла линия, и проходит дуги без рывков.
Результат урока
Робот плавно едет по линии с П-регулятором и проходит дугу своего уровня. Коэффициент и скорость подобраны и записаны в журнал.
Ты научишься
- Перевести показания двух датчиков в общую шкалу от 0 до 100.
- Посчитать ошибку как разницу показаний и объяснить её знак.
- Объяснить, как поправка меняет скорости колёс.
- Подобрать коэффициент и скорость по наблюдениям на дорожке.
Нужно знать
- 3.1 Датчики линии и первое следование
- 2.1 Моторы: едем и останавливаемся
Понадобится
Зачем этот урок
В уроке 3.1 робот ехал по линии рывками: колесо либо крутилось, либо стояло. На крутых дугах итоговой задачи так не проехать. Сегодня робот научится поворачивать чуть-чуть, когда линия ушла немного, и сильно, когда ушла далеко. Так работают автопилоты и роботы-курьеры.
- Как программа из урока 3.1 решала, что под датчиком линия?
- Почему робот в уроке 3.1 ехал рывками?
- Что делает функция
drive()?
Плавно — это как?
8 минЧто должен делать робот. Роботу нужно двигаться по линии плавнее: при маленьком отклонении чуть поправлять курс, а при большом — сильнее. Используем те же датчики отражения и моторы, добавив П-регулятор — правило расчёта поправки.
Что используем и как это работает. Сначала переводим измерения каждого датчика в общую шкалу по его белому и чёрному. Разность уровней называется ошибкой. Коэффициент kp умножает ошибку: чем больше отклонение, тем больше поправка. Одному колесу прибавляем её к базовой команде, у другого вычитаем. Новое измерение позволяет снова скорректировать движение — это обратная связь.
Как связать измерение с действием. Слишком малая поправка не успевает вернуть робота, слишком большая вызывает колебания. Меняем один параметр за раз и сравниваем траекторию и показания на одинаковой трассе. Нулевая ошибка означает равные показания, но не всегда правильное положение: оба датчика могут потерять линию.
Представь, что ты ведёшь велосипед по линии. Если линия ушла чуть влево, ты чуть поворачиваешь руль. Если сильно — поворачиваешь сильнее. Ты не бросаешь руль из крайнего положения в крайнее.
- Встань с напарником: один — «робот», второй держит линию (шнур или полоску бумаги) и плавно смещает её влево и вправо.
- «Робот» поворачивает корпус тем сильнее, чем дальше линия от середины.
Если ты один: положи на стол изогнутый шнур и веди вдоль него ладонью — это «робот». Поворачивай ладонь тем сильнее, чем дальше шнур ушёл от её середины.
Ты можешь объяснить: сила поворота зависит от того, насколько далеко ушла линия.
Представь, что ты ведёшь велосипед по линии: чуть ушла — чуть поворачиваешь руль, сильно ушла — сильнее. Программа из урока 3.1 так не умеет: она знает только «линия есть» или «нет».
- Запусти программу из урока 3.1 на дуге 350 мм и посмотри, как едет робот.
- Запиши в журнал одной фразой: чего не хватает программе, чтобы поворачивать плавно?
Ты можешь объяснить: сила поворота должна зависеть от того, насколько далеко ушла линия.
Исследуй проблему, прежде чем решать.
- Запусти свою программу из урока 3.1 на дуге 350 мм, потом на дуге 125 мм. Запиши, как едет робот: рывки, виляние, сход с линии.
- Объясни в журнале, какую информацию с датчиков программа 3.1 выбрасывает и почему из-за этого робот едет рывками.
- До того как читать дальше, придумай своё правило: как скорости колёс должны зависеть от показаний датчиков. Запиши словами или формулой. В конце урока сравни его с П-регулятором.
Подсказка: Если не знаешь, с чего начать
Программа 3.1 сравнивает показание с порогом и получает «да или нет». А что, если использовать само число?
В журнале — наблюдение на двух дугах, объяснение причины рывков и твоё правило управления.
Общая шкала для двух датчиков
15 минУ каждого датчика свои числа белого и чёрного. Чтобы сравнивать их, переведём оба в одну шкалу: чёрное — 0, белое — 100, край линии — где-то посередине.
- Создай скетч
L32_Error. Перенеси из журнала свои измерения белого и чёрного.
// Наблюдение в 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); }}
- Загрузи, открой монитор порта. Проверь: на белом оба числа около 100, на чёрном — около 0.
- Медленно проведи край линии под левым датчиком. Как меняется число?
Оба датчика показывают около 100 на белом и около 0 на чёрном, на краю линии — промежуточные числа.
Как это устроено: Как работает функция level
value - black — насколько показание отличается от чёрного. white - black — весь путь от чёрного до белого. Их отношение, умноженное на 100, — «сколько процентов пути до белого». Формула работает, даже если у датчика чёрное — большое число, а белое — маленькое. constrain(…, 0, 100) обрезает случайные выбросы за границы шкалы.
У датчиков разные числа белого и чёрного, поэтому сравнивать их напрямую нельзя. Нужна общая шкала: чёрное — 0, белое — 100. Показание на шкале — это «сколько процентов пути от чёрного до белого» оно прошло.
- Создай скетч
L32_Errorпо шаблону, перенеси измерения из журнала. - Напиши функцию
levelсам. Ошибку вloop()пока не трогай.
// Наблюдение в 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? Посчитай до запуска.
- Загрузи и проверь на белом, чёрном и краю линии.
- Подними робота на 3–4 см над столом: датчику почти нечего отразить. Какое «сырое» число получается, что показывает шкала и какая часть функции за это отвечает?
- Ответь в журнале: какого типа число ты взял для расчёта и почему не обычный
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?
Возьми каркас: в нём только контакты, управление моторами и защита. Всё остальное напишешь сам.
// Наблюдение в 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, перенеси функцию в свою программу и проверь шкалу обоих датчиков на дорожке.
// Проверка функции 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)
- Насколько показание ушло от чёрного?
- Раздели на длину всего пути и умножь на 100.
- Обрежь результат границами шкалы.
Подсказка: Фрагмент кода (3 из 3)
long result = (long)(value - black) * 100 / (white - black); — почему здесь long и почему умножение стоит раньше деления?
Тест показывает «Все проверки пройдены». На дорожке шкала показывает около 0 на чёрном и около 100 на белом.
Считаем ошибку
15 минОшибка — разница показаний: error = leftLevel - rightLevel. Когда линия ровно посередине, оба датчика видят одинаково и ошибка равна нулю.
Линия ушла под левый датчик. Какой знак будет у ошибки — плюс или минус? А если под правый?
- Посмотри на строку с ошибкой в мониторе порта.
- Подвинь линию под левый датчик, потом под правый. Запиши в журнал знак и примерные числа.
| Где линия | leftLevel | rightLevel | Ошибка |
|---|---|---|---|
| Посередине | |||
| Под левым | |||
| Под правым |
Линия слева — ошибка отрицательная, справа — положительная, посередине — около нуля.
Ошибка показывает, насколько и в какую сторону линия ушла от середины. Считаем её как разницу: левое показание минус правое.
- Напиши расчёт ошибки в
loop()сам и загрузи скетч.
Линия ушла под левый датчик. Какой знак будет у ошибки? Почему?
- Проверь прогноз: подвинь линию под левый датчик, потом под правый. Запиши в журнал.
| Где линия | leftLevel | rightLevel | Ошибка |
|---|---|---|---|
| Посередине | |||
| Под левым | |||
| Под правым |
- Ответь в журнале: в какую сторону должен повернуть робот, если ошибка отрицательная?
Подсказка: Знак ошибки
Чёрная линия — 0 на шкале. Если линия под левым датчиком, какое из двух чисел меньше?
Знаки в таблице совпали с прогнозом, и ты можешь объяснить, куда поворачивать при каждом знаке.
Требование 2. Программа считает ошибку — число со знаком: насколько и в какую сторону линия ушла от середины. Знак выбираешь ты, но потом поправка должна поворачивать робота к линии.
- Добавь расчёт ошибки и временный вывод
leftLevel,rightLevel, ошибки в монитор порта. - Заполни таблицу и запиши в журнал своё правило знака одной фразой.
| Где линия | leftLevel | rightLevel | Ошибка |
|---|---|---|---|
| Посередине | |||
| Под левым | |||
| Под правым |
- Разберись с краем: линия ушла за пределы обоих датчиков. Что покажет ошибка и что тогда сделает робот? Запиши, чем это опасно на крутой дуге.
Подсказка: Идея (1 из 2)
Если линия ровно посередине, оба датчика видят одно и то же — ошибка должна быть ноль.
Подсказка: Проверка знака (2 из 2)
Выбери: left - right или right - left. Запиши, какое колесо должно ускориться, когда линия слева, — это понадобится в требовании 3.
Таблица заполнена, правило знака записано. Ты знаешь, что видит регулятор, когда линия ушла за оба датчика.
От ошибки к скоростям
10 минПоправка — ошибка, умноженная на коэффициент: correction = kp * error. К левому колесу поправку прибавляем, у правого — отнимаем.
- Создай скетч
L32_P_Line, перенеси измерения.
// Наблюдение в 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); }}
- Посчитай на бумаге:
baseSpeed = 100,kp = 0.5, ошибка −40. Какие скорости получат колёса?
Ты посчитал: поправка −20, левое колесо 80, правое 120 — робот поворачивает налево, к линии.
Как это устроено: Зачем constrain
При большой ошибке скорость может получиться −30 или 300. Мотор понимает только 0…255. constrain(число, 0, 255) ставит границы: меньше нуля станет 0, больше 255 — 255.
Правило П-регулятора: поправка пропорциональна ошибке. Чем дальше ушла линия, тем сильнее одно колесо ускоряется, а другое замедляется. Насколько сильно — задаёт kp.
- Создай скетч
L32_P_Lineпо шаблону, перенеси измерения и свою функциюlevel. - Напиши
loop()сам: ошибка, поправка, скорости колёс в пределах 0…255 и команда моторам.
// Наблюдение в 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); }}
- Посчитай на бумаге:
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 остаётся).
Приёмка на бумаге. Посчитай скорости колёс для своей программы:
baseSpeed | kp | Ошибка | Левое | Правое |
|---|---|---|---|---|
| 100 | 0.5 | −40 | ||
| 200 | 0.8 | 60 | ||
| 200 | 1.0 | 100 |
В последней строке одно колесо упирается в границу. Насколько слабее робот повернёт, чем «хотел»? Запиши в журнал, что можно с этим сделать.
Подсказка: Идея (1 из 3)
Одно колесо получает базовую скорость плюс поправку, другое — минус поправку.
Подсказка: План (2 из 3)
- Ошибка из требования 2.
- Поправка = коэффициент × ошибка.
- Левое и правое колесо: база ± поправка, каждое — в границах 0…255.
- Отправить скорости на моторы.
Подсказка: Фрагмент кода (3 из 3)
int leftSpeed = constrain(baseSpeed + correction, 0, 255); — а что для правого? Проверь знак по своему правилу из требования 2.
Программа выполняет требование 3, таблица посчитана, ограничение записано в журнал.
Проверь на подставке
7 мин- Поставь робота на подставку, загрузи программу, включи питание моторов.
- Подведи линию под левый датчик: левое колесо замедлится, правое ускорится.
- Подведи под правый — наоборот. Линия посередине — колёса крутятся одинаково.
Колёса меняют скорость плавно, а не «стоп — едет».
| Что видишь | Что сделать |
|---|---|
| Колёса не крутятся | Вписаны ли измерения? Включено ли питание моторов? |
| Ускоряется не то колесо | Проверь знак ошибки по монитору порта и формулы скоростей |
- Поставь робота на подставку, загрузи программу, включи питание моторов.
- Подведи линию под левый датчик, потом под правый, потом посередине. Запиши, какое колесо ускоряется и какое замедляется.
- Проверь свой прогноз про
kp = 0: поставьkp = 0, загрузи и подвигай линию. Что делают колёса? Верни свойkp.
| Где линия | Левое колесо | Правое колесо | Совпало с прогнозом? |
|---|---|---|---|
| Под левым | |||
| Под правым | |||
| Посередине |
Колёса меняют скорость плавно. Таблица заполнена, прогноз про kp = 0 проверен.
| Что видишь | Что сделать |
|---|---|
| Колёса не крутятся | Написан ли loop() с командой моторам? Вписаны ли измерения? |
| Ускоряется не то колесо | Проверь знак ошибки по монитору порта и формулы скоростей |
Приёмка требования 3 на подставке.
- Временно выведи в монитор порта ошибку, поправку и обе скорости колёс.
- Подводи линию под левый датчик, правый и посередине. Совпадает ли направление поправки с твоим правилом знака из требования 2?
- Найди положение линии, при котором одно колесо упирается в 0 или 255. Сравни скорости в мониторе с тем, что ты посчитал в таблице требования 3.
- Отключи измерения (нули) — колёса стоят? Верни измерения.
Направление поправки, упор в границу и защита проверены по монитору порта. Расхождения с таблицей объяснены.
Подбери коэффициент
18 минКоэффициент kp подбирают на дорожке. Меняй одно число за раз и смотри, что изменилось.
- Поставь робота на дорожку с дугой. Попробуй
kp= 0.2, 0.5, 0.8 при одной скорости. - Запиши в журнал, как робот ведёт себя при каждом значении.
- Выбери лучший
kpи попробуй увеличитьbaseSpeed.
kp | baseSpeed | Что видно |
|---|---|---|
| 0.2 | 100 | |
| 0.5 | 100 | |
| 0.8 | 100 |
Как это устроено: Как понять, куда менять kp
- Робот плавно уходит с дуги наружу — поправка слишком слабая, увеличь
kp. - Робот виляет из стороны в сторону даже на прямой — поправка слишком сильная, уменьши
kp. - Хороший
kp— робот держится линии на дуге и почти не виляет на прямой.
Подобраны kp и baseSpeed, при которых робот проходит дугу. Числа записаны в журнал.
Подбери kp сам. Правило одно: меняй одно число за раз.
- Начни с
kp = 0.5приbaseSpeed = 100. Затем попробуй одно явно меньшее и одно явно большее значение и сузь диапазон. - Запиши в журнал не меньше четырёх попыток.
kp | baseSpeed | Что видно | Что поменять дальше и почему |
|---|---|---|---|
| 0.5 | 100 | ||
| 100 | |||
| 100 | |||
| 100 |
- С лучшим
kpподнимиbaseSpeed. Что пришлось изменить, чтобы робот снова держал дугу?
Подсказка: Как понять, куда менять kp
Робот уходит с дуги наружу — поправка слабая. Робот виляет даже на прямой — поправка сильная.
В журнале четыре попытки с объяснением каждого шага и пара kp — baseSpeed, на которой робот проходит дугу.
Подбери kp и скорость методом, а не наугад, и докажи выбор записями.
- При
baseSpeed = 100начни сkp = 0.1и удваивай: 0.1, 0.2, 0.4, 0.8… Найди наименьшийkp, при котором робот начинает вилять на прямой. - Рабочий
kpвозьми примерно вдвое меньше. Проверь его на дуге своего уровня. - Поднимай
baseSpeedшагами по 20. Для каждой скорости — три заезда. Запомни последнюю скорость, на которой робот прошёл все три. - Запиши в журнал таблицу и вывод: почему именно эта пара, что ограничивает скорость.
- Вернись к правилу, которое ты придумал в первом шаге. Чем оно похоже на П-регулятор и чем отличается? Что бы ты в нём изменил теперь?
kp | baseSpeed | Заезды (из 3) | Что видно |
|---|---|---|---|
Подсказка: Робот виляет при любом kp
Проверь, не упирается ли колесо в 255 или 0 на дуге — вспомни последнюю строку таблицы из требования 3.
Таблица с методом подбора, пара kp — baseSpeed, проверенная тремя заездами, вывод с обоснованием и сравнение своего правила с П-регулятором.
Задания по уровням
10 минПройти дугу радиусом 350 мм, как на начальном поле итоговой задачи, не теряя линию.
Пройти дугу радиусом 250 мм, как на среднем поле, на скорости не ниже той, что подобрана для прямой. Если не получается — что важнее увеличить: kp или уменьшить скорость?
Пройти две дуги радиусом 125 мм подряд в разные стороны, как на продвинутом поле. Начни с пары kp — скорость, выбранной в шаге подбора. Если она не удерживает S-связку, измени один параметр и дополни существующую таблицу результатом; заново переписывать удачные и неудачные попытки не нужно.
Проверь себя
Выберите ответы для подготовки. Они не сохраняются и не оцениваются на сайте. Пройдите проверку знаний в программе курса или обсудите ответы с преподавателем.
● Основное
★ Средний уровень
★★ Продвинутый уровень
Отвечай на вопросы своего уровня. Нужно верных ответов: Начальный — 4 из 5; Средний — 4 из 5; Продвинутый — 4 из 5. Ответы проверит преподаватель или программа курса.
Итог
Робот поворачивает пропорционально ошибке — это П-регулятор. Он плавнее правила «стоп — едет» и проходит крутые дуги. Дальше робот научится находить перекрёстки на линии.
Как подтвердить результат
Для каждого опыта сохраняй условия, версию программы, наблюдение и вывод. Сравнивая попытки, меняй одну причину за раз; неудачные попытки тоже остаются в журнале. Отметка результата относится к уже выполненным действиям, повторять их ради отметки не нужно. Если результат не получен, запиши расхождение и следующую проверяемую гипотезу. Модель, измерения настоящего робота и объяснение — разные виды доказательств.
Словарь урока
| ошибка | Насколько линия ушла от середины: разница показаний левого и правого датчиков. |
| поправка | На сколько изменить скорости колёс, чтобы вернуться к линии. |
| коэффициент | Число kp: во сколько раз поправка больше или меньше ошибки. |
| П-регулятор | Правило «поправка пропорциональна ошибке»: больше ушли — сильнее поворачиваем. |
| constrain | Команда, которая не даёт числу выйти за границы, например за 0…255. |
Находим и считаем перекрёстки
Робот замечает перекрёсток, останавливается на нём и не считает один перекрёсток дважды.
Результат урока
Робот едет по линии с П-регулятором и останавливается на нужном по счёту перекрёстке. Каждый перекрёсток считается один раз.
Ты научишься
- Описать, что показывают датчики на перекрёстке.
- Записать условие перекрёстка и объяснить выбор порога.
- Объяснить, почему один перекрёсток может посчитаться много раз, и исправить это.
- Остановить робота на перекрёстке с заданным номером.
Нужно знать
- 3.2 П-регулятор: плавно и быстро
Понадобится
Зачем этот урок
На полях итоговой задачи к грузам ведут ответвления от перекрёстков. Чтобы свернуть в нужное место, робот должен замечать перекрёстки и знать, какой по счёту перед ним. Сегодня робот научится находить, считать и проезжать перекрёстки.
- Чему равна ошибка П-регулятора, когда линия ровно посередине?
- Что показывает датчик по шкале 0–100 на чёрном?
- Что делает
constrain?
Что видят датчики на перекрёстке
12 минЧто должен делать робот. Курьер должен заметить перекрёсток, доехать до него и при необходимости остановиться. Отдельный датчик перекрёстка не нужен: используем оба датчика линии и их показания вместе.
Что используем и как это работает. На обычной узкой дорожке датчики находятся по её краям. На широкой поперечной полосе оба могут одновременно увидеть чёрное. Программа проверяет это сочетание и решает, продолжать ли движение. Пока датчики остаются над одной полосой, многократные чтения относятся к одному перекрёстку, а не к нескольким.
Как связать измерение с действием. Сначала проверим признаки на неподвижном роботе, затем въезд и выезд с полосы. Для счёта нужно отделить обнаружение перекрёстка от проезда за его границу. Иначе быстрый цикл насчитает одну и ту же полосу много раз. Другая широкая тёмная область тоже может дать такой сигнал: датчики не знают географию поля.
Робот подъезжает к перекрёстку. Что покажут оба датчика по шкале 0–100, когда он окажется на поперечной линии?
- Загрузи скетч
L32_Errorиз урока 3.2. - Медленно провези робота руками по тренировочной дорожке через перекрёсток. Смотри на монитор порта.
- Запиши в журнал показания: на линии посередине, на перекрёстке, после перекрёстка.
На перекрёстке оба датчика показывают почти 0: под ними чёрная поперечная линия.
Робот подъезжает к перекрёстку. Что покажут оба датчика по шкале 0–100, когда он окажется на поперечной линии? А на изгибе линии?
- Загрузи скетч со шкалой из урока 3.2 (
L32_Errorили свой шаблон). - Медленно провези робота руками по дорожке через изгиб и через перекрёсток. Смотри на монитор порта.
- Заполни таблицу и ответь: по какому признаку перекрёсток можно отличить от изгиба?
| Положение | leftLevel | rightLevel |
|---|---|---|
| Линия посередине | ||
| На изгибе | ||
| На перекрёстке | ||
| После перекрёстка |
Таблица заполнена, признак перекрёстка записан словами.
Сколько показаний робот успевает получить, пока едет по поперечной линии?
- Добавь в свою программу из урока 3.2 счётчик повторов
loop()и раз в секунду печатай, сколько повторов прошло. Запиши число повторов в секунду на рабочей скорости. - Возьми свою скорость в мм/с из урока 2.1 или измерь её заново.
- Посчитай: сколько повторов придётся на поперечную линию шириной 19 мм?
- Провези робота руками через перекрёсток и сравни показания со своей таблицей положений.
Подсказка: Как посчитать
Время на линии — ширина линии, делённая на скорость. Повторов — это время, умноженное на число повторов в секунду.
В журнале — повторы в секунду, скорость, расчёт числа повторов на линии и показания на перекрёстке.
Условие перекрёстка
10 минЗапишем правило: перекрёсток — когда оба датчика видят чёрное. «Чёрное» — показание меньше порога 30.
bool onCrossing() {
return leftLevel < 30 && rightLevel < 30;
}
&& читается как «и»: условие верно, только если верны обе части.
Подсказка: Почему 30, а не 0
Даже на чёрном показание редко ровно 0, а при движении робот видит край линии. Порог 30 надёжно отделяет «чёрное» от «края», но его можно подобрать по своим измерениям.
Ты можешь объяснить, почему на изгибе линии условие не срабатывает, а на перекрёстке — срабатывает.
Сформулируй правило сам по своим записям из прошлого шага: какие показания бывают только на перекрёстке и никогда — на прямой или изгибе?
- Запиши правило словами в журнал: что должно быть у левого датчика и что у правого.
- Выбери порог «чёрного» по своим измерениям. Объясни, почему не 0 и почему не 50.
- Запиши условие на языке Arduino. Понадобится знак «и»:
&&.
Кто-то написал условие через || («или»). Где робот тогда остановится по ошибке?
Подсказка: Как записать «и»
a < 30 && b < 30 верно, только если верны обе части.
В журнале — правило словами, порог с объяснением, условие и ответ про ||.
Требование 1. Функция onCrossing() отвечает true на поперечной линии и false во всех остальных положениях робота.
Проверь условие на бумаге, прежде чем писать код. Для каждого положения запиши показания левого и правого датчика по шкале 0–100 и ответ функции:
| Положение | leftLevel | rightLevel | onCrossing() |
|---|---|---|---|
| Линия посередине | |||
| Линия под одним датчиком на крутой дуге 125 мм | |||
| Линия ушла за оба датчика | |||
| Поперечная линия |
- Выбери порог по своим измерениям и обоснуй его: какой запас до самого тёмного показания вне перекрёстка?
- Найди положение, в котором твоё условие может ошибиться. Что тогда сделает робот?
Подсказка: Где искать ошибку
Провези робота руками по дуге 125 мм из урока 3.2: насколько тёмным становится «край» линии под датчиком?
Таблица заполнена по измерениям, порог обоснован запасом, опасное положение и его последствие записаны.
Стоп на перекрёстке
20 мин- Создай скетч
L41_Crossing_Stop. Перенеси измерения,kpиbaseSpeedиз журнала.
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() {}
- Найди функцию
followLineToCrossing(). Объясни напарнику, как из неё выходит программа. - Поставь робота на линию перед перекрёстком и запусти.
Робот едет по линии и останавливается на первом перекрёстке.
Как это устроено: Как работает followLineToCrossing
while (true) — повторять всегда. Внутри: прочитать датчики; если перекрёсток — return, то есть выйти из функции; иначе сделать шаг П-регулятора. После выхода программа продолжается со строки stopMotors(); в setup().
| Что видишь | Что сделать |
|---|---|
| Робот проезжает перекрёсток | Снизь baseSpeed или подними порог до 40 |
| Робот останавливается на изгибе | Порог слишком высокий, попробуй 20 |
| Робот не едет | Вписаны ли измерения из журнала? |
- Создай скетч
L41_Crossing_Stopпо шаблону. Перенеси измерения,kpиbaseSpeedиз журнала. Функции линии из урока 3.2 уже готовы. - Напиши сам
onCrossing()по своему правилу из прошлого шага,followLineToCrossing()и движение вsetup(). ПокаonCrossing()не написана, она всегда отвечает «перекрёсток», и робот не трогается.
// Шаблон среднего уровня. Напиши код в разделе «Твоя часть» по целям в комментариях. 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() {}
Что сделает робот, если после поиска перекрёстка забыть остановить моторы?
- Поставь робота на линию перед перекрёстком и запусти.
Подсказка: Ехать, пока…
Функция повторяет одно и то же — прочитать датчики, сделать шаг по линии — и заканчивается, когда найден перекрёсток. Какой цикл повторяет «всегда», и как выйти из функции посреди него?
Подсказка: Как выйти из функции
return внутри функции сразу заканчивает её. Программа продолжается со следующей строки в setup().
Робот едет по линии и останавливается на первом перекрёстке. Прогноз записан.
| Что видишь | Что сделать |
|---|---|
| Робот проезжает перекрёсток | Снизь baseSpeed или подними порог |
| Робот останавливается на изгибе | Порог слишком высокий |
| Робот не едет | Вписаны ли измерения? Написана ли onCrossing()? |
Требование 2. Продолжи свою программу L32_P_Line из урока 3.2 и сохрани как L41_Crossing_Stop:
- шаг П-регулятора вынесен в функцию
followLineStep(), чтение датчиков — вreadSensors(); П-регулятор при этом работает как раньше; - функция
followLineToCrossing()ведёт робота по линии и заканчивается, как толькоonCrossing()вернулtrue; - в
setup()робот доезжает до первого перекрёстка и останавливается; - защита без измерений и пауза на старте остаются.
Приёмка. Робот останавливается на перекрёстке пять раз из пяти и ни разу на дуге своего уровня.
Подсказка: Идея (1 из 3)
Функция «едет, пока…» — это бесконечный цикл, из которого есть выход по условию.
Подсказка: План (2 из 3)
- Повторять всегда: прочитать датчики.
- Перекрёсток — выйти из функции.
- Иначе — шаг П-регулятора.
Подсказка: Фрагмент кода (3 из 3)
while (true) { readSensors(); if (onCrossing()) { return; } … }
Программа выполняет требования 1–2, пять остановок из пяти записаны в журнал.
Найди ошибку счёта
8 минСоседи хотели считать перекрёстки так:
void loop() {
readSensors();
if (onCrossing()) {
count = count + 1;
}
followLineStep();
}
Робот проехал один перекрёсток, а count стал 40. Почему?
Ты можешь объяснить: пока робот едет по широкой чёрной полосе, условие верно много раз подряд, и каждый раз счётчик растёт.
Соседи заметили двойной счёт и добавили паузу после счёта:
void loop() {
readSensors();
if (onCrossing()) {
count = count + 1;
delay(100);
}
followLineStep();
}
Робот проехал один перекрёсток, а count стал 3. Почему пауза не помогла? Чем она ещё опасна для езды по линии?
- Запиши ответ в журнал.
- Предложи исправление словами: что должна сделать программа после того, как нашла перекрёсток, прежде чем искать следующий?
Ты можешь объяснить, почему пауза не помогает, и предложить исправление.
Соседи решили считать только момент, когда перекрёсток появился:
bool was = false;
void loop() {
readSensors();
bool now = onCrossing();
if (now && !was) {
count = count + 1;
}
followLineStep();
}
Робот проехал один перекрёсток, а count всё равно стал 40. Где ошибка?
- Найди ошибку и запиши исправленный код в журнал.
- Сравни 40 со своим расчётом числа повторов на линии из прошлого шага. Похоже?
- Чем счёт по моменту появления хуже или лучше проезда перекрёстка, который будет в следующем шаге?
Подсказка: Что хранит was
Когда в этой программе меняется значение was?
Ошибка найдена, исправление записано кодом, число 40 объяснено расчётом, сравнение двух способов записано.
Проехать перекрёсток
10 минЧтобы посчитать перекрёсток один раз, его нужно проехать: ехать прямо, пока датчики не съедут с чёрного.
void passCrossing() {
drive(FORWARD, baseSpeed, FORWARD, baseSpeed);
do {
readSensors();
} while (onCrossing());
delay(100);
}
do { … } while (условие) — сделать и повторять, пока условие верно. Короткая пауза в конце уводит датчики подальше от края поперечной линии.
Ты можешь объяснить, когда закончится цикл в passCrossing().
Чтобы посчитать перекрёсток один раз, его нужно проехать: ехать прямо, пока датчики не съедут с чёрного. Напиши функцию passCrossing() в журнале:
- Включи оба мотора вперёд со скоростью
baseSpeed. - Читай датчики, пока робот на перекрёстке.
- После этого подожди 100 мс.
do { … } while (условие); — сделать и повторять, пока условие верно.
Подсказка: Почему do, а не while
Датчики нужно прочитать хотя бы один раз, прежде чем проверять условие.
Подсказка: Зачем пауза в конце
Сразу после чёрного датчики видят край поперечной линии. Что будет, если в этот момент снова искать перекрёсток?
Функция записана в журнал. Ты можешь объяснить, когда закончится цикл и зачем пауза.
Требование 3. Функция passCrossing() проезжает перекрёсток прямо и заканчивается, когда датчики уже не на поперечной линии и не на её краю.
- Напиши функцию в своей программе.
- Исследуй паузу после схода с чёрного: 0, 50, 100, 200 мс. Для каждой запусти робота через два перекрёстка подряд (в
setup()—followLineToCrossing(),passCrossing(),followLineToCrossing(), стоп) и отметь, на каком перекрёстке он остановился. - Запиши, какая пауза нужна и от чего она зависит. Что будет, если поднять
baseSpeed?
| Пауза, мс | Где остановился | Что видно |
|---|---|---|
| 0 | ||
| 50 | ||
| 100 | ||
| 200 |
Подсказка: Идея (1 из 2)
Ехать прямо и повторять чтение датчиков, пока условие перекрёстка верно.
Подсказка: Фрагмент кода (2 из 2)
do { readSensors(); } while (onCrossing()); — почему здесь do, а не while?
passCrossing() работает, таблица паузы заполнена, вывод о связи паузы и скорости записан.
Стоп на нужном перекрёстке
17 мин- Создай скетч
L41_Count. ВTARGETукажи, на каком перекрёстке остановиться.
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? Что робот сделает на первом перекрёстке?
- Проверь на дорожке с тремя перекрёстками:
TARGET= 1, 2, 3.
Робот останавливается ровно на перекрёстке с номером TARGET.
- Создай скетч
L41_Countпо шаблону. ВTARGETукажи, на каком перекрёстке остановиться. - Напиши сам
passCrossing()— по своей записи в журнале — и движение в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;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. Сколько раз робот должен проехать перекрёсток прямо?
- Проверь на дорожке с тремя перекрёстками:
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 выбрано и проверено.
Задания по уровням
10 минДоехать по линии до первого перекрёстка и остановиться на нём.
Остановиться на третьем перекрёстке, проехав два первых прямо. На среднем поле итоговой задачи робот проезжает перекрёстки, не сворачивая.
Сделай так, чтобы робот проезжал все перекрёстки без остановки и не терял линию после них. Подсказка: в loop() — если перекрёсток, passCrossing(), иначе шаг П-регулятора.
Проверь себя
Выберите ответы для подготовки. Они не сохраняются и не оцениваются на сайте. Пройдите проверку знаний в программе курса или обсудите ответы с преподавателем.
● Основное
★ Средний уровень
★★ Продвинутый уровень
Отвечай на вопросы своего уровня. Нужно верных ответов: Начальный — 4 из 5; Средний — 4 из 5; Продвинутый — 4 из 5. Ответы проверит преподаватель или программа курса.
Итог
Робот находит перекрёстки и считает их без повторов. Следующий шаг — сворачивать на перекрёстке в нужную сторону и проходить маршрут.
Как подтвердить результат
Для каждого опыта сохраняй условия, версию программы, наблюдение и вывод. Сравнивая попытки, меняй одну причину за раз; неудачные попытки тоже остаются в журнале. Отметка результата относится к уже выполненным действиям, повторять их ради отметки не нужно. Если результат не получен, запиши расхождение и следующую проверяемую гипотезу. Модель, измерения настоящего робота и объяснение — разные виды доказательств.
Словарь урока
| перекрёсток | Место, где линию пересекает другая линия. Оба датчика одновременно видят чёрное. |
| порог | Число для сравнения: показание меньше 30 по шкале 0–100 считаем чёрным. |
| цикл while | Повторяет команды, пока условие верно. |
| return | Немедленно завершает функцию и возвращает программу туда, откуда функцию вызвали. |
Повороты на перекрёстке и маршрут
Робот сворачивает с перекрёстка на нужную линию и проходит маршрут из нескольких решений.
Результат урока
Робот поворачивает с перекрёстка налево или направо до новой линии, разворачивается на линии и проходит маршрут из нескольких решений, записанных в программе по порядку.
Ты научишься
- Объяснить, почему поворот по датчикам надёжнее поворота по времени.
- Подобрать выезд на центр перекрёстка, чтобы после поворота робот оказался на линии.
- Записать маршрут последовательностью решений «налево / направо / прямо».
- Проверить маршрут на поле и найти, на каком решении он ломается.
Нужно знать
- 4.1 Находим и считаем перекрёстки
- 2.2 Повороты и развороты
Понадобится
Зачем этот урок
Робот находит перекрёсток. Теперь он должен свернуть — к грузу, к месту доставки или дальше по дороге. В уроке 2.2 повороты были по времени и зависели от батареи. Сегодня робот будет поворачивать по датчикам: крутиться, пока не найдёт новую линию.
- Почему поворот по времени получается разным при разряженной батарее?
- Что делает
passCrossing()? - Как робот понимает, что под ним перекрёсток?
Как повернуть точно
8 минЧто должен делать робот. Роботу нужно пройти несколько перекрёстков и выбрать нужное направление на каждом. Используем датчики линии для обнаружения событий и заранее записанный план маршрута для выбора действия.
Что используем и как это работает. Датчики отвечают на вопрос «что сейчас под роботом», а маршрут — «что делать следующим». Программа доезжает до перекрёстка, выполняет поворот или проезд прямо, выходит с поперечной полосы и переходит к следующему пункту. Массив или последовательность вызовов хранит порядок, но не измеряет положение робота.
Как связать измерение с действием. Проверяем маршрут по участкам: отмечаем, какой перекрёсток найден и какое действие выполнено. Если робот не обнаружил полосу или повернул не туда, следующие команды будут выполняться уже в другом месте. Поэтому сначала проверим каждую функцию, затем соберём целый маршрут.
Робот стоит на перекрёстке и крутится налево. Как он может сам понять, что повернул достаточно и уже смотрит вдоль новой линии?
- Обсудите с напарником. Поставьте робота на перекрёсток и медленно поверните руками.
- Следите за левым датчиком: когда он окажется над новой линией?
Ты можешь объяснить идею: крутиться, пока датчик со стороны поворота не увидит новую линию.
Робот стоит на перекрёстке и крутится налево. Как он может сам понять, что повернул достаточно и уже смотрит вдоль новой линии?
- Поставь робота на центр перекрёстка и медленно поверни руками налево, потом направо.
- Для каждого поворота запиши: какой датчик первым оказался над новой линией и какой датчик проходит над старой линией в начале поворота.
| Поворот | Первым видит новую линию | Сначала проходит над старой |
|---|---|---|
| Налево | ||
| Направо |
Таблица заполнена. Ты можешь объяснить идею: крутиться, пока датчик со стороны поворота не увидит новую линию.
Проверь идею «крутиться до новой линии» измерением, прежде чем её программировать.
- Положи под робота лист с транспортиром или линиями через 10°. Поставь робота на центр перекрёстка.
- Медленно поворачивай руками налево. Запиши угол, на котором левый датчик впервые видит новую линию, и угол, на котором робот смотрит точно вдоль неё.
- Повтори направо.
- Совпадают ли углы? Что тогда сделает робот, который останавливается в момент, когда датчик увидел линию? Как это исправить?
В журнале — два угла для каждого поворота и вывод о том, где остановится робот.
Выезд на центр перекрёстка
12 минДатчики стоят впереди колёс. Когда датчики заметили перекрёсток, колёса ещё до него не доехали. Если повернуть сразу, робот окажется сбоку от новой линии. Поэтому сначала надо проехать вперёд так, чтобы колёса встали над центром перекрёстка.
- Поставь робота так, чтобы датчики были на поперечной линии.
- Измерь линейкой, сколько ещё ехать колёсам до центра перекрёстка. Запиши в журнал.
- Время этого проезда —
centerMs— подберём на следующем шаге.
Ты знаешь, почему перед поворотом нужен выезд на центр.
Датчики стоят впереди колёс. Когда датчики заметили перекрёсток, колёса ещё до него не доехали.
Робот повернул на месте сразу, как только датчики увидели перекрёсток. Где окажется робот относительно новой линии? Покажи на дорожке руками.
- Поставь робота датчиками на поперечную линию и поверни его руками на месте на 90°. Где оказались датчики — над новой линией или сбоку?
- Измерь линейкой, сколько ещё ехать колёсам до центра перекрёстка. Запиши в журнал.
- Время этого проезда —
centerMs— подберёшь на следующем шаге. Как ты поймёшь, что время слишком маленькое, а как — что слишком большое? Запиши.
Прогноз проверен руками, расстояние записано, признаки «мало» и «много» для centerMs записаны.
Датчики стоят впереди колёс, поэтому перед поворотом робот выезжает на центр перекрёстка. Рассчитай время этого выезда, а не подбирай наугад.
- Измерь расстояние от датчиков до оси колёс в миллиметрах.
- Возьми из журнала урока 2.1 свою скорость в миллиметрах в секунду при близкой
baseSpeed. Нет таблицы — измерь: проезд 1 секунду, путь линейкой, три раза. - Посчитай
centerMsи запиши расчёт. На следующем шаге сравни его с подобранным на дорожке. - Запиши, почему выезд по времени — слабое место и от чего его точность зависит.
В журнале — расстояние, скорость, расчёт centerMs и две причины, по которым выезд по времени может ошибаться.
Поворот на перекрёстке
22 мин- Создай скетч
L42_Turn_At_Crossing. Перенеси измерения,kpиbaseSpeed.
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() {}
- Разберите функцию
turnUntilLine. Зачем в нейdelay(leaveMs)перед поиском линии? - Запусти на дорожке с перекрёстком. Подбери
centerMs: после поворота робот должен уверенно поехать по новой линии.
Робот доезжает до перекрёстка, поворачивает налево и едет по новой линии.
Как это устроено: Зачем leaveMs
В начале поворота датчик ещё над старой линией и сразу «нашёл бы» её. Поэтому первые leaveMs миллисекунд робот просто крутится, не глядя на датчики, — чтобы датчик ушёл со старой линии. Потом ищем новую.
| Что видишь | Что сделать |
|---|---|
| После поворота робот сбоку от линии | Подбери centerMs |
| Робот крутится бесконечно | Датчик не видит линию: проверь порог 30 и измерения |
| Робот останавливается сразу | Увеличь leaveMs |
- Создай скетч
L42_Turn_At_Crossingпо шаблону. Перенеси измерения,kpиbaseSpeed. Функции линии и перекрёстков уже готовы. - Напиши сам выезд на центр
toCenter(), поворот до линииturnUntilLine()и повороты с перекрёстка налево и направо.setup()ниже проверяет их на дорожке.
// Шаблон среднего уровня. Напиши код в разделе «Твоя часть» по целям в комментариях.// 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?
- Запусти на дорожке с перекрёстком. Подбери
centerMsпо признакам, которые записал в прошлом шаге.
Подсказка: Крутиться до линии
Это похоже на passCrossing() из урока 4.1: повторять чтение датчиков, пока условие верно. Какое условие значит «линии под датчиком ещё нет»?
Подсказка: Какой датчик смотреть
Поворачиваем налево — новую линию первым увидит левый датчик.
Робот доезжает до перекрёстка, поворачивает налево и едет по новой линии. Прогноз записан.
| Что видишь | Что сделать |
|---|---|
| После поворота робот сбоку от линии | Подбери centerMs |
| Робот крутится бесконечно | Датчик не видит линию: проверь условие поиска и измерения |
| Робот останавливается сразу | Увеличь leaveMs |
Требование 1. Продолжи свою программу L41_Count и сохрани как L42_Turn_At_Crossing. Функция turnUntilLine(left) поворачивает робота на месте до новой линии:
- сначала робот крутится, не глядя на датчики, пока датчик не уйдёт со старой линии;
- потом — пока датчик со стороны поворота не увидит линию;
- если линия не нашлась за 3 секунды, робот останавливается: на поле без защиты он будет крутиться бесконечно.
Требование 2. turnLeftAtCrossing() и turnRightAtCrossing(): выезд на центр по твоему centerMs, затем поворот до линии.
Приёмка. По пять поворотов налево и направо: после каждого робот уезжает по новой линии. Отдельно проверь защиту: подними робота над столом во время поворота.
Подсказка: Идея (1 из 3)
Поиск линии — цикл «читать датчики, пока линии нет». Для защиты запомни время начала поиска.
Подсказка: План (2 из 3)
- Включить поворот, подождать
leaveMs. - Запомнить
millis(). - Повторять чтение, пока линии нет и прошло меньше 3 секунд.
- Остановить моторы.
Подсказка: Фрагмент кода (3 из 3)
while ((left ? leftLevel : rightLevel) > 30 && millis() - start < 3000) — прочитай условие словами.
Программа выполняет требования 1–2, десять поворотов и проверка защиты записаны. Подобранный centerMs сравнён с расчётом.
Разворот на линии
10 минВ конце ответвления, у груза или места доставки, роботу нужно развернуться и вернуться к перекрёстку. Разворот — тот же поворот по датчикам, только линия теперь сзади.
- В скетче найди вызов
turnAroundOnLine(). Проверь на дорожке: проехать по ответвлению, развернуться, вернуться к перекрёстку.
Робот разворачивается, находит ту же линию и возвращается к перекрёстку.
В конце ответвления роботу нужно развернуться и вернуться к перекрёстку. Разворот — тот же поворот по датчикам, только линия теперь сзади.
- Найди в скетче функцию
turnAroundOnLine(). Почему в ней нет выезда на центр, как в повороте на перекрёстке? Запиши ответ. - Проверь на дорожке три раза: проехать по ответвлению, развернуться, вернуться к перекрёстку.
Робот три раза разворачивается и возвращается к перекрёстку. Ответ про выезд на центр записан.
Требование 3. turnAroundOnLine() разворачивает робота на линии и останавливается, когда линия снова под датчиком. Защита 3 секунды из требования 1 работает и здесь.
Приёмка. Пять разворотов на конце ответвления: после каждого робот возвращается к перекрёстку. Для каждого измерь, насколько нос смотрит мимо линии сразу после разворота.
| Разворот | Вернулся? | Нос мимо линии, мм |
|---|---|---|
| 1 |
Если нос каждый раз смотрит в одну сторону, предложи и проверь одно исправление.
Пять разворотов в таблице, исправление систематической ошибки проверено.
Маршрут по порядку
22 минМаршрут — это решения на перекрёстках по порядку. Робот не видит, какой это перекрёсток: он просто выполняет следующее решение.
- Создай скетч
L42_Route. Начерти в журнале тренировочную дорожку и путь робота.
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() {}
По программе: налево, прямо, направо. Нарисуй, где робот остановится на дорожке.
- Запусти. Если маршрут сломался — на каком решении? Почему?
Робот проходит маршрут из трёх решений и останавливается там, где ты предсказал.
У соседей после turnLeftAtCrossing() сразу стоит turnRightAtCrossing(), без followLineToCrossing() между ними. Что сделает робот?
Маршрут — это решения на перекрёстках по порядку. Робот не видит, какой это перекрёсток: он просто выполняет следующее решение.
- Начерти в журнале тренировочную дорожку и путь: налево, прямо, направо.
- Создай скетч
L42_Routeпо шаблону и напиши маршрут в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 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() {}
Нарисуй, где робот остановится на дорожке.
- Запусти. Если маршрут сломался — на каком решении? Почему?
Подсказка: Одно решение
Перед каждым решением робот должен доехать до перекрёстка.
Робот проходит маршрут из трёх решений и останавливается там, где ты предсказал.
У соседей после turnLeftAtCrossing() сразу стоит turnRightAtCrossing(), без followLineToCrossing() между ними. Что сделает робот?
Требование 4. Маршрут планируется до программы и проверяется по частям.
- Выбери на тренировочном поле путь из четырёх решений или больше, в котором есть налево, направо и прямо. Если в классе разложено поле итоговой задачи, возьми путь от старта до одной зоны доставки. Запиши его таблицей:
| № | Перекрёсток | Решение | Куда смотрит робот после |
|---|---|---|---|
| 1 |
- Запиши маршрут в программу
L42_Routeиз своих функций. - Если маршрут сломался, не переписывай всё: запусти только первые решения, потом половину, и найди первое решение, на котором робот ошибается. Запиши, как искал.
На каком решении маршрут скорее всего сломается и почему? Запиши до первого запуска.
Подсказка: Как проверять по частям
Временно поставь stopMotors(); и return; после нужного решения в setup().
Таблица маршрута, программа по ней, прогноз и запись поиска ошибки. Робот проходит маршрут на тренировочной дорожке.
Задания по уровням
13 минПовернуть с перекрёстка налево и направо и уехать по новой линии; развернуться на линии и вернуться.
Составь на тренировочном поле маршрут из четырёх решений, в котором есть налево, направо и прямо. Запиши его в журнал, затем в программу, и пройди без ошибок.
Запиши решения массивом: char route[] = {'L', 'S', 'R', 'L'};. Напиши функцию decide(d), которая выполняет одно решение, и цикл, который проходит весь массив. Составь маршрут по схеме поля так, чтобы для нового маршрута менялась только строка с массивом.
Проверь себя
Выберите ответы для подготовки. Они не сохраняются и не оцениваются на сайте. Пройдите проверку знаний в программе курса или обсудите ответы с преподавателем.
● Основное
★ Средний уровень
★★ Продвинутый уровень
Отвечай на вопросы своего уровня. Нужно верных ответов: Начальный — 4 из 5; Средний — 4 из 5; Продвинутый — 4 из 5. Ответы проверит преподаватель или программа курса.
Итог
Робот сворачивает по датчикам и проходит маршрут из решений. Для итоговой задачи осталось научиться видеть груз и брать его захватом.
Как подтвердить результат
Для каждого опыта сохраняй условия, версию программы, наблюдение и вывод. Сравнивая попытки, меняй одну причину за раз; неудачные попытки тоже остаются в журнале. Отметка результата относится к уже выполненным действиям, повторять их ради отметки не нужно. Если результат не получен, запиши расхождение и следующую проверяемую гипотезу. Модель, измерения настоящего робота и объяснение — разные виды доказательств.
Словарь урока
| маршрут | Последовательность решений на перекрёстках: налево, направо или прямо. |
| центр перекрёстка | Точка, где пересекаются линии. Для поворота на месте над ней должны оказаться колёса. |
| массив | Список значений под одним именем: route[0], route[1] и так далее. |
| millis() | Сколько миллисекунд прошло с включения платы. Позволяет ехать заданное время, не останавливая программу. |
Ультразвук: видим груз
Робот измеряет расстояние звуком и останавливается перед грузом, не путая «нет эха» с «груз рядом».
Результат урока
Робот едет по линии и останавливается на заданном расстоянии от груза. Если эхо не вернулось, программа не принимает это за «груз рядом».
Ты научишься
- Объяснить, как ультразвуковой датчик измеряет расстояние.
- Вывести расстояние в монитор порта и проверить его линейкой.
- Отличить отсутствие эха от маленького расстояния.
- Остановить робота перед грузом по измерению, а не по времени.
Нужно знать
- 3.2 П-регулятор: плавно и быстро
Понадобится
Зачем этот урок
Груз стоит в конце ответвления. Робот должен подъехать к ней и остановиться ровно так, чтобы захват мог её взять. Если ехать «по времени», робот то не доедет, то врежется. Сегодня он научится видеть груз ультразвуком — так же, как летучая мышь видит препятствия.
- Как робот останавливается на перекрёстке в программе из урока 4.1?
- Что делает
breakв цикле? (Если не помнишь — разберём сегодня.) - Какие две команды нужны, чтобы робот ехал по линии с П-регулятором?
Как работает ультразвук
8 минЧто должен делать робот. Курьеру нужно обнаружить предмет впереди, оценить расстояние до него и остановиться перед грузом. Датчики линии смотрят вниз и эту задачу не решают. Добавим ультразвуковой датчик: он поможет измерить расстояние, но не отличит груз от стены.
Что используем и как это работает. Передатчик датчика излучает короткую серию ультразвуковых колебаний, которые человек не слышит. Звук отражается от предмета, а приёмник улавливает эхо. Arduino запускает измерение импульсом на TRIG и измеряет длительность сигнала ECHO. Это электрический сигнал о времени измерения, а не сам звук. Путь звука двойной: до предмета и обратно. Поэтому расстояние равно скорости звука, умноженной на время и делённой на два.
Как связать измерение с действием. В учебной программе время в микросекундах делится на 58, чтобы получить приблизительные сантиметры: около 29 мкс на сантиметр в одну сторону и столько же обратно. Например, 580 мкс соответствует примерно 10 см. Целочисленный результат отбрасывает дробную часть; температура и отражающая поверхность тоже влияют на измерение. Сравниваем его с линейкой от передней поверхности датчика.
Если измерение не получилось. Наклонная поверхность может отразить звук в сторону, мягкая — поглотить, а слишком близкий или далёкий предмет — не дать пригодного эха. Поэтому «нет эха» означает «расстояние неизвестно», а не ноль и не свободный путь. В коде для этого есть отдельное значение −1. Программа принимает решение об остановке только после проверки измерения. Базовый пример при −1 продолжает движение — это ограничение учебного алгоритма, которое разберём в задании на потерю эха.
Датчик посылает короткий щелчок ультразвука и слушает эхо. Чем дальше предмет, тем дольше звук летит туда и обратно. По времени полёта считаем расстояние.
Звук пролетает 1 см примерно за 29 микросекунд. Предмет в 10 см. Сколько времени пройдёт до возвращения эха? (Не забудь: туда и обратно.)
Ты можешь объяснить: время эха → расстояние. До предмета в 10 см и обратно — около 580 мкс.
Датчик посылает короткий щелчок ультразвука и слушает эхо. Чем дальше предмет, тем дольше звук летит туда и обратно.
Звук пролетает 1 см примерно за 29 микросекунд. Сколько времени пройдёт до возвращения эха, если предмет в 10 см? А в 30 см?
- Запиши расчёт в журнал.
- Объясни, почему время нужно умножать на два.
Расчёт для 10 и 30 см записан с объяснением про «туда и обратно».
Датчик посылает короткий щелчок ультразвука и слушает эхо. По времени полёта считают расстояние.
- Звук пролетает 1 см примерно за 29 мкс. Выведи формулу: расстояние в сантиметрах по времени эха в микросекундах.
- Скорость звука зависит от температуры. При 0 °C звук пролетает 1 см примерно за 30 мкс. Груз стоит в 50 см. Сколько покажет датчик зимой в холодном коридоре, если программа считает по 29 мкс? Важна ли эта ошибка для остановки в 5 см?
Формула и расчёт ошибки записаны. Ты можешь объяснить, когда ошибкой можно пренебречь.
Расстояние в мониторе порта
20 мин- Посмотри номера контактов TRIG и ECHO на карточке у робота. Карточки нет — проследи провода от выводов TRIG и ECHO датчика до платы и прочитай номера у гнёзд. Запиши в журнал.
- Создай скетч
L51_Distanceи впиши номера контактов вместо-1.
// Контакты ультразвукового датчика: впиши номера из таблицы проводки своего робота.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);}
- Загрузи, открой монитор порта. Ставь груз на 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 или датчик не получает питание |
| Числа прыгают | Груз должна стоять прямо напротив датчика |
- Посмотри номера контактов TRIG и ECHO на карточке у робота. Карточки нет — проследи провода от выводов TRIG и ECHO датчика до платы и прочитай номера у гнёзд. Запиши в журнал.
- Создай скетч
L51_Distanceпо шаблону и впиши номера контактов вместо-1. - Напиши функцию
readDistanceCm()сам. Печать в мониторе порта уже готова. Пока функция не написана, монитор печатает «нет эха».
| Команда | Что делает |
|---|---|
digitalWrite(TRIG_PIN, HIGH); … LOW | Щелчок: TRIG в HIGH на 10 мкс, перед этим коротко LOW |
delayMicroseconds(мкс); | Ждёт указанное число микросекунд |
pulseIn(ECHO_PIN, HIGH, 25000) | Время эха в микросекундах; 0 — эха нет за 25 000 мкс |
// Шаблон среднего уровня. Напиши код в разделе «Твоя часть» по целям в комментариях. // Контакты ультразвукового датчика: впиши номера из таблицы проводки своего робота.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 мкс. Сколько сантиметров до груза?
- Загрузи, открой монитор порта. Ставь груз на 5, 10, 20 и 30 см по линейке. Запиши показания.
| По линейке | Показание датчика |
|---|---|
| 5 см | |
| 10 см | |
| 20 см | |
| 30 см |
Подсказка: На что делить
Звук летит туда и обратно: 29 мкс на сантиметр в одну сторону и 29 — в другую. Сколько микросекунд приходится на один сантиметр расстояния до груза?
Подсказка: Время эха — большое число
Время может быть больше, чем помещается в int. Для него берут unsigned long.
Показания близки к линейке, разница — 1–2 см. Прогноз для 1160 мкс сошёлся.
| Что видишь | Что сделать |
|---|---|
| Программа ничего не печатает | Вписаны ли номера TRIG и ECHO вместо −1? |
| Всегда «нет эха» | Есть ли щелчок перед ожиданием эха? Не перепутаны ли TRIG и ECHO? |
| Числа прыгают | Груз должен стоять прямо напротив датчика |
- Посмотри номера контактов 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 см |
- Какое самое большое расстояние может измерить функция при ожидании 25 000 мкс?
- Растёт ли ошибка с расстоянием? Чем ты это объяснишь?
Подсказка: Идея (1 из 3)
Щёлкнуть, измерить длину импульса эха, перевести микросекунды в сантиметры.
Подсказка: План (2 из 3)
- TRIG: LOW 2 мкс, HIGH 10 мкс, LOW.
pulseInна ECHO с ожиданием 25 000 мкс.- Ноль — нет эха. Иначе — время делить на 58.
Подсказка: Фрагмент кода (3 из 3)
unsigned long echoUs = pulseIn(ECHO_PIN, HIGH, 25000); — почему unsigned long, а не int?
Функция выполняет требование 1, таблица с ошибками заполнена, ответы записаны.
«Нет эха» — не «рядом»
15 мин- Направь датчик в пустоту или на мягкую ткань под углом. Что печатает монитор?
- Поднеси ладонь вплотную к датчику, ближе 2 см. Что печатает?
Соседи написали остановку так: if (readDistanceCm() <= 5) { stopMotors(); }. Робот иногда останавливается посреди пустой дороги. Почему? Что вернёт функция, если эха нет?
Ты можешь объяснить, почему условие остановки должно проверять, что расстояние больше нуля.
- Проверь датчик в трёх положениях и запиши, что печатает монитор:
| Положение | Что печатает |
|---|---|
| В пустоту | |
| На мягкую ткань под углом | |
| Ладонь ближе 2 см |
Соседи написали остановку так: if (readDistanceCm() <= 5) { stopMotors(); }. Робот иногда останавливается посреди пустой дороги. Почему? Что вернёт функция, если эха нет?
- Запиши правильное условие остановки.
Таблица заполнена, ошибка соседей объяснена, правильное условие записано.
Найди границы, за которыми датчику нельзя верить.
- Поставь груз в 20 см и поворачивай его боком к датчику. С какого угла пропадает эхо?
- Придвигай груз к датчику. С какого расстояния показания перестают совпадать с линейкой?
- Направь датчик на мягкую ткань. Что печатает монитор?
Соседи написали остановку так: if (readDistanceCm() <= 5) { stopMotors(); }. Робот иногда останавливается посреди пустой дороги. Почему?
- Запиши, какие из найденных границ опасны для доставки и как программа должна себя вести в каждом случае.
В журнале — угол потери эха, ближняя граница, поведение на ткани и правила для программы.
Едем к грузу и останавливаемся
17 мин- Создай скетч
L51_Stop_At_Can. Перенеси измерения,kp,baseSpeedи контакты датчика.
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() {}
- Поставь груз в конце линии. Запусти робота с другого конца.
Робот едет по линии и останавливается перед грузом, не касаясь его.
Как это устроено: Что делает break
break немедленно выходит из цикла while. После цикла программа выполняет stopMotors(). Так «стоп по расстоянию» оказывается в одном месте программы.
- Создай скетч
L51_Stop_At_Canпо шаблону. Перенеси измерения,kp,baseSpeed, контакты датчика и замениreadDistanceCmсвоей функцией. - Напиши движение в
setup()сам: робот едет по линии и останавливается перед грузом. Помни шаг «Нет эха — не рядом».
// Шаблон среднего уровня. Напиши код в разделе «Твоя часть» по целям в комментариях.// Функцию 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() {}
- Поставь груз в конце линии. Запусти робота с другого конца.
Подсказка: Ехать, пока…
Как в 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, обе проверки приёмки записаны.
Точная остановка
15 мин- Измерь линейкой, на каком расстоянии от груза остановился робот.
- Подбери
STOP_CMиbaseSpeed, чтобы робот останавливался там, где удобно захвату. - Повтори три раза. Запиши в журнал
STOP_CMи фактическое расстояние.
Подсказка: Робот останавливается дальше или ближе, чем STOP_CM
Робот не останавливается мгновенно: он ещё немного катится. Чем выше скорость, тем дальше. Можно перед грузом ехать медленнее или поставить STOP_CM чуть больше.
Робот три раза подряд останавливается на нужном расстоянии от груза.
Подбери STOP_CM и baseSpeed так, чтобы робот останавливался там, где удобно захвату. Меняй одно число за раз.
- Реши, на каком расстоянии от груза роботу нужно стоять. Если захват уже стоит, измерь у него; если нет — выбери расстояние, при котором робот не касается груза, и уточни его на уроке 5.2. Запиши.
- Запусти, измерь линейкой, где робот остановился. Запиши в таблицу и реши, что менять.
STOP_CM | baseSpeed | Где остановился, см | Что поменять и почему |
|---|---|---|---|
- Когда получилось, повтори три раза.
Подсказка: Робот останавливается ближе, чем STOP_CM
Робот не останавливается мгновенно: он ещё немного катится. Чем выше скорость, тем дальше.
Робот три раза подряд останавливается на нужном расстоянии. В журнале — таблица попыток с объяснением.
Добейся остановки на нужном расстоянии ±1 см три раза из трёх на своей рабочей скорости — той, на которой робот проходит линию.
- Измерь выбег: при
baseSpeed80, 120 и своей рабочей поставьSTOP_CM = 15и измерь, на сколько сантиметров робот прокатился после команды стоп. - Выбери способ точной остановки и обоснуй его таблицей: поправить
STOP_CMна выбег или ехать медленнее в последние сантиметры перед грузом. - Проверь три раза.
baseSpeed | STOP_CM | Где остановился, см | Выбег, см |
|---|---|---|---|
| 80 | 15 | ||
| 120 | 15 | ||
| 15 |
Подсказка: Медленно в конце
Два порога: дальше первого — рабочая скорость, между первым и вторым — медленная, у второго — стоп.
Таблица выбега, выбранный способ с обоснованием и три остановки ±1 см записаны.
Задания по уровням
13 минОстановиться в 5 см от груза, стоящего в конце линии.
Если эха нет, измерь ещё раз. Если эха нет пять раз подряд — останови робота: что-то не так с датчиком. Так робот не уедет с поля, потеряв груз.
Делай три измерения и бери среднее по величине — медиану. Одно случайное неверное число тогда не испортит остановку. Сравни в мониторе порта сырые измерения и медиану.
Проверь себя
Выберите ответы для подготовки. Они не сохраняются и не оцениваются на сайте. Пройдите проверку знаний в программе курса или обсудите ответы с преподавателем.
● Основное
★ Средний уровень
★★ Продвинутый уровень
Отвечай на вопросы своего уровня. Нужно верных ответов: Начальный — 4 из 5; Средний — 4 из 5; Продвинутый — 4 из 5. Ответы проверит преподаватель или программа курса.
Итог
Робот видит груз и останавливается перед ним по измерению. Осталось научиться его брать — в следующем уроке.
Как подтвердить результат
Результат опыта связывает условия, версию программы, наблюдение и вывод. Сравнение попыток с одним изменением помогает проверить причину, включая неудачные попытки. Отметка относится к уже выполненной практике; повтор ради отметки не нужен. Расхождение с прогнозом задаёт следующую проверяемую гипотезу. Модель, измерения настоящего робота и объяснение — разные виды доказательств.
Словарь урока
| ультразвук | Звук такой высоты, что человек его не слышит. Датчик посылает щелчок и ждёт эхо. |
| эхо | Звук, отразившийся от предмета и вернувшийся к датчику. |
| pulseIn | Команда, которая измеряет, сколько микросекунд длился сигнал на входе. |
| мкс | Микросекунда — миллионная доля секунды. |
| медиана | Среднее по величине из нескольких чисел: из 4, 25 и 5 это 5. |
Захват: берём и ставим
Робот подъезжает к грузу, берёт его захватом, увозит и ставит в нужном месте.
Результат урока
Робот подъезжает к грузу по ультразвуку, берёт её, разворачивается, везёт по линии и ставит на другом конце. Действия оформлены функциями pickUp() и putDown().
Ты научишься
- Объяснить, как сервопривод поворачивается на заданный угол.
- Найти углы «открыто» и «закрыто» для своего захвата опытом.
- Соблюдать порядок: остановиться — закрыть; открыть — подождать — отъехать.
- Собрать перевозку груза из функций прошлых уроков.
Нужно знать
- 5.1 Ультразвук: видим груз
- 4.2 Повороты на перекрёстке и маршрут
Понадобится
Зачем этот урок
Робот умеет доезжать до груза. Остался последний навык курьера — взять груз, довезти и аккуратно поставить. Сегодня робот соберёт всё вместе: линия, ультразвук и захват.
- Какое условие остановки перед грузом правильное и почему?
- Как робот разворачивается на линии?
- Что делает
followLineFor(1500)?
Не держи пальцы в захвате, когда включено питание. Сервопривод может резко повернуться при загрузке программы.
Как работает сервопривод
8 минЧто должен делать робот. Курьер должен взять груз, удержать его при перевозке и отпустить в нужном месте. Для этого добавим механический захват с сервоприводом. Ультразвук помогает подъехать, но сам ничего не захватывает.
Что используем и как это работает. Внутри сервопривода есть мотор, редуктор, датчик положения и управляющая схема. По управляющим импульсам он получает заданное положение вала и старается его удержать. Библиотека Servo формирует эти импульсы; gripper.write задаёт положение, а не скорость вращения. Рычаг превращает поворот вала в движение губок захвата.
Как связать измерение с действием. Углы «открыто» и «закрыто» подбираем для своей механики небольшими изменениями; чужие числа могут упереть механизм в ограничитель. Сначала останавливаем колёса, затем закрываем захват и даём ему время. Команда закрытия не доказывает, что груз удерживается: это проверяем наблюдением. Подключение и питание сверяем с подтверждённой схемой комплекта.
Обычный мотор крутится, пока включён. Сервопривод поворачивается на заданный угол и держит его. Внутри у него мотор, шестерёнки и датчик положения. Командуем углом в градусах: gripper.write(90).
Что сделает сервопривод после gripper.write(90), а потом gripper.write(40)?
Ты можешь объяснить, чем сервопривод отличается от мотора колеса.
Обычный мотор крутится, пока включён. Сервопривод поворачивается на заданный угол и держит его. Командуем углом в градусах: gripper.write(90).
Что сделает сервопривод после gripper.write(90), а потом gripper.write(40)? А мотор колеса после analogWrite(…, 90)?
- Запиши в журнал: чем команда сервоприводу отличается от команды мотору колеса — что задаёт число и когда движение заканчивается.
Сравнение сервопривода и мотора колеса записано.
Сервопривод поворачивается на заданный угол и держит его: внутри мотор, шестерёнки и датчик положения.
- Посмотри контакт сервопривода на карточке у робота и впиши его в скетч пробы угла.
- Сделай опыт:
gripper.write(40), пауза,gripper.write(90), пауза. Пробуй паузы 100, 300 и 600 мс. При какой паузе захват успевает дойти до угла? - Запиши, сколько примерно времени нужно сервоприводу на 50° и почему программа должна ждать после команды.
В журнале — опыт с паузами и наименьшая пауза, при которой захват доходит до угла.
Найди углы «открыто» и «закрыто»
22 минУ каждого захвата свои углы. Найдём их опытом, как время поворота в уроке 2.2.
- Посмотри контакт сервопривода на карточке у робота и впиши его вместо
-1.
#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() {}
- Начни с
angle = 90. Загрузи. Меняй угол по 10°: найди положение, где захват открыт так, что груз свободно входит. - Найди угол, где захват держит груз крепко, но не давит изо всех сил.
- Запиши оба угла в журнал.
Подсказка: Сервопривод гудит или дрожит
Захват упёрся в груз или в собственный упор и не может дойти до угла. Отступи на 5–10° от этого положения: сервопривод не должен работать в упор.
В журнале записаны углы «открыто» и «закрыто», при которых груз свободно входит и надёжно держится.
У каждого захвата свои углы. Найди их опытом — способ выбери сам, но меняй только angle и записывай каждую попытку.
| Команда | Что делает |
|---|---|
gripper.attach(контакт); | Связывает сервопривод с контактом |
gripper.write(угол); | Поворачивает на угол 0…180 и держит его |
- Создай скетч
L52_Servo_Testпо шаблону. Посмотри контакт сервопривода на карточке у робота и впиши его вместо-1. - Допиши
setup()сам: захват должен повернуться наangle.
// Шаблон среднего уровня. Напиши код в разделе «Твоя часть» по целям в комментариях. #include <Servo.h> // Контакт сервопривода захвата: впиши номер из таблицы проводки своего робота.const int SERVO_PIN = -1;int angle = 90; // меняй понемногу, чтобы найти «открыто» и «закрыто» Servo gripper; // ===== Твоя часть: напиши сам ===== void setup() { if (SERVO_PIN < 0) { while (true) { } } // Захват поворачивается на угол angle и держит его.} void loop() {}
- Найди угол «открыто»: груз свободно входит в захват.
- Найди угол «закрыто»: груз держится крепко, а сервопривод не гудит.
- Проверь «закрыто»: подними робота с грузом и слегка наклони. Груз не выпадает?
| Попытка | angle | Что видно | Что поменять и почему |
|---|---|---|---|
| 1 | 90 | ||
| 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 работает.
Порядок действий важен
15 минСопоставь функции pickUp() и putDown() в итоговом скетче с правилом порядка (следующее занятие):
- Взять: сначала остановиться, потом закрыть захват.
- Поставить: открыть захват, подождать, пока он раскроется, и только потом отъехать.
У соседей putDown() такая: gripper.write(OPEN_ANGLE); и сразу drive(BACKWARD, …). Груз то падает, то уезжает вместе с роботом. Почему? Как исправить?
Ты можешь объяснить, зачем пауза после открытия захвата.
- До того как смотреть код, запиши в журнал по шагам: как роботу правильно взять груз и как поставить. Что должно случиться раньше: остановка или захват? Открытие или отъезд?
- Сравни свой порядок с правилом:
- Взять: сначала остановиться, потом закрыть захват.
- Поставить: открыть захват, подождать, пока он раскроется, и только потом отъехать.
У соседей putDown() такая: gripper.write(OPEN_ANGLE); и сразу drive(BACKWARD, …). Груз то падает, то уезжает вместе с роботом. Почему? Как исправить?
Твой порядок записан и сверен с правилом, ошибка соседей объяснена.
У соседей putDown() такая: gripper.write(OPEN_ANGLE); и сразу drive(BACKWARD, …). Груз то падает, то уезжает вместе с роботом. Почему?
- Запиши правила порядка для «взять» и «поставить».
- Найди опытом наименьшую паузу после открытия, при которой груз остаётся на месте: 100, 200, 400, 600 мс, по три раза каждую.
- Какую паузу ты возьмёшь в программу и почему не самую короткую из удачных?
Правила записаны, таблица опыта и выбранная пауза с обоснованием.
Взять и поставить
20 мин- Создай скетч
L52_Pick_Place. Перенеси измерения,kp,baseSpeed, контакты датчика и сервопривода, свои углы.
#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() {}
- Поставь груз в конце линии, робота — на другом конце. Запусти.
Прочитай setup() и расскажи по шагам, что сделает робот.
Робот подъезжает, берёт груз, разворачивается, везёт его по линии и ставит.
- Создай скетч
L52_Pick_Placeпо шаблону. Перенеси измерения,kp,baseSpeed, контакты датчика и сервопривода, свои углы. Езда по линии, перекрёстки и подъезд к грузу уже готовы. - Напиши сам подготовку захвата
setupGripper(),pickUp(),putDown()и доставку вsetup()— по своим правилам порядка из прошлого шага.
// Шаблон среднего уровня. Напиши код в разделе «Твоя часть» по целям в комментариях. #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() и расскажи напарнику по шагам, что сделает робот.
- Поставь груз в конце линии, робота — на другом конце. Запусти.
Подсказка: Сколько ждать
Захвату нужно время, чтобы дойти до угла. Попробуй паузу 300 и 600 мс: успевает ли он?
Подсказка: Без контакта
Защиту без контакта ты уже видел в скетче пробы угла.
Робот подъезжает, берёт груз, разворачивается, везёт его по линии и ставит.
Требование 4. Продолжи свою программу L51_Stop_At_Can и сохрани как L52_Pick_Place. Робот выполняет доставку по линии:
pickUp()иputDown()соблюдают порядок: остановиться — закрыть; открыть — подождать — отъехать;- захват двигается твоей функцией
moveGripper; - без контакта сервопривода программа не начинает движение;
- в
setup(): подъехать к грузу, взять, развернуться на линии, проехать с грузом, поставить.
Приёмка. Груз в конце линии, робот — на другом конце: доставка без помощи рук.
Подсказка: Идея (1 из 2)
Каждое действие доставки — уже готовая функция из прошлых уроков или этого урока. Осталось соединить их по порядку.
Подсказка: План (2 из 2)
followLineToCan(STOP_CM).pickUp().turnAroundOnLine().- Проехать по линии
CARRY_MS. putDown().
Программа выполняет требования 1–4, доставка проходит без помощи рук.
Надёжность
12 мин- Повтори перевозку три раза подряд. Где робот ошибается чаще всего?
- Подбери
STOP_CMтак, чтобы груз оказывался в середине захвата. - Подбери
CARRY_MS, чтобы груз ставился в нужном месте.
Три перевозки подряд без помощи рук. Значения записаны в журнал.
- Повтори перевозку пять раз подряд. Каждую неудачу запиши: на каком этапе и что случилось.
| Заезд | Подъезд | Захват | Перевозка | Постановка | Что случилось |
|---|---|---|---|---|---|
| 1 | |||||
| 2 | |||||
| 3 | |||||
| 4 | |||||
| 5 |
- Найди этап, где ошибок больше всего. Какое одно число поменять, чтобы это исправить? Поменяй и повтори три раза.
Подсказка: Какие числа есть
STOP_CM — где робот встаёт перед грузом, CARRY_MS — где ставит, углы захвата — как держит.
Таблица из пяти заездов, исправленный этап и три перевозки подряд без помощи рук.
Измерь надёжность доставки и улучши её по данным.
- Сделай 10 доставок подряд и отметь для каждой, на каком этапе она сорвалась: подъезд, захват, разворот, перевозка, постановка.
- Посчитай, сколько доставок из 10 прошли. Какой этап даёт больше всего срывов?
- Исправь только этот этап и снова сделай 10 доставок. Стало лучше? На сколько?
| Серия | Успешно из 10 | Подъезд | Захват | Разворот | Перевозка | Постановка |
|---|---|---|---|---|---|---|
| До | ||||||
| После |
Подсказка: Если срывов нет
Усложни условия: поставь груз на 1 см в сторону от линии или подними baseSpeed. Где сорвётся первым?
Две серии по 10 доставок, этап с больше всего срывов, что изменено и насколько выросла надёжность.
Задания по уровням
13 минВзять груз с конца линии и поставить его на другом конце той же линии.
Груз стоит на одной ветке, место доставки — на другой. Перевези груз через перекрёсток, используя функции урока 4.2.
Напиши функцию deliver(маршрут до груза, маршрут до места, время до места), которая выполняет всю доставку. Маршруты передавай строками решений, например "LS". Тогда в итоговой задаче доставка будет одной строкой программы.
Проверь себя
Выберите ответы для подготовки. Они не сохраняются и не оцениваются на сайте. Пройдите проверку знаний в программе курса или обсудите ответы с преподавателем.
● Основное
★ Средний уровень
★★ Продвинутый уровень
Отвечай на вопросы своего уровня. Нужно верных ответов: Начальный — 4 из 5; Средний — 4 из 5; Продвинутый — 4 из 5. Ответы проверит преподаватель или программа курса.
Итог
У робота есть все умения итоговой задачи: ехать, поворачивать, держаться линии, находить перекрёстки, видеть груз и перевозить его. Дальше — итоговый проект.
Как подтвердить результат
Результат опыта связывает условия, версию программы, наблюдение и вывод. Сравнение попыток с одним изменением помогает проверить причину, включая неудачные попытки. Отметка относится к уже выполненной практике; повтор ради отметки не нужен. Расхождение с прогнозом задаёт следующую проверяемую гипотезу. Модель, измерения настоящего робота и объяснение — разные виды доказательств.
Словарь урока
| сервопривод | Мотор, который поворачивается на заданный угол и держит его. |
| угол | Положение рычага сервопривода в градусах, обычно от 0 до 180. |
| библиотека | Готовый набор команд, который подключают строкой #include. Servo — библиотека для сервоприводов. |
Робот-курьер: план и программа
Итоговая задача, часть 1: выбираешь уровень, планируешь маршрут и собираешь программу из проверенных частей.
Результат урока
Выбран уровень итоговой задачи, записан и проверен план маршрута. Программа проходит весь маршрут хотя бы в пробном заезде, работающие версии сохранены.
Ты научишься
- Выбрать уровень итоговой задачи по своим выполненным заданиям ●, ★ и ★★.
- Разбить задачу на участки и записать план маршрута до написания кода.
- Собрать программу из функций прошлых уроков и проверять её по частям.
- Сохранять рабочую версию после каждой проверенной части.
Нужно знать
- 2.1 Моторы: едем и останавливаемся
- 2.2 Повороты и развороты
- 3.1 Датчики линии и первое следование
- 3.2 П-регулятор: плавно и быстро
- 4.1 Находим и считаем перекрёстки
- 4.2 Повороты на перекрёстке и маршрут
- 5.1 Ультразвук: видим груз
- 5.2 Захват: берём и ставим
Понадобится
Твоя задача
Ты — инженер службы доставки. Такие роботы-курьеры уже ездят по тротуарам и складам: сами находят дорогу, забирают груз и привозят его по адресу. Твой робот должен сам выехать со старта, перевезти два груза на новые места и остановиться в финише. Во время заезда ему никто не помогает: всё решает твоя программа.
Новых команд здесь нет. Всё нужное ты уже делал: ездил и поворачивал, держался линии с П-регулятором, находил перекрёстки, видел груз ультразвуком и работал захватом. Сейчас нужно собрать эти умения в одну программу. В уроке 6.2 — зачётные заезды.
- Чем
setup()отличается отloop()и где должен выполняться маршрут, чтобы после финиша робот стоял? - Как программа понимает, что под роботом перекрёсток?
- Что делать, если ультразвук не получил эхо?
Выбери уровень
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 |
- Открой бортовой журнал и посмотри, какие задания ●, ★ и ★★ у тебя получились.
- Выбери уровень сам и отметь его ниже. Если сомневаешься, начни с начального: после зачёта можно попробовать следующий.
Уровень выбран и записан в журнал.
Задача есть в трёх уровнях. Правила и критерии оценки у них общие, а поля разные. Поле выбирают по тому, какие задания уроков ты уже выполнил.
| Уровень итоговой задачи | Какие задания уроков нужны |
|---|---|
| Начальный 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 |
- Открой бортовой журнал. Выпиши, какие задания ★ у тебя получились и в каких уроках.
- Сравни со списком, который нужен для среднего поля. Чего не хватает?
- Выбери поле сам. Если выбираешь среднее, но какого-то задания ★ нет, запиши, как и когда ты его доделаешь.
Поле выбрано, в журнале — список выполненных заданий ★ и план для недостающих.
Задача есть в трёх уровнях. Правила и критерии оценки у них общие, а поля разные.
| Уровень итоговой задачи | Какие задания уроков нужны |
|---|---|
| Начальный 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 |
- Выпиши из журнала задания ★★, которые у тебя получились, и числа надёжности из уроков: ошибку поворота, сколько раз робот прошёл крутую дугу, точность остановки у груза, надёжность доставки из урока 5.2.
- Для продвинутого поля назови два самых слабых умения по этим числам.
- Выбери поле сам и обоснуй выбор числами. Запиши, что ты сделаешь со слабыми умениями до зачёта.
Поле выбрано и обосновано числами из журнала, два слабых умения и план по ним записаны.
Разберись в задании своего уровня
8 мин- Выехать из зоны старта влево и найти линию.
- По линии пройти левую дугу и доехать до первого перекрёстка.
- Перевезти груз 1 с верхнего конца ответвления на нижний — в место 1.
- Доехать до второго перекрёстка и перевезти груз 2 с нижнего конца на верхний — в место 2.
- Пройти правую дугу, выехать с конца линии в зону финиша и остановиться.
У старта и финиша есть короткие участки без линии: там робот едет прямо по твоим командам.
Груз 1 → место 1, груз 2 → место 2, затем финиш. Маршрут задан по перекрёсткам:
| Участок | Через перекрёстки |
|---|---|
| Старт → груз 1 | А → В → Б |
| Груз 1 → место 1 | Б → В |
| Место 1 → груз 2 | В → Г |
| Груз 2 → место 2 | Г |
| Место 2 → финиш | Г |
Куда поворачивать на каждом перекрёстке — налево, направо или прямо, — определяешь ты. Через некоторые перекрёстки робот проезжает несколько раз. Ветки, которых нет в маршруте, не используются.
Банки стоят в позициях 1 и 5. Перед планированием вытяни карточку из стопки: на ней написано, в каком порядке и куда их перевезти.
| Карточка | Первая доставка | Вторая доставка |
|---|---|---|
| 1 | 1 → 4 | 5 → 3 |
| 2 | 1 → 3 | 5 → 4 |
| 3 | 5 → 3 | 1 → 4 |
| 4 | 5 → 4 | 1 → 3 |
Готового маршрута нет: путь по перекрёсткам ты выбираешь сам, до запуска. Правильных путей несколько. Справа на поле — крутая S-образная связка.
Проверь, что твой робот проходит S-образную связку с грузом.
- Покажи пальцем на схеме: старт, оба груза, оба места доставки, финиш.
- Прочитай критерии оценки в конце урока. За что дают больше всего баллов?
Ты можешь пересказать задание своего уровня своими словами.
Проверь умения по отдельности
10 минПосле загрузки старой программы робот может сразу поехать. Загружай программы при выключенном питании моторов, первый запуск — на подставке.
Если хотя бы одно умение не работает, вся программа не поедет. Проверь каждое коротким опытом.
- Оба колеса крутятся вперёд и назад; моторы стоят, когда выключены (урок 2.1).
- Робот поворачивает на 90° и разворачивается на 180° (урок 2.2).
- Робот едет по линии с П-регулятором и проходит дуги своего поля (урок 3.2).
- Робот находит перекрёсток и поворачивает с него на нужную линию (уроки 4.1, 4.2).
- Ультразвук видит груз на нужном расстоянии (урок 5.1).
- Захват берёт, держит и отпускает груз (урок 5.2).
Все умения проверены. То, что не работает, ты сначала чинишь по разделу «Если не получилось» своего урока.
Если хотя бы одно умение не работает, вся программа не поедет. Проверь каждое три раза коротким опытом.
| Умение | Урок | Прошло из 3 |
|---|---|---|
| Колёса вперёд, назад и стоп | 2.1 | |
| Поворот 90° и разворот 180° | 2.2 | |
| Линия и дуги своего поля | 3.2 | |
| Перекрёсток и поворот с него | 4.1, 4.2 | |
| Остановка перед грузом | 5.1 | |
| Захват берёт и отпускает | 5.2 |
Таблица заполнена. Умения, которые прошли меньше трёх раз из трёх, ты чинишь первыми.
Проверь умения по своим числам: для каждого запиши, какой результат ты считаешь надёжным, и проверь пять раз.
| Умение | Мой критерий | Прошло из 5 |
|---|---|---|
| Прямо: увод на 1 м | не больше … см | |
| Поворот 90° | ошибка не больше …° | |
| Крутая дуга своего поля | без схода | |
| Перекрёсток и поворот с него | на новую линию | |
| Остановка у груза | … ± 1 см | |
| Захват | груз не выпадает при развороте |
Умения, которые не прошли пять раз из пяти, отметь — с них начнёшь работу.
Критерии записаны числами, у каждого умения — результат из пяти проб.
Составь план маршрута
16 минХороший инженер сначала планирует, потом пишет код. Самое сложное — повороты на обратном пути: «налево» и «направо» считаются от носа робота, а не от тебя.
| Участок | Что делает робот | Какая функция |
|---|---|---|
| Старт → линия | Едет прямо до линии | driveStraight() |
| … |
- Заполни таблицу плана в бортовом журнале. Одна строка — один участок.
- Для каждого перекрёстка запиши: откуда приехал, куда повернул, что делаешь дальше.
- Проведи выключенного робота по плану руками. Куда смотрит его нос после каждого разворота?
- Запиши, где скорее всего будет ошибка и как ты это проверишь.
В плане семь участков: выезд, линия до перекрёстка 1, груз 1, место 1, перекрёсток 2, груз 2 и место 2, финиш.
В плане есть повороты налево, направо и проезд прямо. Каждая встреча с перекрёстком записана отдельной строкой — даже если это тот же перекрёсток. Программа не видит букв: она знает только, какое по счёту решение сейчас выполняет.
План составлен по своей карточке. Для каждой доставки ты выбрал путь по буквам перекрёстков и можешь объяснить, почему именно его. Маршрут записан так, чтобы для другой карточки менялись только решения на перекрёстках.
Подсказка: Робот развернулся — а что теперь «налево»?
Встань и повернись так же, как робот. Твоя левая рука показывает его «налево».
План записан. Напарник прошёл по нему с выключенным роботом и не нашёл ошибок.
Подготовь заготовку
6 минПрограмму собираем из функций. Функция — действие с понятным именем, например turnLeft(). Когда маршрут записан вызовами функций, его легко сверить с планом.
// Робот-курьер: моя программа итоговой задачи.// Это заготовка, а не решение. Наполни функции своим проверенным кодом// из прошлых уроков и проверяй каждую отдельно, прежде чем собирать маршрут. // --- Моторы (урок 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(). После финиша робот стоит.}
- Открой заготовку и сохрани её под своим именем.
- Перенеси в функции свой проверенный код из прошлых уроков.
Как это устроено: Зачем делить программу на функции
Если поворот работает неправильно, исправлять нужно одно место — саму функцию, а не каждый поворот на маршруте. А маршрут в setup() читается почти как план из журнала.
Программу собираем из функций. Когда маршрут записан вызовами функций, его легко сверить с планом.
// Робот-курьер, средний уровень: маршрут по плану из журнала.// Функций здесь нет: перенеси свои проверенные функции из уроков 2.1–5.2 в раздел ниже.// Маршрут в setup() разбит на участки плана. В каждый участок впиши вызовы функций. // --- Контакты, измерения и настройки из своих скетчей --- // --- Мои функции (перенеси и проверь каждую отдельно) ---// Нужны: остановка моторов, прямой проезд, линия до перекрёстка, проезд перекрёстка// прямо, поворот налево и направо на перекрёстке, разворот на линии, подъезд к грузу,// взять груз, поставить груз. void setup() { // Подготовь выводы, датчики и захват. Моторы стоят. // Участок 1. Старт → линия: // Участок 2. Линия → груз 1: // Участок 3. Груз 1 → место 1: // Участок 4. Место 1 → груз 2: // Участок 5. Груз 2 → место 2: // Участок 6. Место 2 → финиш, стоп:} void loop() { // Задание выполняется один раз в setup(). После финиша робот стоит.}
- Открой заготовку и сохрани её под своим именем. Готовых функций в ней нет.
- Перенеси свои проверенные функции из уроков 2.1–5.2. Проверь по списку в заготовке, что ни одной не забыл.
- Перед каждой строкой плана в журнале запиши, какая функция её выполняет. Если для строки нет функции — напиши её и проверь отдельно.
Все нужные функции перенесены и проверены, у каждой строки плана есть своя функция.
Требование 1. Программа собрана из твоих проверенных программ уроков 4.2–5.2, а не из заготовки:
- действия — функции: линия до перекрёстка, решения на перекрёстке, подъезд к грузу, взять, поставить, разворот;
- маршрут — данные, отдельные от действий: для другой карточки меняются только строки маршрута, функции не трогаются;
- у каждой функции, которая чего-то ждёт, есть защита от бесконечного ожидания.
Своей программы из урока 5.2 нет — возьми у преподавателя решение урока 5.2 как основу; маршрут и защиты пишешь сам.
- Запиши в журнал список функций своей программы: что делает каждая и в каком уроке проверена.
- Отметь функции без защиты от бесконечного ожидания и добавь защиту.
Список функций с уроком проверки, маршрут отделён от действий, защиты добавлены.
Запиши решения на перекрёстках массивом, например char route[] = {'L', 'S', 'R'};, и выполняй их по очереди в цикле, как в уроке 4.2. Тогда для новой карточки достаточно поменять одну строку.
Часть 1: от старта до первого груза
12 мин- В
setup()вызови функции первого участка: выезд, линия, перекрёсток, поворот, подъезд к грузу. - Проверь на поле только эту часть.
- Работает — сохрани копию: «рабочая версия 1».
Робот выезжает со старта, доезжает по линии до груза 1 и останавливается перед ним.
| Что видишь | Что проверить |
|---|---|
| Робот не находит линию после старта | Смотрит ли нос робота вдоль линии? Хватает ли длины выезда? |
| Робот останавливается на дуге | Дуга — не перекрёсток. Проверь условие поиска перекрёстка. |
| Робот проезжает перекрёсток | Скорость велика или условие перекрёстка не срабатывает. |
| После поворота робот теряет линию | Проверь, что поворот заканчивается, когда датчик нашёл новую линию. |
- Запиши в журнал вызовы функций первого участка по плану: выезд, линия, перекрёстки, повороты, подъезд к грузу. Потом перенеси их в
setup(). - Проверь на поле только эту часть три раза. Робот должен каждый раз остановиться перед грузом 1.
- Работает — сохрани копию: «рабочая версия 1».
Подсказка: Где искать ошибку
Сравни, что сделал робот, со строкой плана. На какой строке разошлось? Проверь эту одну функцию отдельно.
Робот три раза подряд выезжает со старта и останавливается перед грузом 1.
Приёмка части 1. Робот от старта до груза 1: три раза из трёх, в том числе когда его поставили в зоне старта на 2 см левее или правее и чуть повёрнутым.
- Запиши маршрут первой части в данные программы и проверь.
- Каждую неудачу запиши: какая функция не сработала и почему.
- Работает — сохрани «рабочую версию 1».
| Запуск | Как стоял на старте | Результат | Что сорвалось |
|---|---|---|---|
| 1 | Ровно | ||
| 2 | 2 см левее | ||
| 3 | 2 см правее, повёрнут |
Три запуска из трёх с разной постановкой на старте, неудачи разобраны.
Часть 2: первая доставка
12 мин- Допиши: захват, разворот, путь к месту 1, отпускание, возвращение к дороге.
- Проверь только эту часть: поставь робота перед грузом 1 и запусти.
- Работает — сохрани «рабочую версию 2».
Подсказка: Когда закрывать захват?
Сначала останови моторы, потом закрывай захват. Подъезд заканчивай по измерению ультразвука. Если эха нет, это не значит «груз совсем рядом».
Подсказка: Груз уезжает вместе с роботом
Отъезжай только после того, как захват полностью открылся.
Груз 1 стоит в месте 1, захват открыт, робот на дороге и смотрит туда, куда нужно дальше.
- Запиши в журнал вызовы функций первой доставки: захват, разворот, путь к месту 1, отпускание, возвращение к дороге. Потом допиши их в программу.
- Проверь только эту часть три раза: ставь робота перед грузом 1 и запускай.
- Работает — сохрани «рабочую версию 2».
Подсказка: Порядок с захватом
Вспомни правило урока 5.2: остановиться — закрыть; открыть — подождать — отъехать.
Три раза подряд груз 1 стоит в месте 1, робот на дороге и смотрит туда, куда нужно дальше.
Приёмка части 2. Первая доставка три раза из трёх, в том числе когда груз сдвинут на 1 см в сторону от линии.
- Допиши в данные маршрута первую доставку и проверь её отдельно: ставь робота перед грузом 1.
- Как робот узнаёт место отпускания? Запиши, чем это место определяется в твоей программе и насколько точно.
- Работает — сохрани «рабочую версию 2».
Три доставки из трёх, включая сдвинутый груз; способ найти место отпускания и его точность записаны.
Часть 3: вторая доставка и финиш
15 мин- Допиши вторую доставку и путь к финишу. Груз 2 стоит в другом месте, поэтому повороты будут другими — сверься с планом.
- В конце — прямой въезд в финиш и остановка. После финиша робот должен стоять.
- Запусти весь маршрут от старта. Сохрани «рабочую версию 3».
Подсказка: Как понять, что пора въезжать в финиш
Чёрной метки финиша на поле нет. Используй последний ориентир — конец дуги или последний перекрёсток — и проверенное время прямого движения. Два «белых» показания ещё не значат, что линия кончилась: она может быть между датчиками.
Робот проходит весь маршрут хотя бы в одной пробе. Все рабочие версии сохранены.
- Допиши вторую доставку и путь к финишу по плану. Груз 2 стоит в другом месте, поэтому повороты будут другими. Каждую встречу с перекрёстком — отдельной строкой.
- В конце — прямой въезд в финиш и остановка. После финиша робот должен стоять.
- Запусти весь маршрут от старта. Если сорвалось — запусти с места срыва только оставшуюся часть. Сохрани «рабочую версию 3».
Подсказка: Как понять, что пора въезжать в финиш
Чёрной метки финиша на поле нет. Используй последний перекрёсток и проверенное время прямого движения.
Робот проходит весь маршрут хотя бы в одной пробе. Все рабочие версии сохранены.
- Допиши вторую доставку и путь к финишу. После финиша робот стоит.
- Запусти весь маршрут от старта. Сохрани «рабочую версию 3».
- Проверь отделение маршрута от действий: вытяни другую карточку, составь для неё маршрут и поменяй только данные маршрута. Робот проходит новый маршрут хотя бы один раз.
- Запиши, какие части маршрута ненадёжны и что ты проверишь в уроке 6.2.
Маршрут своей карточки пройден целиком, маршрут второй карточки пройден после замены только данных. Список ненадёжных частей записан.
Критерии оценки
Зачётные заезды пройдут в уроке 6.2 по этим критериям.
Как оценивается заезд
| Критерий | Баллы | Макс. |
|---|---|---|
| Прямой выезд со старта | 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 за её доставку.
- После помощи руками автономный заезд окончен. Продолжать можно для тренировки, но баллы не добавляются.
- Каждая попытка оценивается отдельно. Лучшие части разных попыток не складываются.
- Аварийная остановка ради безопасности не штрафуется.
Проверь себя
Выберите ответы для подготовки. Они не сохраняются и не оцениваются на сайте. Пройдите проверку знаний в программе курса или обсудите ответы с преподавателем.
● Основное
★ Средний уровень
★★ Продвинутый уровень
Отвечай на вопросы своего уровня. Нужно верных ответов: Начальный — 4 из 5; Средний — 4 из 5; Продвинутый — 4 из 5. Ответы проверит преподаватель или программа курса.
Итог
У тебя есть план и программа, которая проходит маршрут. В уроке 6.2 ты сделаешь её надёжной и проведёшь зачётные заезды.
Как подтвердить результат
Результат опыта связывает условия, версию программы, наблюдение и вывод. Сравнение попыток с одним изменением помогает проверить причину, включая неудачные попытки. Отметка относится к уже выполненной практике; повтор ради отметки не нужен. Расхождение с прогнозом задаёт следующую проверяемую гипотезу. Модель, измерения настоящего робота и объяснение — разные виды доказательств.
Словарь урока
| функция | Именованный блок команд, который можно вызывать много раз: например, turnLeft(). |
| план маршрута | Таблица участков пути: что делает робот и какая функция за это отвечает. |
| рабочая версия | Копия программы, в которой всё написанное уже проверено на поле. |
Робот-курьер: зачётные заезды
Итоговая задача, часть 2: делаешь программу надёжной, проводишь зачётные заезды и разбираешь результат.
Результат урока
Проведены две зачётные попытки, каждая оценена по листу. Команда объясняет свою программу и рассказывает, какую ошибку нашла и как исправила.
Ты научишься
- Находить причину ошибки по наблюдению и менять за раз одну вещь.
- Проверять, что программа работает при немного других условиях старта.
- Провести автономный заезд по правилам и оценить его по критериям.
- Объяснить устройство своей программы и следующий шаг улучшения.
Нужно знать
- 6.1 Робот-курьер: план и программа
Понадобится
Зачем этот урок
Программа, которая прошла маршрут один раз, — ещё не готовая программа. Сегодня ты сделаешь её надёжной, проведёшь две зачётные попытки и разберёшь результат, как настоящий инженер.
- Какой участок твоего плана в прошлый раз работал ненадёжно?
- Почему после помощи руками в зачётной попытке баллы продолжения не засчитываются?
Пробный заезд целиком
8 минЧто должен делать робот. Задача робота — повторяемо выполнить доставку по своему плану. Задача команды — доказать это наблюдениями и объяснить, почему программа выбирает каждое действие. Используем готового робота, поле, журнал и свою проверенную программу.
Что используем и как это работает. Во время заезда работает цепочка: датчик даёт сигнал, программа проверяет условие, исполнитель выполняет команду, новое измерение показывает изменение. Ошибка может возникнуть на любом звене. Отладка начинается с первого места, где наблюдение расходится с прогнозом: например, датчик увидел полосу, но условие её не распознало.
Как связать измерение с действием. Записывай ожидаемое действие и фактический результат, меняй одну причину за раз и повторяй тот же участок. Разовый успех не доказывает повторяемость. Успешная сборка доказывает допустимость программы для компилятора, а доставка и остановка подтверждаются только наблюдением самого заезда.
- Поставь грузы на места, робота — в зону старта.
- Запусти весь маршрут и не вмешивайся.
- Запиши в журнал, на каком участке плана робот впервые сделал не то, что нужно.
Ты знаешь, какой участок работает ненадёжно, — или что весь маршрут прошёл.
- Поставь грузы на места, робота — в зону старта.
- Запусти весь маршрут и не вмешивайся.
- Отметь в своём плане из урока 6.1 каждую строку: выполнена или нет. Первая невыполненная строка — участок, с которого начнёшь отладку.
План размечен по пробному заезду, участок для отладки выбран.
- Проведи два пробных заезда подряд без исправлений между ними.
- Для каждого запиши: на каком этапе первый срыв (выезд, линия, перекрёсток, подъезд, захват, разворот, постановка, финиш) и сколько секунд длился заезд.
- Срыв в одном месте в обоих заездах или в разных? Что это говорит о причине?
Два заезда в журнале с этапом срыва и временем, вывод о том, случайная ошибка или постоянная.
Найди и исправь ошибку
25 минОтладка идёт по кругу из четырёх шагов. Главное правило: меняй одну вещь за раз, иначе не поймёшь, что помогло.
| Шаг | Что делаешь | Пример |
|---|---|---|
| Наблюдаю | Описываю, что робот сделал | Робот проехал мимо груза 2 |
| Причина | Предлагаю одну возможную причину | Не досчитал перекрёсток: проехал его, не заметив |
| Одно изменение | Меняю одну вещь | Снижаю скорость перед перекрёстком |
| Проверка | Запускаю только этот участок | Робот останавливается на перекрёстке 3 раза из 3 |
- Заполни такую таблицу в журнале для своей ошибки.
- Проверяй исправление на своём участке, а не всем маршрутом: так быстрее.
- Когда участок работает три раза подряд, проверь весь маршрут и сохрани новую рабочую версию.
Подсказка: Не понимаю, что делает программа
Временно добавь вывод в монитор порта: какое решение маршрута выполняется и что видят датчики. Перед зачётом этот вывод можно оставить — он не мешает.
Ошибка найдена и исправлена, в журнале записан путь к причине.
Отладка идёт по кругу: наблюдаю → причина → одно изменение → проверка. Меняй одну вещь за раз, иначе не поймёшь, что помогло.
| Наблюдаю | Причина | Одно изменение | Проверка |
|---|---|---|---|
- Заполни таблицу в журнале для своей ошибки. Если первая причина не подтвердилась, добавь строку со следующей.
- Проверяй исправление только на своём участке: поставь робота перед ним.
- Когда участок работает три раза подряд, проверь весь маршрут и сохрани новую рабочую версию.
Подсказка: Не знаю, на каком участке ошибка
Запусти первую половину маршрута. Сработала — ищи во второй. Так за два-три запуска найдёшь участок.
Ошибка найдена и исправлена, в журнале — строки круга отладки.
Отлаживай по доказательствам, а не по догадке.
- Добавь в программу диагностику: номер выполняемого решения маршрута и показания датчиков в момент решения выводятся в монитор порта. Подключи USB-кабель и запусти робота с ним, придерживая кабель; или зажигай светодиод L на каждом решении.
- Для своей ошибки запиши гипотезу и опыт, который может её опровергнуть, — до того, как менять код.
- Проведи опыт. Гипотеза не подтвердилась — запиши новую. В журнале должно быть не меньше двух кругов, если первый не дал ответа.
- Исправление засчитывается, когда участок проходит пять раз из пяти.
| Гипотеза | Опыт | Что показал | Вывод |
|---|---|---|---|
Диагностика работает, в журнале — гипотезы, опыты и выводы, участок проходит пять раз из пяти.
Проверь надёжность
12 минНа зачёте условия будут чуть другими: робот поставлен на старт не идеально, батарея немного разряжена. Проверь заранее.
- Поставь робота на старт чуть левее, потом чуть правее. Маршрут проходит?
- Если робот стал ехать медленнее, чем на пробах, попроси преподавателя проверить батарею. После замены или зарядки заново подбери время прямых участков и поворотов.
- Проведи весь маршрут два раза подряд без исправлений между запусками.
Два полных маршрута подряд с разных положений на старте.
Все решения на перекрёстках верны в двух заездах подряд.
Попроси напарника выдать тебе другую карточку и пройди её, поменяв только маршрут в программе. Если для этого нужно переписывать функции, программа ещё не готова к зачёту.
Программа проходит маршрут два раза подряд. Рабочая версия для зачёта сохранена.
Зачётная попытка 1
10 мин- Расставь грузы на исходные места по схеме своего поля.
- Перед стартом договоритесь, за какими четырьмя решениями на перекрёстках следить.
- Один запуск. Дальше робот едет сам: нельзя трогать его, поправлять грузы или отправлять команды.
- Если нужна помощь, останови робота. Безопасность важнее баллов.
Попытка записана в лист оценки.
Исправь, если нужно
8 минМежду попытками можно исправлять программу. Используй круг отладки: наблюдение → причина → одно изменение → проверка участка.
Исправление проверено на участке и сохранено — или ты оставил программу без изменений.
Зачётная попытка 2
10 минПроведи вторую попытку по тем же правилам. Каждая попытка оценивается отдельно: лучшие части разных попыток не складываются.
Обе попытки записаны в лист оценки.
Разбор результата
12 мин- Сначала оцени свою попытку по листу оценки сам, потом сверь оценку с преподавателем.
- Покажи в своей программе: прямое движение, расчёт П-регулятора, остановку по ультразвуку, управление захватом и строку, которая завершает поездку.
- Расскажи об одной ошибке, которую ты нашёл и исправил. Как ты понял, в чём причина?
- Запиши в журнал: какое умение у твоего робота работает лучше всего, а какое ты бы улучшил дальше.
- Сохрани итоговый скетч и лист оценки.
- Сначала оцени свою попытку по листу оценки сам, потом сверь оценку с преподавателем.
- Покажи в своей программе: прямое движение, расчёт П-регулятора, остановку по ультразвуку, управление захватом и строку, которая завершает поездку.
- Расскажи об одной ошибке, которую ты нашёл и исправил, по строкам круга отладки.
- Объясни одно своё решение: почему на каком-то перекрёстке или участке ты сделал именно так. Что было бы при другом решении?
- Запиши в журнал: какое умение у твоего робота работает лучше всего, а какое ты бы улучшил дальше и как.
- Сохрани итоговый скетч и лист оценки.
- Сначала оцени свою попытку по листу оценки сам, потом сверь оценку с преподавателем.
- Покажи в своей программе: прямое движение, расчёт П-регулятора, остановку по ультразвуку, управление захватом и строку, которая завершает поездку.
- Напиши короткий инженерный отчёт в журнал: - надёжность по своим данным: сколько проб каждого участка прошло; - главная ошибка, гипотезы и опыты, которыми ты её нашёл; - самый большой оставшийся риск и одно изменение, которое бы его уменьшило, — и как ты проверил бы, что оно помогло.
- Расскажи отчёт классу за две минуты.
- Сохрани итоговый скетч, лист оценки и отчёт.
Критерии оценки
Как оценивается заезд
| Критерий | Баллы | Макс. |
|---|---|---|
| Прямой выезд со старта | 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 за её доставку.
- После помощи руками автономный заезд окончен. Продолжать можно для тренировки, но баллы не добавляются.
- Каждая попытка оценивается отдельно. Лучшие части разных попыток не складываются.
- Аварийная остановка ради безопасности не штрафуется.
Проверь себя
Выберите ответы для подготовки. Они не сохраняются и не оцениваются на сайте. Пройдите проверку знаний в программе курса или обсудите ответы с преподавателем.
● Основное
★ Средний уровень
★★ Продвинутый уровень
Отвечай на вопросы своего уровня. Нужно верных ответов: Начальный — 4 из 5; Средний — 4 из 5; Продвинутый — 4 из 5. Ответы проверит преподаватель или программа курса.
Итог
Ты прошёл весь путь инженера: разобрал задачу, составил план, собрал программу из проверенных частей, нашёл и исправил ошибки и проверил результат на поле. Если зачёт сдан, попробуй следующий уровень или факультативные уроки — энкодеры и ПИД-регулятор.
Как подтвердить результат
Результат опыта связывает условия, версию программы, наблюдение и вывод. Сравнение попыток с одним изменением помогает проверить причину, включая неудачные попытки. Отметка относится к уже выполненной практике; повтор ради отметки не нужен. Расхождение с прогнозом задаёт следующую проверяемую гипотезу. Модель, измерения настоящего робота и объяснение — разные виды доказательств.
Словарь урока
| отладка | Поиск и исправление ошибки: наблюдаем, находим причину, меняем одну вещь и проверяем снова. |
| автономный заезд | Заезд, в котором робот после старта едет сам: никто его не трогает и не отправляет команд. |
| надёжность | Программа работает не один раз случайно, а каждый раз — даже если робот поставлен на старт чуть иначе. |
Энкодеры: считаем обороты и точное расстояние
Робот считает обороты колёс и проезжает отмеренное расстояние, а не «сколько успеет за время».
Результат урока
Программа считает обороты колёс энкодерами и проезжает отмеренное расстояние. Число импульсов на оборот измерено, точность проверена линейкой. Не нужен для итоговой задачи.
Ты научишься
- Объяснить, как энкодер превращает оборот колеса в импульсы.
- Измерить число импульсов на оборот колеса.
- Пересчитать расстояние в импульсы через длину окружности колеса.
- Проехать отмеренное расстояние и оценить точность линейкой.
Нужно знать
- 2.1 Моторы: едем и останавливаемся
- 3.2 П-регулятор: плавно и быстро
Понадобится
Зачем этот урок
В уроке 2.1 робот ехал «по времени»: моторы включались на две секунды. Но сколько он проедет за две секунды, зависит от заряда батарей, пола и веса. Энкодер считает обороты колеса, поэтому программа может выбирать момент остановки по вращению колёс, а не только по времени. Точность пути всё равно нужно проверить линейкой.
Для урока нужен дополнительный модуль: энкодер на каждое колесо. В основной комплект курса он не входит; без него урок не провести. Факультатив не нужен для итоговой задачи.
- Как в уроке 2.1 робот проезжал заданный участок?
- Почему при тех же двух секундах робот с севшими батареями проезжает меньше?
- Что такое П-регулятор из урока 3.2 в одной фразе?
Почему время — плохая линейка
8 минЧто должен делать робот. Роботу нужно проехать заданное расстояние точнее, чем по секундомеру. Для этого понадобятся дополнительные энкодеры: в базовом комплекте их нет. Они измеряют вращение колёс, а не расстояние до препятствия.
Что используем и как это работает. Энкодер выдаёт импульсы при вращении. Счётчик накапливает их; число импульсов за оборот связывает счёт с оборотами колеса. Длина окружности связывает обороты с предполагаемым путём. Прерывание позволяет быстро увеличивать счётчик при импульсе, даже когда основная программа занята другой работой.
Как связать измерение с действием. Измерим импульсы на оборот и используем диаметр своего колеса. На AVR чтение многобайтного счётчика прерывание может разорвать посередине, поэтому копируем его при кратко выключенных прерываниях. Проверим результат линейкой: при проскальзывании колесо вращается, а корпус проходит меньше, поэтому энкодер не даёт абсолютной координаты.
- Вспомни программу из урока 2.1, которая едет вперёд по времени. Запусти её дважды: на полной скорости и на скорости вдвое меньше. Время одинаковое.
- Отметь, где робот остановился оба раза. Измерь линейкой оба пути и запиши в журнал.
Ты видишь: при одном и том же времени робот проезжает разное расстояние.
- Запусти программу «по времени» из урока 2.1 трижды: скорость 100, 150 и 200 при одном времени. Измерь пути линейкой.
- Ответь в журнале: что, кроме скорости в программе, меняет путь за одно и то же время? Назови не меньше трёх причин.
Три пути измерены, причины записаны.
- Измерь пути «по времени» при скорости 100, 150 и 200. Построй в журнале таблицу «скорость — путь».
- Оцени: можно ли по этой таблице пересчитать время для 50 см на скорости 120? Насколько ты доверяешь такому пересчёту завтра, на другом заряде батарей? Объясни.
- Запиши, что нужно измерять во время езды, чтобы путь не зависел от этих причин.
Таблица, оценка пересчёта и ответ — в журнале.
Как устроен энкодер
12 минЭнкодер — это диск с прорезями на оси колеса и датчик-«вилка». Внутри вилки с одной стороны светит светодиод, с другой — приёмник. Когда мимо проходит прорезь, свет попадает на приёмник, и датчик посылает импульс. Один оборот колеса — столько импульсов, сколько прорезей на диске.
- Посчитай прорези на диске энкодера. Запиши в журнал.
- Найди в таблице проводки своего робота, к каким контактам подключены энкодеры. Запиши номера.
Не подключай энкодеры при включённом питании. Если в таблице проводки контакты энкодеров не указаны — подключение делает преподаватель.
В журнале — число прорезей и номера контактов левого и правого энкодеров.
Как это устроено: Что такое прерывание
Импульсы приходят быстро — десятки в секунду. Если программа занята чем-то ещё, она может их пропустить. Поэтому счёт делает прерывание: на каждый импульс Arduino сама откладывает основную программу, прибавляет 1 к счётчику и возвращается обратно. На Arduino Uno так умеют только контакты 2 и 3.
Энкодер — диск с прорезями на оси колеса и оптический датчик щели: прорезь проходит мимо датчика — датчик даёт импульс. Считать импульсы будет прерывание: Arduino сама вызывает короткую функцию на каждый импульс, что бы ни делала основная программа.
- Посчитай прорези на диске, запиши номера контактов энкодеров из таблицы проводки.
Колесо диаметром 45 мм, на диске 20 прорезей. Сколько миллиметров проезжает колесо между двумя импульсами? Посчитай до запуска.
- Ответь в журнале: почему счётчик импульсов нельзя увеличивать в
loop()послеdelay(200)?
Не подключай энкодеры при включённом питании. Контакты не указаны в таблице — подключает преподаватель.
Подсказка: Сколько проезжает колесо за оборот
За один оборот колесо проходит длину своей окружности: π × диаметр.
Число прорезей, контакты, прогноз «мм на импульс» и ответ про delay — в журнале.
- Посчитай прорези, запиши контакты энкодеров из таблицы проводки.
- Оцени частоту импульсов: колесо 45 мм, 20 прорезей, робот едет 0,3 м/с. Сколько импульсов в секунду даёт каждый энкодер?
- Объясни в журнале, почему счёт делают прерыванием и почему на Arduino Uno для этого подходят только контакты 2 и 3.
Не подключай энкодеры при включённом питании. Контакты не указаны в таблице — подключает преподаватель.
Подсказка: Частота импульсов
Сначала — сколько оборотов в секунду делает колесо при 300 мм/с.
Оценка частоты и объяснение — в журнале.
Импульсов на один оборот
22 мин- Создай скетч
LF1_Count. Впиши номера контактов энкодеров.
// Контакты энкодеров: впиши номера из таблицы проводки своего робота.// На 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);}
- Загрузи и открой монитор порта. Поверни колесо рукой: число должно расти.
- Сделай мелом или маркером метку на колесе. Прокрути колесо рукой ровно 10 оборотов по метке. Раздели число импульсов на 10 — это импульсов на оборот.
- Повтори для второго колеса. Запиши оба числа в журнал.
В журнале — импульсов на оборот для каждого колеса. Обычно это число прорезей на диске.
| Что видишь | Что сделать |
|---|---|
| «Впиши контакты энкодеров» | Впиши номера контактов вместо −1 |
| Число не растёт | Проверь провода и что диск проходит через щель датчика |
| Число растёт, когда колесо стоит | Колесо остановилось на краю прорези: сдвинь его чуть-чуть |
- Создай скетч
LF1_Countпо шаблону. Впиши контакты, напиши свою часть.
// Шаблон среднего уровня. Напиши код в разделе «Твоя часть» по целям в комментариях. // Контакты энкодеров: впиши номера из таблицы проводки своего робота.// На 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();}
- Прокрути каждое колесо рукой ровно 10 оборотов по метке и посчитай импульсы на оборот.
- Сравни с числом прорезей. Совпало? Если нет — найди причину.
Подсказка: Как не испортить число при чтении
Счётчик — long, это 4 байта, а Arduino читает их по одному. Прерывание посередине даст смесь старого и нового числа. Есть пара команд, которая на время выключает прерывания.
Счёт работает, импульсы на оборот обоих колёс записаны и сравнены с числом прорезей.
Требование 1. Программа считает импульсы каждого колеса прерыванием и печатает оба счётчика в монитор порта; счётчики читаются так, что прерывание не портит число. Без вписанных контактов программа не идёт дальше.
- Напиши программу сам.
Как это устроено: Справка по командам
| Команда | Что делает |
|---|---|
attachInterrupt(digitalPinToInterrupt(pin), f, RISING) | Вызывать функцию f на каждый фронт импульса на контакте pin |
volatile long x | Переменная, которую меняет прерывание |
noInterrupts() / interrupts() | Выключить и снова включить прерывания |
- Измерь импульсы на оборот: прокрути колесо 10 оборотов по метке, потом 20 оборотов. Насколько различаются два результата? Какой точнее и почему?
- Найди положение колеса, при котором счёт растёт без движения. Объясни в журнале, что происходит в датчике.
Подсказка: Идея (1 из 2)
Функции прерывания должны быть короткими: только увеличить счётчик.
Подсказка: Счёт без движения (2 из 2)
На краю прорези свет попадает на приёмник «наполовину», и сигнал дрожит около порога.
Программа выполняет требование 1, импульсы на оборот измерены дважды, объяснение дрожания записано.
Миллиметры в импульсы
15 минЗа один оборот колесо проезжает длину окружности: π × диаметр. Диаметр колеса — 45 мм. Чтобы проехать 500 мм, нужно 500 / (π × 45) оборотов, а импульсов — в TICKS_PER_REV раз больше.
- Посчитай на бумаге: сколько импульсов на 500 мм, если на оборот 20 импульсов?
- Создай скетч
LF1_Distance, впиши контакты и своё число импульсов на оборот.
// Контакты энкодеров: впиши номера из таблицы проводки своего робота.// На 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() {}
- Поставь робота на пол у метки старта, включи питание моторов. Робот проедет и остановится. Измерь путь линейкой от метки до той же точки корпуса.
Ты посчитал: около 71 импульса. Робот проехал примерно 50 см, путь измерен.
- Создай скетч
LF1_Distanceпо шаблону, впиши контакты и импульсы на оборот. - Напиши
ticksForиaverageTicksпо целям.
// Шаблон среднего уровня. Напиши код в разделе «Твоя часть» по целям в комментариях. // Контакты энкодеров: впиши номера из таблицы проводки своего робота.// На 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)? Посчитай до запуска.
- Запусти поездку от метки, измерь путь линейкой. Сравни с монитором порта: насколько «насчитано после остановки» больше цели?
Подсказка: Округление до целого
Если прибавить 0.5 и отбросить дробную часть, получится округление до ближайшего целого.
Прогноз совпал: 71. Робот проехал около 50 см, перекат после остановки виден в мониторе порта.
Требование 2. Функция long ticksFor(int mm, int ticksPerRev, int diameterMm) переводит миллиметры в импульсы с округлением до ближайшего целого и не переполняется на Arduino Uno.
Приёмка. Напиши функцию в тестовом скетче. Когда все строки PASS, перенеси её в свою программу.
// Проверка функции 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. Программа едет вперёд, пока среднее двух счётчиков не достигнет цели, останавливается и печатает цель и насчитанное после остановки. Без вписанного числа импульсов на оборот робот стоит.
- Выполни требования 2 и 3, проедь 500 мм от метки и измерь путь.
Подсказка: Идея (1 из 2)
Импульсы = обороты × импульсы на оборот, обороты = путь / длина окружности.
Подсказка: Тест на 2000 мм не проходит (2 из 2)
2000 * 40 — 80 000, больше, чем вмещает int. Считай в float или long до умножения.
Тест пройден, поездка на 500 мм сделана и измерена.
Почему робот проезжает дальше
15 минПосле команды «стоп» робот ещё немного катится — как велосипед, если перестать крутить педали.
- Сделай три заезда на 500 мм. Запиши пути в журнал.
- Уменьши
speedдо 70 и сделай ещё три заезда. Стало точнее?
speed | Заезд 1 | Заезд 2 | Заезд 3 |
|---|---|---|---|
| 100 | |||
| 70 |
Таблица заполнена. Ты видишь, на какой скорости робот точнее.
- Сделай три заезда на 500 мм. Для каждого запиши путь линейкой и «перекат» в импульсах из монитора порта.
- Добавь в программу поправку: останавливайся на
OVERSHOOTимпульсов раньше цели, гдеOVERSHOOT— средний перекат из трёх заездов. Сделай ещё три заезда. - Ответь в журнале: будет ли эта поправка верна на другой скорости? Почему?
Шесть заездов в журнале, поправка проверена, ответ про скорость записан.
Требование 4. За SLOW_TICKS импульсов до цели робот сбавляет скорость, чтобы меньше катиться после остановки.
- Выполни требование 4 и подбери
SLOW_TICKSи пониженную скорость. - Сравни три заезда без замедления и три с замедлением: среднюю ошибку и разброс.
- Объясни в журнале, чем замедление лучше поправки «остановиться раньше на N импульсов».
Требование 4 выполнено, сравнение двух способов и вывод — в журнале.
Задания по уровням
12 минПроехать 500 мм с ошибкой не больше 2 см в трёх заездах из трёх.
Проехать 1 м, выравнивая колёса по разнице импульсов: если левое насчитало больше — замедлить левое и ускорить правое, как П-регулятор из урока 3.2. Робот уходит от прямой не больше чем на 3 см.
Повернуть на месте на 90°: колёса крутятся в разные стороны, каждое проходит четверть окружности диаметром в колею робота — 90 мм. Посчитать импульсы, проверить угол угольником. Ошибка — не больше 10°.
Проверь себя
Выберите ответы для подготовки. Они не сохраняются и не оцениваются на сайте. Пройдите проверку знаний в программе курса или обсудите ответы с преподавателем.
● Основное
★ Средний уровень
★★ Продвинутый уровень
Отвечай на вопросы своего уровня. Нужно верных ответов: Начальный — 4 из 5; Средний — 4 из 5; Продвинутый — 4 из 5. Ответы проверит преподаватель или программа курса.
Итог
Энкодер превращает оборот колеса в импульсы, а импульсы — в пройденный путь. Робот едет не «сколько успеет», а сколько нужно, при любом заряде батарей.
Как подтвердить результат
Результат опыта связывает условия, версию программы, наблюдение и вывод. Сравнение попыток с одним изменением помогает проверить причину, включая неудачные попытки. Отметка относится к уже выполненной практике; повтор ради отметки не нужен. Расхождение с прогнозом задаёт следующую проверяемую гипотезу. Модель, измерения настоящего робота и объяснение — разные виды доказательств.
Словарь урока
| энкодер | Датчик оборотов: диск с прорезями на оси колеса и датчик, который даёт импульс на каждую прорезь. |
| импульс | Короткий сигнал энкодера: прорезь прошла мимо датчика. |
| прерывание | Arduino откладывает основную программу и сразу выполняет короткую функцию, когда на контакт пришёл импульс. |
| volatile | Пометка переменной, которую меняет прерывание: программа каждый раз читает её заново. |
| длина окружности | Путь колеса за один оборот: π × диаметр. |
Регуляторы: релейный, П, ПД и ПИД на одной трассе
Робот сам измеряет, насколько точно едет, и команда по числам выясняет, какой регулятор лучше.
Результат урока
Команда сравнила релейный, П-, ПД- и ПИД-регуляторы на одной трассе и одной скорости по средней ошибке, наибольшей ошибке и числу переходов через линию и объяснила результат. Не нужен для итоговой задачи.
Ты научишься
- Провести честное сравнение: одна трасса, одна скорость, одно изменение за раз, три заезда.
- Объяснить, чем релейный регулятор отличается от П-регулятора.
- Объяснить, на что реагируют Д- и И-части регулятора.
- Сделать вывод о регуляторах по измерениям, а не по впечатлению.
Нужно знать
- 3.1 Датчики линии и первое следование
- 3.2 П-регулятор: плавно и быстро
Понадобится
Зачем этот урок
В курсе робот ездил по линии с двумя регуляторами: релейным в уроке 3.1 («колесо едет или стоит») и П-регулятором в уроке 3.2 («чем дальше линия, тем сильнее поворот»). Инженеры используют ещё два: ПД и ПИД. Какой из четырёх лучше? Сегодня ответим не «на глаз», а по числам: робот сам измерит, насколько точно проехал, а команда сравнит все четыре регулятора на одной трассе и одной скорости.
Факультатив не нужен для итоговой задачи. Он нужен тем, кто хочет понять, как устроены настоящие автопилоты, и проверить это опытом.
- Что делает колесо в программе урока 3.1, когда под его датчиком линия?
- Как в уроке 3.2 считали ошибку и поправку?
- Почему при подборе
kpменяли одно число за раз?
Как сравнить честно
8 минЧто должен делать робот. Роботу нужно удерживать линию при разных отклонениях. Сравним четыре способа выбирать команды колёсам на тех же датчиках и моторах, чтобы понимать, что меняет именно алгоритм.
Что используем и как это работает. Релейное правило выбирает одно из заранее заданных действий. П-составляющая реагирует на текущую ошибку, Д — на изменение ошибки между шагами, И — на накопленную ошибку. В этом скетче шаг фиксирован, поэтому коэффициенты настроены для его длительности. Накопление ограничивается, чтобы большая прошлая ошибка не давала чрезмерную поправку после возвращения к линии.
Как связать измерение с действием. Проводим заезды с одинаковой базовой командой и на одном поле, сравниваем отклонения и колебания. Больший коэффициент не означает лучший результат. Проверка формулы на числах помогает найти ошибку кода, но не заменяет испытания на трассе; коэффициенты теста не являются настройками вашего робота.
Чтобы сравнить регуляторы, всё остальное должно быть одинаковым: одна дорожка, одно место старта, одна скорость, одни и те же измерения белого и чёрного. Меняется только регулятор. Каждый регулятор едет три раза: один заезд может оказаться случайным.
- Выберите трассу сравнения: прямой участок и дуга 250 мм из урока 3.2. Отметьте карандашом место старта.
- Перерисуй в журнал таблицу — её вы заполните к концу урока.
| Регулятор | Заезд | Средняя ошибка | Наибольшая ошибка | Переходов через линию | Сошёл с линии? |
|---|---|---|---|---|---|
| Релейный | 1, 2, 3 | ||||
| П | 1, 2, 3 | ||||
| ПД | 1, 2, 3 | ||||
| ПИД | 1, 2, 3 |
Какой регулятор, по-твоему, проедет точнее всех, а какой — хуже всех? Запиши в журнал.
Трасса и старт отмечены, таблица перерисована, прогноз записан.
Сравнение честное, только если меняется одно: регулятор. Трасса, старт, скорость, измерения и длина заезда — одни и те же. Каждый регулятор — три заезда.
- Выберите трассу сравнения: прямой участок и дуга 250 мм из урока 3.2. Отметьте старт.
- Запиши в журнал список того, что должно оставаться одинаковым во всех заездах. Найди не меньше пяти пунктов.
- Перерисуй таблицу сравнения.
| Регулятор | Заезд | Средняя ошибка | Наибольшая ошибка | Переходов через линию | Сошёл с линии? |
|---|---|---|---|---|---|
| Релейный | 1, 2, 3 | ||||
| П | 1, 2, 3 | ||||
| ПД | 1, 2, 3 | ||||
| ПИД | 1, 2, 3 |
Расставь четыре регулятора от лучшего к худшему по средней ошибке и объясни порядок одной фразой.
Список одинаковых условий, таблица и прогноз с объяснением — в журнале.
Сначала спланируй опыт, потом пиши код.
- Выберите трассу сравнения: прямой участок и дуга 250 мм из урока 3.2. Отметьте старт.
- Запиши в журнал план опыта: - что меняется (одно); - что остаётся одинаковым (не меньше пяти пунктов); - что измеряем и почему этих чисел достаточно, чтобы назвать лучший регулятор; - сколько заездов делаем и какое число берём из трёх, если один заезд сильно отличается.
- Запиши прогноз: порядок четырёх регуляторов по средней ошибке.
Подсказка: Какое число брать из трёх заездов
Если один заезд из трёх сильно отличается, среднее арифметическое «утянет» его за собой. Есть число, которое на один случайный заезд не реагирует.
В журнале план опыта и прогноз. Ты знаешь, какое число из трёх заездов пойдёт в таблицу.
Робот измеряет сам
15 минПрограмма ведёт робота заданное время RUN_MS, а потом останавливается и сохраняет итоги заезда в память Arduino. Память не стирается, когда выключаешь питание. В начале каждого запуска программа печатает в монитор порта итоги прошлого заезда.
| Что печатает робот | Что значит |
|---|---|
| Средняя ошибка | Насколько в среднем линия уходила от середины. Чем меньше, тем точнее |
| Наибольшая ошибка | Самый сильный уход от линии за заезд |
| Переходов через линию | Сколько раз линия перескочила с одной стороны на другую: виляние |
- Создай скетч
LF2_Compare, перенеси измерения из журнала и свойkpиз урока 3.2.
// Наблюдение в 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); }}
- Поставь
MODE = 1(П-регулятор), загрузи программу. - Поставь робота на старт: линия между датчиками. Через секунду программа увидит старт, ещё через две — робот поедет.
- Робот должен останавливаться до конца дорожки. Если доезжает до края — уменьши
RUN_MS. Подобрал — большеRUN_MSне меняй.
Подключай кабель, держа робота в руке или на подставке. Если поставить его на белый стол, программа примет стол за старт и робот поедет.
- Возьми робота в руки, подключи кабель и открой монитор порта: появятся итоги прошлого заезда.
RUN_MS подобрано, робот останавливается до конца дорожки, в мониторе порта видны итоги заезда.
Как это устроено: Почему робот ждёт старта
Когда открываешь монитор порта, Arduino перезапускается. Если бы робот сразу ехал, он уехал бы со стола и записал бы «заезд» поверх настоящего. Поэтому программа ждёт, пока оба датчика целую секунду видят белое: так бывает, когда робот стоит на старте.
Программа ведёт робота заданное время RUN_MS, останавливает его и сохраняет итоги заезда в память, которая не стирается без питания. При следующем запуске итоги печатаются в монитор порта. Измерение заезда в шаблоне готово. Твоя часть — функция controller: сегодня в ней П-регулятор, на втором занятии добавишь Д и И.
- Создай скетч
LF2_Compareпо шаблону, перенеси измерения и свойkp. - Напиши в
controllerтолько П-часть — как в уроке 3.2.
// Наблюдение в 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); }}
- Поставь
MODE = 1, загрузи, поставь робота на старт. ПодбериRUN_MSтак, чтобы робот останавливался до конца дорожки, и больше его не меняй. - Найди в готовой части функцию
recordи ответь в журнале: когда она добавляет переход через линию и зачем там порог 10?
Подключай кабель, держа робота в руке или на подставке: на белом столе программа примет стол за старт.
Подсказка: Робот не трогается со старта
Программа ждёт, пока оба датчика целую секунду видят белое (больше 70 по шкале). Посмотри шкалу датчиков на старте.
controller возвращает П-поправку, RUN_MS подобрано, в журнале — ответ про record.
Каркас ниже берёт на себя измерение: итоги заезда копятся в record(error), сохраняются в память после RUN_MS и печатаются при следующем запуске. Остальное напишешь сам.
// Наблюдение в 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. Д и И — на втором занятии.
- Перенеси
levelи ошибку из урока 3.2. - Выполни требование 1 для режимов 0 и 1. Подбери
RUN_MSтак, чтобы робот останавливался до конца дорожки. - Найди в
recordпорог 10. Запиши в журнал, что изменится в числе переходов, если порог убрать, и почему.
Подключай кабель, держа робота в руке или на подставке: на белом столе программа примет стол за старт.
Подсказка: Робот не трогается со старта
waitForStart ждёт белого по шкале level. Пока level возвращает 0, робот не стартует никогда — это защита каркаса.
Режимы 0 и 1 работают, RUN_MS подобрано, в журнале — ответ про порог.
Релейный против П
20 мин- Поставь
MODE = 0— релейный регулятор из урока 3.1. Сделай три заезда. После каждого подключи кабель и перепиши итоги в таблицу. - Поставь
MODE = 1— П-регулятор. Ещё три заезда. - Сравни строки. Какой регулятор вилял больше? У какого меньше средняя ошибка?
| Что видишь | Что сделать |
|---|---|
| «Записанных заездов нет» после заезда | Заезд не доехал до конца RUN_MS: питание отключили раньше. Повтори |
| Робот поехал прямо со стола | Ты подключил кабель, когда робот стоял на белом. Держи его в руке |
| Релейный сходит с дуги | Это тоже результат: запиши «да» в «Сошёл с линии?» |
В таблице по три заезда релейного и П-регулятора. Ты можешь сказать, какой точнее и по какому числу это видно.
- Сделай по три заезда с
MODE = 0иMODE = 1. Итоги — в таблицу. - Для каждого регулятора выбери из трёх средних ошибок среднюю по величине — она и пойдёт в сравнение.
- Ответь в журнале: почему у релейного больше переходов через линию? Объясни через то, какие скорости получают колёса.
Подсказка: Почему средняя по величине
Если один заезд случайно вышел плохим, среднее арифметическое изменится сильно, а средняя по величине — нет.
Таблица для двух регуляторов заполнена, выбраны средние по величине, объяснение переходов записано.
- Проведи опыт по своему плану для режимов 0 и 1.
- Сравни не только средние ошибки, но и разброс трёх заездов: насколько повторяется результат каждого регулятора?
- Запиши в журнал объяснение через устройство регулятора: почему у релейного больше переходов и наибольшая ошибка, хотя оба видят одни и те же датчики.
Данные двух регуляторов, разброс и объяснение — в журнале. Прогноз из первого шага проверен для первой пары.
ПД: тормозим поворот заранее
14 минП-регулятор поворачивает, пока линия не вернулась на середину, и часто проскакивает её: робот виляет. Д-часть смотрит, как быстро меняется ошибка. Если линия быстро возвращается к середине, Д-часть заранее ослабляет поворот — как велосипедист, который начинает выпрямлять руль ещё до того, как выехал на линию.
// Наблюдение в 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 × (ошибка сейчас − ошибка на прошлом шаге).
- Посчитай на бумаге:
kp = 0.5,kd = 4, на прошлом шаге ошибка была 10, сейчас 25. - Поставь
MODE = 2и попробуйkd= 5, 10 и 20 — по одному заезду на дорожке. Выбериkd, при котором робот меньше всего виляет. - С выбранным
kdсделай три заезда и запиши итоги в таблицу.
Ты посчитал поправку: 12,5 + 60 = 72,5, программа отбросит дробную часть — 72. Выбран kd, в таблице три заезда ПД.
Как это устроено: Почему kd такой большой
Регулятор считает 100 раз в секунду. За одну сотую секунды ошибка меняется всего на несколько единиц, поэтому множитель kd нужен больше, чем kp.
П-регулятор часто проскакивает линию: поправка не ослабевает, пока ошибка не станет нулём. Д-часть реагирует на то, как быстро меняется ошибка, и гасит поворот заранее.
- Допиши Д-часть в
controllerпо цели в шаблоне.
kp = 0.5, kd = 4, на прошлом шаге ошибка 10, сейчас 25. Какую поправку вернёт controller для MODE = 2? Посчитай до запуска.
- Поставь
MODE = 2. Подбериkd: начни с 2.5 и удваивай, по одному заезду. Запиши, при какомkdробот начал дёргаться на прямой, и возьми предыдущее значение. - С выбранным
kd— три заезда в таблицу.
Подсказка: Где взять прошлую ошибку
Она уже лежит в lastError, если ты запоминаешь её в конце функции.
Подсказка: Робот дёргается даже при маленьком kd
Проверь знак: Д-часть прибавляется, а изменение ошибки — «сейчас минус раньше».
Прогноз совпал: 72. Д-часть написана, kd подобран удвоением, три заезда записаны.
Требование 2. Функция int controller(int mode, int error):
- режим 1 — только П-часть;
- режимы 2 и 3 — плюс Д-часть:
kd, умноженный на изменение ошибки с прошлого шага; - режим 3 — плюс И-часть: ошибки копятся в
errorSum, сумма не выходит за±SUM_LIMIT, поправкаki × errorSum; в других режимах сумма не копится; - прошлая ошибка обновляется на каждом шаге во всех режимах.
Приёмка. Напиши controller в тестовом скетче, загрузи и открой монитор порта. Когда все строки PASS, перенеси функцию в свою программу. Сегодня тест проверяет и И-часть — значит, её тоже нужно написать.
// Проверка функции 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)
- П-часть.
- Если режим 2 или 3 — прибавить Д-часть.
- Если режим 3 — обновить сумму, ограничить её, прибавить И-часть.
- Запомнить ошибку.
Подсказка: Фрагмент кода (3 из 3)
errorSum = constrain(errorSum + error, -SUM_LIMIT, SUM_LIMIT); — почему тест «пять шагов по 100» без этой строки даёт 175?
Тест: «Все проверки пройдены». kd подобран методом, три заезда ПД записаны.
ПИД: что даёт накопленная ошибка
11 минИ-часть складывает ошибки шаг за шагом. Если линия долго остаётся с одной стороны — например, на длинной дуге, — сумма растёт, и робот поворачивает всё сильнее. Чтобы сумма не росла бесконечно, её ограничивает SUM_LIMIT.
- Оставь свой
kd, поставьMODE = 3и попробуйki= 0.005, 0.01 и 0.02 — по одному заезду. - Выбери лучший
kiи сделай три заезда в таблицу.
Будет ли ПИД точнее ПД на нашей трассе? Запиши прогноз до заездов.
В таблице три заезда ПИД. Прогноз проверен.
И-часть копит ошибку. Пока линия долго с одной стороны, сумма растёт и усиливает поворот. Опасность — сумма продолжает тянуть в ту же сторону и после дуги.
- Допиши И-часть в
controllerпо цели в шаблоне. - Поставь
MODE = 3, подбериki: 0.005, 0.01, 0.02. Три заезда с лучшим — в таблицу. - Временно поставь
SUM_LIMIT = 30000и проедь один раз. Что изменилось после дуги? Верни 3000.
Подсказка: Как не выпустить сумму за предел
Та же команда, что держит скорость колёс в 0…255, подойдёт и для суммы.
И-часть написана, ki подобран, три заезда записаны. Ты видел, что делает робот без предела суммы.
- Поставь
MODE = 3, подбериkiметодом: от 0.005, удваивая. Три заезда с лучшим — в таблицу. - Опыт с пределом: один заезд с
SUM_LIMIT = 30000. Опиши, что происходит после дуги, и объясни через значениеerrorSum. - Ответь в журнале: в каких условиях И-часть даёт выигрыш и есть ли они на нашей трассе?
Подсказка: Когда И-часть полезна
Подумай о долгой ошибке одного знака: длинная дуга одного радиуса или робот, у которого одно колесо всегда крутится медленнее другого.
ki подобран, опыт с пределом описан, условия пользы И-части записаны.
Сравнение всех четырёх
12 мин- Выпиши для каждого регулятора среднюю ошибку из трёх заездов — среднюю по величине.
- Расставь регуляторы от лучшего к худшему.
| Место | Регулятор | Средняя ошибка | Переходов через линию |
|---|---|---|---|
| 1 | |||
| 2 | |||
| 3 | |||
| 4 |
- Сравни с прогнозом из начала урока. Что совпало, что нет?
Четыре регулятора расставлены по местам, прогноз проверен.
- Составь итоговую таблицу: регулятор, средняя по величине средняя ошибка, наибольшая ошибка, переходы, сходы.
- Назови лучший регулятор для этой трассы и скорости и обоснуй двумя числами.
- Ответь: оправдала ли И-часть своё усложнение? Почему?
- Какой регулятор ты взял бы для итоговой задачи и почему?
Итоговая таблица, обоснованный вывод и ответы — в журнале.
- Составь итоговую таблицу всех четырёх регуляторов с разбросом трёх заездов.
- Напиши вывод из трёх частей: что показали числа, чем это объясняется в устройстве регуляторов, что можно было сделать нечестно и как ты это исключил.
- Сравни результат с прогнозом и планом опыта из первого шага.
Итоговая таблица и вывод из трёх частей — в журнале.
Задания по уровням
8 минЗаполнить таблицу сравнения всех четырёх регуляторов на одной скорости, по три заезда, и назвать лучший по средней ошибке.
Подобрать kd, при котором ПД проходит трассу с меньшей средней ошибкой, чем П, и объяснить по числам почему.
Найти для каждого из четырёх регуляторов наибольшую baseSpeed, на которой он проходит трассу три раза из трёх: повышать шагами по 20. Составить рейтинг по скорости и сравнить его с рейтингом по средней ошибке.
Проверь себя
Выберите ответы для подготовки. Они не сохраняются и не оцениваются на сайте. Пройдите проверку знаний в программе курса или обсудите ответы с преподавателем.
● Основное
★ Средний уровень
★★ Продвинутый уровень
Отвечай на вопросы своего уровня. Нужно верных ответов: Начальный — 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 · П-регулятор
kp | baseSpeed | Что видно |
|---|---|---|
Выбрано: kp = ____ , baseSpeed = ____
Урок 4.1–4.2 · Перекрёстки и маршрут
| Порог перекрёстка | centerMs | leaveMs |
|---|---|---|
Урок 5.1–5.2 · Ультразвук и захват
| TRIG | ECHO | STOP_CM | Сервопривод | Угол «открыто» | Угол «закрыто» |
|---|---|---|---|---|---|
Урок 6.1 · План итоговой задачи
Уровень поля: начальный / средний / продвинутый. Карточка (для продвинутого): ____
| Участок | Что делает робот | Какая функция |
|---|---|---|
Где скорее всего будет ошибка и как я это проверю:
________________________________________________
Урок 6.2 · Отладка и зачёт
| Наблюдаю | Причина | Одно изменение | Проверка |
|---|---|---|---|
| Попытка | Баллы | Что исправили после неё |
|---|---|---|
| 1 | ||
| 2 |
Что у робота работает лучше всего и что я улучшу дальше:
________________________________________________