WS2812B, STM32F103C8T6 и цветовая схема HSV.



Расскажу о том, как дружил STM32F103C8T6 c WS2812B через RGB и HSV, и том зачем это было нужно. Исходники прилагаются, красивых картинок не будет.

Ловко проведу по граблям, на которые в этом процессе наступил, любезно предложу опереться на созданные костыли и великодушно подсажу на собственноручно изобретённый велосипед.

Собственно была поставлена цель: сделать гирлянду на ёлку, и для этого написать на «таблетку» прошивку, которая бы позволяла работать с лентой практически любой длины. При чём «работать» означало «выводить анимацию не зацикленную короткими интервалами, а ту, что можно будет как-то рассчитать в МК». Такой подход позволяет создавать неповторимую анимацию как на ленте, так и, нарезав её полосками, создать из неё «экран». И такие экраны в интернете я потом увидел. Вот пример: https://www.youtube.com/watch?v=POMQOsVfDEM .

И пока я не углубился в дерби реализации сразу укажу на свою ошибку номер один. Не берите ленту в роли гирлянды! Я купил ленту в силиконовой оболочке (чтобы она была защищена от случайного замыкания на ёлке). Вот только оказалось, что чипы на ленте перед изломом беззащитны! Лента годится только для раскатывания по ровной поверхности. На гирлянду берите чипы на проводках (собственно гирлянду). Однако в плотном расположении чипов на ленте есть свой шарм, что меня и склонило в своё время в пользу ленты. Так вот, сломанный на моей ленте чип пропускал через себя сигнал странным образом: заливка красным или зелёным всей ленты работала прекрасно, а попытка залить любыми другими цветами приводила к совершенно непредсказуемым результатам. И я, виня во всём свои кривые руки, долго искал ошибку в коде (даже генетическом), пока не понял, что оболочку надо вскрыть, а чип выпаять и заменить. После выпаивания чип сразу развалился пополам. Я с лентой обращался как с хрустальной, но всё равно не могу с полной уверенностью заявить, что он сломался не по моей вине.

К реализации. Общение с лентой решил реализовать через связку DMA->Timer->PWM. И тут совершил ошибку номер 2. На плате рядом находятся выводы GND, +5V, B9. Такой соблазн был воткнуть их вместе в один разъём и подсоединить его прямиком на ленту! Вот тебе сразу и земля, и питание, и сигнальный вывод. А вот нет! Нет канала DMA, который бы позволял регулировать длительность импульса импульсов Tim4_Ch4! Период — да, длительность импульса — нет. На поиск ответа почему не работает DMA – PWM так, как хочется, времени по зелени потратил не мало. Брать нужно тот таймер и канал, который может принимать данные от DMA. Например, Tim3 Ch3.

Определившись с методой общения с лентой надо решить, а как, собственно, анимацию считать / хранить? Каждый бит данных ленты — это байт (хватит и одного) который определяет длительность импульса импульса для таймера. 1 СД = 24 байта, 300 СД = 7200 байт, а менять данные в буфере, на лету, если из него же сейчас данные выдаются не ленту — неправильно, значит буфер должен быть двойным! (Благо в даташите упоминается, что есть прерывание на достижение середины буфера.) А это уже 14400 байт! А если диодов больше? А памяти всего 20КБ…

Было решено сократить количество СД в странице буфера до 10-50, оставить две страницы. А сам кадр хранить в более компактном формате целиком. А можно и два кадра, один выводим, второй — считаем, как на видео картах.

Какой формат будет более компактным? На ум сразу приходит RGB. Лента RGB, формат RGB, полное побитовое соответствие, и видимая простота реализации. Если подумать об экране, то картинки опять таки из RGB выводить на него будет удобно. Короче, RGB быть! Однако, поскольку изначальная цель была — расчёт анимации, создание динамических и масштабируемых эффектов — оказалось, что RGB для этого дела не очень-то подходит. Как внятно написать в терминах RGB пульсацию яркости? Как её вообще регулировать? А как сделать правильную радугу и растянуть её на произвольной длины ленту? Изучение вопроса привело меня в пространство HSV (H — цвет, S — насыщенность, V — яркость). Ещё посматриваю в сторону HSL, но это отдельная тема.

Итак, описание алгоритма, имеем: двух-кадровый буфер с анимацией. Один — текущий, второй рассчитывается. Текущий кадр кусками по прерываниям выгружается в одну из двух страниц, откуда DMA берёт длительность импульса. Всё то время, от того момента, как прерывание закончит подготовку следующей страницы, до возникновения очередного прерывания доступно для расчёта нового кадра. Время выполнения прерывания DMA определяется числом СД на одной странице.

Ну и последнее лирическое отступление и третья моя ошибка, исправленная и преодолённая. Творить решил с помощью модных и мало известных мне инструментов: CubeMX, HAL… Я говорю не об их качестве (кто я такой, чтобы судить!), а о полном отсутствии у меня опыта на тот момент работы с ними. Я наивно полагал, что если в сгенерированных процедурах инициализации, в отведённых местах, допишу свои правки, то они никуда не денутся. Как бы не так! Нужно очень внимательно изучать код библиотеки, которой пользуешься (особенно при такой документации), а с большой и серьёзной библиотекой это может быть непросто. А в Кубе есть куча разных настроек для того же таймера. И очень важно реально понимать, что к чему. Я готов был мышке хвост зубами откусить, когда не понимал, почему лента плюётся в меня какими-то случайными цветами, а не тем, что я в неё посылаю при стандартных, правильных по паспорту таймингах, но начинает работать адекватно, если все тайминги завысить (а, значит, скорость передачи данных занизить) в 10 раз.

Не работает лента WS2812B? Проверь настройку GPIO output speed для пина вывода PWM сигнала. Должна стоять в максимум. Я до этого дошёл цистерну кофе спустя, после того, как плюнув на всё, написал ручками весь код на SPL. И внезапно многое заработало как и было задумано!

С лирикой всё. Дальше только пошаговые инструкции и код. На высокий уровень не претендую, пишу как умею.
Для тех, кому важно, CubeMX v 4.27.0, STM32Cube MCU Package for STM32F1 Series Version 1.6.1.

<здесь должна была быть картинка 1>

Тактируемся от 8МГц кварца через PLL с умножением до 72 МГц.

 

Настраиваем Таймер.
Вкладка DMA: получатель TIM3_CH3, память-в-периферию, режим циклический, инкремент памяти по байту.
Вкладка константы (люблю их): T_PERIOD 90 (отсчёт таймера), T_RESET 0 (режим сброса на сигнальной линии), T0H 26 (бит 0), T1H 65 (бит 1).
Вкладка параметры: Устанавливаем период счёта T_PERIOD-1
Вкладка GPIO: скорость на максимум (см моя ошибка № 3).

Всё, генерируем проект.

Для ясности определю два термина, которые я всё время использую:
«страница» — часть буфера из которого по DMA берёт ширину импульсов таймер;
«кадр» — собственно кадр, буфер, где хранятся сведения о цвете LED-точек.

Далее понадобятся

  • набор функций для работы с лентой и подготовки буфера — файл ws2812b.c и заголовочный к нему ws2812b.h,
  • набор функций для работы с выбранным цветовым пространством RGB / HSV — color_schemes.c, и заголовочный color_schemes.h,
  • коллекция анимаций в виде однотипных функций — animations.c, animations.h,
  • ну и заготовка предустановленных цветов, чтобы было удобно — colors.h

Связываем всё это вместе в ws2812b.h, включаем его в main.h и понеслась.

 

 

Цветовые схемы.

scheme_colors.h объявляет те схемы, которые я использую.

 

 

Порядок каналов в структуре RGB таков потому, что так удобнее побитовым сдвигом извлекать и подавать в ленту данные. Канал alpha нужен больше для выравнивания структуры до 4 байт. Преобразование структуры в uint32_t и обратно оказалось довольно удобно. А вот попытка использовать alpha канал для регулирования яркости горения СД — нет. Ну и здесь же пара говорящих самих за себя функций про RGB.

 

 

С цветовой схемой HSV стоит самостоятельно ознакомиться подробнее в интернете. Но если кратко, H задаёт цвет на радуге, S — его интенсивность от разбавленного в 0, до концентрированного 255, V — яркость (0 не горит, 255 сияет во всю). H-радугу условно можно поделить на 6 секторов, и размер сектора определяет, сколько цветов можно задать. H_SECTOR_SIZE 256 выбрана не просто так. С одной стороны это очень много цветов, с другой стороны на 2^8 МК умеет делить быстро. А при преобразовании HSV в RGB ему это делать придётся. Ну и пара говорящих за себя функций.

Добавляю в ws2812b.h объявления длины ленты (оно же размер кадра), размер буфера кадров, длину страницы DMA, и размер двустраничного буфера DMA. Декларирую существование буферов.

 

 

Поясню про типы bit_unit_t и led_unit_t. Они появились из экспериментов с типами и размерами единицы данных в буферах. Оказалось, что сохранение этих типов даже несколько облегчает чтение программы человеком. Ну и публикую прототипы функций.

В файле ws2812b.c реализация этих функций и ряд служебных. В основном они очень просты для понимания, но некоторые я поясню. Хотел использовать систему флагов, но в итоге полезными оказались только два флага:

  1. расчёт следующего кадра окончен — можно курить бамбук, пока он не понадобится;
  2. при транслировании кадра в задержки достигнут конец кадра — на линии данных выставляем сброс, проверяем готовность следующего кадра, и если он готов, переключаемся на него и считаем новый.

По сути оба этих действия осуществляются в функции UpdateDmaPage.

 

 

Она вызывается из прерываний DMA при достижении середины и конца буфера, т. е. кончилась первая страница — её можно переписывать новыми данными из готового кадра, кончилась вторая — пишем её. И так, пока весь кадр не будет исчерпан. После чего очередную страницу заполняю сигналом сброса, или даже не одну. Сброс можно держать, пока новый кадр не будет готов. У меня таких долгих расчётов кадров не было.

 

 

Структура RGB_t такая, как я описал, не просто так. Её запросто можно превратить в 4х-байтный int и накладывая на него битовую маску из одного ползущего бита снять с неё (со структуры) значения всех 24 задержек в страницу DMA, и на выходе дать указатель, откуда следует продолжить.

Остальное, мне кажется здесь достаточно тривиальным.

Перейдём к анимациям.

Их пока не много. Кому интересно, несколько больше проектов есть в архиве, там я пробую и моделирую различные эффекты на веб странице. В файле animations.h есть только указатель на функцию анимации, именно через него она всегда вызывается в CalcNextFrame(), и ряд доступных анимаций. Функция анимации принимает два параметра: 1-й указатель на кадр, в который можно писать, 2-й указатель на предыдущий кадр по сути, на тот случай, если оттуда что-то понадобится прочитать.

Можно рассмотреть функцию создания плывущей радуги

 

 

getNextRainbowColor — это вспомогательная для определения следующего цвета с заданным шагом. А сама радуга строится теперь просто — весь диапазон значений H в модели HSV — готовая радуга. Надо лишь растянуть её на всю ленту, определив шаг разности цвета между ближайшими сведодиодами:

 

 

рассчитать цвет первого в ленте, а все остальные уже с заданным шагом отступая от предыдущего.

 

 

такой расчёт цвета первого СД позволяет задать радуге плавное движение-перелив. Яркость установил жёстко, в четверть максимума (0x40), большего на ёлке мне не надо. Но можно легко установить светимость в максимум (0xFF), или же пускать по радуге волну, например.

Осталось всего-ничего, в сгенерированных файлах прописать вызовы нужных функций.

В main нужно немногое. После инициализации периферии добавим вызов инициализации буферов и всякого, что сейчас создадим, запустим таймер в связке с DMA, и бесконечный цикл сунем расчёт кадров.

 

 

В dma.c написать две функции, которые будут вызываться по прерываниям на середине буфера (конец первой страницы) и в конце его (конец второй страницы).

 

 

прототип первой функции объявим ещё и в dma.h, он сейчас понадобится

 

 

а вторая — это переопределение HAL'овской, которая объявлена как __weak. Про это я тоже сейчас поясню. Осталось последнее: сообщить DMA, что по обозначенным событиям надо что-то делать. Об этом сообщим в файле tim.c: в функцию HAL_TIM_PWM_MspInit добавим всего одну строчку:

 

 

Спросите а как же, hdma_tim3_ch3.XferCpltCallback ? Он же должен вызываться по окончании буфера? Это один из камней, о который я споткнулся. Это поле будет переписано при вызове HAL_TIM_PWM_Start_DMA в main. И чтобы поучаствовать в этом коллбэке надо переопределить HAL_TIM_PWM_PulseFinishedCallback, что и было сделано выше.

Всё. Можно собирать и заливать. Любуемся плавной радугой. А главное — МК теперь знает о RGB и HSV. Дальше можно делать многое!  Хоть мультики в окно показывать!

 

 

Исходный код

 

 

Метки: , , , ,

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

*