Инструменты пользователя

Инструменты сайта


Содержание

Практикум для «Олимпийца» (Olympic Bot). Часть 1: езда по линии с перекрёстками

В первой части практикума построим тренировочного робота «Олимпиец» (Olympic Bot) на Arduino, научим его ездить по линии и разберёмся с двумя примерами олимпиадных заданий за 2022–2023 учебный год.

 Amperka Olympic Bot

Обрати внимание

Для большей эффективности стоит дополнительно пройти два курса:

1. Коротко о платах

Контроллер Arduino — это инструмент для создания электронных устройств, которые более плотно взаимодействуют с физической средой, чем обычные компьютеры.

Arduino — это платформа, куда входит аппаратная часть и программная часть.

  • Аппаратная часть — это семейство контроллеров, например: Uno, Nano, Leonardo, Mega и другие.
  • Программная часть — это среда разработки Arduino IDE.

Контроллер Iskra Uno (Arduino Uno)

Распиновка

Платформа Iskra Uno предоставляtт 32 физических контакта на штатных колодках Arduino R3 и 6 дублирующих контактов на разъёме ICSP для подключения дополнительных плат расширения, модулей и сенсоров.

  • Всего физических контактов: 38 (32 + 6)
  • Контактов ввода-вывода GPIO: 20
  • Контакты с ШИМ: 6 с разрядностью 8 бит
  • Контакты с АЦП: 6 с разрядностью до 10 бит
  • Аппаратные интерфейсы: 1× UART, 1× SPI, 1× I²C

Распиновка Iskra Uno

Пины ввода-вывода GPIO

Большинство платформ имеют 20 контактов ввода-вывода. Режим работы контакта вы задаёте в своей программе. При этом некоторые контакты могут работать в режиме 0 или 1, некоторые поддерживают диапазон значений [0…255], а 6 контактов распознают аж [0…1023] значений при подключении аналоговых датчиков на вход.

АЦП

A0A5 — это как раз контакты для подключения датчиков и приёма значений от 0 до 1023. Однако их можно переключить и на выход, например, чтобы управлять сервоприводами.

ШИМ

Если контакта (порт) отмечен символом ~, то он поддерживает режим [0…255] и на вход, и на выход. Для управления скоростью моторов нужны именно такие порты.

Управляющих плат Arduino бесконечно много — их выпускают тысячи производителей, включая Амперку. Конечно, все они более-менее стандартизированы, но на олимпиаде подразумевается, что школьник увидит оборудование первый раз в жизни. Поэтому стоит заранее ознакомиться и поработать с разными версиями контроллеров и плат расширения.

В первой части практикума, помимо платы Iskra Uno, фигурируют две платы расширения: Motor Shield для управления моторами и Troyka Shield для подключения датчиков. Иногда их ещё называют шилдами.

Плата расширения Troyka Shield

Плата расширения Troyka Shield

Плата расширения Motor Shield

Плата расширения Motor Shield

В основе Motor Shield лежит микросхема L298P — драйвер моторов, который позволяет управлять скоростью и направлением вращения двух коллекторных двигателей.

Распиновка Motor Shield

Номер пина Имя пина Назначение
5 SPEED Управления скоростью вращения мотора на первом канале M1.
6 SPEED Управления скоростью вращения мотора на втором канале M2.
4 DIR Направления вращения мотором на первом канале M1.
7 DIR Направления вращения мотором на втором канале M2.

Vcc — вывод для положительного напряжения питания. Vcc относятся к схемам, построенным на биполярных транзисторах, отсюда и буквы «cc» (от англ. collector, коллектор). Но почему буквы две? Чтобы отличать напряжение питания Vcc от напряжения на выводе транзистора Vc, за рубежом принято писать две буквы. Такое обозначение часто встретится тебе на схемах и платах.

Питать шилд можно от Arduino (5 В), через контакт Vin (6,5–12 В) или от отдельного источника питания через клеммник PWR +/- (4,8–12 В). При установленной перемычке PWR JOIN моторы и Ардуино будут питаться от одного источника. При этом он может быть подключён как к DC-разъёму Ардуино, так и к клемме питания моторов.

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

По любым вопросам старайся сначала самостоятельно разобраться и найти ответ на многочисленных профильных сайтах про Arduino и робототехнику, например: wiki.amperka.ru, arduino.ru и так далее.

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

2. Сборка робота

Что понадобится

FIXME Раздел в разработке

Основные моменты

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

  • На крепеже для моторов видно, что туда надо устанавливать именно гайки. Шляпки винтов тут торчать не должны.
  • Не вкручивай винты, которые по сути ничего не держат. Это отверстие будет использовано в будущем для крепления аккумуляторного блока.
  • Цвета проводов важны!
    • Красный — питание 5 В Vcc.
    • Чёрный / Синий / Коричневый — земля GND.
    • Сигнальные. Для подключения сигналов на одном модуле бери разные по цвету провода. Например на триггера и эхо дальномера нужно брать разные цвета, иначе ведь сам потом запутаешься.

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

Шаги сборки

FIXME Раздел в разработке

Исходный код с подключённой электроникой

2-1.ino
  1. // Даём понятное имя пину направления вращения левого мотора
  2. #define MOTOR_L_DIR_PIN 7
  3. // Даём понятное имя пину управления скоростью вращения левого мотора
  4. #define MOTOR_L_SPEED_PIN 6
  5. // Даём понятное имя пину направления вращения правого мотора
  6. #define MOTOR_R_DIR_PIN 4
  7. // Даём понятное имя пину управления скоростью вращения правого мотора
  8. #define MOTOR_RIGHT_SPEED_PIN 5
  9. // Даём понятное имя пину левого датчика линии
  10. #define SENSOR_LINE_L_PIN A0
  11. // Даём понятное имя пину правого датчика линии
  12. #define SENSOR_LINE_R_PIN A1
  13. // Даём понятные имена пинам левого энкодера A и B
  14. #define ENCODER_L_PIN_A 2
  15. #define ENCODER_L_PIN_B 12
  16. // Даём понятные имена пинам правого энкодера A и B
  17. #define ENCODER_R_PIN_A 3
  18. #define ENCODER_R_PIN_B 13
  19. // Даём понятное имя пину ИК-дальномера
  20. #define SENSOR_DIST_IR_PIN A2
  21. // Даём понятные имена пинам УЗ-дальномера TRIG и ECHO
  22. #define SENSOR_DIST_UZ_TRIG_PIN 11
  23. #define SENSOR_DIST_UZ_ECHO_PIN 10
  24. // Даём понятное имя пину сервомотора дальномера
  25. #define SERVO_DIST_UZ_PIN 8
  26. // Даём понятное имя пину сервомотора захвата
  27. #define SERVO_GRIP_PIN 9

Собирать робота нужно внимательно, не напортачив в мелочах.

Все проводочки при возможности сразу прячь, чтобы ничего не торчало. И вообще, при сборке своих устройств старайся придерживаться хорошего кабель-менеджмента: прокладывай провода удобно и аккуратно, группируя «лапшу» по смыслу, по длине и по логике. Тогда под капотом будет настоящий порядок.

Не торопись с креплением захватов! Их устанавливают только после того, как протестируют и узнают, где находятся положения 0 и 90°.

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

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

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

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

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

3. Настройка Arduino IDE

Скачай подходящую версию Arduino IDE для операционной системы твоего компьютера и следуй нашим инструкциям по установке и настройке.

 Интерфейс Arduino IDE с тёмной темой

После всех манипуляций твоя среда Arduino IDE должна увидеть плату, подключённую по USB к компьютеру. Если что-то не получается с первого раза, внимательно сверься с инструкциями ещё раз.

Если плата продолжает капризничать, ты можешь поискать решение проблемы на breadboard.ru, форуме или обратиться в техподдержку Амперки.

 Дополнительные опции Arduino IDE

Общие советы по Arduino IDE для подготовки к соревнованиям

  • Купи бумажные стикеры и выпиши на них сочетания горячих клавиш, наподобие CTRL+C (копировать), CTRL+V (вставить) или CTRL+/ (закомментировать фрагмент кода). Заучи самые частые и полезные для себя комбинации, чтобы пользоваться ими на автоматизме.
  • Включи опции «Показывать номера строк» и «Сворачивание кода», чтобы быстрее ориентироваться в своей программе.
  • Используй окошко текстового редактора как буфер, в котором удобно хранить и копировать самые частые команды.
  • Тебе нужно будет много и быстро печатать. Воспользуйся онлайн-тренажёрами, чтобы научиться печатать вслепую, не отвлекая взгляд на клавиатуру.
  • Переключись на тёмную тему, чтобы поменьше уставали глаза.

4. Обозначение контактов и создание переменных

Конечно, код нужно писать и оформлять на совесть. Но когда у тебя буквально 2–3 часа на выполнение задачи, нет времени печатать лишнее на клавиатуре. Поэтому используй понятные сокращения для обозначения контактов: L вместо left, M вместо motor и так далее.

Что делает define? Вообще говоря, это не создание переменных. Это функция «Найти и заменить» в тексте первое слово вторым. Код хорошо читаемый, а оперативку у ардуинки при этом не отбираем.

4-1.ino
  1. //найти в программе первое «слово» и заменить вторым (для всех пинов)
  2. #define DL 12 //пин для направления вращения левого мотора
  3. #define SL 10 //пин для управления скоростью вращения левого мотора
  4. #define DR 13 //пин для направления вращения правого мотора
  5. #define SR 11 //пин для управления скоростью вращения правого мотора
  6. #define IR A2 //пин для подключения инфракрасного дальномера
  7. #define UT 7 //пин для подключения ультразвукового дальномера (TRIG в T)
  8. #define UE 8 //пин для подключения ультразвукового дальномера (для эхо)
  9. #define TURN A3 //для подключения сервомотора для поворота "головы"
  10. #define GRIP 9 //пин для подключения сервомотора для захвата
  11. #define LL A4 //пин для левого датчика "цвета"
  12. #define LR A5 //пин для правого датчика "цвета"
  13. // все нужные (и не очень) глобальные переменные
  14. float Kp = 3, Kd = 1.0; // коэффициенты ПД-регулятора
  15. float er = 0, erO = 0; //ошибка и прошлая ошибка
  16. int PL, PR; //мощности левого и правого моторов
  17. int c1, c2, c3; //счётчики каких-нибудь объектов
  18. unsigned long old, t1, t2, t3; // разные таймеры
  19. int dl, dr; //данные с датчиков цвета
  20. int ir, us; //данные с дальномеров
  21. int tmp1, tmp2; //вспомогательные переменные
  22. bool f1 = false, f2 = false, f3 = false, f4 = false, f5 = false;

Стоит также почитать про типы данных: int, float, bool, unsigned long, а также про локальные и глобальные переменные.

Вообще говоря, современный синтаксис C++ предпочитает выражения const и constexpr для объявления именованных констант. Мы разобрали это подробнее в третьей части практикума.

5. Настраиваем режим работы контактов

Далее описываем режимы работы (pinMode) всех используемых контактов в процедуре setup. В ней потом будет располагаться основная программа (алгоритм).

5-1.ino
  1. //=====настройка и то, что выполняется 1 раз ======
  2. void setup() {
  3. //настройка выходов
  4. pinMode(DL, OUTPUT);
  5. pinMode(SL, OUTPUT);
  6. pinMode(DR, OUTPUT);
  7. pinMode(SR, OUTPUT);
  8. pinMode(TURN, OUTPUT);
  9. pinMode(GRIP, OUTPUT);
  10. pinMode(UT, OUTPUT);
  11. //настройка входов
  12. pinMode(UE, INPUT);
  13. pinMode(IR, INPUT);
  14. pinMode(LL, INPUT);
  15. pinMode(LR, INPUT);
  16. //подключаем монитор последовательного порта
  17. Serial.begin(9600);
  18. delay(1000);
  19. //=================================
  20. //И вот тут будет основной алгоритм...
  21. //runForest();
  22. //=================================
  23. }
  24. //=====бесконечный цикл===========
  25. void loop() {
  26. // и этот цикл будет пустым...
  27. }

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

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

Обращать внимание будут на следующие критерии:

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

6. Ультразвуковой и инфракрасный дальномеры, датчики линии, сервопривод

Ультразвуковой дальномер HC-SR04 работает так: подаём импульс с продолжительностью 10 мкс на пин Trig, и модуль посылает ультразвуковой импульс, который отражается от препятствия и детектируется. Затем с пина Echo возвращается импульс, продолжительность которого равна времени путешествия звуковой волны. Ардуинка должна измерить этот импульс.

Инфракрасный дальномер Sharp GP2Y0A21 предназначен для измерения расстояния до препятствий от 10 до 80 см.

В датчиках Sharp установлен инфракрасный светодиод (IR LED) с линзой, который излучает узкий световой луч. Отражённый от объекта луч направляется через другую линзу на позиционно-чувствительный фотоэлемент (Position-Sensitive Detector, PSD). От местоположения падающего на PSD луча зависит его проводимость и, следовательно, напряжение. Таким образом можно вычислить расстояние до объекта.

Работать с сенсором Sharp довольно просто — достаточно подключить к нему питание и завести вывод на аналоговый вход Arduino. Значение на входе представляет собой целое число от 0 до 1023. Можно не преобразовывать данные в сантиметры, а просто сравнивать показания.

Итак, для ультразвукового дальномера есть две рекомендации по коду: брать значение по модулю и ограничить расстояние (100 см вполне хватит). Так он будет меньше «глючить» и ошибаться. А вот писать фильтр данных, к сожалению, будет слишком долго.

6-1.ino
  1. //ультразвуковой дальномер (получаем см)
  2. int us_cm() {
  3. digitalWrite(UT, LOW);
  4. delayMicroseconds(5);
  5. digitalWrite(UT, HIGH);
  6. delayMicroseconds(10);
  7. digitalWrite(UT, LOW);
  8. int tmp0 = pulseIn(UE, HIGH);
  9. int tmp = abs(tmp0 / 58);
  10. if (tmp > 100) { tmp = 100; }
  11. return tmp;
  12. }

Аналоговый датчик линии выполнен на оптопаре TCRT5000, которая состоит из двух элементов: ИК-светодиода (излучателя) и фототранзистора (приёмника). Когда светодиод излучает инфракрасный свет, световой поток отражается от поверхности и попадает на фототранзистор, где преобразуется в электрический сигнал. Тёмный цвет отражает меньше света, светлый — больше.

Датчик непрерывно генерирует импульсы в ИК-диапазоне. Если импульс не отражается совсем или отражается так слабо, что уровень детектируемого сигнала не превышает заданное пороговое значение (для этого и регулятор — потенциометр), то цифровой выход модуля находится в состоянии 1 (высокий логический уровень). Как только в области детекции появляется какой-нибудь объект, то будет 0, о чём сигнализирует встроенный светодиод.

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

6-2.ino
  1. // тестирование всех датчиков
  2. void test_sensors(int t) {
  3. old = millis();
  4. while (millis() - old < t) {
  5. dl = (1024 - analogRead(LL)) / 16;
  6. dr = (1024 - analogRead(LR)) / 16;
  7. ir = analogRead(IR);
  8. us = us_cm();
  9. Serial.print(" l=");
  10. Serial.print(dl);
  11. Serial.print(" r=");
  12. Serial.print(dr);
  13. Serial.print(" ir= ");
  14. Serial.print(ir);
  15. Serial.print(" us=");
  16. Serial.println(us);
  17. delay(100);
  18. }
  19. }

Замечание по датчикам линии

Если тебе доводилось писать ПД-регулятор для Lego-робота, то там датчики показывали от 0 до 100. А тут от 0 до 1023, де ещё и с инверсией. Мозгу тяжело перестроиться. Поэтому есть смысл перевести показания в леговские [0…100].

Программная инверсия, чтобы на белом датчик показывал больше, а на чёрном меньше (диапазон от 0 до 1023):

dl = (1024 - analogRead(LL))
dr = (1024 - analogRead(LR))

Сжимаем диапазон до [0…100], но реально он будет меньше.

dl = (1024 - analogRead(LL))/10;
dr = (1024 - analogRead(LR))/10;

Поскольку линии прямые, то лучше ещё сжать диапазон до [0…60]:

dl = (1024 - analogRead(LL))/16;
dr = (1024 - analogRead(LR))/16;

Сервопривод — это электромотор с редуктором и блоком управления в одном корпусе. FS90 — аналоговый сервопривод с удержанием угла, то есть мотор, который преобразует управляющие сигналы в установку и удержание заданного угла. Его диапазон вращения от 0 до 180°.

Сервоприводы используются для открывания/закрывания заслонок, поворота различных механизмов на заданный градус и в качестве «суставов» при создании разнообразных механизмов. Для этого в комплекте сервы есть разные насадки, которые надеваются на вал мотора для дальнейшего соединения с механизмами устройств.

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

6-3.ino
  1. //управление захватом (указывать градусы от 0 до 180)
  2. void grip(int x) {
  3. int wait = 7 * x + 500;
  4. for (int i = 0; i <= 50; i++) {
  5. digitalWrite(GRIP, HIGH);
  6. delayMicroseconds(wait);
  7. digitalWrite(GRIP, LOW);
  8. delay(20);
  9. }
  10. }
  11. //управление захватом (указывать градусы от 0 до 180)
  12. void gripV2(int a) {
  13. // Преобразуем угол (0-180) в ширину импульса (500-2500 мкс)
  14. int pw = map(a, 0, 180, 500, 2500);
  15. // Отправляем импульс управления сервоприводу
  16. digitalWrite(GRIP, HIGH);
  17. delayMicroseconds(pw);
  18. digitalWrite(GRIP, LOW);
  19. delay(20);
  20. }

Ответственный момент — всё аккуратно тестируем.

6-4.ino
  1. //пишем основной алгоритм для робота
  2. //всё тестирование тоже можно писать
  3. void runForest() {
  4. gripV1(0);
  5. delay(1000);
  6. gripV1(90);
  7. delay(1000);
  8. gripV1(0);
  9. delay(1000);
  10. gripV1(90);
  11. delay(1000);
  12. gripV1(0);
  13. delay(1000);
  14. gripV1(90);
  15. delay(1000);
  16. gripV2(0);
  17. delay(1000);
  18. gripV2(90);
  19. delay(1000);
  20. gripV2(0);
  21. delay(1000);
  22. gripV2(90);
  23. delay(1000);
  24. gripV2(0);
  25. delay(1000);
  26. gripV2(90);
  27. delay(1000);
  28. test_sensors(30000);
  29. }

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

6-5.ino
  1. //настраиваем положение захвата и "головы"
  2. void test_servo() {
  3. turn(0); //"голова" вперёд
  4. gripV1(0); // 0 = захват опущен
  5. delay(1500);
  6. gripV1(120); //90-120 = захват поднят
  7. turn(90); //поворот "головы" влево
  8. gripV1(0); // 0 = захват опущен
  9. delay(1500);
  10. gripV1(120); //90-120 = захват поднят; ЭТО ПЕРВОНАЧАЛЬНОЕ ПОЛОЖЕНИЕ
  11. turn(0); //"голова смотрит вперёд"; ЭТО ПЕРВОНАЧАЛЬНОЕ ПОЛОЖЕНИЕ
  12. }

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

Поворачивай робота руками так, будто он сам повернул вокруг своей оси на небольшой угол. Старайся при этом не допускать никакого продольного смещения робота относительно тестового препятствия. Иначе все измерения придётся начинать заново!

Например, при 10 см до объекта его наличие точно определяется условием: показания больше 300. Если так — объект есть, иначе объекта нет. Запиши полученное значение и запомни: ближе 10 см — показания больше 400.

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

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

6-6.ino
  1. //====спецнастройки датчиков линии
  2. int delL = 15; // подобрать делитель, чтобы на белом было 55–60
  3. int delR = 12; // подобрать делитель, чтобы на белом было 55–60
  4. int wl = 59; // внести из монитора данные на белом (левый)
  5. int bl = 10; // внести из монитора данные на чёрном (левый)
  6. int wr = 57; // внести из монитора данные на белом (правый)
  7. int br = 10; // внести из монитора данные на чёрном (правый)
  8. int Br = (wr + br) / 2; // это для остановок на линии при движении и поворотах
  9. int Bl = (wl + br) / 2; // это для остановок на линии при движении и поворотах
  10. //================================
  11.  
  12. void setup() {
  13.  
  14. }
  15.  
  16. void loop() {
  17. dl = (1024 - analogRead(LL)) / delL;
  18. dr = (1024 - analogRead(LR)) / delR;
  19. }

Подбираем делители delL и delR так, чтобы получить значения 55–60.

Пример плохого подбора

Пример хорошего подбора

7. Мотор с редуктором GA12-N20

Моторы с редуктором GA12-N20 имеют угловую скорость от 10 до 300 об/мин. Какой именно попадётся тебе — неизвестно… Нужно знать общий принцип, как с ними работать.

Любой мотор постоянного тока требует для работы ток силой 0,2–1 А. Но цифровой выход Arduino может дать нам только 20 мА. Микроконтроллеры электрически слишком слабы, чтобы управлять вращением моторов напрямую. Поэтому контроллеру нужен мощный посредник — драйвер моторов.

Для ВсОШ прописан драйвер L298N. Он может контролировать скорость и направление вращения двух двигателей постоянного тока суммарным током до 4 А.

Для Arduino делаются специальные платы расширения — шилды на драйвере L298N, такие как Motor Shield и многие другие. С ними Ардуинка уже сможет управлять двумя моторами по 2 А.

arduino-motor-shield.jpg

7-1.ino
  1. //движение вперёд
  2. void fwd(int sl, int sr, int t) {
  3. digitalWrite(DL, LOW);
  4. digitalWrite(DR, LOW);
  5. analogWrite(SL, sl);
  6. analogWrite(SR, sr);
  7. old = millis();
  8. while (millis() - old < t) {
  9. delay(1);
  10. }
  11. analogWrite(SL, 0);
  12. analogWrite(SR, 0);
  13. delay(250);
  14. }
7-2.ino
  1. //движение назад
  2. void fwd(int sl, int sr, int t) {
  3. digitalWrite(DL, HIGH);
  4. digitalWrite(DR, HIGH);
  5. analogWrite(SL, sl);
  6. analogWrite(SR, sr);
  7. old = millis();
  8. while (millis() - old < t) {
  9. delay(1);
  10. }
  11. analogWrite(SL, 0);
  12. analogWrite(SR, 0);
  13. delay(250);
  14. }

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

8. Повороты на перекрёстках

Алгоритм здесь такой:

  1. Включаем мотор.
  2. Немного ждём.
  3. Ждём, когда датчик «увидит» линию.
  4. Останавливаем мотор.
  5. Немного вращаем его обратно.
8-1.ino
  1. //поворот НАПРАВО до линии
  2. void turnR(int sl, int t) {
  3. digitalWrite(DL, LOW); //указываем направление вращения
  4. analogWrite(SL, sl); //включаем мотор и даём съехать с линии
  5. delay(t);
  6. //ждём черную линию под датчиком
  7. dl = (1024 - analogRead(LL)) / delL;
  8. while (dl > bl) {
  9. dl = (1024 - analogRead(LL)) / delL;
  10. delay(10);
  11. }
  12. analogWrite(SL, 0); //останавливаем мотор
  13. delay(500);
  14. digitalWrite(DL, HIGH); //нужно чуть назад покрутить левое колесо
  15. analogWrite(SL, sl);
  16. delay(200);
  17. analogWrite(SL, 0);
  18. delay(250);
  19. }
  20. //поворот НАЛЕВО до линии
  21. void turnL(int sr, int t) {
  22. digitalWrite(DR, LOW); //указываем направление вращения
  23. analogWrite(SR, sr); //включаем мотор и даём съехать с линии
  24. delay(t);
  25. dr = (1024 - analogRead(LR)) / delR; // ждём чёрную линию под датчиком
  26. while (dr > Br) {
  27. dr = (1024 - analogRead(LR)) / delR;
  28. delay(10);
  29. }
  30. analogWrite(SR, 0); //останавливаем мотор
  31. delay(500);
  32. digitalWrite(DR, HIGH); //нужно чуть назад покрутить левое колесо
  33. analogWrite(SR, sr);
  34. delay(200);
  35. analogWrite(SR, 0);
  36. delay(250);
  37. }
  • Значения задержки (аргумент функции DELAY) 200 и 250 мс даны здесь для примера.
  • Тебе нужно подбирать эти значения экспериментально в зависимости от скорости вращения моторов. Для разных моделей они будут разные.
  • Всегда закладывай паузу порядка 250–500 мс (или больше) для торможения моторов, ведь у них есть инерция.
  • Во время подготовки прогоняй все тестовые процедуры раза по 4, чтобы набить руку. На олимпиаде такой роскоши уже не будет.

9. Повороты обратно

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

Этот манёвр выполняется так:

  1. Включаем мотор в обратную сторону.
  2. Ждём время.
  3. Ждём сигнал датчика.
  4. Снова выжидаем паузу.
9-1.ino
  1. //возврат после поворота НАПРАВО до линии
  2. //возврат обратно после поворота НАПРАВО до линии
  3. void turnR_back(int sl, int t) {
  4. digitalWrite(DL, HIGH); //указываем направление вращения
  5. analogWrite(SL, sl); //включаем мотор и даём съехать с линии
  6. delay(t);
  7. // ждём черную линию
  8. dl = (1024 - analogRead(LL)) / delL;
  9. while (dl > Bl) {
  10. dl = (1024 - analogRead(LL)) / delL;
  11. delay(10);
  12. }
  13. delay(100);
  14. analogWrite(SL, 0);
  15. delay(250);
  16. }

Не располагай датчики слишком широко. Посмотри на фото: расстояние между датчиками примерно соответствует ширине линии, может, на 3–5 мм больше.

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

9-2.ino
  1. //возврат обратно после поворота НАЛЕВО до линии
  2. void turnL_back(int sr, int t) {
  3. digitalWrite(DR, HIGH); //указываем направлние вращения
  4. analogWrite(SR, sr); //включаем мотор и даём съехать с линии
  5. delay(t);
  6. dl = (1024 - analogRead(LR)) / delR;
  7. while (dl > Bl) {
  8. dl = (1024 - analogRead(LR)) / delR;
  9. delay(10);
  10. }
  11. delay(100);
  12. analogWrite(SR, 0);
  13. delay(250);
  14. //может слетать с линии
  15. //"костыль": чуть вперёд и довернуть до линии
  16. digitalWrite(DL, LOW);
  17. digitalWrite(DR, LOW);
  18. analogWrite(SL, 70);
  19. analogWrite(SR, 90);
  20. delay(200);
  21. analogWrite(SL, 0);
  22. analogWrite(SR, 0);
  23. delay(250);
  24. turnR(70, 50);
  25. }

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

10. Поворот на месте

Роботу просто необходимо уметь делать разворот на 180° для большинства задач. В какую сторону поворачивать — не имеет значения.

Идея алгоритма такая:

  1. Крутим колёса в разные стороны какое-то время, чтобы оба датчика оказались над белым полем.
  2. После этого доворачиваем, пока датчик не «увидит» линию.
  3. Так как робот встанет не очень ровно, чуть-чуть поворачиваем обратно (~150 мс).
10-1.ino
  1. //разворот на 180 (крутится влево)
  2. void turn180(int sl, int sr, int t) {
  3. digitalWrite(DR, LOW); //указываем направление вращения
  4. digitalWrite(DL, HIGH); //указываем направление вращения
  5. analogWrite(SR, sr); //включаем мотор и даём съехать с линии
  6. analogWrite(SL, sl); //включаем мотор и даём съехать с линии
  7. delay(t);
  8. //а вот теперь ждём чёрную линию под датчиком
  9. dr = (1024 - analogRead(LR)) / delR;
  10. while (dr > Br) {
  11. dr = (1024 - analogRead(LR)) / delR;
  12. delay(10);
  13. }
  14. analogWrite(SR, 0); //останавливаем мотор
  15. analogWrite(SL, 0);
  16. delay(200);
  17. digitalWrite(DR, HIGH); //покрутимся в обратную сторону
  18. digitalWrite(DL, LOW);
  19. analogWrite(SL, sl);
  20. analogWrite(SR, sr);
  21. delay(150);
  22. analogWrite(SR, 0);
  23. analogWrite(SL, 0);
  24. delay(250);
  25. }

В принципе, имея процедуру разворота влево, довольно легко сделать разворот в правую сторону. Достаточно лишь скопировать код и поменять всё «левостороннее» вращение моторов на «правое».

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

11. Движение по линии

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

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

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

Мощности моторов тогда будем вычислять по формулам:

PL = sl + Kp × (dl − dr);

PR = sr − Kp × (dl − dr);

  • sl — стартовая скорость левого мотора, sr — стартовая скорость правого.
  • dl — показания левого датчика линии, dr — показания правого.
  • Kp × (dl − dr) — управляющее воздействие на моторы. Как видно из формул, оно пропорционально ошибке.
  • dl − dr — это и есть ошибка.
  • Kp — усиливающий коэффициент.

Регулятор — это устройство (или программа), выполняющее задачу поддержания объекта управления в определённом состоянии. Регулятор получает информацию о состоянии объекта управления с помощью датчиков и вырабатывает управляющее воздействие для того, чтобы его корректировать.

Пропорциональный регулятор — это регулятор, в котором управляющее воздействие пропорционально отклонению. Говоря проще, чем сильнее робот (наш объект управления) отклонился от желаемого значения — тем сильнее он будет корректировать своё отклонение подруливанием.

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

Пропорциональность. Пропорциональными называются две взаимно зависимые величины, если отношение их значений остаётся неизменным. Например: x = 2 × y. Здесь y всегда в 2 раза больше, чем x — значит, эти числа пропорциональны.

Отклонение (ошибка) — величина, характеризующая, насколько сильно отличается текущее значение датчика(ов) от желаемого состояния.

Управляющее воздействие — сила, с которой объект корректирует отклонение.

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

Пишем процедуру движения робота по линии на заданное время, используя П-регулятор.

11-1.ino
  1. // движение по линии (скорости моторов и миллисекунды)
  2. void goLine_s(int sl, int sr, int t) {
  3. digitalWrite(DL, LOW);
  4. digitalWrite(DR, LOW);
  5. analogWrite(SL, sl);
  6. analogWrite(SR, sr);
  7. old = millis();
  8. while (millis() - old < t) {
  9. dl = (1024 - analogRead(LL)) / delL;
  10. dr = (1024 - analogRead(LR)) / delR;
  11. int PL = sl + Kp * (dl - dr);
  12. int PR = sr - Kp * (dl - dr);
  13. if (PL < 0) { PL = 0; }
  14. if (PR < 0) { PR = 0; }
  15. if (PL > 200) { PL = 200; }
  16. if (PR > 200) { PR = 200; }
  17. analogWrite(SL, PL);
  18. analogWrite(SR, PR);
  19. delay(10);
  20. }
  21. analogWrite(SL, 0);
  22. analogWrite(SR, 0);
  23. }
  24. float se = 0;
  25. float Ki = 0.01;
  26. erO = 0;
  27. old = millis();
  28. while (millis() - old < t) {
  29. dl = (1024 - analogRead(LL)) / delL;
  30. dr = (1024 - analogRead(LR)) / delR;
  31. er = dl - dr;
  32. se = se + er;
  33. int PL = sl + (Kp * er + Ki * se + Kd * (er - erO));
  34. int PR = sr - (Kp * er + Ki * se + Kd * (er - erO));
  35. if (PL < 0) { PL = 0; }
  36. if (PR < 0) { PR = 0; }
  37. if (PL > 200) { PL = 200; }
  38. if (PR > 200) { PR = 200; }
  39. analogWrite(SL, PL);
  40. analogWrite(SR, PR);
  41. delay(10);
  42. erO = er;
  43. }

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

Это интересно

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

12. Движение по линии до перекрёстка

12-1.ino
  1. // движение до перекрёстка (скорости моторов)
  2. void goLine_c(int sl, int sr) {
  3. digitalWrite(DL, LOW);
  4. digitalWrite(DR, LOW);
  5. analogWrite(SL, sl);
  6. analogWrite(SR, sr);
  7. old = millis();
  8. while (millis() - old < 500) {
  9. dl = (1024 - analogRead(LL)) / delL;
  10. dr = (1024 - analogRead(LR)) / delR;
  11. PL = sl + Kp * (dl - dr);
  12. PR = sr - Kp * (dl - dr);
  13. if (PL < 0) { PL = 0; }
  14. if (PR < 0) { PR = 0; }
  15. if (PL > 200) { PL = 200; }
  16. if (PR > 200) { PR = 200; }
  17. analogWrite(SL, PL);
  18. analogWrite(SR, PR);
  19. delay(5);
  20. }
  21. while (dl + dr > Bl + Br) {
  22. dl = (1024 - analogRead(LL)) / delL;
  23. dr = (1024 - analogRead(LR)) / delR;
  24. PL = sl + Kp * (dl - dr);
  25. PR = sr - Kp * (dl - dr);
  26. if (PL < 0) { PL = 0; }
  27. if (PR < 0) { PR = 0; }
  28. if (PL > 200) { PL = 200; }
  29. if (PR > 200) { PR = 200; }
  30. analogWrite(SL, PL);
  31. analogWrite(SR, PR);
  32. delay(5);
  33. }
  34. delay(150); // чтобы чуть проехал за линию
  35. analogWrite(SL, 0);
  36. analogWrite(SR, 0);
  37. delay(250);
  38. }

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

Помни, что отладка – важнейшая часть работы, и большая часть времени при создании эффективных роботов посвящена именно ей.

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

13. Пищалка для отладки

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

Если под рукой есть пищалка, она может стать твоим помощником при тестировании робота и подготовке. В нашем роботе как раз есть такой модуль.

13-1.ino
  1. #define BUZ 4 //пищалка (если есть)
  2.  
  3. pinMode(BUZ, OUTPUT);
  4.  
  5. //три раза пикнуть
  6. void pip() {
  7. for (int i = 0; i < 3; i++) {
  8. digitalWrite(BUZ, HIGH);
  9. delay(200);
  10. digitalWrite(BUZ, LOW);
  11. delay(200);
  12. }
  13. }
  14.  
  15. void setup() {
  16. // put your setup code here, to run once:
  17. }
  18.  
  19. void loop() {
  20. // put your main code here, to run repeatedly:
  21. }

Когда робот будет обнаруживать объекты, это нужно как-то контролировать. Например, моргнуть светодиодом или подать звуковой сигнал пищалкой.

Скорее всего, под рукой будет встроенный светодиод контроллера, но его порт может быть занят другой периферией. А вот пищалка весьма кстати — она легко подключается напрямую к контактам Arduino, как показано на схеме.

14. Задание № 1

Это практическое задание заключительного этапа Всероссийской олимпиады школьников по труду (технологии) по профилю «Робототехника» 2022–2023 учебного года для 9-х классов.

Построить и запрограммировать робота, который:

  • Начинает движение в зоне старта (целиком своей вертикальной проекцией находится внутри зоны, ограниченной квадратом жёлтого цвета).
  • Движется по линии с перекрёстками и перемещает кубики, расположенные в нижней части полигона, на противолежащие перекрёстки в верхней части полигона, на которых не расположены кубики, за бортик внутрь синего квадрата 100×100 мм.
  • Останавливается в зоне финиша (жёлтый квадрат 300×300 мм).

Итак, какой план для робота? Можно поступить так:

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

Пока так. Подробности уточним по мере того, как начнём кодить. ;-)

14-1.ino
  1. // пишем основной алгоритм для робота
  2. void runForest() {
  3. pip();
  4. // сканим верхний ряд
  5. bool o[5] = {0, 0, 0, 0, 0}; // массив логических элементов
  6. int n = 5; // количество перекрёстков
  7. int i = 0; // номер текущего перекрёстка
  8. while (i < n) {
  9. goLine_c(100, 120); // едем до перекрёстка
  10. turnR(120, 1000); // поворачиваем
  11. delay(500); // замерли перед замером
  12. if (analogRead(IR) > 300) { // проверяем наличие кубика
  13. pip(); // пикаем
  14. o[i] = 1; // запоминаем, что на i-м месте есть кубик
  15. }
  16. turnR_back(120, 500); // возвращаемся на основную линию
  17. i++; // увеличиваем номер перекрёстка (следующий)
  18. }
  19. goLine_c(100, 120); // едем в верхний правый угол
  20. turnR(120, 1000); // поворачиваем
  21. goLine_c(100, 120); // едем в нижний правый угол
  22. turnR(120, 1000); // поворачиваем
  23. // мы в правом нижнем углу, верхний ряд отсканирован
  24. // для теста вернёмся в зону старта
  25. i = 0;
  26. while (i < n) {
  27. goLine_c(100, 120); // едем до перекрёстка
  28. i++; // увеличиваем номер перекрёстка (следующий)
  29. }
  30. goLine_c(100, 120); // едем в нижний левый угол
  31. turnR(120, 1000); // поворачиваем
  32. goLine_c(100, 120); // едем в верхний левый угол
  33. }

Эта программа уже позволяет набрать какое-то количество баллов. Если ничего не получилось бы далее — этот кусочек кода можно сдавать жюри.

Итак, верхнюю часть отсканировали, её больше не трогаем. Теперь тестируем обработку нижнего ряда и запускаем робота из нижнего правого угла. Если нужно — повторяем процедуру с этой же точки, а не с самого старта.

14-2.ino
  1. // пишем основной алгоритм для робота
  2. void runForest()
  3. {
  4. pip();
  5. delay(3000); // это чтобы успеть нажать на "монитор порта"
  6. Serial.println("===============");
  7. bool o[5] = {1, 1, 0, 0, 1}; // заменяем расположение кубиков двиочным кодом
  8. bool o2[5] = {0, 0, 0, 0, 0}; // это массив для нижнего ряда, какие брать
  9. for (int j = 0; j < 5; j++)
  10. { // заполняем массив нижнего ряда
  11. o2[j] = o[4 - j]; // последний вверху - это первый внизу :)
  12. Serial.print(o2[j]); // выводим для контроля
  13. }
  14. }

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

Ну, вперёд! Разбирайся, вникай, тестируй, исследуй.

Как быстро научиться искать зависимости

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

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

14-3.ino
  1. // пишем основной алгоритм для робота
  2. void runForest()
  3. {
  4. // мы в правом нижнем углу, верхний ряд отсканирован
  5. pip();
  6. gripV1(100);
  7. bool o[5] = {1, 1, 0, 0, 0}; // заменяем расположение кубиков кодом
  8. bool o2[5] = {0, 0, 0, 0, 0}; // это массив для нижнего ряда, какие брать
  9. for (int j = 0; j < 5; j++)
  10. o2[j] = o[4 - j]; // заполняем массив нижнего ряда
  11. // последний вверху - это первый внизу :)
  12. int n = 5; // количество перекрёстков
  13. int x = 0; // номер текущего перекрёстка
  14. while (x < 5)
  15. {
  16. goLine_c(100, 120); // едем до перекрёстка
  17. if (o2[x] == 0)
  18. {
  19. turnR(120, 1000); // поворачиваем
  20. goLine_c(100, 120); // подъезжаем к кубику
  21. gripV1(0); // захватываем
  22. turn180(70, 90, 1000); // разворот
  23. goLine_c(100, 120); // обратно к перекрёстку
  24. turnL(120, 1000); // поворот
  25. // надо ехать к нижнему правому углу
  26. for (int z = 0; z < x + 1; z++)
  27. goLine_c(100, 120);
  28. turnL(120, 1000);
  29. goLine_c(100, 120);
  30. turnL(120, 1000);
  31. // теперь робот в верхнем правом углу
  32. pip(); // для отладки
  33. // едем к нужной зоне
  34. for (int z = 0; z < x + 1; z++)
  35. goLine_c(100, 120);
  36. // выгрузка кубика
  37. turnL(120, 1000);
  38. goLine_c(100, 120);
  39. gripV1(100);
  40. turn180(70, 90, 1000);
  41. goLine_c(100, 120); // обратно к перекрёстку
  42. turnR(120, 1000); // поворот
  43. for (int z = 0; z < x + 1; z++)
  44. goLine_c(100, 120);
  45. turnR(120, 1000); // поворот
  46. goLine_c(100, 120);
  47. turnR(120, 1000);
  48. // и мы снова в правом нижнем углу
  49. for (int z = 0; z < x + 1; z++)
  50. goLine_c(100, 120);
  51. // и мы на том перекрёстке, который обработали
  52. // переходим к следующему шагу
  53. pip();
  54. x++; // следующий шаг
  55. }
  56. else
  57. x++;
  58. }
  59. goLine_c(100, 120);
  60. turnR(120, 1000);
  61. goLine_c(100, 120);
  62. pip();
  63. }

Посмотри на видео, как должен работать робот.

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

После всех экспериментов получаем итоговый алгоритм.

14-4.ino
  1. // пишем основной алгоритм для робота
  2. void runForest()
  3. {
  4. delay(6000);
  5. pip();
  6. gripV1(100);
  7. // сканим верхний ряд
  8. bool o[5] = {0, 0, 0, 0, 0}; // массив логических элементов
  9. int n = 5; // количество перекрёстков
  10. int i = 0; // номер текущего перекрёстка
  11. while (i < n)
  12. {
  13. goLine_c(100, 120); // едем до перекрёстка
  14. turnR(120, 1000); // поворачиваем
  15. delay(500); // замерли перед замером
  16. if (analogRead(IR) > 300)
  17. { // проверяем наличие кубика
  18. pip(); // пикаем
  19. o[i] = 1; // запоминаем, что на i-м месте есть кубик
  20. }
  21. turnR_back(120, 500); // возвращаемся на основную линию
  22. i++; // увеличиваем номер перекрёстка (следующий)
  23. }
  24. goLine_c(100, 120); // едем в верхний правый угол
  25. turnR(120, 1000); // поворачиваем
  26. goLine_c(100, 120); // едем в нижний правый угол
  27. turnR(120, 1000); // поворачиваем
  28. //==================================================
  29. // мы в правом нижнем углу, верхний ряд отсканирован
  30. pip();
  31. gripV1(100);
  32. // bool o[5] = { 1, 1, 0, 0, 0 }; //заменяем расположение кубиков кодом
  33. bool o2[5] = {0, 0, 0, 0, 0}; // это массив для нижнего ряда, какие брать
  34. for (int j = 0; j < 5; j++)
  35. { // заполняем массив нижнего ряда
  36. o2[j] = o[4 - j]; // последний вверху - это первый внизу :)
  37. }
  38. // int n = 5; // количество перекрёстков
  39. int x = 0; // номер текущего перекрёстка
  40. while (x < 5)
  41. {
  42. // for (int z = 0; z < x + 1; z++)
  43. goLine_c(100, 120); // едем до перекрёстка
  44. if (o2[x] == 0)
  45. {
  46. turnR(120, 1000); // поворачиваем
  47. goLine_c(100, 120); // подъезжаем к кубику
  48. gripV1(0); // захватываем
  49. 40 turn180(70, 90, 1000); // разворот
  50. goLine_c(100, 120); // обратно к перекрёстку
  51. turnL(120, 1000); // поворот
  52. // надо ехать к нижнему правому углу
  53. for (int z = 0; z < x + 1; z++)
  54. goLine_c(100, 120);
  55. turnL(120, 1000);
  56. goLine_c(100, 120);
  57. turnL(120, 1000);
  58. // теперь робот в верхнем правом углу
  59. pip(); // для отладки
  60. // едем к нужной зоне
  61. for (int z = 0; z < x + 1; z++)
  62. goLine_c(100, 120);
  63. // выгрузка кубика
  64. turnL(120, 1000);
  65. goLine_c(100, 120);
  66. gripV1(100);
  67. turn180(70, 90, 1000);
  68. goLine_c(100, 120); // обратно к перекрёстку
  69. turnR(120, 1000); // поворот
  70. for (int z = 0; z < x + 1; z++)
  71. goLine_c(100, 120);
  72. turnR(120, 1000); // поворот
  73. goLine_c(100, 120);
  74. turnR(120, 1000);
  75. // и мы снова в правом нижнем углу
  76. for (int z = 0; z < x + 1; z++)
  77. goLine_c(100, 120);
  78. // и мы на том перекрёстке, который обработали
  79. // переходим к следующему шагу
  80. pip();
  81. x++; // следующий шаг
  82. }
  83. else
  84. {
  85. x++;
  86. }
  87. }
  88. goLine_c(100, 120);
  89. turnR(120, 1000);
  90. goLine_c(100, 120);
  91. pip();
  92. }

450 строк кода за 3 часа — это немало, а ведь ещё есть проект и теория! Поэтому тренировать придётся всё: сборку/разборку робота, написание кода и отладку программы на время.

15. Роботы могут быть любыми

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

Посмотри также несколько репортажей с заключительного этапа ВсОШ по робототехнике.

16. Задание № 2

Это практическое задание заключительного этапа Всероссийской олимпиады школьников по труду (технологии) по профилю «Робототехника» 2022–2023 учебного года для 11-х классов.

Построить и запрограммировать робота, который:

  • Начинает движение в зоне старта (целиком своей вертикальной проекцией находится внутри зоны, ограниченной квадратом жёлтого цвета).
  • Движется по линии с перекрёстками и перемещает кубики, расположенные на перекрёстках в верхней и нижней части полигона, на противолежащие перекрёстки внутрь синего квадрата 100×100 мм.
  • Останавливается в зоне финиша (жёлтый квадрат 300×300 мм).

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

  1. Просканируем и верхний, и нижний ряды с кубиками, составив «карту» (два массива) с ними.
  2. После этого будем их сортировать.

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

16-1.ino
  1. // пишем основной алгоритм для робота
  2. void runForest()
  3. {
  4. int spdL = 150, spdR = 170; // скорости
  5. pip();
  6. gripV1(100);
  7. // сканим верхний ряд
  8. bool o[5] = {0, 0, 0, 0, 0}; // массив логических элементов
  9. int n = 5; // количество перекрёстков
  10. int i = 0; // номер текущего перекрёстка
  11. while (i < n)
  12. {
  13. goLine_c(spdL, spdR); // едем до перекрёстка
  14. turnR(spdL, 1000); // поворачиваем
  15. delay(200); // замерли перед замером
  16. if (analogRead(IR) > 300)
  17. { // проверяем наличие кубика
  18. pip(); // пикаем
  19. o[i] = 1; // запоминаем, что на i-м месте есть кубик
  20. }
  21. turnR_back(spdL, 500); // возвращаемся на основную линию
  22. i++; // увеличиваем номер перекрёстка (следующий)
  23. }
  24. goLine_c(spdL, spdR); // едем в верхний правый угол
  25. turnR(spdL, 1000); // поворачиваем
  26. goLine_c(spdL, spdR); // едем в нижний правый угол
  27. turnR(120, 1000); // поворачиваем
  28. // сканим нижний ряд
  29. bool o2[5] = {0, 0, 0, 0, 0}; // массив логических элементов
  30. i = 0; // номер текущего перекрёстка
  31. while (i < n)
  32. {
  33. goLine_c(spdL, spdR); // едем до перекрёстка
  34. turnR(spdL, 1000); // поворачиваем
  35. delay(200); // замерли перед замером
  36. if (analogRead(IR) > 300)
  37. { // проверяем наличие кубика
  38. pip(); // пикаем
  39. o2[4 - i] = 1; // запоминаем, что на i-м месте есть кубик
  40. }
  41. turnR_back(120, 500); // возвращаемся на основную линию
  42. i++; // увеличиваем номер перекрёстка (следующий)
  43. }
  44. goLine_c(spdL, spdR); // едем в верхний правый угол
  45. turnR(spdL, 1000); // поворачиваем
  46. goLine_c(spdL, spdR); // едем в нижний правый угол
  47. turnR(spdL, 1000); // поворачиваем
  48. // мы на старте, знаем где кубики...
  49. }

Целых 80 секунд на сканирование — долго! Но мы можем изменить расположение датчика — поставить его сбоку и сканировать объекты, не поворачивая. Тогда впишемся в 40 с.

16-2.ino
  1. void runForest()
  2. {
  3. int spdL = 170, spdR = 185;
  4. pip();
  5. gripV1(100);
  6. bool o[5] = {0, 0, 0, 0, 0}; // для верхнего ряда
  7. bool o2[5] = {0, 0, 0, 0, 0}; // для нижнего ряда
  8. int n = 5, i; // перекрёстки... и текущий номер
  9. int x = 0; // номер ряда
  10. while (x < 2)
  11. {
  12. i = 0; // обнуляем счётчик перекрёстков в ряду
  13. while (i < n)
  14. {
  15. goLine_c(spdL, spdR); // едем до перекрёстка
  16. delay(200); // замерли перед замером
  17. if (analogRead(IR) > 200)
  18. { // проверяем наличие кубика
  19. pip();
  20. if (x == 0)
  21. o[i] = 1; // если верхний ряд, то...
  22. if (x == 1)
  23. o2[4 - i] = 1; // если нижний ряд, то...
  24. }
  25. i++; // увеличиваем номер перекрёстка (следующий)
  26. }
  27. goLine_c(spdL, spdR);
  28. turnR(spdL, 600);
  29. goLine_c(spdL, spdR);
  30. turnR(spdL, 600);
  31. x++;
  32. } // мы на старте и знаем где кубики...
  33. }

Когда проведём сканирование, закомментируем код и принудительно укажем массивы o и о2.

16-3.ino
  1. int spdL = 170, spdR = 185;
  2. pip();
  3. gripV1(100);
  4. // scan_cubes();
  5. bool o[5] = {0, 1, 0, 1, 0}; // для верхнего ряда
  6. bool o2[5] = {1, 0, 1, 0, 1}; // для нижнего ряда
  7. int n = 5, i; // перекрёстки... и текущий номер
  8. goLine_c(spdL, spdR); // к 1-й зоне верхнего ряда
  9.  
  10. if (o[0] == 1 && o2[0] == 0)
  11. {
  12. pip();
  13. // отправить кубик в нижний ряд и вернуться на перекрёсток
  14. }
  15. goLine_c(spdL, spdR); // ко 2-й зоне верхнего ряда
  16. if (o[1] == 1 && o2[1] == 0)
  17. {
  18. pip();
  19. // отправить кубик в нижний ряд и вернуться на перекрёсток
  20. }
  21. goLine_c(spdL, spdR); // к 3-й зоне верхнего ряда
  22. if (o[2] == 1 && o2[2] == 0)
  23. {
  24. pip();
  25. // отправить кубик в нижний ряд и вернуться на перекрёсток
  26. }
  27. goLine_c(spdL, spdR); // к 4-й зоне верхнего ряда
  28. if (o[3] == 1 && o2[3] == 0)
  29. {
  30. pip();
  31. // отправить кубик в нижний ряд и вернуться на перекрёсток
  32. }
  33. goLine_c(spdL, spdR); // к 5-й зоне верхнего ряда
  34. if (o[4] == 1 && o2[4] == 0)
  35. {
  36. pip();
  37. // отправить кубик в нижний ряд и вернуться на перекрёсток
  38. }

Это не программа, а так — болванка, шаблончик. Видишь закономерность, которую можно заменить циклом? Ещё нужно срочно написать процедуру для движения из любого верхнего перекрёстка в соответствующий нижний.

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

16-4.ino
  1. // разворот на 180 (крутится вправо)
  2. void turn180R(int sl, int sr, int t)
  3. {
  4. digitalWrite(DR, HIGH); // указываем направление вращения
  5. digitalWrite(DL, LOW); // указываем направление вращения
  6. analogWrite(SR, sr); // включаем мотор и даём съехать с линии
  7. analogWrite(SL, sl); // включаем мотор и даём съехать с линии
  8. delay(t);
  9. // а вот теперь ждём чёрную линию под датчиком
  10. dl = (1024 - analogRead(LL)) / delL;
  11. while (dl > Bl)
  12. {
  13. dl = (1024 - analogRead(LL)) / delL;
  14. delay(10);
  15. }
  16. analogWrite(SR, 0); // останавливаем мотор
  17. analogWrite(SL, 0);
  18. delay(200);
  19. // нужно чуть назад покрутить левое колесо
  20. digitalWrite(DR, LOW);
  21. digitalWrite(DL, HIGH); // указываем направление вращения
  22. analogWrite(SL, sl);
  23. analogWrite(SR, sr);
  24. delay(150);
  25. analogWrite(SR, 0);
  26. analogWrite(SL, 0);
  27. delay(250);
  28. }
  29.  
  30. void teleport_012(int spdL, int spdR, int x)
  31. {
  32. turnR(spdL, 1000); // поворачиваем
  33. goLine_c(spdL, spdR); // подъезжаем к кубику
  34. gripV1(0); // захватываем
  35. turn180(100, 120, 1000); // разворот
  36. goLine_c(spdL, spdR); // обратно к перекрёстку
  37. turnL(spdR, 600); // поворот
  38. // надо ехать к углу
  39. for (int z = 0; z < x + 1; z++)
  40. goLine_c(spdL, spdR);
  41. turnL(spdR, 600);
  42. goLine_c(spdL, spdR);
  43. turnL(spdR, 600);
  44. // едем к нужной зоне
  45. for (int z = 0; z < x + 1; z++)
  46. goLine_c(spdL, spdR);
  47. // выгрузка кубика
  48. turnL(spdR, 600);
  49. goLine_c(spdL, spdR);
  50. gripV1(100);
  51. turn180R(100, 120, 1000);
  52. goLine_c(spdL, spdR); // обратно к перекрёстку
  53. turnR(spdL, 600);
  54. for (int z = 0; z < x + 1; z++)
  55. goLine_c(spdL, spdR);
  56. turnR(spdL, 600);
  57. goLine_c(spdL, spdR);
  58. turnR(spdL, 600);
  59. for (int z = 0; z < x + 1; z++)
  60. goLine_c(spdL, spdR);
  61. // и мы на том перекрёстке, который обработали
  62. }
  63.  
  64. void runForest()
  65. {
  66. int spdL = 200, spdR = 220;
  67. pip();
  68. gripV1(100);
  69. bool o[5] = {1, 1, 1, 1, 0}; // для верхнего ряда
  70. bool o2[5] = {0, 0, 0, 0, 1}; // для нижнего ряда
  71. int n = 5, i; // перекрёстки... и текущий номер
  72. //=====================================
  73. goLine_c(spdL, spdR); // к 1-й зоне верхнего ряда
  74. if (o[0] == 1 && o2[0] == 0)
  75. teleport_012(spdL, spdR, 0);
  76. goLine_c(spdL, spdR); // ко 2-й зоне верхнего ряда
  77. if (o[1] == 1 && o2[1] == 0)
  78. teleport_012(spdL, spdR, 1);
  79. goLine_c(spdL, spdR); // к 3-й зоне верхнего ряда
  80. if (o[2] == 1 && o2[2] == 0)
  81. teleport_012(spdL, spdR, 2);
  82. }
  • Возможно, твой робот не уложится в 180 секунд, если достанутся слишком медленные моторы. В таком случае нужно отстаивать свои права и убеждать жюри дать роботу доделать задание.
  • Чтобы не путаться в процедурах с поворотами робота, удобно составить схему или тренироваться «прокручивать» выполнение кода прямо в своём воображении. Это трудное, но полезное умение.
  • Прогони процедуру с разными сочетаниями 0 и 1 и посмотри, как робот обрабатывает первые три кубика.

17. Задание № 2. Переделываем!

Предыдущий алгоритм получился очень долгий. Что ж, ещё раз внимательно прочитаем условие задачи.

Видишь, что сканировать можно только верхнюю область?

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

Что потеряем? Будет лишняя обработка одной пары (В4 и Н4 на рисунке). Условно говоря, это первый подход.

Второй подход: последовательно сканировать и сразу обрабатывать зоны C1–C5. Увидел в В1 кубик — перемести в Н1, не увидел — перемещай из Н1 в В1. Есть только один подвох: 1024 возможные комбинации!

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

Ещё можно как бы совместить два подхода: начать сканирование; если найден кубик — переместить его и продолжить сканирование с другой стороны. Чтобы переключить сторону, можно использовать флаг — логическую переменную со значениями (true|false).

Не верится, но весь алгоритм довольно компактный.

17-1.ino
  1. i = 4;
  2. side_up == true;
  3. while (i < 5)
  4. {
  5. goLine_c(spdL, spdR); // едем до ближайшего перекрёстка
  6. find = false; // сбрасываем флаг "Объект обнаружен"
  7. ir = analogRead(IR); // измеряем расстояние
  8. ir2 = analogRead(IR2);
  9. if (ir > 220 || ir2 > 220)
  10. find = true;
  11. if (find)
  12. { // если объект обнаружен, то...
  13. pip();
  14. if (side_up)
  15. {
  16. //=========================================
  17. // объект обнаружен и робот в верхнем ряду
  18. //=========================================
  19. }
  20. else
  21. {
  22. //=========================================
  23. // объект обнаружен и робот в нижнем ряду
  24. //=========================================
  25. }
  26. side_up = !side_up; // инвертируем сторону (на противоположную)
  27. }
  28. else
  29. {
  30. if (side_up)
  31. {
  32. //=========================================
  33. // объект НЕ обнаружен и робот в верхнем ряду
  34. //=========================================
  35. }
  36. else
  37. {
  38. //=========================================
  39. // объект НЕ обнаружен и робот в нижнем ряду
  40. //=========================================
  41. }
  42. }
  43. i++; // увеличиваем номер перекрёстка (следующий)
  44. }

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

Обрати внимание, что в задании для 11 класса были строчки:

  • Инфракрасный дальномер (10–80 см) Sharp GP2Y0A21 или аналог, 2 шт.
  • Пассивное крепление для дальномера, 2 шт.

Как думаешь, зачем дают два ИК-дальномера? Правильно, чтобы можно было сканировать с двух сторон.

Ещё несколько небольших уточнений:

  • Хорошо бы уже дописать ПД- или ПИД-регулятор.
  • Все временны́е задержки в коде надо бы сделать через переменные, чтобы было удобнее их массово изменять сразу во всей программе.
  • Написать небольшую процедуру, чтобы в самом начале робот ждал, когда окажется на белом поле. Это ускорит процесс тестирования и отладки.

Алгоритм сравнительно простой, но программа постепенно усложняется. Приступаем к написанию.

FIXME



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

  • Мы ввели процедуру ожидания перед тем, как робот начнёт выполнять задание на поле.
  • Внимательно изучи процедуру управления захватом. Постарайся понять принцип и запомнить формулу.
  • У нас езда по коротким прямым, поэтому в программе применили ПИД-регулятор с нулевым коэффициентом интегральной составляющей. То есть, получился ПД-регулятор. Подробнее читай в брошюре про ПИД-регуляторы.
  • Если бы линии были кривые или очень длинные, то стоило бы ввести интегральную составляющую для дополнительной коррекции курса.
  • Скорости моторов в программе не оптимизировались, но если время позволяет — это полезно сделать.
  • Как видно из объёма кода, необходим план, в какой последовательности его писать. Общая идея — поменьше набирать код текстом, побольше использовать функции «копировать/вставить».

Дополнительное задание

Улучши алгоритм так, чтобы робот не возил 4 и 5 кубики дальней дорогой. Оставь текущий алгоритм только для первых трёх кубиков.

И засеки общее время работы над задачей — это важно для самопроверки!

18. Задание № 2. Оптимизируем работу

Кубики 4 и 5 робот должен перевозить короткой дорогой. В прошлом алгоритме нужно уменьшить i до 3 и дописать шаблон.

FIXME



А дописать программу нужно самостоятельно. ;-)

И останется последний шажок — учесть в алгоритме ситуацию, когда в вертикальном ряду нет кубиков, чтобы робот не возил пустоту. Для этого нужно добавить проверку наличия кубика перед его захватом через отдельную логическую переменнную (т. е. флаг).

  • Идея создавать сначала шаблоны, а затем уточнять в них действия робота может сэкономить тебе время. Поначалу всё умещается в голове, но когда программа достигнет 450 строк, в ней будет тяжелее ориентироваться.
  • Ещё один плюс в таком подходе — возможность получить баллы за все рабочие участки кода. Напомним, что у нас две цели: научиться решать задачи и уметь набирать как можно больше баллов.

Контрольные задания

1. Поле остаётся прежним. Во всех 10 зонах расположены кубики двух цветов: чёрного и белого. Если кубик белый, то его не трогаем, а если чёрный — то везём в красную зону на схеме.

Подсказка: проверь сначала, можно ли отличить чёрный и белый объект с помощью инфракрасного дальномера. Может, он показывает разные значения, и этой разницы достаточно, чтобы различать цвета кубиков?

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

Структурные схемы Э1, Э3

Основным видом конструкторских документов в различных областях электротехники, радиоэлектроники, связи и др. являются схемы.

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

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

Правила выполнения и оформления схем регламентируют стандарты седьмой классификационной группы ЕСКД. ГОСТ 2.701-84 устанавливает единую классификацию схем для всех отраслей техники. По этой классификации все схемы (см. таблицу 1) разделены на виды (по характеру отображаемых физических процессов), типы (в зависимости от основного назначения) и имеют наименование и буквенно-цифровое обозначение (шифр, код).

Наименование схемы определяется её видом и типом, а шифр схемы состоит из буквенного и цифрового обозначения. Например, схема электрическая принципиальная имеет шифр Э3.

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

Обрати внимание, что на этой схеме нет дальномеров. Их надо добавить по аналогии с датчиками линии (светоотражения).

Рекомендации по оценке принципиальной схемы (для 10–11 классов)

Схему можно считать выполненной, если соблюдены следующие условия:

  • Схема соответствует устройству участника (все линии взаимосвязи указаны верно, очевидны подключения всех компонентов схемы, собранных участником).
  • Использованы верные условные графические обозначения (УГО) элементов.
  • Линии взаимосвязи и их повороты выполнены горизонтально, вертикально или под углом, кратным 45°. Пересечения линий взаимосвязи строго под углом 90°.
  • Узлы, соединяющие более трёх проводников, обозначены точкой.
  • Подписаны позиционные обозначения элементов на схеме.
  • Указаны контакты разъёмов функциональных блоков (контроллер Arduino, схема управления моторами и т. д.), к которым осуществлено подключение.

Один балл можно снизить, если:

  • Допущена одна ошибка в подключении линий взаимосвязи (всё остальное верно).
  • Схема нарисована небрежно: не соблюдены углы линий взаимосвязи, элементы не имеют позиционных обозначений, но все подключения верны.
  • Большинство использованных УГО не соответствуют ГОСТ, но все подписи верны и схема читаема.

Если нарушений больше, задание оценивается в ноль баллов.

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

Продолжение следует

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

Благодарность автору

«Амперка» выражает особую благодарность преподавателю Денису Геннадьевичу Копосову, чьё учебное пособие «Робототехника: подготовка к практическому туру регионального этапа Всероссийской олимпиады школьников» легло в основу данного практикума.

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

Ресурсы

Файлы

Навигация

Полезные ссылки