Приобрел я на Aliexpress так называемую адресную светодиодную ленту на светодиодах WS2812B. В отличие от обычной RGB-ленты, тут есть возможность
управлять цветом и яркостью свечения каждого светодиода отдельно. Однако, это накладывает некоторые сложности в управлении этой лентой. Для того, чтобы зажечь нужный цвет, необходимо загрузить в ленту последовательность бит данных, содержащую информацию о цвете свечения каждого светодиода. Есть готовые библиотеки, и для всяких Ардуино, и для тех же STM32, которые реализуют цифровой протокол обмена данными с WS2812B. Однако, я хочу изобрести свой велосипед в этой области, ну и заодно немного поупражняться в программировании, поэтому, поехали! 😉
Содержание:
Введение
Интерфейс светодиодов WS2812B является довольно жестким в отношении к временным интервалам, поэтому его реализация в виде Bit-banging-а является еще той задачей. В результате выбор пал на вариант с хитрой настройкой таймера в режиме ШИМ и контроллера прямого доступа к памяти (DMA). Идея была позаимствована с https://habr.com/post/257131/. Там была описана реализация для микроконтроллера STM32F407VE. В моем случае был взят STM32F103C8 в составе отладочной платы с Али за 2 бакса:
Рис. 1. Отладочная плата с STM32F103C8T6 на борту
Описание библиотеки управления
Описывать идею реализации протокола на таймере не буду, всегда можно обратиться к первоисточнику на Хабре, перейду сразу к конкретной реализации библиотеки. Из периферии задействован таймер-счетчик TIM2, контроллер DMA1 и, в зависимости от конфигурации, один из портов PA0, PA1, PA2 или PA3. Библиотека состоит из 3-х файлов:
- ws2812b.h — прототипы функций и настройка количества светодиодов в ленте
- ws2812b_config.h — тонкая настройка библиотеки, включает подстойку таймингов и выбор ножки порта, через которую производится управление лентой
- ws2812b.c — сама реализация протокола
Библиотека использует только CMSIS без всякой высокоуровневой требухи в виде SPL. Только регистры, только хардкор!!!
Начнем с простого: файл ws2812b.h:
//Количество светодиодов в ленте #define WS2812B_NUM_LEDS 144 //Инициализация интерфейса ws2812b void ws2812b_init(void); //Очистить буфер светодиодной ленты. //Устанавливает всем светодиодам значения //R=0, G=0, B=0 void ws2812b_buff_claer(void); //Установить компоненты RGB светодиода номер pixn //pixn=0..WS2812B_NUM_LEDS-1 //r=0..255, g=0..255, b=0..255 //Возвращаемые значения // 0 - выполнено успешно // 1 - неверное значение pixn int ws2812b_set(int pixn, uint8_t r, uint8_t g, uint8_t b); //Загрузить подготовленный буфрер //в светодиодную ленту. //Возврашает 1 если предыдущая операция //обмена данными еще не завершена int ws2812b_send(void); //Возвращает 1 если предыдущая операция //обмена данными с светодиодной лентой //завершена успешно int ws2812b_is_ready(void);
С помощью WS2812B_NUM_LEDS указываем количество светодиодов в ленте, в данном случае 144. Функция ws2812b_init() производит инициализацию интерфейса, ее надо вызвать один раз в самом начале программы, ws2812b_buff_claer() очищает внутренний буфер библиотеки, присваивая всем светодиодам выключенное состояние. С помощью ws2812b_set(int pixn, uint8_t r, uint8_t g, uint8_t b) присваиваем pixn-му светодиоду компоненты R, G, и B. Нумерация светодиодов идет с нуля, максимальное значение номера пикселя WS2812B_NUM_LEDS — 1. Функция ws2812b_send() запускает процесс отправки внутреннего буфера в светодиодную ленту, причем, если предыдущая операция отправки еще не завершена, то не оказывает ни какого влияния на процесс и возвращает единицу. С помощью ws2812b_is_ready() можно узнать текущий статус интерфейса: если эта функция вернула 0, то интерфейс сейчас занят операцией обмена, если 1, то интерфейс свободен.
Следующий файл — ws2812b_config.h:
//Период следования бит в тиках таймера //должно быть 1.25мкс #define WS2812B_TIMER_AAR 0x0059 //Передача лог. нуля 0.4мкс #define WS2812B_0_VAL (WS2812B_TIMER_AAR / 3) //Передача лог. единицы 0.85мкс #define WS2812B_1_VAL ((WS2812B_TIMER_AAR / 3) * 2) //Сигнал RET или RESET более 50мкс #define WS2812B_TIMER_RET (WS2812B_TIMER_AAR * 45) //убрать коментарий, если нужно инвертировать //выходной сигнал #define WS2812B_OUTPUT_INVERSE //Какой вывод использовать для формирования сигнала /* Возможные варианты Знач. Порт 0 PA0 1 PA1 2 PA2 3 PA3 */ #define WS2812B_OUTPUT_PAx 1
С помощью WS2812B_TIMER_AAR производим точную настройку периода ШИМ-сигнала таймера, чтоб он соответствовал скорости передачи данных. Значение 0x59 соответствует периоду в 1.25 мкс при тактировании таймера частотой 72 МГц.
Если мы используем инвертирующий согласователь логических уровней между микроконтроллером и лентой, то сигнал с выхода микроконтроллера необходимо инвертировать, чтоб в итоге получить управляющие импульсы в нужной полярности. Для этих целей служит #define WS2812B_OUTPUT_INVERSE.
Далее идет выбор пина порта WS2812B_OUTPUT_PAx, с помощью которого будет производиться управление лентой. Варианта тут только 4-ре: PA0, PA1, PA2 или PA3. Выбирай тот, что удобней)).
Перейдем к основному: ws2812b.c. Немного поговорим о том, как реализована логика интерфейса WS2812B. В функции отправки данных в ленту производится настройка таймера TIM2 в режим ШИМ с частотой 800 КГц, что соответствует скорости передачи данных в ленту. После этого включаем DMA, который после каждого события обновления счетчика будет запихивать новое значение в регистр ШИМ-а таймера. После того, как все запихано, возникает прерывание DMA об окончании передачи, в котором производится останов TIM2, установка его периода, равного длительности сигнала RET, включение прерывания при обновлении значения регистров счетчика и снова пуск TIM2. После истечения периода RET, возникает прерывание от TIM2, в котором происходит финализация процесса передачи данных и установка флага готовности шины к следующей передачи данных.
В самом начале файла находится куча define-ов, с помощью которых происходит определение конкретных регистров таймера и DMA в соответствии с выбранной ножкой микроконтроллера WS2812B_OUTPUT_PAx. Конструкция монструозная, но как сделать проще не придумал:
#if (WS2812B_OUTPUT_PAx==0) #define GPIO_CRL_CNFx GPIO_CRL_CNF0 #define GPIO_CRL_CNFx_1 GPIO_CRL_CNF0_1 #define GPIO_CRL_MODEx_1 GPIO_CRL_MODE0_1 #define GPIO_CRL_MODEx_0 GPIO_CRL_MODE0_0 #define TIM_CCER_CCxE TIM_CCER_CC1E #define TIM_CCER_CCxP TIM_CCER_CC1P #define CCMRx CCMR1 #define TIM_CCMRy_OCxM TIM_CCMR1_OC1M #define TIM_CCMRy_OCxM_2 TIM_CCMR1_OC1M_2 #define TIM_CCMRy_OCxM_1 TIM_CCMR1_OC1M_1 #define TIM_CCMRy_OCxPE TIM_CCMR1_OC1PE #define TIM_DIER_CCxDE TIM_DIER_CC1DE #define CCRx CCR1 #define DMA1_Channelx DMA1_Channel5 #define DMA1_Channelx_IRQn DMA1_Channel5_IRQn #define DMA1_Channelx_IRQHandler DMA1_Channel5_IRQHandler #define DMA_CCRx_EN DMA_CCR5_EN #define DMA_CCRx_TCIE DMA_CCR5_TCIE #define DMA_IFCR_CTEIFx DMA_IFCR_CTEIF5 #define DMA_IFCR_CHTIFx DMA_IFCR_CHTIF5 #define DMA_IFCR_CTCIFx DMA_IFCR_CTCIF5 #define DMA_IFCR_CGIFx DMA_IFCR_CGIF5 #elif (WS2812B_OUTPUT_PAx==1) #define GPIO_CRL_CNFx GPIO_CRL_CNF1 #define GPIO_CRL_CNFx_1 GPIO_CRL_CNF1_1 #define GPIO_CRL_MODEx_1 GPIO_CRL_MODE1_1 #define GPIO_CRL_MODEx_0 GPIO_CRL_MODE1_0 #define TIM_CCER_CCxE TIM_CCER_CC2E #define TIM_CCER_CCxP TIM_CCER_CC2P #define CCMRx CCMR1 #define TIM_CCMRy_OCxM TIM_CCMR1_OC2M #define TIM_CCMRy_OCxM_2 TIM_CCMR1_OC2M_2 #define TIM_CCMRy_OCxM_1 TIM_CCMR1_OC2M_1 #define TIM_CCMRy_OCxPE TIM_CCMR1_OC2PE #define TIM_DIER_CCxDE TIM_DIER_CC2DE #define CCRx CCR2 #define DMA1_Channelx DMA1_Channel7 #define DMA1_Channelx_IRQn DMA1_Channel7_IRQn #define DMA1_Channelx_IRQHandler DMA1_Channel7_IRQHandler #define DMA_CCRx_EN DMA_CCR7_EN #define DMA_CCRx_TCIE DMA_CCR7_TCIE #define DMA_IFCR_CTEIFx DMA_IFCR_CTEIF7 #define DMA_IFCR_CHTIFx DMA_IFCR_CHTIF7 #define DMA_IFCR_CTCIFx DMA_IFCR_CTCIF7 #define DMA_IFCR_CGIFx DMA_IFCR_CGIF7 #elif (WS2812B_OUTPUT_PAx==2) #define GPIO_CRL_CNFx GPIO_CRL_CNF2 #define GPIO_CRL_CNFx_1 GPIO_CRL_CNF2_1 #define GPIO_CRL_MODEx_1 GPIO_CRL_MODE2_1 #define GPIO_CRL_MODEx_0 GPIO_CRL_MODE2_0 #define TIM_CCER_CCxE TIM_CCER_CC3E #define TIM_CCER_CCxP TIM_CCER_CC3P #define CCMRx CCMR2 #define TIM_CCMRy_OCxM TIM_CCMR2_OC3M #define TIM_CCMRy_OCxM_2 TIM_CCMR2_OC3M_2 #define TIM_CCMRy_OCxM_1 TIM_CCMR2_OC3M_1 #define TIM_CCMRy_OCxPE TIM_CCMR2_OC3PE #define TIM_DIER_CCxDE TIM_DIER_CC3DE #define CCRx CCR3 #define DMA1_Channelx DMA1_Channel1 #define DMA1_Channelx_IRQn DMA1_Channel1_IRQn #define DMA1_Channelx_IRQHandler DMA1_Channel1_IRQHandler #define DMA_CCRx_EN DMA_CCR1_EN #define DMA_CCRx_TCIE DMA_CCR1_TCIE #define DMA_IFCR_CTEIFx DMA_IFCR_CTEIF1 #define DMA_IFCR_CHTIFx DMA_IFCR_CHTIF1 #define DMA_IFCR_CTCIFx DMA_IFCR_CTCIF1 #define DMA_IFCR_CGIFx DMA_IFCR_CGIF1 #elif (WS2812B_OUTPUT_PAx==3) #define GPIO_CRL_CNFx GPIO_CRL_CNF3 #define GPIO_CRL_CNFx_1 GPIO_CRL_CNF3_1 #define GPIO_CRL_MODEx_1 GPIO_CRL_MODE3_1 #define GPIO_CRL_MODEx_0 GPIO_CRL_MODE3_0 #define TIM_CCER_CCxE TIM_CCER_CC4E #define TIM_CCER_CCxP TIM_CCER_CC4P #define CCMRx CCMR2 #define TIM_CCMRy_OCxM TIM_CCMR2_OC4M #define TIM_CCMRy_OCxM_2 TIM_CCMR2_OC4M_2 #define TIM_CCMRy_OCxM_1 TIM_CCMR2_OC4M_1 #define TIM_CCMRy_OCxPE TIM_CCMR2_OC4PE #define TIM_DIER_CCxDE TIM_DIER_CC4DE #define CCRx CCR4 #define DMA1_Channelx DMA1_Channel7 #define DMA1_Channelx_IRQn DMA1_Channel7_IRQn #define DMA1_Channelx_IRQHandler DMA1_Channel7_IRQHandler #define DMA_CCRx_EN DMA_CCR7_EN #define DMA_CCRx_TCIE DMA_CCR7_TCIE #define DMA_IFCR_CTEIFx DMA_IFCR_CTEIF7 #define DMA_IFCR_CHTIFx DMA_IFCR_CHTIF7 #define DMA_IFCR_CTCIFx DMA_IFCR_CTCIF7 #define DMA_IFCR_CGIFx DMA_IFCR_CGIF7 #endif
Затем идет расчет длины буфера, в котором хранятся значения цвета светодиодов:
//Расчитываем длину буфера #define DATA_LEN ((WS2812B_NUM_LEDS * 24) + 2) static uint8_t led_array[DATA_LEN]; static int flag_rdy = 0;
Каждый светодиод добавляет 24 байта к занимаемой памяти. Так, для светодиодной ленты, состоящей из 144-х пикселей буфер будет занимать 144*24+2=3458~=3.4 Кбайт ОЗУ!!! AVR-щики в шоке!!! Ардуинщики чешут репу. Много это или мало, зависит от конкретной задачи: где-то такой расход памяти допустим, а где-то нет.
Поехали далее. Функция инициализации интерфейса выглядит следующим образом:
void ws2812b_init(void)
{
flag_rdy = 0;
//Разрешаем такирование переферии
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; //Включаем тактирование порта GPIOA
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN; //таймера TIM2
RCC->AHBENR |= RCC_AHBENR_DMA1EN; //и DMA1
/********* Настраиваем PAx *********/
//PA1 freq=10Mhz, AF output Push-pull
GPIOA->CRL &= ~(GPIO_CRL_CNFx);
GPIOA->CRL |= GPIO_CRL_CNFx_1
| GPIO_CRL_MODEx_1 | GPIO_CRL_MODEx_0;
/********* Настойка таймера TIM2 *********/
//Разрешаем таймеру управлять выводом PAx
TIM2->CCER |= TIM_CCER_CCxE; //Разрешаем
#ifdef WS2812B_OUTPUT_INVERSE
TIM2->CCER |= TIM_CCER_CCxP; //Выход инверсный
#else
TIM2->CCER &= ~(TIM_CCER_CCxP); //Выход не инверсный
#endif
TIM2->CCMRx &= ~(TIM_CCMRy_OCxM); //сбрасываем все биты OCxM
//устанавливаем выход в неактивное состояние
TIM2->CCMRx |= TIM_CCMRy_OCxM_2;
TIM2->CCMRx &= ~(TIM_CCMRy_OCxM_2);
TIM2->CCMRx |= TIM_CCMRy_OCxM_2 | TIM_CCMRy_OCxM_1
| TIM_CCMRy_OCxPE; //режим ШИМ-а
TIM2->CR1 |= TIM_CR1_ARPE; //Регистры таймера с буферизацией
TIM2->DIER |= TIM_DIER_CCxDE; //Разрешить запрос DMA
//Настраиваем канал DMA
DMA1_Channelx->CPAR = (uint32_t)(&TIM2->CCRx); //Куда пишем
DMA1_Channelx->CMAR = (uint32_t)(led_array); //откуда берем
DMA1_Channelx->CCR = DMA_CCR7_PSIZE_0 //регистр переферии 16 бит
| DMA_CCR7_MINC //режим инкремента указателя памяти
| DMA_CCR7_DIR; //напревление передачи из памяти в переферию
//Разрешаем обработку прерываний
NVIC_EnableIRQ(TIM2_IRQn); //от таймера
NVIC_EnableIRQ(DMA1_Channelx_IRQn); //от DMA
ws2812b_buff_claer();
bus_retcode(); //сбрасываем шину
}
Первым делом включаем тактирование всех нужных нам модулей. Затем переключаем GPIO в режим альтернативных функций и задаем максимальную частоту вывода 10Мгц. После этого идет настройка таймера, DMA, и включение прерываний от этих модулей.
Дальше функция запуска отправки данных:
int ws2812b_send(void)
{
if(flag_rdy) //Если сейчас ни чего не передается
{
//Устанавливаем флаг занятости интерфейса
flag_rdy = 0;
//Настраиваем передачу данных
DMA1_Channelx->CCR &= ~(DMA_CCR7_EN); //Отключаем канал DMA
DMA1_Channelx->CNDTR = sizeof(led_array); //Устанавливаем количество данных
//Таймер считает до WS2812B_TIMER_AAR, таким образом
//при данной частоте тактирования таймера
//получаем период ШИМ-сигнала, равный 1.25мкс
TIM2->ARR = WS2812B_TIMER_AAR;
TIM2->CCRx = 0x0000; //Устанавливаем ШИМ-регистр таймера в ноль
TIM2->CNT = 0; //Очищаем счетный регистр
TIM2->CR1 |= TIM_CR1_CEN; //Запускаем таймер
//Так как значение ШИМ установили в ноль,
//то на шине будет установлен неактивный уровень
//до момента запуска DMA
DMA1->IFCR = DMA_IFCR_CTEIFx | DMA_IFCR_CHTIFx
| DMA_IFCR_CTCIFx | DMA_IFCR_CGIFx; //Очищаем все флаги прерываний DMA
DMA1_Channelx->CCR |= DMA_CCRx_TCIE; //прерывание завершения передачи
//Включаем канал DMA, тем самым начинаем передачу данных
DMA1_Channelx->CCR |= DMA_CCRx_EN;
return 0;
}
else
{
return 1;
}
}
Здесь происходит предварительная настройка таймера и DMA должным образом и запуск всего процесса.
После завершения передачи данных возникает прерывание от DMA DMA1_Channelx_IRQHandler():
static void bus_retcode(void)
{
TIM2->CR1 &= ~(TIM_CR1_CEN); //останавливаем таймер
TIM2->ARR = WS2812B_TIMER_RET; //Устанавливаем период немного больше 50мкс
TIM2->CNT = 0; //Очищаем счетный регистр
TIM2->CCRx = 0x0000; //значение ШИМ-а ноль
TIM2->SR &= ~(TIM_SR_UIF); //сбрасываем флаг прерывания
TIM2->DIER |= TIM_DIER_UIE; //прерывание по обновлению
TIM2->CR1 |= TIM_CR1_CEN; //Поехали считать!
}
//Прерывание от DMA
//Суда попадаем после завершения передачи данных
void DMA1_Channelx_IRQHandler(void)
{
DMA1_Channelx->CCR &= ~(DMA_CCRx_EN); //Отключаем канал DMA
DMA1->IFCR = DMA_IFCR_CTEIFx | DMA_IFCR_CHTIFx
| DMA_IFCR_CTCIFx | DMA_IFCR_CGIFx; //Сбрасываем все флаги прерываний
//Так как последние 2 элемента массива равны нулю,
//то сейчас предпоследнее значение уже загружено
//в теневой регистр сравнения
//и на шине установлено неактивное состояние.
//Задача заключается в удержании шины в этом состоянии
//в течение 50мкс или более
//перед установкой флага готовности интерфейса.
bus_retcode();
}
В этом прерывании запускается следующая стадия работы логики интерфейса, а именно формирование сигнала RET. В функции bus_retcode() происходит вся настройка таймера для этой стадии. После того, как таймер отсчитал чуть больше 50мкс, возникает прерывание TIM2_IRQHandler():
//прерывание от таймера
//Сюда попадаем после завершения формирования
//сигнала RET шины ws2812b
void TIM2_IRQHandler(void)
{
TIM2->SR = 0; //Сбрасываем все флаги прерываний
//Итак, мы завершили формирование сигнала RET на шине
//и теперь можно сделать все завершающие операции
//и установить флаг готовности интерфейса к следующей
//передаче данных.
TIM2->CR1 &= ~(TIM_CR1_CEN); //останавливаем таймер
TIM2->DIER &= ~(TIM_DIER_UIE); //запрещаем прерывание таймера
flag_rdy = 1;
}
Здесь мы сбрасываем все флаги прерываний таймера TIM2 и останавливаем его, а так же устанавливаем флаг готовности интерфейса flag_rdy.
Из интересного осталось только рассмотреть функцию установки цвета пикселя светодиодной ленты ws2812b_set():
int ws2812b_set(int pixn, uint8_t r, uint8_t g, uint8_t b)
{
int offset = pixn*24;
int i;
uint8_t tmp;
if(pixn > (WS2812B_NUM_LEDS - 1))
return 1;
//g component
tmp = g;
for(i=0; i<8; i++)
{
if(tmp & 0x80)
led_array[offset + i] = WS2812B_1_VAL;
else
led_array[offset + i] = WS2812B_0_VAL;
tmp<<=1;
}
//r component
tmp = r;
for(i=0; i<8; i++)
{
if(tmp & 0x80)
led_array[offset + i + 8] = WS2812B_1_VAL;
else
led_array[offset + i + 8] = WS2812B_0_VAL;
tmp<<=1;
}
//b component
tmp = b;
for(i=0; i<8; i++)
{
if(tmp & 0x80)
led_array[offset + i + 16] = WS2812B_1_VAL;
else
led_array[offset + i + 16] = WS2812B_0_VAL;
tmp<<=1;
}
return 0;
}
На вход принимаем номер светодиода и компоненты RGB. Согласно даташиту на светодиоды, байты цвета идут в следующей последовательности: зеленый, красный, синий, а последовательности бит от старшего к младшему. Массив led_array[] как раз тот самый массив, который выводится через DMA в ШИМ-регистр таймера.
Подключаем ленту
Светодиоды WS2812B питаются напряжением 5 вольт. Максимальный ток потребления одного светодиода при включении на полную яркость всех трех цветов составляет 60mA, таким образом, метр ленты со 144 может сожрать почти 9 ампер тока, что, мягко говоря, дофига и еще немного. Так что если есть желание жечь по полной, нужно организовать хороший теплоотвод ленты и подавать ей питание не только в самом начале, но и еще в нескольких точка по всей длине ленты.
Теперь что касается цифрового входа. Потенциал логической единицы начинается с 0,7Vdd, и при питании ленты от 5-и вольт это будет составлять 3,5 вольт. Так как выводы у STM32 3,3 вольтные, то напрямую подключить ленту к микроконтроллеру не получится, нужен согласователь уровней. Причем это не занудные заморочки любителя «все делать по инструкции», а реальная необходимость: на практике при питании от 5-и вольт лента у меня не завелась при прямом подключении к 3,3 вольтной логике. Чтож, не проблема, сделаем согласователь уровней на полевом транзюке:

Рис. 2. Согласование уровней с помощью транзистора 2N7002
И вот тут нам как раз и пригодится возможность инвертирования управляющего сигнала с выхода микроконтроллера с помощью #define WS2812B_OUTPUT_INVERSE.
Рис. 3. Вот так вся конструкция выглядит в живую
В моем случае в качестве управляющей ножки микроконтроллера была выбрана PA1. Подключаем вывод GPIO (см. рис. 2) к PA1, WS2812B_DATA к управляющему входу ленты, +5V к плюс пяти вольтам, землю к земле))) Одеваем каску и включаем питание)) Если ни чего не бахнуло, то переходим к следующему шагу 😉
Примеры на IAR ARM
В качестве примера я придумал несколько демонстраций работы с библиотекой. Среда разработки — IAR ARM 7.50.2. Начнем с простого: зажжем все светодиоды красным светом на половину яркости. Вот минимальный код запуска:
#include <stdint.h>
#include "stm32f10x.h"
#include "ws2812b.h"
void main(void)
{
ws2812b_init(); //инициализация протокола
while(!ws2812b_is_ready()) //ждем, пока завершится инициализация
;
/// Устанавливаем всем светодиодам
/// красный цвет свечения
/// яркостью 100
int i;
for(i=0; i<WS2812B_NUM_LEDS; i++)
{
ws2812b_set(i, 100, 0, 0);
}
ws2812b_send(); //отправляем буфер из ОЗУ в светодиоды
for(;;)
{
}
}
Из комментариев, думаю, все ясно без пояснений.
В живую это выглядит вот так:
Рис. 4. Подсветка для дома красных фонарей готова))
Перейдем к радуге. Не вдаваясь в подробности, для того, чтобы нарисовать радугу нам нужно уметь делать преобразование из цветового пространства HSV в RGB. Вот немного Википедии. Конвертер позаимствовал из одной статьи на Хабре, о чем честно сообщаю в комментариях))
/// Конвертер из HSV в RGB
/// Взял отсюда:
/// Статья https://habr.com/post/257131/
/// Ссылка https://drive.google.com/file/d/0B5dbvc_yPqJHQ2FEUXpkR3NocnM/view
/// Оригинальный алгоритм немного переделан, а именно
/// изменен механизм возврата значения компонент R, G, B
const uint8_t dim_curve[256] = {
0, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4,
4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6,
6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8,
8, 8, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 11, 11, 11,
11, 11, 12, 12, 12, 12, 12, 13, 13, 13, 13, 14, 14, 14, 14, 15,
15, 15, 16, 16, 16, 16, 17, 17, 17, 18, 18, 18, 19, 19, 19, 20,
20, 20, 21, 21, 22, 22, 22, 23, 23, 24, 24, 25, 25, 25, 26, 26,
27, 27, 28, 28, 29, 29, 30, 30, 31, 32, 32, 33, 33, 34, 35, 35,
36, 36, 37, 38, 38, 39, 40, 40, 41, 42, 43, 43, 44, 45, 46, 47,
48, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62,
63, 64, 65, 66, 68, 69, 70, 71, 73, 74, 75, 76, 78, 79, 81, 82,
83, 85, 86, 88, 90, 91, 93, 94, 96, 98, 99, 101, 103, 105, 107, 109,
110, 112, 114, 116, 118, 121, 123, 125, 127, 129, 132, 134, 136, 139, 141, 144,
146, 149, 151, 154, 157, 159, 162, 165, 168, 171, 174, 177, 180, 183, 186, 190,
193, 196, 200, 203, 207, 211, 214, 218, 222, 226, 230, 234, 238, 242, 248, 255,
};
/*------------------------------------------------------------------------------
Корнвертер из HSV в RGB в целочисленной арифмерите
hue : 0..360
saturation : 0..255
value : 0..255
------------------------------------------------------------------------------*/
void HSV_to_RGB(int hue, int sat, int val, uint8_t *rc, uint8_t *gc, uint8_t *bc)
{
int r;
int g;
int b;
int base;
//uint32_t rgb;
val = dim_curve[val];
sat = 255 - dim_curve[255 - sat];
if ( sat == 0 ) // Acromatic color (gray). Hue doesn't mind.
{
(*rc) = val;
(*gc) = val;
(*bc) = val;
//rgb = val | (val<<8) | (val <<16);
}
else
{
base = ((255 - sat) * val) >> 8;
switch (hue / 60)
{
case 0:
r = val;
g = (((val - base) * hue) / 60) + base;
b = base;
break;
case 1:
r = (((val - base) * (60 - (hue % 60))) / 60) + base;
g = val;
b = base;
break;
case 2:
r = base;
g = val;
b = (((val - base) * (hue % 60)) / 60) + base;
break;
case 3:
r = base;
g = (((val - base) * (60 - (hue % 60))) / 60) + base;
b = val;
break;
case 4:
r = (((val - base) * (hue % 60)) / 60) + base;
g = base;
b = val;
break;
case 5:
r = val;
g = base;
b = (((val - base) * (60 - (hue % 60))) / 60) + base;
break;
}
(*rc) = r & 0xFF;
(*gc) = g & 0xFF;
(*bc) = b & 0xFF;
//rgb = ((r & 0xFF)<<16) | ((g & 0xFF)<<8) | (b & 0xFF);
}
//return rgb;
}
Изменяя H-компоненту от 0 до 360, получаем самую обычную радугу 😉
void HSV_to_RGB(int hue, int sat, int val, uint8_t *rc, uint8_t *gc, uint8_t *bc) ;
void main(void)
{
uint8_t rc;
uint8_t gc;
uint8_t bc;
ws2812b_init();
while(!ws2812b_is_ready())
;
for(int i=0; i<WS2812B_NUM_LEDS; i++)
{
// Делаем радугу с яркостью 100
HSV_to_RGB((int)(i*360/WS2812B_NUM_LEDS), 255, 100, &rc, &gc, &bc);
ws2812b_set(i, rc, gc, bc);
}
//Выводим буфер
ws2812b_send();
for(;;)
{
}
}
Для справки: на яркости 100 лента в режиме радуги потребляет всего 130мА. А вот на полной яркости ест все 3 ампера.
Ну и небольшая демка с плавным зажиганием-погасанием радуги:
void main(void)
{
uint8_t rc;
uint8_t gc;
uint8_t bc;
ws2812b_init();
while(!ws2812b_is_ready())
;
/// Эффект "Мерцающая радуга"
int k;
for(;;)
{
for(k=0; k < 255; k++) //Изменяем яркость от 0 до 255
{
//Заполняем буфер светодиодной ленты радугой с яркостью k
for(int i=0; i<WS2812B_NUM_LEDS; i++)
{
HSV_to_RGB((int)(i*360/WS2812B_NUM_LEDS), 255, k, &rc, &gc, &bc);
ws2812b_set(i, rc, gc, bc);
}
//Выводим буфер
ws2812b_send();
//Ждем окончания передачи
while(!ws2812b_is_ready())
;
}
for(; k >= 0; k--) //Изменяем яркость от 255 до 0
{
for(int i=0; i<WS2812B_NUM_LEDS; i++)
{
HSV_to_RGB((int)(i*360/WS2812B_NUM_LEDS), 255, k, &rc, &gc, &bc);
ws2812b_set(i, rc, gc, bc);
}
ws2812b_send();
while(!ws2812b_is_ready())
;
}
}
}
Вот демка:
На видео видна некая неравномерность скорости затухания, но в реале этого не видно.
На этом все, спасибо за внимание! 🙂
Ссылки:
Проект на github: https://github.com/DiMoonElec/ws2812b_stm32f103c8_demo
Зеркало на Я.Диск: https://yadi.sk/d/K4gsANjZ3Z4Rby
Статья на Хабре, с которой было кое-что позаимствовано: https://habr.com/post/257131/


Доброго времени суток.
Сапасибо за материал. У меня как у новичка в микроконтроллерах возникло пару вопросов:
Автор:
а) — TIM2->CCMRx &= ~(TIM_CCMRy_OCxM); //сбрасываем все биты OCxM
б) — //устанавливаем выход в неактивное состояние
TIM2->CCMRx |= TIM_CCMRy_OCxM_2;
TIM2->CCMRx &= ~(TIM_CCMRy_OCxM_2);
с) — TIM2->CCMRx |= TIM_CCMRy_OCxM_2 | TIM_CCMRy_OCxM_1 | TIM_CCMRy_OCxPE; //режим ШИМ-а
Непонимающий:
а) — все понятно сброс трех битов;
б) — ??? одна строчка устанавливает бит, следующая его же и сбрасывает. Вопрос: в чём тайна этого шаманского действа?
c) — OCxM_2 и OCxM_1 — это установка режима ШИМ PWM mode 1, а OCxPE — этот бит разрешает предзагузку (буферизацию) регистра CCRx — правильно-ли я это понимаю?
СПАСИБО! За продуманный, а главное понятный код.
https://alexgyver.ru/ambilight_karman/
Версия не на таймере, а на SPI будет?
Насколько помню, с SPI есть некоторые трудности в том плане, что нужно кварц использовать на определенную частоту, чтоб тайминги можно было правильные настроить. Можно на UART-е сделать, но он должен поддерживать то ли 7, то ли 9 бит данных, и этого режима нет у F103-м МК.
Насчет того, будет ли продолжение по теме WS2812 пока трудно сказать, задач с переходом с таймера на что-то другое пока нет, так что не могу сказать…