В общем, в этой статье я бы хотел запрограммировать адресную светодиодную ленту WS2812b на микроконтроллере NUCLEO-F411RE в среде STM32 с помощью SPI.

1.Что из себя представляет WS2812b

WS2812B — это лента, которая содержит в себе две вещи:

  1. Светодиоды RGB

У неё есть некоторые технические особенности:

  • Три кристалла в одном корпусе.

  • У всех светодиодов общий минус.

  • 256 уровней яркости для каждого цвета.

2. Микросхема или же чип диода:

  • Принимает цифровой сигнал с линии DATA IN.

  • Распознаёт логические 0 и 1 по временным интервалам.

  • Синхронизируется по фронтам сигнала.

Главное отличие этой ленты от обычной — это то, что светодиодами у адресной ленты можно управлять отдельно, когда у обычной ленты мы можем управлять только режимами, которые встроены в контроллер.

WS2812b
WS2812b

2. Разберём, как подключать и как работает лента

Подключать ленту довольно просто, главное, ничего не путайте

Подключение ws2812b
Подключение ws2812b

На скриншоте, приведённом выше, используется arduino, в нашем случае мы программируем на stm32, но по факту подключение ничем не отличается.

  1. Пин DATA подключаем к пину PA7 (так на моём микроконтроле, смотрите при подключении SPI_MOSI, на какой пин он идёт и он подключается) и не забывайте, что должен быть резистор от 200 до 500 Ом.

  2. Пин VCC от ленты вы можете не подключать к самому микроконтроллеру, это необязательно.

  3. Пин GND от ленты подключаем к пину GND микроконтроллера. Есть ещё VCC и GND, отводящиеся от пинов ленты, их можно подключить к блоку питания (можете вычислить напряжение и ток, к примеру, каждый цвет использует 20 мА,т.е вместе один диод 60 мА,а дальше можете вычислить от количества диодов в ленте).

Что делать нельзя:

  1. Нельзя подключать VCC от ленты к микроконтроллеру напрямую!!! У вас может сгореть либо сам контроллер, либо лента, это очень критично.

  2. Не дайте доп.питанию от ленты замыкаться, если это произошло и вы увидели падение напряжение, отключите USB от компьютера.

Как работает наша лента?

Для задания цвета каждому диоду передаётся 24 бита данных, то есть 3 байта:

8 бит для зелёного (G)

8 бит для красного (R)

8 бит для синего (B)

Порядок строго GRB, а не RGB — это особенность именно WS2812B.

Каждый канал — это число от 0 до 255, где

0 = светодиод полностью выключен,

255 = максимальная яркость.

Примеры:

G = 255, R = 0, B = 0  → горит зелёным
G = 0,   R = 255, B = 0 → горит красным
G = 0,   R = 0,   B = 255 → горит синим
G = 255, R = 255, B = 255 → белый (все три цвета)

Как кодируются нули и единицы

Пример отправки данных
Пример отправки данных

Логика такова: длительность сигнала HIGH/LOW кодирует бит:

  1. Передача осуществляется по одной линии данных — нет отдельного тактового сигнала.

    • Для логического 0: короткий период HIGH, затем длинный LOW.

    • Для логического 1: длинный период HIGH, затем короткий LOW.

  2. Примерные временные параметры:

    • Общий период одного бита ≈ 1.25 µs

    • Бит 0: HIGH ≈ 0.4 µs, LOW ≈ 0.85 µs

    • Бит 1: HIGH ≈ 0.8 µs, LOW ≈ 0.45 µs

  3. То есть:

    • короткий HIGH → “0”

    • длинный HIGH → “1”

Почему важна точность во времени?

Микросхема WS2812B очень чувствительна к длительности импульсов.

Если временные интервалы отклоняются даже на ~100–150 наносекунд, могут произойти ошибки: «1» может быть воспринят как «0» или наоборот.

Это ведёт к неправильным цветам или хаотичному миганию светодиодов.

Поэтому важно:

  1. Использовать аппаратные средства (SPI, PWM‑таймер, DMA) для точной генерации сигнала, а не просто «цикл в программе».

  2. Минимизировать задержки и прерывания, которые могут «сломать» временные рамки.

Сброс и конец передачи (RESET)

После передачи всех данных нужно сделать паузу: линия данных удерживается в состоянии LOW минимум ≈ 50 µs.

Эта пауза сообщает всем светодиодам: «Передача окончена, применяйте новые цвета».

Если паузы не будет — лента может не обновиться корректно, цвета могут «зависнуть» или отобразиться неправильно.

После паузы все чипы обновляют свой внутренний буфер и отображают цвета одновременно.

Передача данных по цепочке

Данные передаются «потоком» от контроллера к первому светодиоду, потом к следующему и так далее.

Пример: для 10 диодов передаётся 10 × 24 = 240 бит данных.

Процесс:

  • Первый диод принимает первые 24 бита и сохраняет их.

  • После этого он передаёт остальной поток вперёд (на DOUT).

  • Второй диод принимает следующие 24 бита, и так далее.

  • После передачи всех битов AND после сигнала RESET все диоды сразу обновляют цвета.

3. Настройка CubeMX в проекте

Мы будем использовать только контакт SPI MOSI для отправки данных на светодиоды. Таким образом, для этой конфигурации по-прежнему требуется только 1 контакт для подключения драйвера WS2812 к микроконтроллеру.

SPI используется из-за его высокой скорости передачи данных. Битовая синхронизация, необходимая для светодиодов, очень мала (около 0,4 мкс), и с помощью SPI мы можем достичь этого.

Идея очень проста. Если мы установим скорость передачи данных SPI на уровне 2,5 Мбит/с, каждый бит будет представлять импульс в 0,4 мкс.

1/25M/s = 0,4us

Согласно техническому описанию WS2812, бит считается равным 0, если сигнал остается ВЫСОКИМ в течение 0,35 мкс и НИЗКИМ в течение 0,8 мкс. И для того, чтобы бит считался 1, сигнал остается ВЫСОКИМ для 0.7 мкс и НИЗКИМ для 0.6 мкс. Конечно, это гибкий с погрешностью до ±150 нс. Кроме того, сигнал сброса подается путем снижения уровня сети передачи данных более чем на 50 мкс.

Мы будем использовать 3 бита SPI для представления одного бита для WS2812. 3 бита SPI будут иметь временной период 1,2 мс (0,4 * 3).

Как работает 0 и 1
Как работает 0 и 1

Как показано выше, чтобы отправить 0 водителю, мы можем отправить биты 100. Это позволит поддерживать высокий пульс в течение 0,4 мкс и низкий в течение 0,8 мкс. Это соответствует времени, необходимому для 0 в техническом описании.

Точно так же, чтобы отправить 1 водителю, мы можем отправить биты 110. Это позволит поддерживать высокий пульс в течение 0,8 мкс и низкий в течение 0,4 мкс, что является приемлемым временем для 1 в соответствии с техническим описанием.

Перейдём к настройке CubeMX

Нам просто нужно включить SPI и установить скорость передачи данных на 2,5 Мбит/с.

Настройка SPI
Настройка SPI
  • Здесь SPI настроен в полудуплексном режиме, так как нам нужно только отправить данные водителю. Мы не принимаем от него никаких данных.

  • Размер данных равен 8 битам, так как мы будем хранить только 3 бита для каждого бита драйвера. Данные сначала должны быть отправлены в формате MSB.

  • Предварительный делитель настроен таким образом, что мы получаем скорость передачи данных 2,5 МБит/с.

  • CPOL имеет низкий уровень, а CPHA — 1 ребро. По сути, это РЕЖИМ 0 SPI.

    В полудуплексном режиме 2 контакта выбираются автоматически, то есть контакты SCK (часы) и MOSI (выход данных). Из них мы будем использовать только контакт MOSI для подключения к контакту данных WS2812.

4. Код и его пояснение

Я создал отдельные файлы библиотеки для WS2812_Spi. Поэтому большая часть кода написана в файле WS2812_spi.c.

#define NUM_LED 8
uint8_t LED_Data[NUM_LED][4];

extern SPI_HandleTypeDef hspi1;

#define USE_BRIGHTNESS 1
extern int brightness;

Сначала мы определим количество светодиодов в ленте или матрице. Затем создайте матрицу с количеством строк таким же, как у светодиодов, и 4 столбцами.

3 столбца будут использоваться для хранения кодов кулера (RGB), а 4-й столбец будет использоваться для хранения номера светодиода.

Затем определите обработчик SPI в качестве внешней переменной. Основное определение этого обработчика находится в основном файле.

Я также включил регулятор яркости (не очень точный, но работает). Вы можете решить, использовать ли функцию яркости (установив значение 1) или не использовать (установив значение 0). Внешнее целое число, brightness, определяется для хранения значения яркости. Эта переменная на самом деле определена в функции main и может иметь значение от 0 до 100.

void setLED (int led, int RED, int GREEN, int BLUE)
{
LED_Data[led][0] = led;
LED_Data[led][1] = GREEN;
LED_Data[led][2] = RED;
LED_Data[led][3] = BLUE;
}

Здесь led — количество светодиодов (от 0 до NUM_LED-1), на которых мы хотим сохранить код RGB. Остальные параметры — это цветовые коды.

Первый элемент матрицы хранит номер светодиода, а остальные — цветовые коды. Обратите внимание, что сначала я сохраняю ЗЕЛЕНЫЙ цвет. Это связано с тем, что в соответствии с техническим описанием драйвера, данные о цвете отправляются в формате GRB.

Чтобы отправить данные драйверу, нам сначала нужно извлечь каждый бит цветовых данных, а затем назначить 3 бита SPI для каждого из них.

void ws2812_spi (int GREEN, int RED, int BLUE)
 {
 #if USE_BRIGHTNESS
 if (brightness>100)brightness = 100;
 GREEN = GREEN*brightness/100; 
RED = Retuo[\ED*brightness/100;tqeuo[\qetuo[\qetuo[\qetuo[\qetuo[\qetuoqetuo[\qetuo[\qetuo[\\qetuo[\
 BLUE = BLUE*brightness/100;
 #endif
 uint32_t color = GREEN<<16 | RED<<8 | BLUE;
 uint8_t sendData[24];
 int indx = 0;
 for (int i=23; i>=0; i--)
 {
 if (((color>>i)&0x01) == 1) sendData[indx++] = 0b110;  // store 1
 else sendData[indx++] = 0b100;  // store 0
 }
 HAL_SPI_Transmit(&hspi1, sendData, 24, 1000);
 }

Здесь мы сначала объединим 3 байта цвета, чтобы получить одно 24-битное цветовое пространство. Массив sendData определен с 24 элементами для хранения 3 битов для каждого бита цветовых данных.

Теперь в цикле for мы начнем извлекать каждый бит цветовых данных из старшего бита. Если бит равен 1, мы будем хранить биты SPI 110 в соответствующем элементе массива. В противном случае, если бит равен 0, мы сохраним биты 100 в соответствующем элементе массива.

По сути, мы начнем извлекать данные из 23-го бита (G7) и сохраним соответствующие данные SPI в sendData[0]. Когда мы отправим этот массив в SPI, элемент [0] будет отправлен первым, и, следовательно, в некотором смысле мы отправляем 23-й бит (G7) цветовых данных первым.

Мы также используем значение яркости для управления цветами. Значения цвета RGB будут меняться в зависимости от значения яркости еще до того, как мы объединим их.

Как только извлечение будет завершено, мы отправим все 24 байта в SPI.

Вышеуказанная функция предназначена для отправки данных для одного светодиода водителю. Но на самом деле нам нужно отправлять данные для всех светодиодов каждый раз, когда мы вносим изменения даже в один светодиод. WS2812_Send будет использоваться для вызова вышеуказанной функции для всех светодиодов.

void WS2812_Send (void) { 
for (int i=0; i<NUM_LED; i++) 	{ 		
WS2812_Send_Spi(LED_Data[i][1],LED_Data[i][2],LED_Data[i][3]); 
} 	
HAL_Delay (1);
 }  

Здесь мы будем вызывать цикл for столько раз, сколько светодиодов мы соединили последовательно. Для каждого светодиода мы будем отправлять данные через SPI.

Данные хранятся в LED_Data матрице, которую мы определили ранее в файле. ЗЕЛЕНЫЙ цвет сохраняется в 1-м элементе, затем КРАСНЫЙ во 2-м и СИНИЙ в 3-м.

Как только все данные будут отправлены, мы дадим задержку в 1 мс для кода RESET. Согласно техническому описанию, нам нужно держать линию НИЗКОЙ более 50 мкс, чтобы указать, что все данные по светодиодам были отправлены.

while (1)   {
for (int i=0; i<4; i++) { 	
setLED(i, 255, 0, 0); 
} 
WS2812_Send(5);
 HAL_Delay(1000);  
for (int i=0; i<4; i++)  { 
setLED(i, 0, 255, 0); 
} 	 
 WS2812_Send(5); 
 HAL_Delay(1000);  
 for (int i=0; i<4; i++)  { 	
setLED(i, 0, 0, 255); 	
 } 	 
 WS2812_Send(5);
 HAL_Delay(1000); 
  }  
Что должно получится
Что должно получится

Всем спасибо, кто прочитал данный пост, возможно, он был кому-то полезен :)
Большую инфу брал отсюда Тык

Пока :-)

Комментарии (8)


  1. ki11j0y
    07.11.2025 14:41

    Stm32 для ленты это сильно, это как из пушки по воробьям, esp8266 или esp32 последняя избыточна даже, можно использовать attiny даже.


    1. Sun-ami
      07.11.2025 14:41

      STM32 использовать удобно, потому что для того, чтобы микроконтроллер мог делать что-то ещё, очень желательно иметь DMA, иначе понадобятся прерывания с частотой 312,5кГц и максимальной задержкой около 6 мкс, а реализовать это - нетривиальная задача, и такой подход даёт большую загрузку микроконтроллера обработкой этих прерываний. Или нужно запрещать прерывания на время обновления данных в ленте, а это далеко не всегда приемлемо. Другое дело, что STM32F411RE для такой задачи избыточен, вполне подойдёт любой самый мелкий и дешевый STM32 серий C0, G0, L0 или F0 стоимостью от $0,21/шт, но это больше зависит от того, что ещё должен делать этот микроконтроллер.


      1. VBDUnit
        07.11.2025 14:41

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

        Более чем. Делал протокол WS2812b руками на nopах, забил всю память STM32 Discovery. На таймерах/SPI получилось бы сильно проще и меньше.


    1. Wshadow
      07.11.2025 14:41

      Здесь важен принцип, а не конкретный контроллер. А также время реализации и удобство. Сейчас процов аля Cortex-M0 как грязи и стоят многие как грязь. Зачем тогда заниматься самоистязанием и пытаться "эффективно" использовать ресурсы железа? Если это только не самоцель и попытка придраться "к столбу".


    1. VBDUnit
      07.11.2025 14:41

      Смотря сколько лент надо контролировать, сколько диодов на них и с какой частотой их переключать. Если их несколько тысяч, простые контроллеры могут не переварить.


  1. Wshadow
    07.11.2025 14:41

    Когда-то и я такое делал ( https://habr.com/ru/articles/557350/ ). А кто-то и до меня... История циклична :)


  1. poruchuk_Rzevski
    07.11.2025 14:41

    Я делал точно так же, но кодировал по два бита в одном байте.


  1. d_nine
    07.11.2025 14:41

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

    typedef struct {
      uint8_t red;
      uint8_t green;
      uint8_t blue;
    } led_t;
    
    led_t led_string[LED_NUM];

    При желании можно сюда же добавить поле для управления яркостью.