РадиоКот :: Автоматический велосипедный фонарь
Например TDA7294

РадиоКот >Схемы >Цифровые устройства >Автоматика >

Теги статьи: Добавить тег

Автоматический велосипедный фонарь

Автор: eliterr
Опубликовано 30.10.2016

Здравствуйте, уважаемые котоводы!

 

Любите ли вы езду на велосипеде?

Я - да, но я также иногда вижу вдоль дорог сбитых животных, жертв дорожного движения. Чтобы не стать одной из них, мне очень хочется, чтобы меня было как можно луше видно на дороге. А когда я еду на машине, мне бы было гораздо спокойнее, если бы все велосипедисты, особенно в сумерках, ездили с фонарями, а то, бывает, выскочит такой из темноты, а ты пугайся. У меня есть задний фонарь, который достаточно заметно светит красным светом, и с которым я чувствую себя достаточно хорошо обозначенным на дороге.  Фонарь ровно такой, как на картинке внизу:

 

 

Фонарь-то есть, да вот незадача - случается, забуду включить перед тем, как ехать - ведь включать его под седлом не слишком-то удобно.  А иногда, приехав днём, когда светло, забуду выключить (фонарь, опять же, под седлом, и его не слишком-то заметно, когда отходишь от велосипеда), и он светит весь день, разряжая батарейки. В любом случае мне беспокойство - того и гляди, выключится на полпути, и стану я человеком-невидимкой на дороге.

Хотелось бы чтобы было так: берёшь велосипед с места - фонарь включился, приехал на место и поставил велосипед - фонарь сам выключился.

Итак, задача: доработать фонарь, встроив в него датчик движения.

Анализ задачи

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

Оценим донора:

Да, места довольно мало - только центральная выемка под платой (помеченная цифрой "2") и выемка, симметричная кнопке.  Высота просвета под платой - 4.6 мм, и это не считая торчащих из платы вниз концов выводных элементов. Целевой параметр раз - минимизация места.

Я велосипед могу неделями в лапы не брать, а то, может, и месяцами. Позволять моей поделке разряжать батарейки не хочется. Целевой параметр два - минимизация энергопотребления. (UPD: за рекордами энергопотребления в рабочем режиме можно было и не гнаться - при замере оказалось, что в режиме “включено всё” сами светодиоды расходуют ~50mA).

Когда я вижу “низкое энергопотребление”, я думаю про семейство msp430 от Texas Instruments, на котором я уже сделал несколько проектов. Для подобной задачи наверняка подойдёт минимальный представитель - msp430g2001, который у меня есть в нескольких экземплярах как раз в мелком TSSOP. Довольно минималистичный контроллер, но всё что нужно есть - ввод-вывод, таймер, питание от 1.8V. Памяти, правда, не густо - 512 байтов flash, но чувствую, что если поднапрячься - втиснуться можно. Целевой параметр три - минимизация программы.

Итак, приступим. Первое - анализ предмета.

Изучение предмета

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

 

Неведомый контроллер под синей кляксой реализует всю функциональность. Единственный вход - кнопка, замыкающаяся на ноль. Два выхода, управляющие через транзисторные ключи блоками светодиодов. Светодиоды подключены к коллекторам PNP транзисторов.

У фонаря есть четыре режима работы:

  1. Выключено
  2. Включено
  3. Моргание в противофазе (триггер)
  4. Вспышки

Первые три режима понятны, а вот четвертый требует пояснений. Во-первых, надо заметить, что шаблон моргания повторяется с периодом примерно 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-состав к декстрину прилипает из рук вон плохо, и в сплошном слое получились дырки.  А может, так получилось из-за неверного температурного режима переноса.

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

Программная часть

Функциональность фонаря:

  • три активных режима состояния и один режим сна
  • переключение между режимами производится кратким (меньше 0.1сек) кнопки, который переводит в следующий режим в активное состояние
  • в активных состояниях светодиоды переключаются в соответсвии с режимами фонаря-донора
  • после 10 минут работы прибор выключает светодиоды и переходит в состояние покоя
  • срабатывание датчика движения в активном состоянии сбрасывает счётчик времени перехода в состояние покоя
  • срабатывание датчика движения в состоянии покоя переводит фонарь в активное состояние
  • в режиме сна светодиоды выключены, датчик движения также выключен, т.е. выход из режима сна - только по нажатию кнопки
  • долгое нажатие на кнопку (в любом состоянии) переводит прибор в состояние сна

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

С реализацией всего этого в программе пришлось немного повозиться. Программирую я на C++. Пусть меня поборники тёплого лампового С попробуют закидать тапками, но namespace, template вместо #define, const, auto и более строгое приведение типов - очень удобные вещи. Классы и наследование для этого проекта на этом контроллере я, понятно, не использовал, так что в этом проекте никаких накладных расходов и неявного кода от использования C++ нет.

Реализация программы - state machine с тремя состояниями:

  • Работа: таймер тикает, светодиоды моргают
  • Ожидание кнопки: для определения долгого нажатия опрашиваем состояние кнопки после нескольких циклов
  • Сон: входим в режим low power mode, при котором выключается таймер, и выходим из него по прерыванию (оно может быть от датчика движения или от кнопки)

Разработка на более богатом ресурсами контроллере 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;


Не инициализировать ничего ничем, кроме нуля. Данные, инициализированные нулём, размещаются в сегменте bss, от которого в прошивку попадает только адрес начала и длина. При запуске программы вызывается memset, заполняющий RAM нулём. Расход флеша - код memset плюс пара констант. А вот начальные значения данных, инициализированных чем-либо иным, идут в сегмент data и записываются во flash как есть. При исполнении программы вызывается memmove, копирующий начальные значения в RAM. Расход флеша: данные + код memmove, который гораздо длинее, чем memset.

extern "C"
__attribute__((naked))
ISR(RESET, reset_interrupt) {
  _set_SP_register(stack);
  // ...
  main_program();
}


Отказ от библиотечной процедуры инициализации в процедуре запуска (вектор прерывания reset). При инициализации делается несколько переходов, от которых можно избавиться, если знать, что после reset и инициализации данных вызывается main(), из которой возврата нет.

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 байт

 

Итоги

Суммируя итоги проекта:

  • получена масса удовольствия от создания аккуратной штуковины
  • получена масса удовольствия от создания полезной штуковины
  • я точно узнал, что происходит при начальной инициализации C runtime
  • применены на практике подходы к оптимизации программ, о которых я раньше только теоретически задумывался
  • применены новые материалы - однослойный текстолит и toner reactive foils
  • описан простой процесс ЛУТ (вот уж не думал, что когда-либо сам буду об этом писать) и проделана калибровка утюга
  • написана статья, которая может оказаться кому-нибудь полезной

 

Ну и, самое главное, теперь мой велосипед оснащён устройством, надёжно улучшающим мою видимость на дороге, а, следовательно, и моё спокойствие за рулём.

 

Хорошей вам погоды, внимательных участников дорожного движения, и попутного ветра! Мяу!


Файлы:
Архив файлов проекта


Все вопросы в Форум.




Как вам эта статья?

Заработало ли это устройство у вас?

24 6 7