22г. Адресные светодиоды: WS2812B, SK6812 RGBW и SK6812WWA — протокол, тайминги, SPI, таймер с DMA, питание и уровни
Раздел 22.4 показал главное: лента управляется по одному проводу, бит кодируется длительностью импульса, таймер с DMA выдаёт его без участия процессора. Здесь — то, на чём спотыкаются дальше. Почему лента «работала и перестала» с новой катушкой. Чем SK6812 отличается от WS2812B и почему код от одной ленты не всегда годится для другой. Как выглядят посылки RGBW и тёплого белого SK6812WWA. Три способа вывода — программно, через SPI и таймером с DMA — с расчётом запаса по времени, памяти и загрузки процессора. И электрика: ток ленты, падение напряжения вдоль неё, уровень сигнала 3,3 В против 5 В.
Опоры: RM0008 Rev 17 (STM32F1: таблица 76 — разрядность DMA, с. 278;
таблица 78 — запросы DMA1, с. 281); DocID13587 Rev 17 (STM32F103x8/xB:
вывод PB6 — FT, TIM4_CH1, с. 32–33; таблица 36 — выходные уровни, с. 65).
Тайминги WS2812B и SK6812 — по распространённым редакциям даташитов
производителей (Worldsemi и Opsco); в проекте этих документов нет, поэтому
цифры окон оговорены в подписи в конце. Все числа ниже — расчёт
_verify/leds22b/ref22b.py (Python 3.11, образ stm-lab:1) и прогон стенда
sim22b.c на компьютере (gcc 12.2.0) и в QEMU 7.2.22 на Cortex-M3
(arm-none-eabi-gcc 12.2.1).
Как это называется в документации
Оригинал Перевод addressable LED, smart pixel адресный светодиод data in / data out (DIN, DOUT) вход и выход данных cascade, daisy chain цепочка, каскад T0H, T0L, T1H, T1L высокий и низкий уровень бита 0 и бита 1 reset code (RES, TRST) пауза сброса, защёлка кадра GRB, GRBW порядок цветов в посылке warm white, natural white, cold white тёплый, нейтральный, холодный белый amber янтарный bit-banging программный вывод, «ногодрыг» half transfer, transfer complete (HT, TC) половина передачи, конец передачи level shifter преобразователь уровня gamma correction гамма-коррекция power injection подвод питания по длине ленты
22г.1 Как устроен чип: сдвиг по цепочке и защёлка паузой
В каждом светодиоде — маленькая микросхема с одним входом данных DIN и
одним выходом DOUT. Такта нет: приёмник сам отмеряет длительность
каждого импульса и решает, ноль это или единица. Поэтому протокол и
жёсткий по времени — общего такта, который «подождал» бы медленного
передатчика, не существует.
Первый чип в цепочке забирает себе первые 24 бита (у RGBW — 32) и дальше
работает как повторитель: всё, что приходит следом, он восстанавливает
по форме и отдаёт на DOUT. Второй чип забирает следующие 24 бита и так
далее. Адреса у светодиода нет — «адрес» — это его место в цепочке.
Принятые биты не сразу попадают на кристаллы: чип держит их во входном регистре. Новые цвета загораются у всех светодиодов одновременно, когда линия простоит в нуле дольше паузы сброса. После паузы каждый чип снова готов забрать «свои» первые биты. Отсюда два следствия:
- пауза короче сброса — это не конец кадра: следующие биты пройдут сквозь заполненные чипы дальше, за конец ленты, и пропадут;
- кадр можно не дописывать до конца: если послать данные только для первых десяти светодиодов, обновятся только они.
22г.2 Тайминги WS2812B и SK6812: окна, а не числа
Даташит задаёт не одну длительность, а окно — номинал и допуск ±0,15 мкс. Расчёт ниже ведётся по таким окнам:
| WS2812B | SK6812 | |
|---|---|---|
| бит 0: высокий уровень T0H | 0,40 мкс | 0,30 мкс |
| бит 0: низкий уровень T0L | 0,85 мкс | 0,90 мкс |
| бит 1: высокий уровень T1H | 0,80 мкс | 0,60 мкс |
| бит 1: низкий уровень T1L | 0,45 мкс | 0,60 мкс |
| пауза сброса | ≥ 280 мкс у новых партий, ≥ 50 мкс у ранних | ≥ 80 мкс |
Таймер F103 считает на 72 МГц: такт — 13,89 нс, бит 1,25 мкс — 90 тактов,
ARR = 89. Точные значения CCR — 28,8 и 57,6 такта для WS2812B, 21,6 и
43,2 — для SK6812; их округляют. Запас — насколько каждая из четырёх
длительностей отстоит от края своего окна, берётся худшая:
| CCR для 0 и 1 | T0H / T0L / T1H / T1L, нс | Запас WS2812B | Запас SK6812 |
|---|---|---|---|
| 29 / 58 (округление WS2812B) | 403 / 847 / 806 / 444 | 144 нс | −56 нс |
| 28 / 57 (отбрасывание, как в 22.4) | 389 / 861 / 792 / 458 | 139 нс | −42 нс |
| 22 / 43 (округление SK6812) | 306 / 944 / 597 / 653 | −53 нс | 97 нс |
| 25 / 50 (общий вариант) | 347 / 903 / 694 / 556 | 44 нс | 56 нс |
Отрицательный запас — выход за окно. Единица WS2812B (0,81 мкс) для
SK6812 слишком длинная: её окно кончается на 0,75 мкс. Обратное тоже
неверно: единица SK6812 (0,60 мкс) для WS2812B коротка — его окно
начинается с 0,65 мкс. Поэтому
«универсальная» библиотека с таймингами одной ленты на другой работает
через раз. Есть пары, годные обоим, — перебор всех CCR при ARR = 89
даёт 120 таких пар, и лучшая из них по худшему запасу даёт те же 44 нс,
что и 25 / 50. Её и берёт прошивка этого раздела.
#define WS_TIM_HZ 72000000u
#define WS_ARR 89u /* период 90 тактов = 1,25 мкс */
#define WS_TICKS(ns) (((ns) * (WS_TIM_HZ / 1000000u) + 500u) / 1000u) /* с округлением */
#define WS2812_CCR0 WS_TICKS(400u) /* 0,4 мкс → 28,8 → 29 тактов */
#define WS2812_CCR1 WS_TICKS(800u) /* 0,8 мкс → 57,6 → 58 тактов */
#define SK6812_CCR0 WS_TICKS(300u) /* 0,3 мкс → 21,6 → 22 такта */
#define SK6812_CCR1 WS_TICKS(600u) /* 0,6 мкс → 43,2 → 43 такта */
#define ANY_CCR0 25u /* 0,347 мкс: в окне у обоих чипов */
#define ANY_CCR1 50u /* 0,694 мкс: тоже */
Почему код «работал и перестал». У ранних WS2812B пауза сброса — от 50 мкс, у новых партий — от 280 мкс. Программа, написанная под старую ленту, делает между кадрами паузу 50 мкс — 40 периодов бита. Новая лента такую паузу не считает концом кадра: биты второго кадра проходят сквозь заполненные чипы за конец ленты. Стенд моделирует цепочку из трёх чипов и три кадра: красный и зелёный с паузой 50 мкс, синий — с паузой 300 мкс. Чип с порогом 50 мкс показывает красный, зелёный, синий. Чип с порогом 280 мкс после первых двух кадров не показывает ничего, а после третьего загорается красным — первым кадром после прошлого настоящего сброса. Если кадры идут непрерывно, такая лента замирает или обновляется изредка, когда программа случайно задержится. Лечится одним числом: паузой 300 мкс (240 периодов) — её хватает всем партиям.
Попробуйте в приложении: Лаборатория → стенд «Адресные светодиоды».
22г.3 Порядок байтов: GRB, GRBW и три белых канала
WS2812B и SK6812 RGB — 24 бита на светодиод, байты в порядке зелёный,
красный, синий (GRB), старший бит первым. Оранжевый FF 80 00 в
привычном RGB уходит в ленту как 80 FF 00.
SK6812 RGBW — 32 бита: к трём цветам добавлен отдельный белый
кристалл, его байт идёт четвёртым (GRBW). ws_put_rgbw(…, 0x10, 0x20, 0x30,
0x40) даёт в буфере 20 10 30 40. Белый кристалл — не смесь трёх
цветов, а люминофорный светодиод: свет ровнее, и белый выходит без
синеватого или розоватого оттенка. Его выпускают в нескольких вариантах —
тёплый, нейтральный и холодный белый; какой стоит в катушке, указывают в
названии товара, цветовую температуру — в даташите партии. Посылка на
светодиод на треть длиннее, и кадр из 300 светодиодов идёт 12,08 мс против
9,30 мс у RGB (вместе с паузами сброса 80 и 300 мкс), то есть не чаще 82
кадров в секунду против 107.
SK6812WWA — тот же чип и тот же протокол, 24 бита, но вместо красного,
зелёного и синего — два белых разной цветовой температуры и янтарный.
Такая лента нужна там, где важен оттенок белого: подсветка, свет «как от
лампы накаливания». Порядок трёх байтов и оттенки каналов у разных партий
и продавцов описаны по-разному, поэтому не угадывайте: включите по
очереди один байт из трёх (FF 00 00, 00 FF 00, 00 00 FF) и
запишите, какой канал загорелся, — и сверьтесь с даташитом своей катушки.
22г.4 Программный вывод: почему на 72 МГц — на грани
Самый очевидный способ — дёргать вывод из цикла с подсчитанными задержками. Допуск ±0,15 мкс при 72 МГц — 10,8 такта. Вход в прерывание у Cortex-M3 занимает 12 тактов (раздел 7а), то есть 167 нс: одно прерывание, пришедшее во время высокого уровня, растягивает его за окно, и бит читается неверно. Значит, на всю посылку прерывания запрещают.
Посылка 300 светодиодов длится 9000 мкс — девять миллисекунд без прерываний. Отсчёт SysTick раз в миллисекунду за это время пропустит 8 тиков: флаг ожидания у прерывания один, и девять событий сливаются в одно — системные часы отстанут на 8 мс за кадр. Приём UART за 9 мс потеряет байты.
Вторая беда — сам цикл. При 72 МГц флеш-память работает с двумя тактами
ожидания (раздел 6), время выполнения зависит от выравнивания кода и
уровня оптимизации. Код, точно подогнанный при -O2, при -Os или после
правки соседней функции даёт другие длительности. Годится такой способ
для десятка светодиодов в прототипе, не для изделия.
22г.5 SPI: три бита SPI на бит ленты
SPI выдаёт биты с постоянным шагом, и DMA подаёт ему байты без процессора.
Каждый бит ленты кодируют группой бит SPI: ноль — 100, единица — 110.
SPI1 сидит на шине APB2 (72 МГц); делитель 32 (BR = 100) даёт 2,25 МГц,
бит SPI — 0,444 мкс:
| T0H | T0L | T1H | T1L | Запас WS2812B | Запас SK6812 | |
|---|---|---|---|---|---|---|
2,25 МГц, 100 / 110 |
444 нс | 889 нс | 889 нс | 444 нс | 61 нс | −139 нс |
4,5 МГц, 110000 / 111000 |
444 нс | 889 нс | 667 нс | 667 нс | −67 нс | 6 нс |
| 9 МГц, 11 бит: 3 / 5 единиц | 333 нс | 889 нс | 556 нс | 667 нс | −94 нс | 83 нс |
Для WS2812B «100/110» годится с запасом 61 нс, и это лучший вариант на этой частоте — перебор групп от 2 до 16 бит лучше не находит. Для SK6812 та же запись выходит за окно: единица длится 0,889 мкс при пределе 0,75 мкс — на 139 нс. Ближайшие частоты SPI1 (делители 16 и 8) дают SK6812 либо всего 6 нс запаса при 6 битах на бит, либо 83 нс — но ценой 11 бит SPI на бит ленты: 33 байта на светодиод RGB и 44 на RGBW вместо 9 и 12. Таймеру такая проблема не грозит: шаг CCR — 13,89 нс, а не 444 нс.
Кодировщик один на все варианты — ws_spi_encode(dst, src, n, nbits, ones0,
ones1). Байт A5 в записи «100/110» превращается в три байта D3 49 A6.
Стенд раскладывает закодированные A5 00 FF обратно в длительности:
единицы — только по 1 и 2 бита SPI, нули — по 1 и 2, ничего лишнего.
SPI1->CR1 = SPI_CR1_MSTR | SPI_CR1_SSM | SPI_CR1_SSI
| SPI_CR1_BR_2 /* BR = 100: 72 МГц / 32 = 2,25 МГц */
| SPI_CR1_SPE;
SPI1->CR2 = SPI_CR2_TXDMAEN;
DMA1_Channel3->CPAR = (uint32_t)&SPI1->DR;
DMA1_Channel3->CCR = DMA_CCR3_MINC | DMA_CCR3_DIR; /* 8 бит, один проход */
Запрос SPI1_TX идёт в канал 3 DMA1 (таблица 78 RM0008). Две тонкости.
Последний байт буфера — нулевой: тогда линия MOSI после передачи
остаётся в нуле. А паузу сброса удобнее отсчитать таймером, чем тратить на
неё буфер: 280 мкс при шаге 0,444 мкс — это 630 бит SPI, 79 байт нулей.
22г.6 Таймер и DMA: двойной буфер
Способ из 22.4: таймер в режиме ШИМ с периодом бита, DMA на каждом
обновлении пишет в CCR1 следующую длительность. В изделии на F103 берут
TIM4_CH1 на выводе PB6: запрос TIM4_UP идёт в канал 7 DMA1
(таблица 78 RM0008), а PB6 допускает 5 В (FT, DocID13587, с. 32) — это
пригодится в 22г.9.
Полный буфер — 24 полуслова на светодиод — для длинной ленты дорог (22г.7). Двойной буфер обходится 96 отсчётами: DMA читает их по кругу, прерывание на половине (HT) и в конце (TC) сообщает, какая половина освободилась, и программа заполняет её следующими битами. Половина — 48 отсчётов, два светодиода RGB, 60 мкс, 4320 тактов процессора между прерываниями.
TIM4->PSC = 0; /* 72 МГц: APB1 36 МГц × 2 */
TIM4->ARR = WS_ARR; /* 90 тактов = 1,25 мкс */
TIM4->CCR1 = 0;
TIM4->CCMR1 = TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_1 /* ШИМ, режим 1 */
| TIM_CCMR1_OC1PE; /* CCR1 в силе с обновления */
TIM4->CCER = TIM_CCER_CC1E;
TIM4->CR1 = TIM_CR1_ARPE;
TIM4->DIER = TIM_DIER_UDE; /* запрос DMA на каждом обновлении */
DMA1_Channel7->CPAR = (uint32_t)&TIM4->CCR1;
DMA1_Channel7->CMAR = (uint32_t)ws.buf;
DMA1_Channel7->CCR = DMA_CCR7_MINC | DMA_CCR7_CIRC | DMA_CCR7_DIR
| DMA_CCR7_MSIZE_0 | DMA_CCR7_PSIZE_0 /* 16 бит → 16 бит */
| DMA_CCR7_HTIE | DMA_CCR7_TCIE | DMA_CCR7_PL_1;
Предзагрузка OC1PE обязательна: DMA пишет в CCR1 в начале периода, и
без предзагрузки новое значение сработало бы в этом же периоде — импульс
мог бы оборваться или удвоиться (теневые регистры — раздел 13.4). С
предзагрузкой выход отстаёт от DMA на один период, а первый период идёт с
CCR1 = 0 — это лишь чуть удлиняет паузу перед кадром.
Заполнение — одна функция: биты из байтов цвета, старший первым, а после
данных — нули. CCR = 0 держит линию в нуле весь период; так пауза
сброса получается сама, без отдельного таймера. Остановить таймер можно не
сразу, как поток кончился, а только когда DMA дочитает последнюю
заполненную половину:
int ws_dma_irq(ws_dma_t *d, uint32_t half)
{
/* Половина half только что прочитана, DMA читает другую — эту можно переписать. */
int end = ws_stream_fill(&d->s, &d->buf[half * WS_HALF], WS_HALF);
if (d->after_end || end)
d->after_end++;
/* Конец потока лёг в половину, заполненную после прерывания k. DMA прочтёт её
к прерыванию k + 2 — только тогда таймер можно остановить. */
return d->after_end >= 3u;
}
void DMA1_Channel7_IRQHandler(void)
{
uint32_t isr = DMA1->ISR;
uint32_t half = (isr & DMA_ISR_TCIF7) ? 1u : 0u; /* TC — вторая половина свободна */
DMA1->IFCR = DMA_IFCR_CHTIF7 | DMA_IFCR_CTCIF7 | DMA_IFCR_CGIF7;
if (ws_dma_irq(&ws, half)) {
TIM4->CR1 &= ~TIM_CR1_CEN; /* на линии ноль: CCR давно 0 */
DMA1_Channel7->CCR &= ~DMA_CCR7_EN;
ws_busy = 0;
}
}
Стенд моделирует DMA по кругу и прерывания с опозданием и сравнивает каждый прочитанный отсчёт с полным буфером:
| Лента | Пауза сброса | Опоздание прерывания | Отсчётов прочитано | Ошибок | Нулей после данных |
|---|---|---|---|---|---|
| 300 RGB | 240 | 0 | 7440 | 0 | 240 |
| 300 RGB | 240 | 48 периодов (60 мкс) | 7488 | 0 | 288 |
| 300 RGB | 240 | 49 периодов | 7489 | 62 | 289 |
| 1 RGB | 240 | 0 | 288 | 0 | 264 |
| 300 RGB | 64 (SK6812) | 0 | 7296 | 0 | 96 |
Прерывание может опоздать (вместе со временем заполнения) на целую половину — 48 периодов, 60 мкс. На один период больше — DMA читает половину, которую ещё не переписали, и 62 отсчёта уходят со старыми битами. Поэтому приоритет у этого прерывания высокий, а длинные участки с запретом прерываний в остальной программе недопустимы — 60 мкс это предел.
22г.7 Память и процессор: что выбрать
Для ленты из 300 светодиодов (F103C8 — 20 КБ ОЗУ, 20 480 байт):
| Способ | Буфер RGB | Буфер RGBW | Процессор во время вывода |
|---|---|---|---|
| программный вывод | 0 | 0 | занят весь кадр, прерывания запрещены 9000 мкс |
| SPI, 2,25 МГц, 3 бита | 2700 байт | 3600 байт | свободен; кодирование кадра заранее |
| SPI, 9 МГц, 11 бит (SK6812) | 9900 байт | 13 200 байт | свободен; кодирование заранее |
| таймер, полный буфер 16 бит | 14 400 байт (70,3 %) | 19 200 байт (93,8 %) | свободен |
| таймер, полный буфер 8 бит | 7200 байт | 9600 байт | свободен |
| таймер, двойной буфер | 192 байта | 192 байта | прерывание раз в 60 мкс |
Сами цвета — ещё 900 байт для RGB и 1200 для RGBW, они нужны при любом способе. Хвост нулей сброса в полном буфере — 240 отсчётов, 480 байт.
Полный буфер можно уполовинить: DMA F1 читает из памяти байт и пишет в
16-битный регистр, дополняя старшие биты нулями (таблица 76 RM0008).
CCR не больше 89 помещается в байт, так что MSIZE = 8 бит, PSIZE = 16
бит — и 24 байта на светодиод вместо 48.
Итог: для коротких лент годится любой аппаратный способ; для 300 светодиодов RGBW полный буфер съедает почти всё ОЗУ — двойной буфер. SPI проще всех, но для SK6812 на частотах SPI1 у F103 с «100/110» не выходит. Прошивка раздела — таймер, двойной буфер и общие тайминги 25 / 50: 1168 байт кода и 400 байт ОЗУ при 60 светодиодах.
22г.8 Питание: ток, падение вдоль ленты, конденсатор и резистор
Ток светодиода пропорционален яркости канала: внутри чипа — источник тока на каждый кристалл и ШИМ. Распространённая оценка — около 20 мА на канал при значении 255, до 60 мА на светодиод RGB в белом и до 80 мА у RGBW. Лента из 300 штук в полном белом — 18 А (90 Вт при 5 В), RGBW — 24 А. Точные токи у разных производителей разные; для бюджета блока питания берут эти верхние оценки или меряют свою катушку.
Медь ленты тонкая. Пример: шины шириной 10 мм из фольги 35 мкм — 0,0491 Ом на метр каждая, 0,0983 Ом на метр петли «плюс — минус». Модель: 60 светодиодов на метр, по 60 мА, ток не зависит от напряжения. Наибольшая просадка:
| Лента и подвод питания | Просадка |
|---|---|
| 1 м, питание с одного конца | 0,17 В |
| 5 м, с одного конца | 4,41 В |
| 5 м, с обоих концов | 1,10 В |
| 5 м, подвод через каждый метр | 0,04 В |
Четыре с лишним вольта — это не точное число, а знак, что модель здесь неприменима: в конце пятиметровой ленты, запитанной с одного края, светодиоды просто не получат напряжения. Первыми гаснут синий и зелёный — им нужно больше напряжения, чем красному, поэтому белый к концу ленты желтеет и краснеет. Лечится подводом питания с обоих концов, у длинной ленты — ещё и в середину и через каждый метр-два, проводом толще дорожек ленты.
Ещё три правила подключения (рисунок):
- Конденсатор 1000 мкФ между +5 В и землёй у входа ленты. Лента включает и гасит амперы за микросекунды; без запаса энергии у входа напряжение проседает и звенит на проводах питания.
- Резистор 300–500 Ом последовательно в линии данных, ближе к передатчику. Он гасит звон на проводе и ограничивает ток во вход, когда лента обесточена, а микроконтроллер уже выдаёт единицу: через защитные диоды входа не больше 11,0 мА при 300 Ом и 6,6 мА при 500 Ом.
- Общая земля микроконтроллера и ленты, а провод данных — рядом с проводом земли. Без общей земли уровни сигнала теряют смысл.
22г.9 Уровень сигнала: 3,3 В против 5 В
Вход DIN считает единицей напряжение не ниже 0,7·VDD. При питании 5 В
это 3,50 В. Выход F103 при питании 3,3 В даёт без нагрузки около 3,3 В —
на 0,20 В меньше порога; гарантированный минимум по таблице 36
DocID13587 — VDD − 0,4 = 2,9 В, на 0,60 В меньше. То, что лента всё же
часто работает, — заслуга реальных порогов, которые обычно ниже
гарантированных. Но реальный порог зависит от партии, температуры и
напряжения питания ленты: при 4,5 В (просадка) порог — 3,15 В, и лента
работает, а при полных 5 В у новой катушки — «то работает, то нет».
Отсюда типичная загадка: на столе от лабораторного источника всё в
порядке, в изделии первый светодиод мигает случайными цветами.
Решения, от надёжного к простому:
- Буфер с входом ТТЛ-уровня, питаемый от 5 В: 74AHCT125 или 74HCT245. Их порог единицы — 2,0 В, запас 1,3 В от 3,3 В и 0,9 В даже от гарантированных 2,9 В. Буква T в названии обязательна: у 74HC125 и 74AHC125 при питании 5 В порог тот же 0,7·VDD, и они ничего не исправят.
- «Жертвенный» первый светодиод. Его питают через кремниевый диод:
VDD около 4,3 В, порог 0,7·4,3 = 3,01 В — на 0,29 В ниже 3,3 В. Его
выход
DOUTвыдаёт уже 4,3-вольтовые импульсы, которые остальная лента принимает уверенно. Цена — один светодиод работает при пониженном питании, а при гарантированных 2,9 В запаса нет и здесь (−0,11 В). - Открытый сток с подтяжкой к 5 В. PB6 допускает 5 В (FT), поэтому можно включить выход открытым стоком и подтянуть линию к +5 В резистором 1 кОм. Цена — фронт: при ёмкости линии и входа около 50 пФ (для примера) до порога 3,5 В линия дойдёт за 60 нс, на столько же укоротив высокий уровень; ток в открытый ключ — 5,0 мА.
22г.10 Гамма-коррекция, яркость и ограничение тока
Глаз воспринимает яркость нелинейно: значение 128 из 255 выглядит заметно
ярче «половины». Плавное затухание по линейной шкале кажется быстрым у
верхнего края и «ступенчатым» у нижнего. Гамма-коррекция пересчитывает
желаемую яркость в значение ШИМ: round(255·(i/255)^2,2). После неё
128 превращается в 56, 64 — в 12.
Цена — потеря тёмных оттенков: первые 15 значений (0…14) дают ноль, а всего различимых уровней остаётся 184 из 256. Плавный «рассвет» поэтому начинается рывком. Общую яркость умножают уже после гаммы: при яркости 127 значение 255 станет 127, а 128 — 28.
Ограничение тока — программный предохранитель для блока питания. Ток оценивается по сумме значений каналов (20 мА на канал при 255), и если он больше бюджета, все каналы умножаются на один множитель:
uint32_t ws_limit(uint8_t *buf, uint32_t nbytes, uint32_t ma_full, uint32_t budget_ma)
{
uint32_t need = ws_current_ma(buf, nbytes, ma_full);
if (need <= budget_ma)
return 256u;
uint32_t scale = budget_ma * 256u / need; /* множитель в долях 1/256, всегда меньше 256 */
for (uint32_t i = 0; i < nbytes; i++)
buf[i] = (uint8_t)((buf[i] * scale) >> 8);
return scale;
}
300 светодиодов в белом просят 18 000 мА; при бюджете 2000 мА множитель — 28/256, каждый канал становится 27, и оценка тока — 1905 мА. Множитель одинаков для всех каналов, поэтому оттенки сохраняются. Ограничитель ставят последним — после гаммы и общей яркости, — иначе он считает не те числа, что уйдут в ленту.
22г.11 На осциллографе: что смотреть
Щуп на DIN первого светодиода (после резистора и буфера), земля — на
землю ленты рядом с ним, короткой пружиной (36а.2). Развёртка 0,5–1 мкс на
деление.
- Ширина импульсов. Автоизмерение ширины положительного импульса или курсоры (36е): у прошивки раздела — 0,35 и 0,69 мкс (25 и 50 тактов). Сравните с окнами 22г.2 для своего чипа. Сравнивайте у входа ленты: длинный провод и буфер растягивают фронты.
- Уровни. Верх импульса у
DIN— не ниже 3,5 В (при буфере — около 5 В). Если на нём «полочка» около 3,3 В — буфера нет или он не ТТЛ. - Паузы между байтами. У SPI без DMA или с прерываниями посреди передачи между байтами появляются удлинённые нули — бит растягивается за окно T0L или T1L. У таймера с DMA пауз нет: каждый период — 1,25 мкс.
- Пауза сброса. Запуск по тайм-ауту или по ширине импульса (36д.13, 36д.8): низкий уровень дольше 50 мкс ловит начало каждого кадра. Курсором измерьте паузу между кадрами — у новых WS2812B не меньше 280 мкс.
- Длинный кадр. Логический анализатор с декодером WS2812 (36а.6) покажет цвета каждого светодиода — удобнее, чем считать импульсы глазами.
22г.12 Грабли
Новая катушка «замерла» или обновляется через раз. Пауза между кадрами 50 мкс, а новым WS2812B нужно 280 мкс: кадры сливаются, и лента показывает первый кадр после случайной долгой паузы. Пауза 300 мкс.
Код от WS2812B на SK6812 даёт случайные цвета. Единица 0,81 мкс длиннее окна SK6812 (до 0,75 мкс). Общие тайминги 25 / 50 тактов подходят обоим.
SPI «100/110» на SK6812 работает не на всех лентах. Единица 0,889 мкс выходит за окно на 139 нс. Таймер с DMA или SPI 9 МГц, 11 бит на бит.
Красный и синий поменялись местами, «оранжевый» стал зелёным. Порядок GRB, а не RGB. У SK6812WWA порядок каналов проверяйте по одному.
Белый на RGBW «грязный» или цвета съехали на один канал. Посылали по 24 бита на светодиод вместо 32: каждый следующий светодиод получает сдвинутые байты.
Первый светодиод мигает, остальные повторяют. Уровень 3,3 В на пороге 3,5 В. Буфер 74AHCT125, а не 74HC125.
Лента к концу желтеет. Просадка питания вдоль ленты. Питание с двух концов и в середину.
Лента мерцает при прерываниях программы. Двойной буфер не успевает: обработчик опоздал больше чем на 60 мкс. Поднять приоритет DMA, не держать прерывания запрещёнными дольше.
Блок питания уходит в защиту на белом. 300 светодиодов — 18 А. Программное ограничение тока.
22г.13 Итоги
- Чип забирает первые 24 бита (RGBW — 32) и отдаёт остальные дальше; цвета загораются после паузы сброса.
- Окна: WS2812B — 0,40 / 0,80 мкс, SK6812 — 0,30 / 0,60 мкс, допуск ±0,15 мкс; сброс — 280 мкс у новых WS2812B, 50 у ранних, 80 у SK6812. Пауза 300 мкс годится всем.
- Таймер 72 МГц:
ARR = 89; WS2812B — CCR 29 / 58 (запас 144 нс), SK6812 — 22 / 43 (97 нс), обоим — 25 / 50 (44 и 56 нс). - Программный вывод: допуск 10,8 такта меньше входа в прерывание (12); 300 светодиодов — 9 мс без прерываний.
- SPI 2,25 МГц «100/110» — для WS2812B (запас 61 нс), для SK6812 — нет (−139 нс).
- Двойной буфер: 192 байта вместо 14 400, прерывание раз в 60 мкс; опоздать можно на 48 периодов.
- Питание: до 60 мА на RGB и 80 мА на RGBW; 300 штук — 18 А; питание с двух концов (1,10 В вместо 4,41 В просадки на 5 м), конденсатор 1000 мкФ, резистор 300–500 Ом в данных.
- Уровень: порог 3,50 В при 5 В — буфер 74AHCT125 с порогом 2,0 В или «жертвенный» светодиод на диоде.
- Гамма 2,2: 128 → 56, 184 уровня из 256; ограничение тока — после гаммы и яркости.
Полные исходники
leds22b.h — тайминги в тактах, порядок байтов, поток бит, двойной буфер, SPI, яркость.
/* leds22b.h — раздел 22г: адресные светодиоды WS2812B, SK6812 RGBW и SK6812WWA.
* Без регистров: кодирование бит для ШИМ и для SPI, поток с двойным буфером,
* гамма-коррекция и ограничение тока. Тот же код — в прошивке fw22b.c и в стенде sim22b.c. */
#ifndef LEDS22B_H
#define LEDS22B_H
#include <stdint.h>
/* ── тайминги в тактах таймера 72 МГц ─────────────────────────────────── */
#define WS_TIM_HZ 72000000u
#define WS_ARR 89u /* период 90 тактов = 1,25 мкс */
#define WS_TICKS(ns) (((ns) * (WS_TIM_HZ / 1000000u) + 500u) / 1000u) /* с округлением */
#define WS2812_CCR0 WS_TICKS(400u) /* 0,4 мкс → 28,8 → 29 тактов */
#define WS2812_CCR1 WS_TICKS(800u) /* 0,8 мкс → 57,6 → 58 тактов */
#define SK6812_CCR0 WS_TICKS(300u) /* 0,3 мкс → 21,6 → 22 такта */
#define SK6812_CCR1 WS_TICKS(600u) /* 0,6 мкс → 43,2 → 43 такта */
#define ANY_CCR0 25u /* 0,347 мкс: в окне у обоих чипов */
#define ANY_CCR1 50u /* 0,694 мкс: тоже */
/* пауза сброса в периодах бита 1,25 мкс */
#define WS_RESET_OLD 40u /* 50 мкс — ранние WS2812B */
#define WS_RESET_NEW 240u /* 300 мкс — с запасом над 280 мкс */
#define SK_RESET 64u /* 80 мкс — SK6812 */
/* ── порядок байтов в ленте ───────────────────────────────────────────── */
void ws_put_rgb(uint8_t *buf, uint32_t i, uint8_t r, uint8_t g, uint8_t b); /* G R B */
void ws_put_rgbw(uint8_t *buf, uint32_t i, uint8_t r, uint8_t g, uint8_t b, uint8_t w); /* G R B W */
/* ── поток бит в отсчёты ШИМ: данные, затем нули сброса ───────────────── */
typedef struct {
const uint8_t *data; /* байты в порядке выдачи */
uint32_t bits, pos; /* всего бит и следующий бит */
uint32_t tail; /* сколько нулевых отсчётов сброса осталось */
uint16_t c0, c1; /* CCR для нуля и для единицы */
} ws_stream_t;
void ws_stream_start(ws_stream_t *s, const uint8_t *data, uint32_t nbytes,
uint32_t reset_slots, uint16_t c0, uint16_t c1);
int ws_stream_fill(ws_stream_t *s, uint16_t *dst, uint32_t n); /* 1 — поток кончился */
/* ── двойной буфер: DMA читает одну половину, прерывание заполняет другую ── */
#define WS_HALF 48u /* отсчётов в половине: два светодиода RGB */
typedef struct {
ws_stream_t s;
uint16_t buf[2u * WS_HALF];
uint8_t after_end; /* прерываний после конца потока */
} ws_dma_t;
void ws_dma_prepare(ws_dma_t *d, const uint8_t *data, uint32_t nbytes,
uint32_t reset_slots, uint16_t c0, uint16_t c1);
int ws_dma_irq(ws_dma_t *d, uint32_t half); /* half: 0 — после HT, 1 — после TC; 1 — стоп */
/* ── SPI: каждый бит ленты — nbits бит SPI, из них первые ones0/ones1 — единицы ── */
uint32_t ws_spi_encode(uint8_t *dst, const uint8_t *src, uint32_t n,
uint32_t nbits, uint32_t ones0, uint32_t ones1); /* вернуть байт SPI */
/* ── яркость ──────────────────────────────────────────────────────────── */
extern const uint8_t ws_gamma[256]; /* round(255 * (i / 255)^2,2) */
void ws_apply_gamma(uint8_t *buf, uint32_t nbytes, uint8_t bright);
uint32_t ws_current_ma(const uint8_t *buf, uint32_t nbytes, uint32_t ma_full);
uint32_t ws_limit(uint8_t *buf, uint32_t nbytes, uint32_t ma_full, uint32_t budget_ma);
#endif
leds22b.c — кодирование без регистров: общая часть прошивки и стенда.
/* leds22b.c — раздел 22г: кодирование и яркость для адресных светодиодов (без регистров). */
#include "leds22b.h"
void ws_put_rgb(uint8_t *buf, uint32_t i, uint8_t r, uint8_t g, uint8_t b)
{
uint8_t *p = &buf[i * 3u];
p[0] = g; /* первым идёт зелёный */
p[1] = r;
p[2] = b;
}
void ws_put_rgbw(uint8_t *buf, uint32_t i, uint8_t r, uint8_t g, uint8_t b, uint8_t w)
{
uint8_t *p = &buf[i * 4u];
p[0] = g;
p[1] = r;
p[2] = b;
p[3] = w; /* белый кристалл — четвёртым байтом */
}
void ws_stream_start(ws_stream_t *s, const uint8_t *data, uint32_t nbytes,
uint32_t reset_slots, uint16_t c0, uint16_t c1)
{
s->data = data;
s->bits = nbytes * 8u;
s->pos = 0;
s->tail = reset_slots;
s->c0 = c0;
s->c1 = c1;
}
int ws_stream_fill(ws_stream_t *s, uint16_t *dst, uint32_t n)
{
for (uint32_t k = 0; k < n; k++) {
if (s->pos < s->bits) {
uint32_t bit = (s->data[s->pos >> 3] >> (7u - (s->pos & 7u))) & 1u; /* старший первым */
dst[k] = bit ? s->c1 : s->c0;
s->pos++;
} else {
dst[k] = 0; /* CCR = 0: весь период низкий уровень — это и есть сброс */
if (s->tail)
s->tail--;
}
}
return s->pos >= s->bits && s->tail == 0u;
}
void ws_dma_prepare(ws_dma_t *d, const uint8_t *data, uint32_t nbytes,
uint32_t reset_slots, uint16_t c0, uint16_t c1)
{
ws_stream_start(&d->s, data, nbytes, reset_slots, c0, c1);
d->after_end = 0;
if (ws_stream_fill(&d->s, d->buf, 2u * WS_HALF)) /* обе половины — до старта */
d->after_end = 1;
}
int ws_dma_irq(ws_dma_t *d, uint32_t half)
{
/* Половина half только что прочитана, DMA читает другую — эту можно переписать. */
int end = ws_stream_fill(&d->s, &d->buf[half * WS_HALF], WS_HALF);
if (d->after_end || end)
d->after_end++;
/* Конец потока лёг в половину, заполненную после прерывания k. DMA прочтёт её
к прерыванию k + 2 — только тогда таймер можно остановить. */
return d->after_end >= 3u;
}
uint32_t ws_spi_encode(uint8_t *dst, const uint8_t *src, uint32_t n,
uint32_t nbits, uint32_t ones0, uint32_t ones1)
{
uint32_t out = 0, acc = 0, have = 0;
for (uint32_t i = 0; i < n; i++) {
for (int b = 7; b >= 0; b--) {
uint32_t ones = ((src[i] >> b) & 1u) ? ones1 : ones0;
for (uint32_t k = 0; k < nbits; k++) {
acc = (acc << 1) | (k < ones); /* сначала единицы, потом нули */
if (++have == 8u) {
dst[out++] = (uint8_t)acc;
acc = 0;
have = 0;
}
}
}
}
if (have) /* добить последний байт нулями */
dst[out++] = (uint8_t)(acc << (8u - have));
return out;
}
const uint8_t ws_gamma[256] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2,
3, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6,
6, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 11, 11, 11, 12,
12, 13, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 19, 19,
20, 20, 21, 22, 22, 23, 23, 24, 25, 25, 26, 26, 27, 28, 28, 29,
30, 30, 31, 32, 33, 33, 34, 35, 35, 36, 37, 38, 39, 39, 40, 41,
42, 43, 43, 44, 45, 46, 47, 48, 49, 49, 50, 51, 52, 53, 54, 55,
56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71,
73, 74, 75, 76, 77, 78, 79, 81, 82, 83, 84, 85, 87, 88, 89, 90,
91, 93, 94, 95, 97, 98, 99, 100, 102, 103, 105, 106, 107, 109, 110, 111,
113, 114, 116, 117, 119, 120, 121, 123, 124, 126, 127, 129, 130, 132, 133, 135,
137, 138, 140, 141, 143, 145, 146, 148, 149, 151, 153, 154, 156, 158, 159, 161,
163, 165, 166, 168, 170, 172, 173, 175, 177, 179, 181, 182, 184, 186, 188, 190,
192, 194, 196, 197, 199, 201, 203, 205, 207, 209, 211, 213, 215, 217, 219, 221,
223, 225, 227, 229, 231, 234, 236, 238, 240, 242, 244, 246, 248, 251, 253, 255,
};
void ws_apply_gamma(uint8_t *buf, uint32_t nbytes, uint8_t bright)
{
/* сначала гамма, потом общая яркость: так ступени у тёмных цветов не теряются дважды */
for (uint32_t i = 0; i < nbytes; i++)
buf[i] = (uint8_t)((ws_gamma[buf[i]] * (bright + 1u)) >> 8);
}
uint32_t ws_current_ma(const uint8_t *buf, uint32_t nbytes, uint32_t ma_full)
{
uint32_t sum = 0;
for (uint32_t i = 0; i < nbytes; i++)
sum += buf[i];
return sum * ma_full / 255u; /* ток канала пропорционален скважности его ШИМ */
}
uint32_t ws_limit(uint8_t *buf, uint32_t nbytes, uint32_t ma_full, uint32_t budget_ma)
{
uint32_t need = ws_current_ma(buf, nbytes, ma_full);
if (need <= budget_ma)
return 256u;
uint32_t scale = budget_ma * 256u / need; /* множитель в долях 1/256, всегда меньше 256 */
for (uint32_t i = 0; i < nbytes; i++)
buf[i] = (uint8_t)((buf[i] * scale) >> 8);
return scale;
}
fw22b.c — прошивка STM32F103: TIM4 + DMA1 с двойным буфером или SPI1 + DMA1.
/* fw22b.c — раздел 22г: адресная лента от STM32F103 (BluePill), 72 МГц.
* Способ 1 (основной): TIM4_CH1 на PB6, DMA1 Channel7 по событию обновления TIM4 (TIM4_UP),
* двойной буфер с прерываниями на половине (HT) и в конце (TC).
* Способ 2 (WS22B_SPI): SPI1, MOSI на PA7, 72 МГц / 32 = 2,25 МГц, «100/110», DMA1 Channel3 (SPI1_TX).
* Каналы DMA — по таблице 78 RM0008. Тактирование до 72 МГц — как в разделе 6 (здесь не повторяется). */
#include "stm32f10x.h"
#include "leds22b.h"
#define NLED 60u
static uint8_t colors[NLED * 3u];
#ifndef WS22B_SPI
/* ── способ 1: таймер + DMA, двойной буфер ───────────────────────────── */
static ws_dma_t ws;
static volatile uint8_t ws_busy;
static void ws_hw_init(void)
{
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
RCC->APB1ENR |= RCC_APB1ENR_TIM4EN;
RCC->AHBENR |= RCC_AHBENR_DMA1EN;
/* PB6: альтернативная функция, двухтактный выход, 50 МГц (CNF = 10, MODE = 11) */
GPIOB->CRL = (GPIOB->CRL & ~(0xFu << 24)) | (0xBu << 24);
TIM4->PSC = 0; /* 72 МГц: APB1 36 МГц × 2 */
TIM4->ARR = WS_ARR; /* 90 тактов = 1,25 мкс */
TIM4->CCR1 = 0;
TIM4->CCMR1 = TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_1 /* ШИМ, режим 1 */
| TIM_CCMR1_OC1PE; /* CCR1 в силе с обновления */
TIM4->CCER = TIM_CCER_CC1E;
TIM4->CR1 = TIM_CR1_ARPE;
TIM4->DIER = TIM_DIER_UDE; /* запрос DMA на каждом обновлении */
DMA1_Channel7->CPAR = (uint32_t)&TIM4->CCR1;
DMA1_Channel7->CMAR = (uint32_t)ws.buf;
DMA1_Channel7->CCR = DMA_CCR7_MINC | DMA_CCR7_CIRC | DMA_CCR7_DIR
| DMA_CCR7_MSIZE_0 | DMA_CCR7_PSIZE_0 /* 16 бит → 16 бит */
| DMA_CCR7_HTIE | DMA_CCR7_TCIE | DMA_CCR7_PL_1;
NVIC_EnableIRQ(DMA1_Channel7_IRQn);
}
void ws_show(void)
{
while (ws_busy)
;
ws_busy = 1;
ws_dma_prepare(&ws, colors, sizeof colors, WS_RESET_NEW, ANY_CCR0, ANY_CCR1);
DMA1_Channel7->CNDTR = 2u * WS_HALF;
DMA1_Channel7->CCR |= DMA_CCR7_EN;
TIM4->CNT = 0;
TIM4->CR1 |= TIM_CR1_CEN;
}
void DMA1_Channel7_IRQHandler(void)
{
uint32_t isr = DMA1->ISR;
uint32_t half = (isr & DMA_ISR_TCIF7) ? 1u : 0u; /* TC — вторая половина свободна */
DMA1->IFCR = DMA_IFCR_CHTIF7 | DMA_IFCR_CTCIF7 | DMA_IFCR_CGIF7;
if (ws_dma_irq(&ws, half)) {
TIM4->CR1 &= ~TIM_CR1_CEN; /* на линии ноль: CCR давно 0 */
DMA1_Channel7->CCR &= ~DMA_CCR7_EN;
ws_busy = 0;
}
}
#else
/* ── способ 2: SPI1 + DMA, каждый бит ленты — три бита SPI ───────────── */
static uint8_t spi_buf[NLED * 9u + 1u]; /* 9 байт на светодиод + нулевой в конце */
static void ws_hw_init(void)
{
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_SPI1EN;
RCC->AHBENR |= RCC_AHBENR_DMA1EN;
/* PA7 (MOSI): альтернативная функция, двухтактный выход, 50 МГц */
GPIOA->CRL = (GPIOA->CRL & ~(0xFu << 28)) | (0xBu << 28);
SPI1->CR1 = SPI_CR1_MSTR | SPI_CR1_SSM | SPI_CR1_SSI
| SPI_CR1_BR_2 /* BR = 100: 72 МГц / 32 = 2,25 МГц */
| SPI_CR1_SPE;
SPI1->CR2 = SPI_CR2_TXDMAEN;
DMA1_Channel3->CPAR = (uint32_t)&SPI1->DR;
DMA1_Channel3->CCR = DMA_CCR3_MINC | DMA_CCR3_DIR; /* 8 бит, один проход */
}
void ws_show(void)
{
uint32_t n = ws_spi_encode(spi_buf, colors, sizeof colors, 3u, 1u, 2u);
spi_buf[n++] = 0; /* линия останется в нуле */
DMA1_Channel3->CCR &= ~DMA_CCR3_EN;
DMA1->IFCR = DMA_IFCR_CGIF3;
DMA1_Channel3->CMAR = (uint32_t)spi_buf;
DMA1_Channel3->CNDTR = n;
DMA1_Channel3->CCR |= DMA_CCR3_EN;
/* паузу сброса (≥ 280 мкс) отсчитать таймером, прежде чем звать ws_show снова */
}
#endif
int main(void)
{
ws_hw_init();
for (uint32_t i = 0; i < NLED; i++)
ws_put_rgb(colors, i, 255u, 140u, 0u); /* оранжевый */
ws_apply_gamma(colors, sizeof colors, 64u); /* четверть яркости после гаммы */
ws_limit(colors, sizeof colors, 20u, 1000u); /* не больше 1 А на ленту */
ws_show();
for (;;)
;
}
Дальше: раздел 22д «Символьный ЖК-дисплей LCD1602 на HD44780: 4 бита, инициализация, время команд, CGRAM, I²C-переходник».