Перейти до основного вмісту

🚦 Неопікселі - Драйвер

В попередній статті ми розглядали протокол керування адресними світлодіодами на прикладі неопікселів WS2812B та МК STM32G030. Сьогодні ж давайте напишемо вже повноцінний драйвер для зручнішого керування та з кількома простими анімаціями для неопікселів. Нажаль на руках світлодіодної RGB стрічки у мене немає, але є 2 матриці 8 на 8, з'єднаних послідовно. Тому писати будемо для них, хоча якоїсь принципової різниці зі стрічками немає.

Давайте коротко пригадаємо принцип роботи примітивного драйвера з попередньої статті. У нас був DMA масив з наступною довжиною: кількість неопікселів х 24 біти кольору + біти (нулі) для RESET сигналу. В цьому масиві були записані уже готові значення ширини імпульсів, які потрібно було лише передати на таймер, який в свою чергу сформує сигнал і неопікселі WS2812B засвітяться певним кольором:

  • 1/4 заповнення - логічний 0
  • 3/4 заповнення - логічна 1
  • 0 заповнення - сигнал RESET (або відсутність сигналу)

Тобто на 24 біти кольору для одного неопікселя потрібно було відправити 24 імпульси шириною або 25% або 75% заповнення і в кінці багато 0% імпульсів (сумарною тривалістю 280+ мікросекунд) для сигналу RESET, який дає зрозуміти неопікселю, що потрібно відобразити щойно отриманий колір. Якщо потрібно засвітити наприклад 2 неопікселі, то потрібно відправити вже не 24, а 48 імпульсів і в самому кінці так само сигнал RESET. Ось і все - це весь протокол керування.

🔢 Масив з кольорами

Але керувати неопікселями через такий масив не дуже зручно. Легше і зрозуміліше було б оперувати просто масивом кольорів, де кожен елемент масиву буде означати колір конкретного неопікселя. Тому створимо такий масив з кольорами, кожен з яких потім буде конвертуватися в 24 імпульси в DMA масив. Отже, кожен неопіксель має 24-бітний колірний формат (8 біт на кожен з 3-ох кольорів - RGB). Найближчий тип даних, в який може поміститися 24 біти це 32 біти (uint32_t). І якщо у нас буде 2 світлодіодні матриці по 64 неопікселя, то це означає, що нам знадобиться масив розміром 512 байт (264328){\large \left(\frac{2 \cdot 64 \cdot 32}{8}\right)}.

🔗 Таймер, DMA + Колбеки

Масив в 512 байт з кольорами це "сирі" дані, які не готові до відправлення на таймер. Нам потрібен ще один (проміжний) масив, який буде містити безпосередню ширину імпульсів для неопікселів. На щастя його розмір ми можемо зробити набагато меншим за 512 байт. В попередній статті ми використовували 2 периферії STM32 мікроконтролеру:

  • таймер - для формування імпульсів потрібної ширини
  • DMA - для передачі інформації про ширину імпульсів на таймер

Сьогодні їх так само використаємо, але додатково застосуємо колбеки для DMA, які допоможуть нам зекономити значну кількість пам'яті. В STM32 на DMA можна повісити 2 колбеки: коли DMA відправив половину даних і коли відправив всі дані. Можна зробити цей проміжний масив невеликого розміру, щоб він тримав ширини імпульсів лише для 2 "тимчасових" світлодіодів. Якщо запустити DMA в циклічному режимі і на кожен колбек записувати в цей масив дані для наступного світлодіоду, то нам знадобиться лише 48 байт (224){\left(2 \cdot 24\right)}. Тобто послідовність дій наступна:

  1. DMA циклічно ходить по масиву з даними з шириною імпульсів, які і відправляє на таймер
  2. Після відправлення перших 24 значень (для 1-го неопікселя) на таймер DMA викликає колбек, що половина даних відправлена
  3. В цьому колбеці ми беремо дані з масиву з кольорами про наступний (вже 3-ій) неопіксель і записуємо в перші 24 позиції ширину кожного імпульсу
  4. Далі DMA відправив наступні 24 (з 24-го по 47-ий) значення імпульсів для наступного неопікселя і знову викликав колбек - цього разу про завершення відправлення всіх даних
  5. В цьому колбеці ми беремо дані з масиву з кольорами про наступний неопіксель (вже 4-ий) і записуємо в позиції з 24-ої по 47-у ширину кожного імпульсу
  6. Оскільки DMA працює циклічно, то починає проходитись по масиву з самого спочатку, де його чекають вже оновлені дані для наступних 2-ох неопікселів, які ми приготували в колбеках
  7. Якщо дані з масиву з кольорами закінчились, то ми відправили всі дані і тепер можна відправляти сигнал RESET

Таким чином, маючи масив зі значеннями ширини імпульсів для лише 2-ох неопікселів, можна керувати нескінченною* кількістю неопікселів. Дані записані в масив з кольорами будуть автоматично пересилатися на світлодіоди завдяки роботи таймера, DMA та DMA колбеків. Все, що залишається - написати зручні функції для роботи з цим масивом, тобто власне - бібліотеку.

* Ну майже. Насправді - допоки вистачить пам'яті для основного масиву з кольорами. Хоча можна було б генерувати дані про кольори напряму в 2-ий масив з шириною імпульсів, але це помітно ускладнило б нашу бібліотеку.

📚🏛️ Драйвер WS2812B

Для початку давайте придумаємо які елементарні функції чисто теоретично повинна мати бібліотека для зручного керування RGB світлодіодами:

  • задати колір окремого неопікселя
  • задати колір всіх неопікселів
  • вимкнути окремий неопіксель
  • вимкнути всі неопікселі
  • дати можливість користувачу бібліотеки задавати колір в RGB форматі замість GRB (дивись попередню статтю)

Тепер ці всі функції легко написати, тому що це вже буде вимагати лише маніпуляції з масивом кольорів. А фізична частина з передачею сигналу повністю автоматично виконується DMA і таймером в "окремому" потоці (колбеках).

Отже це все реалізовано в проєкті, розміщеному на GitHub. Ось кілька ключових шматків коду з нього:

uint32_t *ws2812b_init(void (*wait)(uint32_t ms))
{
waitMs = wait;
for (uint16_t i = 0; i < 2 * COLOR_BITS; i++)
{
pixels_pwm_data[i] = 1;
}

return (uint32_t *)pixels_pwm_data;
}

static uint32_t convertRgbToGrb(uint32_t rgb)
{
uint8_t green = (rgb >> 8) & 0xFF;
uint8_t red = (rgb >> 16) & 0xFF;

return (rgb & CLEAR_RG_MASK) | (red << 8) | (green << 16);
}

void ws2812b_set_pixel(uint16_t index, uint32_t rgbColor)
{
if (index >= PIXELS_AMOUNT)
{
return;
}

uint32_t grb = convertRgbToGrb(rgbColor);
pixels_data[index] = grb;
}

void ws2812b_allOff()
{
for (uint16_t i = 0; i < PIXELS_AMOUNT; i++)
{
ws2812b_clear_pixel(i);
}
}

void delay(uint32_t ms)
{
waitMs(ms);
}

void ws2812b_clear_pixel(uint16_t index)
{
ws2812b_set_pixel(index, 0);
}

void ws2812b_allOn(uint32_t rgbColor)
{
for (uint16_t i = 0; i < PIXELS_AMOUNT; i++)
{
ws2812b_set_pixel(i, rgbColor);
}
}

🎬 Анімації

Тепер з базовими функціями для роботи зі світлодіодами легко написати якісь прості анімації. Анімації написані та розміщені в тому ж репозиторії на GitHub

🌈 Веселка

☄️ Комета

Ефект затухаючого хвоста комети з можливістю задати довжину хвоста, колір і швидкість.

💡 Просте мигання

Мигання з можливістю задати колір і швидкість. Можна було б ще додати виправлення нелінійності рівня світіння світлодіодів, але для простоти залишив як є.



🔚 Висновки

Ось так в 3 рівні ми створили бібліотеку для роботи з WS2812B світлодіодами:

  1. Таймер + DMA
  2. Масив з кольорами світлодіодів та прості функції для роботи з ним
  3. Створення анімації на основі простих функцій

Кожен рівень відповідає за свою частину і дозволяє писати простіший код загалом. Адже тепер для того, щоб створити нову анімацію не потрібно пригадувати/розбиратися в тому, як працює протокол WS281X. Треба лише написати нову логіку роботи з масивом кольорів.




Дата публікації
2025.07.27