Interface WS2812 with STM32
In this tutorial, we will interface the WS2812 LED with STM32. The same process can be used for WS2812B also. I am going to use PWM with DMA to send the data to the LED. I will explain the steps along the way, and how they are related to the datasheet of the device.
Let’s start with Setup
CubeMX Setup
First of all Let’s see the clock setup. Notice that the APB2 Timer Clock is running at 72 MHz
Now we will enable the Timer 1 in the PWM output mode. Also note that the Prescalar is set to 0, and ARR is 72-1
- Since the Timer 1 is connected to the APB2 clock, it was initially running at 72 MHz
- Now we use prescalar of 0, that means we are diving this Frequency by 1, So the timer is still running at same 72 MHz frequency
- The ARR of 90 will bring down this timer frequency to 72 MHz/90 = 800 KHz
- Also this 90 in the ARR is the maximum duty cycle i.e 100% Duty = 90 (in the CCR)
- Now if we want to change the duty cycle, we have to calculate the percentage from 90
- For example, if we want the duty cycle of 30%, we will use the value 27, and for 70% Duty, the value will be 63
Now let’s setup the DMA
- DMA is set for TIM1 channel 1
- Memory to peripheral direction is needed, since we are sending the data to the peripheral
- Use the normal mode, since we only want the DMA to transfer the data, when we command it to do so
- I am using the data width of half word here, for no particular reason
This completes the setup process. Now let’s see some important points from the datasheet of the device
INFO from the DATASHEET
Below is the picture from the datasheet, explaining how to send a 0 and a 1 to the device
- As you can see in the picture above, in order to send a 0, the Pulse should be HIGH for 0.4 us, and then LOW for 0.85 us
- This makes up about 1/3rd of the Pulse should be HIGH
- Similarly, to send a 1, 2/3rd of the Pulse must be HIGH
- Reset code consists of the LOW Pulse for more than 50 us
Next we have the bit arrangement
- You can see that each data consists of 24 bits, G R B
- And we must send the Green bits (MSB) first
- Also the data for all the LEDs should be sent together. For eg- If you have 12 LEDs, then you need to send 12×24 = 288 bits one after other
- The driver will assign the first 24 bits to the first LED, 2nd set of 24 bits to second LED, and so on
- After sending the data for all the LEDs, we must keep the signal LOW for more than 50 us, or else, the driver will consider that this data is for the NEXT LED.
This is all the infomation we needed from the datasheet. Now Let’s go through the CODE
Some insight into the CODE
Some Definitions
#define MAX_LED 8
#define USE_BRIGHTNESS 1
uint8_t LED_Data[MAX_LED][4];
uint8_t LED_Mod[MAX_LED][4]; // for brightness
- Define the Maximum LEDs Connected in cascade
- USE_BRIGHTNESS can be set to 1, if you want to use the brightness control. Or set it to 0
- LED_Data and LED_Mode are the matrices to store the LED related data.
Storing the LED data
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;
}
#define PI 3.14159265
void Set_Brightness (int brightness) // 0-45
{
#if USE_BRIGHTNESS
if (brightness > 45) brightness = 45;
for (int i=0; i<MAX_LED; 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));
}
}
#endif
}
- LED_Data is used to store the color data for individual LED. Here First Column Represents the LED number, 2nd column for Green, then Red and Last column for blue color
- LED_Mod is also used to store LED related data, but the scaled values, as per the brightness settings
- Controlling the Brightness is fairly simple. All we need to so is divide the actual value by some number. For eg- Red 255 will be brightest, 127 will be 50% bright and 63 will be 25% bright
- I am using the Tangent function to bring some linearity in the scaling.
- The brightness values can vary from 0 to 45
Convert and send the data to DMA
uint16_t pwmData[(24*MAX_LED)+50];
void WS2812_Send (void)
{
uint32_t indx=0;
uint32_t color;
for (int i= 0; i<MAX_LED; i++)
{
#if USE_BRIGHTNESS
color = ((LED_Mod[i][1]<<16) | (LED_Mod[i][2]<<8) | (LED_Mod[i][3]));
#else
color = ((LED_Data[i][1]<<16) | (LED_Data[i][2]<<8) | (LED_Data[i][3]));
#endif
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++;
}
HAL_TIM_PWM_Start_DMA(&htim1, TIM_CHANNEL_1, (uint32_t *)pwmData, indx);
while (!datasentflag){};
datasentflag = 0;
}
- Here we first convert the individual color values to a single 24 bit data
- Now we will check each bit of this color data
- if the bit is 1, we will store 60 in the buffer.
- This 60 comes from the fact that, in order to store a 1, we need to keep the Pulse High for 2/3rd of the time for one period.
- Since the ARR value is 90 (100% Duty), 2/3rd of this will be 60.
- Also note that I am storing the MSB value to the LSB position, because as per the device requirement, we need to send the MSB first
- Similarly, if the value is 0, we will store 30 (1/3rd of 90) in that position
- After storing all the values for the LEDs, we need to also store the values, to keep the Pulse LOW for more than 50 us
- To achieve this, we can store 50 zeroes. Since our period is 1.25 us, 50 zeroes will cover more than 50 us of the LOW time
- And now we send this data to the DMA
Stopping the DMA
void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim)
{
HAL_TIM_PWM_Stop_DMA(&htim1, TIM_CHANNEL_1);
datasentflag=1;
}
- Once the data transfer has been finished, the Pulse Finish callback function will be called
- Here we will just STOP the DMA, so that another transfer doesn’t take place automatically
The main Function
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_DMA_Init();
MX_TIM1_Init();
Set_LED(0, 255, 0, 0);
Set_LED(1, 0, 255, 0);
Set_LED(2, 0, 0, 255);
Set_LED(3, 46, 89, 128);
Set_LED(4, 156, 233, 100);
Set_LED(5, 102, 0, 235);
Set_LED(6, 47, 38, 77);
Set_LED(7, 255, 200, 0);
while (1)
{
for (int i=0; i<46; i++)
{
Set_Brightness(i);
WS2812_Send();
HAL_Delay (50);
}
for (int i=45; i>=0; i--)
{
Set_Brightness(i);
WS2812_Send();
HAL_Delay (50);
}
}
}
- Here first of all I am setting the different colors to the LEDs
- Inside the while loop, I am increasing the brightness every 50 ms
- and then decreasing the brightness. You can see the Result below
- Note that after setting the LED, you must call
WS2812_Send
function, to send the data to the LED - I am calling that function inside the while loop
Result
Below is the output of the above program. To see the entire process, check the the video also