В третьей части практикума рассмотрим решения олимпиадных заданий регионального этапа ВсОШ за 2024–2025 учебный год. Разберёмся с алгоритмами управления роботом-тележкой.
Невозможно даже и думать о выполнении заданий олимпиады, если ты не в состоянии составить в голове алгоритм действий робота и находить в нём зависимости. Поэтому, прежде чем переходить к следующему разделу, обязательно пройди два уровня прокачки твоего логического мышления.
1. Игры Блокли, выделенные плюсом.
2. Миссии для робота от известного специалиста К. Ю. Полякова.
Участнику необходимо собрать робота, составить его электрическую структурную схему Э1, написать и отладить следующую программу.
Рисунок 1. Пример начального расположения кубиков
Рисунок 2. Пример правильного выполнения задания
Рисунок 3. Пример перемещения робота и зелёного куба
В конце файла с заданием есть критерии оценки. Глядя на них, становится ясно, что задания нужно выполнять в полном соответствии с примечаниями. И код грамотно оформлять, и писать комментарии.
| № | Критерий оценки | Макс. балл |
|---|---|---|
| 1 | Робот полностью выехал со старта (все точки вертикальной проекции робота покинули стартовую зону). | 3 |
| 2 | Робот однократно разместил зелёный куб в каждой пустой белой квадратной зоне, ограниченной красной линией. | 7×2 |
| 3 | Робот однократно разместил зелёный куб в одной из зелёных зон рядом с зоной финиша. | 3 |
| 4 | Зелёная зона, в которой робот разместил куб, находится строго напротив зоны начального размещения зелёного куба. | 4 |
| 5 | Робот полностью переместил синий куб из белой зоны изначального размещения. | −3×6 |
| 6 | Робот остановился в зоне финиша после успешного выполнения любой части заданий п. п. 2–4 (робот любым колесом соприкасается с зоной финиша). | 4 |
| 7 | Составлена электрическая структурная схема Э1 робота на базе Arduino (в соответствии с ГОСТ 2.702-2011). | 2 |
| 8 | Код программы оптимизирован (в коде используются циклы, ветвления, регуляторы). Код взаимосвязан с заданием и выполняет осмысленные действия. Программа компилируется без ошибок. | 2 |
| 9 | Читаемость кода (наличие комментариев к основным блокам кода, информативные имена переменных, выделение отступами циклов и так далее). | 1 |
| 10 | Отсутствие грубых ошибок в конструкции робота (незакрепленные или плохо закреплённые части, провод касается колеса и пола, шины соприкасаются с деталями шасси и так далее). | 2 |
Чтобы было проще и веселее составлять алгоритм для робота, носи с собой Lego-человечка или маленький предмет. Используй его, чтобы отслеживать текущее состояние робота и действия на поле.
Робот-тележка — это автоматически управляемое транспортное средство, которое перемещает грузы по заданному маршруту. Она полностью автономна, то есть для её управления не нужен человек.
Некоторые особенности использования роботов-тележек:
Роботы-тележки постепенно стали основой логистики, и без них не обходится ни один большой склад. Именно поэтому задачи с ними часто встречаются на олимпиадах. Быть может, кто-то из участников вдохновится и будет потом участвовать в разработке современных технологий для складов.
Сейчас можно выделить два типа устройств для автоматизированного перемещения грузов в промышленной среде, которые обеспечивают транспортировку без непосредственного участия оператора.
Автоматически управляемое транспортное средство (от англ. Automated Guided Vehicle, AGV) — это промышленное транспортное средство, которое можно предварительно запрограммировать для перевозки грузов на производстве, складе и между ними. Система AGV — это комплекс таких устройств, взаимодействующих друг с другом для выполнения широкого круга задач по перемещению грузов внутри предприятия.
Среди них:
Автономный мобильный робот (от англ. Autonomous Mobile Robot, AMR) — более сложный технически, но более простой в эксплуатации вариант AGV, в котором для автономного перемещения материалов без физических направляющих или маркеров используются бортовые датчики и процессоры. Он изучает своё окружение, запоминает своё местоположение и динамически планирует свой собственный путь от одной путевой точки до другой.
Существует три основных вида AGV и AMR: транспортные тележки, вилочные погрузчики и буксирующие устройства.
В цепи питания нашего робота есть пара важных элементов: понижающий преобразователь напряжения на основе микросхемы TPS54540 и литиевые аккумуляторы популярного формата 18650.
Стабилизаторы на микросхеме TPS54540 широко используются в робототехнике и самодельной электронике. Они позволяют подавать стабильное напряжение на моторы, сервоприводы и другие компоненты, чтобы обеспечить их надёжную работу и защиту от скачков напряжения.
Уровень выходного напряжения регулируется в диапазоне 0,8–25 В специальным винтом на подстроечном резисторе VOUT.
VIN и GND.VOUT и GND.Понижающий преобразователь питания нужен, чтобы твой робот сохранял скорость движения по мере разрядки аккумуляторов. Ведь физическая скорость влияет и на интервалы времени езды, которые ты выставляешь в своей программе.
Маркировка аккумуляторов содержит полезные сведения: форм-фактор, рабочее напряжение и ёмкость.
Например, наш тренировочный робот питается от двух аккумуляторов Robiton формата 18650 ёмкостью 2200 мА·ч с защитой. Но на олимпиаде тебе могут попасться десятки других вариантов. Главное — знать общие принципы.
Тебя больше всего интересует ёмкость аккумуляторов. От неё напрямую зависит время автономной работы робота.
Ёмкость обычно указывают для 20-часового цикла разрядки до остаточного напряжения. Таким образом, ёмкость 2200 мА·ч — это примерно 20 часов работы с током разрядки 1100 мА.
Давай подсчитаем, сколько тока потребляет наш робот? Для этого обратимся к документации и постараемся учесть всё оборудование на его борту.
| Устройство | Потребляемый ток | Количество |
|---|---|---|
| Arduino Uno | 50 мА | 1 |
| Драйвер моторов L298 | 36 мА | 1 |
| Ультразвуковой дальномер HC-SR04 | 15 мА | 1 |
| Аналоговый датчик линии | 10 мА | 2 |
| Инфракрасный дальномер | 30–40 мА | 1 |
| Микросервопривод FS90 | 100 мА | 1 |
| Светодиод | 20 мА | 2 |
| Коллекторный двигатель N20 | 200 мА | 2 |
| Итого | ~700 мА |
Исходя из этого, двух аккумуляторов на 2200 мА·ч хватит на добрых 6 часов активного использования нашего бота-олимпийца. Вполне достаточно на целый день тренировок!
На соревнованиях же часто выдают по два комплекта питания, чтобы можно было экстренно заменить подсевшие батареи. Но наездить даже час-полтора чистого времени вряд ли получится, ведь на один полигон претендует сразу 5–7 участников.
Перед окончательной сборкой робота в плату Arduino стоит прошить скетч для проверки моторов. Пока будешь собирать, как раз быстренько проверишь работоспособность.
На олимпиаде стоит поступать так же — избавишь себя от неожиданностей с железом! Никто не застрахован от небольшого процента брака и дефектов. В твоих же интересах быстренько проверить платы и электронные модули, что они хотя бы «заводятся» и работают.
К каким контактам подключать то или иное оборудование, будет видно из приведённого ниже фрагмента кода. Всё это тебе уже хорошо знакомо.
//описываем константы с помощью директивы препроцессора: #define DR 4 //пин для направления вращения правого мотора (M1) #define SR 5 //пин для управления скоростью вращения правого мотора (M1) #define DL 7 //пин для направления вращения левого мотора (M2) #define SL 6 //пин для управления скоростью вращения левого мотора(M2) #define IR A2 //пин для подключения инфракрасного дальномера #define UT 11 //пин для подключения ультразвукового дальномера (T) #define UE 10 //пин для подключения ультразвукового дальномера (R для эхо) #define GRP 9 //пин для подключения сервомотора для захвата #define LL A5 //пин для левого датчика "цвета" #define LR A4 //пин для правого датчика "цвета" #define EL1 3 //пин для первого сигнального контакта левого энкодера #define ER1 2 //пин для первого сигнального контакта правого энкодера #define LED_R 13 //пин для красного светодиода #define LED_G 12 //пин для зелёного светодиода
А что не очень знакомо, так это то, что директива #define — из старого доброго языка C (Си). И есть такой очень дискуссионный вопрос: что лучше использовать — #define или более современный синтаксис?
Да, в языке C определять символьные константы можно было только с помощью #define. А в языке С++ для создания именованных констант можно применять constexpr (или const).
Ключевое слово const сообщает компилятору, что переменная (или указатель) не может быть изменена. Однако это всё равно переменная, и от того, как она используется в коде, зависит потребление оперативной памяти.
Компилятор avr-gcc в Arduino IDE достаточно умён, чтобы понимать, что переменная с модификатором const не может быть изменена в рамках активной программы, и он постарается экономить за счёт этого оперативную память.
constexpr и #define эквивалентны, если использовать их только для простых целочисленных констант, таких как назначение выводов. А это именно наш случай!
//описываем константы constexpr int DR = 4; // для направления вращения правого мотора (M1) constexpr int SR = 5; // для управления скоростью вращения правого мотора constexpr int DL = 7; // для направления вращения левого мотора (M2) constexpr int SL = 6; // для управления скоростью вращения левого мотора constexpr int IR = A2; // для подключения инфракрасного дальномера constexpr int UT = 11; // для подключения ультразвукового дальномера (T) constexpr int UE = 10; // для подключения УЗ-дальномера (R для эхо) constexpr int GRP = 9; // для подключения сервомотора для захвата constexpr int LL = A5; // для левого датчика "цвета" constexpr int LR = A4; // для правого датчика "цвета" constexpr int EL1 = 3; // для сигнального контакта левого энкодера constexpr int ER1 = 2; // для сигнального контакта правого энкодера constexpr int LED_R = 13; // для красного светодиода constexpr int LED_G = 12; // для зелёного светодиода
Тут может возникнуть удивление: как же так, разве А4 — целое число? Да! Контакты A0–A5 имеют номера с 14 по 19, то есть целые числа. А в коде их незаметно «подменят» именно так, как ты укажешь через define.
constexpr уже морально нельзя использовать имена из заглавных букв. Надо применять «Верблюжий» стиль CamelCase — тем более, что код будут оценивать.Процесс сборки робота описан в отдельной инструкции.
В третьей части мы постараемся не повторять по кругу изученный материал, а углубить и расширить твои знания и возможности.
Возьмём уже знакомый инфракрасный дальномер GP2Y0A21YK0F. Эта модель предназначена для измерения расстояний до препятствий в диапазоне от 10 до 80 см.
Принцип работы датчика таков: излучатель испускает инфракрасные импульсы, которые отражаются от объектов в поле зрения сенсора и возвращаются на приёмник.
Отражённые импульсы собираются линзой и образуют собой некоторое «пятно» на детекторе положения (матрице).
При изменении положения объекта изменяется угол отражённого луча и положение пятна.
В результате датчик выдаёт аналоговый сигнал, который зависит от положения объекта перед датчиком.
А вот дальше существует масса формул и готовых библиотек для пересчёта показаний датчика в сантиметры. Сейчас мы разберёмся, как это правильно сделать.
Если скачать техническую спецификацию, можно увидеть график, на котором практически линейная функция: y = kx + b (красная линия).
Только она в таком виде: V = k ⋅ 1 / (L + 0,42) + b
Тебе нужно просто подставить две точки, которые видны на графике, получив систему из двух линейных уравнений с двумя неизвестными.
Отношение выходного напряжения датчика обратно пропорционально расстоянию и практически линейно на всём диапазоне измерений.
При этом нам не очень нужна высокая точность. Например, нас устроит зелёная линия, проходящая через начало координат. Следовательно, в уравнении b можно считать равным нулю. Значит, можно взять всего одну жёлтую точку и найти этот коэффициент k.
Имеем уравнение: V = k / L [1]
Тогда: L = k / V [2]
Подставляем в уравнение [1] данные выбранной точки: V = 2,3 и L = 10. Получаем k = 23.
Отлично! Теперь наше уравнение [2] выглядит так: L = 23 / V
Его легко запомнить. Только учти, что область допустимых значений L лежит в пределах [10, 80].
А у нас на аналоговом входе контроллера появляются числа от 0 до 1023. Нужно перевести их в вольты.
Чтобы разобраться с преобразованием результатов измерений, обратимся к технической документации.
Расчёт для ATmega328P (плата Arduino Uno и прямые аналоги)
Когда мы подключаем к микроконтроллеру внешние датчики, их сигнал часто является аналоговым по своей природе. Но микроконтроллер умеет обрабатывать только цифровые сигналы.
Следовательно, сигнал нужно пропустить через аналогово-цифровой преобразователь (АЦП, ADC), который преобразует аналоговый сигнал в цифровую форму, понятную для микроконтроллера.
Платы Arduino Uno имеют 6 встроенных каналов АЦП, которые можно использовать для считывания аналогового сигнала в диапазоне 0–5 В. Эти входы обозначены на плате как A0–A5 (иногда их ещё называют «аналоговые пины»).
Для считывания значения с одного из аналоговых входов используют функцию analogRead с аргументом в виде номера пина. Например, analogRead(A2) считает аналоговое значение на 2 пине.
Обычно платы Arduino имеют 10-битный АЦП. Это означает, что он будет выдавать цифровое значение в диапазоне 0…1023. Если мы учтём ноль, получится 1024 (2¹⁰) ступеней. Общее количество этих дискретных значений называют разрешением АЦП.
Пример оцифровки сигнала 0–5 В на АЦП разрядностью 2 бита (4 ступени) и 3 бита (8 ступеней)
Чем выше разрешение АЦП — тем более тонко «нарезан» диапазон аналоговых значений от нуля до величины опорного напряжения Vref. А значит, и точность преобразования сигналов выше.
В нашем случае 10-битное разрешение позволяет отличать сигналы с минимальным шагом в 4,8 мА (делим 5 В на максимальное значение 1023). Вполне достаточно для любительского применения!
Чтобы узнать напряжение с датчика на входе A2, используй следующую формулу:
V = analogRead(A2) * 5 / 1023
Посмотри ещё раз на график. Когда мы заменили его зелёной линией, это было сделано неспроста. Мы применили научный метод аппроксимации — заменили сложную зависимость более простой, но достаточно близкой по поведению.
Кроме того, по графику видно, что реальное расстояние будет чуть больше, чем вычисленное по нашей формуле значение. Разница небольшая, но об этом нужно помнить.
К счастью, нам в задании не нужна особая точность. Роботу достаточно знать, присутствует ли объект в этом диапазоне расстояний.
В формуле из документации указано напряжение Vin, которое питает датчик. Поэтому стоит измерить напряжение именно при питании от аккумуляторов, а не от USB.
Для этого воспользуйся парой проводков и мультиметром в режиме измерения постоянного напряжения DC.
Итак, мы научились переводить значения АЦП в вольты. Подставим их в удобную формулу для аппроксимации измерений дальномера, и получим на выходе сантиметры. За дело!
! — это отрицание, то есть логическая операция «НЕ».Ультразвуковой дальномер HC-SR04 совмещает в себе приёмник и передатчик ультразвукового сигнала. Принцип действия HC-SR04 основан на хорошо известном явлении эхолокации. Излучатель формирует акустический сигнал, который отражается от преграды и регистрируется приёмником датчика. Зная скорость распространения звука в воздухе и время запаздывания между излучённым и принятым сигналом, легко рассчитать расстояние до акустической преграды.
При выборе направления ультразвукового дальномера надо учитывать, что угол наблюдения — 30°, а эффективный угол — 15°.
Чтобы получить данные с ультразвукового дальномера, необходимо:
Trig импульс длительностью 10 мкс. Излучатель отправит 8 импульсов частотой 40 кГц.pulseIn, которая считывает длину сигнала на заданном порту (HIGH или LOW) и возвращает его время ожидания в микросекундах.Echo.Кстати, наше крепление позволяет настроить, куда «смотрит» датчик.
На заключительном этапе олимпиады в 2025 году проходной балл был 52 из 100. Робот смог выехать со старта — уже хорошо, остальные баллы — на проекте и теории. Как так?
Дело в том, что везде возможны проблемы, включая железо. Их нужно научиться прогнозировать до, а не во время работы.
Особенность ИК-дальномера в том, что по документации он должен работать до 80 см. Но если объект маленький, как кубик, то дальномер будет ошибаться и при 30 см.
В предыдущих практикумах объекты были близко, и этот нюанс нам не мешал. Поэтому датчик нужно выносить подальше в бок. Например, как на фотографиях.
Ещё одна проблема — пижонство! Вот хочется, чтобы ИК-дальномер выдавал сантиметры, а не непонятные числа. И это своего рода ловушка. Ты можешь потерять драгоценное время на реализацию формул, но при любых ошибках всё равно полезешь смотреть сырые данные от 0 до 1023.
Другими словами, делать «красивое» воплощение надо на шоу и выставках, а не на олимпиадах, где времени и так в обрез.
Не заморачивайся с формулами для обёртки данных и не усложняй себе задачу!
Следующая проблема — светодиоды. Они есть в требованиях к оборудованию, но их почти никто не подключает. А их обязательно стоит подключать по 2–3 штуки! Очень понадобится для отладки программы, ведь при движении робота ты не заглянешь в Serial.print…
Если быстро не понять, какой груз ждёт тебя и робота, то минут 15 улетит на подгонку захвата.
Посмотри на два захвата. Как думаешь, какой из них лучше? Когда робот будет переставлять зелёные кубики, точность всегда будет неидеальной. Поэтому захват робота должен покрывать почти весь белый квадрат, а не красиво «облегать» кубик.
Теперь о датчиках линии. Их лучше всего ставить в 2–2,5 см от поверхности, а не 1,6 см, как у леговских! А в технической документации вообще написано 1,4 см. Лишние перестановки обойдутся тебе как минимум в 5 минут времени.
Прежде чем писать процедуры, стоит составить список: какие точно будут, а какие писать не надо. Посмотри-ка на схему.
Да, основа всего — движение до перекрёстка, причём робот сперва должен съехать с линии перекрёстка. Понадобится езда вперёд/назад, так как роботу нужно будет развернуться после перемещения кубика. Если не отъехать назад, кубик собьётся. Вперёд тоже (посмотри на фото) иногда подвинуть надо.
Повороты точно нужны. Но какие именно? Вроде бы робот всегда поворачивает вправо. По схеме точно видно, что только направо. Но на схеме есть проблемные точки — нижние перекрёстки. В них будет 4 случая: понадобится и поворот налево, и разворот на 180°.
Рассмотри четыре схемы ниже, и всё поймёшь. Нужно ли после захвата кубика пятиться роботом назад? Он будет 6 раз подъезжать к ячейкам и может ошибиться при возврате к перекрёстку. Поэтому пока не будем пятиться.
Ну, вот теперь-то мы можем накидать список процедур и функций.
// быстро протестировать все сенсоры void sensorsTest(int s) {} //вывод данных с сенсоров //событие при срабатывании прерывания (правый энкодер) void eventR() {} // правый счётчик увеличиваем //событие при срабатывании прерывания (левый энкодер) void eventL() {} // левый счётчик увеличиваем // ставим захват в положение 90 градусов void grip90() {} // плавное поднятие захвата void up() {} // плавное опускание захвата void down() {} // движение вперёд по энкодерам (ПД-регулятор) void fwd(int speed, int limit) {} // движение назад по энкодерам (ПД-регулятор) void back(int speed, int limit) {} // помигать красным светодиодом (для визуализации) void blinkRed(int dt, int n) {} // помигать зелёным светодиодом (для визуализации) void blinkGreen(int dt, int n) {} // по линии до перекрёстка (серый цвет - gr) //1 шаг = по линии по энкодерам, 2 шаг = по линии до перекрёстка void toCross(int sL, int sR, int limit) {} // измерить расстояние до объекта в см int usCm() {} //ультразвуковой дальномер // поворот направо (скорость, предел по энкодерам) void turnRightFwd(int speed, int limit) {} // поворот налево (скорость, предел по энкодерам) void turnLeftFwd(int speed, int limit) {} // разворот танковый (скорость, предел по энкодерам) void turn180(int speed, int limit) {} // взять куб (едет от перекрёстка и возвращается) void getCube(int x) {} // положить куб (едет от перекрёстка и возвращается) void putCube(int x) {} // проверка, есть ли кубик справа и свободно ли место слева int checkDist(int cm, int ik, int n) {} // основной алгоритм (процедура) void run() {} // все настройки и запуск основного алгоритма работы робота void setup() {} // всегда пустой цикл void loop() {}
А ещё нужны константы (define чуть короче и проще) и глобальные переменные.
//описываем константы с помощью директивы препроцессора: #define DR 4 //пин для направления вращения правого мотора (M1) #define SR 5 //пин для управления скоростью вращения правого мотора (M1) #define DL 7 //пин для направления вращения левого мотора (M2) #define SL 6 //пин для управления скоростью вращения левого мотора(M2) #define IR A0 //пин для подключения инфракрасного дальномера #define UT 11 //пин для подключения ультразвукового дальномера (T) #define UE 10 //пин для подключения ультразвукового дальномера (R для эхо) #define GRP 9 //пин для подключения сервопривода захвата #define LL A3 //пин для левого датчика линии #define LR A1 //пин для правого датчика линии #define EL1 3 //пин для первого сигнального контакта левого энкодера #define ER1 2 //пин для первого сигнального контакта правого энкодера #define LED_R 13 //пин для красного светодиода #define LED_G 12 //пин для зелёного светодиода // описываем глобальные переменные volatile long countL = 0, countR = 0; // счётчики для энкодеров int delL = 10, delR = 10; // для настройки диапазона значений сенсоров линии int w = 62, b = 20, gr = 42; // на белом, на чёрном и среднее int dtStop = 500; // учёт инерции при каждой остановке после действия int turn = 520; // счётчик для поворота одним колесом на 90 градусов int irFindData = 200; // данные ИК, если больше - есть объект int usFindCm = 30; // данные УЗ, если больше - место для кубика свободно int crossEncoders = 150; //при старте движения по линии надо съехать с линии
define уже не принято использовать. Ничего страшного, это не ошибка. Может, нам так удобнее!Итак, ты разобрался с блоком «дефайнов» и подставил правильные номера контактов.
Лови ркомендации по глобальным переменным:
volatile long, чтобы их можно было изменять в любой момент через прерывания.Пройдёмся по всем процедурам и функциям.
// быстро протестировать все сенсоры void sensorsTest(int s) { //вывод данных с сенсоров while (!Serial) delay(5); unsigned int rst = millis(); while (millis() - rst < s * 1000) { //int dist1 = irCm(); int dist1 = analogRead(IR); int dist2 = usCm(); int ll = (1024 - analogRead(LL)) / 10; int lr = (1024 - analogRead(LR)) / 10; Serial.print("L="); Serial.print(ll); Serial.print("\t"); Serial.print("R="); Serial.print(lr); Serial.print("\t"); Serial.print("IR_data = "); Serial.print(dist1); Serial.print("\t"); Serial.print("US_cm = "); Serial.println(dist2); delay(50); } }
int ll = (1024 - analogRead(LL)) / delL
Так будет легче воспринимать показания, например: 10–20 на чёрном и 60–80 на белом.
Обрати внимание, что тут и ожидание нажатия на иконку «Монитор последовательного порта», и демонстрация как бы сброса таймера, хотя его сбросить и нельзя. Можно только запомнить текущее значение. И уже от него считать время дальше.
Нам нужны два обработчика прерываний: eventL() и eventR().
//событие при срабатывании прерываний (правый и левый энкодеры) void eventR() { // правый счётчик увеличиваем if (digitalRead(ER1)) countR++; } void eventL() { // левый счётчик увеличиваем if (digitalRead(EL1)) countL++; }
Идея для компактной записи кода: если нужно задать логическое условие == true, можно пропустить оператор == в сравнении.
if (digitalRead(ER1)) == countR++; //То же самое, только короче if (digitalRead(ER1)) countR++;
Счётчики всегда будут увеличиваться фоном при движении робота, куда бы не вращались колёса. Поэтому важно не забывать обнулять счётчики, когда хочешь сделать что-то с использованием энкодеров.
Прерывание — это сигнал, который сообщает процессору о наступлении какого-либо события. Процессор реагирует на этот сигнал, прервав выполнение текущих инструкций и передав управление обработчику прерывания (ISR).
Обработчик — это наша функция, которая должна выполняться при прерывании. После обслуживания прерывания функция завершает свою работу, а процессор продолжает выполнять код с того места, на котором остановился.
Обрати внимание, что мы отслеживаем у робота именно вторые датчики Холла (используем жёлтый выход энкодера), а не первые.
В нашем случае захват по умолчанию будет в поднятом состоянии. Это задано перед стартом основного алгоритма.
Поэтому нам понадобятся три процедуры для сервопривода:
Следующие процедуры — движение робота вперёд и назад с ограничением по показаниям энкодеров, а не продолжительности работы моторов. Процедуры будут отличаться только названием и направлением вращения (HIGH и LOW).
ПИД-регуляторы — это разновидность систем управления, которые используют обратную связь для управления процессом или системой.
Что отслеживает регулятор?
Чтобы поглубже вникнуть в тему, рекомендуем почитать учебный материал про ПИД-регуляторы.
Простейшие программки мигания светодиодом нужны, чтобы тебе было видно, нашёл ли робот объект. К примеру, ты можешь визуализировать выбор в условных операторах, или какая часть программы сейчас выполняется, чтобы её было проще отлаживать.
Потратив пару минут на написание светодиодной индикации, ты получишь выигрыш при первом же поиске недочёта или ошибки в своём алгоритме.
Следующая процедура — движение по линии до перекрёстка. При этом в начале робот должен проехать по линии какое-то расстояние, которое будет задаваться счётчиками энкодеров (точнее, их средним арифметическим). В предыдущих частях практикума мы ещё не рассматривали такую процедуру.
Стабилизацию движения робота по линии будет выполнять программный ПД-регулятор.
с.
Дальномеры уже разбирали, поэтому просто повторим наши любимые аппроксимированные формулы.
А теперь три поворота: направо, налево и разворот на 180° (по часовой стрелке).
Обрати внимание, что кучу кода здесь можно «копировать-вставить». Учись тиражировать похожие процедуры максимально быстро.
Очень помогут две дополнительные процедуры: взять кубик и положить его. При этом робот при старте находится на перекрёстке.
x будем указывать номер перекрёстка (от 0 до 3). Это важно, потому что на последнем перекрёстке с индексом 3 не всегда нужны действия.А теперь — функция проверки наличия объектов справа и слева, когда робот стоит на перекрёстке.
Идея такая:
n) и считаем, сколько раз робот увидел объект (то есть сравниваем с каким-то числом).Напомним, что для ИК-дальномера объект — это кубик, а для УЗ-дальномера — «дырка» в стене из кубиков.
С++ не Python, и на олимпиаде не будет времени делать вывод нескольких результатов. Поэтому нужно в один числовой результат спрятать (закодировать) сразу два числа. Раз УЗ-дальномер находится слева, а ИК — справа, то и кодировать надо аналогично.
Например, так:
// rez - это специальный код; может иметь 4 значения: // 0 - слева кубик, справа его нет (по умолчанию) // 1 - слева кубик, справа есть кубик // 10 - слева нет кубика, справа нет кубика // 11 - слева нет кубика, справа есть кубик
В итоге у нас получится такая функция.
Полученную функцию стоит потестировать, например:
void testCheckDist() { while (1) { int find = checkDist(30, 200, 10); Serial.print("Rez: "); Serial.println(find); if (find / 10 == 1) blinkRed(200, 2); if (find % 10 == 1) blinkGreen(200, 2); delay(2000); } }
Итак, нам осталось доделать только три процедуры.
Вся подготовительная часть написана, осталось составить основной алгоритм.
Если вкратце:
Начинаем составлять программу! Все нюансы — в комментариях к коду.
Посмотри демонстрационное видео с примером работы робота.
Крылатая фраза отсылает нас не только к проблемам во время важной миссии, но и ко второму значению слова «problem» — серьёзная задачка.
Вот и в нашем алгоритме есть одна проблема — кубику в розовую зону никак нельзя!
Внимательно посмотри на фото и попробуй решить задачу самостоятельно.
Участнику необходимо собрать робота, составить его электрическую принципиальную схему Э3, написать и отладить программу робота в соответствии с задачами.
Рисунок 1. Пример начального расположения кубиков
Рисунок 2. Пример правильного выполнения задания
Рисунок 3. Пример перемещения робота и зелёного куба
Обрати внимание, что в этом задании достаточно одного дальномера. Конечно, лучше использовать инфракрасный, но мы выберем ультразвуковой, так как он уже установлен на роботе с нужной стороны.
Не стоит находить закономерности сразу для трёх вертикальных зон. Лучше проходить их последовательно: 1, 2, 3.
Советы по обработке зон роботом:
С таким подходом в нашем алгоритме будет чуть меньше действий.
Например, если начать обрабатывать первую зону снизу, то зелёные кубики придётся отвозить чуть назад (во всех случаях, кроме всего одного). А третью зону, наоборот, лучше начать снизу. Переносить кубики справа налево будет проще.
Самая сложная зона — вторая. Поэтому и стоит разделить её на 3 части (отмечены зелёными прямоугольниками), чтобы не запутаться.
Попробуй проговорить последовательность действий: как робот поедет по первой зоне, как проверит объект, что будет делать при обнаружении/отсутствии объекта. И так далее.
Для решения этой задачи нам надо изменить и дописать некоторые процедуры. Кроме того, нам пригодятся три массива: один для хранения данных о кубиках слева (по ходу движения робота) и ещё два массива для кубиков справа (второй и третий вертикальные ряды). Логично, что после обработки первой зоны робот уже узнает, как расположены кубики справа во второй зоне. Аналогично, обработав вторую зону, он будет знать, где кубики в третьей зоне справа (а слева там заведомо пустые ячейки).
Добавим в блок к глобальным переменным описание массивов.
// кодируем наличие объектов в ячейках // 0 - пусто в ячейке; 1 - синий кубик; 2 - зелёный кубик int left[4] = { 0, 0, 0, 0 }; // все места/зоны по умолчанию пусты int right2[4] = { 0, 0, 0, 0 }; // все места/зоны по умолчанию пусты int right3[4] = { 0, 0, 0, 0 }; // все места/зоны по умолчанию пусты
Немного скорректируем три процедуры.
И добавим одну новую: сканирование ячейки ультразвуковым дальномером с запоминанием в массив, какой кубик в ней будет. Если ячейка занята, то ставим «1» (синий куб), если свободна, то записываем «2» (будет зелёный).
Посмотри видео с примером работы робота и его ускоренную версию.
Сделай перерыв и поразмышляй, как ты будешь кодить, в какой последовательности. Заряди аккумуляторы, осмотри робота. Пускай информация уляжется в голове.
Выдели день, когда ты сможешь позаниматься 3 часа, и попробуй написать алгоритм самостоятельно. Прочувствуй, сколько времени тебе реально потребуется. Это важный этап в подготовке.
А теперь уточнение: реши не всю задачу. Просто захвати и перевези один ближайший кубик — естественно, соблюдая все условия задачи и правила перестановки. Знаешь, почему?
Посмотри на критерии оценки. Сколько баллов ты получишь, если перевезёшь всего 1 куб?
3+2+2+3+4 = 14. За код будет 5 баллов. Прибавь сюда проект (+20) и теорию (+10), и получится уже 49 баллов. Напомним, что на заключительный этап для 10-х классов в 2025 проходной балл был 46!
В итоге нужно проверить, за какое время ты напишешь программу, чтобы робот просто перевёз кубик. Никуда не смотря и ничего не анализируя, а только двигаясь от перекрёстка до перекрёстка.
Напоминаем, что за схему Э3 дают 2 балла.
А вот и долгожданный итоговый алгоритм полного решения задания для 10-х классов. Он, конечно, не до конца оптимизирован.
Как всё сделаешь и проверишь на практике, обязательно займись поиском улучшений. Большое счастье, если внимательная шлифовка алгоритма приносит тебе удовольствие. Если нет — это всё равно очень полезное умение, которым не стоит пренебрегать!
Кстати, робот на видео с решением не укладывается в 3 минуты. Конечно, нужно уменьшить все задержки.
Осталось разобрать последнее задание регионального этапа ВсОШ 2024/2025 года для одиннадцатиклассников.
Участнику необходимо собрать робота, составить его электрическую принципиальную схему Э3, написать и отладить программу робота в соответствии с задачами.
Рисунок 1. Пример начального расположения кубиков
Рисунок 2. Пример правильного выполнения задания
Рисунок 3. Пример перемещения робота и зелёного куба
В принципе, все необходимые процедуры уже написаны. Скорее всего, ты уже примерно представляешь итоговый алгоритм.
Однако давай разобьём задачу на две:
Если корректно перевезти даже один кубик, можно заработать приличное количество баллов!
Посмотрим на схему и поразмыслим, как можно упрощённо решить первую задачу.
Используя полученные знания, реализуй обе задачи: перевезти один кубик и перевезти два кубика.
Для тренировок можно использовать вот такое поле.
Задание 1. Проехать роботом трассу и расставить кубики на дальние перекрёстки.
Задание 2. Проехать роботом трассу и расставить кубики на ближние перекрёстки.
Задание 3. Проехать роботом трассу и привезти все непарные кубики в зону старта.
Задание 4. Придумай какое-нибудь задание и найди решение самостоятельно.
Благодарность автору
«Амперка» выражает особую благодарность преподавателю Денису Геннадьевичу Копосову, чьё учебное пособие «Робототехника: подготовка к практическому туру регионального этапа Всероссийской олимпиады школьников» легло в основу данного практикума.
Все материалы использованы с разрешения автора. При перепечатке обязательна ссылка на оригинал.