Ардуина и светодиод, или как прокачать детский конструктор
Мой сын крепко «подсел» на магнитный конструктор 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 и микроконтроллером не предусмотрен. Пришлось доработать немного схему и выпаять лишние детали.
Так, теперь питание от USB поступает на ножку VIN и дальше уходит на вход зарядника. Выход зарядника (по сути аккумулятор подключается напрямую) заходит назад в плату через ножку 5V. И хотя на самом деле там будет от 3 до 4.2В (напряжение аккумулятора) это вполне нормально — диапазон рабочих напряжений микроконтроллера 1.8–5.5В. И даже светодиодный модуль нормально работает от 2.7В, хотя ниже 3.2В синему светодиоду немного не хватает и цвета немного «плывут» в желтый.
В целях экономии электроэнергии вечно горящий светодиод D2 я тоже выпаял. Общая схема теперь выглядит так
Питать схему можно было бы и через USB разъем в заряднике, но тогда бы потерялась возможность заливать прошивку через USB разъем на плате контроллера. Можно было бы оставить два USB разъема различного назначения — один для зарядки, другой для прошивки, но это как-то неправильно.
Аккумулятор размера 6×25х35 купил на ебее, но он оказался либо бракованный, либо я его убил коротким замыканием или большим током заряда (у платы по умолчанию ток заряда установлен в 1А и нужно перепаивать один резистор, чтобы уменьшить ток). В любом случае при подключении нагрузки даже в 10 мА напряжение на аккуме падало до 1В. На время тестирования я переключился на полу-сдохшую LiPo батарею от мелкого квадрокоптера. Чуть позже заказал аккум у другого продавца и он оказался хорошим.
В принципе, на этом можно было бы и остановится, припаять соединительные провода и аккуратно затолкать все в какой нибудь корпус, но я решил измерить потребление схемы. И тут я прослезился. Ладно, что в рабочем состоянии (когда лампочки сияют на полную) эта штука жрет до 130 мА, так в состоянии покоя потребление более 25 мА! Т.е. мою батарею в 600 мАч эта моргалка слопает менее чем за сутки!
Оказалось, что около 10 мА потребляют светодиоды. Даже если они не светятся — в каждом из них все равно работает микроконтроллер и ожидает команду. Т.е. нужно придумать схему отключения питания светодиодам.
Оставшиеся 15 мА потребляет микроконтроллер. Да, его можно уложить спать и согласно даташиту потребление будет измеряться микроамперами, но на деле меньше 1 мА получить не удалось. Я и АЦП отключал и пины переводил в input. Похоже где-то в схеме есть какая-то утечка, но моих скромных познаний в электронике недостаточно, чтобы ее найти и понять.
Усложняем схему
Тут я вспомнил, что я себе купил на пробу микросхему PT1502. Эта микросхема — контроллер заряда литиевого аккумулятора в комплекте с источником питания с несколькими управляющими входами. Единственная сложность — микросхема идет в корпусе QFN20 размером 4×4 мм и требует некоторой обвязки. Паять такое дома сложно, но можно. Плата получается сложной для обычного ЛУТа и нужно заказывать у китайцев. Но мы ведь не боимся сложностей, правда?
В нескольких квадратиках схему можно описать так.
В выключенном состоянии питание на контроллер и светодиоды не поступает. У устройства есть кнопка «Power», которая включает моргалку (она же переключает режимы). Светодиод сияет, скажем, минуту и если пользовательской активности нет (никто не нажимает кнопку), то устройство выключается. Т.е. не просто уходит в сон, а именно отключает само себе питание сигналом Power Hold. Причем отключает все сразу — и микроконтроллер, и светодиоды. Функциональность включения и отключения питания реализуется внутри микросхемы PT1502
Осталось всего ничего: нарисовать принципиальную схему и сделать плату. Схема, по большей части, слизана с даташита PT1502, а также модуля Digispark ATTiny85. Микросхема контроллера питания PT1502 функционально делится на несколько частей, потому на схеме разбита на блоки.
Это, собственно, контроллер заряда литиевой батареи со своей обвязкой. Светодиод LED1 показывает состояние заряда — горит, значит идет заряд. Резистор R6 задает ток заряда в 470 мА. Поскольку у меня батарея на 600 мАч, в принципе можно поднять ток и до 600 мА поставив резистор на 780–800 Ом. Впрочем я не уверен в особом качестве моей батареи — пусть лучше заряжает медленнее, но дольше проживет.
Рассмотрим схему управления питанием
Кнопка SW1 запускает всю систему — микросхема PT1502 просыпается сама и затем запускает все источники питания (которых у нее 3). Когда питание установится микросхема запустит микроконтроллер, отпустив сигнал RESET. Для удобства отладки я еще добавил отдельную кнопку Reset.
Сигнал HOLD используется для выключения всей системы. Когда микроконтроллер запустится он должен выставить на этой линии единицу. Когда пора закругляться, микроконтроллер выставляет на линии HOLD ноль и микросхема питания PT1502 остановит все источники питания.
Можно было бы еще отслеживать низкий заряд батареи с помощью вывода BAT_LOW, но в этой поделке я на это забил — никаких данных сохранять не нужно и ничего не взорвется если вовремя не заметить севшую батарею. Сдохнет так сдохнет. Но на всякий случай на плате предусмотрел контакт под это дело.
Вернемся на секунду к кнопке SW1. Я решил не делать 2 отдельные кнопки для включения и для управления. Поэтому та же кнопка подключена еще и к ATTiny85 и во время работы переключает режимы моргания. Номиналы делителя R7-R8 подобраны так, чтобы не спалить порт микроконтроллера PB2. При всех диапазонах напряжений батареи (3,3 — 4.2В) на ногу контроллера будет поступать напряжение в оговоренных даташитом пределах (0.7*VCC — VCC+0.5В)
Рассмотрим источник питания
Это импульсный DC-DC преобразователь. Напряжение на выходе задается резисторами R10-R11 и согласно формуле из даташита настроено на 3.3В. Все остальное — несложная обвязка.
По хорошему такой навороченный источник питания не особо то и нужен — можно было микроконтроллер бы вообще запитать напрямую от батареи. Просто этот источник уже реализован в микросхеме PT1502 и он может включаться/выключаться когда нам будет нужно — почему бы этим не воспользоваться?
В микросхеме также имеются 2 линейных стабилизатора, но я их использовать не буду. К сожалению, как выяснилось, подавать входное напряжение на этот источник все равно нужно, иначе микросхема думает что питание все еще недостаточно стабильно и не запускает микроконтроллер (это знание мне далось неделей перепаивания тестовой платы туда-сюда — никак не мог понять почему оно не работает)
Перейдем к логической части.
Обвязка USB слизана с платы Digispark без изменений. Это нужно для согласования напряжений USB (по которому бегает 3.3В) и сигналов микроконтроллера (который в оригинале питается от 5В). Поскольку в моем случае микроконтроллер также питается от 3.3В, то схему можно было бы и упростить, но на всякий случай я развел на плате оригинальную схему.
В обвязке микроконтроллера ничего интересного.
Финальный штрих это разъем
По сути у меня получилась такая себе отладочная плата на ATTiny85 с поддержкой USB и контроллером питания от литиевой батареи. Потому я не стал ограничиваться только выводом линии на светодиод. Вместо этого я вывел все линии микроконтроллера на гребенку — заодно и к программатору удобно подключать.
И пускай почти все линии жестко привязаны к определенному функционалу (PB1 — линия Hold, PB2 — кнопка включения, PB3/PB4 — USB, PB5 — Reset) в будущем можно будет в некоторых пределах обойти. Например, не распаивать обвязку USB и освободить линии PB3/PB4. Или, например, отказаться от ресета и освободить PB5. Ну, а пока свободным остается только PB0 — к нему и подключим наш светодиод.
Переходим к плате. Учитывая ограничения по размерам платы в 40×40 мм, количество компонентов и QFN20 корпус микросхемы PT1502, я даже не стал рассматривать изготовление платы в домашних условиях. Поэтому я сразу стал разводить максимально компактную двухслойную плату. Вот что у меня получилось
Для удобства использования на обратной стороне подписал все возможные функции выводов (идею слямзил с платы Digispark)
Плату заказывал на JLCPCB. Качеством, если честно, не очень доволен — если много раз перепаивать микросхему, то маска возле мелких контактов PT1502 чуток облазит. Ну и мелкие надписи немного поплыли. Впрочем, если все запаять с первого раза, то норм.
Для пайки 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[] = {0, 1, 2, 3, 4, 11, 12, 13, 14, 15, 8, 7};
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
Пора это оживить. Светофор работает по специальной программе, заданной в чем-то вроде байткода. В табличке описан режим и время на которое этот режим нужно включить.
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
Где-то тут закрался злобный баг. Иногда звездочки резко загораются, или наоборот резко гаснут. Но мне, если честно, лень было с этим разбираться — выглядит оно вполне нормально.
Пора подумать об экономии батареи. Я уже приводил значения потребления этой всей штуки. Если не подумать об отключении питания, то светодиоды съедят батарейку за пару часов. Вот эта функция занимается отключением питания через 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.
Если ребенок, скажем, играл в полицейскую машину и через 1.5 минуты мигалка отключилась, то скорее всего после повторного включения сын захочет продолжить играть в полицейскую машину. Для этого выбранный режим сохраняется в EEPROM (ячейка номер 10 выбрана от балды).
Вот видео, которое показывает как это все работает.
Бутлоадер
Почти все готово. Но есть еще одна штука, которую нужно подпилить — бутлоадер. Дело в том, что стандартный бутлоадер нам не подходит.
Во-первых, при включении питания он ждет целых 6 секунд — авось в него прошивку вливать начнут. Только после этого управление передается основной прошивке. Это удобно на стадии разработки, но будет раздражать в готовом устройстве.
А во-вторых, стандартный загрузчик ничего не знает про микросхему PT1502, которой неплохо было бы подать сигнал HOLD. Без этого сигнала микросхема думает, что микроконтроллер либо не завелся, либо наоборот хочет выключаться. А раз так, то через несколько миллисекунд PT1502 отрубит питание всей схеме.
Благо исправить обе проблемы не составляет труда. В плате digispark ATTiny85 используется загрузчик micronucleus. Этот загрузчик достаточно просто подпилить под наши нужды. Нужно только подправить соответствующие дефайны в файле конфигурации.
Первым делом я скопировал стандартную конфигурацию firmware\configuration\t85_default в свою собственную директорию и в ней уже делал все изменения. Так будет в случае чего легко откатиться на оригинальный загрузчик.
В файле bootloaderconfig.h есть выбор способа входа в загрузчик. Из того, что предлагается из коробки нам ничего не подходит, но ближе всего вариант ENTRY_JUMPER. В этом варианте вход в загрузчик происходит только если на определенном пине появляется определенный уровень (на плате замыкают джампер).
#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) проверяет состояние кнопки. Если кнопку отпустили раньше, то входить в бутлоадер нам не нужно. Терпеливых и настойчивых пускают дальше в загрузчик.
Как оказалось файл bootloaderconfig.h участвует в компиляции ассемблерных файлов и сишный код в этом файле вызывает ошибки. Пришлось функцию поместить в блок #ifndef __ASSEMBLER__
Еще один параметр, который я подправил, указывает бутлоадеру что делать если его не подключили к 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 не составила проблем.
Заключение
Это был весьма интересный путь от прототипа на макетке до законченного устройства. Вроде выглядит как обычная моргалка из урока по ардуине, а на самом деле в процессе работы пришлось решить целую кучу интересных проблем — тут и борьба с потреблением, и выбор элементной базы, и проектирование корпуса, и доведение до ума прошивки с бутлоадером. Я искренне надеюсь, что мой опыт кому нибудь пригодится.
Можно ли было проще? Конечно можно. Я думаю все можно было бы сделать с помощью транзистора. К сожалению вот эту статью я прочитал уже после того как спаял плату. Увидел бы статью раньше — сделал бы все на том же народном TP4056 — его паять легче. Все равно DC-DC преобразователь, который есть внутри PT1502 в этом устройстве, по хорошему, не нужен. Впрочем, практическое исследование микросхемы PT1502 мне пригодится для моего другого проекта, как и умение паять микросхемы в корпусе QFN20.
Напоследок вот ссылки на мой проект:
Код прошивки
Схема и плата
Модель корпуса и рассеивателя
Готовые STL модели для печати