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

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



Много лет назад 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 карточку. По хорошему, на выход нужно ставить преобразователь уровня в худшем случае или драйвер дифференциальной линии в лучшем. Но для теста программного обеспечения и так сойдет.



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



Совершенно случайно нашелся рояль в кустах старый переделаный источник питания, он может 60 Ампер по 5 Вольтам выдать. А двум панелям в динамике 10 ампер хватает. Системы выглядит даже как-то убого:



Две панели — 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.


Файловой системой там даже не пахнет — сам прикручивай.
Нет, я не трус, но я боюсь. Бежать впереди паровоза — не самое благодарное занятие. Мне не к спеху, подожду, пока они допилят свои библиотеки. Или для Ардуино кто-то допилит, на что надежда очень маленькая. Какая-то библиотека есть, а для большинства применений достаточно и низкой скорости доступа.

Комментарии (15)


  1. 9a75sd
    21.10.2022 21:11

    А почему именно Pico?

    Если не принципиален выбор контроллера, то предложил бы использовать ESP32, любой с подключенной PSRAM, например, ESP32S3R2, оперативки на 2 МБ псевдостатической.

    Плюс, использовать ESP-IDF или Arduino. Передавать фреймы на светики с помощью модуля RMT (их 8 в контроллере из примера), получать картинку через WiFi или SPI-Ethernet, ну или SD карты, через USB на худой конец.


    1. DustyZebra Автор
      21.10.2022 21:27
      +2

      В основном из любопытства - такой PIO не каждый день встретишь. А с ESP32 без дополнительных компонентов 16 линий подключить не получится. По крайней мере у меня не получилось даже с обвязкой, тоже пробовал ради любопытства. Для формирования видео при таком количестве светодиодов, WiFi скорости не хватит.


  1. jaiprakash
    22.10.2022 00:07
    +2

    Тот случай, когда можно начать изучать ПЛИС))


  1. axe_chita
    22.10.2022 07:13
    +1

    Спасибо за статью, было интересно.

    Нет, я не трус, но я боюсь. Бежать впереди паровоза — не самое благодарное занятие. Мне не к спеху, подожду, пока они допилят свои библиотеки.
    «Нормальные герои, всегда идут в обход!»
    Правильно, иногда надо подождать «пока всё устаканится». Или платформа поменяется. ;)


  1. jaiprakash
    22.10.2022 09:02

    Если можно, в двух словах, про предшествующие варианты.


    1. DustyZebra Автор
      22.10.2022 10:07
      +1

      В тексте есть ссылка https://habr.com/ru/post/591119/ и еще немного про другой вариант https://mysku.club/blog/aliexpress/85295.html


  1. hogstaberg
    22.10.2022 20:13

    Если вам нужно сделать гирлянду, где переливается десяток-сотня светодиодов, то эта статья будет вам мало полезна

    Ну вот неправда, у меня с прошлого нового года имеются две гирлянды по 600 светодиодов именно на pi pico. Потому что тогда и сами платы были достаточно дешевыми и сам PIO просто офигенно для таких задач подходит =)


  1. progchip666
    22.10.2022 21:49

    Ну кино на десятке тысяч адресных светодиодов с помощью расбери точно не покажешь!!!


  1. progchip666
    22.10.2022 21:59
    +1

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


  1. osmanpasha
    23.10.2022 09:40

    Значит, у нас есть время для загрузки линии 33.3 мсек. Частота сигнала для светодиодов — 800кГц. Для одной точки нужно 24 импульса — по 8 на каждый цвет. На загрузку пикселя уходит 30 микросекунд. Значит, за время отображения кадра можно загрузить 1111 пиксель. 

    А вы точно правильно считаете количество пикселей которые можно послать за секунду? Если вы используете четырехногие светодиоды, то им же требуется сигнал reset для начала посылки нового кадра, он тоже время занимает, 50 мкс по-моему. Вроде бы поэтому такие светодиоды плохо подходят для POV-экранов.


    1. DustyZebra Автор
      23.10.2022 14:03
      +1

      reset нужен перед передачей блока в линию для более, чем 1000 светодиодов, при таком количестве им можно и пренебречь.


      1. osmanpasha
        23.10.2022 19:15

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


        1. DustyZebra Автор
          23.10.2022 20:15
          +1

          Все с паузой

          На загрузку пикселя уходит 30 микросекунд.

          пикселей в пачке до 1300

          50 мкс - это чуть больше времени передачи 1 пикселя.

          Потери - примерно 0.1% времени, с этим вполне можно жить.


        1. jaiprakash
          24.10.2022 13:40

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


  1. jaiprakash
    24.10.2022 14:49

    Я правильно понял, что прошивка лежит во внешней флеш памяти и никак не защищена?