Neopixels are great fun to work with, as you can set each "pixel" (or say LED) to a different color to achieve various animations and effects. Also, neopixels only require single data line from a microcontroller (to control them all !).
There are mainly two reasons why I created this driver :
- I have already worked with neopixels on Arduino (8-bit) and NodeMCU (esp8266), using the already available libraries for their respective runtimes. However, there is not a library for Ada on STM32, hence I felt a need for creating library (driver) to control neopixels.
- Secondly, the asynchronous protocol used by WS2812b is a bit strict about timing requirements and requires some form of BitBanging, because no such peripheral exists on any microcontroller which can directly drive neopixels. Hence, it would be quite an experience creating this driver.
As seen from images #1 and #2, the bits of ws2812b are different than the normal HIGH (1) and LOW (0) bits :
- A 0 bit is 1/3 of time voltage HIGH and 2/3 of the time voltage LOW (effectively a PWM signal with 33% duty cycle).
- A 1 bit is 2/3 of time voltage HIGH and 1/3 of time voltage LOW (a PWM signal with 66% duty cycle ).
- both 1 and 0 bits are approximately 1.25 microseconds long. (PWM frequency of 800 KHz = 800 Kbit/s).
- Also, there is a reset code, where voltage stays low for 50 microseconds. A reset code should be sent before and after sending the data. So, this reset code tells the neopixels, when to start reading the data and when to stop.
- As per image #3, each pixel requires 24-bits of data (8 bits each for R, G, B), with sequence of GRB and Most Significant Bit (MSB) first.
Hence, if you have 5 such pixels, then you would need to transfer data in this manner :
Reset > 24 bits of pixel 1 > 24 bits for pixel 2 >... > 24 bits for pixel 5 > Reset
Libraries for controlling neopixels already exist for different microcontrollers and runtimes, utilizing various methods to achieve bitbanging :
- GPIO + Delay : Tero's Arduino blog (This one uses AVR-Ada)
- GPIO + Inline Assembly : Adafruit Neopixel Arduino library and Josh.com
- Timer + PWM + DMA : The VFD Collective and stm32f4-discovery.net
- SPI : Adafruit Circuitpython Neopixel SPI module and Zephyr ws2812b module
- Inverted UART : NodeMCU ws2812b module and mikrokontrolery
- RP2040 PIO : Adafruit
- FPGA : fpga-neopixel and Vivonomicon
SPI seems to be easiest method (at least for me) to control neopixels. Also, the SPI code can be ported to other microcontrollers easily compared to other methods.
So, as we know, there is no peripheral on any microcontroller, which can transmit the bit stream as per protocol used by ws2812b, upon moving a byte into Data Register of respective peripheral.
SPI also can't generate bits of ws2812b directly. However, we can visualize the bits of ws2812b protocol as bytes of the SPI (image #4). The good thing about SPI is that on data-line, there are no additional Start/Stop/Parity bits like UART or Address/ACK/NACK bits like i2c. SPI simply transfers databits only, which makes it an ideal choice for such bitbanging.
The only care we need to take now is that calculating the SPI Baudrate. Since, we are now sending a "Neopixel Bit" as a "SPI Byte", we need to multiply the original baudrate of neopixels by 8.
SPI Baudrate = 8 * Neopixel Baudrate
= 8 * 800 Kbit/s
= 6400 Kbit/s
= 6.4 Mbit/s (approximately)
SPI can easily handle such baudrate in Mbit/s.
Code- Package Addressable_LEDs defines an abstract tagged private record LED_Strip and its primitive operations like Get_Color and Set_Color to manipulate/read buffer containing colors for each pixel. Also, a Show abstract procedure, which will be implemented in further record extensions of LED_Strip type. (note that implementation of this package is heavily inspired from Neopixel package provided with Ada Drivers Library.) The LED_Strip must be extended for given type of LED (like neopixel, dotstarts, etc.) with required protocol implemetation.
type LED_Strip (Mode : LED_Mode; Count : Positive; Buf_Last : Positive)
is abstract tagged
record
Buffer : aliased UInt8_Array (0 .. Buf_Last);
end record;
- Child package Addressable_LEDs.neopixel_spi extends the LED_Strip type as a private record type LED_Strip_ws2812b_SPI with few more elements and function Create to create and return an instance of this type. Also, there are declaration of "SPI bytes" for "Neopixel bits" as per image #4
This package provides two additional private procedures Generate_BitStream and Flash_BitStream.Generate_BitStreamgenerates "SPI Bytes" for each "Neopixel Bit" of colors stored in Bufferarray and stores them in BitStream array. Flash_BitStream procedure simply transmits these bytes (via SPI) stored in BitStream array with Reset bytes at beginning and end. When Show procedure is called by application (main file), then Show first calls Generate_BitStream and then calls Flash_BitStream.
Bit_0 : constant UInt8 := 2#11000000#; -- 192 in Decimal
Bit_1 : constant UInt8 := 2#11111000#; -- 248 in Decimal
Bit_Reset : constant UInt8 := 2#00000000#;
type LED_Strip_ws2812b_SPI(Mode : LED_Mode; Count : Positive; Buf_Last : Positive; Bit_Last : Positive; Reset_Last : Positive; Port : not null Any_SPI_Port)
is new LED_Strip(Mode => Mode, Count => Count, Buf_Last => Buf_Last) with
record
BitStream : aliased SPI_Data_8b (0 .. Bit_Last);
ResetStream : aliased SPI_Data_8b (0 .. Reset_Last);
end record;
- Package SPI_config is part of the application and defines SPI_port to be used for transmitting the data to neopixels. It also has procedure Initialize_NeoPixel to initialize SPI with required Baudrate. Since, we need to only transmit using MOSI pin (PB15) of SPI_2, we will not be initializing SCK and MISO pins of SPI_2 port.
STM32F407 has 3 SPI Ports : SPI_1, SPI_2 and SPI_3. However, on STM32F4 Discovery board, SPI_1 is connected to onboard accelerometer, hence I am using SPI_2 in this project for driving neopixels, but you can also use SPI_3.
Also, we cannot set SPI Baudrate on STM32F407 to the value of our choice (6.4 Mbit/s), but it can be only set to one of the divisions (by 2) of clock, known as "Baudrate Prescaler", and the nearest Baudrate I was able to achieve is 5.25 Mbit/s (at prescaler 8 of APB1, which runs at 42 MHz).
One more thing, although the datasheet of ws2812b says that a reset code should be 50 microseconds long, but the experiments have shown that 6-8 microseconds is enough. Hence, I am sending 9 SPI reset bytes (as per the initialization in below main file), so that 9 * 1.25 = 11.25 > 8 microseconds.
- Package LED_magic is a helper library that contains procedures to manipulate Buffer (Fiill/Rotate/Mirror/Magic_Copy) and procedures to generates Color_Palette for different effects (Rainbow/Monochromatic Gradient), to make things easy.
- And there are multiple main files for demo of different effects like fill_and_mirror.adb, monochrome_scroll.adb, rainbow2_swirl.adb, cylon_eye.adb, etc. you can upload any of the one main file to STM32 for demo.
-- fill_and_scroll.adb (one of the many main files)
with Addressable_LEDs; use Addressable_LEDs;
with Addressable_LEDs.neopixel_spi; use Addressable_LEDs.neopixel_spi;
with HAL; use HAL;
with HAL.SPI; --use HAL.SPI;
with SPI_config; use SPI_config;
with LED_magic; use LED_magic;
with Ada.Real_Time; use Ada.Real_Time;
procedure fill_and_scroll is
Strip_count : constant Positive := 5;
Strip_mode : constant LED_Mode := GRB;
Strip_1 : aliased LED_Strip_ws2812b_SPI := Create(Strip_mode, Strip_count, 8, Npxl_SPI'Access);
begin
Initialize_NeoPixel;
Set_Color(Strip_1, 0, (0, 125, 0, 0) );
Set_Color(Strip_1, 1, (100, 100, 0, 0) );
Set_Color(Strip_1, 2, (125, 0, 0, 0) );
Set_Color(Strip_1, 3, (100, 0, 100, 0) );
Set_Color(Strip_1, 4, (0, 0, 125, 0) );
loop
Strip_1.Show;
Rotate_Buffer(Strip_1, 1);
delay until Clock + Milliseconds(250);
end loop;
end fill_and_scroll;
ConnectionsSTM32F407 Discovery <-> Neopixel LED Strip
PB15 <-> Data In of First Pixel
For power supply, refer Adafruit's guide here.How to compile and upload this code to STM32 ?
- Make sure you have installed native gnat compiler for your machine as well as gnat cross compiler for arm-elf. Can be downloaded from Adacore website here.
- Clone Ada drivers Library from github repository.
- extract the attached zip file within following folder of Ada drivers library.
~\Ada_Drivers_Library-master\examples\STM32F4_DISCO\neopixel_ws2812b_SPI\
So, that neopixel_ws2812b_SPI folder has following files and folders (image #5) :
- Open the GNAT Studio > Open Project > browse to neopixel_ws2812b_stm32_spi.gpr and open it.
- Right click on Flash to Board button and click on any one the main files you wish to upload (image #6).
Comments