Светодиодные матрицы для "чайников" (часть 2)
Ну что, дружище, надеюсь ты освоил первую часть моего лонгрида и уже запилил свою лампу Гайвера с блэкджеком и всем остальным? Тогда вперед, через тернии к звездам, гексагонам, кругам, гирляндам.
Во многих подобных конструкциях раскладка светодиодов не имеет строк и столбцов, а порой она и вовсе хаотичная. Да и и соединены диоды не рядами, а как было удобно для разводки платы.
Возьмем, к примеру, мою новогоднюю звезду . Диоды там уложены концентрическими звездами, да еще и змейкой.
Но отчаиваться не будем, попробуем мыслить логически. У каждого диода на звезде есть реальные координаты центра в миллиметрах, или координаты на картинке в пикселях.
Мы можем создать таблицу соответствия порядкового номера диода и его координат. Изменим наш цикл обхода диодов по координатам на обход по номерам.Перепишем функцию мэппинга так, чтобы она искала координаты по индексу диода в таблице и дело в шляпе.
uint16_t mapTable[][2] = {
{ 206, 340 },
{ 181, 354 },
{ 156, 369 },
{ 132, 383 },
{ 107, 398 },
{ 89, 404 },
{ 89, 385 },
{ 95, 357 },
{ 101, 329 },
{ 107, 301 },
{ 114, 273 },
... // и так далее, для всех 180 диодов
};
struct ledCoords {
uint16_t x;
uint16_t y;
};
ledCoords mapIdxToXY( byte index ) {
return { mapTable[index][0], mapTable[index][1] };
}
void loop() {
for ( byte i = 0; i < NUM_LEDS; i++ ) {
ledCoords lc = mapIdxToXY(i);
leds[i] = effectColorByCoords(lc.x, lc.y);
}
FastLED.show();
}
На практике это оказывается не очень удачным решением. Координаты - штука относительная. У кого-то линейка в дюймах, а у кого-то 4К картинка с четырехзначными координатами. Замучаешься подгонять масштаб анимации под каждое устройство. Чтобы избежать этого выполняют нормализацию координат.
Звучит страшно, но на практике это просто пересчет в некую фиксированную систему координат. Например, давай примем за правило, что минимальная координата по X среди всех диодов это 0 в нашей "нормализованной" системе, а максимальная - 1. И все остальные координаты пересчитаем пропорционально Xn = ( x - Xmin )/(Xmax - Xmin). Аналогично поступим с координатами Y.
Теперь у нас все координаты на любом устройстве лежат в пространстве 0..1
Такой подход используется в программируемых контроллерах Pixelblaze.
Казалось бы, можно закончить занудствовать на этом, но позволь еще немного помучать тебя.
Во-первых все эти дробные координаты для контроллера являются числами с плавающей точкой (float) и он тебе не скажет спасибо за их использование. В отместку контроллер будет тратить кучу времени на расчеты даже простой арифметики, не говоря уж о корнях, степенях и тригонометрии.
Во-вторых, некоторые алгоритмы эффектов требуют расчета по всей площади, а не только в точках расположения диодов. Например операция размытия (blur) требует значения цветов всех соседних точек в пределах радиуса размытия.
Ну и в-третьих, тут уж мое личное мнение, куча дробных чисел в формулах снижает читабельность кода, а отладка такого кода вызывает у меня тихий ужас.
И тут ты подкидываешь наивную идею: "А можно как-то так чтобы координаты остались целыми, но не такими большими и более-менее схожей размерности на разных устройствах?"
Можно!
Потребуется всего лишь привести реальные координаты диодов к прямоугольной матрице низкого разрешения. Представь, что мы взяли листок бумаги в клеточку и положили поверх нашей звезды и пометили клетки в которые попали центры диодов.
Теперь у каждого диода есть простые координаты в координатной системе листочка в клеточку. И эти координаты гораздо более удобоваримые чем реальные. И если мы захотим посчитать наш эффект для каждой клеточки на этом листочке, то даже наш не супер-мощный контроллер это осилит. Попробуем записать результат в таблицу мэппинга.
byte mapTable[][2] = {
{ 12, 18 },
{ 11, 19 },
{ 9, 20 },
{ 7, 21 },
{ 6, 22 },
{ 5, 22 },
{ 5, 21 },
{ 5, 19 },
{ 6, 18 },
{ 6, 16 },
{ 6, 15 },
... // и так далее, для всех 180 диодов
}
Вроде неплохо получается.
А чтобы все алгоритмы были похожи на работу с обычной прямоугольной матрицей,можно развернуть мэппинг. Будем хранить в таблице не координаты, а индексы диодов для каждой ячейки нашей матрицы 27x23
Только как же быть с пустыми клеточками, ведь их нужно как-то пропустить при обработке? Да запиши в них просто какое-то значение индекса которое ты отловишь в цикле и пропустишь обработку. Например любое число большее общего количества диодов. И таблица примет такой вид:
byte mapTableIndex[23][27] = {
{ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 27,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 },
{ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 26, 255,
28, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 },
{ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 25, 77,
29, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 },
{ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 78, 255,
76, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 },
{ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 24, 255, 255,
255, 30, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 },
{ 255, 255, 255, 255, 255, 255, 255, 255, 255, 23, 255, 79, 117,
75, 255, 31, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 },
... // и еще 17 строк
}
Тогда функция мэппинга и рабочий цикл будут выглядеть как-то так:
byte mapXYtoIdx( byte x, byte y ) {
return mapTableIndex[y][x];
}
void loop() {
for ( byte y = 0; y < 23; y++ ) {
for ( byte x = 0; x < 27; x++ ) {
byte ledIndex = mapXYToIdx(x,y);
if ( ledIndex == 255 ) continue;
leds[ledIndex] = effectColorByCoords(x, y);
}
}
FastLED.show();
}
Конечно, хранить такие большие массивы с таблицами мэппинга в оперативной памяти не стоит, обычно их загоняют во flash используя PROGMEM.
Собираем все полученные знания в кучу и зажигаем)
(Продолжение следует, stay tuned)