Raspbery Pi Pico для управления адресуемыми светодиодами

65utlwa-_8zny_cb5me0avlvub8.jpeg

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

sy6rya7olklhhmxl0g7iehgs1qy.jpeg

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

p9ngtfritj9ltaqwgh1n4kpc2cg.jpeg

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

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

8z2seavffklaeyvjf5wyngnh1tc.png

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

wl9da2uhhwmk8jin1y36k37s3sk.jpeg

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

-ulbnctl9mfbhxnnf1m-ckqe2go.png

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

kuqcszcgyapab3yzqjckwx5muai.png

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

f6rzpzwqfww6x-bqyyirl7o_xdk.png

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

-jloqnwk58lujcp6_7nc37wkh0c.jpeg

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

8yhicfzrzdd8aecpxsjujmleykq.png

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

Еще одна проблема со светодиодами. Не знаю, обращали вы внимание или нет, но очень часто в холодную погоду изображение на рекламных дисплеях слегка краснеет. Дело в том, что световая отдача светодиодов зависит от температуры. И зависимость разных светодиодов разная. Зеленые и синие имеют более-менее одинаковую зависимость, а красные сильно другие. Если кому интересно, можете здесь прочитать, десятая страница.

_votzrqz2zhfuqxn-pvogarfd20.png

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

wyi1h9gkjddafp3tbgkgke4kdbi.png

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

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

Если используем готовые панели 16×16 точек — можно подключить к одной линии до 5 панелей — 1280 светодиодов. Можно и 4-мя ограничиться — будет 1024 точки.

Теперь поговорим про управление. Я с товарищем несколько лет назад делал систему управления такими дисплеями. Самый большой был вморожен в лед, размер — 9 на 9 метров, но разрешающая способность довольно низкая — 288×288 пикселей. Были дисплеи меньше размерами, но с бОльшим разрешением. Каждый микроконтроллер управлял или 8 или 16 линиями светодиодов, одновременно работало несколько контроллеров. Самый тяжелый случай был — 14 контроллеров по 8 линий. Но это была разработка на заказ, поэтому дополнительной информации по ней не будет. Кое-что без деталей реализации можно почитать здесь:
Большой дисплей. Замороженный проект.

cgwkvxljpogscpcytgfxusr_mwc.jpeg

Мне захотелось попробовать сделать управление дисплеем на 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; ipio_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

Теперь в основной программе инициализируем DMA — все будет работать автоматом, безо всякого участия процессора. Я использовал Platformio для компиляции.

#include 
#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);
}

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

evb9ugzn4s0bruhy4thdkxnpywi.png

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

tirlxr5jv43yfzad1gnivoliafu.png

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

dcmxgsdpgae5jxojfxp4wbjbukk.png

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

ggt5zpy8cot3gnftnzsz0d2l77i.jpeg

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

eda3_0qxpyhhzya4d0wvofibisu.jpeg

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

crgtvjldzygml2rgmbtyd0b6epi.jpeg

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

f3zdia1wp-h813-1al6iuwmzrpw.gif

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

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

lps6z0ybsz7ihqy3islhw8tlucm.png

Видео с одним из старых дисплеев:

В моей старой системе данные, кадр за кадром, гонятся по проводному 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.

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

© Habrahabr.ru