Привет, Хабр! Сегодня поделюсь опытом работы со связкой stm32F401CCu6 и светодиодной лентой на базе диодов ws2812. В наличии у меня имеется микроконтроллер семейства ARM и обычная адресная лента на 144 диода а также блок питания на 5 вольт, 3 ампера, для наших целей его вполне хватит. Также стоит заранее подготовить какой-нибудь понижающий DC-DC преобразователь, я взял LM2596.

Принцип работы WS2812

Прежде чем приступать к работе необходимо разобрать принцип управления нашими светодиодами. На вход каждого из них поступает комбинация из 24 битов, по 8 на каждый цвет(красный, зелёный, синий), иногда эта последовательность может отличаться, все биты идущие далее - поступают на следующие диоды. Однако нам недостаточно передать "классическую" последовательность из нулей и единица, ws2812 обладает собственным правилом управления. Каждый бит передаётся в течении 1.25 микросекунд, соответственно скорость передачи составляет 800 Кбит в секунду или 800кГц, запомните, это нам пригодится в дальнейшем. Для передачи нуля необходимо чтобы на линии сохранилась устойчивая единица в течении минимум 0.35 мкс, а оставшиеся 0.9 мкс сохранился устойчивый ноль. Передачи же единицы происходит по зеркальному принципу: 0.9 мкс - 1, 0.35 мкс - 0. Таким образом чтобы нам передать 24 бита - нужно чтобы на линии были установлены необходимые логические уровни строго определённое время.

  • один бит длится 1,25 мкс (частота ~800 кГц),

  • передача 0: 0,35 мкс — высокий уровень, затем 0,9 мкс — низкий,

  • передача 1: 0,9 мкс — высокий уровень, затем 0,35 мкс — низкий.

Логический ноль на ленте
Логический ноль на ленте
Единица на ленте
Единица на ленте

Приступаем к схеме

Для начала необходимо подключить питание ленты, здесь важно учесть момент: лента питается от 3.5 вольт до 5.3, но так как выходное напряжение пинов stm составляет 3.3 вольта, поскольку светодиод может не воспринять это как единицу, необходимо слегка понизить питание ленты через LM2596, я выставил 4.5 вольта, этого вполне достаточно. Зелёный провод ленты необходимо подключить к выводу A0 stm32 - через него будет происходить управление, то есть передаваться последовательность битов. Также белый провод земли ленты необходимо дополнительно соединить с выводом GND самой STM.

LM2596
LM2596

Настройка проекта в CubeIDE

Тактирование

  • Задаём тактовую частоту 84 МГц.

  • Используем внешний кварцевый резонатор.

Таймер и ШИМ

  • Настраиваем TIM2, канал 1 на генерацию ШИМ.

  • Нам нужна частота 800 кГц.
    Формула для расчёта:

Counter = F / f - 1

Где F = 84 МГц, f = 800 кГц.
Итого: 84 000 000 / 800 000 - 1 = 104.

DMA

  • Включаем передачу Memory-to-Peripheral.

  • Подключаем прерывание DMA (Stream5).

Настройки таймера
Настройки таймера
DMA
DMA

Пишем управление

Приступаем к написанию основной программы. Для начала создадим несколько макросов. LOW, со значением обозначающим ноль - 30 HIGH, аналогично, только уже для единицы - 75 Макрос LEDS будет хранить общие число светодиодов на ленте, у меня это 144.

#define LOW 30
#define HIGH 75
#define LEDS 144

Помимо макроса создаём массив, или же буфер, в который будем складывать данные для каждого цвета в двоичном представлении. Размер массива равняется количеству диодов умноженному на 24, поскольку на каждый из них приходится 24 бита(по 8 на каждый цвет).

uint32_t DMA_BUFFER[24 * LEDS];

Основная функция - setColor() где в качестве аргумента передаётся номер(позиция) интересуемого светодиода, а затем последовательно передаём 3 байта цветов соответственно: red, green, blue. Внутри самой функции производим расчёт полученных значений, исходя из установленного уровня яркости, чтобы каждый цвет уменьшался пропорционально значению яркости. Для этого в начале добавим глобальную переменную bright и выставим ей максимальное значение - 255. После полученных значений сольём 3 отдельных байта в одну 4ых байтовую переменную color. Где каждый байт сдвигается на свою позицию. Теперь обратным циклом перебираем все значения и заполняем им ранее созданный массив DMA_BUFFER, здесь также необходимо создать макрос проверки бита - BitIsSet(color, i), который проверяет чему равен конкретный бит в наборе и в зависимости от этого кладёт в буфер ранее созданные значения HIGH и LOW.

#define BitIsSet(reg, bit) ((reg & (1 << bit)) != 0)

void setColor(uint16_t pos, uint8t red, uint8t green, uint8t _blue){

	red = red / (256 / ((uint16_t)bright +1));

	green = green / (256 / ((uint16_t)bright +1));

	blue = blue / (256 / ((uint16_t)bright +1));

	uint32_t color = (_green << 16) | (_red << 8) | _blue;

	for (int8_t i = 23; i >= 0; i--){

		if (BitIsSet(color, i)){

			DMA_BUFFER[(23-i)+_pos*24] = HIGH;

		}

		else {

			DMA_BUFFER[(23-i)+_pos*24] = LOW;

		}

	}

}

Следующей функцией станет вывод имеющегося массива на ленту - showColor() . В ней просто запускаем настроенный таймер и передаём ему заполненный буфер.

void showColor(){

	HAL_TIM_PWM_Start_DMA(&htim2, TIM_CHANNEL_1, (uint32_t*)DMA_BUFFER, sizeof(DMA_BUFFER));

}

ВАЖНО!!! Чтобы после отправки всех битов на ленту таймер остановился - необходимо добавить функцию его остановке внутри прерывания DMA в файле stm32f4xx_it.c. Добавляем внутрь обработчика прерывания DMA1_Stream5_IRQHandler() функцию

HAL_TIM_PWM_Stop_DMA(&htim2, TIM_CHANNEL_1);

Возвращаемся в главный файл. Чтобы избавить себя от необходимости вручную задавать цвет каждого диода, добавим функцию заливки ленты сплошным цветом - setLed(). В ней перебираем циклом весь набор светодиодов, для каждого из них устанавливая каждый компонент цветы введённым значением.

void setLed(uint8_t red, uint8t green, uint8t _blue){

	for (uint16_t i = 0; i < LEDS; i++){

		setColor(i, red, green, _blue);

	}

}

Добавим также обратную функцию очистки ленты- clearLed(), где каждый градиент будет равен нулю.

void clearLed(){

	for (uint16_t i = 0; i < LEDS; i++){

		setColor(i, 0, 0, 0);

	}

}

Ну и последним добавим функцию установки яркости - setBrightness(), где просто значение аргумента функции кладётся в глобальную переменную яркости.

Разбираем HSV

Для более «живой» анимации удобно использовать HSV-палитру.

  • H (Hue, оттенок): 0–360° (0° — красный, 120° — зелёный, 240° — синий).

  • S (Saturation, насыщенность): 0–100%.

  • V (Value, яркость): 0–100%.

Конвертация HSV → RGB позволяет легко создавать градиенты и эффекты.

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

ВАЖНО!!! Здесь уже в качестве параметра насыщенности и яркости задаём значения от 0 до 1.0, где второе - максимальное значение.

void convertHSV(uint16_t position, uint8_t H, float S, float V){

	float C;

	float X;

	float m;

	uint8_t red, green, blue;

	//V = 1.0 / (256 / V + 1);

	uint8_t r, g, b;

	C = V * S;

	X = C * (1 - abs(((H / 60)%2) - 1));

	m = V - C;

	switch(H/60){

	case 0:

		r = C;

		g = X;

		b = 0;

		break;

	case 1:

		r = X;

		g = C;

		b = 0;

		break;

	case 2:

		r = 0;

		g = C;

		b = X;

		break;

	case 3:

		r = 0;

		g = X;

		b = C;

		break;

	case 4:

		r = X;

		g = 0;

		b = C;

		break;

	case 5:

		r = C;

		g = 0;

		b = X;

		break;

	}

	red = (r+m)*255;

	green = (g+m)*255;

	blue = (b+m)*255;

	setColor(position, red, green, blue);

}

Чтобы добавить ленте интерактива, создадим функцию радуги, которая будет заполнять ленту цветами HSV палитры. Также создадим глобальную переменную hue и присвоим ей значение 0, отвечающую за цветовой сдвиг. Сама функция rainbow() весьма простая, в ней перебираем все диоды на ленте. Насыщенность и яркость выставляем максимальной. Чтобы цветы сильнее походили на радугу будем умножать значения оттенка на 5, это можно настроить дополнительно. После цикла увеличиваем значения оттенка на 10, это также можно подогнать из своих предпочтений. Задержка в конце необходима чтобы создать эффект бегущей радуги, это время напрямую отвечает за скорость эффекта.

void rainbow(){

	for (uint16_t i = 0; i < LEDS; i++){

		convertHSV(i, hue + i * 5, 1.0, 1.0);

	}

	hue = hue + 10;

	showColor();

	HAL_Delay(75);

}

Добавляем Bluetooth

Чтобы управлять лентой со смартфона, подключим модуль HC-06.

  • В CubeMX настраиваем USART2 на 9600 бод.

  • Подключаем прерывания USART2 global interrupt .

Внутри главного файла, где уже имеются глобальные переменные дописываем ещё несколько: rxByte, куда будем складывать полученную через uart информацию, rxIndex задаём стартовым значением 0, необходимо для перебора полученных байт, rainbowActive, через неё будем отслеживать работает ли в данный момент эффект радуги, и управлять им, и последним создаём буфер на два байта куда будем складывать набор полученных байт. Теперь внутри функции main() добавляем получение данных по uart через прерывание.

Добавляем callback функцию, вызываю прерывание при получении новых данных и в ней помещаем полученный байт в созданный буфер. Если поступает символ конца строки - вызываем функцию parseMsg() и передаём в параметре адрес буфера, для этого необходимо создать эту функцию.

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){

	if (huart->Instance == USART2){

		rainbowActive = 0;

		if (rxIndex < 2){

			rxBuffer[rxIndex++] = rxByte;

			rxBuffer[rxIndex] = '\0';

		}

		if (rxByte == '\n'){

			parseMsg(rxBuffer);

			rxIndex = 0;

		}

		HAL_UART_Receive_IT(&huart2, &rxByte, 1);

	}

}

Её суть весьма проста, поскольку она просто преобразовывает имеющийся буффер в число. Затем через switch case парсит полученное значение. Алгоритм общения представленный здесь очень прост, поскольку в нём программа просто переключает цвет ленты в зависимости от принятой команды, состоящей из одной цифры. В случае команды 4, переменной управления радугой задаётся значение 1.

void parseMsg(char *str){

	int cmd = atoi(str);

	switch(cmd){

	case 1:

		setLed(255, 0, 0);

		break;

	case 2:

		setLed(0, 255, 0);

		break;

	case 3:

		setLed(0, 0, 255);

		break;

	case 4:

		rainbowActive = 1;

		break;

	}

	showColor();

}

Последние что осталось выполнить - добавить внутри бесконечного цикла while(1) проверку на состояние переменной rainbowActive и если она ровна единицы - вызвать функцию rainbow(). Смотрим результат Устанавливаем приложение Serial Bluetooth Terminal.

Заглавная страница приложения
Заглавная страница приложения

Затем в списке устройств находим наш модуль HC-06. В строке терминала просто задаём поочерёдно наши команды от 1-4 и наблюдаем за результатом.

 while (1)

  {

 	  if (rainbowActive){

 		  rainbow();

 	  }

  }

}
Первая команда
Первая команда
Вторая команда
Вторая команда
Третья команда
Третья команда
Радуга
Радуга

Итог

  • Реализована работа WS2812 на STM32 через ШИМ + DMA.

  • Добавлены режимы RGB и HSV, включая радугу.

  • Управление реализовано через Bluetooth.

В дальнейшем можно расширить проект, добавив:

  • более сложные эффекты (огонь, плавные переходы),

  • интеграцию с мобильным приложением,

  • управление через Wi-Fi (ESP8266/ESP32).

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


  1. Va_sil
    11.09.2025 21:18

    Мда , без DMA нынче никак. Как удалось уместить столь навороченный код в stm32f4 ? Может стоило raspbery pi 4 ?


    1. Danchkin_Sab Автор
      11.09.2025 21:18

      Код, на мой взгляд, не такой и навороченный, главное разобраться в алгоритме работы самой ленты и аккуратно его выполнять

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


      1. randomsimplenumber
        11.09.2025 21:18

        Задача для esp32.


    1. osmanpasha
      11.09.2025 21:18

      Ну вообще DMA - наилучшее решение для такой задачи. А вы что предлагаете, CPU дергать два раза на каждый бит? Зачем, если можно этого не делать.


      1. randomsimplenumber
        11.09.2025 21:18

        Задача решается на ардуине без dma.


        1. VBDUnit
          11.09.2025 21:18

          Решается. Но ИМХО было очень интересно узнать, как подобная задача решается с помощью аппаратных таймеров STM32. В своё время я делал подобное на STM32 Discovery, но интервалы в 400 нс выдерживал с помощью тучи asm("nop"). Без циклов.

          __disable_irq(); //Лочим прерывания
          {
          GPIOC->ODR = VALUE_C0; asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); 
          GPIOC->ODR = VALUE_C1; asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop");
          GPIOC->ODR = VALUE_C2; asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop");
          GPIOC->ODR = VALUE_C3; asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop");
          ...
          100500 строк
          }
          __enable_irq(); //Разрешаем прерывания

          В комментариях умные люди мне объяснили про эти таймеры, а в данной статье ещё и наглядный пример, как с ними управляться в задаче, подобной той, что я решал. И хотя это не совсем мой случай, всё равно, информация очень полезная.

          Аппаратные таймеры STM32 очень навороченные, с кучей разных флагов и режимов, и примеры работы с ними — это хорошо. А уж с Bluetooth — вдвойне.


    1. VBDUnit
      11.09.2025 21:18

      Может стоило raspbery pi 4 ?

      Имхо это из пушки по воробьям. У меня была схожая задача, только лента была не одна а 17. Я пытался, в том числе, решать её и малинкой, т.к. опыта не было, а классические подходы не срабатывали.

      И даже с учётом этого всего малинка — это перебор. А уж для одной ленты тем более. В итоге я остановился как раз на STM32.


      1. An_private
        11.09.2025 21:18

        Подозреваю, что в исходном сообщении сильно не хватает тега sarcasm


      1. osmanpasha
        11.09.2025 21:18

        Малина даже не перебор, она просто для таких задач не предназначена. Для вывода таких критичных ко времени битовых последовательностей там используют либо PWM, либо SPI, и то и то немного костыльно и на 17 лент никак не масштабируется. Всё-таки для каждой задачи нужен свой инструмент, и данном случае это микроконтроллер.


  1. osmanpasha
    11.09.2025 21:18

    В наличии у меня имеется микроконтроллер семейства AVR

    Если вы про STM32, то это ARM.


    1. Danchkin_Sab Автор
      11.09.2025 21:18

      Опечатался, благодарю что заметили


  1. GooseWing
    11.09.2025 21:18

    И чем же эта статья отличается от тонны остальных в интернете включая зарубежные? В коде каждая строка разделена пробелами, зачем? Как такое читать потом?



  1. DungeonLords
    11.09.2025 21:18

    Предлагаю в следующей части вместе HC-06 использовать stm32wb55 где уже есть все необходимое для Bluetooth... Чтобы антенну не разводить, можно взять SOM, такой как STM32WB55MMG


    1. Danchkin_Sab Автор
      11.09.2025 21:18

      Идея хорошая, я думал над этим

      Но никак не мог подобрать модель stm


  1. An_private
    11.09.2025 21:18

    Статья забавная, но применённый подход устарел лет на 5 минимум. Сам в своё время развлекался с blue pill и ws2812. Сейчас куда проще всё это реализовать на esp32.

    А чтобы не заниматься изобретением велосипеда есть FastLED, в котором всё описанное есть и есть намного намного больше.