![]() |
![]() |
||||||||||||
Автоматический велосипедный фонарь
Автор: eliterr Здравствуйте, уважаемые котоводы!
Любите ли вы езду на велосипеде? Я - да, но я также иногда вижу вдоль дорог сбитых животных, жертв дорожного движения. Чтобы не стать одной из них, мне очень хочется, чтобы меня было как можно луше видно на дороге. А когда я еду на машине, мне бы было гораздо спокойнее, если бы все велосипедисты, особенно в сумерках, ездили с фонарями, а то, бывает, выскочит такой из темноты, а ты пугайся. У меня есть задний фонарь, который достаточно заметно светит красным светом, и с которым я чувствую себя достаточно хорошо обозначенным на дороге. Фонарь ровно такой, как на картинке внизу:
Фонарь-то есть, да вот незадача - случается, забуду включить перед тем, как ехать - ведь включать его под седлом не слишком-то удобно. А иногда, приехав днём, когда светло, забуду выключить (фонарь, опять же, под седлом, и его не слишком-то заметно, когда отходишь от велосипеда), и он светит весь день, разряжая батарейки. В любом случае мне беспокойство - того и гляди, выключится на полпути, и стану я человеком-невидимкой на дороге. Хотелось бы чтобы было так: берёшь велосипед с места - фонарь включился, приехал на место и поставил велосипед - фонарь сам выключился. Итак, задача: доработать фонарь, встроив в него датчик движения. Анализ задачи При переделке фонаря хотелось бы максимально использовать имеющиеся детали - держатель батареек, кнопку, влагозащиту, светодиоды и всё остальное. Всё-таки неглупые люди, наверное, фонарь проектировали, много чего предусмотрели, и светодиоды размещены именно там, где нужно, чтобы светить далеко. Да и режимы свечения и моргания хотелось бы тоже оставить. Вдруг их тоже как-то специально выбирали (UPD: а вот это маловероятно). Оценим донора: Да, места довольно мало - только центральная выемка под платой (помеченная цифрой "2") и выемка, симметричная кнопке. Высота просвета под платой - 4.6 мм, и это не считая торчащих из платы вниз концов выводных элементов. Целевой параметр раз - минимизация места. Я велосипед могу неделями в лапы не брать, а то, может, и месяцами. Позволять моей поделке разряжать батарейки не хочется. Целевой параметр два - минимизация энергопотребления. (UPD: за рекордами энергопотребления в рабочем режиме можно было и не гнаться - при замере оказалось, что в режиме “включено всё” сами светодиоды расходуют ~50mA). Когда я вижу “низкое энергопотребление”, я думаю про семейство msp430 от Texas Instruments, на котором я уже сделал несколько проектов. Для подобной задачи наверняка подойдёт минимальный представитель - msp430g2001, который у меня есть в нескольких экземплярах как раз в мелком TSSOP. Довольно минималистичный контроллер, но всё что нужно есть - ввод-вывод, таймер, питание от 1.8V. Памяти, правда, не густо - 512 байтов flash, но чувствую, что если поднапрячься - втиснуться можно. Целевой параметр три - минимизация программы. Итак, приступим. Первое - анализ предмета. Изучение предмета Как можно видеть на фото раскрытого фонаря, дорожки на плате довольно хорошо прослеживаются под маской, и их даже почти не приходится прозванивать. Очень быстро вырисовывается следующая схема:
Неведомый контроллер под синей кляксой реализует всю функциональность. Единственный вход - кнопка, замыкающаяся на ноль. Два выхода, управляющие через транзисторные ключи блоками светодиодов. Светодиоды подключены к коллекторам PNP транзисторов. У фонаря есть четыре режима работы:
Первые три режима понятны, а вот четвертый требует пояснений. Во-первых, надо заметить, что шаблон моргания повторяется с периодом примерно 0.4с. Если разбить этот период на 8 “блоков” по 0.05с, то у каждого светодиода в каждом блоке может быть две смены состояния - в начале блока и через мгновение (скорее всего, один такт контроллера). Шаблон вспышек случаен, но определяется периодом работы контроллера (т.е. постоянен до вынимания батарей). Я взял за модель одну последовательность. Режимы с первого по третий, кстати, тоже укладываются в эту схему, но там на переключения внутри блока нету, и состояние светодиодов внути блока постоянно - 0 или 1.
Режим моргания: Режим вспышек:
Датчик движения Исходя из требования минимизации энергопотребления, датчик движения я спроектировал механическим. Датчик состоит из стакана, у которого есть дно и две стенки. Дно подключено к земле, стенки стакана - притянуты к питанию резисторами подтяжки контроллера. Стакан, размещённый поперёк оси велосипеда, при наклоне велосипеда также наклоняется влево-вправо. При наклоне стакана в нём катается шарик, который, подкатываясь к краю, замыкает пару дно-стенка. Через резисторы подтяжки течет ток, хотя и небольшой, поэтому при срабатывании датчика будем отключать подтяжку от сработавшей стороны датчика. UPD: в процессе написания статьи пришла в голову более простая схема - одна контактная пара в стакане, которая замыкается шариком при прокатывании через неё.
Развертка стакана состоит из шести сторон, которые склеиваются вместе коптановой лентой, после чего лепестки сторон верх-низ-перед-зад припаиваются друг к другу. Затем коптановой лентой приклеивается одна боковина, кладётся шарик, приклеивается вторая боковина, и всё обматывается ещё одном слоем. Выдающиеся лепестки задней и боковых сторон служат выводами датчика. Эту конструкцию удалось сделать довольно компактной - 4мм в толщину, и она отлично входит в полость, симметричную кнопке. Фото на фоне куска стандартной макетной платы. На левом снимке хорошо виден лепесток стороны back, к которому припаян синий провод. Шарика на фотографии не попал - уже кпакован вовнутрь датчика.
Контроллер Управляющая схема тривиальна - вводы JP4 (motion sensor и кнопка), LED-выводы JP2 и JP5, блокировочный конденсатор C2, разъём программирования JP1 и схема сброса C1-R2. Коденсатор C3 и внутренний резистор подтяжки входа P1.3 образуют RC-фильтр подавления дребезга кнопки S1. Плату контроллера, как и датчик движения, удалось сделать тонкой и спрятать между батарейками в полости под оригинальной платой. На фотографии приведена плата первой итерации. Для программирования используются контактные площадки, на которые встаёт пружинный щуп, который гибким шлейфом подключён к программатору. Идея подсмотрена на hackaday. Поскольку разводка контактов программирования в разных проектах может быть разной (да и щуп можно использовать не только для программирования), площадки подписаны, и “первый” контакт щупа тоже. Это обозначение прямо на плате - просто бесценно, если бы его не было, то куча нервов ушла бы на поиск правильной ориентации щупа при прошивке. Попробуйте-ка не глядя вставить USB-разъём.
Конечный результат - вторая итерация платы. После вытравливания и припаивания деталей плата обрезана под ширину платы-носителя и припаяна к ней проводами. Здесь уже температурный режим ЛУТа правлильный, и белая маска легла гораздо лучше: ЛУТ по-современному Подобная миниатюрность не удалась бы без современных технологий - всем известной жёлтой коптановой ленты и не столь распространенного однослойного текстолита от PulsarProFX. Основное предназначение последнего - гибкие шлейфы и тому подобные вещи, но меня привлекла его малая толщина и возможность в будущем делать двух- и, если понадобится, более многослойные платы без сложностей с совмещением. А судя по моей активности, двух листов формата Letter мне хватит минимум на пару лет. Кроме текстолита я использовал ещё три продукта от PulsarProFX, специально сделанных для ЛУТа, или, по-английски, toner transfer. Первый из них - Toner Transfer Paper. Это плотная альбомная бумага со слоем декстрина. При намачивании декстрин размокает и растворяется, а подложка сама собой отплывает от тонера, приклеившегося к плате. После глянцевых журналов и прочего шаманства это просто прелесть - после калибровки утюга перенос идеальный, а при снятии подложки в принципе нет возможности оторвать тонер от платы при снятии.
Два других продукта - Green и White TRF (Toner Reactive Film), плёнки с цветным слоем. Их цель - перенести на плату тонер обычным термоспособом, а потом таким же способом термопереноса приклеить к тонеру напылённый слой, плёнкой кверху. Получается такой бутерброд: текстолит-фольга-тонер-цветнойслой-плёнка. После остывания тонера пленка-носитель отрывается, а слой так и остаётся приклеенным к тонеру. Green TRF - не реагирует с травильным раствором, его назначение - закрытие дефектов термопереноса и “дырявого” тонера. А White TRF - декторативный слой, который можно использовать как паяльную маску или для шелкографии белым цветом поверх тестолита (текстолит-тонер-белыйслой) или черным поверх белого фона (текстолит-тонер-белыйфон-тонер). Мне второй способ понравился больше. Пример использования Green TRF. Картридж в принтере у меня на исходе, и слой тонера выдаёт неравномерный. Где обычный, где совсем тонкий, а на маленьких участках тонера может и вовсе не быть. После приклеивания к тонеру зелёного слоя при отдираниии носителя он, за счёт некоторой своей жёсткости, эти места закрывает. Которед, к сожалению, все картинки пережимает, поэтому придётся включить воображение. Серия снимков "до" - "после приклеивания" - "после отрывания": На фотографии в самом низу - плёнки WhiteTRF и GreenTRF, они поставляются намотанными в несколько слоёв на картонку половинного размера (A5?). Поверх них - лист однослойного текстолита. Третий слой - Toner Transfer Paper. Четвертый - прозрачная плёнка, на которой я калибровал утюг. Поверх всего - измерение толщины текстолита, 0.14 мм. Так вышло, что плату я делал дважды, и первый раз получился так себе - перед приклеиванием WhiteTRF я не отмыл как следует остатки декстрина. Для простого тонера это не важно - тонер после травления смывается, сдирается, или остаётся как есть (для шелкографии тонером по текстолиту), а вот TRF-состав к декстрину прилипает из рук вон плохо, и в сплошном слое получились дырки. А может, так получилось из-за неверного температурного режима переноса. Для второй итерации я калибровал утюг. Не знаю, почему о подборе температуры там редко упоминают, это сэкономило бы мне много времени несколько лет назад, когда я только делал первые шаги в изготовлении плат. Вполне возможно, это просто какая-то тайная магия, о которой мало кто знает, так что я описал экстремально простую процедуру в предыдущей статье. Программная часть Функциональность фонаря:
В первой итерации фонаря я реализовал подавление дребезга программно. После перехода на аппаратное подавление удалось реализовать последний пункт, который оказался довольно удобным. С реализацией всего этого в программе пришлось немного повозиться. Программирую я на C++. Пусть меня поборники тёплого лампового С попробуют закидать тапками, но namespace, template вместо #define, const, auto и более строгое приведение типов - очень удобные вещи. Классы и наследование для этого проекта на этом контроллере я, понятно, не использовал, так что в этом проекте никаких накладных расходов и неявного кода от использования C++ нет. Реализация программы - state machine с тремя состояниями:
Разработка на более богатом ресурсами контроллере g2553 прошла прекрасно, но итоговая программа получилась байтов на 200+ больше, так что пришлось заняться оптимизацией. Приёмы оптимизации ####################################################################################### CFLAGS += -mmcu=$(MCU) -g -Os -Wall -Wunused $(INCLUDES) -ggdb -fwhole-program -I . CXXFLAGS += $(CFLAGS) -std=c++0x -fno-rtti -fno-exceptions ASFLAGS += -mmcu=$(MCU) -x assembler-with-cpp -Wa,-gstabs LDFLAGS += -mmcu=$(MCU) -flto -Wl,-Map=$(TARGET).map -ggdb -minrt -nostdlib ######################################################################################## Ключ -fwhole-program в CFLAGS (у меня - и в CXXFLAGS). Позволяет компилятору иметь в виду, что никто кроме моего кода ничего не будет использовать. Компилятор может инлайнить код и оптимизировать его по максимуму. enum State { STATE_NORMAL = 0, STATE_WAIT_FOR_SETTLE = 2, STATE_SLEEP = 4, }; volatile bool s_button_pressed = false; volatile bool s_motion_detected = false; uint16_t cycles_count = 0; uint16_t light1_tmp; uint16_t light2_tmp; State state = STATE_NORMAL; const uint8_t MODE_SLEEP = 0; uint8_t mode = MODE_SLEEP;
extern "C" __attribute__((naked)) ISR(RESET, reset_interrupt) { _set_SP_register(stack); // ... main_program(); }
ISR(RESET, reset_interrupt) { // ... // clear bss memory, 16bit word at a time for (uint16_t i = 0; i <= bsssize; i += 2) { bssstart_w[i] = 0; } // ... } Стандартный memset, как и полагается библиотечной функции, должен работать везде, и записывает ровно столько байт, сколько затребовано. Мне же memset нужен только для инициализации bss в RAM перед началом выполнения программы. Я спокойно могу обнулить больше RAM, чем надо, и, зануляя по 2 байта за раз, могу сэкономить несколько байтов на коде memset. for (uint16_t i = 0; i <= bsssize; i += 2) { bssstart_w[i] = 0; } Не использовать знаковые 8-битные типы, или использовать только там, где есть необходимость именно в знаковом типе (у меня таковой нет, поэтому переменная цикла в процедуре инициализации беззнаковая). У процессора есть 8-битная арифметика, но 8-битные данные хранятся в 16-битных регистрах, и компилятор после операции над числом всегда вставляет команду sxt (sign extension). Для беззнакового 8-битного, понятно, не вставляет, т.е. можно сэкономить два байта на каждой записи нового значения. // count up max // ... cycles_count = 0; // ... cycles_count = 0; // ... cycles_count++; // ... if (cycles_count == MAX_CYCLES) { cycles_count = 0; // ... // count down cycles_to_sleep = MAX_CYCLES; // ... cycles_to_sleep = MAX_CYCLES; // ... cycles_to_sleep--; // ... if (cycles_to_sleep == 0) { cycles_to_sleep = MAX_CYCLES; // ... Как правило, загрузка нуля в регистр - операция более простая, чем загрузка чего-либо ещё. У меня счётчик циклов до автоотключения сбрасывается несколько раз, и из двух вариантов второй немного компактнее. /** * Equivalent of: * uint8_t bit0 = x0 & 0x01; * uint8_t bit1 = x1 & 0x01; * x0 >>= 1; * x1 >>= 1; * return (bit1 << 1) | bit0 */ inline uint8_t shiftRightAndReturnCarry(uint16_t &x0, uint16_t &x1) { uint16_t tmp0 = x0; uint16_t tmp1 = x1; uint8_t carry; __asm__ __volatile__( "sub.b %2, %2 n" "rra.w %1 n" "rlc.b %2 n" "rra.w %0 n" "rlc.b %2 n" : "+r" ((uint16_t) tmp0) , "+r" ((uint16_t) tmp1) , "=r" ((uint8_t) carry)); x0 = tmp0; x1 = tmp1; return carry; } Ассемблерные вставки. Очень не хотелось, но выхода не было. Во-первых, в системе команд MSP430 нет логических сдвигов, только арифметические. Мне это без разницы (у меня всего 15 сдвигов), но компилятору об этом неизвестно, и он упорно вставляет библиотечную функцию для логических сдвигов. А во-вторых, та конструкция, которую я использовал, на C выражается вообще не очень хорошо: 6 операций на C, и 5 ассемберных команд. #define MKPATTERN(c0,c1,c2,c3,c4,c5,c6,c7) (((uint16_t)(c7 == '1') << 15) + (uint16_t)((c7 != '0') << 14) + ((uint16_t)(c6 == '1') << 13) + (uint16_t)((c6 != '0') << 12) + ((uint16_t)(c5 == '1') << 11) + (uint16_t)((c5 != '0') << 10) + ((uint16_t)(c4 == '1') << 9) + (uint16_t)((c4 != '0') << 8) + ((uint16_t)(c3 == '1') << 7) + (uint16_t)((c3 != '0') << 6) + ((uint16_t)(c2 == '1') << 5) + (uint16_t)((c2 != '0') << 4) + ((uint16_t)(c1 == '1') << 3) + (uint16_t)((c1 != '0') << 2) + ((uint16_t)(c0 == '1') << 1) + (uint16_t)((c0 != '0') << 0)) const uint16_t light1_pattern[4] = { 0, 0xffffu, MKPATTERN('1', '1', '0', '0', '1', '1', '0', '0'), MKPATTERN('f', '0', 'f', '1', '0', '1', '0', 'f'), }; const uint16_t light2_pattern[4] = { 0, 0xffffu, MKPATTERN('0', '0', '1', '1', '0', '0', '1', '1'), MKPATTERN('1', '0', 'f', 'f', 'f', 'f', '0', '0'), }; Упаковка шаблонов моргания в битовые массивы. Шаблоны для двух групп светодиодов для четырех режимов упаковались в 16 байт
Итоги Суммируя итоги проекта:
Ну и, самое главное, теперь мой велосипед оснащён устройством, надёжно улучшающим мою видимость на дороге, а, следовательно, и моё спокойствие за рулём.
Хорошей вам погоды, внимательных участников дорожного движения, и попутного ветра! Мяу!
Файлы: Все вопросы в Форум.
|
|
||||||||||||
![]() |
![]() |


![]() |
![]() |
|||
|
||||
![]() |
![]() |