Приобрел я на 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 пока трудно сказать, задач с переходом с таймера на что-то другое пока нет, так что не могу сказать…