Драйвер светодиодной ленты на WS2812B для STM32F103C8

Приобрел я на 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(;;)
  {
  }
  
}

Рис. 5. Радуга

Для справки: на яркости 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/

Метки: , , , . Закладка Постоянная ссылка.

5 комментариев: Драйвер светодиодной ленты на WS2812B для STM32F103C8

  1. Василий пишет:

    Доброго времени суток.
    Сапасибо за материал. У меня как у новичка в микроконтроллерах возникло пару вопросов:

    Автор:
    а) — 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 — правильно-ли я это понимаю?

  2. Александр пишет:

    СПАСИБО! За продуманный, а главное понятный код.

  3. Леонид пишет:

    Версия не на таймере, а на SPI будет?

    • DiMoon пишет:

      Насколько помню, с SPI есть некоторые трудности в том плане, что нужно кварц использовать на определенную частоту, чтоб тайминги можно было правильные настроить. Можно на UART-е сделать, но он должен поддерживать то ли 7, то ли 9 бит данных, и этого режима нет у F103-м МК.
      Насчет того, будет ли продолжение по теме WS2812 пока трудно сказать, задач с переходом с таймера на что-то другое пока нет, так что не могу сказать…

Добавить комментарий для Александр Отменить ответ