В первой части практикума построим тренировочного робота «Олимпиец» (Olympic Bot) на Arduino, научим его ездить по линии и разберёмся с двумя примерами олимпиадных заданий за 2022–2023 учебный год.
Для большей эффективности стоит дополнительно пройти два курса:
По любым вопросам старайся сначала самостоятельно разобраться и найти ответ на многочисленных профильных сайтах про Arduino и робототехнику, например: wiki.amperka.ru, arduino.ru и так далее.
Готовый ответ, полученный от учителя или ИИ-помощника, ты забудешь на следующий день. А вот знания, «выстраданные» своими усилиями, останутся с тобой надолго и обязательно пригодятся на олимпиадах.
Всю сборку робота «Олимпийца» мы привели в отдельной инструкции по сборке с подробными шагами и красочными иллюстрациями.
Для начала познакомимся с электроникой, которая будет управлять роботом «Олимпиец».
Контроллер Iskra Uno — «мозг» будущего робота. Именно она будет выполнять программу, считывать показания датчиков и передавать команды моторам и сервоприводу. Iskra Uno совместима с платформой Arduino, которую мы будем использовать для программирования робота.
Платформа Iskra Uno предоставляет 32 физических контакта на штатных колодках Arduino R3 и 6 дублирующих контактов на разъёме ICSP для подключения дополнительных плат расширения, модулей и сенсоров.
| Имя пина | Описание |
|---|---|
| VIN | Входной пин для подключения внешнего источника напряжения в диапазоне от 7 до 12 В. Дублирует питание от внешнего DC-разъёма. |
| 5V | Выходной пин от стабилизатора напряжения с выходом 5 В и максимальным током 1 А. Регулятор обеспечивает питание микроконтроллера и другой обвязки платы. |
| 3V3 | Выходной пин от стабилизатора напряжения с выходом 3,3 В и максимальным током 300 мА. |
| IOREF | Выходной пин, который предоставляет платам расширения информацию о рабочем напряжении микроконтроллера. В нашем случае рабочее напряжение платформы 5 В. |
| AREF | Входной пин для подключения внешнего опорного напряжения АЦП, относительно которого происходят аналоговые измерения при использовании функции analogReference() с параметром EXTERNAL. |
| GND | Контакты общей земли. |
| Категория пинов | Количество пинов | Используемые пины | Описание |
|---|---|---|---|
| Пины ввода-вывода GPIO | 20+6 | 0–20, A0–A5 | Пины могут быть настроены как вход или выход. Логический уровень единицы — 5 В, нуля — 0 В. Диапазон входного напряжения от 0 до 5 В, при подаче большего напряжения микроконтроллер может выйти из строя. |
| ШИМ / PWM | 6 | 3, 5, 6 и 9–11 | Позволяет выводить аналоговое напряжение в виде ШИМ-сигнала из цифровых значений. Разрядность ШИМ не меняется и установлена в 8 бит. |
| АЦП / ADC | 6 | A0–A5 | Позволяет представить аналоговое напряжение в виде цифровом виде. Разрядность АЦП не меняется и установлена в 10 бит. Диапазон входного напряжения от 0 до 5 В, при подаче большего напряжения микроконтроллер может выйти из строя. |
Не пугайся количества контактов и обозначений. Сейчас запоминать их не нужно. По ходу практикума мы будем знакомиться с ними постепенно и использовать только те, которые нужны для конкретной задачи.
Остальную информацию про контроллер Iskra Uno ты найдёшь в нашей инструкции.
Плата расширения Troyka Shield служит для комфортного подключения датчиков к контроллерам формфактора Arduino R3.
Troyka Shield устанавливается на управляющие платы формфактора Arduino R3 сверху методом бутерброда.
Датчики подключаются через стандартные трёхпроводные шлейфы без пайки и макетной платы.
Подробнее про плату расширения Troyka Shield читай в нашей инструкции.
Контакты Iskra Uno не рассчитаны на питание двигателей напрямую — моторы потребляют значительно больший ток. Плата расширения Motor Shield берёт на себя силовую часть: получает управляющие сигналы от Iskra Uno и с их помощью управляет направлением и скоростью вращения моторов.
Motor Shield устанавливается на управляющие платы формфактора Arduino R3 сверху методом бутерброда.
Моторы подключаются через клеммники M1 и M2.
По умолчанию Motor Shield использует следующие пины Iskra Uno.
| Номер пина | Имя пина | Назначение |
|---|---|---|
| 5 | E1 | Управление скоростью вращения мотора на первом канале M1. |
| 6 | E2 | Управление скоростью вращения мотора на втором канале M2. |
| 4 | H1 | Направление вращения мотора на первом канале M1. |
| 7 | H2 | Направление вращения мотора на втором канале M2. |
Подробнее про плату расширения Motor Shield читай в нашей инструкции.
ON.
После всех манипуляций среда Arduino IDE должна определить подключённую плату Iskra Uno.
Если твой компьютер не видит Iskra Uno:
CTRL + C: копироватьCTRL + V: вставитьCTRL + /: закомментировать или раскоментировать фрагмент кода.Показывать номера строк и Сворачивание кода.Перед тем как перейти к управлению оборудованием робота, подготовим основные настройки программы:
Каждый модуль робота подключён к определённому пину контроллера. Чтобы не запоминать номера контактов и каждый раз не искать их в схеме подключения, дадим пинам понятные имена в самой программе.
Для назначения имён используем директиву препроцессора #define. Препроцессор заменяет заданное имя на соответствующее значение перед компиляцией программы. При этом оперативная память Arduino не расходуется.
Приведём пример, как назначить имена для всех задействованных пинов, и не запутаться.
// Даём понятное имя пину направления вращения левого мотора #define PIN_ML_DIR 7 // Даём понятное имя пину управления скоростью вращения левого мотора #define PIN_ML_SPEED 6 // Даём понятное имя пину направления вращения правого мотора #define PIN_MR_DIR 4 // Даём понятное имя пину управления скоростью вращения правого мотора #define PIN_MR_SPEED 5 // Даём понятное имя пину левого датчика линии #define PIN_LINE_L A0 // Даём понятное имя пину правого датчика линии #define PIN_LINE_R A1 // Даём понятные имена пинам левого энкодера A и B #define PIN_ENC_LA 2 #define PIN_ENC_LB 12 // Даём понятные имена пинам правого энкодера A и B #define PIN_ENC_RA 3 #define PIN_ENC_RB 13 // Даём понятное имя пину ИК-дальномера #define PIN_IRR A2 // Даём понятные имена пинам УЗ-дальномера TRIG и ECHO #define PIN_USR_T 11 #define PIN_USR_E 10 // Даём понятное имя пину сервопривода головы #define PIN_SRV_HEAD 8 // Даём понятное имя пину сервомотора захвата #define PIN_SRV_GRIP 9
Современный синтаксис C++ предпочитает выражения const и constexpr для объявления именованных констант. Мы разобрали это подробнее в третьей части практикума.
Во время работы «Олимпийцу» понадобится хранить различные данные: показания датчиков, рассчитанное расстояние и другие изменяющиеся значения. Для этого в программе используются переменные.
Чтобы было проще разобраться, как устроена программа и за что отвечает каждая её часть, сначала познакомься с несколькими базовыми понятиями:
int, float, bool, unsigned long и другие.Создадим базовые необходимые переменные в начале программы.
// Переменные для хранения данных с датчиков линии int senLineL, senLineR; // Переменная для хранения данных с ИК-дальномера int senIRR; // Переменная для хранения данных с УЗ-дальномера int senUSR;
Мы уже дали всем контактам понятные имена. Теперь нужно указать контроллеру, как будет использоваться каждый из них — для получения или передачи сигнала.
Для настройки пина используй функцию pinMode(). Настройку выполняем в функции setup(), поскольку этот код должен выполниться только один раз — при запуске контроллера.
void setup() { // Настраиваем пины управления моторами в режим выхода pinMode(PIN_ML_DIR, OUTPUT); pinMode(PIN_ML_SPEED, OUTPUT); pinMode(PIN_MR_DIR, OUTPUT); pinMode(PIN_ML_SPEED, OUTPUT); // Настраиваем пины управления сервоприводами в режим выхода pinMode(PIN_SRV_HEAD, OUTPUT); pinMode(PIN_SRV_GRIP, OUTPUT); // Настраиваем пины УЗ-дальномера // пин TRIG в режим выхода, пин ECHO в режим входа pinMode(PIN_USR_T, OUTPUT); pinMode(PIN_USR_E, INPUT); // Настраиваем пины датчиков линии в режим входа pinMode(PIN_LINE_L, INPUT); pinMode(PIN_LINE_R, INPUT); // Настраиваем пин ИК-дальномера в режим входа pinMode(PIN_IRR, INPUT); // Настраиваем пины энкодеров в режим входа pinMode(PIN_ENC_LA, INPUT); pinMode(PIN_ENC_LB, INPUT); pinMode(PIN_ENC_RA, INPUT); pinMode(PIN_ENC_RB, INPUT); }
В оформлении кода стоит придерживаться правила: «Лишь бы работало!» Не страшно, если везде будут глобальные переменные, а код не слишком оптимизированный и эффективный.
Базовая конфигурация готова: мы дали понятные имена используемым контактам, создали необходимые переменные и настроили режим работы выводов. Теперь программа знает, к каким контактам подключены основные элементы робота. Пора научить «Олимпийца» получать информацию об окружающем мире.
Основные моменты при проверке жюри программы:
Чтобы ориентироваться на трассе, «Олимпийцу» необходимо получать информацию об окружающем мире. Ориентироваться на трассе роботу помогают сразу несколько датчиков:
Ультразвуковой дальномер HC-SR04 служит для определения расстояния до объекта с помощью ультразвуковых волн. Диапазон измерений от 2 до 400 см.
Ультразвуковой дальномер HC-SR04 работает по принципу эхолокации.
| Фаза | Действие |
|---|---|
| 1. Запускаем измерение. | Подаём на пин TRIG импульс длительностью 10 мкс. |
| 2. Отправляем ультразвук. | Дальномер излучает ультразвуковую волну, частота которой находится за пределами человеческого слуха. |
| 3. Ловим отражение. | Волна достигает препятствия, отражается от него и возвращается к приёмнику дальномера. |
| 4. Измеряем время. | На пине ECHO появляется импульс, длительность которого соответствует времени прохождения звука до препятствия и обратно. |
| 5. Получаем расстояние. | Измеряем длительность импульса и по полученному времени рассчитываем расстояние до препятствия. |
Измерим расстояние с помощью УЗ-дальномера и выведем полученное значение в монитор порта.
// Даём понятные имена пинам УЗ-дальномера TRIG и ECHO #define PIN_USR_T 11 #define PIN_USR_E 10 void setup() { // Открываем COM-порт на работу со скоростью 9600 бит/сек Serial.begin(9600); // Настраиваем пины УЗ-дальномера // пин TRIG в режим выхода, пин ECHO в режим входа pinMode(PIN_USR_T, OUTPUT); pinMode(PIN_USR_E, INPUT); } void loop() { // Считываем расстояние с УЗ-дальномера int senUSR = readDistUSR(); // Выводим показания датчиков в монитор порта Serial.print("USR = "); Serial.println(senUSR); // Ждём 100 мс delay(100); } // Получение показаний с УЗ-дальномера int readDistUSR() { // Формируем импульс длительностью 10 мкс на пине TRIG digitalWrite(PIN_USR_T, LOW); delayMicroseconds(5); digitalWrite(PIN_USR_T, HIGH); delayMicroseconds(10); digitalWrite(PIN_USR_T, LOW); // Измеряем длительность импульса на пине ECHO int duration = pulseIn(PIN_USR_E, HIGH); // Переводим время прохождения сигнала в расстояние в см // Возвращаем измеренное расстояние return distance; }
Инфракрасный дальномер Sharp GP2Y0A21 служит для определения расстояния до объектов с помощью инфракрасного света. Диапазон измерений от 10 до 80 см.
Дальномер Sharp GP2Y0A21 работает в невидимом для глаза инфракрасном спектре.
| Фаза | Действие |
|---|---|
| 1. Излучаем инфракрасный свет. | Светодиод дальномера направляет узкий инфракрасный луч в сторону объекта. |
| 2. Ловим отражение. | Луч отражается от объекта и через линзу попадает на чувствительный элемент датчика — PSD. |
| 3. Определяем положение луча. | В зависимости от расстояния до объекта отражённый луч попадает на разные участки PSD. |
| 4. Получаем напряжение. | Дальномер преобразует положение луча в выходное напряжение: при изменении расстояния меняется и напряжение на выходе. |
| 5. Получаем расстояние. | Контроллер Iskra Uno считывает напряжение через аналоговый вход и преобразует его в расстояние. |
В рабочем диапазоне датчика положение светового пятна на PSD изменяется вместе с расстоянием до объекта. Электроника датчика преобразует это положение в выходное напряжение. Зависимость напряжения от расстояния нелинейная, поэтому для получения расстояния в сантиметрах потребуется преобразование показаний.
Измерим расстояние с помощью ИК-дальномера и выведем полученное значение в монитор порта.
// Даём понятное имя пину ИК-дальномера #define PIN_IRR A2 void setup() { // Открываем COM-порт на работу со скоростью 9600 бит/сек Serial.begin(9600); // Настраиваем пин ИК-дальномера в режим входа pinMode(PIN_IRR, INPUT); } void loop() { // Считываем расстояние с ИК-дальномера int senIRR = readDistIRR(); // Выводим показания датчиков в монитор порта Serial.print("IRR = "); Serial.println(senIRR); // Ждём 100 мс delay(100); } // Получение показаний с ИК-дальномера int readDistIRR() { // Считываем показания с ИК-дальномера в отчётах АЦП int value = analogRead(PIN_IRR); // Конвертируем показания в напряжение float voltage = value * (5.0 / 1024.0); // Конвертируем напряжение в расстояние в см // Возвращаем полученное значение return int(distance); }
Датчик линии определяет отражающую способность поверхности под роботом и позволяет отличать светлую поверхность от тёмной линии.
Датчик линии выполнен на оптопаре TCRT5000, которая состоит из двух элементов:
Когда светодиод излучает инфракрасный свет, световой поток отражается от поверхности и попадает на фототранзистор, где преобразуется в электрический сигнал. Тёмный цвет отражает меньше света, светлый — больше.
Контроллеры Arduino считывают показания с датчиков линии в диапазоне [0…1023]. При этом значения получаются инверсными: чёрному цвету соответствует большее значение, а белому цвету — меньшее.
Если тебе доводилось писать ПД-регулятор для Lego-робота, то привычнее работать со шкалой [0…100], где всё наоборот: чёрному цвету соответствует меньшее значение, а чёрному цвету — большее. Поэтому приведём показания Arduino к привычной Lego-шкале.
senLineL = (1024 - analogRead(PIN_LINE_L)) senLineR = (1024 - analogRead(PIN_LINE_R))
senLineL = (1024 - analogRead(PIN_LINE_L)) / 10; senLineR = (1024 - analogRead(PIN_LINE_R)) / 10;
map():senLineL = map(analogRead(PIN_LINE_L), 0, 1024, 100, 0); senLineR = map(analogRead(PIN_LINE_R), 0, 1024, 100, 0);
Считаем показания датчиков линии и выведем полученные значения в монитор порта.
// Даём понятное имя пину левого датчика линии #define PIN_LINE_L A0 // Даём понятное имя пину правого датчика линии #define PIN_LINE_R A1 void setup() { // Открываем COM-порт на работу со скоростью 9600 бит/сек Serial.begin(9600); // Настраиваем пины датчиков линии в режим входа pinMode(PIN_LINE_L, INPUT); pinMode(PIN_LINE_R, INPUT); } void loop() { // Считываем данные с датчиков линии readSenLines(); } // Получение показаний с датчиков линии void readSenLines() { // Считываем показание левого датчика линии int senLineL = map(analogRead(PIN_LINE_L), 0, 1024, 100, 0); // Считываем показание правого датчика линии int senLineR = map(analogRead(PIN_LINE_R), 0, 1024, 100, 0); // Выводим показания датчиков в монитор порта Serial.print("L = "); Serial.print(senLineL); Serial.print("\tR = "); Serial.print(senLineR); // Ждём 100 мс delay(100); }
Теперь проверим работу всех датчиков одновременно. Выведем показания сенсоров в монитор последовательного порта и убедимся в правильности работы.
// Даём понятное имя пину левого датчика линии #define PIN_LINE_L A0 // Даём понятное имя пину правого датчика линии #define PIN_LINE_R A1 // Даём понятное имя пину ИК-дальномера #define PIN_IRR A2 // Даём понятные имена пинам УЗ-дальномера TRIG и ECHO #define PIN_USR_T 11 #define PIN_USR_E 10 void setup() { // Открываем COM-порт на работу со скоростью 9600 бит/сек Serial.begin(9600); } void loop() { // Считываем показание левого датчика линии int senLineL = map(analogRead(PIN_LINE_L), 0, 1024, 100, 0);; // Считываем показание правого датчика линии int senLineR = map(analogRead(PIN_LINE_R), 0, 1024, 100, 0);; // Считываем расстояние с ИК-дальномера int senIRR = readDistIRR(); // Считываем расстояние с УЗ-дальномера int senUSR = readDistUSR(); // Выводим показания датчиков в монитор порта Serial.print("L = "); Serial.print(senLineL); Serial.print("\tR = "); Serial.print(senLineR); Serial.print("\tIRR = "); Serial.print(senIRR); Serial.print("\tUSR ="); Serial.println(senUSR); // Ждём 100 мс delay(100); } // Получение показаний с ИК-дальномера int readDistIRR() { // Считываем показания с ИК-дальномера в отчётах АЦП int value = analogRead(PIN_IRR); // Конвертируем показания в напряжение float voltage = value * (5.0 / 1024.0); // Конвертируем напряжение в расстояние в см // Возвращаем полученное значение return int(distance); } // Получение показаний с УЗ-дальномера int readDistUSR() { // Формируем импульс длительностью 10 мкс на пине TRIG digitalWrite(PIN_USR_T, LOW); delayMicroseconds(5); digitalWrite(PIN_USR_T, HIGH); delayMicroseconds(10); digitalWrite(PIN_USR_T, LOW); // Измеряем длительность импульса на пине ECHO int duration = pulseIn(PIN_USR_E, HIGH); // Переводим время прохождения сигнала в расстояние в см // Возвращаем измеренное расстояние return distance; }
В роботе «Олимпиец» используются два сервопривода, каждый из которых управляет своим механизмом:
Сначала разберёмся с принципом работы сервопривода и способами управления им, а затем применим полученные знания к обоим механизмам робота.
Сервопривод — это электромотор со встроенной системой управления и обратной связью, которая позволяет точно устанавливать и удерживать вал в заданном положении.
В нашем роботе используется аналоговый сервопривод Feetech FS90. Он поворачивает вал на заданный угол в диапазоне от 0 до 180° и обладает крутящим моментом до 1,5 кг·см.
Сервоприводы используются для открывания/закрывания заслонок, поворота различных механизмов на заданный градус и в качестве «суставов» при создании разнообразных механизмов. Для этого в комплекте сервы есть разные насадки, которые надеваются на вал мотора для дальнейшего соединения с механизмами устройств.
Как сервопривод понимает, куда повернуться? На управляющий провод сервопривода нужно периодически подавать короткие импульсы PDM (Pulse Duration Modulation). Импульсы повторяются с одинаковым периодом в 20 мс, но имеют разную длительность — от 544 до 2400 мкс. Эта длительность определяет положение вала сервопривода в диапазоне от 0 до 180°.
Для управления сервоприводом рассмотрим три способа:
map() — преобразуем заданный угол в ширину управляющего импульса с помощью встроенной функции в Arduino.Servo — преобразуем заданный угол в ширину управляющего импульса с помощью встроенной библиотеки в Arduino.Проверим все три способа на повороте головы и выберем наиболее удобный для дальнейшей работы.
// Даём понятное имя пину сервопривода головы #define PIN_SRV_HEAD 8 void setup() { } void loop() { // Устанавливаем вал сервопривода на 0° headMath(0); // Ждём 1 секунду delay(1000); // Устанавливаем вал сервопривода на 90° headMath(90); // Ждём 1 секунду delay(1000); // Устанавливаем вал сервопривода на 180° headMath(180); // Ждём 1 секунду delay(1000); // Устанавливаем вал сервопривода на 90° headMath(90); // Ждём 1 секунду delay(1000); } // Управление сервоприводом захвата с помощью формулы // Угол задаётся в градусах от 0° до 180° void headMath(int pos) { int wait = 10.3 * pos + 544; for (int i = 0; i <= 50; i++) { digitalWrite(PIN_SRV_HEAD, HIGH); delayMicroseconds(wait); digitalWrite(PIN_SRV_HEAD, LOW); delay(20); } }
// Даём понятное имя пину сервопривода головы #define PIN_SRV_HEAD 8 void setup() { } void loop() { // Устанавливаем вал сервопривода на 0° headMap(0); // Ждём 1 секунду delay(1000); // Устанавливаем вал сервопривода на 90° headMap(90); // Ждём 1 секунду delay(1000); // Устанавливаем вал сервопривода на 180° headMap(180); // Ждём 1 секунду delay(1000); // Устанавливаем вал сервопривода на 90° headMap(90); // Ждём 1 секунду delay(1000); } // Управление сервоприводом захвата с помощью функции map() // Угол задаётся в градусах от 0° до 180° void headMap(int pos) { // Преобразуем угол [0…180] в ширину импульса [544…2400] мкс int wait = map(pos, 0, 180, 544, 2400); // Отправляем серию управляющих импульсов for (int i = 0; i <= 50; i++) { digitalWrite(PIN_SRV_HEAD, HIGH); delayMicroseconds(wait); digitalWrite(PIN_SRV_HEAD, LOW); delay(20); } }
// Даём понятное имя пину сервопривода головы #define PIN_SRV_HEAD 8 // Библиотека для работы с сервоприводами #include <Servo.h> // Создаём объект для работы сервоприводом головы Servo head; void setup() { // Инициализируем сервопривод головы head.attach(PIN_SRV_HEAD); } void loop() { // Устанавливаем вал сервопривода на 0° head.write(0); // Ждём 1 секунду delay(1000); // Устанавливаем вал сервопривода на 90° head.write(90); // Ждём 1 секунду delay(1000); // Устанавливаем вал сервопривода на 180° head.write(180); // Ждём 1 секунду delay(1000); // Устанавливаем вал сервопривода на 90° head.write(90); // Ждём 1 секунду delay(1000); }
Ручной расчёт управляющих импульсов помог разобраться в принципе работы сервопривода. Для дальнейшей работы воспользуемся библиотекой Servo — она возьмёт формирование импульсов на себя и сделает управление сервоприводами значительно удобнее.
Проверим финальную работу сервопривода головы. В примере будем последовательно поворачивать голову робота в несколько заданных положений и наблюдать за движением сервопривода.
// Даём понятное имя пину сервопривода #define PIN_SRV_HEAD 8 // Библиотека для работы с сервоприводами #include <Servo.h> // Создаём объект для работы сервоприводом головы Servo head; // Задаём крайние углы поворота головы влево и вправо constexpr int HEAD_MAX_L = 0; constexpr int HEAD_MAX_R = 180; // Вычисляем угол для положения головы прямо constexpr int HEAD_STRAIGHT = (HEAD_MAX_L + HEAD_MAX_R) / 2; void setup() { // Инициализируем сервопривода захвата head.attach(PIN_SRV_HEAD); } void loop() { // Поворачиваем голову влево head.write(HEAD_MAX_L); // Ждём 1 секунду delay(1000); // Поворачиваем голову прямо head.write(HEAD_STRAIGHT); // Ждём 1 секунду delay(1000); // Поворачиваем голову вправо head.write(HEAD_MAX_R); // Ждём 1 секунду delay(1000); // Возвращаем голову прямо head.write(HEAD_STRAIGHT); // Ждём 1 секунду delay(1000); }
Проверим работу сервопривода захвата и подберём рабочий диапазон. Движение захвата ограничено корпусом робота, поэтому нужно определить опытным путём наклон захвата в крайних положениях:
GRIP_UP — угол в верхнем положенииGRIP_DOWN — угол в нижнем положенииИзменяй значения, пока не получишь нужный диапазон движения захвата.
// Даём понятное имя пину сервомотора захвата #define PIN_SRV_GRIP 9 // Библиотека для работы с сервоприводами #include <Servo.h> // Создаём объект для работы сервоприводом захвата Servo grip; // Задаём углы для верхнего и нижнего положения захвата // Значения углов подбираются экспериментально в диапазоне от 0 до 180 constexpr int GRIP_UP = 175; constexpr int GRIP_DOWN = 75; void setup() { // Инициализируем сервопривода захвата head.attach(PIN_SRV_GRIP); } void loop() { // Захват поднят grip.write(GRIP_UP); // Ждём 1 секунду delay(1000); // Захват опущен grip.write(GRIP_DOWN); // Ждём 1 секунду delay(1000); }
Колёса «Олимпийца» приводятся в движение коллекторными моторами GA12-N20 с редуктором и энкодером. Редуктор уменьшает скорость вращения двигателя и увеличивает крутящий момент, благодаря чему мотор лучше подходит для привода колёс робота. Независимое управление моторами позволяет роботу двигаться вперёд и назад, а также поворачивать влево и вправо.
Подключать моторы напрямую к Iskra Uno нельзя. Для работы им требуется значительно больший ток, чем может обеспечить контроллер. Поэтому между контроллером и моторами нужен драйвер моторов Motor Shield — он получает команды от Iskra Uno и управляет питанием двигателей.
Для начала разберём базовое управление моторами — научимся задавать направление, скорость вращения, а также останавливать моторы.
// Даём понятное имя пину направления вращения левого мотора #define PIN_ML_DIR 7 // Даём понятное имя пину управления скоростью вращения левого мотора #define PIN_ML_SPEED 6 // Даём понятное имя пину направления вращения правого мотора #define PIN_MR_DIR 4 // Даём понятное имя пину управления скоростью вращения правого мотора #define PIN_MR_SPEED 5 void setup() { // Настраиваем пины управления моторами в режим выхода pinMode(PIN_ML_DIR, OUTPUT); pinMode(PIN_ML_SPEED, OUTPUT); pinMode(PIN_MR_DIR, OUTPUT); pinMode(PIN_ML_SPEED, OUTPUT); } void loop() { // Задаём направление движение вперёд digitalWrite(PIN_ML_DIR, LOW); digitalWrite(PIN_MR_DIR, LOW); // Едем вперёд со скоростью 50% в течении трёх секунд analogWrite(PIN_ML_SPEED, 126); analogWrite(PIN_MR_SPEED, 126); delay(3000); // Едем вперёд со скоростью 100% в течении трёх секунд analogWrite(PIN_ML_SPEED, 255); analogWrite(PIN_MR_SPEED, 255); delay(3000); // Останавливаемся на пять секунд analogWrite(PIN_ML_SPEED, 126); analogWrite(PIN_MR_SPEED, 126); delay(5000); // Задаём направление движение назад digitalWrite(PIN_ML_DIR, HIGH); digitalWrite(PIN_MR_DIR, HIGH); // Едем назад со скоростью 50% в течение трёх секунд analogWrite(PIN_ML_SPEED, 126); analogWrite(PIN_MR_SPEED, 126); delay(3000); // Едем назад со скоростью 100% в течение трёх секунд analogWrite(PIN_ML_SPEED, 255); analogWrite(PIN_MR_SPEED, 255); delay(3000); }
Управлять каждым мотором отдельными командами неудобно — особенно когда программа робота станет больше. Поэтому объединим управление обоими моторами в одну функцию drive(). В функцию будем передавать скорость левого и правого мотора, при этом знак «минус» означает вращение в обратном направлении.
// Даём понятное имя пину направления вращения левого мотора #define PIN_ML_DIR 7 // Даём понятное имя пину управления скоростью вращения левого мотора #define PIN_ML_SPEED 6 // Даём понятное имя пину направления вращения правого мотора #define PIN_MR_DIR 4 // Даём понятное имя пину управления скоростью вращения правого мотора #define PIN_MR_SPEED 5 // Настраиваем направление вращения моторов // Если мотор вращается в обратную сторону, меняем false на true constexpr bool ML_INVERT = false; constexpr bool MR_INVERT = false; void setup() { // Настраиваем пины управления моторами в режим выхода pinMode(PIN_ML_DIR, OUTPUT); pinMode(PIN_ML_SPEED, OUTPUT); pinMode(PIN_MR_DIR, OUTPUT); pinMode(PIN_MR_SPEED, OUTPUT); } void loop() { // Едем вперёд со скоростью 50% motorsDrive(126, 126); delay(3000); // Едем вперёд со скоростью 100% motorsDrive(255, 255); delay(3000); // Останавливаемся motorsDrive(0, 0); delay(3000); // Едем назад со скоростью 50% motorsDrive(-126, -126); delay(3000); // Едем назад со скоростью 100% motorsDrive(-255, -255); delay(3000); // Останавливаемся motorsDrive(0, 0); delay(3000); } // Управление движением робота // Указываем скорость левого и правого мотора от 0 до 255, // где 0 — остановка, а 255 — максимальная скорость // Знак задаёт направление вращения: плюс — вперёд, минус — назад void motorsDrive(int ml, int mr) { // Определяем направление вращения левого мотора bool mlDir = ml < 0; // Определяем направление вращения правого мотора bool mrDir = mr < 0; // При необходимости инвертируем направление моторов if (ML_INVERT) mlDir = !mlDir; if (MR_INVERT) mrDir = !mrDir; // Задаём направление вращения моторов digitalWrite(PIN_ML_DIR, mlDir); digitalWrite(PIN_MR_DIR, mrDir); // Задаём скорость вращения моторов }
Ты наверняка заметил, что при движении робота немного уводит вправо или влево. Это нормально: двух абсолютно одинаковых моторов не бывает, и при одинаковой заданной скорости их редукторы всегда будут вращаться с погрешностью. Позже мы вернёмся к этому и научимся балансировать моторы.
Теперь, когда мы научились считывать показания датчиков и управлять моторами, пора переходить к более сложным элементам движения. Один из базовых приёмов — поворот робота на перекрёстке.
Робот может поворачивать тремя способами:



Для нашего алгоритма рассмотрим поворот вокруг одного из колёс. В этом случае один мотор вращается, а второй остаётся на месте:


Чтобы робот примерно понимал своё положение относительно перекрёстка, будем использовать датчики линии в качестве обратной связи.
Датчики линии расположены не строго по центру робота, а немного смещены по сторонам. Поэтому после завершения основного поворота робот может стоять немного под углом к линии. Чтобы выровнять его положение, нужно добавить небольшой доворот другим мотором в конце манёвра.
Алгоритм поворота робота направо:
Реализуем алгоритм поворота направо в программе. Напишем функцию turnR() с двумя аргументами:
speed — скорость вращения колеса.timeTurn — время в миллисекундах, необходимое для того, чтобы робот съехал с линии. Это время может отличаться у разных роботов и его нужно подобрать экспериментально.
// Функция поворота направо void turnR(int speed, int timeTurn) { // Включаем левый мотор вперёд motorsDrive(speed, 0); // Выжидаем время, чтобы левый датчик успел сойти с вертикальной чёрной линии delay(timeTurn); // Ожидаем сигнал с левого датчика о фиксации горизонтальной чёрной линии while (readLineL() > LINE_BLACK) { delay(10); } // Выключаем все моторы motorsDrive(0, 0); delay(500); // Включаем правый мотор вперёд motorsDrive(0, speed); // Выжидаем время, пока робот не выровняется прямо относительно линии delay(TIME_TURN_BACK); // Выключаем все моторы motorsDrive(0, 0); delay(250); }
Поворот налево выполняется аналогичным способом, но зеркально.
Ниже приведён полный программный код для этого раздела. Но мы советуем тебе не заглядывать туда сразу. Сначала попробуй написать функцию поворота налево самостоятельно!
// Даём понятное имя пину направления вращения левого мотора #define PIN_ML_DIR 7 // Даём понятное имя пину управления скоростью вращения левого мотора #define PIN_ML_SPEED 6 // Даём понятное имя пину направления вращения правого мотора #define PIN_MR_DIR 4 // Даём понятное имя пину управления скоростью вращения правого мотора #define PIN_MR_SPEED 5 // Даём понятное имя пину левого датчика линии #define PIN_LINE_L A0 // Даём понятное имя пину правого датчика линии #define PIN_LINE_R A1 // Настраиваем направление вращения моторов // Если мотор вращается в обратную сторону, меняем false на true constexpr bool ML_INVERT = false; constexpr bool MR_INVERT = false; // Задаем порог, ниже которого считаем, что датчик видит чёрную линию constexpr int LINE_BLACK = 20; // Задаём скорост вращения мотора при повороте constexpr int TURN_SPEED = 128; // Задаём время для сьезда с линии при повороте в миллисекундах constexpr int TURN_WAIT = 150; // Задаем время поворота робота назад для выравнивания по линии constexpr int TURN_BACK = 200; void setup() { // Настраиваем пины управления моторами в режим выхода pinMode(PIN_ML_DIR, OUTPUT); pinMode(PIN_ML_SPEED, OUTPUT); pinMode(PIN_MR_DIR, OUTPUT); pinMode(PIN_MR_SPEED, OUTPUT); // Настраиваем пины датчиков линии в режим входа pinMode(PIN_LINE_L, INPUT); pinMode(PIN_LINE_R, INPUT); // Делаем паузу 10 секунд, чтобы успеть поставить робота на поле delay(10000); } void loop() { // Поворачиваем направо turnR(TURN_SPEED, TURN_WAIT); // Делаем паузу 10 секунд delay(10000); // Поворачиваем налево turnL(TURN_SPEED, TURN_WAIT); // Делаем паузу 10 секунд delay(10000); } // Функция поворота направо void turnR(int speed, int waitTime) { // Включаем левый мотор motorsDrive(speed, 0); // Даем роботу время съехать с линии delay(waitTime); // Ждём, пока левый датчик снова увидит линию while (readLineL() > LINE_BLACK) { delay(10); } // Останавливаемся после обнаружения линии motorsDrive(0, 0); delay(500); // Немного доворачиваем назад, чтобы выровнять робота motorsDrive(0, speed); delay(TURN_BACK); // Останавливаем моторы motorsDrive(0, 0); delay(250); } // Функция поворота налево void turnL(int speed, int waitTime) { // Включаем правый мотор motorsDrive(0, speed); // Даем роботу время съехать с линии delay(waitTime); // Ждём, пока правый датчик снова увидит линию while (readLineR() > LINE_BLACK) { delay(10); } // Останавливаемся после обнаружения линии motorsDrive(0, 0); delay(500); // Немного доворачиваем назад, чтобы выровнять робота motorsDrive(speed, 0); delay(TURN_BACK); // Останавливаем моторы motorsDrive(0, 0); delay(250); } // Функция управления моторами void motorsDrive(int ml, int mr) { // Определяем направление вращения левого мотора bool mlDir = ml < 0; // Определяем направление вращения правого мотора bool mrDir = mr < 0; // При необходимости инвертируем направление моторов if (ML_INVERT) mlDir = !mlDir; if (MR_INVERT) mrDir = !mrDir; // Задаём направление вращения моторов digitalWrite(PIN_ML_DIR, mlDir); digitalWrite(PIN_MR_DIR, mrDir); // Задаём скорость вращения моторов } // Считывание левого датчика линии int readLineL() { return map(analogRead(PIN_LINE_L), 0, 1024, 100, 0); } // Считывание правого датчика линии int readLineR() { return map(analogRead(PIN_LINE_R), 0, 1024, 100, 0); }
Обрати внимание: задержки, приведённые в коде, подобраны экспериментально и могут отличаться от тех значений, которые нужны именно тебе. Прогони алгоритм несколько раз, изменяя задержки, пока робот не будет стабильно поворачивать на 90° на перекрёстке.
Заметь, что у моторов есть инерция, и после отключения напряжения они ещё некоторое время продолжают вращаться. Чтобы этого избежать, перед полной остановкой робота можно «растормозить» моторы кратковременным включением в обратную сторону. Тогда робот остановится ближе к ожидаемой точке.
Иногда после поворота роботу нужно просто вернуться обратно в исходную точку. Для возвратного поворота нужно перевернуть последовательность действий обычного поворота. Рассмотрим алгоритм возвращения после поворота направо.
Реализуем этот алгоритм в программе. Для обратного поворота напишем функцию turnBackR(). Она принимает два аргумента:
speed — скорость вращения колеса.waitTime — время в миллисекундах, необходимое для того, чтобы робот выровнялся по линии (подбирается экспериментально).
// Функция возврата после поворота направо void turnBackR(int speed, int waitTime) { // Включаем левый мотор в обратную сторону motorsDrive(-speed, 0); // Ждём, пока левый датчик снова увидит линию while (readLineL() > LINE_BLACK) { delay(10); } // Даем время, чтобы выровнять робота delay(waitTime); // Останавливаем моторы motorsDrive(0, 0); delay(250); }
Если в тестах что-то идёт не так, смело делай программный «костыль» — быстро написанный код, который хоть как-то решает проблему. Например, добавь ещё небольшое движение вперёд и поворот направо до линии, чтобы окончательно выровнять робота.
Возврат после поворота налево выполняется аналогичным способом, но зеркально.
Ниже приведён полный программный код для этого раздела. Но мы советуем тебе не заглядывать туда сразу. Сначала попробуй написать функцию возвратного поворота самостоятельно!
// Даём понятное имя пину направления вращения левого мотора #define PIN_ML_DIR 7 // Даём понятное имя пину управления скоростью вращения левого мотора #define PIN_ML_SPEED 6 // Даём понятное имя пину направления вращения правого мотора #define PIN_MR_DIR 4 // Даём понятное имя пину управления скоростью вращения правого мотора #define PIN_MR_SPEED 5 // Даём понятное имя пину левого датчика линии #define PIN_LINE_L A0 // Даём понятное имя пину правого датчика линии #define PIN_LINE_R A1 // Настраиваем направление вращения моторов // Если мотор вращается в обратную сторону, меняем false на true constexpr bool ML_INVERT = false; constexpr bool MR_INVERT = false; // Задаем порог, ниже которого считаем, что датчик видит чёрную линию constexpr int LINE_BLACK = 20; // Задаём скорост вращения мотора при повороте constexpr int TURN_SPEED = 128; // Задаём время для сьезда с линии при повороте в миллисекундах constexpr int TURN_WAIT = 150; // Задаем время поворота робота назад для выравнивания по линии constexpr int TURN_BACK = 200; void setup() { // Настраиваем пины управления моторами в режим выхода pinMode(PIN_ML_DIR, OUTPUT); pinMode(PIN_ML_SPEED, OUTPUT); pinMode(PIN_MR_DIR, OUTPUT); pinMode(PIN_MR_SPEED, OUTPUT); // Настраиваем пины датчиков линии в режим входа pinMode(PIN_LINE_L, INPUT); pinMode(PIN_LINE_R, INPUT); // Делаем паузу 10 секунд, чтобы успеть поставить робота на поле delay(10000); } void loop() { // Поворачиваем направо turnBackR(TURN_SPEED, TURN_WAIT); // Делаем паузу 10 секунд delay(10000); // Поворачиваем налево turnBackL(TURN_SPEED, TURN_WAIT); // Делаем паузу 10 секунд delay(10000); } // Функция возврата после поворота направо void turnBackR(int speed, int waitTime) { // Включаем левый мотор в обратную сторону motorsDrive(-speed, 0); // Ждём, пока левый датчик снова увидит линию while (readLineL() > LINE_BLACK) { delay(10); } // Даем время, чтобы выровнять робота delay(waitTime); // Останавливаем моторы motorsDrive(0, 0); delay(250); } // Функция возврата после поворота налево void turnBackL(int speed, int waitTime) { // Включаем левый мотор в обратную сторону motorsDrive(0, -speed); // Ждём, пока левый датчик снова увидит линию while (readLineR() > LINE_BLACK) { delay(10); } // Даем время, чтобы выровнять робота delay(waitTime); // Останавливаем моторы motorsDrive(0, 0); delay(250); } // Функция управления моторами void motorsDrive(int ml, int mr) { // Определяем направление вращения левого мотора bool mlDir = ml < 0; // Определяем направление вращения правого мотора bool mrDir = mr < 0; // При необходимости инвертируем направление моторов if (ML_INVERT) mlDir = !mlDir; if (MR_INVERT) mrDir = !mrDir; // Задаём направление вращения моторов digitalWrite(PIN_ML_DIR, mlDir); digitalWrite(PIN_MR_DIR, mrDir); // Задаём скорость вращения моторов } // Считывание левого датчика линии int readLineL() { return map(analogRead(PIN_LINE_L), 0, 1024, 100, 0); } // Считывание правого датчика линии int readLineR() { return map(analogRead(PIN_LINE_R), 0, 1024, 100, 0); }
Каждому роботу необходимо уметь выполнять разворот на 180°. При этом не имеет значения, в какую сторону будет поворачивать робот — по часовой стрелке или против.
Рассмотрим алгоритм поворота вокруг своей оси на месте:
Как и в прошлом примере, будем использовать датчики линии для получения обратной связи. Алгоритм здесь будет следующим:
Реализуем этот алгоритм в программе. Для разворота на 180° по часовой стрелке мы добавили функцию turn180()_R. Как и функция поворота из предыдущего примера, она принимает 2 аргумента:
speed — скорость вращения колёс.waitTime — время в миллисекундах, необходимое для того, чтобы робот съехал с линии (подбирается экспериментально).
// Функция разворота на 180° по часовой стрелке (направо) void turn180_R(int speed, int waitTime) { // Включаем моторы в разные стороны motorsDrive(speed, -speed); // Даем роботу время съехать с линии delay(waitTime); // Ждём, пока датчик снова увидит линию while (readLineL() > LINE_BLACK) { delay(10); } // Останавливаемся после обнаружения линии motorsDrive(0, 0); delay(200); // Немного доворачиваем назад, чтобы выровнять робота motorsDrive(-speed, speed); delay(TURN_BACK); // Останавливаем моторы motorsDrive(0, 0);; delay(250); }
Имея функцию разворота по часовой стрелке, довольно легко сделать разворот в обратную сторону. Достаточно лишь заменить все «правосторонние» действия на «левосторонние», и наоборот.
Ниже приведён полный программный код для этого раздела. Но мы советуем тебе не заглядывать туда сразу. Сначала попробуй написать функции разворота робота в разные стороны самостоятельно. На олимпиаде на это не будет времени, а вот на тренировке стоит попробовать все варианты!
// Даём понятное имя пину направления вращения левого мотора #define PIN_ML_DIR 7 // Даём понятное имя пину управления скоростью вращения левого мотора #define PIN_ML_SPEED 6 // Даём понятное имя пину направления вращения правого мотора #define PIN_MR_DIR 4 // Даём понятное имя пину управления скоростью вращения правого мотора #define PIN_MR_SPEED 5 // Даём понятное имя пину левого датчика линии #define PIN_LINE_L A0 // Даём понятное имя пину правого датчика линии #define PIN_LINE_R A1 // Настраиваем направление вращения моторов // Если мотор вращается в обратную сторону, меняем false на true constexpr bool ML_INVERT = false; constexpr bool MR_INVERT = false; // Задаем порог, ниже которого считаем, что датчик видит чёрную линию constexpr int LINE_BLACK = 20; // Задаём скорост вращения мотора при повороте constexpr int TURN_SPEED = 200; // Задаём время для сьезда с линии при повороте в миллисекундах constexpr int TURN_WAIT = 500; // Задаем время поворота робота назад для выравнивания по линии constexpr int TURN_BACK = 200; void setup() { // Настраиваем пины управления моторами в режим выхода pinMode(PIN_ML_DIR, OUTPUT); pinMode(PIN_ML_SPEED, OUTPUT); pinMode(PIN_MR_DIR, OUTPUT); pinMode(PIN_MR_SPEED, OUTPUT); // Настраиваем пины датчиков линии в режим входа pinMode(PIN_LINE_L, INPUT); pinMode(PIN_LINE_R, INPUT); // Делаем паузу 10 секунд, чтобы успеть поставить робота на поле delay(10000); } void loop() { // Поворачиваем направо turn180_R(TURN_SPEED, TURN_WAIT); // Делаем паузу 10 секунд delay(10000); // Поворачиваем налево turn180_L(TURN_SPEED, TURN_WAIT); // Делаем паузу 10 секунд delay(10000); } // Функция разворота на 180° по часовой стрелке (направо) void turn180_R(int speed, int waitTime) { // Включаем моторы в разные стороны motorsDrive(speed, -speed); // Даем роботу время съехать с линии delay(waitTime); // Ждём, пока датчик снова увидит линию while (readLineL() > LINE_BLACK) { delay(10); } // Останавливаемся после обнаружения линии motorsDrive(0, 0); delay(200); // Немного доворачиваем назад, чтобы выровнять робота motorsDrive(-speed, speed); delay(TURN_BACK); // Останавливаем моторы motorsDrive(0, 0);; delay(250); } // Функция разворота на 180° против часовой стрелке (налево) void turn180_L(int speed, int waitTime) { // Включаем моторы в разные стороны motorsDrive(-speed, speed); // Даем роботу время съехать с линии delay(waitTime); // Ждём, пока датчик снова увидит линию while (readLineR() > LINE_BLACK) { delay(10); } // Останавливаемся после обнаружения линии motorsDrive(0, 0); delay(200); // Немного доворачиваем назад, чтобы выровнять робота motorsDrive(speed, -speed); delay(TURN_BACK); // Останавливаем моторы motorsDrive(0, 0);; delay(250); } // Функция управления моторами void motorsDrive(int ml, int mr) { // Определяем направление вращения левого мотора bool mlDir = ml < 0; // Определяем направление вращения правого мотора bool mrDir = mr < 0; // При необходимости инвертируем направление моторов if (ML_INVERT) mlDir = !mlDir; if (MR_INVERT) mrDir = !mrDir; // Задаём направление вращения моторов digitalWrite(PIN_ML_DIR, mlDir); digitalWrite(PIN_MR_DIR, mrDir); // Задаём скорость вращения моторов } // Считывание левого датчика линии int readLineL() { return map(analogRead(PIN_LINE_L), 0, 1024, 100, 0); } // Считывание правого датчика линии int readLineR() { return map(analogRead(PIN_LINE_R), 0, 1024, 100, 0); }
Итак, переходим к движению по линии. Так как времени обычно немного и нет соревнования по скорости роботов, для движения по линии вполне достаточно П-регулятора.
П-регулятор работает просто: чем больше ошибка, тем сильнее корректировка. Именно поэтому робот сам подстраивает своё движение под линию и удерживается на траектории в течение всего времени движения.
Идея здесь простая. Сначала договоримся, что в идеальном случае показания левого и правого датчиков линии одинаковые. Тогда разность между ними равна нулю. Это и есть целевое значение, или уставка.
Если показания датчиков отличаются, значит робот отклонился от линии. Такое отклонение называют ошибкой. В нашем случае ошибкой будет разность между показаниями левого и правого датчика.
err = leftLine - rightLineПосле вычисления ошибки нужно сформировать управляющее воздействие. Оно вычисляется как произведение ошибки на коэффициент Кp. Этот коэффициент показывает, насколько сильно робот будет реагировать на отклонение. Если Кp увеличить, робот начнёт подруливать резче. Если сделать Кp слишком маленьким, робот будет реагировать медленно и может не успевать возвращаться на линию.
out = Kp * errПосле вычисления управляющего воздействия его нужно применить к скорости моторов. В зависимости от того, с какой стороны робот отклонился от линии, скорость одного мотора увеличивается, а другого уменьшается. Благодаря этому робот поворачивает в нужную сторону и возвращается на линию.
motorsDrive(speed + out, speed - out)Чтобы лучше понять работу регулятора, разберём несколько терминов.
x = 2 × y, то одна величина в два раза больше другой.
Напишем функцию followLine() с алгоритмом следования по линии с П-регулятором. На входе функция будет принимать два параметра:
speed - скорость, с которой должен двигаться робот.followTime - время, в течение которого робот должен ехать по линии.
// Функция следования по линии void followLine(int speed, int followTime) { // Задаём пропорционаальный коэффициеент П-регулятора int Kp = 5; // Сохраняем время начала движения int startTime = millis(); // Следуем по линии в течение задангого времени waitTime while (millis() - startTime < followTime) { // Считываем показания датчиков линии int leftLine = readLineL(); int rightLine = readLineR(); // Расчитываем оибку int err = leftLine - rightLine; // Формируем управляющий сигнал int out = Kp * err ; // Ограничиваем максимальное и минимальное значения управляющего сингала out = constrain(out, 0, 200); // Ругулируем скорость моторов в соответсвии с управляющим сигналом motorsDrive(speed + out, speed - out); } // Останавливаем моторы motorsDrive(0, 0); }
Ниже мы привели полный программный код для этого раздела. Поставь робота под углом к линии, чтобы протестировать надёжность стабилизации движения. Попробуй изменять коэффициент Кp в большую или меньшую сторону и посмотри, как это влияет на поведение робота.
// Даём понятное имя пину направления вращения левого мотора #define PIN_ML_DIR 7 // Даём понятное имя пину управления скоростью вращения левого мотора #define PIN_ML_SPEED 6 // Даём понятное имя пину направления вращения правого мотора #define PIN_MR_DIR 4 // Даём понятное имя пину управления скоростью вращения правого мотора #define PIN_MR_SPEED 5 // Даём понятное имя пину левого датчика линии #define PIN_LINE_L A0 // Даём понятное имя пину правого датчика линии #define PIN_LINE_R A1 // Настраиваем направление вращения моторов // Если мотор вращается в обратную сторону, меняем false на true constexpr bool ML_INVERT = false; constexpr bool MR_INVERT = false; // Задаём скорост моторов constexpr int FOLLOW_SPEED = 150; // Задаём время, в течение которого робот делжен ехать constexpr int FOLLOW_TIME = 10000; void setup() { // Настраиваем пины управления моторами в режим выхода pinMode(PIN_ML_DIR, OUTPUT); pinMode(PIN_ML_SPEED, OUTPUT); pinMode(PIN_MR_DIR, OUTPUT); pinMode(PIN_MR_SPEED, OUTPUT); // Настраиваем пины датчиков линии в режим входа pinMode(PIN_LINE_L, INPUT); pinMode(PIN_LINE_R, INPUT); // Делаем паузу 10 секунд, чтобы успеть поставить робота на поле delay(10000); // Даем команду роботу ехать по линии followLine(FOLLOW_SPEED, FOLLOW_TIME); } void loop() { // В этом примере loop() ничего не делает } // Функция следования по линии void followLine(int speed, int followTime) { // Задаём пропорционаальный коэффициеент П-регулятора int Kp = 5; // Сохраняем время начала движения int startTime = millis(); // Следуем по линии в течение задангого времени waitTime while (millis() - startTime < followTime) { // Считываем показания датчиков линии int leftLine = readLineL(); int rightLine = readLineR(); // Расчитываем оибку int err = leftLine - rightLine; // Формируем управляющий сигнал int out = Kp * err ; // Ограничиваем максимальное и минимальное значения управляющего сингала out = constrain(out, 0, 200); // Ругулируем скорость моторов в соответсвии с управляющим сигналом motorsDrive(speed + out, speed - out); } // Останавливаем моторы motorsDrive(0, 0); } // Функция управления моторами void motorsDrive(int ml, int mr) { // Определяем направление вращения левого мотора bool mlDir = ml < 0; // Определяем направление вращения правого мотора bool mrDir = mr < 0; // При необходимости инвертируем направление моторов if (ML_INVERT) mlDir = !mlDir; if (MR_INVERT) mrDir = !mrDir; // Задаём направление вращения моторов digitalWrite(PIN_ML_DIR, mlDir); digitalWrite(PIN_MR_DIR, mrDir); // Задаём скорость вращения моторов } // Считывание левого датчика линии int readLineL() { return map(analogRead(PIN_LINE_L), 0, 1024, 100, 0); } // Считывание правого датчика линии int readLineR() { return map(analogRead(PIN_LINE_R), 0, 1024, 100, 0); }
Можно написать и более навороченный ПИД-регулятор, но в нём сейчас попросту нет смысла. П-регулятор и так позволит роботу надёжно двигаться по линии. Однако советуем тебе почитать брошюру про ПИД-регулятор для общего развития.
Движение по линии до перекрёстка очень похоже на обычное движение по линии из прошлого раздела. Разница лишь в том, что вместо движения в течение заданного времени мы будем ехать до тех пор, пока не встретим перекрёсток.
Как понять, что робот доехал до перекрёстка? Очень просто: если оба датчика линии показывают, что видят чёрную линию, значит робот находится на перекрёстке.
В коде это будет выглядеть так:
// Считываем показания датчиков линии int leftLine = readLineL(); int rightLine = readLineR(); // Ожидаем перекрёсток while (leftLine > LINE_BLACK && rightLine > LINE_BLACK) { // Выполняем движение по линии }
Напишем функцию goToCrossroad() для этого алгоритма. Она будет примимать однин параметр speed — скорость, с которой должен двигаться робот.
// Функция езды до перекрестка void goToCrossroad(int speed) { // Задаём пропорциональный коэффициент П-регулятора int Kp = 10; // Считываем показания датчиков линии int leftLine = readLineL(); int rightLine = readLineR(); // Следуем по линии, пока не встретим перекрёсток while (leftLine > LINE_BLACK && rightLine > LINE_BLACK) { // Считываем показания датчиков линии leftLine = readLineL(); rightLine = readLineR(); // Рассчитываем ошибку int err = leftLine - rightLine; // Формируем управляющий сигнал int out = Kp * err; // Ограничиваем максимальное и минимальное значения управляющего сингала out = constrain(out, -100, 100); // Регулируем скорость моторов в соответствии с управляющим сигналом motorsDrive(speed + out, speed - out); } // Даем роботу немного времени заехать за линию motorsDrive(speed, speed); delay(500); // Останавливаем моторы motorsDrive(0, 0); }
Поэкспериментируй со значениями коэффициента и скорости робота. Подбери такие параметры, при которых робот будет двигаться по линии максимально быстро и плавно, уверенно проходя все изгибы трассы без сбоев.
Помни: отладка — важнейшая часть работы. Именно на неё уходит большая часть времени при создании эффективных роботов.
Постарайся довести настройку до такого уровня, чтобы ты мог буквально за несколько минут подобрать параметры П-регулятора для любого робота. Такая интуиция приходит только с практикой и постепенно формируется в ходе работы.
Ниже мы привели полный программный код для этого раздела.
// Даём понятное имя пину направления вращения левого мотора #define PIN_ML_DIR 7 // Даём понятное имя пину управления скоростью вращения левого мотора #define PIN_ML_SPEED 6 // Даём понятное имя пину направления вращения правого мотора #define PIN_MR_DIR 4 // Даём понятное имя пину управления скоростью вращения правого мотора #define PIN_MR_SPEED 5 // Даём понятное имя пину левого датчика линии #define PIN_LINE_L A0 // Даём понятное имя пину правого датчика линии #define PIN_LINE_R A1 // Настраиваем направление вращения моторов // Если мотор вращается в обратную сторону, меняем false на true constexpr bool ML_INVERT = false; constexpr bool MR_INVERT = false; // Задаем порог, ниже которого считаем, что датчик видит чёрную линию constexpr int LINE_BLACK = 40; // Задаём скорост моторов constexpr int M_SPEED = 150; void setup() { // Настраиваем пины управления моторами в режим выхода pinMode(PIN_ML_DIR, OUTPUT); pinMode(PIN_ML_SPEED, OUTPUT); pinMode(PIN_MR_DIR, OUTPUT); pinMode(PIN_MR_SPEED, OUTPUT); // Настраиваем пины управления сервоприводами в режим выхода pinMode(PIN_LINE_L, INPUT); pinMode(PIN_LINE_R, INPUT); // Делаем небольшую задержку, чтобы успеть поставить робота delay(10000); // Запускаем алгоритм езды до перекрестка goToCrossroad(M_SPEED); } void loop() { // Функция loop() ничего не делает } // Функция езды до перекрестка void goToCrossroad(int speed) { // Задаём пропорциональный коэффициент П-регулятора int Kp = 10; // Считываем показания датчиков линии int leftLine = readLineL(); int rightLine = readLineR(); // Следуем по линии, пока не встретим перекрёсток while (leftLine > LINE_BLACK && rightLine > LINE_BLACK) { // Считываем показания датчиков линии leftLine = readLineL(); rightLine = readLineR(); // Рассчитываем ошибку int err = leftLine - rightLine; // Формируем управляющий сигнал int out = Kp * err; // Ограничиваем максимальное и минимальное значения управляющего сингала out = constrain(out, -100, 100); // Регулируем скорость моторов в соответствии с управляющим сигналом motorsDrive(speed + out, speed - out); } // Даем роботу немного времени заехать за линию motorsDrive(speed, speed); delay(500); // Останавливаем моторы motorsDrive(0, 0); } // Функция управления моторами void motorsDrive(int ml, int mr) { // Определяем направление вращения левого мотора bool mlDir = ml < 0; // Определяем направление вращения правого мотора bool mrDir = mr < 0; // При необходимости инвертируем направление моторов if (ML_INVERT) mlDir = !mlDir; if (MR_INVERT) mrDir = !mrDir; // Задаём направление вращения моторов digitalWrite(PIN_ML_DIR, mlDir); digitalWrite(PIN_MR_DIR, mrDir); // Задаём скорость вращения моторов } // Считывание левого датчика линии int readLineL() { return map(analogRead(PIN_LINE_L), 0, 1024, 100, 0); } // Считывание правого датчика линии int readLineR() { return map(analogRead(PIN_LINE_R), 0, 1024, 100, 0); }
Когда робот подключён к компьютеру по USB-кабелю, для отладки удобно использовать сообщения в последовательный порт. Но во время прохождения трассы такой возможности уже не будет.
Поэтому в боевых условиях для отладки удобно использовать пищалку (англ. buzzer). Она может подавать сигнал при обнаружении какого-либо объекта, а также обозначать начало или завершение отдельной фазы алгоритма.
Напишем функцию beep(), которая позволит тебе подавать звуковой сигнал во время выполнения задания. На вход она будет принимать один параметр n - количество раз, которое нужно пропищать.
// Даём понятное имя пину пищалки #define PIN_BUZZER 4 void setup() { // Настраиваем пин управления пищалкой pinMode(PIN_BUZZER, OUTPUT); } void loop() { // Пищим 3 раза beep(3); // Делаем паузу 5 секунд delay(5000); } // Функция для звукового сигнала void beep(int n) { // Включаем и выключам пищалку в цикле n раз for (int i = 0; i < n; i++) { // Включаем пищалку на 250 миллисекунд digitalWrite(PIN_BUZZER, HIGH); delay(250); // Выключаем пищалку на 250 миллисекунд digitalWrite(PIN_BUZZER, LOW); delay(250); } }
[Тут нужно вставить схему подключения пищалки на Робота олимпийца]
Мы рассмотрели основные алгоритмы движения робота, которые понадобятся вам на олимпиаде. Теперь перейдём к практическому заданию из реальной олимпиады.
Рассмотрим практическое задание заключительного этапа Всероссийской олимпиады школьников по труду (технологии) по профилю «Робототехника» для 9-х классов в 2022/23 учебном году.
Участникам предлагается такое поле:
Разберёмся с его элементами:
На трассе есть два ряда перекрёстков: верхний и нижний. Назовём их рядами A и B. Столбцы с перекрёстками пронумеруем от 0 до 4.
В дальнейшем мы будем обозначать секции сочетанием буквы и цифры. Например, секция A2 — это третий перекрёсток верхнего ряда, а секция B1 — второй перекрёсток нижнего.
Нумерация столбцов начинается с нуля не случайно: так будет проще сопоставлять секции с кодом, поскольку элементы массивов в программировании обычно нумеруются начиная с нуля.
Перед каждым заездом жюри проводит жеребьёвку, чтобы определить расположение кубиков в ряду A. При этом ряд B всегда полностью заполнен кубиками.
Например, после жеребьёвки трасса может выглядеть так:
Задача участника — разработать алгоритм и запрограммировать робота так, чтобы он переместил кубики из ряда B в соответствующие свободные ячейки ряда A.
После правильного выполнения задания трасса должна выглядеть так:
Обратите внимание: после завершения задания робот должен вернуться в стартовую точку и полностью находиться внутри жёлтой зоны.
Алгоритм кажется достаточно сложным. С чего же начать?
Любой сложный алгоритм лучше разбивать на небольшие подзадачи. Их проще решать по отдельности, а затем объединять решения в одну программу. Мы поступим именно так.
Сначала нужно определить, где расположены кубики в ряду A. Для этого робот последовательно проедет через каждый перекрёсток. На каждом перекрёстке он остановится, повернётся к ячейке с кубиком и измерит расстояние с помощью ИК-дальномера.
Размеры трассы заранее известны, поэтому по показанию дальномера можно определить, есть ли кубик в ячейке. Предположим, что расстояние от перекрёстка до стены составляет 50 см, а кубик установлен на расстоянии 30 см от перекрёстка. Тогда показание около 30 см означает, что кубик есть. Если дальномер показывает расстояние, близкое к 50 см, значит, ячейка свободна.
Робот повторит эту процедуру пять раз — по одному разу для каждого перекрёстка. Результаты сканирования мы сохраним в массиве логических значений типа bool.
[Пояснение о том, что такое массив, как это было сделано в «8-битной академии». Пока оставляю эту заглушку.]
Чтобы сделать код короче и понятнее, вместо значений true и false будем использовать числа:
1 означает true: кубик есть.0 означает false: ячейка свободна.Итак, нам нужно выполнить следующие действия:
A — от 0 до 4.1.0.Обратите внимание: все этапы этого алгоритма ты уже научился выполнять в предыдущих главах. В этом и заключается идея разбиения большой задачи на маленькие подзадачи. Сейчас мы объединяем изученные действия в единый алгоритм.
Приступим к написанию кода. Сначала подготовим его основу, а затем по ходу разбора задания будем постепенно добавлять необходимые функции.
Весь алгоритм поместим в отдельную функцию runRobot(). В конце функции setup() вызовем runRobot() один раз. Это нужно для того, чтобы робот выполнил задание только один раз, а не повторял его снова и снова по кругу.
// Библиотека для работы с сервоприводами #include <Servo.h> // Даём понятное имя пину направления вращения левого мотора #define PIN_ML_DIR 7 // Даём понятное имя пину управления скоростью вращения левого мотора #define PIN_ML_SPEED 6 // Даём понятное имя пину направления вращения правого мотора #define PIN_MR_DIR 4 // Даём понятное имя пину управления скоростью вращения правого мотора #define PIN_MR_SPEED 5 // Даём понятное имя пину левого датчика линии #define PIN_LINE_L A0 // Даём понятное имя пину правого датчика линии #define PIN_LINE_R A1 // Даём понятные имена пинам левого энкодера A и B #define PIN_ENC_LA 2 #define PIN_ENC_LB 12 // Даём понятные имена пинам правого энкодера A и B #define PIN_ENC_RA 3 #define PIN_ENC_RB 13 // Даём понятное имя пину ИК-дальномера #define PIN_IRR A2 // Даём понятные имена пинам УЗ-дальномера TRIG и ECHO #define PIN_USR_T 11 #define PIN_USR_E 10 // Даём понятное имя пину сервопривода головы #define PIN_SRV_HEAD 8 // Даём понятное имя пину сервомотора захвата #define PIN_SRV_GRIP 9 // Даём понятное имя пину пищалки #define PIN_BUZZER A3 // Создаём объект для работы сервоприводом головы Servo head; // Создаём объект для работы сервоприводом захвата Servo grip; // Задаём углы для верхнего и нижнего положения захвата // Значения углов подбираются экспериментально в диапазоне от 0 до 180 constexpr int GRIP_UP = 175; constexpr int GRIP_DOWN = 75; // Настраиваем направление вращения моторов // Если мотор вращается в обратную сторону, меняем false на true constexpr bool ML_INVERT = false; constexpr bool MR_INVERT = false; // Задаем порог, ниже которого считаем, что датчик видит чёрную линию constexpr int LINE_BLACK = 20; // Задаём скорост вращения мотора при повороте constexpr int TURN_SPEED = 128; // Задаём время для сьезда с линии при повороте в миллисекундах constexpr int TURN_WAIT = 150; // Задаем время поворота робота назад для выравнивания по линии constexpr int TURN_BACK = 200; // Задаём скорост моторов для движения по линии constexpr int DRIVE_SPEED = 150; void setup() { // Настраиваем пины управления моторами в режим выхода pinMode(PIN_ML_DIR, OUTPUT); pinMode(PIN_ML_SPEED, OUTPUT); pinMode(PIN_MR_DIR, OUTPUT); pinMode(PIN_ML_SPEED, OUTPUT); // Настраиваем пины управления сервоприводами в режим выхода pinMode(PIN_SRV_HEAD, OUTPUT); pinMode(PIN_SRV_GRIP, OUTPUT); // Настраиваем пины УЗ-дальномера // пин TRIG в режим выхода, пин ECHO в режим входа pinMode(PIN_USR_T, OUTPUT); pinMode(PIN_USR_E, INPUT); // Настраиваем пины датчиков линии в режим входа pinMode(PIN_LINE_L, INPUT); pinMode(PIN_LINE_R, INPUT); // Настраиваем пин ИК-дальномера в режим входа pinMode(PIN_IRR, INPUT); // Настраиваем пины энкодеров в режим входа pinMode(PIN_ENC_LA, INPUT); pinMode(PIN_ENC_LB, INPUT); pinMode(PIN_ENC_RA, INPUT); pinMode(PIN_ENC_RB, INPUT); // Инициализируем сервопривод головы head.attach(PIN_SRV_HEAD); // Инициализируем сервопривода захвата head.attach(PIN_SRV_GRIP); // Запускаем выполнение алгоритма runRobot(); } void loop() { // Функция loop() не используется } // Получение показаний с ИК-дальномера int readDistIRR() { // Считываем показания с ИК-дальномера в отчётах АЦП int value = analogRead(PIN_IRR); // Конвертируем показания в напряжение float voltage = value * (5.0 / 1024.0); // Конвертируем напряжение в расстояние в см // Возвращаем полученное значение return int(distance); } // Функция считывания левого датчика линии int readLineL() { return map(analogRead(PIN_LINE_L), 0, 1024, 100, 0); } // Функция считывания правого датчика линии int readLineR() { return map(analogRead(PIN_LINE_R), 0, 1024, 100, 0); } // Функция поднятия захвата void gripUp() { // Плавно поднимаем захват из нижнего положения в верхнее for (int i = GRIP_DOWN; i < GRIP_UP; i += 5) { grip.write(i); delay(50); } } // Функция опускания захвата void gripDown() { // Плавно опускаем захват из верхнего положения в нижнее for (int i = GRIP_UP; i > GRIP_DOWN; i -= 5) { grip.write(i); delay(50); } } // Управление движением робота // Указываем скорость левого и правого мотора от 0 до 255, // где 0 — остановка, а 255 — максимальная скорость // Знак задаёт направление вращения: плюс — вперёд, минус — назад void motorsDrive(int ml, int mr) { // Определяем направление вращения левого мотора bool mlDir = ml < 0; // Определяем направление вращения правого мотора bool mrDir = mr < 0; // При необходимости инвертируем направление моторов if (ML_INVERT) mlDir = !mlDir; if (MR_INVERT) mrDir = !mrDir; // Задаём направление вращения моторов digitalWrite(PIN_ML_DIR, mlDir); digitalWrite(PIN_MR_DIR, mrDir); // Задаём скорость вращения моторов } // Функция поворота направо void turnR(int speed, int waitTime) { // Включаем левый мотор motorsDrive(speed, 0); // Даем роботу время съехать с линии delay(waitTime); // Ждём, пока левый датчик снова увидит линию while (readLineL() > LINE_BLACK) { delay(10); } // Останавливаемся после обнаружения линии motorsDrive(0, 0); delay(500); // Немного доворачиваем назад, чтобы выровнять робота motorsDrive(0, speed); delay(TURN_BACK); // Останавливаем моторы motorsDrive(0, 0); delay(250); } // Функция поворота налево void turnL(int speed, int waitTime) { // Включаем правый мотор motorsDrive(0, speed); // Даем роботу время съехать с линии delay(waitTime); // Ждём, пока правый датчик снова увидит линию while (readLineR() > LINE_BLACK) { delay(10); } // Останавливаемся после обнаружения линии motorsDrive(0, 0); delay(500); // Немного доворачиваем назад, чтобы выровнять робота motorsDrive(speed, 0); delay(TURN_BACK); // Останавливаем моторы motorsDrive(0, 0); delay(250); } // Функция возврата после поворота направо void turnBackR(int speed, int waitTime) { // Включаем левый мотор в обратную сторону motorsDrive(-speed, 0); // Ждём, пока левый датчик снова увидит линию while (readLineL() > LINE_BLACK) { delay(10); } // Даем время, чтобы выровнять робота delay(waitTime); // Останавливаем моторы motorsDrive(0, 0); delay(250); } // Функция возврата после поворота налево void turnBackL(int speed, int waitTime) { // Включаем левый мотор в обратную сторону motorsDrive(0, -speed); // Ждём, пока левый датчик снова увидит линию while (readLineR() > LINE_BLACK) { delay(10); } // Даем время, чтобы выровнять робота delay(waitTime); // Останавливаем моторы motorsDrive(0, 0); delay(250); } // Функция разворота на 180° по часовой стрелке (направо) void turn180R(int speed, int waitTime) { // Включаем моторы в разные стороны motorsDrive(speed, -speed); // Даем роботу время съехать с линии delay(waitTime); // Ждём, пока датчик снова увидит линию while (readLineL() > LINE_BLACK) { delay(10); } // Останавливаемся после обнаружения линии motorsDrive(0, 0); delay(200); // Немного доворачиваем назад, чтобы выровнять робота motorsDrive(-speed, speed); delay(TURN_BACK); // Останавливаем моторы motorsDrive(0, 0);; delay(250); } // Функция разворота на 180° против часовой стрелке (налево) void turn180L(int speed, int waitTime) { // Включаем моторы в разные стороны motorsDrive(-speed, speed); // Даем роботу время съехать с линии delay(waitTime); // Ждём, пока датчик снова увидит линию while (readLineR() > LINE_BLACK) { delay(10); } // Останавливаемся после обнаружения линии motorsDrive(0, 0); delay(200); // Немного доворачиваем назад, чтобы выровнять робота motorsDrive(speed, -speed); delay(TURN_BACK); // Останавливаем моторы motorsDrive(0, 0);; delay(250); } // Функция езды до перекрестка void goToCrossroad(int speed) { // Задаём пропорциональный коэффициент П-регулятора int Kp = 10; // Считываем показания датчиков линии int leftLine = readLineL(); int rightLine = readLineR(); // Следуем по линии, пока не встретим перекрёсток while (leftLine > LINE_BLACK && rightLine > LINE_BLACK) { // Считываем показания датчиков линии leftLine = readLineL(); rightLine = readLineR(); // Рассчитываем ошибку int err = leftLine - rightLine; // Формируем управляющий сигнал int out = Kp * err; // Ограничиваем максимальное и минимальное значения управляющего сингала out = constrain(out, -100, 100); // Регулируем скорость моторов в соответствии с управляющим сигналом motorsDrive(speed + out, speed - out); } // Даем роботу немного времени заехать за линию motorsDrive(speed, speed); delay(500); // Останавливаем моторы motorsDrive(0, 0); } // Функция для звукового сигнала void beep(int n) { // Включаем и выключам пищалку в цикле n раз for (int i = 0; i < n; i++) { // Включаем пищалку на 250 миллисекунд digitalWrite(PIN_BUZZER, HIGH); delay(250); // Выключаем пищалку на 250 миллисекунд digitalWrite(PIN_BUZZER, LOW); delay(250); } } // Функция с алгоритмом выполнения задания void runRobot() { // Количество перекрёстков const int n = 5; // Расположение кубиков bool cubes[] = {0, 0, 0, 0, 0}; // Сигнализируем о начале выполнения алгоритма beep(2); // Отслеживаем текущий перекрёсток int i = 0; }
Сейчас этот код ещё ничего не делает: в нем нет команд движения робота. Это скелет — основа нашего будущего алгоритма. Дальше мы будем постепенно добавлять в функцию новые команды и подзадачи.
Сначала разберёмся, как робот будет сканировать расположение кубиков в ряду A. Он должен последовательно проехать через каждый перекрёсток, проверить наличие кубика и сохранить результат в массиве.
Модифицируем скелет программы. Добавим в функцию runRobot() цикл, в котором робот будет по очереди подъезжать к перекрёсткам и сканировать ячейки.
void runRobot() { // Количество перекрёстков const int n = 5; // Расположение кубиков bool cubes[] = {0, 0, 0, 0, 0}; // Сигнализируем о начале выполнения алгоритма beep(2); // Отслеживаем текущий перекрёсток int i = 0; // Сканируем кубики в верхнем ряду while (i < n) { // Едем до перекрёстка goToCrossroad(); // Сканируем ячейку scan(i, cubes); // Переходим к следующему перекрёстку i++; } }
Обратите внимание: этот код пока ещё не будет работать, потому что мы не написали функцию scan(). Именно она отвечает за проверку ячейки и запись результата в массив. Давайте теперь создадим эту функцию.
void scan(int i, bool cubes[5]) { // Поворачиваемся к ячейке turnR(); // Немного ждём, пока показания дальномера стабилизируются delay(500); // Проверяем, есть ли в ячейке кубик if (readDistIR() < 30) { // Сигнализируем, что обнаружили кубик beep(1); // Запоминаем, что в этой ячейке есть кубик cubes[i] = 1; } // Поворачиваемся обратно к линии turnBackR(); }
Теперь первый шаг алгоритма готов, и его можно протестировать. Загрузите программу в робота, установите его в стартовую зону и запустите.
Понаблюдайте за его движением. Робот должен последовательно просканировать все пять перекрёстков и остановиться у последнего из них.
После сканирования ряда A робот останавливается у последнего перекрёстка. Теперь он знает, где находятся кубики и свободные ячейки. Следующий шаг — перейти к ряду B.
Дополним функцию runRobot() соответствующей командой:
void runRobot() { // Количество перекрёстков const int n = 5; // Расположение кубиков bool cubes[] = {0, 0, 0, 0, 0}; // Сигнализируем о начале выполнения алгоритма beep(2); // Отслеживаем текущий перекрёсток int i = 0; // Сканируем кубики в верхнем ряду while (i < n) { // Едем до перекрёстка goToCrossroad(); // Сканируем ячейку scan(i, cubes); // Переходим к следующему перекрёстку i++; } // Все перекрёстки просканированы — переходим к нижнему ряду goToB(); }
Осталось написать функцию goToB().
void goToB() { // Доезжаем до верхнего угла трассы goToCrossroad(); // Поворачиваем направо turnR(); // Доезжаем до нижнего угла трассы goToCrossroad(); // Поворачиваем к ряду B turnR(); }
</WRAP>
Протестируйте функцию goToB() отдельно. Установите робота на последний перекрёсток ряда A и убедитесь, что он правильно проезжает к ряду B.
После выполнения этой функции робот должен оказаться у первого перекрёстка нижнего ряда и быть ориентирован вдоль него.
Чтобы протестировать отдельную функцию, временно закомментируйте вызов runRobot() и вместо него вызовите функцию, которую хотите проверить. После завершения тестирования удалите строку с вызовом тестируемой функции и раскомментируйте вызов runRobot().
Теперь робот занял нужную позицию и готов перейти к следующему этапу — расстановке кубиков по своим местам.
Ему предстоит проехать перекрёстки ряда B в обратном порядке: от последнего к первому. На каждом перекрёстке робот будет проверять, нужно ли переместить кубик в соответствующую свободную ячейку ряда A.
void runRobot() { // Количество перекрёстков const int n = 5; // Расположение кубиков bool cubes[] = {0, 0, 0, 0, 0}; // Сигнализируем о начале выполнения алгоритма beep(2); // Отслеживаем текущий перекрёсток int i = 0; // Сканируем кубики в верхнем ряду while (i < n) { // Едем до перекрёстка goToCrossroad(); // Сканируем ячейку scan(i, cubes); // Переходим к следующему перекрёстку i++; } // Все перекрёстки просканированы — переходим к нижнему ряду goToB(); // Проезжаем перекрёстки в обратном порядке while (i > 0) { // Едем до следующего перекрёстка goToCrossroad(); // Проверяем, нужно ли переместить кубик if (cubes[i - 1] == 0) { // Перемещаем кубик moveCube(i, n); } // Переходим к следующему перекрёстку i--; } }
В условии cubes[i - 1] == 0 мы проверяем, была ли свободной соответствующая ячейка в ряду A. Индекс i - 1 нужен потому, что переменная i после сканирования равна 5, а элементы массива имеют индексы от 0 до 4.
Если в соответствующей ячейке ряда A нет кубика, робот забирает кубик из ряда B и перемещает его наверх. Если кубик уже находится на своём месте, робот просто едет дальше.
Теперь напишем функцию moveCube(). Она будет объединять все действия, необходимые для перемещения одного кубика:
Действий довольно много, поэтому разделим их на отдельные функции:
void moveCube(int i, int n) { // Забираем кубик loadCube(); // Везём кубик к нужной ячейке deliverCube(i, n); // Выгружаем кубик unloadCube(); // Возвращаемся на исходную позицию goBack(i, n); }
Функция loadCube() отвечает за «загрузку» кубика — захват кубика роботом.
Её задача состоит из трёх действий:
void loadCube() { // Поворачиваем к ячейке turnR(); // Доезжаем до кубика goToCrossroad(); // Опускаем захват gripDown(); // Разворачиваемся на 180° turnR_180(); // Едем обратно к линии goToCrossroad(); // Выезжаем на линию turnL(); }
Сначала робот поворачивается к ячейке и подъезжает к кубику. Затем он опускает захват и разворачивается на 180°. После этого робот возвращается к линии уже с захваченным кубиком.
Теперь, когда кубик «загружен», его нужно доставить в соответствующую ячейку ряда A. За это отвечает функция deliverCube().
Параметр i показывает, какой кубик сейчас перемещается, а параметр n содержит общее количество перекрёстков.
void deliverCube(int i, int n) { // Едем до угла трассы for (int j = 0; j < n - i + 1; j++) { goToCrossroad(); } // Поворачиваем налево turnL(); // Едем до верхнего угла goToCrossroad(); // Поворачиваем налево turnL(); // Едем к нужной ячейке for (int j = 0; j < n - i + 1; j++) { goToCrossroad(); } }
Сначала робот проезжает вдоль ряда B до нужного угла трассы. Затем он поворачивает к ряду A и движется вдоль него до ячейки, в которую нужно поместить кубик.
Цикл for позволяет повторить команду движения нужное количество раз. Число повторений зависит от положения текущего кубика.
Выражение n - i + 1 определяет, сколько раз нужно выполнить команду goToCrossroad(). Переменная i показывает положение текущего кубика в ряду B, а соответствующий элемент массива имеет индекс i - 1. Поэтому количество проездов вычисляется по формуле n - (i - 1), которую можно записать короче: n - i + 1. Одно и то же значение используется в обоих циклах, потому что расстояния от текущего кубика до угла трассы и от угла до соответствующей ячейки ряда A одинаковы.
Положение кубика в ряду B | Индекс в массиве | Значение i | Количество проездов |
|---|---|---|---|
| 5-е | cubes[4] | 5 | 1 |
| 4-е | cubes[3] | 4 | 2 |
| 3-е | cubes[2] | 3 | 3 |
| 2-е | cubes[1] | 2 | 4 |
| 1-е | cubes[0] | 1 | 5 |
Робот привёз кубик к нужному перекрёстку ряда A. Теперь его нужно выгрузить в ячейку. Для этого напишем функцию unloadCube().
void unloadCube() { // Поворачиваем к ячейке turnL(); // Доезжаем до места установки кубика goToCrossroad(); // Опускаем захват gripDown(); // Разворачиваемся на 180° turnR_180(); // Едем обратно к линии goToCrossroad(); // Выезжаем на линию turnR(); }
Робот подъезжает к свободной ячейке, опускает захват и оставляет в ней кубик. Затем он разворачивается, возвращается на линию и готов двигаться обратно к ряду B.
После выгрузки кубика робот должен вернуться к исходной позиции — к соответствующему перекрёстку ряда B. Напишем для этого функцию goBack().
void goBack(int i, int n) { // Едем до угла трассы for (int j = 0; j < n - i + 1; j++) { goToCrossroad(); } // Поворачиваем направо turnR(); // Доезжаем до нижнего угла трассы goToCrossroad(); // Поворачиваем направо turnR(); // Едем к исходной ячейке for (int j = 0; j < n - i + 1; j++) { goToCrossroad(); } }
Сначала робот возвращается к углу трассы, затем переходит на рядBи движется к исходному перекрёстку. Оттуда он сможет продолжить проверку следующих ячеек.
Финальным шагом алгоритма будет возвращение робота в стартовую зону, обозначенную жёлтым квадратом. Робот должен полностью заехать в эту зону и остановиться.
Дополним функцию runRobot() командами возвращения на старт:
void runRobot() { // Количество перекрёстков const int n = 5; // Расположение кубиков bool cubes[] = {0, 0, 0, 0, 0}; // Сигнализируем о начале выполнения алгоритма beep(2); // Отслеживаем текущий перекрёсток int i = 0; // Сканируем кубики в верхнем ряду while (i < n) { // Едем до перекрёстка goToCrossroad(); // Сканируем ячейку scan(i, cubes); // Переходим к следующему перекрёстку i++; } // Все перекрёстки просканированы — переходим к нижнему ряду goToB(); // Проезжаем перекрёстки в обратном порядке while (i > 0) { // Едем до перекрёстка goToCrossroad(); // Проверяем, нужно ли переместить кубик if (cubes[i - 1] == 0) { // Перемещаем кубик moveCube(i, n); } // Переходим к следующему перекрёстку i--; } // Все кубики на своих местах — возвращаемся в стартовую зону // Доезжаем до нижнего угла трассы goToCrossroad(); // Поворачиваем направо turnR(); // Доезжаем до верхнего угла трассы рядом со стартовой зоной goToCrossroad(); // Сигнализируем о завершении алгоритма beep(3); }
Так одна большая задача разбивается на несколько понятных этапов: загрузка, доставка, выгрузка и возвращение. Каждый этап находится в отдельной функции, поэтому программу проще читать, тестировать и исправлять.
Ниже приведён полный код программы из этого раздела:
// Библиотека для работы с сервоприводами #include <Servo.h> // Даём понятное имя пину направления вращения левого мотора #define PIN_ML_DIR 7 // Даём понятное имя пину управления скоростью вращения левого мотора #define PIN_ML_SPEED 6 // Даём понятное имя пину направления вращения правого мотора #define PIN_MR_DIR 4 // Даём понятное имя пину управления скоростью вращения правого мотора #define PIN_MR_SPEED 5 // Даём понятное имя пину левого датчика линии #define PIN_LINE_L A0 // Даём понятное имя пину правого датчика линии #define PIN_LINE_R A1 // Даём понятные имена пинам левого энкодера A и B #define PIN_ENC_LA 2 #define PIN_ENC_LB 12 // Даём понятные имена пинам правого энкодера A и B #define PIN_ENC_RA 3 #define PIN_ENC_RB 13 // Даём понятное имя пину ИК-дальномера #define PIN_IRR A2 // Даём понятные имена пинам УЗ-дальномера TRIG и ECHO #define PIN_USR_T 11 #define PIN_USR_E 10 // Даём понятное имя пину сервопривода головы #define PIN_SRV_HEAD 8 // Даём понятное имя пину сервомотора захвата #define PIN_SRV_GRIP 9 // Даём понятное имя пину пищалки #define PIN_BUZZER A3 // Создаём объект для работы сервоприводом головы Servo head; // Создаём объект для работы сервоприводом захвата Servo grip; // Задаём углы для верхнего и нижнего положения захвата // Значения углов подбираются экспериментально в диапазоне от 0 до 180 constexpr int GRIP_UP = 175; constexpr int GRIP_DOWN = 75; // Настраиваем направление вращения моторов // Если мотор вращается в обратную сторону, меняем false на true constexpr bool ML_INVERT = false; constexpr bool MR_INVERT = false; // Задаем порог, ниже которого считаем, что датчик видит чёрную линию constexpr int LINE_BLACK = 20; // Задаём скорост вращения мотора при повороте constexpr int TURN_SPEED = 128; // Задаём время для сьезда с линии при повороте в миллисекундах constexpr int TURN_WAIT = 150; // Задаем время поворота робота назад для выравнивания по линии constexpr int TURN_BACK = 200; // Задаём скорост моторов для движения по линии constexpr int DRIVE_SPEED = 150; void setup() { // Настраиваем пины управления моторами в режим выхода pinMode(PIN_ML_DIR, OUTPUT); pinMode(PIN_ML_SPEED, OUTPUT); pinMode(PIN_MR_DIR, OUTPUT); pinMode(PIN_ML_SPEED, OUTPUT); // Настраиваем пины управления сервоприводами в режим выхода pinMode(PIN_SRV_HEAD, OUTPUT); pinMode(PIN_SRV_GRIP, OUTPUT); // Настраиваем пины УЗ-дальномера // пин TRIG в режим выхода, пин ECHO в режим входа pinMode(PIN_USR_T, OUTPUT); pinMode(PIN_USR_E, INPUT); // Настраиваем пины датчиков линии в режим входа pinMode(PIN_LINE_L, INPUT); pinMode(PIN_LINE_R, INPUT); // Настраиваем пин ИК-дальномера в режим входа pinMode(PIN_IRR, INPUT); // Настраиваем пины энкодеров в режим входа pinMode(PIN_ENC_LA, INPUT); pinMode(PIN_ENC_LB, INPUT); pinMode(PIN_ENC_RA, INPUT); pinMode(PIN_ENC_RB, INPUT); // Инициализируем сервопривод головы head.attach(PIN_SRV_HEAD); // Инициализируем сервопривода захвата head.attach(PIN_SRV_GRIP); // Запускаем выполнение алгоритма runRobot(); } void loop() { // Функция loop() не используется } // Получение показаний с ИК-дальномера int readDistIRR() { // Считываем показания с ИК-дальномера в отчётах АЦП int value = analogRead(PIN_IRR); // Конвертируем показания в напряжение float voltage = value * (5.0 / 1024.0); // Конвертируем напряжение в расстояние в см // Возвращаем полученное значение return int(distance); } // Считывание левого датчика линии int readLineL() { return map(analogRead(PIN_LINE_L), 0, 1024, 100, 0); } // Считывание правого датчика линии int readLineR() { return map(analogRead(PIN_LINE_R), 0, 1024, 100, 0); } // Функция поднятия захвата void gripUp() { // Плавно поднимаем захват из нижнего положения в верхнее for (int i = GRIP_DOWN; i < GRIP_UP; i += 5) { grip.write(i); delay(50); } } // Функция опускания захвата void gripDown() { // Плавно опускаем захват из верхнего положения в нижнее for (int i = GRIP_UP; i > GRIP_DOWN; i -= 5) { grip.write(i); delay(50); } } // Управление движением робота // Указываем скорость левого и правого мотора от 0 до 255, // где 0 — остановка, а 255 — максимальная скорость // Знак задаёт направление вращения: плюс — вперёд, минус — назад void motorsDrive(int ml, int mr) { // Определяем направление вращения левого мотора bool mlDir = ml < 0; // Определяем направление вращения правого мотора bool mrDir = mr < 0; // При необходимости инвертируем направление моторов if (ML_INVERT) mlDir = !mlDir; if (MR_INVERT) mrDir = !mrDir; // Задаём направление вращения моторов digitalWrite(PIN_ML_DIR, mlDir); digitalWrite(PIN_MR_DIR, mrDir); // Задаём скорость вращения моторов } // Функция поворота направо void turnR(int speed, int waitTime) { // Включаем левый мотор motorsDrive(speed, 0); // Даем роботу время съехать с линии delay(waitTime); // Ждём, пока левый датчик снова увидит линию while (readLineL() > LINE_BLACK) { delay(10); } // Останавливаемся после обнаружения линии motorsDrive(0, 0); delay(500); // Немного доворачиваем назад, чтобы выровнять робота motorsDrive(0, speed); delay(TURN_BACK); // Останавливаем моторы motorsDrive(0, 0); delay(250); } // Функция поворота налево void turnL(int speed, int waitTime) { // Включаем правый мотор motorsDrive(0, speed); // Даем роботу время съехать с линии delay(waitTime); // Ждём, пока правый датчик снова увидит линию while (readLineR() > LINE_BLACK) { delay(10); } // Останавливаемся после обнаружения линии motorsDrive(0, 0); delay(500); // Немного доворачиваем назад, чтобы выровнять робота motorsDrive(speed, 0); delay(TURN_BACK); // Останавливаем моторы motorsDrive(0, 0); delay(250); } // Функция возврата после поворота направо void turnBackR(int speed, int waitTime) { // Включаем левый мотор в обратную сторону motorsDrive(-speed, 0); // Ждём, пока левый датчик снова увидит линию while (readLineL() > LINE_BLACK) { delay(10); } // Даем время, чтобы выровнять робота delay(waitTime); // Останавливаем моторы motorsDrive(0, 0); delay(250); } // Функция возврата после поворота налево void turnBackL(int speed, int waitTime) { // Включаем левый мотор в обратную сторону motorsDrive(0, -speed); // Ждём, пока левый датчик снова увидит линию while (readLineR() > LINE_BLACK) { delay(10); } // Даем время, чтобы выровнять робота delay(waitTime); // Останавливаем моторы motorsDrive(0, 0); delay(250); } // Функция разворота на 180° по часовой стрелке (направо) void turn180R(int speed, int waitTime) { // Включаем моторы в разные стороны motorsDrive(speed, -speed); // Даем роботу время съехать с линии delay(waitTime); // Ждём, пока датчик снова увидит линию while (readLineL() > LINE_BLACK) { delay(10); } // Останавливаемся после обнаружения линии motorsDrive(0, 0); delay(200); // Немного доворачиваем назад, чтобы выровнять робота motorsDrive(-speed, speed); delay(TURN_BACK); // Останавливаем моторы motorsDrive(0, 0);; delay(250); } // Функция разворота на 180° против часовой стрелке (налево) void turn180L(int speed, int waitTime) { // Включаем моторы в разные стороны motorsDrive(-speed, speed); // Даем роботу время съехать с линии delay(waitTime); // Ждём, пока датчик снова увидит линию while (readLineR() > LINE_BLACK) { delay(10); } // Останавливаемся после обнаружения линии motorsDrive(0, 0); delay(200); // Немного доворачиваем назад, чтобы выровнять робота motorsDrive(speed, -speed); delay(TURN_BACK); // Останавливаем моторы motorsDrive(0, 0);; delay(250); } // Функция езды до перекрестка void goToCrossroad(int speed) { // Задаём пропорциональный коэффициент П-регулятора int Kp = 10; // Считываем показания датчиков линии int leftLine = readLineL(); int rightLine = readLineR(); // Следуем по линии, пока не встретим перекрёсток while (leftLine > LINE_BLACK && rightLine > LINE_BLACK) { // Считываем показания датчиков линии leftLine = readLineL(); rightLine = readLineR(); // Рассчитываем ошибку int err = leftLine - rightLine; // Формируем управляющий сигнал int out = Kp * err; // Ограничиваем максимальное и минимальное значения управляющего сингала out = constrain(out, -100, 100); // Регулируем скорость моторов в соответствии с управляющим сигналом motorsDrive(speed + out, speed - out); } // Даем роботу немного времени заехать за линию motorsDrive(speed, speed); delay(500); // Останавливаем моторы motorsDrive(0, 0); } // Функция для звукового сигнала void beep(int n) { // Включаем и выключам пищалку в цикле n раз for (int i = 0; i < n; i++) { // Включаем пищалку на 250 миллисекунд digitalWrite(PIN_BUZZER, HIGH); delay(250); // Выключаем пищалку на 250 миллисекунд digitalWrite(PIN_BUZZER, LOW); delay(250); } } void scan(int i, bool cubes[5]) { // Поворачиваемся к ячейке turnR(TURN_SPEED, TURN_WAIT); // Немного ждём, пока показания дальномера стабилизируются delay(500); // Проверяем, есть ли в ячейке кубик if (readDistIRR() < 30) { // Сигнализируем, что обнаружили кубик beep(1); // Запоминаем, что в этой ячейке есть кубик cubes[i] = 1; } // Поворачиваемся обратно к линии turnBackR(TURN_SPEED, TURN_WAIT); } void goToB() { // Доезжаем до верхнего угла трассы goToCrossroad(DRIVE_SPEED); // Поворачиваем направо turnR(TURN_SPEED, TURN_WAIT); // Доезжаем до нижнего угла трассы goToCrossroad(DRIVE_SPEED); // Поворачиваем к ряду B turnR(TURN_SPEED, TURN_WAIT); } void loadCube() { // Поворачиваем к ячейке turnR(TURN_SPEED, TURN_WAIT); // Доезжаем до кубика goToCrossroad(DRIVE_SPEED); // Опускаем захват gripDown(); // Разворачиваемся на 180° turn180R(TURN_SPEED, TURN_WAIT); // Едем обратно к линии goToCrossroad(DRIVE_SPEED); // Выезжаем на линию turnL(TURN_SPEED, TURN_WAIT); } void deliverCube(int i, int n) { // Едем до угла трассы for (int j = 0; j < n - i + 1; j++) { goToCrossroad(DRIVE_SPEED); } // Поворачиваем налево turnL(TURN_SPEED, TURN_WAIT); // Едем до верхнего угла goToCrossroad(DRIVE_SPEED); // Поворачиваем налево turnL(TURN_SPEED, TURN_WAIT); // Едем к нужной ячейке for (int j = 0; j < n - i + 1; j++) { goToCrossroad(DRIVE_SPEED); } } void unloadCube() { // Поворачиваем к ячейке turnL(TURN_SPEED, TURN_WAIT); // Доезжаем до места установки кубика goToCrossroad(DRIVE_SPEED); // Опускаем захват gripDown(); // Разворачиваемся на 180° turn180R(TURN_SPEED, TURN_WAIT); // Едем обратно к линии goToCrossroad(DRIVE_SPEED); // Выезжаем на линию turnR(TURN_SPEED, TURN_WAIT); } void goBack(int i, int n) { // Едем до угла трассы for (int j = 0; j < n - i + 1; j++) { goToCrossroad(DRIVE_SPEED); } // Поворачиваем направо turnR(TURN_SPEED, TURN_WAIT); // Доезжаем до нижнего угла трассы goToCrossroad(DRIVE_SPEED); // Поворачиваем направо turnR(TURN_SPEED, TURN_WAIT); // Едем к исходной ячейке for (int j = 0; j < n - i + 1; j++) { goToCrossroad(DRIVE_SPEED); } } void moveCube(int i, int n) { // Забираем кубик loadCube(); // Везём кубик к нужной ячейке deliverCube(i, n); // Выгружаем кубик unloadCube(); // Возвращаемся на исходную позицию goBack(i, n); } void runRobot() { // Количество перекрёстков const int n = 5; // Расположение кубиков bool cubes[] = {0, 0, 0, 0, 0}; // Сигнализируем о начале выполнения алгоритма beep(2); // Отслеживаем текущий перекрёсток int i = 0; // Сканируем кубики в верхнем ряду while (i < n) { // Едем до перекрёстка goToCrossroad(DRIVE_SPEED); // Сканируем ячейку scan(i, cubes); // Переходим к следующему перекрёстку i++; } // Все перекрёстки просканированы — переходим к нижнему ряду goToB(); // Проезжаем перекрёстки в обратном порядке while (i > 0) { // Едем до перекрёстка goToCrossroad(DRIVE_SPEED); // Проверяем, нужно ли переместить кубик if (cubes[i - 1] == 0) { // Перемещаем кубик moveCube(i, n); } // Переходим к следующему перекрёстку i--; } // Все кубики на своих местах — возвращаемся в стартовую зону // Доезжаем до нижнего угла трассы goToCrossroad(DRIVE_SPEED); // Поворачиваем направо turnR(TURN_SPEED, TURN_WAIT); // Доезжаем до верхнего угла трассы рядом со стартовой зоной goToCrossroad(DRIVE_SPEED); // Сигнализируем о завершении алгоритма beep(3); }
Год от года условия олимпиады меняются, и роботы попадаются разные. Посмотри на фото с заключительных этапов олимпиады, чтобы увидеть, какие варианты роботов там применялись.
Посмотри также несколько репортажей с заключительного этапа ВсОШ по робототехнике.
Это практическое задание заключительного этапа Всероссийской олимпиады школьников по труду (технологии) по профилю «Робототехника» 2022–2023 учебного года для 11-х классов.
Построить и запрограммировать робота, который:
Сперва попробуем долгий, но полезный для тренировки алгоритм:
Долгий алгоритм заведомо не подойдёт на самой олимпиаде: только на сканирование уйдёт порядка 80 секунд. Но можно считерить и расположить ИК-дальномер так, чтобы роботу не приходилось лишний раз поворачивать.
// пишем основной алгоритм для робота void runForest() { int spdL = 150, spdR = 170; // скорости pip(); gripV1(100); // сканим верхний ряд bool o[5] = {0, 0, 0, 0, 0}; // массив логических элементов int n = 5; // количество перекрёстков int i = 0; // номер текущего перекрёстка while (i < n) { goLine_c(spdL, spdR); // едем до перекрёстка turnR(spdL, 1000); // поворачиваем delay(200); // замерли перед замером if (analogRead(IR) > 300) { // проверяем наличие кубика pip(); // пикаем o[i] = 1; // запоминаем, что на i-м месте есть кубик } turnR_back(spdL, 500); // возвращаемся на основную линию i++; // увеличиваем номер перекрёстка (следующий) } goLine_c(spdL, spdR); // едем в верхний правый угол turnR(spdL, 1000); // поворачиваем goLine_c(spdL, spdR); // едем в нижний правый угол turnR(120, 1000); // поворачиваем // сканим нижний ряд bool o2[5] = {0, 0, 0, 0, 0}; // массив логических элементов i = 0; // номер текущего перекрёстка while (i < n) { goLine_c(spdL, spdR); // едем до перекрёстка turnR(spdL, 1000); // поворачиваем delay(200); // замерли перед замером if (analogRead(IR) > 300) { // проверяем наличие кубика pip(); // пикаем o2[4 - i] = 1; // запоминаем, что на i-м месте есть кубик } turnR_back(120, 500); // возвращаемся на основную линию i++; // увеличиваем номер перекрёстка (следующий) } goLine_c(spdL, spdR); // едем в верхний правый угол turnR(spdL, 1000); // поворачиваем goLine_c(spdL, spdR); // едем в нижний правый угол turnR(spdL, 1000); // поворачиваем // мы на старте, знаем где кубики... }
Целых 80 секунд на сканирование — долго! Но мы можем изменить расположение датчика — поставить его сбоку и сканировать объекты, не поворачивая. Тогда впишемся в 40 с.
void runForest() { int spdL = 170, spdR = 185; pip(); gripV1(100); bool o[5] = {0, 0, 0, 0, 0}; // для верхнего ряда bool o2[5] = {0, 0, 0, 0, 0}; // для нижнего ряда int n = 5, i; // перекрёстки... и текущий номер int x = 0; // номер ряда while (x < 2) { i = 0; // обнуляем счётчик перекрёстков в ряду while (i < n) { goLine_c(spdL, spdR); // едем до перекрёстка delay(200); // замерли перед замером if (analogRead(IR) > 200) { // проверяем наличие кубика pip(); if (x == 0) o[i] = 1; // если верхний ряд, то... if (x == 1) o2[4 - i] = 1; // если нижний ряд, то... } i++; // увеличиваем номер перекрёстка (следующий) } goLine_c(spdL, spdR); turnR(spdL, 600); goLine_c(spdL, spdR); turnR(spdL, 600); x++; } // мы на старте и знаем где кубики... }
Когда проведём сканирование, закомментируем код и принудительно укажем массивы o и о2.
int spdL = 170, spdR = 185; pip(); gripV1(100); // scan_cubes(); bool o[5] = {0, 1, 0, 1, 0}; // для верхнего ряда bool o2[5] = {1, 0, 1, 0, 1}; // для нижнего ряда int n = 5, i; // перекрёстки... и текущий номер goLine_c(spdL, spdR); // к 1-й зоне верхнего ряда if (o[0] == 1 && o2[0] == 0) { pip(); // отправить кубик в нижний ряд и вернуться на перекрёсток } goLine_c(spdL, spdR); // ко 2-й зоне верхнего ряда if (o[1] == 1 && o2[1] == 0) { pip(); // отправить кубик в нижний ряд и вернуться на перекрёсток } goLine_c(spdL, spdR); // к 3-й зоне верхнего ряда if (o[2] == 1 && o2[2] == 0) { pip(); // отправить кубик в нижний ряд и вернуться на перекрёсток } goLine_c(spdL, spdR); // к 4-й зоне верхнего ряда if (o[3] == 1 && o2[3] == 0) { pip(); // отправить кубик в нижний ряд и вернуться на перекрёсток } goLine_c(spdL, spdR); // к 5-й зоне верхнего ряда if (o[4] == 1 && o2[4] == 0) { pip(); // отправить кубик в нижний ряд и вернуться на перекрёсток }
Это не программа, а так — болванка, шаблончик. Видишь закономерность, которую можно заменить циклом? Ещё нужно срочно написать процедуру для движения из любого верхнего перекрёстка в соответствующий нижний.
А ещё у нашего робота теперь сбоку выступает дальномер, и становится особенно важно, в какую сторону делать разворот. Протестируем всё на первых трёх верхних кубиках.
// разворот на 180 (крутится вправо) void turn180R(int sl, int sr, int t) { digitalWrite(DR, HIGH); // указываем направление вращения digitalWrite(DL, LOW); // указываем направление вращения analogWrite(SR, sr); // включаем мотор и даём съехать с линии analogWrite(SL, sl); // включаем мотор и даём съехать с линии delay(t); // а вот теперь ждём чёрную линию под датчиком dl = (1024 - analogRead(LL)) / delL; while (dl > Bl) { dl = (1024 - analogRead(LL)) / delL; delay(10); } analogWrite(SR, 0); // останавливаем мотор analogWrite(SL, 0); delay(200); // нужно чуть назад покрутить левое колесо digitalWrite(DR, LOW); digitalWrite(DL, HIGH); // указываем направление вращения analogWrite(SL, sl); analogWrite(SR, sr); delay(150); analogWrite(SR, 0); analogWrite(SL, 0); delay(250); } void teleport_012(int spdL, int spdR, int x) { turnR(spdL, 1000); // поворачиваем goLine_c(spdL, spdR); // подъезжаем к кубику gripV1(0); // захватываем turn180(100, 120, 1000); // разворот goLine_c(spdL, spdR); // обратно к перекрёстку turnL(spdR, 600); // поворот // надо ехать к углу for (int z = 0; z < x + 1; z++) goLine_c(spdL, spdR); turnL(spdR, 600); goLine_c(spdL, spdR); turnL(spdR, 600); // едем к нужной зоне for (int z = 0; z < x + 1; z++) goLine_c(spdL, spdR); // выгрузка кубика turnL(spdR, 600); goLine_c(spdL, spdR); gripV1(100); turn180R(100, 120, 1000); goLine_c(spdL, spdR); // обратно к перекрёстку turnR(spdL, 600); for (int z = 0; z < x + 1; z++) goLine_c(spdL, spdR); turnR(spdL, 600); goLine_c(spdL, spdR); turnR(spdL, 600); for (int z = 0; z < x + 1; z++) goLine_c(spdL, spdR); // и мы на том перекрёстке, который обработали } void runForest() { int spdL = 200, spdR = 220; pip(); gripV1(100); bool o[5] = {1, 1, 1, 1, 0}; // для верхнего ряда bool o2[5] = {0, 0, 0, 0, 1}; // для нижнего ряда int n = 5, i; // перекрёстки... и текущий номер //===================================== goLine_c(spdL, spdR); // к 1-й зоне верхнего ряда if (o[0] == 1 && o2[0] == 0) teleport_012(spdL, spdR, 0); goLine_c(spdL, spdR); // ко 2-й зоне верхнего ряда if (o[1] == 1 && o2[1] == 0) teleport_012(spdL, spdR, 1); goLine_c(spdL, spdR); // к 3-й зоне верхнего ряда if (o[2] == 1 && o2[2] == 0) teleport_012(spdL, spdR, 2); }
0 и 1 и посмотри, как робот обрабатывает первые три кубика.Предыдущий алгоритм получился очень долгий. Что ж, ещё раз внимательно прочитаем условие задачи.
Видишь, что сканировать можно только верхнюю область?
Нижняя будет или инверсной, или в ней будет пара пустых зон (вверху и внизу). А так получаем пары: (В0, Н1) и (В1, Н0). Поэтому можно просканировать только верхний ряд и начинать обработку с зелёной зоны у пятого верхнего кубика.
Что потеряем? Будет лишняя обработка одной пары (В4 и Н4 на рисунке). Условно говоря, это первый подход.
Второй подход: последовательно сканировать и сразу обрабатывать зоны C1–C5. Увидел в В1 кубик — перемести в Н1, не увидел — перемещай из Н1 в В1. Есть только один подвох: 1024 возможные комбинации!
Однако, если задуматься над тем, как должен двигаться робот, то вариантов очень мало.
Ещё можно как бы совместить два подхода: начать сканирование; если найден кубик — переместить его и продолжить сканирование с другой стороны. Чтобы переключить сторону, можно использовать флаг — логическую переменную со значениями (true|false).
Не верится, но весь алгоритм довольно компактный.
i = 4; side_up == true; while (i < 5) { goLine_c(spdL, spdR); // едем до ближайшего перекрёстка find = false; // сбрасываем флаг "Объект обнаружен" ir = analogRead(IR); // измеряем расстояние ir2 = analogRead(IR2); if (ir > 220 || ir2 > 220) find = true; if (find) { // если объект обнаружен, то... pip(); if (side_up) { //========================================= // объект обнаружен и робот в верхнем ряду //========================================= } else { //========================================= // объект обнаружен и робот в нижнем ряду //========================================= } side_up = !side_up; // инвертируем сторону (на противоположную) } else { if (side_up) { //========================================= // объект НЕ обнаружен и робот в верхнем ряду //========================================= } else { //========================================= // объект НЕ обнаружен и робот в нижнем ряду //========================================= } } i++; // увеличиваем номер перекрёстка (следующий) }
Как видишь, понадобилось всего 4 разных действия. Сложность только одна — увидеть это сразу… Такой навык появляется только с тренировками. Не сдавайся и продолжай разбирать задачи!
Обрати внимание, что в задании для 11 класса были строчки:
Как думаешь, зачем дают два ИК-дальномера? Правильно, чтобы можно было сканировать с двух сторон.
Ещё несколько небольших уточнений:
Алгоритм сравнительно простой, но программа постепенно усложняется. Приступаем к написанию.
Чтобы лучше понять алгоритм, посмотри видео с примером выполнения роботом этого задания.
Дополнительное задание
Улучши алгоритм так, чтобы робот не возил 4 и 5 кубики дальней дорогой. Оставь текущий алгоритм только для первых трёх кубиков.
И засеки общее время работы над задачей — это важно для самопроверки!
Кубики 4 и 5 робот должен перевозить короткой дорогой. В прошлом алгоритме нужно уменьшить i до 3 и дописать шаблон.
А дописать программу нужно самостоятельно.
И останется последний шажок — учесть в алгоритме ситуацию, когда в вертикальном ряду нет кубиков, чтобы робот не возил пустоту. Для этого нужно добавить проверку наличия кубика перед его захватом через отдельную логическую переменнную (т. е. флаг).
1. Поле остаётся прежним. Во всех 10 зонах расположены кубики двух цветов: чёрного и белого. Если кубик белый, то его не трогаем, а если чёрный — то везём в красную зону на схеме.
Подсказка: проверь сначала, можно ли отличить чёрный и белый объект с помощью инфракрасного дальномера. Может, он показывает разные значения, и этой разницы достаточно, чтобы различать цвета кубиков?
2. Посмотри на поле и придумай какое-нибудь задание сам. Будешь варьировать алгоритмы — будет больше понимания во всех нюансах происходящего.
Основным видом конструкторских документов в различных областях электротехники, радиоэлектроники, связи и др. являются схемы.
Схема — это графический конструкторский документ, на котором показаны в виде условных изображений или обозначений составные части изделия и связи между ними.
Схема является основой для разработки сборочных и электромонтажных чертежей и применяется при изучении принципа работы, монтаже и наладке изделия.
Правила выполнения и оформления схем регламентируют стандарты седьмой классификационной группы ЕСКД. ГОСТ 2.701-84 устанавливает единую классификацию схем для всех отраслей техники. По этой классификации все схемы (см. таблицу 1) разделены на виды (по характеру отображаемых физических процессов), типы (в зависимости от основного назначения) и имеют наименование и буквенно-цифровое обозначение (шифр, код).
Наименование схемы определяется её видом и типом, а шифр схемы состоит из буквенного и цифрового обозначения. Например, схема электрическая принципиальная имеет шифр Э3.
Функциональные части показывают в виде прямоугольников, внутрь которых вписывают наименования функциональных частей.
Обрати внимание, что на этой схеме нет дальномеров. Их надо добавить по аналогии с датчиками линии (светоотражения).
Схему можно считать выполненной, если соблюдены следующие условия:
Один балл можно снизить, если:
Если нарушений больше, задание оценивается в ноль баллов.
Ключевой фактор при оценивании — правильно отражённые на схеме подключения электрических соединений робота, произведённые участником во время выполнения основного задания. Рамка и основная надпись не относятся к критериям оценивания схемы.
Во второй части практикума ты узнаешь, как настраивать ПИД-регулятор для робота, оснащённого моторами с энкодерами.
Благодарность автору
«Амперка» выражает особую благодарность преподавателю Денису Геннадьевичу Копосову, чьё учебное пособие «Робототехника: подготовка к практическому туру регионального этапа Всероссийской олимпиады школьников» легло в основу данного практикума.
Все материалы использованы с разрешения автора. При перепечатке обязательна ссылка на оригинал.