Практикум для «Олимпийца» (Olympic Bot). Часть 1: езда по линии с перекрёстками
В первой части практикума построим тренировочного робота «Олимпиец» (Olympic Bot) на Arduino, научим его ездить по линии и разберёмся с двумя примерами олимпиадных заданий за 2022–2023 учебный год.
Для большей эффективности стоит дополнительно пройти два курса:
Советы и заметки
По любым вопросам старайся сначала самостоятельно разобраться и найти ответ на многочисленных профильных сайтах про Arduino и робототехнику, например: wiki.amperka.ru, arduino.ru и так далее.
Готовый ответ, полученный от учителя или ИИ-помощника, ты забудешь на следующий день. А вот знания, «выстраданные» своими усилиями, останутся с тобой надолго и обязательно пригодятся на олимпиадах.
1. Сборка робота
Всю сборку робота «Олимпийца» мы привели в отдельной инструкции по сборке с подробными шагами и красочными иллюстрациями.
2. Коротко о платах
Для начала познакомимся с электроникой, которая будет управлять роботом «Олимпиец».
Контроллер Iskra Uno
Контроллер Iskra Uno — «мозг» будущего робота. Именно она будет выполнять программу, считывать показания датчиков и передавать команды моторам и сервоприводу. Iskra Uno совместима с платформой Arduino, которую мы будем использовать для программирования робота.
Элементы платы
Распиновка
Платформа Iskra Uno предоставляет 32 физических контакта на штатных колодках Arduino R3 и 6 дублирующих контактов на разъёме ICSP для подключения дополнительных плат расширения, модулей и сенсоров.
- Всего физических контактов: 38 (32+6)
- Контактов ввода-вывода GPIO: 20
- Контакты с ШИМ: 6 с разрядностью 8 бит
- Контакты с АЦП: 6 с разрядностью до 10 бит
- Аппаратные интерфейсы: 1× UART, 1× SPI, 1× I²C
Пины питания
| Имя пина | Описание |
|---|---|
| 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
Плата расширения Troyka Shield служит для комфортного подключения датчиков к контроллерам формфактора Arduino R3.
Troyka Shield устанавливается на управляющие платы формфактора Arduino R3 сверху методом бутерброда.
Датчики подключаются через стандартные трёхпроводные шлейфы без пайки и макетной платы.
Элементы платы
Подробнее про плату расширения Troyka Shield читай в нашей инструкции.
Плата расширения Motor 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 читай в нашей инструкции.
3. Установка и настройка Arduino IDE
- Подключи контроллер Iskra Uno к компьютеру через кабель USB. При успешном подключении должен загореться индикаторный светодиод
ON.
- Скачай и установи Arduino IDE для операционной системы твоего компьютера.
После всех манипуляций среда Arduino IDE должна определить подключённую плату Iskra Uno.
Если твой компьютер не видит Iskra Uno:
- Внимательно сверься с инструкциями ещё раз.
- Поищи решение проблемы на breadboard.ru или обратись в техподдержку Амперки.
Советы и заметки
- Заучи самые частые и полезные комбинации сочетания клавиш:
CTRL + C: копироватьCTRL + V: вставитьCTRL + /: закомментировать или раскоментировать фрагмент кода.
- Включи опции
Показывать номера строкиСворачивание кода. - Воспользуйся онлайн-тренажёрами по печати текста, чтобы научиться печатать вслепую, не отвлекая взгляд на клавиатуру.
4. Начальная конфигурация
Перед тем как перейти к управлению оборудованием робота, подготовим основные настройки программы:
- Обозначение контактов — зададим понятные имена контактам, к которым подключено оборудование.
- Создание переменных — подготовим переменные для хранения и обработки данных.
- Настройка контактов — укажем, какие контакты контроллера работают как входы, а какие — как выходы.
Обозначение контактов
Каждый модуль робота подключён к определённому пину контроллера. Чтобы не запоминать номера контактов и каждый раз не искать их в схеме подключения, дадим пинам понятные имена в самой программе.
Для назначения имён используем директиву препроцессора #define. Препроцессор заменяет заданное имя на соответствующее значение перед компиляцией программы. При этом оперативная память Arduino не расходуется.
Приведём пример, как назначить имена для всех задействованных пинов, и не запутаться.
- 4-1-pin-names-example.ino
- // Даём понятное имя пину направления вращения левого мотора
- #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и другие. - Локальные и глобальные переменные.
- Функции, их параметры и возвращаемые значения.
Создадим базовые необходимые переменные в начале программы.
- 4-2-basic-variables.ino
- // Переменные для хранения данных с датчиков линии
- int senLineL, senLineR;
- // Переменная для хранения данных с ИК-дальномера
- int senIRR;
- // Переменная для хранения данных с УЗ-дальномера
- int senUSR;
Настройка контактов
Мы уже дали всем контактам понятные имена. Теперь нужно указать контроллеру, как будет использоваться каждый из них — для получения или передачи сигнала.
Для настройки пина используй функцию pinMode(). Настройку выполняем в функции setup(), поскольку этот код должен выполниться только один раз — при запуске контроллера.
- 4-3-pin-mode-setup.ino
- 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);
- }
В оформлении кода стоит придерживаться правила: «Лишь бы работало!» Не страшно, если везде будут глобальные переменные, а код не слишком оптимизированный и эффективный.
Базовая конфигурация готова: мы дали понятные имена используемым контактам, создали необходимые переменные и настроили режим работы выводов. Теперь программа знает, к каким контактам подключены основные элементы робота. Пора научить «Олимпийца» получать информацию об окружающем мире.
Советы и заметки
Основные моменты при проверке жюри программы:
- Читаемость кода. Используй понятные названия переменных и функций, а код разбивай на логические блоки.
- Комментарии. Добавляй комментарии к блокам кода, процедурам и функциям — комментировать каждую строку не нужно.
- Настройки. Выделяй комментариями параметры, которые часто требуются для настройки робота, чтобы их можно было быстро найти и изменить.
5. Датчики
Чтобы ориентироваться на трассе, «Олимпийцу» необходимо получать информацию об окружающем мире. Ориентироваться на трассе роботу помогают сразу несколько датчиков:
- Ультразвуковой дальномер для измерения расстояния до объектов.
- Инфракрасный дальномер для детекции препятствий.
- Датчики линии для считывания границ линии на поверхности трассы.
Ультразвуковой дальномер
Ультразвуковой дальномер HC-SR04 служит для определения расстояния до объекта с помощью ультразвуковых волн. Диапазон измерений от 2 до 400 см.
Принцип работы
Ультразвуковой дальномер HC-SR04 работает по принципу эхолокации.
| Фаза | Действие |
|---|---|
| 1. Запускаем измерение. | Подаём на пин TRIG импульс длительностью 10 мкс. |
| 2. Отправляем ультразвук. | Дальномер излучает ультразвуковую волну, частота которой находится за пределами человеческого слуха. |
| 3. Ловим отражение. | Волна достигает препятствия, отражается от него и возвращается к приёмнику дальномера. |
| 4. Измеряем время. | На пине ECHO появляется импульс, длительность которого соответствует времени прохождения звука до препятствия и обратно. |
| 5. Получаем расстояние. | Измеряем длительность импульса и по полученному времени рассчитываем расстояние до препятствия. |
Пример работы
Измерим расстояние с помощью УЗ-дальномера и выведем полученное значение в монитор порта.
- ultrasonic-distance-sensor-example.ino
- // Даём понятные имена пинам УЗ-дальномера 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 изменяется вместе с расстоянием до объекта. Электроника датчика преобразует это положение в выходное напряжение. Зависимость напряжения от расстояния нелинейная, поэтому для получения расстояния в сантиметрах потребуется преобразование показаний.
Пример работы
Измерим расстояние с помощью ИК-дальномера и выведем полученное значение в монитор порта.
- ir-distance-sensor-example.ino
- // Даём понятное имя пину ИК-дальномера
- #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))
- Сжимаем диапазон до [0…100].
senLineL = (1024 - analogRead(PIN_LINE_L)) / 10; senLineR = (1024 - analogRead(PIN_LINE_R)) / 10;
- Также можно воспользоваться встроенной в Arduino IDE функцией
map():senLineL = map(analogRead(PIN_LINE_L), 0, 1024, 100, 0); senLineR = map(analogRead(PIN_LINE_R), 0, 1024, 100, 0);
Пример работы
Считаем показания датчиков линии и выведем полученные значения в монитор порта.
- line-sensor-example.ino
- // Даём понятное имя пину левого датчика линии
- #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);
- }
Тестирование датчиков
Теперь проверим работу всех датчиков одновременно. Выведем показания сенсоров в монитор последовательного порта и убедимся в правильности работы.
- all-sensors-test.ino
- // Даём понятное имя пину левого датчика линии
- #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;
- }
6. Сервопривод
В роботе «Олимпиец» используются два сервопривода, каждый из которых управляет своим механизмом:
- Сервопривод головы — поворачивает голову с ультразвуковым дальномером.
- Сервопривод захвата — поднимает и опускает захват.
Сначала разберёмся с принципом работы сервопривода и способами управления им, а затем применим полученные знания к обоим механизмам робота.
Краткое описание
Сервопривод — это электромотор со встроенной системой управления и обратной связью, которая позволяет точно устанавливать и удерживать вал в заданном положении.
В нашем роботе используется аналоговый сервопривод Feetech FS90. Он поворачивает вал на заданный угол в диапазоне от 0 до 180° и обладает крутящим моментом до 1,5 кг·см.
Сервоприводы используются для открывания/закрывания заслонок, поворота различных механизмов на заданный градус и в качестве «суставов» при создании разнообразных механизмов. Для этого в комплекте сервы есть разные насадки, которые надеваются на вал мотора для дальнейшего соединения с механизмами устройств.
Способы управления
Как сервопривод понимает, куда повернуться? На управляющий провод сервопривода нужно периодически подавать короткие импульсы PDM (Pulse Duration Modulation). Импульсы повторяются с одинаковым периодом в 20 мс, но имеют разную длительность — от 544 до 2400 мкс. Эта длительность определяет положение вала сервопривода в диапазоне от 0 до 180°.
Для управления сервоприводом рассмотрим три способа:
- Математическое выражение — преобразуем заданный угол в ширину управляющего импульса по математической формуле.
- Функция
map()— преобразуем заданный угол в ширину управляющего импульса с помощью встроенной функции в Arduino. - Библиотека
Servo— преобразуем заданный угол в ширину управляющего импульса с помощью встроенной библиотеки в Arduino.
Проверим все три способа на повороте головы и выберем наиболее удобный для дальнейшей работы.
Математическое выражение
- servo-math-control-example.ino
- // Даём понятное имя пину сервопривода головы
- #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);
- }
- }
Функция map()
- servo-map-control-example.ino
- // Даём понятное имя пину сервопривода головы
- #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);
- }
- }
Библиотека Servo
- servo-lib-control-example.ino
- // Даём понятное имя пину сервопривода головы
- #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 — она возьмёт формирование импульсов на себя и сделает управление сервоприводами значительно удобнее.
Тестирование поворота головы
Проверим финальную работу сервопривода головы. В примере будем последовательно поворачивать голову робота в несколько заданных положений и наблюдать за движением сервопривода.
- servo-head-test.ino
- // Даём понятное имя пину сервопривода
- #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— угол в нижнем положении
Изменяй значения, пока не получишь нужный диапазон движения захвата.
- servo-gripper-test.ino
- // Даём понятное имя пину сервомотора захвата
- #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);
- }
7. Мотор GA12-N20
Колёса «Олимпийца» приводятся в движение коллекторными моторами GA12-N20 с редуктором и энкодером. Редуктор уменьшает скорость вращения двигателя и увеличивает крутящий момент, благодаря чему мотор лучше подходит для привода колёс робота. Независимое управление моторами позволяет роботу двигаться вперёд и назад, а также поворачивать влево и вправо.
Подключать моторы напрямую к Iskra Uno нельзя. Для работы им требуется значительно больший ток, чем может обеспечить контроллер. Поэтому между контроллером и моторами нужен драйвер моторов Motor Shield — он получает команды от Iskra Uno и управляет питанием двигателей.
Управление моторами
Для начала разберём базовое управление моторами — научимся задавать направление, скорость вращения, а также останавливать моторы.
- motors-basic-control-example.ino
- // Даём понятное имя пину направления вращения левого мотора
- #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(). В функцию будем передавать скорость левого и правого мотора, при этом знак «минус» означает вращение в обратном направлении.
- motors-function-control-example.ino
- // Даём понятное имя пину направления вращения левого мотора
- #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);
- // Задаём скорость вращения моторов
- }
Ты наверняка заметил, что при движении робота немного уводит вправо или влево. Это нормально: двух абсолютно одинаковых моторов не бывает, и при одинаковой заданной скорости их редукторы всегда будут вращаться с погрешностью. Позже мы вернёмся к этому и научимся балансировать моторы.
8. Повороты на перекрёстках
Теперь, когда мы научились считывать показания датчиков и управлять моторами, пора переходить к более сложным элементам движения. Один из базовых приёмов — поворот робота на перекрёстке.
Виды поворотов
Робот может поворачивать тремя способами:
- Поворот вокруг своей оси. Для такого поворота необходимо, чтобы моторы вращались с одинаковой скоростью, но в разную сторону.

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

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

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

- Для поворота направо → вращаем левый мотор вперёд.

Чтобы робот примерно понимал своё положение относительно перекрёстка, будем использовать датчики линии в качестве обратной связи.
Датчики линии расположены не строго по центру робота, а немного смещены по сторонам. Поэтому после завершения основного поворота робот может стоять немного под углом к линии. Чтобы выровнять его положение, нужно добавить небольшой доворот другим мотором в конце манёвра.
Алгоритм поворота робота направо:
- Включаем левый мотор вперёд.
- Выжидаем экспериментально время, чтобы левый датчик успел пересечь вертикальную чёрную линию.
- Ожидаем сигнал с левого датчика о фиксации горизонтальной чёрной линии и выключаем левый мотор.
- Включаем правый мотор вперёд.
- Выжидаем экспериментально время, пока робот не выровняется прямо относительно линии и выключаем правый мотор.
Реализуем алгоритм поворота направо в программе. Напишем функцию turnR() с двумя аргументами:
speed— скорость вращения колеса.timeTurn— время в миллисекундах, необходимое для того, чтобы робот съехал с линии. Это время может отличаться у разных роботов и его нужно подобрать экспериментально.
- 8-1-function-turn-r.ino
- // Функция поворота направо
- 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);
- }
Поворот налево выполняется аналогичным способом, но зеркально.
- Включаем правый мотор вперёд.
- Выжидаем экспериментально время, чтобы правый датчик успел пересечь вертикальную чёрную линию.
- Ожидаем сигнал с правого датчика о фиксации горизонтальной чёрной линии и выключаем правый мотор.
- Включаем левый мотор вперёд.
Ниже приведён полный программный код для этого раздела. Но мы советуем тебе не заглядывать туда сразу. Сначала попробуй написать функцию поворота налево самостоятельно!
- 8-2-robot-turn-lr.ino
- // Даём понятное имя пину направления вращения левого мотора
- #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° на перекрёстке.
Заметь, что у моторов есть инерция, и после отключения напряжения они ещё некоторое время продолжают вращаться. Чтобы этого избежать, перед полной остановкой робота можно «растормозить» моторы кратковременным включением в обратную сторону. Тогда робот остановится ближе к ожидаемой точке.
9. Повороты с возвратом
Иногда после поворота роботу нужно просто вернуться обратно в исходную точку. Для возвратного поворота нужно перевернуть последовательность действий обычного поворота. Рассмотрим алгоритм возвращения после поворота направо.
- Включаем правый мотор в обратную сторону.
- Продолжаем поворачивать, пока левый датчик линии не увидит чёрную линию.
- Еще немно доворачиваем правым моторм, чтобы выровнятся по линии.
- Останавливаем мотор.
Реализуем этот алгоритм в программе. Для обратного поворота напишем функцию turnBackR(). Она принимает два аргумента:
speed— скорость вращения колеса.waitTime— время в миллисекундах, необходимое для того, чтобы робот выровнялся по линии (подбирается экспериментально).
- right-turnback-function-example.ino
- // Функция возврата после поворота направо
- void turnBackR(int speed, int waitTime) {
- // Включаем левый мотор в обратную сторону
- motorsDrive(-speed, 0);
- // Ждём, пока левый датчик снова увидит линию
- while (readLineL() > LINE_BLACK) {
- delay(10);
- }
- // Даем время, чтобы выровнять робота
- delay(waitTime);
- // Останавливаем моторы
- motorsDrive(0, 0);
- delay(250);
- }
Если в тестах что-то идёт не так, смело делай программный «костыль» — быстро написанный код, который хоть как-то решает проблему. Например, добавь ещё небольшое движение вперёд и поворот направо до линии, чтобы окончательно выровнять робота.
Возврат после поворота налево выполняется аналогичным способом, но зеркально.
- Включаем левый мотор в обратную сторону.
- Продолжаем поворачивать, пока правый датчик линии не увидит чёрную линию.
- Еще немно доворачиваем правым моторм, чтобы выровнятся по линии.
- Останавливаем мотор.
Ниже приведён полный программный код для этого раздела. Но мы советуем тебе не заглядывать туда сразу. Сначала попробуй написать функцию возвратного поворота самостоятельно!
- bot-turnback-functions-test.ino
- // Даём понятное имя пину направления вращения левого мотора
- #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);
- }
10. Поворот на месте
Каждому роботу необходимо уметь выполнять разворот на 180°. При этом не имеет значения, в какую сторону будет поворачивать робот — по часовой стрелке или против.
Рассмотрим алгоритм поворота вокруг своей оси на месте:
Как и в прошлом примере, будем использовать датчики линии для получения обратной связи. Алгоритм здесь будет следующим:
- Крутим моторы в разные стороны какое-то время, чтобы оба датчика оказались над белым полем.
- После этого доворачиваем робота, пока датчик не увидит черную линию.
- Так как робот может встать не совсем ровно, выполняем небольшой доворот для выравнивания.
Реализуем этот алгоритм в программе. Для разворота на 180° по часовой стрелке мы добавили функцию turn180()_R. Как и функция поворота из предыдущего примера, она принимает 2 аргумента:
speed— скорость вращения колёс.waitTime— время в миллисекундах, необходимое для того, чтобы робот съехал с линии (подбирается экспериментально).
- right-turnaround-function-example.ino
- // Функция разворота на 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);
- }
Имея функцию разворота по часовой стрелке, довольно легко сделать разворот в обратную сторону. Достаточно лишь заменить все «правосторонние» действия на «левосторонние», и наоборот.
Ниже приведён полный программный код для этого раздела. Но мы советуем тебе не заглядывать туда сразу. Сначала попробуй написать функции разворота робота в разные стороны самостоятельно. На олимпиаде на это не будет времени, а вот на тренировке стоит попробовать все варианты!
- bot-turnaround-functions-test.ino
- // Даём понятное имя пину направления вращения левого мотора
- #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);
- }
11. Движение по линии
Итак, переходим к движению по линии. Так как времени обычно немного и нет соревнования по скорости роботов, для движения по линии вполне достаточно П-регулятора.
П-регулятор работает просто: чем больше ошибка, тем сильнее корректировка. Именно поэтому робот сам подстраивает своё движение под линию и удерживается на траектории в течение всего времени движения.
Идея здесь простая. Сначала договоримся, что в идеальном случае показания левого и правого датчиков линии одинаковые. Тогда разность между ними равна нулю. Это и есть целевое значение, или уставка.
Если показания датчиков отличаются, значит робот отклонился от линии. Такое отклонение называют ошибкой. В нашем случае ошибкой будет разность между показаниями левого и правого датчика.
err = leftLine - rightLine
После вычисления ошибки нужно сформировать управляющее воздействие. Оно вычисляется как произведение ошибки на коэффициент Кp. Этот коэффициент показывает, насколько сильно робот будет реагировать на отклонение. Если Кp увеличить, робот начнёт подруливать резче. Если сделать Кp слишком маленьким, робот будет реагировать медленно и может не успевать возвращаться на линию.
out = Kp * err
После вычисления управляющего воздействия его нужно применить к скорости моторов. В зависимости от того, с какой стороны робот отклонился от линии, скорость одного мотора увеличивается, а другого уменьшается. Благодаря этому робот поворачивает в нужную сторону и возвращается на линию.
motorsDrive(speed + out, speed - out)
Чтобы лучше понять работу регулятора, разберём несколько терминов.
- Регулятор — это устройство или программа, которая поддерживает объект управления в нужном состоянии. Для этого он получает данные с датчиков и формирует управляющее воздействие, которое помогает устранить отклонение.
- Пропорциональный регулятор — это регулятор, у которого управляющее воздействие зависит от величины отклонения. Проще говоря, чем сильнее робот отклонился от линии, тем сильнее он будет подруливать.
- Пропорциональность — это зависимость, при которой две величины изменяются согласованно. Например, если
x= 2 ×y, то одна величина в два раза больше другой.
- Отклонение (ошибка) — это величина, показывающая, насколько текущее значение отличается от желаемого.
- Управляющее воздействие — это действие, с помощью которого робот исправляет отклонение.
Напишем функцию followLine() с алгоритмом следования по линии с П-регулятором. На входе функция будет принимать два параметра:
speed- скорость, с которой должен двигаться робот.followTime- время, в течение которого робот должен ехать по линии.
- line-follow-function-example.ino
- // Функция следования по линии
- 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 в большую или меньшую сторону и посмотри, как это влияет на поведение робота.
- bot-line-follow-test.ino
- // Даём понятное имя пину направления вращения левого мотора
- #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);
- }
Можно написать и более навороченный ПИД-регулятор, но в нём сейчас попросту нет смысла. П-регулятор и так позволит роботу надёжно двигаться по линии. Однако советуем тебе почитать брошюру про ПИД-регулятор для общего развития.
12. Движение по линии до перекрёстка
Движение по линии до перекрёстка очень похоже на обычное движение по линии из прошлого раздела. Разница лишь в том, что вместо движения в течение заданного времени мы будем ехать до тех пор, пока не встретим перекрёсток.
Как понять, что робот доехал до перекрёстка? Очень просто: если оба датчика линии показывают, что видят чёрную линию, значит робот находится на перекрёстке.
В коде это будет выглядеть так:
- crossroads-detection.ino
- // Считываем показания датчиков линии
- int leftLine = readLineL();
- int rightLine = readLineR();
- // Ожидаем перекрёсток
- while (leftLine > LINE_BLACK && rightLine > LINE_BLACK) {
- // Выполняем движение по линии
- }
Напишем функцию goToCrossroad() для этого алгоритма. Она будет примимать однин параметр speed — скорость, с которой должен двигаться робот.
- crossroads-function-example.ino
- // Функция езды до перекрестка
- 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);
- }
Поэкспериментируй со значениями коэффициента и скорости робота. Подбери такие параметры, при которых робот будет двигаться по линии максимально быстро и плавно, уверенно проходя все изгибы трассы без сбоев.
Помни: отладка — важнейшая часть работы. Именно на неё уходит большая часть времени при создании эффективных роботов.
Постарайся довести настройку до такого уровня, чтобы ты мог буквально за несколько минут подобрать параметры П-регулятора для любого робота. Такая интуиция приходит только с практикой и постепенно формируется в ходе работы.
Ниже мы привели полный программный код для этого раздела.
- bot-line-crossroads-test.ino
- // Даём понятное имя пину направления вращения левого мотора
- #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);
- }
13. Пищалка для отладки
Когда робот подключён к компьютеру по USB-кабелю, для отладки удобно использовать сообщения в последовательный порт. Но во время прохождения трассы такой возможности уже не будет.
Поэтому в боевых условиях для отладки удобно использовать пищалку (англ. buzzer). Она может подавать сигнал при обнаружении какого-либо объекта, а также обозначать начало или завершение отдельной фазы алгоритма.
Напишем функцию beep(), которая позволит тебе подавать звуковой сигнал во время выполнения задания. На вход она будет принимать один параметр n - количество раз, которое нужно пропищать.
- buzzer-function-example.ino
- // Даём понятное имя пину пищалки
- #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);
- }
- }
[Тут нужно вставить схему подключения пищалки на Робота олимпийца]
14. Задание № 1
Мы рассмотрели основные алгоритмы движения робота, которые понадобятся вам на олимпиаде. Теперь перейдём к практическому заданию из реальной олимпиады.
Рассмотрим практическое задание заключительного этапа Всероссийской олимпиады школьников по труду (технологии) по профилю «Робототехника» для 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() один раз. Это нужно для того, чтобы робот выполнил задание только один раз, а не повторял его снова и снова по кругу.
- 14-1.ino
- // Библиотека для работы с сервоприводами
- #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() цикл, в котором робот будет по очереди подъезжать к перекрёсткам и сканировать ячейки.
- 14-2.ino
- 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(). Именно она отвечает за проверку ячейки и запись результата в массив. Давайте теперь создадим эту функцию.
- 14-3.ino
- void scan(int i, bool cubes[5]) {
- // Поворачиваемся к ячейке
- turnR();
- // Немного ждём, пока показания дальномера стабилизируются
- delay(500);
- // Проверяем, есть ли в ячейке кубик
- if (readDistIR() < 30) {
- // Сигнализируем, что обнаружили кубик
- beep(1);
- // Запоминаем, что в этой ячейке есть кубик
- cubes[i] = 1;
- }
- // Поворачиваемся обратно к линии
- turnBackR();
- }
Теперь первый шаг алгоритма готов, и его можно протестировать. Загрузите программу в робота, установите его в стартовую зону и запустите.
Понаблюдайте за его движением. Робот должен последовательно просканировать все пять перекрёстков и остановиться у последнего из них.
Переход на нижний ряд
После сканирования ряда A робот останавливается у последнего перекрёстка. Теперь он знает, где находятся кубики и свободные ячейки. Следующий шаг — перейти к ряду B.
Дополним функцию runRobot() соответствующей командой:
- 14-4.ino
- 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().
- 14-5.ino
- void goToB() {
- // Доезжаем до верхнего угла трассы
- goToCrossroad();
- // Поворачиваем направо
- turnR();
- // Доезжаем до нижнего угла трассы
- goToCrossroad();
- // Поворачиваем к ряду B
- turnR();
- }
</WRAP>
Протестируйте функцию goToB() отдельно. Установите робота на последний перекрёсток ряда A и убедитесь, что он правильно проезжает к ряду B.
После выполнения этой функции робот должен оказаться у первого перекрёстка нижнего ряда и быть ориентирован вдоль него.
Чтобы протестировать отдельную функцию, временно закомментируйте вызов runRobot() и вместо него вызовите функцию, которую хотите проверить. После завершения тестирования удалите строку с вызовом тестируемой функции и раскомментируйте вызов runRobot().
Сортировка кубиков
Теперь робот занял нужную позицию и готов перейти к следующему этапу — расстановке кубиков по своим местам.
Ему предстоит проехать перекрёстки ряда B в обратном порядке: от последнего к первому. На каждом перекрёстке робот будет проверять, нужно ли переместить кубик в соответствующую свободную ячейку ряда A.
- 14-6.ino
- 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(). Она будет объединять все действия, необходимые для перемещения одного кубика:
- подъехать к кубику и захватить его.
- доставить кубик в нужную ячейку ряда A.
- выгрузить кубик.
- вернуться на исходную позицию.
Действий довольно много, поэтому разделим их на отдельные функции:
- 14-7.ino
- void moveCube(int i, int n) {
- // Забираем кубик
- loadCube();
- // Везём кубик к нужной ячейке
- deliverCube(i, n);
- // Выгружаем кубик
- unloadCube();
- // Возвращаемся на исходную позицию
- goBack(i, n);
- }
Загрузка кубика
Функция loadCube() отвечает за «загрузку» кубика — захват кубика роботом.
Её задача состоит из трёх действий:
- заехать в ячейку с кубиком.
- захватить кубик.
- вернуться на линию.
- 14-8.ino
- void loadCube() {
- // Поворачиваем к ячейке
- turnR();
- // Доезжаем до кубика
- goToCrossroad();
- // Опускаем захват
- gripDown();
- // Разворачиваемся на 180°
- turnR_180();
- // Едем обратно к линии
- goToCrossroad();
- // Выезжаем на линию
- turnL();
- }
Сначала робот поворачивается к ячейке и подъезжает к кубику. Затем он опускает захват и разворачивается на 180°. После этого робот возвращается к линии уже с захваченным кубиком.
Доставка кубика
Теперь, когда кубик «загружен», его нужно доставить в соответствующую ячейку ряда A. За это отвечает функция deliverCube().
Параметр i показывает, какой кубик сейчас перемещается, а параметр n содержит общее количество перекрёстков.
- 14-9.ino
- 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().
- 14-10.ino
- void unloadCube() {
- // Поворачиваем к ячейке
- turnL();
- // Доезжаем до места установки кубика
- goToCrossroad();
- // Опускаем захват
- gripDown();
- // Разворачиваемся на 180°
- turnR_180();
- // Едем обратно к линии
- goToCrossroad();
- // Выезжаем на линию
- turnR();
- }
Робот подъезжает к свободной ячейке, опускает захват и оставляет в ней кубик. Затем он разворачивается, возвращается на линию и готов двигаться обратно к ряду B.
Возвращение на исходную позицию
После выгрузки кубика робот должен вернуться к исходной позиции — к соответствующему перекрёстку ряда B. Напишем для этого функцию goBack().
- 14-11.ino
- 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() командами возвращения на старт:
- 14-12.ino
- 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);
- }
Так одна большая задача разбивается на несколько понятных этапов: загрузка, доставка, выгрузка и возвращение. Каждый этап находится в отдельной функции, поэтому программу проще читать, тестировать и исправлять.
Полный код
Ниже приведён полный код программы из этого раздела:
- 14-13.ino
- // Библиотека для работы с сервоприводами
- #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);
- }
15. Роботы могут быть любыми
Год от года условия олимпиады меняются, и роботы попадаются разные. Посмотри на фото с заключительных этапов олимпиады, чтобы увидеть, какие варианты роботов там применялись.
Посмотри также несколько репортажей с заключительного этапа ВсОШ по робототехнике.
16. Задание № 2
Это практическое задание заключительного этапа Всероссийской олимпиады школьников по труду (технологии) по профилю «Робототехника» 2022–2023 учебного года для 11-х классов.
Построить и запрограммировать робота, который:
- Начинает движение в зоне старта (целиком своей вертикальной проекцией находится внутри зоны, ограниченной квадратом жёлтого цвета).
- Движется по линии с перекрёстками и перемещает кубики, расположенные на перекрёстках в верхней и нижней части полигона, на противолежащие перекрёстки внутрь синего квадрата 100×100 мм.
- Останавливается в зоне финиша (жёлтый квадрат 300×300 мм).
Сперва попробуем долгий, но полезный для тренировки алгоритм:
- Просканируем и верхний, и нижний ряды с кубиками, составив «карту» (два массива) с ними.
- После этого будем их сортировать.
Долгий алгоритм заведомо не подойдёт на самой олимпиаде: только на сканирование уйдёт порядка 80 секунд. Но можно считерить и расположить ИК-дальномер так, чтобы роботу не приходилось лишний раз поворачивать.
- 16-1.ino
- // пишем основной алгоритм для робота
- 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 с.
- 16-2.ino
- 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.
- 16-3.ino
- 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();
- // отправить кубик в нижний ряд и вернуться на перекрёсток
- }
Это не программа, а так — болванка, шаблончик. Видишь закономерность, которую можно заменить циклом? Ещё нужно срочно написать процедуру для движения из любого верхнего перекрёстка в соответствующий нижний.
А ещё у нашего робота теперь сбоку выступает дальномер, и становится особенно важно, в какую сторону делать разворот. Протестируем всё на первых трёх верхних кубиках.
- 16-4.ino
- // разворот на 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);
- }
- Возможно, твой робот не уложится в 180 секунд, если достанутся слишком медленные моторы. В таком случае нужно отстаивать свои права и убеждать жюри дать роботу доделать задание.
- Чтобы не путаться в процедурах с поворотами робота, удобно составить схему или тренироваться «прокручивать» выполнение кода прямо в своём воображении. Это трудное, но полезное умение.
- Прогони процедуру с разными сочетаниями
0и1и посмотри, как робот обрабатывает первые три кубика.
17. Задание № 2. Переделываем!
Предыдущий алгоритм получился очень долгий. Что ж, ещё раз внимательно прочитаем условие задачи.
Видишь, что сканировать можно только верхнюю область?
Нижняя будет или инверсной, или в ней будет пара пустых зон (вверху и внизу). А так получаем пары: (В0, Н1) и (В1, Н0). Поэтому можно просканировать только верхний ряд и начинать обработку с зелёной зоны у пятого верхнего кубика.
Что потеряем? Будет лишняя обработка одной пары (В4 и Н4 на рисунке). Условно говоря, это первый подход.
Второй подход: последовательно сканировать и сразу обрабатывать зоны C1–C5. Увидел в В1 кубик — перемести в Н1, не увидел — перемещай из Н1 в В1. Есть только один подвох: 1024 возможные комбинации!
Однако, если задуматься над тем, как должен двигаться робот, то вариантов очень мало.
Ещё можно как бы совместить два подхода: начать сканирование; если найден кубик — переместить его и продолжить сканирование с другой стороны. Чтобы переключить сторону, можно использовать флаг — логическую переменную со значениями (true|false).
Не верится, но весь алгоритм довольно компактный.
- 17-1.ino
- 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 класса были строчки:
- Инфракрасный дальномер (10–80 см) Sharp GP2Y0A21 или аналог, 2 шт.
- Пассивное крепление для дальномера, 2 шт.
Как думаешь, зачем дают два ИК-дальномера? Правильно, чтобы можно было сканировать с двух сторон.
Ещё несколько небольших уточнений:
- Хорошо бы уже дописать ПД- или ПИД-регулятор.
- Все временны́е задержки в коде надо бы сделать через переменные, чтобы было удобнее их массово изменять сразу во всей программе.
- Написать небольшую процедуру, чтобы в самом начале робот ждал, когда окажется на белом поле. Это ускорит процесс тестирования и отладки.
Алгоритм сравнительно простой, но программа постепенно усложняется. Приступаем к написанию.
Чтобы лучше понять алгоритм, посмотри видео с примером выполнения роботом этого задания.
- Мы ввели процедуру ожидания перед тем, как робот начнёт выполнять задание на поле.
- Внимательно изучи процедуру управления захватом. Постарайся понять принцип и запомнить формулу.
- У нас езда по коротким прямым, поэтому в программе применили ПИД-регулятор с нулевым коэффициентом интегральной составляющей. То есть, получился ПД-регулятор. Подробнее читай в брошюре про ПИД-регуляторы.
- Если бы линии были кривые или очень длинные, то стоило бы ввести интегральную составляющую для дополнительной коррекции курса.
- Скорости моторов в программе не оптимизировались, но если время позволяет — это полезно сделать.
- Как видно из объёма кода, необходим план, в какой последовательности его писать. Общая идея — поменьше набирать код текстом, побольше использовать функции «копировать/вставить».
Дополнительное задание
Улучши алгоритм так, чтобы робот не возил 4 и 5 кубики дальней дорогой. Оставь текущий алгоритм только для первых трёх кубиков.
И засеки общее время работы над задачей — это важно для самопроверки!
18. Задание № 2. Оптимизируем работу
Кубики 4 и 5 робот должен перевозить короткой дорогой. В прошлом алгоритме нужно уменьшить i до 3 и дописать шаблон.
А дописать программу нужно самостоятельно.
И останется последний шажок — учесть в алгоритме ситуацию, когда в вертикальном ряду нет кубиков, чтобы робот не возил пустоту. Для этого нужно добавить проверку наличия кубика перед его захватом через отдельную логическую переменнную (т. е. флаг).
- Идея создавать сначала шаблоны, а затем уточнять в них действия робота может сэкономить тебе время. Поначалу всё умещается в голове, но когда программа достигнет 450 строк, в ней будет тяжелее ориентироваться.
- Ещё один плюс в таком подходе — возможность получить баллы за все рабочие участки кода. Напомним, что у нас две цели: научиться решать задачи и уметь набирать как можно больше баллов.
Контрольные задания
1. Поле остаётся прежним. Во всех 10 зонах расположены кубики двух цветов: чёрного и белого. Если кубик белый, то его не трогаем, а если чёрный — то везём в красную зону на схеме.
Подсказка: проверь сначала, можно ли отличить чёрный и белый объект с помощью инфракрасного дальномера. Может, он показывает разные значения, и этой разницы достаточно, чтобы различать цвета кубиков?
2. Посмотри на поле и придумай какое-нибудь задание сам. Будешь варьировать алгоритмы — будет больше понимания во всех нюансах происходящего.
Структурные схемы Э1, Э3
Основным видом конструкторских документов в различных областях электротехники, радиоэлектроники, связи и др. являются схемы.
Схема — это графический конструкторский документ, на котором показаны в виде условных изображений или обозначений составные части изделия и связи между ними.
Схема является основой для разработки сборочных и электромонтажных чертежей и применяется при изучении принципа работы, монтаже и наладке изделия.
Правила выполнения и оформления схем регламентируют стандарты седьмой классификационной группы ЕСКД. ГОСТ 2.701-84 устанавливает единую классификацию схем для всех отраслей техники. По этой классификации все схемы (см. таблицу 1) разделены на виды (по характеру отображаемых физических процессов), типы (в зависимости от основного назначения) и имеют наименование и буквенно-цифровое обозначение (шифр, код).
Наименование схемы определяется её видом и типом, а шифр схемы состоит из буквенного и цифрового обозначения. Например, схема электрическая принципиальная имеет шифр Э3.
Функциональные части показывают в виде прямоугольников, внутрь которых вписывают наименования функциональных частей.
Обрати внимание, что на этой схеме нет дальномеров. Их надо добавить по аналогии с датчиками линии (светоотражения).
Рекомендации по оценке принципиальной схемы (для 10–11 классов)
Схему можно считать выполненной, если соблюдены следующие условия:
- Схема соответствует устройству участника (все линии взаимосвязи указаны верно, очевидны подключения всех компонентов схемы, собранных участником).
- Использованы верные условные графические обозначения (УГО) элементов.
- Линии взаимосвязи и их повороты выполнены горизонтально, вертикально или под углом, кратным 45°. Пересечения линий взаимосвязи строго под углом 90°.
- Узлы, соединяющие более трёх проводников, обозначены точкой.
- Подписаны позиционные обозначения элементов на схеме.
- Указаны контакты разъёмов функциональных блоков (контроллер Arduino, схема управления моторами и т. д.), к которым осуществлено подключение.
Один балл можно снизить, если:
- Допущена одна ошибка в подключении линий взаимосвязи (всё остальное верно).
- Схема нарисована небрежно: не соблюдены углы линий взаимосвязи, элементы не имеют позиционных обозначений, но все подключения верны.
- Большинство использованных УГО не соответствуют ГОСТ, но все подписи верны и схема читаема.
Если нарушений больше, задание оценивается в ноль баллов.
Ключевой фактор при оценивании — правильно отражённые на схеме подключения электрических соединений робота, произведённые участником во время выполнения основного задания. Рамка и основная надпись не относятся к критериям оценивания схемы.
Продолжение следует
Во второй части практикума ты узнаешь, как настраивать ПИД-регулятор для робота, оснащённого моторами с энкодерами.
Благодарность автору
«Амперка» выражает особую благодарность преподавателю Денису Геннадьевичу Копосову, чьё учебное пособие «Робототехника: подготовка к практическому туру регионального этапа Всероссийской олимпиады школьников» легло в основу данного практикума.
Все материалы использованы с разрешения автора. При перепечатке обязательна ссылка на оригинал.






