Контроллер светодиодов для авиамодели

Контроллер светодиодов для авиамодели

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

По ряду причин предложеный вариант мне не подошел. В этой статье я описал свой вариант контроллера моргающих и не моргающих светодиодов для авиамодели.

Контроллер сделан на базе ATTiny13A, т.о. статья будет так же полезна тем, кто разбирается с микроконтроллерами AVR. Я старался все разжевать и разложить по полочкам, так что статья будет интересна в первую очередь новичкам.

Способности железяки можно оценить по этому видео:

Вступление

Когда я начал ковырять этот вопрос и даже спаял плату предложеную в той статье, оказалось что это совсем не то, что мне нужно. Во-первых всего два канала, которые могут моргать только по очереди. Можно подрегулировать тайминги, но алгоритм зашит в прошивке. Во-вторых на плате присутствуют аж 3 кнопки, которые добавляют лишние граммы. Я не собираюсь переставлять моргалку с модели на модель и перепрограммировать светодиоды после каждого полета, а значит эти кнопки мне без надобности. Я согласен один раз напаять провода прямо на контроллер и запрограммировать тот алгоритм, который нужен. И наконец в-третьих прошивка есть только в бинарном виде, без исходников, а значит доработать что либо нельзя.

  • БАНО (Бортовые АэроНавигационные Огни — зеленый и красный фонари на концах крыльев) – эти штуки включены всегда
  • Посадочные фары – будут включаться и выключаться с пульта, т.е. схема должна реагировать на PWM сигнал с приемника
  • Стробы — белые огни которые время от времени моргают короткими и яркими вспышками
  • Проблесковые маячки – красные огни, которые включаются и выключаются плавно, напоминая крутящийся ламповый олдскульный проблесковый маяк.

Модель большая, летает далеко. А значит, что бы светодиоды было видно, они должны быть мощными. На предыдущей модели я делал БАНО на одноваттных светодиодах – их прекрасно видно с расстояния в 50м даже в яркий солнечный день вечер. Значит это мой размерчик.

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

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

Электроника

С требованиями разобрались. Пора браться за паяльник.

  • Выкинуть кнопки
  • Завести PWM вход от приемника
  • 3 ноги ATTiny определить как выходы и подключить к ним светодиодные драйверы
  • Добавить четвертый драйвер, который будет всегда включен (для БАНО)

В общем от оригинала мало что осталось.

В качестве драйвера удачно подошла микросхема ZXLD1350 которая как раз и рассчитана на питание одноваттных светодиодов (ток до 350мА). При чем в каждый канал можно ставить последовательно любое светодиодов, лишь бы все вместе суммарно вкладывались в напряжение питания. Т.е. если я буду запитывать схему от батареи 3S (11.1В) то смогу в каждый канал поставить до 3 светодиодов на кажом из которых падает 3.2В.

Микроконтроллер я запитал отдельно от приемника, тем же проводом что и PWM вход.

Схема. Каждый канал построен по схеме из даташита. Таких каналов на плате 4 штуки (я нарисовал только один). Светодиодов я нарисовал 3, но, как я уже сказал, можно ставить любое количество светодиодов в каждый канал. Можно даже ставить светодиоды разного цвета (на них падает разное напряжение), главное что бы они были рассчитаны на одинаковый ток. Драйвер сам подберет такое напряжение, что бы ток через диоды не превышал 350мА.

Вход ADJ трех из каналов подключен к выходу контроллера через транзистор. У драйвера ZXLD1350 есть специальный механизм с помощью которого можно включать и выключать светодиод с контроллера. Более того, можно плавно регулировать яркость меняя напряжение на входе или с помощью ШИМ. Вот только рабочее напряжение входа от 0.3В до 2.5В, а с контроллера выдает 5В. Благо даташит рекомендует решение в виде транзистора. Нужно только учесть, что этот транзистор инвертирует логическое состояние – ноль на ноге контроллера будет включать светодиод, а единица выключать. Впрочем, это не проблема пофиксить программно.

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

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

Готовое изделие. Лишний текстолит обрезал по рамочке. Получилась платка 27х22мм и весом 4г. Ну еще 2г на провода и раземы получилось. К приемнику устройство подключается через стандартный трехпиновый JR разъем. Светодиодные драйверы берут питание с балансировочного разъема батарейки.

Кому 1Вт мало может посмотреть на микросхему ZXLD1360. Она рассчитана на питание 3Вт светодиодов (ток 750мА). Схема включения и цоколевка такие же, так что разводка платы подойдет. Только номиналы некоторых деталей поменять нужно, курите даташит.

Для тех, кто не прокачался еще в травлении двухсторонних плат, я так же выкладываю несколько вариантов односторонних – для 2, 3 и 4 каналов.

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

А вот так на предыдущей модели.

Прошивка

Теперь нужно вдохнуть жизнь в эту железяку. Поскольку исходников прошивки от САМОКАТ ВЕТЕРАНА в инете не нашлось, то пришлось делать все самому. Нет, я, конечно, дизассемблировал его прошивку, что бы посмотреть что внутри, но гораздо полезнее было просто прочитать спецификацию на ATTiny13.

Несмотря на то, что в микроконтроллере всего 1Кб флеша, я решил писать на С. Так удобнее и нагляднее. Скетчи ардуино, конечно, в чем то попроще будут, но все что я задумал не влезет в память контроллера. Поэтому пришлось спуститься на более низкий уровень и программировать регистры напрямую. К моему удивлению компилятор (gcc 3.4.2 из Atmel Studio 6) сгенерил довольно хороший код. Было, правда, пару мест, где компилятор действовал неоптимально, но эти места удалось скорректировать.

Архитуктурная проблема прошивки в том, что мне требовалось делать несколько концептуально разных действий одновременно – тут моргать, тут не моргать, тут рыбу заворачивать, тут слушать ШИМ вход, тут генерить ШИМ на выходе.

Приведу классический пример. Что если нам нужно моргать одним светодиодом? Ну тогда наша программа будет выглядеть как то так:

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

Но скорее всего будет очнь сложно подобрать тайминги и последовтельность включений-выключений. Если получится, конечно (в чем я сомневаюсь). А если нужно тремя диодами моргать? А четырьмя?

Правильное решение — использовать таймеры. Но есть проблема: таймер в микроконтроллере всего 1, да и тот восьмибитный.

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

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

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

Вопрос настройки микроконтроллерного таймера я опишу чуть позже. А сейчас про реализацию програмных таймеров. Функция инициализации выглядит просто – обнуляем список таймеров

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

Основной цикл выглядит следующим образом.

Вначале запускаем таймер (об этом чуть ниже, там не все так тривиально). Вместо активного ожидания я использую sleep. В начале каждого цикла процессор переходит в режим Idle. Это означает что сам CPU заснет, но все таймеры (ок, весь, он один!) продолжат работать. Когда таймер досчитает до конца и ресетнется в ноль возникнет прерывание, которое разбудит процессор и программа пойдет дальше. Как раз то, что нам нужно.

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

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

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

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

Что бы это все заработало, нужно чтобы обработчик как-то вызвался в первый раз. Для этого перед запуском основного цикла — просто положим в очередь сообщение, что пора вызвать обработчик с задеркой 0 мс (т.е. сразу при первой же возможности).

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

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

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

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

Разумеется, к такому стилю программирования еще нужно привыкнуть, но в целом подход работает неплохо. Вспомните, если мы пишем многопоточное приложение для большого компа, обычно каждый поток имеет вечный цикл и, возможно, какой нибудь sleep или wait. Считайте, что представленые выше обработчики — это тело того самого вечного цикла, а вызов addTimer() это тот же самый sleep.

Как часто должен тикать основной таймер? Если он будет тикать редко, то это снизит точность отмеряемых временных отрезков. С другой стороны на каждый цикл таймера нужно будет сделать определенное число полезных действий. И эти действия нужно успеть закончить до следующего цикла таймера. Значит таймер тикать должен и не очень часто так же.

Т.е. не часто и не редко. Но как именно? Ок, для предыдущей задачи диапазон возможных значений достаточно большой. Но нужно так же не забывать про задачу «слушать PWM вход». Конкретнее, там ходят импульсы длительностью 800-2200мкс и эту длину нам прийдется измерять. Для нашей задачи включать/выключать светодиод по команде с пульта будем считать так: если импульс короче чем 1500мкс – светодиод выключен, если длинее – включен.

В переводе на язык микроконтроллеров и таймеров мы будем считать сколько тиков таймера уместится в измеряемом отрезке времени. Проблема возникает когда длительность импульса примерно равна пороговой. Тогда возможны ложные срабатывания и светодиод будет промаргивать при изменении длины импульса. Что бы уменьшить вероятность промаргивания нам нужно точнее измерять длину импульса. Я думаю разрешение таймера должно быть в районе 1-2 мкс – такое разрешение обеспечит достаточную точность измерений.

Раз уж речь зашла про конкретные цифры нужно разобраться с частотой микроконтроллера. Контроллер может тактироваться от внутреннего и от внешнего генератора. Внешний генератор точнее, но это дополнительные детали и вес. Да и не нужна нам точность особо. Из внутренних генераторов доступны 128кГц, 4.8МГц и 9.6МГц. 128кГц маловато будет, будем выбирать между двумя другими вариантами.

Таймер в свою очередь может иметь ту же частоту, что и микроконтроллер, а может задействовать делитель частоты на 8, 64, 256 или 1024. Сам же таймер считает от 0 до 255 и потом сбрасывается в 0. В случае если делитель не используется один тик таймера соответствует одному тику процессора, что в большинстве случаев соответствует одной команде. Мы собирались делать полезную работу каждый полный цикл таймера. Но если нам эту работу нужно будет делать каждые 256 команд, то мы попросту не будем успевать делать эту работу (либо ее должно быть очень очень мало).

Итак, нужно выбирать между частотой 4.8МГц и 9.6Мгц, и делителями 8, 64 и 256. Как по мне, вариант 4.8МГц с делителем 8 довольно удачный. Таймер будет тикать с частотой 4.8МГц / 8 = 600кГц. А это означает, что один тик будет занимать 1.666мкс. Как раз укладывается в искомые 1-2мкс. Полный цикл таймера будет занимать 1,666 * 256 = 426.66 мкс. В качестве програмного таймера мы используем 16-битную переменную, а значит мы способны отмерять отрезки времени 65536 * 426,66мкс = 27,96с (с точностью тех же самых 426,66мкс)

Код запуска таймера:

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

Этот макрос определяет количество циклов по 426,6мкс необходимых для отмерения заданого количества милисекунд. К сожалению когда я бахнул туда полную формулу (ту, что в комментарии) то компилятор начал генерить страшные ворнинги с которыми я не справился. Пришлось формулу пересчитать до непонятных теперь 600/256.

Но вернемся к слушанию ШИМ входа. Что бы было чуть понятнее, расскажу еще раз как все работает, но другими словами. Основной 8-битный таймер тикает от 0 до 255. Каждый полный цикл таймера мы процессим список программных таймеров и запускаем обработчики, если нужно. Помимо этого значение самого 8-битного таймера используется в измерение длины импульса на входе. Делается это очень просто: если начался импульс – запоминаем значение таймера. Пока импульс идет таймер продолжает тикать. К моменту когда закончится импульс таймер дотикает до какого то нового значения. Соответственно по разнице значений мы можем вычислить длину импульса просто умножив на время одного тика (1,666мкс)

Стоп! Таймер то у нас 8битный а это означает, что таким способом можно измерить только импульсы длиной до 256 * 1,66 = 426,66мкс, в то время как входящие импульсы длиной до 2200мкс. Не беда! Можно искусственно расширить счетчик таймера добавив столько старших байт сколько нужно. Действует обычная бинарная математика – когда младший байт переполнился инкрементируем старшие байты.

Почти все тоже самое. Добавилась только переменная tcnth – «старший» байт в дополнение к младшему байту внутри таймера. Еще важна последняя строка – она включает прерывание по переполнению таймера. Это прерывание и будет инкрементировать старший байт:

Обратите внимание, что переменная tcnth объявлена как volatile. Без этого ключевого слова компилятор в другой части программы может подумать, что переменная не меняется и оптимизировать лишнего. Он же не в курсе, что переменная изменяется в прерывании (по сути в другом потоке).

Для того, что бы словить начало и конец импульса можно задействовать специально предназначеный для этого pin change interrupt – прерывание, которое вызовется как раз когда значение на входе поменяется. Т.о. не нужно постоянно опрашивать вход – всю работу сделает микроконтроллер. Нам остается только написать обработчик этого прерывания

Первая часть обработчика посвящена вытягиванию значения таймерного счетчика (расширенного дополнительным внешним байтом). К сожалению, вычитывание значения в лоб не работало – время от времени светодиод самопроизвольно помаргивал. Это происходило потому, что 2 прерывания возникали примерно одновременно. А поскольку у прерывания таймера приоритет ниже, то обработчик иногда не вызывался тогда, когда следовало. В результате старший байт оказывался не увеличеным, а значит общее значение оказывалось на 256 единиц меньше. Это существенно.

Решение довольно простое – проверить, а не возникло ли переполнение таймера и если возникло, сделать ту же самую работу, что и обработчик этого переполнения – сделать +1 к старшему байту.

В этом месте я наткнулся на неоптимальность кода, сгенеренного гнусью. Код (tcnth << 8) + TCNT0 компилировал вот так, как написано — со сдвигами и сложениями. И это не смотря на включенную оптимизацию (-O1). Мне же в этом месте нужно всего лишь 2 байта трактовать как 16-битное число. Пришлось городить огород с юнионами.

Вторая часть обработчика делает собственно полезную работу. Если мы словили начало импульса – просто запомним временнУю метку в переменной pwmPulseStartTime. Если словили окончание импульса — считаем разницу временнЫх меток и включаем/выключаем светодиод в зависимости от значения. Порог срабатывания – 1500мс, или 900 тиков таймера по 1,66мкс каждый.

Чего тут не хватает, так это инициализации этого самого pin change interrupt:

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

Можно написать код, который будет включать и выключать светодиод. Благо таймеров теперь можно наделать сколько хочешь. Но зачем, если PWM генерация уже встроена в контроллер? При чем можно независимо управлять аж двумя каналами генерации – на ножках OC0A и OC0B (они же PB0 и PB1).

Работает это так. Единственный таймер крутится как обычно с заданой скоростью. В начале цикла на ножке выставляется единица, по достижению некоторого определенного значения (задается регистрами OCR0A и OCR0B) на ножке выставляется ноль. Далее цикл повторяется. Чем больше значение регистра, тем больше скважность и тем ярче светится диод. Это называется Non-Inverting режим. Поскольку светодиоды подключены через инвертор то нам больше подойдет Inverting mode – включаемся по значению в регистре, выключаемся когда таймер добегает до конца и ресетится.

Пришлось слегка подкорректировать инициализацию таймера. Биты WGM00 и WGM01 включают режим генерации Fast-PWM. Биты COM0A0, COM0A1, COM0B0 и COM0B1 включают Inverting mode в каналах A и B. Точнее закоментареная строка включает в обоих, раскоментареная только для OC0A.

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

Можно, конечно, и напрямую сразу пихать значения в регистры OCR0A и OCR0B, но даташит это не рекомендует. Это может привести к «проскакиванию», когда значение пина меняется раньше или позже чем должно было бы. Визуально это проявлялось бы в нежелательном резком и кратковременном изменении яркости.

Саму яркость можно менять в уже знакомых програмных таймерах. Например, так:

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

Не уверен, что это похоже на проблесковый маяк, но в этом куске кода делается предвспышка до яркости 127, потом уменьшаем яркость до 33 и делаем полную вспышку (до 255).

На этом, наверное, по прошивке и все. Со всеми потрохами и моргульками все влазит в 500-600 байт – даже еще запас остается. Остается осветить один важный момент – Fuse биты. Они равны hfuse=0xff, lfuse=0x79. За расшифровкой попрошу в даташит. В двух словах, пару бит в этих байтах заставляют контроллер работать на частоте 4.8МГц. Остальные биты оставлены в дефолтном состоянии.

Как это выглядит в реальности можно оценить по видео в шапке статьи.

Заключение

В этой статье я описал свой вариант контроллера для разных авиамодельных лампочек – БАНО, посадочных фар и стробов. Останется только чуток подковырять прошивку и Ваши модели копии станут выглядеть как настоящие.

Более того, подобную моргалку можно поставить и на автомодель. А можно, например, подсвечивать рекламную вывеску. Все в Ваших руках.

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

  • Как на одном таймере сэмулировать несколько таймеров
  • Как выбирать параметры таймера
  • Как настраивать таймер в режим генерации PWM
  • Как слушать PWM вход и делать полезную работу на основе считаных значений

Если абстрагироваться от задачи моргания светодиодами, то получился неплохой такой каркас для «многозадачных» («многопоточных»?) приложений на микроконроллере. Конечно, это еще не RTOS, но уже избавляет от целой кучи рутинных операций. В прошивке этот каркас я вынес в отдельный модуль EventQueue.c/.h. Используйте наздоровье.

Приятно то, что получилось 3 совершенно независимых задачи (отсчет длинных отрезков времени, измерение длительности импульсов на входе и генерацию ШИМ на выходе) удалось повесить на один единственный 8-битный таймер. Ну а на добавленых програмных таймерах можно еще кучу всего полезного сделать.

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

Статья не является справочником — вам все равно за разъяснением тех или иных битов и регистров прийдется лезть в даташит. Это всго лишь некий пример, как можно скудными средствами младшего контроллера в линейке AVR сделать некоторую полезную штуку.

Изначально я не планировал прикладывать к статье финальный скомпилированые hex файл. Дело в том, что все модели разные. Где-то нужно другое число каналов, где-то нужно по другому моргать, возможно нужно добавить пару входов, или сделать что нибудь еще. Вместо этого я бы предложил вам самим попробовать изменить прошивку так, что бы полностью соответствовать вашей задумке. Там все просто!

Тем не менее, не все авиамоделисты дружат с компилятором. Так что я все же скомпилировал некий средний вариант: один канал моргает раз в 2 секунды (строб), ШИМ канал мигает чуть-чуть чаще двойными вспышками, третий канал включается по команде с пульта, четвертый, как и раньше, светит всегда. Эта прошивка будет некой отправной точкой для последующего допиливания. Примеры представленые в статье я так же оставил в коде, только вызовы закоментарил.

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

📎📎📎📎📎📎📎📎📎📎