Skip to content
LED-лента (адресная)

Адресные светодиоды (WS2812B / SK6812)

Один провод данных управляет десятками независимых светодиодов, каждый со своим цветом — на библиотеке FastLED, поверх периферии RMT ESP32.

💡 Адресные светодиоды (WS2812B / SK6812) на ESP32 — FastLED

Учебный проект про адресные LED-ленты: один и тот же провод данных управляет десятками независимых светодиодов, каждый из которых можно зажечь своим цветом. Проект показывает, как это работает “под капотом” (протокол, периферия RMT), и даёт готовый набор из 9 анимационных эффектов (радуга, дыхание, комета, твинкл и др.), которыми можно управлять прямо из Serial-консоли, не перепрошивая плату.

📋 Документация проекта

ФайлЧто внутриКому
THEORY.mdФизика протокола WS2812B/RMT, целочисленная математика эффектов FastLED, архитектура класса LedEffects, поддержка RGBWХочешь понять, как это работает изнутри
WIRING.mdСхема подключения, резистор, расчёт бюджета мощности, согласование уровней 3.3В/5ВСобираешь железо

Архитектура кода

Один класс-драйвер на компонент, без обёрток из свободных функций поверх класса — стандартный паттерн для всех компонентов этого репозитория (см. COMPONENT_STANDARD.md):

├── include/
│   ├── LedEffects.h          # Класс-драйвер: 9 эффектов, машина состояний, isAlive()
│   ├── LedPhysicalLink.h     # Внутренний помощник: RGBW-упаковка (см. THEORY.md, раздел 7.3)
│   └── LedHardwareConfig.h   # ЕДИНАЯ точка настройки чипа, цвет. порядка и режима каналов (RGB/RGBW)
├── src/
│   ├── LedEffects.cpp        # Реализация эффектов (HSV, синусоиды времени, неблокирующие таймеры)
│   ├── LedPhysicalLink.cpp   # Упаковка кадра в 3 или 4 байта/пиксель перед FastLED.show()
│   └── main.cpp              # Точка входа: разбор команд из Serial + вызов LedEffects
├── platformio.ini            # Конфигурация окружения pioarduino
├── THEORY.md                 # Физика WS2812B/RMT, математика эффектов, архитектура класса
└── WIRING.md                 # Схема подключения встроенного диода и внешних лент

LedEffects — единственный публичный класс-драйвер: конструктор получает указатель на буфер CRGB и его длину (Dependency Injection — класс ничего не знает про конкретный GPIO/чип), а весь публичный API — это setEffect() + update() как обязательный минимум, плюс компактные сеттеры с разумными значениями по умолчанию (tickRainbow(speedMs = 20, ...) и т.д.). Именно поэтому отдельный упрощённый слой поверх класса не потребовался — main.cpp использует LedEffects напрямую, без фасада.

LedPhysicalLink — небольшой внутренний помощник (не альтернативный “слой API”, а часть реализации): решает исключительно задачу упаковки RGBW-кадра в 4 байта на пиксель для чипов с отдельным белым каналом (подробности — THEORY.md, раздел 7.3). В обычном 3-канальном (RGB) режиме это нулевые накладные расходы: sync() сразу выходит, физический и логический буферы — один и тот же указатель.

Вотчдог isAlive()

Это компонент-актуатор (прямой GPIO/RMT-выход), а не датчик — “обрыв провода” в классическом смысле неприменимо. strip.isAlive() отвечает на другой, педагогически более вероятный вопрос: вызывается ли update() достаточно регулярно. Если loop() где-то завис (например, на delay() или бесконечном цикле) — анимация молча застынет на одном кадре, а диагностика в консоли покажет !!! ЛЕНТА НЕ ОБНОВЛЯЕТСЯ !!! вместо ACTIVE (тот же принцип, что и в Components/2. LED_middle).

Минимальный пример использования

#include "LedEffects.h"
#include "LedHardwareConfig.h"

constexpr int LED_PIN = 5;
constexpr uint16_t NUM_LEDS = 60;

CRGB leds[NUM_LEDS];
LedEffects strip(leds, NUM_LEDS);   // autoShow = true по умолчанию — этого достаточно для одной ленты

void setup() {
    FastLED.addLeds<LED_CHIPSET, LED_PIN, LED_COLOR_ORDER>(leds, NUM_LEDS);
    strip.setEffect(LedEffects::Effect::RAINBOW);
}

void loop() {
    strip.update();   // ОДИН вызов в loop() — и вся анимация крутится сама
}

Тип чипа (WS2812B / SK6812 / …) и порядок цветов настраиваются один раз в LedHardwareConfig.h — в коде выше про них ничего не сказано. Если нужна RGBW-лента (4-й, белый канал) или несколько независимых лент одновременно — смотри разделы “Выбор типа светодиодов” и “Масштабирование на несколько лент” ниже — там показан полный LedPhysicalLink-вариант из main.cpp этого проекта.

Как менялась архитектура (для понимания читаемого кода)

БылоСталоЗачем
Таймеры на EVERY_N_MILLISECONDS и static внутри методовТаймеры — private-поля объекта (_lastRainbowMs и т.д.)Несколько экземпляров LedEffects (несколько лент) больше не делят таймеры и состояние strobe друг с другом — см. THEORY.md 6.1
tickStrobe() красил только _leds[0]fill_solid(_leds, _numLeds, color)Strobe корректно работает на ленте любой длины
Эффект выбирался комментированием строк в main.cppenum class Effect + update() + словесные Serial-командыПереключение эффекта без повторной прошивки, понятный синтаксис для новичков
Каждый tick*() сам вызывал FastLED.show() без альтернативыФлаг autoShow, вызывающий код сам делает один общий show()Несколько лент на одном стенде без лишних вызовов FastLED.show()THEORY.md 6.3
Только WS2812B зашит в кодеLED_CHIPSET в LedHardwareConfig.h (WS2812B/SK6812/…)Лента SK6812 теперь действительно работает — THEORY.md, раздел 7
Поддерживался только 3-канальный RGBLED_CHANNEL_MODE (LED_MODE_RGB/LED_MODE_RGBW) + класс LedPhysicalLinkЛента SK6812 RGBW (4 канала, с отдельным белым кристаллом) теперь тоже работает корректно — THEORY.md, раздел 7.3
Только 4 эффекта9 эффектов (включая Comet, ColorWipe, Twinkle, Pulse-от-точки)Богаче визуально, проще для новичков — THEORY.md, раздел 8
Посимвольный разбор Serial-командПострочный парсер, словесные команды (“rainbow”, “pulse 12”)Надёжнее и понятнее — THEORY.md, раздел 9.2
Отдельный слой simple_api.h/.cpp (свободные функции-обёртки) поверх классаУбран — main.cpp использует LedEffects/LedPhysicalLink напрямуюСтандарт репозитория прямо запрещает фасад из свободных функций поверх класса; публичный API самого класса уже достаточно компактен

⚙️ Конфигурация окружения (platformio.ini)

Использование форка pioarduino обязательно, так как оригинальная платформа espressif32 в PlatformIO не имеет полноценной поддержки Arduino Core 3.x. Зависимость проекта — fastled/FastLED @ ^3.9.0.


💡 Выбор типа светодиодов: чип, 3 или 4 канала, и почему не NeoPixel

Шаг 1 — какой у вас чип

Если лента не реагирует на код — скорее всего, дело не в проводах, а в том, что чип ленты не совпадает с тем, что зашит в коде (подробная физика — THEORY.md, раздел 7.1). Откройте include/LedHardwareConfig.h и поменяйте одну строку:

#define LED_CHIPSET SK6812   // было WS2812B — поменяйте на чип вашей ленты

Шаг 2 — 3 канала (RGB) или 4 канала (RGBW)

У SK6812 бывают обе версии: обычная (3 канала, как у WS2812B) и RGBW (4 канала — добавлен отдельный белый кристалл). Это тоже одна строка в том же файле:

#define LED_CHANNEL_MODE LED_MODE_RGBW   // или LED_MODE_RGB для 3-канальной ленты

Как понять, какая у вас лента: если после LED_MODE_RGB первые 1-2 диода работают нормально, а дальше по ленте цвета “съезжают”/смешиваются — это RGBW-лента, переключайтесь на LED_MODE_RGBW. Подробное объяснение симптома — THEORY.md, раздел 7.3.

В RGBW-режиме управление белым каналом — через команду white N в Serial-консоли (см. таблицу команд ниже) или напрямую stripLink.setAllWhite(255) в коде. Для RGB-ленты это безопасно, но ничего не делает — физического белого канала там просто нет.

⚠️ Если белый канал светится “не тем” цветом или цвета внутри RGBW перепутаны — поменяйте порядок байт LED_RGBW_BYTE_ORDER_* в том же файле (по умолчанию настроен самый частый вариант GRBW).

Шаг 3 — а не лучше ли использовать Adafruit_NeoPixel?

Закономерный вопрос: у Adafruit_NeoPixel RGBW поддерживается “из коробки” — Adafruit_NeoPixel(n, pin, NEO_GRBW + NEO_KHZ800) и setPixelColor(i, r, g, b, w), без описанного в THEORY.md трюка с переинтерпретацией буфера. Это честно проще, если вам нужна только RGBW-лента без анимаций. Мы тем не менее остаёмся на FastLED — вот честное сравнение:

FastLED (этот проект)Adafruit_NeoPixel
RGBW из коробкиНет — нужен LedPhysicalLink (но он уже написан и спрятан от вас)Да, нативно: setPixelColor(i, r, g, b, w)
Готовая математика эффектов (радуга, синусоиды, шум, угасание, насыщенная арифметика)Да — fill_rainbow, beatsin8, nscale8, fadeToBlackBy, qadd8/qsub8, random8 и т.д.Нет — есть только setPixelColor/fill, остальное пишете сами
Палитры и цветовые градиентыДа, встроеныНет
Опора всего класса LedEffects на этой математикеПолная — переход на NeoPixel означал бы переписать все 9 эффектов
RMT-драйвер на ESP32Свой, FASTLED_RMT_BUILTIN_DRIVER, активно поддерживается под новые чипы (включая ESP32-C6)Свой (другой) RMT-драйвер; в сообществе периодически всплывают проблемы с заявкой RMT-канала при использовании совместно с другими RMT-устройствами

Итог: если бы это был только “зажечь RGBW-ленту ровным светом” — NeoPixel был бы проще и короче. Но раз вся ценность проекта — в эффектах (радуга, дыхание, комета, твинкл, пульс и т.д.), переезд на NeoPixel означал бы выбросить готовую, проверенную математику FastLED и написать её самостоятельно. Подробный разбор — THEORY.md, раздел 7.4.


🐧 Настройка хост-системы Linux (при работе через Native USB)

Для работы с портом Native USB (/dev/ttyACM*) обычному пользователю требуются права на чтение и запись.

sudo usermod -a -G dialout,plugdev $USER

После этого необходимо перезагрузить компьютер (или хотя бы выйти и снова войти в графическую сессию), чтобы новые группы применились. Экстренный фикс без перезагрузки:

sudo chmod 666 /dev/ttyACM*

🚀 Как запустить

  1. Подключите ленту по схеме из WIRING.md и плату — к компьютеру по USB.
  2. Соберите, залейте прошивку и откройте монитор порта:
    pio run -t upload -t monitor
  3. При старте лента на секунду вспыхивает сплошным белым (проверка, что все диоды подключены и чип задан верно), затем автоматически запускается эффект Rainbow.

🎛️ Управление эффектами без повторной прошивки

Во время работы можно переключать эффекты прямо в мониторе порта — наберите слово (или короткую букву) и нажмите Enter:

КомандаДействие
solidSolid — сплошной белый
rainbow / rRainbow — бегущая радуга
breath / bBreath — плавное дыхание цветом Aqua
strobe / sStrobe — стробоскоп Red/Blue
comet / cComet — бегущая комета с угасающим хвостом
wipe / wColor Wipe — заливка цветом по очереди, диод за диодом
twinkle / tTwinkle — случайно мерцающие звёзды
pulse / pPulse — волна, расходящаяся от середины ленты
pulse NPulse — волна, расходящаяся от диода номер N (например pulse 12)
off / 0Off — погасить ленту
white NУровень белого канала N (0..255) — только для RGBW-ленты (LED_MODE_RGBW)
+ / -Программная яркость ярче/тусклее
help / hПоказать справку по командам ещё раз

Если команда не распознана, прошивка явно ответит [CMD] Неизвестная команда: "..." — это удобный способ проверить, что Serial физически работает, даже если вы просто ошиблись в слове. Подробнее о том, почему построчный разбор надёжнее посимвольного — THEORY.md, раздел 9.2.

Раз в секунду в консоль выводится диагностическая строка:

Status: ACTIVE | Brightness: 255 | Free SRAM: 430124 bytes

Status берётся из strip.isAlive() — если вместо ACTIVE вы видите !!! ЛЕНТА НЕ ОБНОВЛЯЕТСЯ !!!, значит loop() где-то завис (см. раздел “Вотчдог isAlive()” выше).


🚨 Диагностика неисправностей

СимптомПричинаРешение
Ошибка could not open port /dev/ttyACM*Порт заблокирован, или плата сменила индекс (ttyACM0ttyACM1).Переподключите кабель, sudo chmod 666 /dev/ttyACM*. Перед прошивкой зажмите BOOT, нажмите RST, отпустите BOOT.
В консоли циклический мусор (Guru Meditation Error)Ошибка инициализации RMT-драйвера или битая Flash-память.Убедитесь, что #define FASTLED_RMT_BUILTIN_DRIVER 1 объявлен строго до #include <FastLED.h> в LedEffects.h.
Лента совсем не светится / мигает хаотично, хотя провода в порядкеВ LedHardwareConfig.h указан не тот чип (например, WS2812B вместо реальной SK6812).Смените LED_CHIPSET в LedHardwareConfig.h — см. раздел “Выбор типа светодиодов” выше и THEORY.md, 7.1.
Первые 1-2 диода работают нормально, дальше цвета “съезжают”/смешиваютсяЛента — это SK6812 RGBW (4 канала), а в LedHardwareConfig.h стоит LED_MODE_RGB.Поставьте LED_CHANNEL_MODE = LED_MODE_RGBW — см. раздел “Выбор типа светодиодов” выше и THEORY.md, 7.3.
Белый канал светится не тем цветом / RGBW-цвета перепутаныПорядок 4 байт в потоке у вашей конкретной партии ленты не GRBW.Поменяйте LED_RGBW_BYTE_ORDER_* в LedHardwareConfig.h.
Команды в Serial-мониторе “не работают”Чаще всего — забыли нажать Enter после ввода слова (так работает фильтр send_on_enter в platformio.ini).Наберите команду и нажмите Enter. Если после этого видите [CMD] Неизвестная команда: ... — Serial работает, просто проверьте написание слова. Если вообще ничего не печатается — проверьте monitor_speed = 115200 и что используется верный /dev/ttyACM*.
Один-два диода горят одним цветом или хаотично мигают белым, остальные в порядкеСкорее всего дефект самого диода в цепочке, либо отсутствует общий GND при подключении внешней ленты (см. WIRING.md).Проверьте общий провод земли; если не помогает — вероятен брак конкретного диода.
Strobe мигает только на первом диоде ленты, остальные “замёрзли”Используется версия класса до v2.0 (_leds[0] = ... вместо fill_solid).Обновите LedEffects.cpp до текущей версии — см. THEORY.md, раздел 5.4.
При двух лентах эффекты “путают” скорость или фазу друг другаИспользована версия класса до v2.0 со static-таймерами внутри методов.Обновите класс до текущей версии — таймеры теперь поля объекта, см. THEORY.md, раздел 6.1.
В консоли !!! ЛЕНТА НЕ ОБНОВЛЯЕТСЯ !!! вместо ACTIVEupdate() не вызывается достаточно часто — loop() где-то завис (например, на delay() или в бесконечном цикле).Проверьте, не добавили ли вы блокирующий код в loop().

📈 Масштабирование на несколько лент

Класс LedEffects готов к одновременной работе с несколькими независимыми лентами:

CRGB ledsA[60];
CRGB ledsB[1];

// autoShow = false на ОБОИХ экземплярах — см. THEORY.md, раздел 6.3
LedEffects stripA(ledsA, 60, /*autoShow=*/false);
LedEffects stripB(ledsB, 1,  /*autoShow=*/false);

void loop() {
    stripA.update();
    stripB.update();
    FastLED.show(); // один общий вызов на весь кадр сразу для всех лент
}

Подробное объяснение того, почему именно так, а не иначе — THEORY.md, раздел 6.3 и 6.4.

RGBW-лента через LedPhysicalLink

Если лента RGBW (4 канала), используйте LedPhysicalLink — именно так устроен main.cpp этого проекта:

#include "LedEffects.h"
#include "LedPhysicalLink.h"

LedPhysicalLink stripLink(60, /*rgbw=*/true);
LedEffects strip(stripLink.logicalBuffer(), 60, /*autoShow=*/false);

void setup() {
    FastLED.addLeds<SK6812, 8, GRB>(stripLink.physicalBuffer(), stripLink.physicalSlotCount());
    strip.setEffect(LedEffects::Effect::RAINBOW);
}

void loop() {
    strip.update();
    stripLink.setAllWhite(128);           // независимое управление белым каналом
    stripLink.sync(strip.getBrightnessScale());
    FastLED.show();
}

LedEffects здесь, как и везде в проекте, работает только с обычным 3-канальным буфером и ничего не знает про White — RGBW-специфика целиком в LedPhysicalLink.


📝 Разбор используемых технологий

  • FastLED (v3.9.0+): оптимизированная 8-битная математика цвета (HSV-пространство), целочисленные вычисления без float.
  • Неблокирующие таймеры на полях объекта: в коде полностью исключена функция delay() в рабочих эффектах. Каждый экземпляр класса хранит собственные отметки времени, что делает диспетчеризацию задач по-настоящему независимой между несколькими лентами — подробности в THEORY.md.

Связанные материалы

  • Резистор — токоограничение и расчёт мощности; для ленты особенно важен бюджет питания.
  • Ambilight — проект на адресной ленте: подсветка, повторяющая цвета экрана.