Skip to main content
Visitor II
November 28, 2022
Question

STM32103 bluepill bare-metal WS2812b neopixel driver no libraries

  • November 28, 2022
  • 6 replies
  • 7277 views

I have spent a few weeks reviewing HAL related and LL related neopixel libs however I would love if there was someone who had a working version of literally just the ability to set up a neopixel on any port and simply light it or light a few neopixels in an array.

I have found several with HALs but it really does not give a good understanding of how this is done from scratch.

To be clear I am not looking for a HAL solution. I am looking for a bare-metal no library solution.

Thank you in advance.

    This topic has been closed for replies.

    6 replies

    Graduate
    November 28, 2022

    Without any external components, STM32F103 may drive WS2812 either via any SPI MOSI line, or (with much bigger overhead) via any timer output, both methods using DMA, unless you want to play Arduino style and use 100% of procesing power for stupid task of toggling the port output. I never did this with HAL, so no code to share. My only HAL-driven WS2812 uses STM32G0 series UART and DMA. This may be done with any STM32 series having the "new style" UART providing TX line negation (and preferrably also 7-bit data frames but one could live without it), like F0, G0, G4, L4 and others.

    Visitor II
    November 28, 2022

    Appreciate the response but yes this is not what I am looking for. I am looking for an example without HAL as there are tons of HAL examples. Thank you anyway.

    Graduate
    November 28, 2022

    Then try this (code for F0/L4 series):

    static uint16_t enc_wsdata[NFRAMES];
     
    	static const uint16_t encode[] = {
    		04444, 04446, 04464, 04466, 04644, 04646, 04664, 04666,
    		06444, 06446, 06464, 06466, 06644, 06646, 06664, 06666
    	};	// bit-to triple encoding table - 4->12 bits
    	
    	uint8_t *wsptr = (uint8_t *)ptr;
    	uint16_t *ep = enc_wsdata + WS_RST_FRM;	// skip reset frames
    	
    	for (uint32_t i = 0; i < NBYTES; i +=2)
    	{
    		uint32_t ev;
    		// encode 2 bytes as 3 16-bit words
    		ev = encode[*wsptr >> 4] << 12;
    		ev |= encode[*wsptr ++ & 0xf];	// 24 bits in ev
    		*ep ++ = ev >> 8;	// store 16 bits, 8 bits left
    		ev = ev << 12 | encode[*wsptr >> 4];	// 20 b total
    		*ep ++ = ev >> 4;	// 4 bits left
    		*ep ++ = ev << 12 | encode[*wsptr ++ & 0xf];	// 16 bits
    	}
    	
    	if ((DMA1->ISR & DMA_ISR_TCIF3) || DMA1_Channel3->CCR == 0)
    	{
    		// setup WS2812 data transfer
    		DMA1->IFCR = DMA_IFCR_CGIF3;
    		DMA1_Channel3->CCR = 0;	// disable
    		DMA1_Channel3->CPAR = (uint32_t)&SPI1->DR;
    		DMA1_Channel3->CMAR = (uint32_t)enc_wsdata;
    		DMA1_Channel3->CNDTR = NFRAMES;
    		// medium priority, increment memory adress, mem->periph, enable
    		DMA1_Channel3->CCR = DMA_CCR_PL_0 | DMA_CCR_MSIZE16 | DMA_CCR_PSIZE16
    			| DMA_CCR_MINC | DMA_CCR_DIR | DMA_CCR_EN;
    	}

    SPI must be initialized to 16-bit frames and SPI clock frequency should ideally be 2.4 MHz +-10%, which is a little tricky cause the main clock frequency should be 20 or 40 MHz. The routine should be called from timer interrupt, guaranteing thet there will be at least 50 us (may be much more as well) pause between consecutive data runs.

    Visitor II
    November 28, 2022

    I am confused on this code. What GPIO port do you have your neopixel array hooked to? In addition I am on a bluepill STM32F103C6T8 which at baremetal works at 8MHz.

    Reviewing this code it is not clear how you would light a neopixel or neopixels in a given array from a particular pin.

    I am also confused why you suggest SPI as this should work right from a GPIO pin using timers I would assume?

    I appreciate the code however I am struggling to convert this or understand this to get an actual full baremetal STM32F103 bluepull example where a neopixel illuminates.

    Graduate
    November 28, 2022

    The 8-bit values are in the rgb structure array pointed to by ptr, assigned to wsptr variable. The code was using SPI1 and WS2812 string was controlled by MOSI output of SPI1. With SPI, you need to prepare 3 bits for a single WS2812 bit to be sent (18 bytes of data for 2 pixels, 9 bytes per pixel). With timer output, you need a 16-bit value for each bit = 48 bytes per pixel. F103 has three SPI which could drive 3 strings independently. Using UART (not in F103) you need only 8 bytes per pixel.

    Visitor II
    November 28, 2022

    I am sorry I am not doing a good enough job of explaining what I am looking for. I see what you are getting at but I did not want to tie up a SPI device for this.

    Here is what inspired the question to begin with. They are working with DMA and TIM and not a SPI as they are working off a single GPIO pin.

    https://controllerstech.com/interface-ws2812-with-stm32/

    This example is more what I am looking for bare-metal without any STM GUI config.

    Graduate
    November 28, 2022

    The SPI uses a single MOSI pin for WS2812 output. There is no GUI config needed, just 3 lines of code to setup the SPI. When it comes to computing overhead and memory footprint, UART solution is the best (but impossible on F103), SPI close to it and timer - far, far worse.

    Well, if you prefer timer, here it is:

    /*
    	WS2812 control using Timer PWM output & DMA
    	gbm 04'2016
    	
    	Configure timer prescaler to get 2.4 MHz timer clock, period 3
    	byte order: GRB for WS2812, RGB for ?
    */
    #include "board.h"
     
    #define WSDMA	DMA1
    #define WSDMACH	DMA1_Channel1
    #define	DMA_IFCR_CGIF_WS	DMA_IFCR_CGIF1
    #define WSTIM_CCR	TIM17->CCR1
     
    void ws2812_send(uint32_t v)
    {
    	static uint16_t wsdata[25];
    	
    	for (uint8_t i = 0; i < 24; i++)
    		wsdata[i] = (v >> (23 - i) & 1) + 1;
    	WSDMACH->CCR = 0;
    	WSDMA->IFCR = DMA_IFCR_CGIF_WS;
    	WSDMACH->CPAR = (uint32_t)&WSTIM_CCR;
    	WSDMACH->CMAR = (uint32_t)wsdata;
    	WSDMACH->CNDTR = 25;
    	WSDMACH->CCR = DMA_CCR_MSIZE16 | DMA_CCR_PSIZE16
    			| DMA_CCR_MINC | DMA_CCR_DIR | DMA_CCR_EN;
    }

    The timer used must be programmed as described in the comment. This was written to control a single WS2812, hence 25 values - 24 duties of 1 or 2 for WS2812 bits and the last one - 0 - for reset/start.

    Visitor II
    November 28, 2022

    This is very helpful. I am going to try this on my STM32F103CT8 bluepill now. I assume the "board.h" is my #include "stm32f1xx.h" as the STMCubeIDE does not recognize "board.h".

    Working with the bluepill I do not have a TIM17 however on the datasheet I have TIM1 on the APB2 bus and TIM2, TIM3, TIM4 on the APB1 bus.

    Regarding WSDMACH->CCR = DMA_CCR_MSIZE16 | DMA_CCR_PSIZE16 I get DMA_CCR_PSIZE16' undeclared (first use in this function); did you mean 'DMA_CCR_PSIZE'?

    My question is this. Are you using SPI here? If so I have a SPI1 on the APB2 bus and a SPI2 on the APB1 bus I assume I can use. I do see where you are assigning a specific SPI so I am unsure how to configure this by reading my datasheet and reference manual.

    Here is my updated code based on your example. Just need to understand which SPI to hook up the Din pin of the WS2812b to and how to set up the SPI exactly.

    #define WSDMA	DMA1
    #define WSDMACH	DMA1_Channel1
    #define	DMA_IFCR_CGIF_WS	DMA_IFCR_CGIF1
    #define WSTIM_CCR	TIM1->CCR1
     
     
    void ws2812_send(uint32_t v)
    {
    	static uint16_t wsdata[25];
     
    	for (uint8_t i = 0; i < 24; i++)
    		wsdata[i] = (v >> (23 - i) & 1) + 1;
    	WSDMACH->CCR = 0;
    	WSDMA->IFCR = DMA_IFCR_CGIF_WS;
    	WSDMACH->CPAR = (uint32_t)&WSTIM_CCR;
    	WSDMACH->CMAR = (uint32_t)wsdata;
    	WSDMACH->CNDTR = 25;
    	WSDMACH->CCR = DMA_CCR_MSIZE_0 | DMA_CCR_PSIZE
    			| DMA_CCR_MINC | DMA_CCR_DIR | DMA_CCR_EN;
    }

    Graduate
    November 29, 2022

    The first snippet was for SPI, the second is for timer PWM output - it's written in the text. Some convenient #defines from my own header files are used in the code to make it easier to read - they are quite easy to guess.

    Visitor II
    November 29, 2022
    #define WSDMA	DMA1
    #define WSDMACH	DMA1_Channel1
    #define	DMA_IFCR_CGIF_WS	DMA_IFCR_CGIF1
    #define WSTIM_CCR	TIM1->CCR1

    DMA_IFCR_CGIF1 I assume this is SPI1?

    TIM1->CCR1 I assume this is PWM?

    Visitor II
    December 22, 2023

    I know this is an old thread, but I'm struggling to get anything to work on a STM32G030J6M6, I want to drive a number of smd ws2812. Is there maybe any code you could share if you got it working? Or maybe could you share the many HAL examples that you mentioned?

     

    Thanks!!

    Graduate
    December 23, 2023

    This is my code for G030 using UART with 7-bit frames, MSB first, used in my Christmas star.

     

    int main(void)
    {
    	RCC->PLLCFGR = RCC_PLLCFGR_PLLMV(1) | RCC_PLLCFGR_PLLNV(9) | RCC_PLLCFGR_PLLRV(3)
    	| RCC_PLLCFGR_PLLREN | RCC_PLLCFGR_PLLSRC_HSI;	// 
    	RCC->CR |= RCC_CR_PLLON;	// turn PLL on
    	while (!(RCC->CR & RCC_CR_PLLRDY));	// wait for PLL lock
    	FLASH->ACR |= FLASH_ACR_PRFTEN | 1;	// enable prefetch, 1 wait state
    	RCC->CFGR |= RCC_CFGR_SW_PLLRCLK;	// switch to PLL clock
    	
    	RCC->IOPENR |= RCC_IOPENR_GPIOAEN | RCC_IOPENR_GPIOBEN;
    	RCC->AHBENR |= RCC_AHBENR_DMA1EN;
    	RCC->APBENR2 = RCC_APBENR2_USART1EN;
    	
    	// the UART may also work with 8-bit frames, this requires shifting the data by one to the left (verified)
    	WS_UART->BRR = (HCLK_FREQ + WSBAUD / 2) / WSBAUD;
    	WS_UART->CR2 = USART_CR2_TXINV | USART_CR2_MSBFIRST | USART_CR2_DATAINV | USART_CR2_SWAP;
    	WS_UART->CR3 = USART_CR3_DMAT;
    	WS_UART->CR1 = USART_CR1_M1 | USART_CR1_TE | USART_CR1_UE;
    	
    	WS_DMACH->CPAR = (uint32_t)&WS_UART->TDR;
    	
    	GPIOB->MODER = BF2(7, GPIO_MODER_AF);	// USART1
    	
    	DMAMUX1_Channel0->CCR = DMAREQ_ID_USART1_TX;
    	
    	SysTick_Config(HCLK_FREQ / SYSTICK_FREQ);
    	
    	for (;;)
    		__WFI();
    }
    
    struct rgb_ {
    	uint8_t r, g, b;
    };
    
    struct rgb_ lc[NLEDS];
    static uint8_t txbuf[NLEDS * 8];
    
    void encode(uint8_t *encbuf, const struct rgb_ *img, uint16_t nleds)
    {
    	// MSB-first encoding table
    	static const uint8_t uenc[] = {
    		0000 | 022, 0001 | 022, 0010 | 022, 0011 | 022,
    		0100 | 022, 0101 | 022, 0110 | 022, 0111 | 022
    	};
    
    	for (uint16_t i = 0; i < nleds; i++)
    	{
    		uint32_t v = img[i].r | img[i].g << 8 | img[i].b << 16;
    		for (uint8_t b = 0; b < 8; b++)
    		{
    			*encbuf++ = uenc[v >> 21 & 7];
    			v <<= 3;
    		}
    	}
    }
    
    void ws_start(void)
    {
    	encode(txbuf, lc, NLEDS);
    	
    	WS_DMACH->CCR = 0;
    	WS_DMACH->CMAR = (uint32_t)txbuf;
    	WS_DMACH->CNDTR = sizeof(txbuf);
    	WS_DMACH->CCR = DMA_CCR_DIR_M2P | DMA_CCR_MINC | DMA_CCR_EN;
    }