В общем, в этой статье я бы хотел запрограммировать адресную светодиодную ленту 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); 
  }  
Что должно получится
Что должно получится

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

Пока :-)

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


  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. besitzeruf
      07.11.2025 14:41

      и как вашим attiny через wifi управлять?


  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];

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


  1. alekseypro
    07.11.2025 14:41

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


    1. alcotel
      07.11.2025 14:41

      Я вот не любитель жёстких извращений с NOPами, HAL_Delay, и тем более переключением скорости налету. Я беру 7-битный UART, по таблице LUT пихаю в него 3x3 бита для WS2812, натравливаю DMA на подготовленный фрейм-буфер, и получаю нужную скорость без танцев с бубном. Не забываю инвертировать сигнал UARTа при преобразовании из 3V в 5V.

      С 1-wire и через USB-UART это всё тоже работает.


      1. alekseypro
        07.11.2025 14:41

        Про переключение скоростей это я ляпнул, да, спутал с чем то другим :)


  1. Z55
    07.11.2025 14:41

    Кто такой "водитель"? Это "драйвер"? Статья не ваша, это перевод?


    1. Sun-ami
      07.11.2025 14:41

      Там ещё много недопереведенного. Например:

      • " CPOL имеет низкий уровень, а CPHA — 1 ребро ". Edge ошибочно переведено как "ребро" вместо "по фронту"

      • "сигнал сброса подается путем снижения уровня сети передачи данных более чем на 50 мкс" вместо "установки низкого уровня на линии данных на 50мкс".

      В статье по ссылке в конце поста есть видео с оригинальным английским текстом.


      1. Z55
        07.11.2025 14:41

        Печально, что люди выдают чужой труд за свой, даже не понимая о чём пишут...


    1. alcotel
      07.11.2025 14:41

      Я десяток опечаток в стиле "трудности перевода" автору отправил. Пока нет реакции.


  1. kholdy_micro
    07.11.2025 14:41

    А без hal уже все разучились писать?


    1. d_nine
      07.11.2025 14:41

      Ну напишет с CMSIS и выиграет что? Я понимаю когда в память сильно упираемся или надо пик производительности выжать.


    1. Sun-ami
      07.11.2025 14:41

      С HAL переносимость лучше. Это ведь макет, и скорее всего на другом контроллере, чем в целевом устройстве. Потом если надо можно оптимизировать.


    1. Z55
      07.11.2025 14:41

      А в чём смысл игнорировать hal?


      1. Sun-ami
        07.11.2025 14:41

        Смысл в том, что HAL использует в разы больше ресурсов, чем программа, написанная на прямом обращении к регистрам - флэша, RAM, и процессорного времени, в том числе и в прерываниях. При этом существуют и эффективные библиотеки на шаблонах C++, которые делают пользовательский код даже проще, чем с использованием HAL, и он при этом сохраняет максимальную эффективность. Это может быть важно, если микроконтроллер имеет мало памяти, и занят ещё какими-то ресурсоёмкими или критичными по времени отклика задачами. В статье ведь ничего не говорится о том, сколько всего будет светодиодов в ленте - 8, 200, или 1000, с какой частотой предполагается обновлять видеоинформацию, что это вообще за устройство, основная ли его функция управление светодиодами или побочная, и макет ли это, или конечный вариант электроники, и если макет, то какой контроллер будет использоваться в конечном варианте. Но HAL имеет преимущество по сравнению с библиотеками C++ и тем более по сравнению с ручным кодированием управления регистрами в переносимости. С HAL пользовательский код можно переносить с минимальными изменениями между вообще всеми микроконтроллерами STM32, включая самые новые, которые постоянно появляются десятками в год, и использовать в проектах с RTOS и без неё.


        1. Z55
          07.11.2025 14:41

          Ну вот вы по сути сами написали суть. HAL имеет смысл, когда идёт борьба за ресурсы. Но по факту, всем нужна быстрая разработка, переносимость кода и ... возможность поддержки уже существующих проектов. Поэтому, выбирают hal, вместо того, чтобы забыть на месяц-другой про разработчиков, что бы те разобрались с дадашитом. Это дорого, и ничем не оправдано. Не хватает ресурсов у 3 серии, просто переходим на 4-ю. Клиент оплатит. Тоже самое и проекты на асме. Для удовлетворения личного эго (смотрите, как я могу!) - пожалуйста, но не в проект.


          1. Sun-ami
            07.11.2025 14:41

            В данном случае задача совсем не такая сложная, чтобы нужно было месяц-другой разбираться с даташитом, даже если писать на регистрах без библиотек. Это легко можно написать за день, пользуясь только даташитом и референс мануалом, и ещё быстрее - используя LLM, и тот же HAL как пример проверенного кода при неоднозначности документации. А с библиотеками на С++ это вообще несколько строк кода, к тому же обычно переносимого в рамках по крайней мере одной серии микроконтроллеров. С HAL может быть и ещё быстрее, но в чём тогда смысл статьи на Хабре - ведь здесь гораздо более уместны статьи, разбирающие сложные темы, а не то, что любой, кто в теме, может сам сделать за полчаса.


            1. Z55
              07.11.2025 14:41

              Думаю, смысл данной статьи - просто поднять себе карму. Но, как уже выяснили, статья - кривой перевод, а автор даже не понимает о чём пишет.


              1. An_private
                07.11.2025 14:41

                Так он и не заморачивался - он просто тупо скопировал бОльшую часть статьи из https://controllerstech.com/ws2812-leds-using-spi/ причём прямо с кривым тамошним автопереводом с английского. Картинки и видео оттуда же


                1. Z55
                  07.11.2025 14:41

                  Всё так )


  1. andruxxx
    07.11.2025 14:41

    Про WLED еще не писали ?


  1. Evgencheg
    07.11.2025 14:41

    Когда то тоже добрался реализовывать протокол для них на STM32, и тогда приглянулся именно SPI. Но мне показалось логичным организовать это управление бит в бит (бит SPI -> бит в интерфейсе WS2812). Тогда цветовой буфер это именно массив структуры GRB с атрибутом {packet*} - представлялся в памяти непрерывным массивом байт для передачи по SPI. Оставалось только затактировать SPI (включен он режиме slave) от одного из таймеров. Таймер настраивался на время передачи одного бита SmartLED и выходы пары его регистров CCRx были настроены один на время длительности H-уровня протокола, другой L-уровня. (один из них выдавался инвертированым, т.к. подключплся к внешней комбинаторной схемке (2И-НЕ или 2ИЛИ-НЕ). Один из импульсов генератора тактировал SPI и оба они вместе с выходом SPI (MISO) подключались к комбинаторке на выходе которой формировался непосредственно протокол управления SmartLED.
    Тогда даже применил последовательно соединение таймеров. - Тоесть этот таймер тактироваЛ другой по событию обновления соответственно тот считал количество отправляемых бит протокола
    и генерил прерывание как только нужное количество бит(по длине ленты ) отсчитает. - Устанавливал в первом счётчике значение счёта на продолжительность RET и прерывание по окончанию фиксило завершение обновление ленты и взводило DMA на следующий запуск. Обновление ленты инициировалосс только в случае изменений в цветовом буфере.