Хабрахабр

Ардуина и светодиод, или как прокачать детский конструктор

Однажды просматривая серию Фиксиков где фигурировал такой же конструктор ребенок спросил: “Папа, а почему у фиксиков детальки светятся, а у нас нет?”. Мой сын крепко “подсел” на магнитный конструктор Magformers.

Поскольку к этому времени у нас уже собрался целый ящик магнитиков всех возможных форм и размеров (как по мне, китайский магформерс ничуть не уступает оригиналу), покупать еще один набор только ради лампочки как-то не хотелось. Оказалось, что действительно существует набор “Magformers Neon LED Set”, где помимо обычных строительных блоков есть еще и элемент со светодиодом. Тем более, что этот набор стоил ощутимо дороже аналогичного без подсветки.

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

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

Первые шаги

Мне показалось, что светяшка на обычном светодиоде (пускай даже RGB) это скучно и банально. А вот пощупать что нибудь вроде WS8212 показалось интересным. На ебее предлагались как отдельные светодиоды, так и матрицы размером до 16х16. Накупив несколько разных модулей свой выбор я остановил на матрице 4х4. В ней достаточно много светодиодов, чтобы побаловаться различными визуальными эффектами, при этом модуль сопоставим по размерам с окошком квадратного блока конструктора.

А вот клон digispark на контроллере ATTiny85 оказался в самый раз — в нем не очень много памяти и пинов, но более чем достаточно для светодиодной моргалки. Для управления светодиодной матрицей достаточно всего одного пина микроконтроллера, так что даже ардуина нано выглядит как перебор (к тому же она не влезет в корпус). Давно хотел его попробовать. Модуль отлично интегрируется с Arduino IDE и имеет на борту загрузчик по USB, поэтому программировать этот модуль очень просто и комфортно.

Начал с простейшей схемы.

Но вот игрушка с проводным питанием это не дело — нужно подумать о питании от батарей. В таком виде удалось достаточно быстро отладить все алгоритмы свечения/моргания (о них ниже). А раз есть литиевая батарея, то нужно думать как ее заряжать. Причем чтобы не разорится на пальчиковых батареях (которые к тому же не влезают в габарит) решено было использовать литиевую. В закромах как раз нашелся купленный по случаю “народный” контроллер заряда на микросхеме TP4056.

Схема модуля Digispark ATTiny85 не очень на такое рассчитана — там либо питание от USB, но тогда питание подается напрямую на микроконтроллер (по шине +5), либо от входа VIN, но тогда питание идет через линейный стабилизатор 7805. Только вот подключить его сразу не получилось. Пришлось доработать немного схему и выпаять лишние детали. Вариант, когда модуль зарядки лития вставляется в разрыв между разъемом USB и микроконтроллером не предусмотрен.

Выход зарядника (по сути аккумулятор подключается напрямую) заходит назад в плату через ножку 5V. Так, теперь питание от USB поступает на ножку VIN и дальше уходит на вход зарядника. 2В (напряжение аккумулятора) это вполне нормально — диапазон рабочих напряжений микроконтроллера 1. И хотя на самом деле там будет от 3 до 4. 5В. 8-5. 7В, хотя ниже 3. И даже светодиодный модуль нормально работает от 2. 2В синему светодиоду немного не хватает и цвета немного “плывут” в желтый.

Общая схема теперь выглядит так В целях экономии электроэнергии вечно горящий светодиод D2 я тоже выпаял.

Можно было бы оставить два USB разъема различного назначения — один для зарядки, другой для прошивки, но это как-то неправильно. Питать схему можно было бы и через USB разъем в заряднике, но тогда бы потерялась возможность заливать прошивку через USB разъем на плате контроллера.

В любом случае при подключении нагрузки даже в 10мА напряжение на аккуме падало до 1В. Аккумулятор размера 6х25х35 купил на ебее, но он оказался либо бракованный, либо я его убил коротким замыканием или большим током заряда (у платы по умолчанию ток заряда установлен в 1А и нужно перепаивать один резистор, чтобы уменьшить ток). Чуть позже заказал аккум у другого продавца и он оказался хорошим. На время тестирования я переключился на полу-сдохшую LiPo батарею от мелкого квадрокоптера.

И тут я прослезился. В принципе, на этом можно было бы и остановится, припаять соединительные провода и аккуратно затолкать все в какой нибудь корпус, но я решил измерить потребление схемы. Т.е. Ладно, что в рабочем состоянии (когда лампочки сияют на полную) эта штука жрет до 130мА, так в состоянии покоя потребление более 25мА! мою батарею в 600мАч эта моргалка слопает менее чем за сутки!

Даже если они не светятся — в каждом из них все равно работает микроконтроллер и ожидает команду. Оказалось, что около 10мА потребляют светодиоды. нужно придумать схему отключения питания светодиодам. Т.е.

Да, его можно уложить спать и согласно даташиту потребление будет измеряться микроамперами, но на деле меньше 1 мА получить не удалось. Оставшиеся 15 мА потребляет микроконтроллер. Похоже где-то в схеме есть какая-то утечка, но моих скромных познаний в электронике недостаточно, чтобы ее найти и понять. Я и АЦП отключал и пины переводил в input.

Усложняем схему

Тут я вспомнил, что я себе купил на пробу микросхему PT1502. Эта микросхема — контроллер заряда литиевого аккумулятора в комплекте с источником питания с несколькими управляющими входами. Единственная сложность — микросхема идет в корпусе QFN20 размером 4х4 мм и требует некоторой обвязки. Паять такое дома сложно, но можно. Плата получается сложной для обычного ЛУТа и нужно заказывать у китайцев. Но мы ведь не боимся сложностей, правда?

В нескольких квадратиках схему можно описать так.

У устройства есть кнопка ‘Power’, которая включает моргалку (она же переключает режимы). В выключенном состоянии питание на контроллер и светодиоды не поступает. Т.е. Светодиод сияет, скажем, минуту и если пользовательской активности нет (никто не нажимает кнопку), то устройство выключается. Причем отключает все сразу — и микроконтроллер, и светодиоды. не просто уходит в сон, а именно отключает само себе питание сигналом Power Hold. Функциональность включения и отключения питания реализуется внутри микросхемы PT1502

Схема, по большей части, слизана с даташита PT1502, а также модуля Digispark ATTiny85. Осталось всего ничего: нарисовать принципиальную схему и сделать плату. Микросхема контроллера питания PT1502 функционально делится на несколько частей, потому на схеме разбита на блоки.

Светодиод LED1 показывает состояние заряда — горит, значит идет заряд. Это, собственно, контроллер заряда литиевой батареи со своей обвязкой. Поскольку у меня батарея на 600мАч, в принципе можно поднять ток и до 600мА поставив резистор на 780-800 Ом. Резистор R6 задает ток заряда в 470мА. Впрочем я не уверен в особом качестве моей батареи — пусть лучше заряжает медленнее, но дольше проживет.

Рассмотрим схему управления питанием

Когда питание установится микросхема запустит микроконтроллер, отпустив сигнал RESET. Кнопка SW1 запускает всю систему — микросхема PT1502 просыпается сама и затем запускает все источники питания (которых у нее 3). Для удобства отладки я еще добавил отдельную кнопку Reset.

Когда микроконтроллер запустится он должен выставить на этой линии единицу. Сигнал HOLD используется для выключения всей системы. Когда пора закругляться, микроконтроллер выставляет на линии HOLD ноль и микросхема питания PT1502 остановит все источники питания.

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

Я решил не делать 2 отдельные кнопки для включения и для управления. Вернемся на секунду к кнопке SW1. Номиналы делителя R7-R8 подобраны так, чтобы не спалить порт микроконтроллера PB2. Поэтому та же кнопка подключена еще и к ATTiny85 и во время работы переключает режимы моргания. 2В) на ногу контроллера будет поступать напряжение в оговоренных даташитом пределах (0. При всех диапазонах напряжений батареи (3,3 — 4. 5В) 7*VCC — VCC+0.

Рассмотрим источник питания

Напряжение на выходе задается резисторами R10-R11 и согласно формуле из даташита настроено на 3. Это импульсный DC-DC преобразователь. Все остальное — несложная обвязка. 3В.

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

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

Перейдем к логической части.

Это нужно для согласования напряжений USB (по которому бегает 3. Обвязка USB слизана с платы Digispark без изменений. Поскольку в моем случае микроконтроллер также питается от 3. 3В) и сигналов микроконтроллера (который в оригинале питается от 5В). 3В, то схему можно было бы и упростить, но на всякий случай я развел на плате оригинальную схему.

В обвязке микроконтроллера ничего интересного.

Финальный штрих это разъем

Потому я не стал ограничиваться только выводом линии на светодиод. По сути у меня получилась такая себе отладочная плата на ATTiny85 с поддержкой USB и контроллером питания от литиевой батареи. Вместо этого я вывел все линии микроконтроллера на гребенку — заодно и к программатору удобно подключать.

Например, не распаивать обвязку USB и освободить линии PB3/PB4. И пускай почти все линии жестко привязаны к определенному функционалу (PB1 — линия Hold, PB2 — кнопка включения, PB3/PB4 — USB, PB5 — Reset) в будущем можно будет в некоторых пределах обойти. Ну а пока свободным остается только PB0 — к нему и подключим наш светодиод. Или, например, отказаться от ресета и освободить PB5.

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

Для удобства использования на обратной стороне подписал все возможные функции выводов (идею слямзил с платы Digispark)

Качеством, если честно, не очень доволен — если много раз перепаивать микросхему, то маска возле мелких контактов PT1502 чуток облазит. Плату заказывал на JLCPCB . Впрочем, если все запаять с первого раза, то норм. Ну и мелкие надписи немного поплыли.

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

Корпус

Пора переходить к корпусу. Его я напечатал на 3Д принтере. Дизайн без излишеств — коробка и кнопка. На коробке предусмотрены специальные зацепы, чтобы устанавливать светяшку в стандартный квадратный модуль конструктора.

В корпусе живет основная плата и батарея.

Светодиодная панель крепится на крышку, которая в свою очередь привинчивается шурупами к основной коробке

Получилось вот так Сначала я думал прикручивать светодиодную панель к к крышке шурупами, но в итоге просто приклеил на двусторонний скотч.

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

Первый вариант рассеивателя я пробовал сделать по технологии усадки ПЭТ бутылок строительным феном (подсмотрено у авиамоделистов).

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

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

Решил попробовать рассеиватель просто напечатать. Пошуршав по сусекам я нашел пробник пластика Verbatim PET Transparent в пару метров. Вероятно это из-за внутренней структуры, т.к. И хотя на входе в принтер пластик кажется кристально прозрачным реальная деталь получается матово мутная. Более того если попробовать обработать деталь наждачкой для более гладкой поверхности то получаем еще большее матирование. слои не заполняют объем полностью а накладываются с промежутками и щелям. Впрочем, это как раз то, что мне и было нужно.

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

Прошивка

Для светодиодной моргалки особо сильно в периферию микроконтроллера погружаться не нужно — достаточно парочки функций по работе с GPIO. Но раз уж модуль стыкуется с платформой Ардуино, то почему бы этим не воспользоваться?

Для начала несколько определений и констант

// Number of total LEDs on the board. Mine has 4x4 LEDs
#define NUM_HW_PIXELS 16 // Pin number where LED data pin is attached
#define DATA_PIN 0
// Pin number where mode switch button is attached
#define BUTTON_PIN 2
// Power Enabled pin
#define POWER_EN_PIN 1 // Max brightness (dimming the light for debugging)
#define MAX_VAL 255

Тут определяется количество пикселей в моей матрице, номера пинов и максимальная яркость светодиодов (во время отладки удобно было ее ставить на уровне 50, чтобы не слепила глаза)

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

// LED indexes for different patterns
uint8_t circleLEDIndexes[] = ;
uint8_t beaconLEDIndexes[] = {6, 5, 10, 9};
uint8_t policeLEDIndexes[] = {7, 6, 10, 11, 4, 5, 9, 8};

Для управлением светодиодов я не стал изобретать велосипед и взял готовую библиотеку для работы со светодиодами WS8211. Интерфейс библиотеки слегка побелил-покрасил. Некоторые вспомогательные функции (например конвертация HSV в RGB) также оттуда слямзил.

Для начала плату и библиотеку WS8211 нужно проинициализировать

// Driver
Ai_WS2811 ws2811; void setup()
{ // Set up power pinMode(POWER_EN_PIN, OUTPUT); digitalWrite(POWER_EN_PIN, HIGH); // initialize LED data pin pinMode(LED_PIN, OUTPUT); // Initialize button pin pinMode(BUTTON_PIN, INPUT); // Initialize WS8211 library static CRGB ledsBuf[NUM_HW_PIXELS]; ws2811.init(DATA_PIN, NUM_HW_PIXELS, ledsBuf); // Set the watchdog timer to 2 sec wdt_enable(WDTO_2S);
}

Первым делом нужно выставить сигнал POWER HOLD в единицу — это будет сигналом микросхеме PT1502, что микроконтроллер завелся и работает исправно. Микросхема в свою очередь будет исправно поставлять электричество микроконтроллеру и светодиодам до тех пор, пока сигнал HOLD выставлен в единицу.

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

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

Библиотека WS8211 хранит в себе буфер с цветовыми значениями каждого светодиода. Теперь нужно определить парочку вспомогательных функций. Работать с буфером напрямую не очень удобно, потому я написал простую функцию записи RGB значения в определенный светодиод

void setRgb(uint8_t led_idx, uint8_t r, uint8_t g, uint8_t b)
{ CRGB * leds = ws2811.getRGBData(); leds[led_idx].r = r; leds[led_idx].g = g; leds[led_idx].b = b;
}

Но в большинстве случаев в цветовой модели RGB считать цвета не очень удобно, а то и вообще невозможно. Например при рисовании всяких радуг удобнее работать с цветовой моделью HSV. Цвет каждого пикселя задается задается значением цветового тона и яркостью. Значение насыщенности для простоты опущено (используется максимальное). Значения цветового тона (hue) сведены к диапазону 0-255 (вместо стандартных 0-359).

/**
* HVS to RGB conversion (simplified to the range 0-255)
**/
void setHue(uint8_t led_idx, int hue, int brightness)
{ //this is the algorithm to convert from RGB to HSV double r = 0; double g = 0; double b = 0; double hf = hue/42.6; // Not /60 as range is _not_ 0-360 int i=(int)floor(hue/42.6); double f = hue/42.6 - i; double qv = 1 - f; double tv = f; switch (i) { case 0: r = 1; g = tv; break; case 1: r = qv; g = 1; break; case 2: g = 1; b = tv; break; case 3: g = qv; b = 1; break; case 4: r = tv; b = 1; break; case 5: r = 1; b = qv; break; } brightness = constrain(brightness, 0, MAX_VAL); setRgb(led_idx, constrain(brightness*r, 0, MAX_VAL), constrain(brightness*g, 0, MAX_VAL), constrain(brightness*b, 0, MAX_VAL) );
}

Функция взята из библиотеки Ai_WS8211 и слегка подпилена. В оригинальном варианте этой функции из библиотеки было парочку багов из-за чего цвет на радугах показывался с рывками.

Каждая функция вызывается из главного цикла для отрисовки одного “кадра”. Перейдем к реализации различных эффектов. Поскольку каждый эффект оперирует разными параметрами между вызовами они сохраняются в статических переменных.

Это самый простой эффект — все светодиоды заливаются одним цветом, который плавно меняется.

void rainbow()
{ static uint8_t hue = 0; hue++; for (int led = 0; led < NUM_HW_PIXELS; led++) setHue(led, hue, MAX_VAL); ws2811.sendLedData(); delay(80);
}

Следующий эффект поинтереснее — он выводит радугу по контуру матрицы, а цвета в радуге постепенно смещаются по кругу.

void slidingRainbow()
{ static uint8_t pos = 0; pos++; for (int led = 0; led < ARRAY_SIZE(circleLEDIndexes); led++) { int hue = (pos + led*256/ARRAY_SIZE(circleLEDIndexes)) % 256; setHue(circleLEDIndexes[led], hue, MAX_VAL); } ws2811.sendLedData(); delay(10);
}

А этот эффект заливает всю матрицу случайным цветом, который сначала плавно загорается, а потом также плавно гаснет.

void randomColorsFadeInOut()
{ static uint8_t color = 0; static bool goesUp = false; static uint8_t curLevel = 0; if(curLevel == 0 && !goesUp) { color = rand() % 256; goesUp = true; } if(curLevel == MAX_VAL && goesUp) { goesUp = false; } for(int led = 0; led < NUM_HW_PIXELS; led++) setHue(led, color, curLevel); if(goesUp) curLevel++; else curLevel--; ws2811.sendLedData(); delay(10); }

Следующая группа эффектов рисует разные проблесковые маячки. Так, например, ребенок любит строить из магнитиков бульдозер и оранжевая мигалка там будет очень кстати.

void orangeBeacon()
{ const int ORANGE_HUE = 17; static uint8_t pos = 0; pos+=3; for (int led = 0; led < ARRAY_SIZE(circleLEDIndexes); led++) { int brightness = brightnessByPos(pos, led*255/ARRAY_SIZE(circleLEDIndexes), 70); setHue(circleLEDIndexes[led], ORANGE_HUE, brightness); } ws2811.sendLedData(); delay(1);
}

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

int brightnessByPos(int pos, int ledPos, int delta)
{ int diff = abs(pos - ledPos); if(diff > 127) diff = abs(256-diff); int brightness = MAX_VAL - constrain(MAX_VAL*diff/delta, 0, MAX_VAL); return brightness;
}

Pos это некоторая условная позиция светящейся точки яркость, отображенная на закольцованный диапазон 0-255. ledPos это позиция светодиода (отображенная на тот же диапазон) яркость которого нужно вычислить. Если разница позиций больше delta, то светодиод не горит, а чем ближе к позиции, тем ярче он светится.

Или вот, например, полицейский красно-синий проблесковый маяк

void policeBeacon()
{ const int RED_HUE = 0; const int BLUE_HUE = 170; static uint8_t pos = 0; pos += 2; for (int led = 0; led < ARRAY_SIZE(policeLEDIndexes); led++) { int ledPos = led*255/ARRAY_SIZE(policeLEDIndexes); int brightness = brightnessByPos(pos, ledPos, 50); setHue(policeLEDIndexes[led], RED_HUE, brightness); if(brightness == 0) { brightness = brightnessByPos((pos+100) % 256, ledPos, 50); setHue(policeLEDIndexes[led], BLUE_HUE, brightness); } } ws2811.sendLedData(); delay(1);
}

Раз уж речь зашла про машины, то и светофор тут реализовать не проблема.

Это функции, которые включают различные сигналы светофора на различных позициях

void clearPixels()
{ for(int i=0; i<NUM_HW_PIXELS; i++) { setRgb(i, 0, 0, 0); }
} void redTrafficLights()
{ for(int i=0; i<4; i++) setRgb(i, MAX_VAL, 0, 0); ws2811.sendLedData();
} void yellowTrafficLights()
{ for(int i=4; i<8; i++) setRgb(i, MAX_VAL, MAX_VAL, 0); ws2811.sendLedData();
} void greenTrafficLights()
{ for(int i=8; i<16; i++) setRgb(i, 0, MAX_VAL, 0); ws2811.sendLedData();
}

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

enum TRAFFIC_LIGHTS
{ NONE, RED, YELLOW, GREEN
}; struct trafficLightState
{ uint8_t state; uint16_t duration;
}; const trafficLightState trafficLightStates[] = { {NONE, 1}, // clear yellow {RED, 7000}, // red {YELLOW, 2000}, // red + yellow {NONE, 1}, // clear red+yellow {GREEN, 7000}, // green {NONE, 300}, // Blinking green {GREEN, 300}, // Blinking green {NONE, 300}, // Blinking green {GREEN, 300}, // Blinking green {NONE, 300}, // Blinking green {GREEN, 300}, // Blinking green {NONE, 1}, // clear green {YELLOW, 2000}, // yellow
};

Собственно функция, которая это все обрабатывает

void trafficLights()
{ static uint8_t curStateIdx = 0; static unsigned long curStateTimeStamp = 0; // Switch to a new state when time comes if(millis() - curStateTimeStamp > (unsigned long)trafficLightStates[curStateIdx].duration) { curStateIdx++; curStateIdx %= ARRAY_SIZE(trafficLightStates); curStateTimeStamp = millis(); } switch(trafficLightStates[curStateIdx].state) { case NONE: clearPixels(); ws2811.sendLedData(); break; case RED: redTrafficLights(); break; case YELLOW: yellowTrafficLights(); break; case GREEN: greenTrafficLights(); break; default: break; } // Just waiting delay(10);
}

По достижению заданного временнОго интервала включается следующий режим светофора и опять начинается отсчет времени.

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

void stars()
{ const uint8_t numleds = 5; static uint8_t ledIndexes[numleds] = {0}; static uint8_t curVal[numleds] = {0}; static uint8_t maxVal[numleds] = {0}; for(int i=0; i<numleds; i++) { if(ledIndexes[i] == 0) { uint8_t led = rand() % (NUM_HW_PIXELS+1); CRGB * leds = ws2811.getRGBData(); if(leds[led].r == 0) { ledIndexes[i] = led; maxVal[i] = rand() % (MAX_VAL-1) + 1; curVal[i] = 0; } } else { uint8_t led = ledIndexes[i]; if(curVal[i] < maxVal[i]) curVal[i]++; else if(curVal[i] == maxVal[i]) maxVal[i] = 0; else if(curVal[i] == 0 || --curVal[i] == 0) ledIndexes[i] = 0; setRgb(led-1, curVal[i], curVal[i], curVal[i]); } } ws2811.sendLedData(); delay(80); }

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

Я уже приводил значения потребления этой всей штуки. Пора подумать об экономии батареи. Вот эта функция занимается отключением питания через 90 секунд бездействия. Если не подумать об отключении питания, то светодиоды съедят батарейку за пару часов. Изначально было 60 секунд, но при реальной игре этого оказалось маловато, а 2 минуты уже как-то долго.

void shutdownOnTimeOut(bool resetTimer = false)
{ static unsigned long periodStartTime = 0; if(periodStartTime == 0 || resetTimer) { periodStartTime = millis(); return; } if(millis() - periodStartTime >= 90000UL) { periodStartTime = 0; shutDown(); }
}

Собственно отключение питания происходит так.

void shutDown()
{ clearPixels(); ws2811.sendLedData(); wdt_disable(); digitalWrite(POWER_EN_PIN, LOW); // No power after this point while(true) ;
}

Если пользователь нажимает на кнопки, то таймер сбрасывается. По истечении установленного времени функция выставляет сигнал HOLD в ноль, что является командой PT1502 на отключение питания. Watchdog, кстати, тоже остановить нужно, иначе через 2 секунды он разбудит систему и включит питание опять.

Наконец, главный цикл, который это все запускает

// List of pointers to functions that serve different modes
void (*Modes[])() = { rainbow, slidingRainbow, orangeBeacon, policeBeacon, trafficLights, stars, randomColorsFadeInOut
}; void loop()
{ static uint8_t mode = eeprom_read_byte( (uint8_t*) 10 ); static bool waitingForBtnUp = false; static long btnPressTimeStamp; // Button switches mode if(digitalRead(BUTTON_PIN) == HIGH && !waitingForBtnUp) { delay(20); if(digitalRead(BUTTON_PIN) == HIGH) { mode++; mode %= ARRAY_SIZE(Modes); // num modes clearPixels(); ws2811.sendLedData(); delay(1); eeprom_write_byte( (uint8_t*) 10, mode ); waitingForBtnUp = true; btnPressTimeStamp = millis(); shutdownOnTimeOut(true); } } // Shut down on long press over 5s if(digitalRead(BUTTON_PIN) == HIGH && waitingForBtnUp && millis() - btnPressTimeStamp > 5000) shutDown(); // Detect button release if(digitalRead(BUTTON_PIN) == LOW && waitingForBtnUp) waitingForBtnUp = false; // display LEDs according to current mode Modes[mode](); // pong shutdown timer shutdownOnTimeOut(); // Yes, we still alive wdt_reset();
}

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

5 минуты мигалка отключилась, то скорее всего после повторного включения сын захочет продолжить играть в полицейскую машину. Если ребенок, скажем, играл в полицейскую машину и через 1. Для этого выбранный режим сохраняется в EEPROM (ячейка номер 10 выбрана от балды).

Вот видео, которое показывает как это все работает.

Бутлоадер

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

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

Без этого сигнала микросхема думает, что микроконтроллер либо не завелся, либо наоборот хочет выключаться. А во-вторых, стандартный загрузчик ничего не знает про микросхему PT1502, которой неплохо было бы подать сигнал HOLD. А раз так, то через несколько миллисекунд PT1502 отрубит питание всей схеме.

В плате digispark ATTiny85 используется загрузчик micronucleus. Благо исправить обе проблемы не составляет труда. Нужно только подправить соответствующие дефайны в файле конфигурации. Этот загрузчик достаточно просто подпилить под наши нужды.

Так будет в случае чего легко откатиться на оригинальный загрузчик. Первым делом я скопировал стандартную конфигурацию firmware\configuration\t85_default в свою собственную директорию и в ней уже делал все изменения.

Из того, что предлагается из коробки нам ничего не подходит, но ближе всего вариант ENTRY_JUMPER. В файле bootloaderconfig.h есть выбор способа входа в загрузчик. В этом варианте вход в загрузчик происходит только если на определенном пине появляется определенный уровень (на плате замыкают джампер).

#define ENTRYMODE ENTRY_JUMPER

Джампера у нас нет, но есть кнопка на ноге PB2. Пускай вход в загрузчик будет происходить если при включении питания кнопку держат в течении 5-7 секунд. А вот если нажали и отпустили, то переход в основную прошивку происходит сразу.

В оригинале они все простые и реализованы макросами. Нам нужно определить 3 функции — инициализации, деинициализации и собственно проверка, а не пора ли входить в бутлоадер. У нас простыми будут только первые 2

#define HOLD_PIN PB1
#define JUMPER_PIN PB2
#define JUMPER_PORT PORTB #define JUMPER_DDR DDRB #define JUMPER_INP PINB #define bootLoaderInit() {JUMPER_DDR &= ~_BV(JUMPER_PIN); JUMPER_DDR |= _BV(HOLD_PIN); JUMPER_PORT &= ~_BV(JUMPER_PIN); JUMPER_PORT |= _BV(HOLD_PIN); _delay_ms(1);}
#define bootLoaderExit() {;}

bootLoaderInit() настраивает пин кнопки (JUMPER_PIN) на вход и выключает на нем подтяжку. Подтяжка у нас уже есть на плате, причем к земле, а при нажатии на кнопку на пине наоборот будет единица. Заодно можно и сразу сконфигурировать сигнал HOLD на вывод и выставить на нем единицу…

За пояснением битовой арифметики ходить, например, сюда, а понимание регистров настройки GPIO в контроллерах AVR можно почерпнуть, например, отсюда.

выставленная конфигурация вполне годится для последующего перехода к основной прошивке Функция bootLoaderExit() пустая, т.к.

Функцию bootLoaderStartCondition() которая отвечает за вход в бутлоадер в формат макроса уже не влезла, а потому стала полноценной функцией

#ifndef __ASSEMBLER__
// Bootloader condition is to hold the button for 5 seconds
inline unsigned char bootLoaderStartCondition()
{ long int i; for(i=0; i<10000000; i++) if( !(JUMPER_INP & _BV(JUMPER_PIN))) return 0; return 1;
}
#endif

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

Пришлось функцию поместить в блок #ifndef __ASSEMBLER__ Как оказалось файл bootloaderconfig.h участвует в компиляции ассемблерных файлов и сишный код в этом файле вызывает ошибки.

Дело в том, что во время обкатки сын часто нажимал кнопку и нечаянно заходил в бутлоадер. Еще один параметр, который я подправил, указывает бутлоадеру что делать если его не подключили к USB — выходить через одну секунду. Потому если нет соединения будем просто выходить в основную программу. Я не знаю каким чудом, но бутлоадер если не видел USB соединения мог случайным образом затирать некоторые страницы памяти.

/* * Define bootloader timeout value. * * The bootloader will only time out if a user program was loaded. * * AUTO_EXIT_NO_USB_MS The bootloader will exit after this delay if no USB is connected. * Set to 0 to disable * Adds ~6 bytes. * (This will wait for an USB SE0 reset from the host) * * All values are approx. in milliseconds */
#define AUTO_EXIT_NO_USB_MS 1000

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

# hexadecimal address for bootloader section to begin. To calculate the best value:
# - make clean; make main.hex; ### output will list data: 2124 (or something like that)
# - for the size of your device (8kb = 1024 * 8 = 8192) subtract above value 2124... = 6068
# - How many pages in is that? 6068 / 64 (tiny85 page size in bytes) = 94.8125
# - round that down to 94 - our new bootloader address is 94 * 64 = 6016, in hex = 1780
BOOTLOADER_ADDRESS = 1940

Тут я просто уменьшил стартовый адрес бутлоадера одну страницу (64 байта) тем самым увеличив место под загрузчик.

В остальном компиляция и заливка бутлоадер с помощью программатора USBAsp не составила проблем.

Заключение

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

Конечно можно. Можно ли было проще? К сожалению вот эту статью я прочитал уже после того как спаял плату. Я думаю все можно было бы сделать с помощью транзистора. Все равно DC-DC преобразователь, который есть внутри PT1502 в этом устройстве, по хорошему, не нужен. Увидел бы статью раньше — сделал бы все на том же народном TP4056 — его паять легче. Впрочем, практическое исследование микросхемы PT1502 мне пригодится для моего другого проекта, как и умение паять микросхемы в корпусе QFN20.

Напоследок вот ссылки на мой проект:

Код прошивки
Схема и плата
Модель корпуса и рассеивателя
Готовые STL модели для печати

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»