Драйвер для STM32 для реализации протокола адресных светодиодов (WS2812, WS2811, SK6812, и т.д.), с рациональным использованием буферной памяти и DMA.
Ссылка на библиотеку на GitHub:
Ролик на YouTube:
Подключение и настройка
На пин DIN первого светодиода (начало ленты) подаётся сигнал, формируемый STM32. Из-за разницы питающих напряжений, сигнал следует поднять до уровня 5 вольт с помощью специальной микросхемы-транслятора логики или с помощью настройки GPIO-пина в режиме Open Drain, подтянув его резистором.
ВАЖНО!
При использовании Open Drain нужно убедиться, что пин выдержит 5 вольт. Узнать это можно в даташите на свой МК.
Пример:
Настройка таймера в CubeMX
ОГРАНИЧЕНИЯ: Из-за особенностей таймеров, минимально стабильная частота работы микроконтроллера — 32 МГц.
Сперва нужно настроить таймер в режиме ШИМ. Обратите внимание на отмеченные стрелками настройки.
Отправка значений в таймер происходит с использованием DMA, поэтому настроим и этот блок.
Ножка должна иметь наивысшую скорость из доступных. Если выбран режим Open Drain, то не забудьте переключиться.
Также проверьте, что генерация DMA_Init стоит выше, чем TIM_Init. Иначе таймер не узнает про DMA, сигнал генерироваться не будет.
Настройка библиотеки
Сгенерируем код, добавим файлы библиотеки в проект. Откроем .h-файл и посмотрим, что можно настроить.
#define WS2811 ///< Семейство: {WS2811S, WS2811F, WS2812, SK6812}
// WS2811S — RGB, 400kHz;
// WS2811F — RGB, 800kHz;
// WS2812 — GRB, 800kHz;
// SK6812 — RGBW, 800kHz
#define NUM_PIXELS 4 ///< Кол-во диодов в цепочке
// Гамма-коррекция, должна чинить красный и зелёный, пробуйте и смотрите
#define USE_GAMMA_CORRECTION 1
#define TIM_NUM 2 ///< Номер таймера
#define TIM_CH TIM_CHANNEL_2 ///< ШИМ-канал таймера
#define DMA_HANDLE hdma_tim2_ch2 ///< Канал DMA
// Канал DMA можно найти в main.c / tim.c
Function Reference
Теперь, для проверки, можно попробовать забилдить проект и посмотреть на доступные функции. Все методы возвращают enum-статусы.
typedef enum ARGB_STATE {
ARGB_BUSY = 0, ///< DMA-отправка в процессе
ARGB_READY = 1, ///< DMA Готов к отправке
ARGB_OK = 2, ///< Успешное выполнение функции
ARGB_PARAM_ERR = 3, ///< Ошибка входных параметров
} ARGB_STATE;
ARGB_STATE ARGB_Init(void); // Инициализация
ARGB_STATE ARGB_Clear(void); // Очистка ленты
ARGB_STATE ARGB_SetBrightness(u8_t br); // Установить глобальную яркость
ARGB_STATE ARGB_SetRGB(u16_t i, u8_t r, u8_t g, u8_t b); // Зажечь диод в RGB
ARGB_STATE ARGB_SetHSV(u16_t i, u8_t hue, u8_t sat, u8_t val); // Зажечь диод в HSV
ARGB_STATE ARGB_SetWhite(u16_t i, u8_t w); // Зажечь белый компонент (для RGBW)
ARGB_STATE ARGB_FillRGB(u8_t r, u8_t g, u8_t b); // Залить всё в RGB
ARGB_STATE ARGB_FillHSV(u8_t hue, u8_t sat, u8_t val); // Залить всё в HSV
ARGB_STATE ARGB_FillWhite(u8_t w); // Заливка белого компонента (для RGBW)
ARGB_STATE ARGB_Ready(void); // Получить статус DMA
ARGB_STATE ARGB_Show(void); // Обновить диоды
Пример использования
void main(void) {
ARGB_Init();
ARGB_Clear();
while (ARGB_Show() == ARGB_BUSY) ; // Вариант 1
ARGB_SetRGB(0, 255, 0, 128);
ARGB_SetHSV(1, 230, 250, 255);
while (!ARGB_Show()) ; // Вариант 2
ARGB_SetRGB(3, 200, 0, 200);
// Вариант 3:
while (ARGB_GetState() != ARGB_READY) ;
ARGB_Show();
}
Описание
Адресные светодиоды ленты используют для различной индикации, как бытовой, так и коммерческой. Ключевое отличие от обычных RGB-диодов в том, что их можно зажигать отдельно, каждый своим цветом.
Это поведение обусловлено тем, что у каждого диода стоит чип-драйвер. Снаружи, как в случае с WS2811, или внутри, как у WS2812 и остальных.
Чип принимает сигнал, запоминает первые импульсы, а остальные передаёт далее по цепочке.
Протокол данных
Свечение каждого субпикселя кодируется 8 битами. Т.е. для RGB (WS281X) 24 бита, для RGBW (SK6812) 32 бита.
Код бита задаётся длиной импульса, то есть скважностью.
Существует и код RET — пауза, означающая конец передачи.
У всех контроллеров разные тайминги:
WS2811 (slow) |
WS2811 (fast, SET=1) |
WS2812(b) |
SK6812 |
|
Частота |
400 КГц |
800 КГц |
800 КГц |
800 КГц |
Период (Т) |
2,5 мкс |
1,25 мкс |
1,25 мкс |
1,25 мкс |
T0H |
0,5 мкс (20%) |
0,25 мкс (20%) |
0,35 мкс (28%) |
0,3 мкс (24%) |
T1H |
1,2 мкс (48%) |
0,6 мкс (48%) |
0,7 мкс (56%) |
0,6 мкс (48%) |
T0L |
2,0 мкс |
1,0 мкс |
0,8 мкс |
0,9 мкс |
T1L |
1,3 мкс |
0,65 мкс |
0,6 мкс |
0,6 мкс |
Допуск |
+/- 150 нс |
+/- 150 нс |
+/- 150 нс |
+/- 150 нс |
RET |
> 50 мкс (20Т) |
> 50 мкс (40Т) |
> 50 мкс (40Т) |
> 80 мкс (64Т) |
Реализация на STM32
Большинство решений основаны на использовании пустых тактов. Это означает, что весь процессор тормозит на время отправки сигнала. Такой способ не только тратит уйму процессорного времени, но и рискует сломаться, в случае возникновения прерывания.
Посчитаем длину передачи сигнала на 1 диод: 1,25 мкс * 24 бит = 30 мкс.
Для n диодов: T = 30 * n + 50 мкс.
30 диодов — уже 1 миллисекунда.
Иными словами, протокол на задержках стоит использовать только для малого количества диодов, чтобы не мешать основной программе.
Именно из-за этой проблемы я в своё время впервые обратился к STM32.
В других вариантах используется шина SPI, которую настраивают на частоту 800 КГц. Я не проверял, но многие пишут про ощутимую потерю точности сигнала.
Что же делать?
В почти всех микроконтроллерах STM32 существует блок DMA (Direct Memory Access). Он позволяет передавать данные между периферией и памятью в разных направлениях без участия процессора.
В качестве исполнительной периферии используется таймер, настроенный в режиме ШИМ.
Буферный массив
Любой способ передачи сигнала подразумевает буфер, в котором хранятся значения скважности сигнала.
В сети встречаются множество вариантов буфера сразу под все диоды. Чаще всего скважность 8-битная, поэтому такой будет весить N диодов * 24 байт. Уже под 100 диодов он займёт более 2 КБ ОЗУ.
А если записывать скважность с шириной 32 бита, как требуют некоторые серии МК, под 100 диодов буфер будет более 9 КБ.
Реализация моего метода была придумана не мной. В ней очень хитро используется память.
Буфер здесь двойной. Первый имеет размер N диодов * 3 байта. В нём хранится цвет в представлении RGB.
Второй буфер — для скважностей. Он фиксированный, занимает всего 48 байт, или 64 байта для RGBW. В него вмещаются всего 2 диода.
Прерывания DMA позволяют заполнять одну часть буфера, пока отправляется вторая. Используя такой подход, можно растягивать цепочку диодов почти до бесконечности, покуда хватит памяти для первичного буфера или частоты обновления.
Преобразование логики
Дело в том, что адресные диоды воспринимают сигнал, опираясь на напряжение своего питания.
Открыв даташит на WS2812b, мы увидим такие строки:
Min |
Max |
|
VIH |
0,7 VDD |
— |
VIL |
— |
0,3 VDD |
Это — границы восприятия сигнала. Иными словами, при питании от 5.1—5.2 Вольт, минимальный уровень сигнала — 3.57 Вольт.
Так как STM32 выдаёт сигнал величиной 3.0—3.3 Вольта, его нужно увеличить.
Вариантов это сделать несколько:
-
Уменьшить напряжение питания ленты
Отрегулировать напряжение на БП
Для небольшого отрезка запитать всю ленту через диод
Отрезать первый светодиод, и запитать только его через диод
Поднять потенциал GND микроконтроллера (подробнее)
Воспользоваться преобразованием логики
Так как предполагается использование в коммерческих проектах, где необходима не только надёжность, но и возможность беспроблемной замены отдельных компонентов пользователем, то самый безопасный вариант — последний.
Способы преобразования логики были рассмотрены в данной статье. В ней сделаны выводы о том, что самый подходящий преобразователь — SN74LVC.
Однако, при его отсутствии или для удешевления BOM, можно воспользоваться режимом Open Drain.
Обход буфера
DMA настраивается в кольцевом режиме передачи. Новые транзакции будут возникать до тех пор, пока не будут остановлены вручную в коде.
DMA генерирует прерывания каждую половину транзакции. Поэтому наш буфер размером в 2 диода. Пока идёт передача сигнала для первого диода, просчитывается и загружается сигнал для второго.
1-я половина |
2-я половина |
Счётчик |
LED [0] |
LED [1] |
0 |
LED [2] |
LED [1] |
1 |
LED [2] |
LED [3] |
2 |
LED [4] |
LED [3] |
3 |
LED [4] |
RET {1} |
4 |
RET {2} |
RET {1} |
5 |
RET {2} |
DMA_STOP |
6 |
Состояние буфера. Bold — текущая передача
Проблемы при разработке
В первую очередь я столкнулся с согласованием логики. От USB компьютера всё работало, а от любого блока питания — нет. Решение пришло после пары тыков вольтметром и чтения даташита. Оказалось, что порты компьютера под просадкой выдавали порядка 4.6 Вольт, что есть 3,2 Вольта логической единицы. А все блоки питания стандартно выдавали в районе 5.2 Вольт, поэтому лента даже не зажигалась.
Вторую проблему принесла библиотека HAL. Дело в том, что у базовых таймеров нет IDLE-состояния ног. Поэтому, после остановки таймера, ножка входила в Z-состояние, а подтяжка выкидывала сигнал вверх.
На осцилограмме видно последовательность: сигнал, RET (пауза), Z-state, запуск таймера, сигнал, RET.
Это приводило к тому, что первый диод в ленте считывал этот импульс и зажигался.
Даже с помощью остановки-запуска таймера невозможно было достичь нормальной работы. Либо из-за массивности HAL-функций, либо из-за особенностей работы периферии, возникала небольшая временная задержка, которой было достаточно для зажигания этого диода.
Решение было не таким очевидным, но нашлось довольно быстро. В функции HAL_TIM_PWM_Stop_DMA
была обнаружена такая строчка:
/* Disable the Capture compare channel */
TIM_CCxChannelCmd(htim->Instance, Channel, TIM_CCx_DISABLE);
Это и есть отключение GPIO-канала таймера. После её удаления, удалось достичь стабильной работы. Поэтому пришлось скопировать весь код этого метода к себе и немного отредактировать.
Третья проблема — фундаментальная. Заключается в особенностях работы таймеров. Если задать частоту ниже 32 МГц, то ощутимо теряется точность сигнала. Например, для 8 МГц: Для получения частоты 800 КГц задаётся ARR = 9. Значит регистру CCRx доступны только значения 0..8. А это примерно 100 КГц точности или разброс в 10 мкс, что уже очень критично.
Вариант борьбы с этим — уничтожение этой концепции генерации сигнала, переход на ассемблерные задержки или прерывания.
В любом случае, зачастую в проектах используют максимальную тактовую частоту, а снижают её только для энергосбережения, когда светодиоды уже должны быть выключены.
Другой вариант — использование отдельного МК, например F0 или G0, как UART/SPI/I2C -> ARGB драйвер. Такие проекты уже существуют.
Мой выбор — принять все ограничения, а для open-source сделать пометку.
Оценка скорости
Максимальная частота обновления адресной ленты упирается напрямую в протокол. Посчитаем предел для 25 FPS.
25 Гц -> 40 мс = 40.000 мкс. Передача для 1 диода занимает 30 мкс.
Таким образом, предельное значение — порядка 1300 шт.
Ссылки
Комментарии (23)
Alexandr_Fedotov
10.05.2022 04:47Транслятор уровней не обязателен. Этот адресный светодиод нормально воспринимает сигнал 3.3В. В даташите можете посмотреть в таблице минимальных и максимальных уровней лог. 1 и лог. 0. Так что можно спокойно настраивать ножку микроконтроллера как push-pull и таких завалов фронтов не будет.
GennPen
10.05.2022 07:33+1В даташите как раз написано "0,7 VDD". И я, вероятно, как и вы думал что это 0.7В, но на деле это оказывается 0.7 * VDD = 0.7 * 5В = 3.5В, что уже не укладывается в 3.3В. И об этом примерно в середине статьи описывается.
Alexandr_Fedotov
11.05.2022 12:28Сейчас глянул даташит на свежие WS2812, и там действительно указано 0.7VDD, я когда этими диодами занимался, у меня был даташит, где были указаны пороги именно в напряжении максимальном и минимальном. Так вот там пороги были как у ттл логики. Я написал комментарий по этой теме исходя из личного опыта. У меня адресная лента с такими светодиодами при питании светодиодов от 5 вольт, а питании МК от 3.0Вольт, прекрасно работала.
"Бюрократически" я не прав, каюсь. Но вот на практике я испытывал, и все работало. Но лучше все делать так как указано в даташите, подавая напряжение на вход в указанных рамках.
Но опять же, если верить на 100% даташиту, и реализовать все по даташиту, то эти светодиоды не заработают. Я реализовал протокол на таймере с DMA, и долго не мог понять почему не работает, почему светодиоды совсем не светятся... Хотя по осциллографу все временные интервалы и задержки строго по тому как описано в даташите. Оказалось что параметр "reset", который должен быть по даташиту более 50мкс, на самом деле должен быть более 100мск... А я ставил 80мкс, и светодиоды не принимали команду...yasnolesskiy Автор
11.05.2022 23:18Вероятнее всего (ИМХО) диоды разные бывают.
Под общей торговой маркой каждый подвал что-то своё делает. Мы же не знаем при заказе с алика, оригинальный ли там чип именно от Worldsemi.
Поэтому само собой характеристики будут отличаться от эталонных
https://www.youtube.com/watch?v=mg7p_1fIyy4
Antares1991
10.05.2022 04:47+1Уже давно для адресных светодиодов использую SPI (сигнал MOSI) +DMA. Так экономится память, т.к. одним байтом SPI можно передать сразу 2 бита WS-ки. Предделитель у SPI не такой продвинутый, как у таймеров, но в требуемые тайминги получается уложиться. Главный момент - в конце всегда надо передавать 0x00, иначе на линии MOSI установится высокий уровень от старшего бита последнего переданного байта
yasnolesskiy Автор
10.05.2022 04:55В бытовом использовании думаю да, имеет смысл. Право на жизнь имеет, я не спорю =)
НО, SPI всё ещё расходует 2 ножки, что может быть критично на мелких корпусах, если МК как буферный драйвер используется.
Да и SPI как канал управления можно использовать, он шустрее чем UART и I2C будет. В некоторых камнях он всего один, что тоже критично.DSarovsky
10.05.2022 09:26+1Так с SPI в этом смысле как раз никаких проблем не будет (в отличие от i2c, например, если бы его так же применить для сторонней задачи): снять СS со всех устройств и гнать по шине все, что угодно. Или в реальных задачах надо беспрерывно менять состояние диодов?
makkarpov
10.05.2022 11:42Так у светодиодов CS нет, когда вы начнете общаться с другими устройствами, светодиоды могут вас и не понять. Или ставить И-НЕ перед светодиодами, чтобы выключать сигнал для них.
Antares1991
10.05.2022 18:49+2В случае с STM32 можно ремапить SPI MOSI между двумя пинами - с одного дорожка уходит на ленту, с другого - на остальные SPI устройства, нужно только ещё руками доинициализировать GPIO, чтобы при переключении с альтернейта на гпио на линии был правильный уровень
Antares1991
10.05.2022 10:49+2В STM32 можно настроить GPIO так, чтобы от SPI только MOSI наружу торчал, но для этого надо в код, сгенеренный кубом, ручками влезть) И при некоторых ухищрениях и с некоторыми ограничениями можно SPI, используемый для ленты, шарить между другими устройствами. Но тут уже надо по-хорошему контроллер под задачу выбирать, а не наоборот
agalakhov
10.05.2022 16:40+2Я управляю WS2812 непосредственно через UART. Мой крейт для Rust:
https://crates.io/crates/ws2812-uart
Принцип управления тот же, что и на SPI, требуется всего один пин. Сигнал следует инвертировать (STM32 умеет). Старт-бит и стоп-бит UART используются как часть сигнала.Antares1991
10.05.2022 19:04Хммм... Очень интересно, честно говоря, не додумался до такого... Надо испробовать, ибо иногда бывает, что SPI не хватает, а уарты простаивают... Хотя так не получится работать с лентами с синхронным интерфейсом, но они и встречаются сильно реже
DSarovsky
10.05.2022 20:17UART, видимо, оправдывает букву 'U' в своём названии:) Через него, как оказалось, удобно также реализовать 1-wire путем включения полудуплексного режима.
av-86
10.05.2022 19:47+1Ещё этим способом с таймером и DMA музычку гнать можно с какой-нить флешки: https://habr.com/ru/post/599629/
Chuvi
10.05.2022 20:07Разрешите докопаться?
Все методы возвращают enum-статусы.
Для чего? Давайте взглянем, например, на метод ARGB_Clear. В нём вызывается либо ARGB_FillRGB либо ARGB_FillWhite. Но их возвращаемые значения утрачены. Они никуда не сохраняются. Даже если вызванная функция как-то сообщит о проблеме, всё равно вернётся ARGB_OK. В чём тогда смысл тратить такт на запись возвращаемого значения в регистр, когда можно просто вернуть void?
yasnolesskiy Автор
10.05.2022 21:13Согласен, косяк, проглядел видимо. Спасибо, что обратили внимание
Issue создал, позже поправлю
Такой подход я вообще в ST-шном HAL-е подсмотрел. Долго думал, имеет ли он смысл, пришёл к выводу, что имеет, при использовании RTOS, например.
Тут вопрос, конечно, к программисту, как он логику тасков будет прописывать, но с общим ресурсом-статусом есть риск ошибиться
Ну и оптимизировать 1-2 такта, используя при этом HAL, как-то странно )
GennPen
Сколько подключал WS281*, прекрасно работают и от 3-вольтовой логики
(ниже уже прочитал в чем проблема)
yasnolesskiy Автор
Это ещё от дохлости ЛДО или нагруженности цепи питания МК зависит. Бывает, что до <3.0 Вольт просаживается, тогда точно не работает
Antares1991
А что Вы используете для согласования уровней? Я обычно использую обычную (HCT/ACT) логику с ТТЛ-входами, интересно узнать, что другие используют)
iiiytn1k
74HCT245 (и производные от неё, типа 74LVC4245) сам б-г велел. LVC245 можно надёргать с плат от старых струйников с LPT-портами, но у неё питание до 3.6В.
HC в принципе тоже нормально работает, так как Vih для нее заявлен typ. 2.4V при Vcc 4.5V. Хотя при этом же Vih завлен 3.15V (т.е. Vcc x 0.7). Но, тем не менее, работает стабильно.
Для преобразования одной-двух линий раньше пользовался транзистором с резисторами, но потом стал использовать одноканальную логику в корпусах SOT-353, типа NL17SZ17, NC7S14 и пр. Вообще их там в каталоге несколько десятков разновидностей: инвертирующие, неинвертирующие, с триггером Шмитта; c CMOS выходом, 3-state, open-drain, с TTL-входом и т.д.
Antares1991
Ну, с HC - это прямо пограничное состояние, при совпадении обстоятельств может быть фейл
yasnolesskiy Автор
Я целый тест разных преобразователей провёл.
Статья на сайте лежит, как до ума доведу (+ перетест сделаю), тут тоже появится
https://crazygeeks.ru/log-level-test/#Микросхемы_преобразователей