In the pervious guide(here), we took a look at the WS2812 and they work and type of communication required.
In this guide, we shall see how to use PWM in DMA mode to control those LED.
4. Setting up the PWM and DMA:
Since we shall use TIMER 1 of the STM32 to generate the required data signal, we shall develop everything around TIMER1.
Since TIM1_CH1 is connected to PA8, we need to enable clock access to GPIOA and set PA8 to alternate function and select AF1 for PA8 as following:
#define AF01 (0x01) RCC->AHB1ENR|=RCC_AHB1ENR_GPIOAEN; GPIOA->MODER|=GPIO_MODER_MODE8_1; GPIOA->MODER&=~GPIO_MODER_MODE8_0; GPIOA->AFR[1]|=(AF01<<0);
Then enable clock access to timer1
RCC->APB2ENR|=RCC_APB2ENR_TIM1EN; //enable clock access to tim1
Set the prescaler to 0:
TIM1->PSC=0; //set prescaller to 0 (no divider)
Set the ARR to 90 (maximum value to count):
TIM1->ARR=90-1; //set the maximum count value
Enable Capture/Compare 1 DMA request:
TIM1->DIER|=TIM_DIER_CC1DE;
Set CH1 as PWM mode and enable Output Compare 1 preload:
TIM1->CCMR1|=TIM_CCMR1_OC1M_2|TIM_CCMR1_OC1M_1|TIM_CCMR1_OC1PE; //configure the pin as PWM
Enable Capture/Compare channel 1:
TIM1->CCER|=TIM_CCER_CC1E; //enable channel1
Set DMA address for full transfer to 1:
TIM1->DMAR|=0x01;
Enable main output (For advanced timers only):
TIM1->BDTR=TIM_BDTR_MOE;
Now, for DMA Setup.
Since TIM1 is connected to DMA2 Stream1 Channel 6, we shall first enable clock access to DMA2 and disable the stream as following:
RCC->AHB1ENR|=RCC_AHB1ENR_DMA2EN; DMA2_Stream1->CR&=~DMA_SxCR_EN; while((DMA2_Stream1->CR)&DMA_SxCR_EN){;}
Then we shall configure the DMA as following:
- Channel number 6
- Memory and peripheral 6 to half-word (16-bit)
- Memory Increment mode.
- Direct memory to peripheral.
- Transfer complete interrupt.
#define CH6 ((0x06<<25)) DMA2_Stream1->CR|=CH6|DMA_SxCR_MSIZE_0|DMA_SxCR_PSIZE_0|DMA_SxCR_MINC|DMA_SxCR_DIR_0|DMA_SxCR_TCIE;
Set the peripheral address to TIM1->CCR1
DMA2_Stream1->PAR=(uint32_t)(&TIM1->CCR1);
Enable DMA2_Stream1 interrupt in NVIC:
NVIC_EnableIRQ(DMA2_Stream1_IRQn);
Hence, the initializing of PWM and DMA is as following:
void WS2812_init(void){ #define CH6 ((0x06<<25)) #define AF01 (0x01) //GPIO init RCC->AHB1ENR|=RCC_AHB1ENR_GPIOAEN; GPIOA->MODER|=GPIO_MODER_MODE8_1; GPIOA->MODER&=~GPIO_MODER_MODE8_0; GPIOA->AFR[1]|=(AF01<<0); //init timer; RCC->APB2ENR|=RCC_APB2ENR_TIM1EN; //enable clock access to tim1 TIM1->PSC=0; //set prescaller to 0 (no divider) TIM1->ARR=90-1; //set the maximum count value TIM1->CNT=0; //reset the current count TIM1->DIER|=TIM_DIER_CC1DE; TIM1->CCMR1|=TIM_CCMR1_OC1M_2|TIM_CCMR1_OC1M_1|TIM_CCMR1_OC1PE; //configure the pin as PWM TIM1->CCER|=TIM_CCER_CC1E; //enable channel1 TIM1->DMAR|=0x01; TIM1->BDTR=TIM_BDTR_MOE; //init dma RCC->AHB1ENR|=RCC_AHB1ENR_DMA2EN; DMA2_Stream1->CR&=~DMA_SxCR_EN; while((DMA2_Stream1->CR)&DMA_SxCR_EN){;} DMA2_Stream1->CR|=CH6|DMA_SxCR_MSIZE_0|DMA_SxCR_PSIZE_0|DMA_SxCR_MINC|DMA_SxCR_DIR_0|DMA_SxCR_TCIE; DMA2_Stream1->PAR=(uint32_t)(&TIM1->CCR1); NVIC_EnableIRQ(DMA2_Stream1_IRQn); }
5. Setting up the LEDs:
In this guide, we shall use LEDs ring which has 24 LEDs. Hence, we can create symbolic name for number of LEDs as following:
#define Num_of_LEDs 24
Then we shall create two two-dimensional arrays, one for GRB data and the other for brightness:
uint8_t LED_Data[Num_of_LEDs][4]; uint8_t LED_Mod[Num_of_LEDs][4]; // for brightness
Hence, the set led function is as following:
It takes four arguments:
- LED Number.
- Red value.
- Green value.
- Blue value.
void Set_LED (int LEDnum, int Red, int Green, int Blue) { LED_Data[LEDnum][0] = LEDnum; LED_Data[LEDnum][1] = Green; LED_Data[LEDnum][2] = Red; LED_Data[LEDnum][3] = Blue; }
For setting the brightness of all LEDs:
The function takes single argument which is the brightness with a value from 0 to 45:
void Set_Brightness (int brightness) // 0-45 { #define PI 3.14159265 if (brightness > 45) brightness = 45; for (int i=0; i<Num_of_LEDs; i++) { LED_Mod[i][0] = LED_Data[i][0]; for (int j=1; j<4; j++) { float angle = 90-brightness; // in degrees angle = angle*PI / 180; // in rad LED_Mod[i][j] = (LED_Data[i][j])/(tan(angle)); } } }
6. Sending the data:
First we create an array with the size of number of LEDs multiplied by 24 then adding 50.
uint16_t pwmData[(24*Num_of_LEDs)+50];
To send the data:
void WS2812_display() { uint32_t indx=0; uint32_t color; for (int i= 0; i<Num_of_LEDs; i++) { color = ((LED_Mod[i][1]<<16) | (LED_Mod[i][2]<<8) | (LED_Mod[i][3])); for (int i=23; i>=0; i--) { if (color&(1<<i)) { pwmData[indx] = 60; // 2/3 of 90 } else pwmData[indx] = 30; // 1/3 of 90 indx++; } } for (int i=0; i<50; i++) { pwmData[indx] = 0; indx++; } DMA2_Stream1->M0AR=(uint32_t)(&pwmData); DMA2_Stream1->NDTR=indx; DMA2->LIFCR|=DMA_LIFCR_CFEIF1|DMA_LIFCR_CDMEIF1|DMA_LIFCR_CTEIF1|DMA_LIFCR_CHTIF1|DMA_LIFCR_CTCIF1; TIM1->CR1|=TIM_CR1_CEN; //enable timer DMA2_Stream1->CR|=DMA_SxCR_EN; while(finished==0); finished=0; }
The function takes first LED and extract the color value. Then, from the color value, it will build the first 24-bit data for first LED by setting the duty cycle to 60 if the bit is 1 and 30 if the bit is zero (as seen in part 1). Then the code shall iterate for all LEDs. After finishing all the LEDs, it will add 50 extra of 0% of duty cycle to indicate the end of transmission. then launch the DMA transfer and wait for the transfer to complete.
DMA2_Steam1 interrupt handler:
void DMA2_Stream1_IRQHandler(void) { if((DMA2->LISR&DMA_LISR_TCIF1)) { finished=1; TIM1->CR1&=~TIM_CR1_CEN; DMA2->LIFCR|=DMA_LIFCR_CTCIF1; } }
Hence, the entire ws2812 source file as following:
#include <WS2812.h> #include "stm32f4xx.h" #include "math.h" #include "string.h" volatile uint8_t finished; #define Num_of_LEDs 24 void WS2812_init(void){ #define CH6 ((0x06<<25)) #define AF01 (0x01) //GPIO init RCC->AHB1ENR|=RCC_AHB1ENR_GPIOAEN; GPIOA->MODER|=GPIO_MODER_MODE8_1; GPIOA->MODER&=~GPIO_MODER_MODE8_0; GPIOA->AFR[1]|=(AF01<<0); //init timer; RCC->APB2ENR|=RCC_APB2ENR_TIM1EN; //enable clock access to tim1 TIM1->PSC=0; //set prescaller to 0 (no divider) TIM1->ARR=90-1; //set the maximum count value TIM1->CNT=0; //reset the current count TIM1->DIER|=TIM_DIER_CC1DE; TIM1->CCMR1|=TIM_CCMR1_OC1M_2|TIM_CCMR1_OC1M_1|TIM_CCMR1_OC1PE; //configure the pin as PWM TIM1->CCER|=TIM_CCER_CC1E; //enable channel1 TIM1->DMAR|=0x01; TIM1->BDTR=TIM_BDTR_MOE; //init dma RCC->AHB1ENR|=RCC_AHB1ENR_DMA2EN; DMA2_Stream1->CR&=~DMA_SxCR_EN; while((DMA2_Stream1->CR)&DMA_SxCR_EN){;} DMA2_Stream1->CR|=CH6|DMA_SxCR_MSIZE_0|DMA_SxCR_PSIZE_0|DMA_SxCR_MINC|DMA_SxCR_DIR_0|DMA_SxCR_TCIE; DMA2_Stream1->PAR=(uint32_t)(&TIM1->CCR1); NVIC_EnableIRQ(DMA2_Stream1_IRQn); } uint8_t LED_Data[Num_of_LEDs][4]; uint8_t LED_Mod[Num_of_LEDs][4]; // for brightness void Set_LED (int LEDnum, int Red, int Green, int Blue) { LED_Data[LEDnum][0] = LEDnum; LED_Data[LEDnum][1] = Green; LED_Data[LEDnum][2] = Red; LED_Data[LEDnum][3] = Blue; } void Set_Brightness (int brightness) // 0-45 { #define PI 3.14159265 if (brightness > 45) brightness = 45; for (int i=0; i<Num_of_LEDs; i++) { LED_Mod[i][0] = LED_Data[i][0]; for (int j=1; j<4; j++) { float angle = 90-brightness; // in degrees angle = angle*PI / 180; // in rad LED_Mod[i][j] = (LED_Data[i][j])/(tan(angle)); } } } uint16_t pwmData[(24*Num_of_LEDs)+50]; void WS2812_display() { uint32_t indx=0; uint32_t color; for (int i= 0; i<Num_of_LEDs; i++) { color = ((LED_Mod[i][1]<<16) | (LED_Mod[i][2]<<8) | (LED_Mod[i][3])); for (int i=23; i>=0; i--) { if (color&(1<<i)) { pwmData[indx] = 60; // 2/3 of 90 } else pwmData[indx] = 30; // 1/3 of 90 indx++; } } for (int i=0; i<50; i++) { pwmData[indx] = 0; indx++; } DMA2_Stream1->M0AR=(uint32_t)(&pwmData); DMA2_Stream1->NDTR=indx; DMA2->LIFCR|=DMA_LIFCR_CFEIF1|DMA_LIFCR_CDMEIF1|DMA_LIFCR_CTEIF1|DMA_LIFCR_CHTIF1|DMA_LIFCR_CTCIF1; TIM1->CR1|=TIM_CR1_CEN; //enable timer DMA2_Stream1->CR|=DMA_SxCR_EN; while(finished==0); finished=0; } void DMA2_Stream1_IRQHandler(void) { if((DMA2->LISR&DMA_LISR_TCIF1)) { finished=1; TIM1->CR1&=~TIM_CR1_CEN; DMA2->LIFCR|=DMA_LIFCR_CTCIF1; } }
For the header file:
#ifndef WS2812_H_ #define WS2812_H_ #include "stdint.h" void WS2812_init(void); void Set_Brightness (int brightness); void Set_LED (int LEDnum, int Red, int Green, int Blue); void WS2812_display(void); #endif /* WS2812_H_ */
In the main function:
extern void SysClockConfig(void); #include "stm32f4xx.h" #include "delay.h" #include "math.h" #include <WS2812.h> #include "stdlib.h" int main(void) { SCB->CPACR |= ((3UL << 10*2)|(3UL << 11*2)); /*Enable floating point unit*/ SysClockConfig(); systick_init_ms(72000000); WS2812_init(); while(1) { for (int i=0;i<24;i++) { srand(millis()); Set_LED(i,random()%256,random()%256,random()%256); Set_Brightness(20); WS2812_display(); delay(random()%100); } delay(200); for (int i=24;i>=0;i--) { Set_LED(i,0,0,0); Set_Brightness(20); WS2812_display(); delay(random()%100); } delay(200); } }
7. Code:
You may download the code from here:
8. Demo:
Happy coding 🙂
1 Comment
You nailed it guys! I ran these leds on AVR and I’ll definitely run them on stm32 board. You’ve done so much things I love (lsd characters, rfid, gps etc) Hat off to you. How about to program 8×8 LED matrix for the future lesson? I had a lot of fun with them, but getting text scrolled from one matrix to the other is a bit of pain. It’d be cool to see how you code it:)
A bit off topic. I figured out that you have courses by accident. I saw a link on YT channel (by accident too) and I just wonder why there is no information about them on this cite. I bought 2 courses yet for f4 and f7 series and it’s worth every penny. I will buy more in the future for sure. Have a good day and Happy codding guys!:)
Add Comment