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


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

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 (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));


  • 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++)
		color = ((LED_Mod[i][1]<<16) | (LED_Mod[i][2]<<8) | (LED_Mod[i][3]));
		color = ((LED_Data[i][1]<<16) | (LED_Data[i][2]<<8) | (LED_Data[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



	for (int i=0; i<50; i++)
		pwmData[indx] = 0;

	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)
  • 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)


  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++)
		  HAL_Delay (50);

	  for (int i=45; i>=0; i--)
		  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


Below is the output of the above program. To see the entire process, check the the video also

Check out the Video Below


You can help with the development by DONATING
To download the code, click DOWNLOAD button and view the Ad. The project will download after the Ad is finished.

26 Comments. Leave new

  • Can anyone help me to create the code with an 8MHz system clock?

  • I got it working after a long period of searching …
    The problem was, that traffic starts approx. 3.24 us after signal changed on bus from high to low. I added one more
    for (int i=0; i<50; i++)
    pwmData[indx] = 0;
    before the iteration loop, and it works fine!!!

  • Hey, good example.
    Is it an option to make a tutorial of “Interface WS2812 with STM32” for the STM32duino core? I’m having trouble specifying what I need from your code and what CubeMX generates?
    That would be very helpful

    • sorry but no. I don’t use or prefer using STM32dunio. Anyway if you are using the arduino ide, why not use the neopixel libraries?

      • The libraries often have poor support for STM32 or only have the F0 and F1 series up their sleeves.
        I would only be interested in the HAL commands that are required for the setup of DMA and TIM.

        • Then use the STM HAL. Why you intend to use Arduino anyway ? Sooner or later you have to switch as you won’t find support for more complicated things in that IDE.

  • Hahaha I am a junior student from a certain college in China. I learned a lot from reading your article. Thank you.

  • Antoine Bureau
    August 11, 2023 8:03 PM

    Hi for people that have some issue with ram, you can save a lot by using an uint8 array on the DMA (pwmData)

    For that you need to change the size of data width in the DMA IOC menu with Byte (keep HalfWord to the peripheral side

    change the pwmData array by an uint8_t.

    I also change the declaration of ledData and ledMod from [4] to [3], we don’t need the index in this array.

    An other little modification is to remove the “+50” in the pwmData and change it with a +1 and made a delay when you finish the display. (becarefull to change the second loop in ws2812_send function)

    With these modifications, I’m lowering the ram usage from 5k7 to 3k7

  • Trying to get this working on an existing board with STM32F103. Must use PA9 pin to drive data line, and the only unused timer I have for this is TIM8, so Channel 4 should be ok. But worse, I am using an older version of the low level library, and cannot update this. So no HAL. How do I go about simulating the pulse end callback? I’m sure it is a simple as enabling one of the interrupts and putting that code into the interrupt handler, but looking at the HAL code, there are dozens of places that call this callback, and it is not at all clear which is the right one (another reason I usually avoid HAL).

  • The fun challenge for me today is to control 100 WS2812 LEDs in a set of XMas lights, from an STM32F030F4. 4K RAM.
    First draft hard faults as the stack descends out of RAM. LOL!
    Moving all the variables to global and the RAM overflows by well over 2.4K!
    I tried reducing all the datatypes as far as I could. Like storing the LEDs as 24bit packed ints in an 8bit array. MAX_LED*3 in size. That saved 100 bytes LOL
    Changing the DMA array to be 8 bit (as my PWM value fits in there) even if that would work, still leaves me high and dry for around 400 bytes.

    The next plan is to write the LED colors directly into the DMA array and dispose of the LED array entirely.

    I can also move some stuff back onto the stack, if I can without running into the top of the heap.

  • I’m confused.
    The pwmData array is of type uint16_t[] of size MAX_LED*24 bits + 50.

    uint16_t pwmData[(24*MAX_LED)+50];

    You track your uint16_t pointer location with “indx”.
    However, you then cast this to a uint32_t* when you transfer it with DMA and do NOT divide indx by 2.

    	HAL_TIM_PWM_Start_DMA(&htim1, TIM_CHANNEL_1, (uint32_t *)pwmData, indx);

    It seems you have N 16 bit values, but you are sending N 32bit values, which is twice the amount of data you should be. I expect you might just be getting lucky that the heap above your array is zero’d.
    I expect this will also work:
    HAL_TIM_PWM_Start_DMA(&htim1, TIM_CHANNEL_1, (uint32_t *)pwmData, indx/2);

  • For all those, who have trouble with getting HAL_TIM_PWM_PulseFinishCallback() to work – You have to Enable the interrupt from corresponding DMA channel in NVIC settings category in Cube and set its priority. Otherwise, the interrupt is not fired and the function never gets called.

  • Tested on STM32F411CEV6 with a single led, works beautifully 🙂

    I’ve replaced the maximum brightness value (45) with this:

    #define MAX_BRIGTHNESS 15
    // like
    if (brightness > 45)  brightness = MAX_BRIGTHNESS;
    // in while loop
    for (int i = 0; i < MAX_BRIGTHNESS; i++) {...}
    for (int i = MAX_BRIGTHNESS; i >= 0; i--) {..}

    Using the value in the two for cycles affects their duration, if you want to keep the original, use only the first “override”.

    Thanks for this guide!!

    • did you have to download any files to make this code work. want to try it out. or just use his code directly

  • For everyone that doesn’t get this working; dataSentFlag should be a volatile int, not an int. Hope it helps!

  • Le tutoriel est complet et fonctionne sur mes Leds RGB, j’ai juste dû adapter le format de division pour obtenir 800kHz. Parce que j’avais 48MHz pour le clock. Merci pour la vidéo et les explications sur ce site 😉

  • Small error:

    • The ARR of 90 will bring down this timer frequency to 72 MHz/80 = 800 KHz
  • Hello. Please clarify one point. After generating the project in CubeMx, the MX_DMA_Init function looks like this:

    static void MX_DMA_Init (void)
      / * DMA controller clock enable * /
      / * DMA interrupt init * /
      / * DMA1_Stream5_IRQn interrupt configuration * /
      HAL_NVIC_SetPriority (DMA1_Stream5_IRQn, 0, 0);
      HAL_NVIC_EnableIRQ (DMA1_Stream5_IRQn);

    Is this correct DMA initialization? It is doubtful that the settings that were made in CubeMx for DMA are not displayed in the initialization function.

  • onur necati perinoğlu
    October 7, 2021 6:59 PM

    not working i tried on l051 datasent while waiting every time

  • Good example. Have tried on F401 with ARR=105. But i don’t get HAL_TIM_PWM_PulseFinishedCallback interrupt. I will try to figure out.


Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.


Adblocker detected! Please consider reading this notice.

We've detected that you are using AdBlock Plus or some other adblocking software which is preventing the page from fully loading.

We don't have any banner, Flash, animation, obnoxious sound, or popup ad. We do not implement these annoying types of ads!

We need money to operate the site, and almost all of it comes from our online advertising.

Please add to your ad blocking whitelist or disable your adblocking software.