🚦 Драйвер
Програмовані RGB світлодіоди з'явились відносно не так давно. Піонером в цій області вважається китайська компанія WorldSemi, яка випустила світлодіод WS2811 в 2013. Але якщо бути точнішим, то WS2811 це була мікросхема поряд з RGB світлодіодами. Одна така мікросхема могла керувати трьома RGB світлодіодами через простий протокол комунікації. В наступних версіях — в WS2812 — цю мікросхему зменшили настільки, що це дозволило помістити її прямо в середину кожного окремого світлодіоду. На сьогодні існує вже багато моделей таких світлодіодів і відомі вони під назвою неопіксель — терміном популяризованим компанією Adafruit Industries.
Протокол керування
Зазвичай неопікселі розміщують в так звані RGB LED стрічки, де вони з'єднуються послідовно між собою один за одним. Кожен світлодіод завдяки вбудованому контролеру може бути увімкнений окремо один від одного. Це досягається завдяки своєрідному протоколу керування з використанням всього однієї лінії даних. По цій лінії передаються біти даних з закодованими значен нями кольорів, які потрібно увімкнути — по 8 біт (0-255 відтінків) на колір. Тобто разом виходить 24 біти для 3 кольорів одного RGB (Red, Green, Blue) світлодіоду.
Кожен неопіксель має 4 контакти: та (GND) для живлення і та для приймання та передачі контролюючого ШІМ сигналу. Кожен вивід наступного неопікселя з'єднаний з виводом попереднього, таким чином вони формують ланцюг. Саме так — кожен неопіксель приймає 24 біти сигналу через і якщо бачить, що за ними йдуть ще, то передає ці отримані 24 біти далі по черзі до наступного неопікселя через , а сам починає приймати наступні 24 біти. Це відбувається аж допоки не отримує сигнал Reset
, який означає, що дані закінчились і вже потрібно відобразити отримані кольори. Сигнал Reset
це всього лиш низький сигнал тривалістю 280+ мікросекунд.
Сигнал керування, яким кодуються біти являється ШІМ сигналом, де кожен біт це імпульс тривалістю 1/4 або 3/4 від періоду. Період з робочим циклом 3/4 означає 1, а період з заповненням 1/4 — 0. Далі на картинці з даташиту WS2812B показані трохи точніші цифри:
Ось такий вигляд протокол має на діаграмі логічного аналізатора для 2-ох послідовно з'єднаних WS2812B світлодіодів:
Як видно з діаграми, для кожного світлодіоду маємо 24 біти. Перший світлодіод в нашій імпровізованій стрічці отримав колір #00ffff , а другий — #ff0000. І в кінці маємо низький сигнал Reset
. Один важливий нюанс полягає в тому, що насправді біти передаються в порядку не RGB, а GRB. Тобто перші 8 біт, які передаються для кожного світлодіоду, то зелений колір. Якщо знаєте чому так — напишіть будь ласка внизу сторінки в Коментарях.
🔬🧪 Тестовий стенд
Давайте зберемо тестовий стенд і спробуємо реалізувати протокол керування неопікселями. Отже нам знадобиться:
- бредборда і джампери (4$)
- світлодіоди WS2812B в корпусі з в ивідними пінами (5 шт - 3$)
- мікроконтролер (наприклад STM32G030F6P6 - 2.5$)
- програматор ST-Link v2 (2.5$)
Згідно даташиту WS2812B напруга живлення має складати 3.7В - 5.3В, що трохи більше за 3.3В з борди STM32G030. Тобто нам було б добре заживити наш неопіксель окремо від 3.7В, але на практиці я перевірив, що для кількох послідовно з'єднаних світлодіодів достатньо і 3.3В.
В результаті тестовий стенд виглядає ось так:
Неопіксель хоч і під'єднаний до 5В, але насправді там 3.3В, оскільки 3.3В приходить на сам мікроконтролер і на понижуючий регулятор. А регулятор працює таким чином, що якщо подати напругу з його "нижчої" сторони, то така сама напруга буде і на вищій. Тобто з обох сторін регулятора присутні 3.3В, що нам підходить. Також можна було б додати резистор на лінію даних для стабільності передачі, але для простоти і невеликої кількості неопікселів цього робити не став.
💾↔️⚙️ Таймер + DMA
В попередніх публікаціях ми розглядали таймери та кілька прикладів їх застосувань. Одним із них був приклад з ШІМ (Широтно-Імпульсна Модуляція). Там таймер сам генерував ШІМ сигнал, а в циклі while
ми лише змінювали ширину імпульсу від 0 до 100%. Це дозволяло нам плавно керувати яскравістю світлодіоду. Для керування неопікселями ми теж використаємо ШІМ таймера. Лише цього разу передачею на ШІМ таймера тривалості імпульсів буде займатися DMA. DMA — Direct Memory Access (прямий доступ до пам'яті), це апаратна периферія мікроконтролера — така сама як і наприклад ті самі таймери чи GPIO. DMA може допомогти розвантажити ядро мікроконтролера від роботи по передачі даних: з однієї області пам'яті — в іншу, з пам'яті — в регістр і навпаки. Для нашого прикладу нам треба буде переміщувати дані з пам'яті (з масиву) в регістр CCR (Capture/Compare Register) нашого таймера, який і відповідає за ширину імпульсів ШІМ сигналу.
Сигнал керування неопікселями досить високочастотний: у старіших моделях 0.8 MHz (1.25 us), у нових може бути вже і 1 MHz (1 us). Тобто якби відправленням даних на CCR регістр таймера займалось ядро мікроконтролера, то воно було б досить завантаженим і не вистачило б його ресурсів для якоїсь більш корисної роботи. Натомість ми лише підготуємо масив зі значеннями заповнення ШІМ періодів, а DMA саме перешле ці дані в регістр CCR. Для запуску таймера в режимі ШІМ можемо звернутися до попередньої публікації, а тут розглянемо як запустити DMA:
- запускаємо тактування DMA
- ініціалізуємо DMA з наступними параметрами:
- DMA номер і канал цього DMA, який хочемо використати
- напрямок копіювання даних, в нашому випадку — з пам'яті в регістр таймера
- розмір регістру CCR і розмір даних в масиві (16 біт і 8 біт відповідно — в нашому випадку)
- режим роботи (циклічний — коли дані в масиві закінчились, починаємо заново з початку масиву)
- запускаємо таймер в режимі ШІМ-DMA.
- Розганя ємо МК до 64 MHz
- Запуск таймера на 4 MHz
- Ініціалізація DMA
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.HSIDiv = RCC_HSI_DIV1;
RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSI;
RCC_OscInitStruct.PLL.PLLM = RCC_PLLM_DIV1;
RCC_OscInitStruct.PLL.PLLN = 8;
RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2;
HAL_RCC_OscConfig(&RCC_OscInitStruct);
// Initializes the CPU, AHB and APB buses clocks
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;
HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2);
}
TIM_HandleTypeDef hTim3;
void startWledTimer()
{
__HAL_RCC_TIM3_CLK_ENABLE();
hTim3.Instance = TIM3;
hTim3.Init.Prescaler = 16 - 1;
hTim3.Init.Period = 4 - 1;
hTim3.Init.CounterMode = TIM_COUNTERMODE_UP;
hTim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_Base_Init(&hTim3);
HAL_TIM_PWM_Init(&hTim3);
TIM_OC_InitTypeDef sConfigOC = {0};
sConfigOC.OCMode = TIM_OCMODE_PWM1;
HAL_TIM_PWM_ConfigChannel(&hTim3, &sConfigOC, TIM_CHANNEL_1);
HAL_TIM_PWM_Start_DMA(&hTim3, TIM_CHANNEL_1, (uint32_t *)arr, sizeof(arr) / sizeof(arr[0]));
}
DMA_HandleTypeDef hdma_tim3_ch1;
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim_base)
{
if (htim_base->Instance == TIM3)
{
__HAL_RCC_DMA1_CLK_ENABLE();
hdma_tim3_ch1.Instance = DMA1_Channel1;
hdma_tim3_ch1.Init.Request = DMA_REQUEST_TIM3_CH1;
hdma_tim3_ch1.Init.Direction = DMA_MEMORY_TO_PERIPH;
hdma_tim3_ch1.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_tim3_ch1.Init.MemInc = DMA_MINC_ENABLE;
hdma_tim3_ch1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
hdma_tim3_ch1.Init.MemDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_tim3_ch1.Init.Mode = DMA_CIRCULAR;
HAL_DMA_Init(&hdma_tim3_ch1);
__HAL_LINKDMA(htim_base, hdma[TIM_DMA_ID_CC1], hdma_tim3_ch1);
}
}
МК запущений на частоті 64 MHz, а таймер на 4 MHz (250ns). У період таймера запишемо число 4, що дорівнюватиме 1000ns (4 x 250ns). Згідно даташиту для логічної одиниці достатнє заповнення у 750ns, а для логічно го нуля — 250ns. І те і інше кратне частоті нашого таймера в 250ns. Тобто в CCR регістр будемо записувати 1 (для 250ns) для логічного нуля, або 3 (750ns = 3 х 250ns) — для логічної одиниці. Також будемо записувати і 0. Про це — пізніше. Дивна річ: DMA/Таймер адекватно передавали біти тільки коли мій МК був розігнаний до 48+ MHz. Якщо менше то інколи в ситуаціях, де мав бути нуль передавалась одиниця.
Підготуємо масив з даними, який DMA буде відправляти на ШІМ таймера та запустимо нарешті приклад.
- Масив з даними та запуск всього
uint8_t arr[] = {
//Green Red Blue
1,1,1,1,1,1,1,1, 3,3,3,3,3,3,3,3, 3,3,3,3,3,3,3,3, //0x00ffff
3,3,3,3,3,3,3,3, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, //0xff0000
// Reset
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
};
int main()
{
HAL_Init();
SystemClock_Config();
GPIO_PA6_Init();
startWledTimer();
while (1)
{
HAL_Delay(100);
}
return 0;
}
void GPIO_PA6_Init()
{
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_6;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Alternate = GPIO_AF1_TIM3;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
Отже перший ряд в масиві — це реальні дані з кольорами для одного неопікселя, решта нулів — низький рівень для сигналу Reset
(280+ us). Судячи з даних, загорітися має лише червоний колір. Таймер в режимі ШІМ запущений на піні PA6, тому не забуваємо перевести його в альтернативний режим. Згідно даташиту для PA6 це GPIO_AF1_TIM3.
Цей проєкт розміщений на GitHub.
🔚 Висновки
DMA разом з таймерами чудово допомагають розвантажити ядро мікроконтролера, виконуючи замість нього рутинну роботу. Таймер генерує сигнал на піні, а DMA надає дані для генерації цього сигналу. В цей час МК може бути зайнятий будь якими іншими справами, наприклад прийманням команд від користувача чи складанням цікавих анімацій роботи неопікселів.
Сьогодні програмовані RGB світлодіоди повсюди і це стало можливим завдяки технічному прогресу. Даний приклад демонструє простий запуск такого світлодіоду та може бути хорошою основою для написання повноцінного драйвера з анімацією та підтримкою динамічної кількості неопікселів.
Дата публікації
2025.05.01