Игрушка-шар для маленьких на ATmega16 + акселерометр MMA7455 + led driver TLC5940

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

Постановка задачи

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


Что нужно?

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

На днях бегал на почту, прислали долгожданный пакетик с волшебным материалом Polymorph Plastic (он же Поликапролактан, Протопласт, ShapeLock, Polymorph, Полиморф, Полиморфус, Friendly Platic). Как использовать? Оч просто! Кидаешь гранулы в кипяток, мешаешь, достаешь прозрачную жвачку, лепишь и в морозилку. Через две минуты готовое пластиковое изделие белого цвета. Потрясающе. Попрубуем именно его и использовать для производства сферы, так как материал легко видоизменять, как пластелин (нагрел и снова лепи, что может быть проще?), а также можно сверлить и склеивать, как пластик.

2. RGB светодиодов в кол-ве N.

Со сферой и материалом для ее изготовления определился. Но с кол-вом светодиодов совсем ничего не ясно. Если шар катится, то хотелось бы подсвечивать его верх. А верх — это сколько лампочек? Лезу в игрушки сына в поисках зацепки. На глаза попадается резиновый мяч, расписанный на манер футбольного — пятиугольники и шестиугольники. Вот оно! Как бишь зовется такое тело? Интернеты подсказывают, что это усечённый икосаэдр с кол-вом вершин равным 60, т.е. потребуется аж 60 светодиодов, а ведь хочется RGB светодиод, т.е. 60x3 — 180 выводов (по выводу на компонент цвета да еще общий анод или катод). Слишком много. Нужно что-нибудь проще. Например, пусть каждая грань будет пятиугольником. Такое тело зовется додекаэдр, а вершин у него 20, т.е. потребуется 20x3 — 60 выводов, а это вполне разумное число. Получается, что катящийся шар всегда подсвечивает верхнюю грань додекаэдра, т.е. пятиугольник. С этим все стало ясно.

3. Какой-нибудь led driver, так как не хочется самому возиться с логикой включения/выключения и яркости.

Будем светить 20-тью RGB светодиодами, а это 60 выводов. А какие они бывают, эти led drivers? В этом сообществе нашел пару тем о драйверах, собрал небольшой список, начал изучать. Мои критерии оч просты:
1. а продается ли на eBay?
2. а есть ли доступный код на C?
3. популярный интерфейс, поддерживаемый AVR (SPI, I2C)
4. возможность соединения драйверов каскадом

Сразу понравился TLC5940: 16 leds, 10-bit PWM, SPI, доступен на ebay, есть приятная на первый взгляд библиотека для Arduino, хотя и требующая небольшого вмешательства.
Т.е. нужно 60 / 16 ~ 4 штуки. А закажу-ка я китайцам всего с запасом, на всякий случай: 5 драйверов и еще 25 RGB светодиодов с общим анодом (так требует даташит TLC5940, с общим катодом никак не подойдут). Заказал. В запасе 3 недели, пока компоненты не придут из Поднебесной.

4. Акселерометр MMA7455.

Собственно с него все и началось. Этот чип на готовой плате (с преобразователем напряжения 5v -> 3.3v) я заказал сразу и давно, так как такая штуковина меня сразу потрясла, а уж если не придумаю для чего использовать, то пусть лежит в ящике, душу греет. Кстати говоря, эту модель я долго не выбирал, просто ничего другого на eBay нет, и только потом оказалось, что ардуиновцы и его поддерживают, а код этой библиотеки оч прост и понятен. Т.е. все-таки есть шанс, что я разберусь со всем этим в реальные сроки, а идея заработает до того, как уже взрослый сын войдет в комнату и спросит басом: «Папа, харе фигней страдать, пойдем-ка лучше пива выпьем!».

5. MCU.

Так уж получилось, что я начал изучение электроники с AVR, обзавелся ATMega16 и проводил «Hello world» тесты именно на нем. Для идеи с шариком ресурсов данного контроллера вполне достаточно, так что остановлюсь именно на нем.

Принцип работы

Я себе это представлял так:
1. Шар в покое, где-нибудь в углу, не светится и вообще себя никак не проявлет.
2. Любой внешний толчок заставляет шар проснуться и подсветить верхнюю грань-пятиугольник.
3. В процессе движения шар всегда выбирает новую верхнюю грань и подсвечивает именно ее (цвета светодиодов и алгоритм индикации пока не имеет значения).

Так как акселерометр в режиме измерения всегда возвращает в своих регистрах текущий вектор ускорения (а в покое это будет g), то мы с легкостью можем перевернуть этот вектор и найти какой же пятиугольник додекаэдра он «протыкает». Т.е., все что нужно, это верно спозиционировать внутри сферы акселерометр (примем его координаты за 0,0,0) и смоделировать «вокруг» додекаэдр. Опрашивая с какой-нибудь небольшой частотой вектор ускорения и обходя последовательно 12 пятиугольников (именно 12-тью гранями обладает додекаэдр), чьи координаты «зашиты» в коде, мы всегда найдем текущий верх или низ. Все оч просто.

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

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

Лепим шарик


Все оказалось несколько сложнее, нежели я себе представлял: форму для будущего шара сделать не очень то просто, полиморф быстро стынет, а шар получается весьма угловат. Но, обо всем по порядку. Для лепки полусферы нужна форма в виде такой же полусферы, ведь я буду выкладывать полиморф изнутри, размазывая пальцем до застывания. Что может быть идеально сферичным и небольшим? Опять окунулся в корзинку с игрушками сына — ничего. Пошел в ближайший магазин для детей в поисках чего-нибудь круглого. В отделе канцтоваров попался небольшой глобус — 900 р. Дорого. Жалко тратить 900 р. лишь для формы. Ходил-бродил, пока не наткнулся на отдел «Все для лепки». А что, это может получиться! Сейчас индустрия лепко-творчества предлагает массу здоровских материалов, мягких и податливых, как пластелин, но застывающих, как глина при обжиге. Идея: взять маленький мячик и облепить половинку его такой вот чудо-пастой для лепки. На следующий же день мячик вынимается, а остается идеальная форма-полусфера.

Чудо-паста:
Чудо-паста

Будущая форма для полиморфа:
Форма

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

Разогреваю в ковшике воду до кипения, высыпаю горсть гранул полиморфа, мешаю, жду, когда масса станет прозрачной и гранулы слипнутся. Получившимся комом горячей жвачки выкладываю форму изнутри, размазывая до ровного слоя, мм в 5-7. Получается оч сносно. После втирания и заполнения всех полостей кладу форму в морозилку на 2 минуты. После окончательного застывания полиморф белеет, в итоге форма заполнена ровным белым слоем пластика — что и требовалось! Вынимаю. Ну, результат мог бы быть и получше: на готовой полусфере трещины, какие-то впадины и в общем-то полусфера местами не оч то и сферична. Ну да ладно, это все — мелочи. В любом случае не хочется тратить много времени и сил на производство идеальной пластиковой продукции, оставим это китайцам, а сами будем двигаться дальше. Делаю вторую половинку шарика, шкуркой нулевкой подгоняю края двух полусфер, минимизируя стыковой шов. На одной полусфере изнутри полоской полиморфа выкладываю паз, чтоб одна полусфера плотно соединялась со своей половинкой. Вдоль шва высверливаю три отверстия под винты 2M, ведь шарик должен плотно закрываться и не разваливаться, когда дорогой сынуля решит бросить это поделие об пол.

Рассыпуха в половинках шарика:


«Ветрянка» из разметки под пятиугольники и елочка из светодиодов:


Диоды в шаре:


Шар в сборе с синей разметкой под додекаэдр:


Hardware


Из-за маленького ребенка и его безграничного любопытсва ко всему я не имею возможности возиться дома с реагентами и ЛУТом, поэтому эта тема для меня сразу закрыта. Был порыв заказать плату у какого-нибудь дешевого производителя PCB (коих в общем тясячи), отправив Eagle-проект, но лишний месяц ждать не хочется. Решил паять сам, связывая все километрами проводков.

Интерфейс между ATmega16, MMA7455 и TLC5940

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

Акселерометр MMA7455 поддерживает как и I2C, так и SPI, а вот led driver только SPI, поэтому выбор протокола в общем-то очевиден:

ATmega16 < — I2C(TWI) --> MMA7455
ATmega16 < — SPI --> TLC5940

Программатор и ISP разьем

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

Разъемы для светодиодов

Каждый RGB светодиод должен быть размещен изнутри сферы (горячей полоской полиморфа обматывается светодиод, обычным феном подплавляется сфера изнутри, диод в эдакой обертке приклеивается на сферу), 4 провода подводятся к каждому диоду (3 катода для каждого компонента цвета и общий анод). Так как один драйвер позволяет подключить 16 обычных светодиодов, то, для упрощения адресации, я буду подключать 5 RGB светодиодов, а один пин остается лишним. Получается, что один драйвер и одна IDC вилка на 16 пинов (16 пин мы как раз возьмем для общего анода) питают пять RGB светодиодов.

Платы: основная и периферийная

Диаметр шара, в который нам нужно уместить все компоненты и батарейки, всего ~17 см, поэтому нужно разместить контроллер, акселерометр и ISP разъем на основной, материнской плате, а 4 светодиодных драйвера и 4 разъема для светодиодов — на отдельной периферийной. Эти две платы будут соединены 10-жильном плоским шлейфом (как раз хватает на 3 SPI вывода: SCK, MISO, MOSI; и 4 специальных, для работы со светодиодным драйвером, плюс питание и земля)

Я не буду подробно расписывать всю распиновку, так как все это есть в даташитах и все видно на моей Eagle-схеме. Единственное, что хотел бы отметить, что при подключении драйверов, нужно заглянуть в код ардуиновской библиотеки, а именно какие пины контроллера будут управлять светодиодными драйверами. Пины для ATmega16 в оригинальном коде вы не найдете, но по аналогии с другими контроллерами серии megaAVR можно запросто портировать на любой другой AVR девайс: я выбрал свободную ножку для VPRG сигнала, а остальные взял из ATmega_8.h, изменив лишь физическое соответствие ног для ATmega16, т.е., например, OC1A для ATmega8 — это PB1, а для ATmega16 — PD5. Все изменения добавил в новый заголовочный файл и разместил его у себя в проекте.

Хотел бы сразу оправдаться: Eagle я осваивал всего пару дней и, например, не знаю до сих пор, как сделать красивую рамку с подписью вокруг какого-нибудь участка схемы. Также не стал заморачиваться и искать IDC вилки, похожие на реальные, а нашел просто какую-то pinhead с нужным кол-вом отверстий (речь про периферийную leds схему).

Основная плата (крупнее):


Перифйрийная плата (крупнее):


Резульаты пайки

Конечно, соглашусь, что это тихий ужас, но в общем, я нигде не ошибся, и вся схема сразу завелась, пожара от КЗ 5 вольт не случилось, взрыва не было, а для моей скромной домашней задачи — это тот еще результат. J

Лицевая часть:


Обратная часть:


«Бутерброд»


Software


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

Leds drivers или грабли номер раз

Решил начать с теста бегущих огоньков на 5-ти светодиодах. Портировав ардуиновскую библиотеку для TLC5940 на свой ATmega16 и прошив, ожидал всего, чего угодно, но только не того, что оно сразу заработает. Спустя минуту стало ясно, что работает-то оно работает, но совсем не так, как написано в коде: «перетекание» цвета происходит с эдаким хроманием, а некоторые цвета просто пропадают. Т.е. складывается ощущение, что какой-то случайный мусор засылается в драйвера. Копался долго. Сначала в коде, потом сотый раз изучая схему на предмет каких-то ошибок или плохо пропаенных контактов. В конце концов подозрения пали на три резистора в 10 кОм, стоявших в линиях SCK, MISO, MOSI между MCU и первым драйвером в каскаде (когда-то читал в ликбезе DI HALTа, что оч полезно «резистором в 10к отделять линии программатора от основной схемы»). Так как осциллографа у меня нет, а из средств отладки лишь китайский мультиметр, то не могу сказать, как именно эти резисторы влияли на фронты сигнала, но без них все заработало, как надо. Возможно, кто-то здесь мне даст четкое объяснение такого «загадочного» поведения, но я решил не тратить время на эксперименты, а просто совсем убрать резисторы.

UART или грабли номер два

В очередной раз сбегал на почту и принес преобразователь USB-UART на CP2102. Захотелось нормального логгинга. За несколько часов написал асинхронный логгинг на lockless кольцевом буфере, взятом и переделанном из PortAudio, написал простецкое тестовое tty приложение, засылающее 256 signed символов в цикле и получающее echo-ответ от контроллера. Запускаю. Все работает. Но ровно наполовину. Т.е. все байты без MSB уходят и возвращаются, а вот знаковый бит теряется. Фантастика! Ну такого же не может быть. Ну или все, или ничего. Тысячу раз проверил протокол, скорость, parity, stop биты — все верно и на стороне контроллера и на стороне хоста. К вечеру хотелось плакать. Начал мучить гугл. Случайно попался чей-то комментарий, что мол частота контроллера от внутреннего резонатора может плавать. Вот оно. Лезу в инициализацию UART на контроллере и добавляю к UBRR регистру (Baud Rate Register) +6, от балды. Запускаю. Работает. Вот же ж… Т.е. получается, что частота внутреннего резонатора отличалась от заявленной, и UART загребал мусор вместо 8-ого бита. Итог: для нормальной работы UART необходим внешний генератор. Беру паяльник, впаиваю генератор на 16MHz. UART взлетел!

Leds драйвер на внешнем генераторе или грабли номер три

Радость после заработавшего UART была не долгой, так как перестал работать тест с бегущими огоньками. В этот раз цвет перетекал как надо, и загорались нужные диоды, но как только плата оказывалась на столе, начинали происходить чудеса: ровное течение цвета сменялось равномерным морганием. Ага! Знаю в чем дело. Пробую пошевелить провод, соединяющий пин OC2 контроллера с пином GSCLK драйвера (clock сигнал для драйверов, задающий PWM, идет от контроллера именно с пина OC2). Точно — моргание усиливается. Значит, где-то в этом месте плохой контакт, что-то не пропаялось. Потратив 10 минут, понимаю, что попал пальцем в небо, и проблема все еще не решена. Начинаю исследовать детальнее: хватаюсь за провод, стараюсь не шевелить, а просто ухватиться — светодиоды просто начинают гаснуть. Вспоминаю очередную статью DI HALT'а, о высокочастотных наводках. На моей чудовищной проводковой распайке провод от генератора 16 MHz идет аккурат над проводом в 8 MHz, задающим PWM для драйвера (максимальная частота для CTC (clear timer on compare match) режима, на котором работает OC2 пин, как раз половина частоты CPU, т.е. 8 MHz). Кажется, это те самые ВЧ наводки начали себя проявлять. Как же быстро можно исправить? Очень просто — пустить 16 MHz от генератора и на драйвер тоже, пусть молотилка тикает и для контроллера и для PWM. 10 минут с паяльником, 10 минут правок в библиотеке TLC — заработало! Вот же ж грабли так грабли!

Огонечки в действии:


Акселерометр или никаких больше граблей

Этот девайс заработал сразу и не подвел. Ардуиновский код оч простой и понятный, ну разве что требующий портирования с ардуиновского I2C API на свою собственную асинхронную TWI поделку. Вектор ускорения можно получать, опрашивая регистры с частотой не более 125 Hz для выбранного режима (так сказано в даташите), а можно ждать прерывание и именно тогда лезть за новой порцией x,y,z координат (конечно, тоже не чаще 125 Hz).

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

Т.е. алгоритм мог бы получится таким:
1. Инициализация всей периферии (UART, TLC5940, MMA7455) и CPU уходит в вечный цикл со спячкой.
2. Акселерометр что-то там измеряет, кладет в регистры, дергает контроллер за ногу.
3. Из внешнего прерывания контроллер асинхронно засылает в акселерометр I2C запрос на чтение текущего вектора ускорения.
4. Из I2C прерывания (операции чтения координат завершилась) делается расчет пересечения и поиска верхней грани, найденная грань раскладыается на вершины и маппируется в физические индексы светодиодов на драйвере, в драйвера через асинхронный SPI засылается поток битов яркости для всех светодиодов.

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

Акселерометр в действии, виртаульная линейка-уровень:


Соединяем все куски воедино

Что у нас есть? А в общем все, что нужно:
1. Отлаженный на хосте и визуализированный в opengl алгоритм нахождения любой грани додекаэдра.
2. Работающая библиотека, управляющая светодиодными драйверами.
3. Работающая библиотека, управляющая акселерометром.

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

while (1) {
    int x,y,z, error;
    // Достаем координаты ускорения
    error = MMA7455_xyz(&x, &y, &z);
    if (error != 0) {
        LOG("xyz err: %x\n", error);
        _delay_ms(100);
        continue;
    }

    // Инициализируем луч из точки 0,0,0 в x,y,z
    // NOTE: мы должны правильно смаппировать систему координат
    //       акселерометра в систему координат OpenGL
    float orig[] = {0.0f, 0.0f, 0.0f};
    float dir[] = {(float)y, -(float)z), (float)x};

    // Идем по всем вершинам граней додекаэдра, строя треугольник
    for (unsigned int i = 0;
         error == 0 && i < ARRAY_SIZE(s_vert_tri_faces);
         i += 3) {
        // Маппируем индекс 1-ой вершины в реальные координаты
        float vert0[] = {
            s_coords_vert[s_vert_tri_faces[i + 0] * 3 + 0],
            s_coords_vert[s_vert_tri_faces[i + 0] * 3 + 1],
            s_coords_vert[s_vert_tri_faces[i + 0] * 3 + 2]};
        // Маппируем индекс 2-ой вершины в реальные координаты
        float vert1[] = {
            s_coords_vert[s_vert_tri_faces[i + 1] * 3 + 0],
            s_coords_vert[s_vert_tri_faces[i + 1] * 3 + 1],
            s_coords_vert[s_vert_tri_faces[i + 1] * 3 + 2]};
        // Маппируем индекс 3-ей вершины в реальные координаты
        float vert2[] = {
            s_coords_vert[s_vert_tri_faces[i + 2] * 3 + 0],
            s_coords_vert[s_vert_tri_faces[i + 2] * 3 + 1],
            s_coords_vert[s_vert_tri_faces[i + 2] * 3 + 2]};

        // Рассчитываем пересечение
        float t, u, v;
        if (intersect_triangle(orig, dir,
                               vert0, vert1, vert2,
                               &t, &u, &v) && t < 0) {
            // Начальное смещение найденной грани
            uint8_t face_idx = i/9*9;

            // Тусклый красный, экономим батарейку
            uint32_t rgb = 0x800000;

            // Выключаем предущие лампочки
            tlc.clear();

            // Выставляем нужный цвет для 5 диодов в пятиугольнике
            TLC_SET_RGB(tlc,
                        &s_leds_vert[s_vert_tri_faces[face_idx + 0] * 3],
                        rgb);
            TLC_SET_RGB(tlc,
                        &s_leds_vert[s_vert_tri_faces[face_idx + 1] * 3],
                        rgb);
            TLC_SET_RGB(tlc,
                        &s_leds_vert[s_vert_tri_faces[face_idx + 2] * 3],
                        rgb);
            TLC_SET_RGB(tlc,
                        &s_leds_vert[s_vert_tri_faces[face_idx + 7] * 3],
                        rgb);
            TLC_SET_RGB(tlc,
                        &s_leds_vert[s_vert_tri_faces[face_idx + 8] * 3],
                        rgb);

            // Засылаем
            tlc.update();

            break;
        }
    }

    // 50 Hz
    _delay_ms(20);
}


Некоторые моменты требуют отдельных пояснений.

Во-первых, еще на этапе проектирования я решил не использовать floating point, а перевести сразу все на fixed point. Т.е. в оригинальном коде никаких float нет, а есть fp_t, здесь же float — для наглядности. Но об этом в разделе оптимизация.

Во-вторых, работа со статическими массивами s_coords_vert и s_vert_tri_faces не очевидна с первого взгляда. Поясняю. Все оч просто — додекаэдр состоит из вершин, а вершины имеют свои координаты в 3D, т.е. обходя массив s_vert_tri_faces и вытаскивая по три вершины, мы «раскладываем» каждую грань-пятиугольник додекаэдра на три треугольника. Например:

const static uint8_t s_vert_tri_faces[] =
{
	1, 2, 3,      1, 3, 4,     1, 4, 5,
        // ...
};


каждые 9 элементов (строка) — это грань из трех треугольников. Первый треугольник — вершины 1, 2, 3; второй треугольник — вершины 1, 3, 4; ну и третий соотвественно — 1, 4, 5. Если нарисовать пятиуогольник:



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

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

Эти массивы я создавал вручную, визуализируая додекаэдр в OpenGL и просто вращая его и переписывая индексы на бумажку. Мартышкин труд, но зато в лоб.

Оптимизация


Это оч важный пункт, который мне помог упихнуть весь этот код в 9 Кб, не сорвать стек и с наименьшим кол-вом тактов найти нужную грань и подсветить ее.

Fixed point VS Floating point

Если уж ATmega не умеет сама делить целочисленные переменные, то об операциях с плавающей точкой говорить вообще не приходится. Предвидя лишнюю трату ROM и тактов, я сразу решил отказаться от типа float и алгоритм нахождения пересечения луча с треугольником переписал на fixed point. Чтобы не быть голословным, уже после того, как все заработало, решил написал небольшой тест, результаты которого подтвердили мои догадки.

uint16_t now = s_msecs;

for (int i = 0; i < 1000; ++i) {
    fp_t t, u, v;
    intersect_triangle(orig, dir,
                       vert0, vert1, vert2,
                       &t, &u, &v);
}

LOG("msecs: %u \n", s_msecs - now);


Переменные orig, dir, ver0, vert1, vert2 проинициализированы реальными координатам, т.е. алгоритм на каждой итерации действительно находит пересечение. Что получилось:

floating point: 4534 ms, 9638 kb ROM
fixed point: 245 ms, 6318 kb ROM

В ~18 раз расчет с плавающей точкой медленнее и в 1.5 раза требует больше кода. В общем-то ничего удивительного, но хотелось каких-то реальных цифр.

Размер ROM

В какой-то момент мой код перестал влезать в отведенные 16 Kb ROM, и пришлось засесть за прочтение разнообразных статей про оптимизационные флаги gcc (например) и tips and tricks от Atmel. Вооружившись этими знаниями, оптимизацию кода я проводил в два этапа:

1. везде, где позволял логика, заменил int32 на int16 или int8. это высвободило ~5Kb.
2. при компиляции и линковке использовал следующие флаги для gcc:
компиляция: -ffunction-sections,-fno-inline-small-functions, дающие результат в ~1KB,
линковка: --relax,--gc-sections флаги, дающие результат в ~1Kb

-ffunction-sections + --gc--sections + --relax: функции помещаются в отдельные секции, позволяя использовать относительные переходы rjmp, rcall, экономя пару байт на инструкцию и 1 такт, неиспользуемые секции просто не линкуются.

-fno-inline-small-functions: флаг ограничивает компилятор в своевольном инлайнинге маленьких функций.

Эти флаги мне показались самим действенными — их и оставил.

Размер RAM

Кое-где я использую логгинг, строковые константы которого располагаются в RAM. Можно сэкономить пару сотен байт и переместить эти строки из RAM в ROM. В gcc это можно сделать, указав перед строковой константой макрос PSTR, взятый из заголовочного файла «avr/pgmspace.h», который раскатывается в аттрибут компилятору __attribute__((__progmem__)), т.е. сделать такую замену:

- do_some_logging("some msg");
+ do_some_logging(PSTR("some msg"));


Энергосбережение, 3 вольта и крепкий сон

Самое время подумать об оптимизации энергопотребления и попытаться что-нибудь сберечь. 3 батарейки АА (4.5v) — это и по весу много (все же помещается внутри шарика) и вольтаж большой, ведь можно же перейти на 3 вольта, т.е. использовать 2 батарейки ААА: и вес оптимизируем и напряжение. Вся периферия поддерживает 3 вольта, а вот ATmega16 — нет. Но есть же серия L, ну т.е. low power, которая может работать и от трех и от пяти вольт. Существенные для меня отличия лишь в том, что серия L не поддерживает внешний генератор > 8 MHz, а у меня 16. Я на это плюю, так как почему-то кажется, что все и так будет работать, а времени идти покупать генератор меньшей частоты у меня нет. Как оказалось, что все действительно заработало. В реальности, такое ощущение, что MCU работает на частоте меньшей, чем 16 MHz, так как _delay_ms проводит времени в цикле больше, чем должен, но вся периферия в норме, таймера тикают как надо, UART шлет/получает, а раз работает, то плюю еще раз — не хочу с этим разбираться.

Теперь нужно подумать и о сне. Когда шарик никто не трогает, он может засыпать, т.е. гасить все светодиоды и переводить CPU в спящий режим, а как только обнаруживается какая-то движуха, то просыпаться и снова включаться в работу. Как это сделать? Я это реализовал так:

Если координаты x,y,z вектора ускорения не изменяются за 30 секунд больше чем на 5 (по даташиту значение 64 — это g, 5 — это 0.07g, взял от балды), то мы
а) переводим акселерометр из режима measurement в режим level detection, т.е. задаем сколько-то g, и если ускорение превысит этот порог, то акселерометр дернет MCU за ножку.
б) отключаем таймера, UART, чтоб не разбудили раньше времени (актуально только для idle режима и его производных)
в) засыпаем

Я сразу нацелился на полное засыпание, т.е. режим power down, из которого можно выйти только по reset или по внешнему прерыванию, а это именно то, что нужно. Но почему-то контроллер не просыпался. Спустя какое-то время стало ясно, что из режима power down можно выйти _только_ по прерыванию low level, а rising/falling edge требует тикающего IO clock, который тоже спит. А вот тут засада. Нет в ATmega16 high level interrupt, а акселерометр генерирует именно его. Никакого другого решения, кроме как покупать внешнюю NOT логику, которая бы конвертировала high в low и наоборот, я не нашел. Пришлось ограничиться сном в пол глаза, т.е. использовать не power down, а idle режим.

В результате всех эти манипуляций ток падает с ~70 mA (все горит и светится) до ~23 mA (idle). Но тут всплывает еще одна проблема моей схемы: с самого начала я использую генератор, а не резонатор, который бы точно так же засыпал бы с контроллером, а вот генератор продолжает тикать себе, потребляя аж 20 mA. Естественно, я об этом раньше не задумывался, а сейчас понимаю, что много энергии мне сберечь не удасться, и даже в idle режиме шарик выжрет все батарейки за сутки. Это печально, так как раскручивать всю конструкцию и менять каждый раз элементы питания очень неудобно. Но эти улучшения оставляю на потом.

Что получилось


Лично я результатом доволен, хотя, уже сейчас понятно, что улучшать: необходим нормальный power down, с потреблением не больше 3 mA; алгоритм расцветки шарика безмерно уныл, необходимо реализовать что-нибудь действительно красивое с fading'ом и blending'ом; нужна какая-то простая фильтрация координат ускорения, так как от акслелерометра оч много мусора, и оч часто загораются смежные грани помеременно. Сын поделку особенно не оценил, пару раз швырнул, пнул ногой и с рыком побежал катать любимую машинку. Возможно, время шарика еще придет, и он завоюет сыновье внимание.

Собственно вот, что получилось:


Выход из спячки:


Ссылки и используемые материалы

Сам проект FedorBall

ATMEGA16
www.atmel.com/Images/doc2466.pdf
www.atmel.com/Images/doc8453.pdf
www.tty1.net/blog/2008-04-29-avr-gcc-optimisations_en.html

TLC5940
code.google.com/p/tlc5940arduino/
www.ti.com/lit/ds/symlink/tlc5940.pdf

MMA7455
arduino.cc/playground/Main/MMA7455
www.freescale.com/files/sensors/doc/app_note/AN3468.pdf
cache.freescale.com/files/sensors/doc/data_sheet/MMA7455L.pdf?fpsp=1

TWI(I2C)
easyelectronics.ru/avr-uchebnyj-kurs-ispolzovanie-avr-twi-dlya-raboty-s-shinoj-iic-i2c.html

DODECAHEDRON INFO
ru.wikipedia.org/wiki/Додекаэдр
www.maths.surrey.ac.uk/hosted-sites/R.Knott/Fibonacci/phi3DGeom.html

RAY-TRIANGLE INTERSECTION
jgt.akpeters.com/papers/MollerTrumbore97/

FIXED POINT
gcc.gnu.org/wiki/FixedPointArithmetic
x86asm.net/articles/fixed-point-arithmetic-and-tricks/

OPENGL
www.songho.ca/opengl/gl_vertexarray.html


  • +18
  • 26 августа 2012, 22:26
  • rouming
  • 2
Файлы в топике: main_board.png, leds_board.png

Комментарии (29)

RSS свернуть / развернуть
круто, но проводки жесть…
у меня тоже был приступ желания сделать для котиков игрушку чтоб — не огоньки, а моторчик и она немного катилась сама при ударе лапой, но что то лень да и щас тоже ребенок в общем не до котиков :)

ps в банке небольшие платы вполне экологично травятся, и кстати с seedstudio платы быстро относительно все приходят
0
Психоделическая световая граната :))
0
Инвертор для прерывания low-level сделать на одном маленьком полевичке можно… Или даже биполярнике.
+3
мне кажется скоро придет ДИ и попросит убрать в личный блог. хотя статья имхо заслуживает похвал
0
  • avatar
  • xar
  • 27 августа 2012, 00:12
мож бложек «поделки» замутить? или «готовые решения» если поофициальнее
+3
Да ну, отличная статья. Мы же easyelectronics всё-таки, не одни же коллайдеры тут изобретать.
+2
ДА вот даже не знаю. С одной стороны да, С другой тут про вычисления, оптимизацию. Так что нехай будет тут.
0
А какие вообще существуют гласные/внегласные правила по пригодности статьи в тот или иной блог?
0
Да как бы формально никаких. Просто я стараюсь раскидывать их так, чтобы тематика конкретного блога размывалась как можно меньше.

Например, если поделка сделана на AVR, но о AVR там только упоминание о нем и кусок кода который с небольшими изменениями можно пихнуть на любой МК, то к AVR он отношения имеет самое посредственное. А потому переносится туда, где ему больше подходит. Либо в персональный блог, если по тематике особо не приткнуть.
0
Лучшая оптимизация — алгоритмическая. Эффективнее было бы подобрать более оптимальный алгоритм поиска грани.
ИМХО, проще всего забить массив из векторов направлений на центры граней (итого 12 векторов, если я правильно помню, что такое додекаэдр) и просто в цикле искать самый близкий по направлению к вектору с акселерометра. В качестве критерия близости проще всего взять векторное произведение — оно считается из координат 3D векторов за три умножения и два сложения и равно |a|*|b|*cos(ab), т.е. пропорционально косинусу угла между векторами, так что достаточно из 12 векторов выбрать тот, скалярное произведение которого на вектор с акселерометра будет наибольшим. Все эти расчеты можно проводить в целочисленном формате — только выбрать длину векторов направлений такой, чтобы их скалярное произведение на -g с акселерометра не выходило за пределы int16/int32. Еще стоит учесть, что при бросках аксель может зашкалить и выдать вектор в духе (+8g, +8g, +8g) (ну или сколько у него максимум), желательно чтобы при умножении на такой вектор результат умножения тоже не выходил за пределы.
0
  • avatar
  • Vga
  • 27 августа 2012, 03:44
нормировать вектор? так и говори :)
0
Зачем? Пустая трата времени.
0
да, это отличная идея. но я так зациклился на _не_ использовании тригонометрических функций из-за их тяжеловестности, что решил просто искать пересечение с гранью. хотя, сейчас понятно, что вместо вызова cos в лоб, можно было использовать статическую таблицу.
0
Тригонометрия не нужна. Для расчета «какой вектор ближе» косинус угла даже лучше, чем сам угол, так что считать арккосинус не требуется.
В качестве критерия близости проще всего взять векторное произведение
Ой, меня таки переклинило. В своем сообщении я говорю исключительно о скалярном произведении векторов. Оно считается как v1.x*v2.x + v1.y*v2.y + v1.z*v2.z. Для минимизации вычислений считать его можно в целочисленном виде, выбрав длину векторов нормалей к граням так, чтобы при умножении на самый длинный вектор, какой может выдать акселерометр результат не вышел за пределы разрядности.
Тогда код вместе с табличкой векторов уложится в полкилобайта, я думаю.
0
Интересный производитель PCB, почитаю условия. А еще есть ссылки (раз их тысячи)?
И что за полиморф? Поподробнее и можно ткнуть где почитать.
0
> А еще есть ссылки (раз их тысячи)?
Да вот, пожалуйста:
www.ladyada.net/library/pcb/manufacturers.html
www.opencircuits.com/PCB_Manufacturers
www.avrfreaks.net/index.php?name=PNphpBB2&file=viewtopic&t=45553
www.avrfreaks.net/index.php?name=PNphpBB2&file=viewtopic&t=107612

> И что за полиморф?
Думаю, видео нагляднее, чем любое описание: www.youtube.com/watch?v=IhVuc6RNyaw
в гугле можно поискать такое «пластик для прототипирования».
0
А что за хреновина на фотке с подписью «чудо-паста»?
0
а так зовется «паста для лепки». производителей много. конкретно этот — Jovi.
0
Что она из себя представляет?
0
эдакий пластелин, который за сутки при комнатной температуре твердеет, как глина при обжиге.
если хочется получить результат быстрее — есть полимерная глина (термопластик), ее можно «запечь» в духовке при 120.
0
Сколько пластика потребовалось на такой шар?
Какой его примерный вес?
0
на каждую половинку ушло примерно 60 граммов. ну т.е. в ковш сыпал ровно 80, минус излишки, которые пришлось удалить после раскатывания по форме.
0
Отличная работа, спасибо что поделились статьей.
Могу посоветовать еще приглядеться на ровне с ebey.com к aliexpress.com
Смысл тот же, только вроде как всегда дают трек-номер и принимают оплату напрямую с QIWI
0
про полиморф взял на заметку. даже для цнцшки голову на такую температуру собрать труда не составит думаю.
0
  • avatar
  • xar
  • 27 августа 2012, 12:22
Напомнило момент из фильма:
youtu.be/zU3Hs36FIrw?t=1m59s
0
Отличная работа. Умение соображать и делать выводы всегда решает.

Вот интересно, как этот полиморфус переживет жаркий летний день на подоконнике? Не получится ли из аккуратного изделия чего-нибудь сплюснутого?
0
  • avatar
  • Ozze
  • 27 августа 2012, 14:46
Спасибо, печалит лишь, что сынец проигнорировал. У взрослых и детей разные представления о здоровкости игрушек.

При солнце нет. Только при 60 градусах поверхность начинает лишь «слезиться» (становится чуть липкой и блестит), а для полной мягкости и «полиморфности» нужно минуту поварить при 100. Хотя, конечно, если у вас тончайший блин толщиной в 0.1 мм, то его можно оч быстро разогреть. Т.е. для домашних всяких поделок — чудо, а не материал.

Что потрясает, что в остывшем состоянии ничем не отличается от обычного пластика. Вообще ничем. Прочный, не ломкий.
0
МОИ ГЛАЗА!!!11
я не удивляюсь, почему шар не понравился
у меня через 10 секунд слезы потекли от этого мерцания

я-то думал, что при движении шарик будет плавно менять цвета по всей поверхности в зависимости от ориентации акселерометра
типа 3 цвета и 3 стороны пространства
не зря же ты прилепил драйвер, который может шим

а у тебя получилась откровенная вырвиглазная херня
мега 16 имеет 3 железных шима, сделай на них 3 цвета на весь шарик
светодиоды надо распологать ближе к центру, а в шарик насыпать светорассеиватель типа битого стекла или специальные гранулы из пластика или еще какого гавна
0
да-да-да. я прошу прощения, что сразу не указал, что ролик может вызвать эпилептические припадки, катаракту и расслоение сознания. впредь буду подробно указывать все возможные последствия от просмотра.
+2
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.