
Если вам нужно сделать гирлянду, где переливается десяток-сотня светодиодов, то эта статья будет вам мало полезна. А вот если у вас несколько десятков тысяч светодиодов и вы еще собираетесь показывать кино с их помощью — тогда вам эта информация определенно сгодится. Тем более, что других источников вы, скорее всего, просто не найдете.
Для начала — объект управления. Уже всем набившие оскомину адресуемые светодиоды, которых уже очень много разных типов. Чисто китайская разработка, все остальное, если и имеется — дешевые подделки.

Много лет назад Nokia мечтала получить светодиоды с интеллектуальным управлением. Со светодиодами тогда не срослось, но кое-что она получила — это была микросхема LP5521, которая потребляя очень мало энергии, моргала светодиодиком по программе внутри, не требуя никакого внимания со стороны основного процессора телефона.
В итоге почти все производители телефонов ставили такую микросхему и она побила все рекорды продаж. Года за 2-3 было продано 100 миллионов микросхем.
National Semiconductors разработчикам с таких барышей даже по кофточке выдал :)

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

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

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

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

Для программирования каждого пикселя нужно 24 бита — 8 бит на цвет. Управление яркостью производится с помощью ШИМ с частотой 2 кГц.

Светодиоды обычно бывают с двух типов по току — около 15-18 мА на светодиод или 5-7 мА. Для дисплеев 5мА подходят лучше, 15мА — слишком ярко. Приходится убирать лишнюю яркость за счет ШИМ. В итоге от 8 битов на цвет остается 6-7. Плюс человеческое зрение логарифмическое, а регулировка линейная. Для того, чтобы не искажать цвета при снижении яркости, необходимо использовать гамма-коррекцию.

От цветовой разрешающей способности вообще ничего не остается.

Недавно эта проблема была решена дико избыточно. Появились новые светодиоды — WS2816. У них уже 16 бит на цвет, частота ШИМ 10 кГц и впридачу ко всему встроенная гамма-коррекция. Но получается, что загружать их можно в 2 раза медленнее — при прежней скорости обмена передать уже нужно 48 бит вместе прежних 24.
Если было бы 10 бит на цвет со встроенной коррекцией — меня лично это устроило бы гораздо больше.
Еще одна проблема со светодиодами. Не знаю, обращали вы внимание или нет, но очень часто в холодную погоду изображение на рекламных дисплеях слегка краснеет. Дело в том, что световая отдача светодиодов зависит от температуры. И зависимость разных светодиодов разная. Зеленые и синие имеют более-менее одинаковую зависимость, а красные сильно другие. Если кому интересно, можете здесь прочитать, десятая страница.

Если вдруг соберетесь дисплей такого типа делать — выбирайте адекватного производителя панелей. Вот что пишут в спецификации:

Многие на это внимания не обращают, но это очень серьезно. Из-за влажности могут быть дефекты пайки. А когда последовательно стоит тысяча светодиодов и отказ любого из них приведет к отказу всей панели, причем, не сразу, а через какое-то время — панель придется демонтировать и ремонтировать. Это грустный опыт.
Теперь начнем считать. Скажем, мы хотим отображать 30 кадров в секунду. Значит, у нас есть время для загрузки линии 33.3 мсек. Частота сигнала для светодиодов — 800кГц. Для одной точки нужно 24 импульса — по 8 на каждый цвет. На загрузку пикселя уходит 30 микросекунд. Значит, за время отображения кадра можно загрузить 1111 пиксель. С учетом возможных временнЫх допусков, реально я использовал в линии до 1300 светодиодов.
Если используем готовые панели 16х16 точек — можно подключить к одной линии до 5 панелей — 1280 светодиодов. Можно и 4-мя ограничиться — будет 1024 точки.
Теперь поговорим про управление. Я с товарищем несколько лет назад делал систему управления такими дисплеями. Самый большой был вморожен в лед, размер — 9 на 9 метров, но разрешающая способность довольно низкая — 288х288 пикселей. Были дисплеи меньше размерами, но с бОльшим разрешением. Каждый микроконтроллер управлял или 8 или 16 линиями светодиодов, одновременно работало несколько контроллеров. Самый тяжелый случай был — 14 контроллеров по 8 линий. Но это была разработка на заказ, поэтому дополнительной информации по ней не будет. Кое-что без деталей реализации можно почитать здесь:
Большой дисплей. Замороженный проект.

Мне захотелось попробовать сделать управление дисплеем на 5-баксовом Raspberry Pi Pico (двухядерный Cortex-M0+ RP2040), из-за наличия возможности программирования машины состояния управления вводом-выводом на собственном ассемблере. Мне показалось, что реализовать управление таким дисплеем будет не просто, а очень просто.
Будем делать систему управления для 16 линий — этого хватит для 20480 светодиодов. Если мало — добавляем микроконтроллеры, благо копейки стоят. Для небольшого рекламного дисплея должно хватить.
Не забудьте про блок питания — если у вас светодиоды на 15мА, то понадобится чуть больше 900 ампер — обычной USB зарядки может не хватить :), все-таки почти 5 киловатт. А если взять 5 миллиамперные светодиоды — то каких-то 1.5 киловатт. Если белым светом не злоупотреблять, то мощность можно существенно уменьшить.
Код ниже — это самое важное в статье, остальное все — от лукавого и можно не читать. А этот код — это все, что действительно надо, остальное любой разработчик легко сам доделает.
Итак, код для PIO:
.define public T1 2
.define public T2 5
.define public T3 3
.wrap_target
out x, 32
mov pins, !null [T1-1]
mov pins, x [T2-1]
mov pins, null [T3-2]
.wrap
Все!
Забираем из входного регистра данные — до 32 бит, реально надо только 16. Выбрасываем в 16-разрядный порт все единицы и задержка. Выбрасываем на выход содержимое введеное из входного регистра — и задержка. И, наконец, выбрасываем нули и задержка.
Все это будет работать абсолютно независимо от процессора и сформирует сигналы управления светодиодом для всех (у нас будет их 16) линий.
Вместе с загрузкой программы PIO и настройкой выводов это будет выглядеть так (файл ws2812.pio)
.program ws2812_parallel
.define public T1 2
.define public T2 5
.define public T3 3
.wrap_target
out x, 32
mov pins, !null [T1-1]
mov pins, x [T2-1]
mov pins, null [T3-2]
.wrap
% c-sdk {
#include "hardware/clocks.h"
static inline void ws2812_parallel_program_init(PIO pio, uint sm, uint offset, uint pin_base, uint pin_count, float freq) {
for(uint i=pin_base; i<pin_base+pin_count; i++) pio_gpio_init(pio, i);
pio_sm_set_consecutive_pindirs(pio, sm, pin_base, pin_count, true);
pio_sm_config c = ws2812_parallel_program_get_default_config(offset);
sm_config_set_out_shift(&c, true, true, 32);
sm_config_set_out_pins(&c, pin_base, pin_count);
sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX);
int cycles_per_bit = ws2812_parallel_T1 + ws2812_parallel_T2 + ws2812_parallel_T3;
float div = clock_get_hz(clk_sys) / (freq * cycles_per_bit);
sm_config_set_clkdiv(&c, div);
pio_sm_init(pio, sm, offset, &c);
pio_sm_set_enabled(pio, sm, true);
}
%}
Идем сюда и воспользуемся онлайн компилятором.
Скомпилированный текст сохраняем в файле ws2812.pio.h
// -------------------------------------------------- //
// This file is autogenerated by pioasm; do not edit! //
// -------------------------------------------------- //
#pragma once
#if !PICO_NO_HARDWARE
#include "hardware/pio.h"
#endif
// --------------- //
// ws2812_parallel //
// --------------- //
#define ws2812_parallel_wrap_target 0
#define ws2812_parallel_wrap 3
#define ws2812_parallel_T1 2
#define ws2812_parallel_T2 5
#define ws2812_parallel_T3 3
static const uint16_t ws2812_parallel_program_instructions[] = {
// .wrap_target
0x6020, // 0: out x, 32
0xa10b, // 1: mov pins, !null [1]
0xa401, // 2: mov pins, x [4]
0xa103, // 3: mov pins, null [1]
// .wrap
};
#if !PICO_NO_HARDWARE
static const struct pio_program ws2812_parallel_program = {
.instructions = ws2812_parallel_program_instructions,
.length = 4,
.origin = -1,
};
static inline pio_sm_config ws2812_parallel_program_get_default_config(uint offset) {
pio_sm_config c = pio_get_default_sm_config();
sm_config_set_wrap(&c, offset + ws2812_parallel_wrap_target, offset + ws2812_parallel_wrap);
return c;
}
#include "hardware/clocks.h"
static inline void ws2812_parallel_program_init(PIO pio, uint sm, uint offset, uint pin_base, uint pin_count, float freq) {
for(uint i=pin_base; i<pin_base+pin_count; i++) pio_gpio_init(pio, i);
pio_sm_set_consecutive_pindirs(pio, sm, pin_base, pin_count, true);
pio_sm_config c = ws2812_parallel_program_get_default_config(offset);
sm_config_set_out_shift(&c, true, true, 32);
sm_config_set_out_pins(&c, pin_base, pin_count);
//sm_config_set_set_pins(&c, pin_base, pin_count);
sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX);
int cycles_per_bit = ws2812_parallel_T1 + ws2812_parallel_T2 + ws2812_parallel_T3;
float div = clock_get_hz(clk_sys) / (freq * cycles_per_bit);
sm_config_set_clkdiv(&c, div);
pio_sm_init(pio, sm, offset, &c);
pio_sm_set_enabled(pio, sm, true);
}
#endif
Теперь в основной программе инициализируем DMA — все будет работать автоматом, безо всякого участия процессора. Я использовал Platformio для компиляции.
#include <Arduino.h>
#include "ws2812.pio.h"
#include "hardware/dma.h"
#include "hardware/pio.h"
#define LEDS_IN_STRING 1280
#define DMA_CHANNEL 0
uint16_t DataArray[LEDS_IN_STRING*24];
// 30720
void dma_init(PIO pio, uint sm)
{
// (X/Y)*sys_clk, where X is the first 16 bytes and Y is the second
// sys_clk is 125 MHz unless changed in code
// we need 800 kHz - divider 156.25 x=4 y=625( 0x271)
dma_hw->timer[0] = 0x00040271;
dma_channel_config c = dma_channel_get_default_config(DMA_CHANNEL);
channel_config_set_dreq(&c, 0x3b); // 0x3b -> Select Timer 0 as TREQ
channel_config_set_transfer_data_size(&c, DMA_SIZE_16);
channel_config_set_read_increment(&c, true);
channel_config_set_write_increment(&c, false);
dma_channel_configure(
DMA_CHANNEL, // Channel to be configured
&c, // The configuration we just created
&pio->txf[sm], // The initial write address
NULL, // The initial read address - set later
LEDS_IN_STRING*24, // Number of transfers;
false // Start immediately?
);
}
void setup()
{
PIO pio = pio0;
int sm = 0;
uint offset = pio_add_program(pio, &ws2812_parallel_program);
ws2812_parallel_program_init(pio, sm, offset, 0, 16, 800000);
dma_init(pio, sm);
// здесь заполните буфер чем-нибудь сами
}
void loop()
{
dma_hw->ch[DMA_CHANNEL].al3_read_addr_trig = (uintptr_t) DataArray;
delay(50);
}
В реальном времени использовать такой дисплей вряд ли получится. Никакого входа для видео тут нет.
Отображаемые данные нужно подготовить заранее, вытащив данные из изображения, учесть место расположения каждой точки — здесь у нас не как в телевизоре — строка за строкой.

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

Раньше у меня была проблема — в гараже стояло несколько больших светодиодных панелей, место занимали. Не так давно все-таки удалось их отдать хозяевам. Естественно, спустя небольшое время такая панель понадобилось. Пришлось заказать пару дешевых панелей на Али. Размер 16х16, итого 256 светодиодов. За 10 евро неплохо — всего 4 цента за точку и паять ничего не надо. Я как-то покупал готовые ленты — там цена вообще получалась 3 цента за светодиод.
Сколько отдельно стоят светодиоды в небольших партиях (в районе 100 тысяч штук) — я точно не знаю, заказывал не я. Думаю, что в районе 2 центов.

К платке Raspbery Pi Pico понадобилось подключить только SD карточку. По хорошему, на выход нужно ставить преобразователь уровня в худшем случае или драйвер дифференциальной линии в лучшем. Но для теста программного обеспечения и так сойдет.

Новая система управления в окружении предшественников:

Совершенно случайно нашелся

Две панели — 512 точек. Панель реально маленькая для такой системы управления — на каждую линию можно подключить до 5 таких панелек, имеем 16 линий — итого 80 панелей. А у меня всего 2 :( — только попробовать хватит. Вот что получилось:

Как я говорил, яркость избыточна. Видео снималась в ярко освещенной комнате, изображение просто по глазам бьет яркостью. А камера воспринимает, как съемку в темноте.
Готовые панели выходят дешевле, но если монтировать дисплей в окно, лучше сделать свои, со множеством отверстий. Тогда помещение не превращается в темнушку, изнутри выглядит примерно как жалюзи.

Видео с одним из старых дисплеев:
В моей старой системе данные, кадр за кадром, гонятся по проводному Ethernet.
Сейчас я попробовал скопировать блок данных на SD. Вылезла обычная ардуиновская проблема — все работает абы как. Быстро чтение данных не работает, я не могу считывать 30 кадров в секунду. Все можно переделать — когда я делал оригинальную систему, то тоже использовал Ethernet библиотеки, заимствованные из Ардуино. Ethernet работал, но очень медленно. Пришлось весь код перерывать — ошибки нашлись в очень мелких деталях. Сейчас копать библиотеки просто не хочется, да и нужды нет. Я хотел проверить только, как вывод на светодиоды работает, функционирующее изделие никому не нужно. «Будет хлеб — будут и песни» — как говорил дорогой Леонид Ильич.
Ну господь с ней, с Ардуиной. Есть же компания, которая продвигает этот проект. Должна же быть у Raspberry поддержка SD карт в их SDK. Как бы не так, как они пишут:
The pico_sd_card code is SDIO only at the moment. It is not really yet in shape for prime-time unless you are feeling brave.
Файловой системой там даже не пахнет — сам прикручивай.
Нет, я не трус, но я боюсь. Бежать впереди паровоза — не самое благодарное занятие. Мне не к спеху, подожду, пока они допилят свои библиотеки. Или для Ардуино кто-то допилит, на что надежда очень маленькая. Какая-то библиотека есть, а для большинства применений достаточно и низкой скорости доступа.