piątek, 1 września 2017

[29] STM32F4 - WS2812 za pomocą PWM z DMA, Biblioteki STD

Ten post chciałbym poświęcić na opisanie sterowania diodami WS2812. Program będzie wykorzystał sygnał PWM wraz z DMA z jednego z timerów:

[Źródło: https://www.tweaking4all.com/hardware/arduino/arduino-ws2812-led/]

Diody WS2812


Aby wysterować pojedynczą diodę należy przesłać do niej 24bity, które będą zawierały informacje odnośnie składowych RGB. Komunikacja odbywa się poprzez prosty interfejs szeregowy, w którym wartości 0 oraz 1 ustalane są za pomocą szerokości impulsu dodatniego.

W związku z tym bardzo ważne jest pilnowanie czasów podczas ich wysyłania. Czas trwania całego bitu wynosi 1,25us (1250ns) +/- 600ns. Bit 1 jest impulsem dodatnim o czasie trwania 0.9us(800ns) +/- 150ns. Bit 0 także jest impulsem dodatnim o czasie trwania 0,4us(400ns) +/- 150ns.
Po wystąpieniu impulsu dodatniego musi wystąpić stan niski o długości 0,35us(350ns) +/- 150ns, dla bitu zerowego impuls ujemny po nim występujący ma mieć długość 0,8us(800ns) +/- 150ns.

Dane są zapisywane do sterownika w diodzie po podaniu sygnału reset o długości minimum 50us. Ten sygnał wprowadza układ także w tryb odbierania nowych danych.

Format danych jak wspomniałem wcześniej jest 24 bitowy po 8 bitów na każdą składową. Najpierw przesyłany jest kolor zielony, potem czerwony, a na końcu niebieski.

Diody połączone są szeregowo wejście danych DIN w pierwszej diodzie jest podłączone do mikrokontrolera, wyjście danych DOUT natomiast jest podłączone do kolejnej diody. Powoduje to, że dane są przesyłane do kolejnych układów. Czyli pierwsze 24 bity są zapisywane w pierwszej podłączonej diodzie, drugie w kolejnej itd. Czyli całkowita ilość potrzebnych danych określona jest przez maksymalną ilość podłączonych diod do linii. 

Podczas przesyłania danych dane są retransmitowane oraz odtwarzane, dzięki temu przesyłany sygnał nie ulega zniekształceniu. 

Jeśli chodzi o część sprzętową to zasilanie układów powinno znajdować się w przedziale od 3,5 do 5,3V. Dane będą z układu przesyłane wobec tego nie ma potrzeby się martwić uszkodzeniem mikrokontrolera. Należy przy tym pamiętać o spadku zasilania na diodach, natomiast będzie to miało wpływ w tylko w przypadku podłączenia znacznej ilości tych diod. W takim przypadku trzeba stosować dodatkowe zasilacze z podłączoną wspólną masą w kolejnych punktach paska.

Aby zasilanie oraz transmisja danych były prawidłowe należy pamiętać aby na paskach (jeśli gotowe ze sklepu to napewno będą zawierały) był umieszczony kondensator filtrujący zasilanie o wartości 100 nF. 

Przebiegi:

Tutaj przedstawię przykładowe przebiegi otrzymane za pomocą analizatora stanów logicznych:

Czasy dla impulsu wysokiego:


Czasy dla przebiegu niskiego:


Programowanie:


Przygotowane funkcje testowałem na łańcuchu składającym się z ośmiu diod WS2812. Jako przykład wykorzystałem timer 3 kanał 1. Przystosowanie przykładu do innych kanałów czy też dla timera 4 nie stanowi problemu i potrzeba jedynie zmienić główne parametry.

Do inicjalizacji należy wywołać tylko jedną funkcje, która wywoła funkcje wewnętrzne przechodzące przez wszystkie niezbędne elementy:

Główna funkcja odpowiedzialna za ustawienie parametrów oraz włączenie niezbędnych funkcji:

Poniżej poszczególne funkcje ustawiające kolejne parametry. Najpierw przygotowanie struktury z parametrami:

GPIO:

  1. static void WS2812InitGPIO(void)
  2. {
  3.   #ifdef WS2812_TIM3_CH1
  4.         WS2812InitGpio(GPIOC, GPIO_Pin_6, RCC_AHB1Periph_GPIOC, GPIO_PinSource6, GPIO_AF_TIM3);
  5.   #endif
  6. }
  7. static void WS2812InitGpio(GPIO_TypeDef* GPIOx, uint32_t GPIO_Pin, uint32_t gpioClock,                               uint8_t pinSource, uint8_talternFunction)
  8. {
  9.   GPIO_InitTypeDef GPIO_InitStructure;
  10.   RCC_AHB1PeriphClockCmd(gpioClock, ENABLE);
  11.   GPIO_InitStructure.GPIO_Pin = GPIO_Pin;
  12.   GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
  13.   GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
  14.   GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;  
  15.   GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP ;
  16.   GPIO_Init(GPIOx, &GPIO_InitStructure);
  17.   GPIOx->BSRRH = GPIO_Pin;
  18.   GPIO_PinAFConfig(GPIOx, pinSource, alternFunction);  
  19. }

Timer:

  1. void WS2812InitTimer(void)
  2. {
  3.   TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
  4.   TIM_OCInitTypeDef  TIM_OCInitStructure;
  5.   #ifdef WS2812_TIM3_CH1
  6.   RCC_APB1PeriphClockCmd(WS2812_TIM_CLOCK, ENABLE);
  7.   RCC_AHB1PeriphClockCmd(WS2812_DMA_CLOCK, ENABLE);
  8.   TIM_TimeBaseStructure.TIM_Period = WS2812_TIM_PERIODE;
  9.   TIM_TimeBaseStructure.TIM_Prescaler = WS2812_TIM_PRESCALE;
  10.   TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
  11.   TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
  12.   TIM_TimeBaseInit(WS2812_TIM, &TIM_TimeBaseStructure);
  13.   TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
  14.   TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
  15.   TIM_OCInitStructure.TIM_Pulse = 0;
  16.   TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
  17.   TIM_OC1Init(WS2812_TIM, &TIM_OCInitStructure);
  18.   TIM_OC1PreloadConfig(WS2812_TIM, TIM_OCPreload_Enable);
  19.   TIM_ARRPreloadConfig(WS2812_TIM, ENABLE);
  20.    
  21.   #endif
  22. }

Przerwania:

  1. void WS2812InitInterNVIC(void)
  2. {
  3.   #ifdef WS2812_TIM3_CH1
  4.       InitInterrupts(WS2812_TIM, WS2812_TIM_DMA_TRG1, WS2812_DMA_CH1_IRQn, 0, 0);
  5.     #endif
  6. }
  7. static void InitInterrupts(TIM_TypeDef* TIMx, uint16_t timerChannel, uint8_t irqChannel,uint8_t preePriority, uint8_t subPriority)
  8. {
  9.   NVIC_InitTypeDef NVIC_InitStructure;
  10.   TIM_DMACmd(TIMx, timerChannel, ENABLE);
  11.   NVIC_InitStructure.NVIC_IRQChannel = irqChannel;
  12.   NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = preePriority;
  13.   NVIC_InitStructure.NVIC_IRQChannelSubPriority = subPriority;
  14.   NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
  15.   NVIC_Init(&NVIC_InitStructure);
  16. }

DMA:

  1. void WS2812InitDMA(void)
  2. {
  3.     DMA_InitTypeDef DMA_InitStructure;
  4.    
  5.   if(TIM3_CH1_t.ws2812Channel==0) return;
  6.   if(TIM3_CH1_t.ws2812Channel==1)
  7.     {
  8.         InitDma(WS2812_DMA_CH1_STREAM, WS2812_DMA_CH1_CHANNEL, (uint32_t)WS2812_TIMER_BUF, (uint32_t)WS2812_TIMER_BUF_LEN1);
  9.   }
  10. }
  11. static void InitDma(DMA_Stream_TypeDef* DMAy_Streamx, uint32_t dmaChannel, uint32_t memmory0Base, uint32_t bufferSize)
  12. {
  13.     DMA_InitTypeDef DMA_InitStructure;
  14.    
  15.     DMA_Cmd(DMAy_Streamx, DISABLE);
  16.     DMA_DeInit(DMAy_Streamx);
  17.     DMA_InitStructure.DMA_Channel = (dmaChannel);
  18.     #ifdef WS2812_TIM3_CH1
  19.     DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&TIM3->CCR1;
  20.     DMA_InitStructure.DMA_Memory0BaseAddr = memmory0Base;
  21.     DMA_InitStructure.DMA_BufferSize = bufferSize;
  22.     #endif
  23.     DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToPeripheral;
  24.     DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
  25.     DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
  26.     DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
  27.     DMA_InitStructure.DMA_MemoryDataSize = DMA_PeripheralDataSize_HalfWord;
  28.     DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
  29.     DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh;
  30.     DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;
  31.     DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull;
  32.     DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;
  33.     DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
  34.     DMA_Init(DMAy_Streamx, &DMA_InitStructure);
  35. }

Funkcje ustawiające dane w buforze

  1. static void WS2812PrepTimerBuf(WS2812_DiodeParam_t * diodeStruct)
  2. {
  3.     uint8_t i = 0;
  4.     uint32_t pos = 0;
  5.     WS2812_RGB_Color_t rgbStruct;
  6.     uint8_t maskTable[8] = {0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01};
  7.    
  8.   /* Set data 24 bits for every diodes */
  9.   for(uint32_t loop=0;loop<diodeStruct->countMax;loop++)
  10.   {
  11.     rgbStruct=diodeStruct->rgbPointer[loop];
  12.        
  13.         for(i=0; i<8; i++)
  14.         {

  15.             if((rgbStruct.green & maskTable[i])!=0) { WS2812_TIMER_BUF[pos++]=WS2812_HI_TIME; }  else{ WS2812_TIMER_BUF[pos++]=WS2812_LO_TIME; }
  16.         }  
  17.        
  18.         DelayTime(500);
  19.        
  20.         for(i=0; i<8; i++)
  21.         {

  22.         if((rgbStruct.red & maskTable[i])!=0) { WS2812_TIMER_BUF[pos++]=WS2812_HI_TIME; }
  23.             else { WS2812_TIMER_BUF[pos++]=WS2812_LO_TIME; }
  24.         }  
  25.         DelayTime(500);
  26.         for(i=0; i<8; i++)

  27. {          if((rgbStruct.blue & maskTable[i])!=0) { WS2812_TIMER_BUF[pos++]=WS2812_HI_TIME; }
  28.             else{ WS2812_TIMER_BUF[pos++]=WS2812_LO_TIME; }
  29.         }  
  30.         DelayTime(500);
  31.   }
  32.    
  33.   /* Add pause after prepare whole data buffer */
  34.   for(uint32_t loop=0;loop<(WS2812_PAUSE*WS2812_ONE_LED_BITS);loop++)
  35.   {
  36.     WS2812_TIMER_BUF[pos++]=0;
  37.   }
  38. }

Kolejnym elementem jest wykonanie kodowanie barw HSV, która pozwala na modyfikacje wyświetlanych kolorów. Jego reprezentacja składa się z odcienia światła, który jest wyrażony kątem na tzw. kole barw (w kodzie zmienna hue). Można go wyrazić w wartości od 0 do 360 stopni. W bibliotece reprezentacja barw znajduje się dla wartości od 0 do 359. Jeśli wykorzystuje się model barw z zakresu od 0 do 255. Przydaje się to w przypadku obsługi słabszych mikrokontrolerów np Atmega328p. Wartość 0 oznacza wartość kompletnie białą, połowa oznacza pół nasycenie barwy. Wartość maksymalna oznacza pełne nasycenie barwy.

Druga zmienna saturation czyli nasycenie koloru odpowiada wymiarom stożka barw HSV. Wartość 0 oznacza barwę białą natomiast maksymalna czyli 255 oznacza pełne kolory. Ostatnia zmienna value czyli wartość. Odnosi się ona do jasności świecenia 0 oznacza brak światła, 255 maksymalna jasność światła. Czyli dzięki niej można wybrać moc światła białego.

Poniżej wartości centrum określonej barwy dla odcieni światła (czyli zmienna hue). Każdy kolor pośredni znajduje się pomiędzy podanymi wartościami:

  • Wartość 0 do 60 - czerwony;
  • Wartość 60 do 120 - żółty
  • Wartość 120 do 180 - zielony;
  • Wartość 180 do 240 - cyjan
  • Wartość 240 do 300 - niebieski;
  • Wartość 300 do 360 - magenta;

Wyświetlany kolor zależy także od pozostałych wartości czyli nasycenia oraz jasności świecenia diody:


  • Czarny - [HSV] - 0, 0, 0; [RGB] - 0, 0, 0;
  • Biały - [HSV] - 0, 0, 100;  [RGB] - 255, 255, 255;
  • Czerwony - [HSV] - 0, 100, 100; [RGB] - 255, 0, 0;
  • Limonkowy - [HSV] - 120, 100, 100; [RGB] - 0, 255, 0;
  • Niebieski - [HSV] - 240, 100, 100; [RGB] - 0, 0, 255;
  • Żółty - [HSV] - 60, 100, 100; [RGB] - 255, 255,  0;
  • Cyan - [HSV] - 180, 100, 100; [RGB] - 0, 255, 255;
  • Magenta - [HSV] - 300, 100, 100;  [RGB] - 255, 0, 255;
  • Srebrny - [HSV] - 0, 0, 75; [RGB] - 191, 191, 191;
  • Szary - [HSV] - 0, 0, 50; [RGB] - 128, 128, 128;
  • Kasztanowy - [HSV] - 0, 100, 50; [RGB] - 128, 0, 0;
  • Oliwkowy - [HSV] - 60, 100, 50; [RGB] - 128, 128, 0;
  • Zielony - [HSV] - 120, 100, 50; [RGB] - 0, 128, 0;
  • Purpurowy - [HSV] - 300, 100, 50; [RGB] - 128, 0, 128;
  • Morski - [HSV] - 180, 100, 50; [RGB] - 0, 128, 128;
  • Granatowy - [HSV] - 240, 100, 50; [RGB] - 0, 0, 128;

  1. void WS2812ConvertRGBToHSV(WS2812_HSV_Color_t hsv_color, WS2812_RGB_Color_t *rgb_color)
  2. {
  3.     /* Check pass value */
  4.   if(hsv_color.hue>359) hsv_color.hue=359}
  5.   if(hsv_color.saturation>100) hsv_color.saturation=100}
  6.   if(hsv_color.value>100) hsv_color.value=100}
  7.     /* Prepare values */
  8.     if(hsv_color.hue <= HSV_YELLOW) {  
  9.       rgb_color->red = 255;
  10.       rgb_color->green = (425 * hsv_color.hue) / 100;
  11.       rgb_color->blue = 0;
  12.     }
  13.     else if(hsv_color.hue <= HSV_GREEN) {
  14.         rgb_color->red = 255 - ((425 * (hsv_color.hue-60))/100);
  15.         rgb_color->green = 255;
  16.     rgb_color->blue = 0;
  17.   }
  18.     else if(hsv_color.hue <= HSV_CYJAN){
  19.     rgb_color->red = 0;
  20.     rgb_color->green = 255;
  21.     rgb_color->blue = (425 * (hsv_color.hue-120))/100;
  22.   }
  23.     else if(hsv_color.hue <= HSV_BLUE){
  24.     rgb_color->red = 0;
  25.     rgb_color->green = 255 - ((425 * (hsv_color.hue-180))/100);
  26.     rgb_color->blue = 255;
  27.   }
  28.     else if(hsv_color.hue <= HSV_MAGENTA){
  29.     rgb_color->red = (425 * (hsv_color.hue-240))/100;
  30.     rgb_color->green = 0;
  31.     rgb_color->blue = 255;
  32.   }
  33.     else {
  34.     rgb_color->red = 255;
  35.     rgb_color->green = 0;
  36.     rgb_color->blue = 255 - ((425 * (hsv_color.hue-300))/100);
  37.   }
  38.     /* rest calculation */
  39.   hsv_color.saturation = 100 - hsv_color.saturation;
  40.     rgb_color->red += ((255 - rgb_color->red) * hsv_color.saturation)/100;
  41.     rgb_color->green += ((255 - rgb_color->green) * hsv_color.saturation)/100;
  42.     rgb_color->blue += ((255 - rgb_color->blue) * hsv_color.saturation)/100;
  43.    
  44.   rgb_color->red *= (hsv_color.value)/100;
  45.   rgb_color->green *= (hsv_color.value)/100;
  46.   rgb_color->blue *= (hsv_color.value)/100;
  47. }

Pozostałe funkcje zostały zawarte w bibliotece, którą można pobrać z dysku Google pod tym linkiem.

Z tej biblioteki można także sterować innymi rodzajami diody WS, wystarczy tylko odpowiednio dostosować czasy dla konkretnych modelów.