Working with STM32 and Addressable LED part 2: Driver development

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

  • Impulse Posted June 29, 2022 11:13 am

    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

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