cancel
Showing results for 
Search instead for 
Did you mean: 

STM32103 bluepill bare-metal WS2812b neopixel driver no libraries

mytechnotalent
Associate II

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.

15 REPLIES 15
gbm
Lead III

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.

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.

gbm
Lead III

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.

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.

gbm
Lead III

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.

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.

Not sure your needs align with everyone elses, to that end you'll need to do a lot of this yourself. Others use the SPL and HAL to speed their work, you can want something more basic if you want, perhaps get it working, and then decant the detail down to the minimum number of register interactions.

You can use TIM+DMA+GPIO into GPIOx->BSRR to drive pattern buffers out a specific, or multiple, pins within a GPIO bank. You get to construct the pattern buffer from the RGBW type driving signals you want coming out the pin(s). The frequency of the TIM determines the rate at which pins might change state. The depth of the buffer determines how many pin transitions are managed, and you can keep constructing the buffer at the DMA HT/TC interrupts in a continuous/circular mode. The depth impacts the interrupt loading, want it deep enough you have time to spare for other things.

You could perhaps use TIM+DMA into TIMx->CCRx to modulate PWM signals on Pins associated with that timer and channel. Again you'd need to construct a table of data controlling how the signal modulates, at 800 KHz or whatever the expectations are.

The STM32F1 can run at 72 MHz in "baremetal" but you have to bring up the PLL, just takes more register work.

Tips, Buy me a coffee, or three.. PayPal Venmo
Up vote any posts that you find helpful, it shows what's working..
gbm
Lead III

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.

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;
}