Драйвер для 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 шт.
Ссылки
Автор: Дмитрий