Код, который светится: архитектура минималистичных световых скетчей
Микроконтроллеры, светодиоды, и немного кода — вот и вся палитра для минималистичного цифрового искусства. В статье подробно рассказывается, как выстроить архитектуру крошечных, но выразительных световых анимаций с использованием C++, платформы STM32 и адресных светодиодов WS2812. Немного философии, немного инженерии — и свет оживает по команде вашего кода.
Можно потратить годы, чтобы написать красивый рендерер. А можно взять 8 строк кода, светодиодную ленту и микроконтроллер, чтобы ночью на стене заиграла световая поэма. Эта статья — про второй путь.
Код, который светится, не имеет интерфейса, не показывает графику на экране и не заботится о фреймрейте. Его задача — свет. Живой, дышащий, мерцающий свет. В идеале — чтобы всё это поместилось в пару килобайт памяти и не жрало больше миллиампера на эффект.
Почему минимализм?
Потому что он вынуждает быть изобретательным. Если у вас всего один байт на канал, значит, каждый цвет должен быть уместен. Если у вас один цикл на кадр — архитектура должна быть кристально чистой. Минимализм в световых скетчах — это не стиль, это ограничение, которое делает конечный результат интереснее.
Базовая архитектура светового скетча
Минималистичный световой скетч можно представить как петлю из трёх фаз:
Обновление состояния анимации
Рендеринг буфера (массив цветов)
Вывод на ленту (или матрицу)
Для примера — используем STM32F103 (он же "blue pill") и ленту WS2812 (aka NeoPixel). Код пишется на C++ с использованием STM32 HAL. Можно адаптировать под Arduino, но тогда будет меньше гибкости.
Подключение
WS2812 — цифровая адресная лента. Подключаем DATA вход к любому GPIO (например, PA7), через резистор 330 Ом. Питание строго 5 В, можно через DC-DC с LDO.
STM32F103C8T6 (PA7) --> 330 Ом --> DATA WS2812
GND --> GND
5V --> VCC WS2812
💡 Важно: WS2812 работает на 5 В, тогда как логика STM32 — 3.3 В. Иногда это прокатывает, но если вдруг эффекты мигают или не запускаются — ставим логический преобразователь уровней.
📷 Схема подключения:
Минимальный код: заглушка с одной анимацией
#include "main.h"
#include "ws2812b.h"
#define LED_COUNT 30
RGB_Color leds[LED_COUNT];
uint32_t millis = 0;
void update_animation() {
for (int i = 0; i < LED_COUNT; ++i) {
uint8_t brightness = (uint8_t)((sinf((millis + i * 20) * 0.01f) + 1.0f) * 127.5f);
leds[i] = RGB_Color{brightness, 0, 255 - brightness};
}
}
int main(void) {
HAL_Init();
SystemClock_Config();
WS2812B_Init();
while (1) {
update_animation();
WS2812B_Send(leds, LED_COUNT);
HAL_Delay(16); // ~60 FPS
millis += 16;
}
}
Здесь RGB_Color
— простая структура:
struct RGB_Color {
uint8_t r, g, b;
};
Кратко о библиотеке ws2812b.h
На Habr нет смысла повторять реализацию готовых библиотек. Но если хочется написать свою — понадобится использовать SPI или TIM + DMA. SPI проще, но требует некоторого трюка с временным кодированием битов в биты. Например:
логическая 1: 0b11100000
логический 0: 0b10000000
Это позволяет кодировать сигналы, совместимые с протоколом WS2812, через SPI, при правильной частоте (около 2.4 Мбит/с).
Анимационные паттерны: трёхбайтная философия
Минималистичные скетчи живут на ограничениях. Можно придумать себе правило: один эффект = не больше 256 байт данных и не больше 128 тактов на кадр.
Примеры паттернов:
Волна дыхания
uint8_t brightness = (uint8_t)((sinf(millis * 0.005f) + 1.0f) * 127.5f);
for (int i = 0; i < LED_COUNT; ++i)
leds[i] = RGB_Color{brightness, 0, 0};
Цветовой шум
for (int i = 0; i < LED_COUNT; ++i) {
uint8_t r = rand() % 256;
uint8_t g = rand() % 256;
uint8_t b = rand() % 256;
leds[i] = RGB_Color{r, g, b};
}
Модулируем архитектуру: эффект как стратегия
Создаём интерфейс эффекта:
class Effect {
public:
virtual void update(uint32_t time, RGB_Color* buffer, int count) = 0;
};
Пример реализации:
class BreathingRed : public Effect {
public:
void update(uint32_t time, RGB_Color* buffer, int count) override {
uint8_t brightness = (uint8_t)((sinf(time * 0.005f) + 1.0f) * 127.5f);
for (int i = 0; i < count; ++i)
buffer[i] = RGB_Color{brightness, 0, 0};
}
};
В main()
:
Effect* current = new BreathingRed();
while (1) {
current->update(millis, leds, LED_COUNT);
WS2812B_Send(leds, LED_COUNT);
HAL_Delay(16);
millis += 16;
}
Идеи для экспансии
Реализация реакций на звук (через аналоговый микрофон и FFT)
Управление через UART или Bluetooth (например, с ESP32)
Синхронизация с другими микроконтроллерами (через I2C или простую синхроимпульсную линию)
Генеративная анимация через случайные деревья или L-системы
Немного личного
Однажды я воткнул подобный контроллер в стеклянную вазу, обмотал светодиодной нитью, заклеил горячим клеем и забыл. Через год — включил. Всё работает. Код живёт. Свет — до сих пор красивый. Ни одна HTML-кнопка не вызывает таких эмоций, как случайно замирающий на миг синий огонёк, задумчиво моргающий в углу комнаты.
Заключение
Минималистичные световые скетчи — это не просто игрушки. Это способ заглянуть в суть кода. У него нет фронтенда, нет API, нет логов. Есть только ты, железо и свет. Всё остальное — шелуха.