Pull to refresh

Протокол WS2812B на STM32 без пустых циклов и прерываний. И как сделать правильную радугу

DIY


На Хабре уже есть пара статей о работе с RGB светодиодами WS2112B, но почему-то они все используют довольно архаичный способ формирования битовой последовательности. Способ заключается в формировании точных интервалов времени с помощью пустых программных циклов. Возможно, это издержки использования Arduino, но мы, конечно, уже давно перешли на ARM Cortex-M4 в лице STM32 и можем себе позволить сделать красивее.

Итак, напомню “протокол” WS2112B.



Светодиодная полоса на WS2112B имеет всего один цифровой вход – DIN, подключенный к первому светодиоду на полосе. На него подается специальная импульсная последовательность, кодирующая биты, как изображено на рисунке. У каждого светодиода есть один цифровой выход – DOUT соединенный с входом DIN следующего светодиода на полосе. Каждому светодиоду нужно передать 24 бита (по 8 бит на каждый цвет: красный R, зеленый G и синий B). Таким образом, чтобы зажечь все светодиоды надо передавать 24*N бит, где N количество светодиодов на полосе.

Приняв биты светодиоды загораются и статично горят пока не получат новую битовую последовательность. Каждая битовая последовательность начинается с установки DIN в лог. ноль на время не менее 50 мкс.

Как видно биты кодируются достаточно короткими импульсами с жесткими допусками. От микроконтроллера, пытающегося сформировать их программными задержками требуется как минимум запретить все прерывания чтобы невзначай не сформировался сброс или сбойный бит. Ресурсы процессорного времени здесь также расходуются нерационально, чтобы зажечь 100 светодиодов процессору нужно отработать 3 мс. Если обновлять состояние светодиодов с частотой 100 Гц, то такой” протокол” заберет 30% процессорного времени.

Слышатся предложения использовать интерфейс SPI для передачи битового потока на WS2112B. Но здесь препятствием может стать недостаточное соответствие тактовой частоты системной шины и сильные погрешности длительностей импульсов.

А между тем в STM32 и вообще во всех чипах на Cortex-M есть отличный механизм прямого доступа к памяти (DMA). Биты можно формировать с помощью таймеров в режиме широтно-импульсной модуляции, а каждый следующий бит извлекать из ОЗУ с помощью DMA.

На рисунке ниже показана схема взаимодействия DMA и таймера TIM4 в чипе STM32F407VET6. Отладка проводилась на моем промышленном контроллере именно с таким чипом, но с тем же успехом все можно повторить на любом чипе семейства STM32. В данном случае у меня именно вывод 8 GPIOB был свободен, чем я и воспользовался.



Далее текст инициализации таймера и контроллера:
#define     BIT(n) (1u << n)
#define     LSHIFT(v,n) (((unsigned int)(v) << n))

#define LEDS_NUM    80
#define COLRS       3

INT16U DMA_buf[LEDS_NUM+2][COLRS][8];

/*------------------------------------------------------------------------------
  Timer4 генерирует импульсы на светодиодную полосу
  Тактирование таймера идет от PCLK1 72 MHz

  Канал 3 таймера используется в режиме Compare с загрузкой по DMA регистра CCR3 для формирования битовых сигналов
 ------------------------------------------------------------------------------*/
void Timer4_init(void)
{
  TIM_TypeDef *tim = TIM4;
  RCC_TypeDef *rcc = RCC;

  rcc->APB1RSTR |= BIT(2);    // Сброс таймера 4
  rcc->APB1RSTR &= ~BIT(2);   
  rcc->APB1ENR |= BIT(2);     // Разрешаем тактирование таймера 4
  tim->CR1 = BIT(7);          //  1: TIMx_ARR register is buffered.
  tim->CR2 = 0;               
  tim->PSC = 0;               // Предделитель генерирует частоту 72 МГц
  tim->ARR = 90 - 1;          // Перегрузка таймера каждые 1.25 мкс
  tim->CCMR2 = 0
               + LSHIFT(6, 4) // OC3M: Output compare 3 mode | 110: PWM mode 1 - In upcounting, channel 1 is active as long as TIMx_CNT<TIMx_CCR1 else inactive.
               + LSHIFT(1, 3) // OC3PE: Output compare 3 preload enable
               + LSHIFT(0, 0) // CC3S: Capture/Compare 3 selection | 00: CC3 channel is configured as output
  ; 
  tim->CNT = 0;
  tim->CCR3 = 0;
  tim->DIER = BIT(11);        // Bit 11 CC3DE: Capture/Compare 3 DMA request enable. Разрешаем запросы DMA
  tim->CR1 |= BIT(0);         // Запускаем таймер
  tim->CCER = BIT(8);         // Разрешаем работы выхода, чтобы возникали сигналы для DMA
}

/*------------------------------------------------------------------------------
  Инициализация канала 2 DMA1 Stream 7
  Используется для пересылки шаблоной битов потока управления светодиодной лентой на WS2812B в таймер TMR4 работающий в режиме генерации PWM 
 ------------------------------------------------------------------------------*/
void DMA1_Stream7_Mem_to_TMR4_init(void)
{
  DMA_Stream_TypeDef *dma_ch = DMA1_Stream7;
  RCC_TypeDef *rcc = RCC;

  rcc->AHB1ENR |= BIT(21);               // Разрешаем DMA1

  dma_ch->CR = 0;    // Выключаем стрим
  dma_ch->PAR = (unsigned int)&(TIM4->CCR3) + 1;  // Назначаем адрес регистра данных ADC
  dma_ch->M0AR = (unsigned long)&DMA_buf;
  dma_ch->NDTR = (LEDS_NUM + 2) * COLRS * 8;
  dma_ch->CR =
               LSHIFT(2, 25) + // CHSEL[2:0]: Channel selection |    010: channel 2 selected
               LSHIFT(0, 23) + // MBURST: Memory burst transfer configuration | 00: single transfer
               LSHIFT(0, 21) + // PBURST[1:0]: Peripheral burst transfer configuration | 00: single transfer
               LSHIFT(0, 19) + // CT: Current target (only in double buffer mode) | 0: The current target memory is Memory 0 (addressed by the DMA_SxM0AR pointer)
               LSHIFT(0, 18) + // DBM: Double buffer mode | 0: No buffer switching at the end of transfer
               LSHIFT(3, 16) + // PL[1:0]: Priority level | 11: Very high.  PL[1:0]: Priority level
               LSHIFT(0, 15) + // PINCOS: Peripheral increment offset size | 0: The offset size for the peripheral address calculation is linked to the PSIZE
               LSHIFT(1, 13) + // MSIZE[1:0]: Memory data size | 00: 8-bit. Memory data size
               LSHIFT(1, 11) + // PSIZE[1:0]: Peripheral data size | 00: 8-bit. Peripheral data size
               LSHIFT(1, 10) + // MINC: Memory increment mode | 1: Memory address pointer is incremented after each data transfer (increment is done according to MSIZE)
               LSHIFT(0, 9) +  // PINC: Peripheral increment mode | 0: Peripheral address pointer is fixed
               LSHIFT(1, 8) +  // CIRC: Circular mode | 1: Circular mode enabled
               LSHIFT(1, 6) +  // DIR[1:0]: Data transfer direction | 01: Memory-to-peripheral
               LSHIFT(0, 5) +  // PFCTRL: Peripheral flow controller | 1: The peripheral is the flow controller
               LSHIFT(1, 4) +  // TCIE: Transfer complete interrupt enable | 1: TC interrupt enabled
               LSHIFT(0, 3) +  // HTIE: Half transfer interrupt enable | 0: HT interrupt disabled
               LSHIFT(0, 2) +  // TEIE: Transfer error interrupt enable | 0 : TE interrupt disabled
               LSHIFT(0, 1) +  // DMEIE: Direct mode error interrupt enable | 0: Direct mode error interrupt disabled
               LSHIFT(0, 0) +  // EN: Stream enable | 1: Stream enabled
               0;

  dma_ch->FCR =
                LSHIFT(0, 7) + // FEIE: FIFO error interrupt enable
                LSHIFT(1, 2) + // DMDIS: Direct mode disable | 1: Direct mode disabled. Разрешаем чтобы была возможность пересылки из байт в двухбайтовый регистр 
                LSHIFT(1, 0) + // FTH[1:0]: FIFO threshold selection | 01: 1/2 full FIFO
                0;
  dma_ch->CR |= BIT(0); //  1: Stream enabled
}





После этой инициализации начинается автоматическая пересылка битового потока из массива DMA_buf расположенного в ОЗУ на внешний вывод 8 GPIOB. Автоматически генерируется и 50-и микросекундная пауза сброса. Процессор в пересылке никак не участвует, не используются даже прерывания. Чтобы зажечь какой-либо светодиод, надо просто записать соответствующее слово в массив DMA_buf по соответствующему смещению. Это делается в проекте функцией LEDstrip_set_led_state.

Нельзя сказать, что данный механизм вообще не влияет на процессор. Его работа несколько замедляется. Поскольку он разделяет вместе с DMA общий доступ к ОЗУ и системной шине. Но измерения показали, что это замедление в данном случае не превышает 0.2%

Для написания проекта использовалась среда разработки MDK-ARM Professional Version: 4.72.1.0. Частота процессора – 144 МГц, частота PCLK1 – 72 МГц. Легко переносится на платы серии STM32 MCU Discovery Kits. Весь проект выложен здесь

В проекте не были использованы библиотеки от ST или какие-либо другие сторонние библиотеки. Проект очень компактный, всё написано через прямые обращения к регистрам, это делает текст короче, яснее и позволяет его легче переносить в другие среды разработки.

И о радуге


Дело в том, что просто линейно инкрементируя байты в слове в RGB формате цвета (битовое представление — 00000000 RRRRRRRR GGGGGGGG BBBBBBBB) нельзя на светодиодной полосе с сотней светодиодов изобразить красивую радугу. Еще трудней у этой радуги регулировать яркость, делая простые манипуляции на 32-битном слове с RGB информацией. Для таких манипуляций используют HSV формат. Например, вся радуга будет представлена если просто линейно инкрементировать H-составляющую. Потом уже преобразовать HSV в RGB и вывести на светодиоды.
В проекте есть два конвертера HSV в RGB, один целочисленный, а другой с использованием вычислений с плавающей точкой. Визуально я отличий не увидел. Да, к сожалению, здесь STM32 отличиться негде.
Tags:
Hubs:
Total votes 42: ↑41 and ↓1 +40
Views 45K
Comments Comments 14