Роботаракан Петя за десять баксов

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


Разумеется, никакого анализа звука я не делал, просто запрограммировал Петю на танец в определённом ритме. Вот ещё один ролик, в котором Петя выказывает своё презрение к мячикам для жонглирования:


В его текущем виде Петя умеет только ходить, но при этом он может видеть (измерять расстояние до) близлежащие препятствия. Его мозги, однако, достаточно производительны для того, чтобы суметь переварить данные с множества других датчиков, присылайте ваши предложения!


Список покупок

Если у вас есть доступ к 3д принтеру, то распечатать непосредственно тело/ноги робота не будет стоить практически ничего. Вот список основных деталей, необходимых для клонирования Пети:


  • Материнская плата. Её вполне можно сделать дома лазерным утюгом, ну или заказать в Китае. Без каких-либо скидок это будет стоить примерно 10€ / 10 шт (включая пересылку), со скидками может спуститься до 2€ / 10 шт. Поскольку платка микромизерная, то я её добавил к другому заказу, и она мне оказалсь вообще бесплатной. Если изготовить плату на заказ — не вариант, то можно найти альтернативу типа cheapduino или чего-то подобного, поскольку схема крайне примитивная.
  • SG90 9G сервопривды, 3×1.47€ / шт
  • 4x AAA держалка для батареек, 1.34€ / шт
  • ATMega8A-AU (QFP-32), 1€ / шт
  • IR LED + IR фототранзистор, 0.20€ / шт
  • Электролитический конденсатор 1000uF 16V, 0.17€ / шт
  • 2n3904 транзисторы, 3×0.01€ / шт
  • А также вам понадобятся провода, термоусадка, винты, гребёнки и несколько 0805 резисторов и конденсаторов. Полный список компонентов для материнки приведён тут: hardware/motherboard/BOM.html.

Покупать конденсаторы штучно, конечно, никто не будет. Лично я конкретно под этот проект заказывал платы (бесплатно, поскольку в нагрузку к другому проекту), три сервопривода и держалку для батареек. Вся остальная мелочёвка у меня лежит в куче всякого другого хлама. Итого десять баксов — это ещё с запасом. Кстати, самой дорогой вещью в этом роботе могут оказаться батарейки ;)

NB: При заказе 9g сервоприводов не забудьте, что они бывают разных размеров. Петя предполагает использвание низкопрофильных серв с пластиковыми редукторами. Конечно, он будет работать и с другими сервами, но в этом случае не исключено, что придётся внести косметические изменения в SketchUp файлы. Ну и вообще металлический редуктор в данном случае не только оверкилл, но и зло в виде излишнего жужжания, трения, энергопотребления и выламывания деталей в случае форсмажора.

6fbe8dc8d4785b6cca757b26968cfad9.jpg


Тело

Тут сюрпризов нет, если у вас есть доступ к 3д принтеру, просто напечатайте содержимое каталога hardware/body/. Распечатанные детали выглядят как-то так:

9f6ae984dc2f1692d60f74c41905f53c.jpg

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

8bd27ee8f42fe6bf04c90c14037ddff8.jpg

М3 нейлоновые винты идеально подходят для сборки. Используйте нейлоновые шайбочки между движущимися частями, и затормозите гайку на резьбе любым удобным вам способом. Лично я просто нежно ткнул паяльником:

b2ac5b640784d5ffa09cc39714143aa9.jpg


Материнка


Мозги

Сама по себе материнка крайне примитивная. Исходники и гербер лежат в папке hardware/motherboard/. Вот рендер гербера:

3bef7a3e23e979919e3ddb8cb7cbd4ed.png

Материнка несёт на себе микроконтроллер ATMega8 и схему датчика препятствий, и ничего кроме этого. Вот так выглядит схема подключения мозга:

5c326f6a50b2aa7705ce86f09d1d2398.png

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

d1fe4a41a8f284a205b650b2e8fa2a6b.jpg

N.B. Обратите внимние, что даташит ATMega8A предписывает рабочее напряжение в диапазоне 2.7–5.5В, и абсолютный максимум напряжения 6В. Безопасный вариант питания Пети — это четыре NiMH 1.2V аккумулятора. Чисто из духа противоречния я перекрестился и засунул в батарейный отсек четыре стандартные щелочные батарейки (6.4В в сумме), и это не сожгло Пете мозги, он вполне нормально бегает. Если вы пойдёте этой дорогой, я вас предупреждал, вы это делаете на свой страх и риск!

Вот фотография полностью распаянной материнской платы (за исключением ИК светодиодов и фототранзисторов):

06ec003b24995e070b025ef623ba62f6.jpg


Датчик препятствий

У Пети два глаза, каждый из них состоит из инфракрасного светодиода и соответствующего фототранзистора. Светодиод излучает инфракрасный свет; этот свет распространяется через воздух и отражается от препятствий назад к фотоприёмнику. Если препятствие близко, отражённый свет будет сильнее, нежели если пепрятствие находится далеко. Обратите внимание, что хоть инфракрасный свет и не виден невооружённым глазом, некоторые камеры его регистрируют и могут показать на записи, что весьма удобно для отладки:

989a7e4d698831420a7ddc789ac9daa3.gif

Схема датчика препятствий крайне примитивна:

8b677b3fbb3ef35e821515eb95cefa1d.png

Мы запитываем два светодиода; когда фототранзисторы не освещены, коллекторы Q3 и Q4 «привязаны» к Vcc, а когда фототранзисторы улавливают достаточное количество ИК излучения, напряжение на коллекторах падает. На следующем примере я тестирую эту схему перед установкой светодиодов и фототранзисторов в глазницы Пети:

b6c914f9236d467385e643196ee22558.gif

Обратите внимание, что в зависимости от того, какие у вас светодиоды, вам может быть нужно подобрать значение резистора R6. 47 Ом дают 55 мА через светодиоды, но некоторые светодиоды могут хотеть больше (или меньше). Например, я выпаял безызвестные ИК светодиоды из поломанной детской игрушки, и они прекрасно работают на трёх миллиамперах (910 Ом)!

Я рекомендую сначала собрать схему приёмника на макетке (без светодиодов). Затем возьмите ИК светодиод, запитайте его напрямую от таблетки типа CR2032, и направьте в фототранзистор. (Я ничего не знаю про внутреннее сопротивление таблеток, но в моём [небогатом] опыте я никогда не видел изжаренных светодиодов от подобных манипуляций, поправьте меня, если я ошибаюсь)
Осветив ИК излучением фототранзисторы, убедитесь, что напряжение на коллекторах Q3 и Q4 падает согласно ожиданиям. Убедившись, что фотоприёмник работает корректно, подберите необходимый резистор для пары светодиодв, для того, чтобы получить поведение похожее на то, что я привёл в моём видео.

Обратите внимание, что лучше надеть на светодиоды и на фотоприёмники термоусадку для отсекания паразитных засветок. Ну и кроме того, с термоусадкой они прекрасно садятся в глазницы. При запаивании 2n3904, я бы посоветовал сначала запаивать центральный вывод, и только потом боковые, а то очень легко посадить трудноубираемую соплю припоя между ногами. Лично я нахожу пайку этих транзисторов более трудной, нежели пайку самого микропроцессора, но я плохой монтажник.

Да, если у вас нет осциллографа, это не беда, вполне можно обойтись парой светодиодов, обратите внимание на синие светодиоды на этом видео:

d8d1d53817abf995be5571f35b6d5131.gif

Для более далёких препятствий светодиоды будут тусклее.

Если у вас не получается заставить работать датчик препятствий, ну или если вам просто не понравилась предложенная схема, то существует множество других вариантов:


  • Можно использовать isf471 вместо фототранзисторов и всей их обвязки типа резисторов и 2n3904.
  • Можно купить измеритель расстояния Sharp GP2Y0A21YK0F:
    d999d39f236d2819a1e2895ff4650230.jpg
  • Ну или крайне примитивный готовый китайский сенсор на базе компаратора LM393:
    eb230b4be464e3ca1aeac627edb4ef78.jpg

В принципе, Петю можно программировать через среду ардуино, но я её нахожу слишком громоздкой и непонятной для такого простого микроконтроллера, как ATMega8. Давайте разобьём объяснение работы прошивки на четыре части:


  • как работает ШИМ-генератор
  • как Петя двигает конечностями
  • последовательности шагов
  • обход препятствий


Шим-сигнал

Хоббийные сервоприводы принимают на вход 50 Гц ШИМ сигнал; 1 мс минимальная длительность импульса (0 градусов положения вала), 2 мс максимальная длительность импульса (90 градусов положения вала). У Пети на борту три сервопривода, два из них заведены на 16 битный таймер (timer1), а третий на восьмибитный таймер (timer2). Если я не ошибаюсь, ардуиновская библиотека Servo.h управляет сервами в режиме софтверного ШИМ, что на мой взгляд расточительно, так что у меня оба таймера тикают в режиме fast PWM.

Сам микроконтроллер тикает на 8 МГц, при этом timer1 работает на частоте 1 МГц (делитель 8).
Регистр ICR1 задаёт значение TOP (20000), таким образом, таймер перезапускается каждые 20 мс, выдавая корректный 50 Гц сигнал. Регистры OCR1A и OCR1B контролируют длительность (в микросекундах) импульсов для левой и правой серв.

А вот с центральной сервой проблема. Она заведена на восьмибитный таймер timer2, а он, к сожалению, не имеет аналога столь удобного ICR1, то есть, частота переполнения счётчика контролируется только через делитель. В таблице делителей нет такого, который позволил бы приблизить 50 Гц с приемлемой точностью, так что вот дикая идея, которая лежит где-то посередине между софтверным и хардверным ШИМ генераторами:


  • Мы инициализируем таймер timer2 тикать с делителем 128, таким образом, он переполняется через 4.096 ms = 256×128/(8×10^6).
  • Сразу после переполнения мы выключаем timer2, то есть, это он нам выдаёт только один импульс.
  • В момент срабатывания capture interrupt таймера timer1 мы перезаряжаем таймер timer2 (и он сработает только на один импульс).

4 мс это больше 2 мс максимальной длительности импульса, которая нам нужна, и сильно меньше перезаряжающего ритма в 20 мс. Подводя итог, если мы хотим поставить все три сервы в среднее положение (1.5 мс длительность импульса), то мы должны сделать следующее:

OCR1A = 1500;    // left servo
OCR1B = 1500;    // right servo
OCR2  = 1500/16; // center servo


Планировщик движений

Перво-наперво, в коде есть шесть важных констант:

const uint8_t  zero[3] = {45, 50, 40};     // zero position of the servo (degrees)
const uint8_t range[3] = {25, 25, 20};     // the servos are allowed to move in the zero[i] +- range[i] interval

Массив zero[3] хранит значения углов для всех трёх сервоприводов, соответствующих нейтральной позиции (см. левую фотографию чуть ниже). В идеале, эти углы должны были бы быть 45° (середина диапазона серв), но на практике дискретность установки ног на зубчатые валы требует отклонения от идеала 45° для того, чтобы добиться симметричности нейтральной позиции. Затем, range[3] предписывает максимальный разрешённый диапазон движения сервоприводов. Это означает, что сервоприводу с индексом i разрешается двигаться только в диапазоне от zero[i]-range[i] до zero[i]+range[i].

d5b98c7ef73966fd3617f0b890187ddf.jpg

Текущее задание положения сервоприводов (в градусах, 0°-90°) хранится в массиве uint8_t pos[3]. Вызов функции update_servo_timers() обновляет значения регистров ШИМ-генератора согласно заданию. Правая фотография из картинки выше соответствует заданию pos[i]=zero[i]+range[i] для всех трёх индексов i=0,1,2.

В текущей реализации все движения планируются как движения с постоянной скоростью. Для этого у меня заведено четыре вспомогательных массива pos_beg[3], pos_end[3], time_start[3] и duration[3]. Давайте предположим, что я хочу двинуть только левой сервой. Для этого нужно выполнить следующие операции:


  • скопировать pos[0] в pos_beg[0], это положение, соответствующее началу движения;
  • установить pos_end[0] в желаемое положение (по-прежнему в градусах);
  • записать в time_start[0] текущую метку времени (миллисекунды, прошедшие с момента загрузки);
  • и, наконец, записать желаемое время движения в duration[0] (в секундах). Таким образом, скорость будет (pos_end[0]-pos_beg[0])/duration[0] градусов/сек.

Затем в бесконечном цикле я вызываю функцию movement_planner(), которая обновляет массив текущего задания положения сервоприводов pos[] согласно плану движения, а затем функцию update_servo_timers(), которая обновляет регистры ШИМ-генератора согласно заданию положения pos[].


Походки

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


  • шаг 1: {zero[0]-range[0], zero[1]-range[1], zero[2]+range[2]}
  • шаг 2: {zero[0]-range[0], zero[1]-range[1], zero[2]-range[2]}
  • шаг 3: {zero[0]+range[0], zero[1]+range[1], zero[2]-range[2]}
  • шаг 4: {zero[0]+range[0], zero[1]+range[1], zero[2]+range[2]}

Мы можем записать эту последовательность как 2д массив (четыре тройки целевых позиций):

const int8_t advance_sequence[4][3] = {{-1, -1,  1}, {-1, -1, -1}, { 1,  1, -1}, { 1,  1,  1}};

Этот массив говорит нам, что конечное положение сервопривода i на шаге step равно zero[i] + range[i]*advance_sequence[step][i].
Ну, а следующий код позволяет Пете идти вперёд неопределённое время:

    uint8_t step = steps_per_sequence-1; // at the initialization stage the (previous) movement is considered to be complete, thus the next movement will be planned starting from the step 0
    while (1) {
        if (is_movement_finished()) {
            step = (step + 1) % 4; // if previous movement is complete, then perform the next step; this variable loops as 0,1,2,3.
            plan_next_movement(step, advance_sequence); // execute next movement
        }
        movement_planner(); // update the servos position according to the planning
        _delay_ms(1);
    }


Обход препятствий

Давайте вспомним, что наш датчик препятствий выдаёт напряжения, заведённые на каналы 4 и 5 АЦП микропроцессора. Для того, чтобы отсечь высокочастотный шум в измерениях (особенно учитывая количество шума, создаваемого сервами), на каждой итерации главного цикла я обновляю переменные adc_left_eye и adc_right_eye по следующему закону, что даёт мне фильтр низких частот:

        adc_left_eye  = adc_left_eye *.99 + adc_read(5)*.01; // low-pass filter on the ADC readings
        adc_right_eye = adc_right_eye*.99 + adc_read(4)*.01;

Частота отсечки может настраивается или через задержку _delay_ms() внутри главного цикла, или же через коэффициент взвешенной суммы .99 и 1-.99 в вышеприведённом коде.

Наличие препятствия определяется как простое пороговое сравнение:

        uint8_t lobst = adc_left_eye  < distance_threshold; // obstacle on the left?
        uint8_t robst = adc_right_eye < distance_threshold; // obstacle on the right?

Затем в конце каждого шага (напоминаю, четыре шага на каждую последовательность) я проверяю наличие препятствий слева и справа и соответственно меняю последовательности:

        if (is_movement_finished()) {
            if (!lobst && !robst) {
                sequence = advance_sequence; // no obstacles => go forward
            } else if (lobst && robst) {
                sequence = retreat_sequence; // obstacles left and right => go backwards
            } else if (lobst && !robst) {
                sequence = turn_right_sequence; // obstacle on the left => turn right
            } else if (!lobst && robst) {
                sequence = turn_left_sequence; // obstacle on the right => turn left
            }
            step = (step + 1) % steps_per_sequence; // if previous movement is complete, then perform the next step
            plan_next_movement(step, sequence); // execute next movement
        }

Это просто, но работает!

Любой вклад приветствуется! Присылайте ваши идеи, а пока я приведу спиок вещей, которые мне хотелось бы увидеть улучшенными:


Софт:


  • Предложите мне элегантный способ получить более «живые», гладкие движения. На данный момент Петя передвигается при помощи линейной интерполяции между ключевыми позами, и было бы хорошо сделать так, чтобы походка стала менее дёрганной. Возможно, нелинейная интерполяция с заранее записанными ускорениями поможет?
  • Предложите новые стратегии обхода препятствий (и их реализации!) Текущаяя реализация крайне проста, в ней читаемость кода имеет высший приоритет, а ВАУ эффект от поведения робота уже идёт на втором плане.
  • Я думаю, что может быть недурно портировать код под среду ардуино для тех людей, кто не может или не хочет вызывать avr-gcc напрямую или ковыряться в регистрах конкретного камня. Если вы можете это сделать, присылайте пулл-реквест или просто форкните проект.


Железо:

Если вы добрая душа, желающая помочь с созданием версии V2 материнки, то не стесняйтесь это сделать! Вот список вещей, которые я хотел бы добавить/изменить/поправить в текущей материнке:


  • Главная вещь — это добавить рубильник, чтобы отключать питание сервприводов в момент прошивки процессора;
  • Убрать кварц, внутренней RC-цепочки вполне должно хватить;
  • Заменить все транзисторы на версии с поверхностным монтажом;
  • Заменить резистор R6 на подстроечник для более лёгкой настройки датчика препятствий;
  • Предложите хорошие (маленькие и с защитой от дураков) коннекторы вместо гребёнки и предложите лучшее их месторасположение;
  • Подключение ИК светодиодов под центральным сервоприводом — плохая идея. На данный момент единственно хороший вариант — это их намертво припаять, что не очень удобно;
  • Чуть-чуть подвинуть электролит. Мне пришлось его наклонить, т.к. иначе центральная левая нога иначе за него задевает;
  • Добавить тестовых площадок с лёгким к ним доступом осциллографом;
  • Добавить пару отладочных светодиодов для отладки без осциллографа;
  • Добавить площадок для пайки для всех неиспользуемых ног микропроцессора для отладки и дальнейшего расширения робота.

Петя — это страшное веселье!


000c57d11630c7c3c5bed78dd1e48683.jpg

© Habrahabr.ru