Having researched for a way to control NeoPixels with a Arduino Portenta I was surprised how much information there was available yet nobody had yet created a working library. I spent the past few days getting this working myself so I wanted to provide what I'd create so that I might save some others the effort.
Another blog was written on this:
(+) Porting NeoPixels to the Arduino Portenta - element14 Community
However after referencing the possible usage of SPI, this author write a driver using blocking code (Delays). This is unfortunately not an option for me so I chose to try and accomplish with SPI what evidently didn't work for others.
In order for this to work, I also gained a lot of information from Josh.com
NeoPixels Revealed: How to (not need to) generate precisely timed signals | josh.com
This blog was very helpful for showing how the protocol worked and what tolerances it would allow.
The SPI driver for the neopixel operates by creating each bit in the neopixel protocol using a byte in the SPI protocol. By changing the value in the SPI message, you can essentially create variable length pulses that mimic the neopixel protocol. SPI's ability to "transfer" whole messages makes this an ideal tool. This library thus crafts a message where each byte represents a bit of neopixel data and then schedules the transfer over the SPI bus. The end result is a very successful neopixel driver.
I completed all my testing using RGB neopixels but I attempted to implement the library to support the other's I've heard of. My testing wasn't exhaustive but it should get you very close to a working solution.
This image shows the SPI creating a few "1" bits follows by a few "0"s. The SPI interpreter on the Saleae reveals how this message is input to the SPI hardware.
/* NeoPixelSPI.h - Library for controlling neopixels with SPI on the Arduino Portenta H7. Created by Mark Schmidt, March 5, 2024. Released into the public domain. This class utilizes the SPI module to control NeoPixels. This is accomplished by having SPI output bytes which coorespond to bits in the neopixel protocol. The value of the SPI byte controls the length of the neopixel bit. See https://wp.josh.com/2014/05/13/ws2812-neopixels-are-not-so-finicky-once-you-get-to-know-them/ for more information as it was very helpful in the creation of the library. I am not associated with josh.com and merely benefitted from his work. */ #ifndef NeoPixelSPI_h #define NeoPixelSPI_h #include "mbed.h" #include "Arduino.h" enum LED_TYPES_E { LED_TYPES_W, //Select for any single color neopixel LED_TYPES_RGB, LED_TYPES_RGBW, }; /// struct LED_CONFIG_S { LED_TYPES_E type = LED_TYPES_RGB; byte red = 0; byte green = 0; byte blue = 0; byte white = 0; }; class NeoPixelSPI { public: /// Constructor: Creates an instance of neoPixelSPI /// Param: spi_device - should be an SPI device which hasn't been initialized. Only /// the MOSI line is required, the other two lines will be configured by setup() /// but can be turned back off after calling setup(). /// Param: numberNeoPixels - this specifies the max (if varying) number of /// numberNeoPixels in the chain. This will allocate a sufficient output array /// as if every neopixel is RGBW. This could be wasteful if you care a lot about ram. NeoPixelSPI(mbed::SPI * spi_device, int numberNeoPixels); /// Constructor: Creates an instance of neoPixelSPI /// Param: spi - should be an SPI device which hasn't been initialized. Only the /// MOSI line is required, the other two lines will be configured by setup() /// but can be turned back off after calling setup(). /// Param: numberNeoPixels - this specifies the max (if varying) number of /// numberNeoPixels in the chain. This will allocate a sufficient output array /// as if every neopixel is RGBW. This could be wasteful if you care a lot about ram. /// Param: countingColors - set this to true if you are providing the number of led colors /// in the full chain (Add 4 for each RGBW, 3 for RGB, 1 for W). This is more memory efficient /// but otherwise just extra hassle. If set false, this operates the same as the other constructor. NeoPixelSPI(mbed::SPI * spi_device, int numberNeoPixels, bool countingColors); /// Setup: Enables and configures the SPI driver. void setup(); /// Transfer: Set the value of all the neopixels in the chain. /// Param: ledConfigurations should be an array of LED Configurations void transfer(LED_CONFIG_S * ledConfigs, int numLEDs); private: /// Convert a byte (one LED color config) into the SPI encoding. void byteToSPI(byte * byteString, byte value); /// Takes in the LED configurations and writes over the outputString /// returns the length of the SPI message. int buildLEDMsg(LED_CONFIG_S * ledConfigs, int numLEDs); /// The SPI Device which this class should control. mbed::SPI * spi; /// The output string array byte * outputString; }; #endif
#include "neoPixelSPI.h" //Number of bits to send per neopixel color. #define BYTE_TRANSFER_LENGTH 8 //Default length per neopixel unless otherwise specified. #define RGBW_TRANSFER_LENGTH 4 //Probably can be skipped but is added to provide a "latch" prior to starting the message. #define HEADER_BITS 30 //This just ensures we meet the "latch" timing #define FOOTER_BITS 30 //SPI Bus frequency: 5.7MHz (175 ns bits) It doesn't actually achieve this specific time but we can dream #define FREQUENCY 5700000 NeoPixelSPI::NeoPixelSPI(mbed::SPI * spi_device, int numberNeoPixels) { //Store the SPI device we should use. spi = spi_device; //Create our output string which will be fed to the SPI module. This needs to be the max length. outputString = (byte*)malloc((numberNeoPixels * RGBW_TRANSFER_LENGTH * BYTE_TRANSFER_LENGTH) + HEADER_BITS + FOOTER_BITS); } NeoPixelSPI::NeoPixelSPI(mbed::SPI * spi_device, int numberNeoPixels, bool countingColors) { //Store the SPI device we should use. spi = spi_device; //Create our output string which will be fed to the SPI module. This needs to be the max length. if(countingColors) { //The user is brave and wants to save some bytes. outputString = (byte*)malloc((numberNeoPixels * BYTE_TRANSFER_LENGTH) + HEADER_BITS + FOOTER_BITS); } else { //Why would you use this constructor then? We can't trust this user, assume 4 color neopixels. outputString = (byte*)malloc((numberNeoPixels * RGBW_TRANSFER_LENGTH * BYTE_TRANSFER_LENGTH) + HEADER_BITS + FOOTER_BITS); } } void NeoPixelSPI::setup() { spi->frequency(FREQUENCY); spi->format(8,0);//8 bits data (meaning each neopixel bit will take 8*frequency to send). } void NeoPixelSPI::transfer(LED_CONFIG_S * ledConfigs, int numLEDs) { int length = buildLEDMsg(ledConfigs, numLEDs); spi->transfer((byte*) outputString, length, (byte*)0, 0, 0); } void NeoPixelSPI::byteToSPI(byte * byteString, byte value) { for(int i = 0; i < BYTE_TRANSFER_LENGTH; i++) { if(value % 2 == 1) { byteString[BYTE_TRANSFER_LENGTH - i - 1] = 0xF0; } else { byteString[BYTE_TRANSFER_LENGTH - i - 1] = 0xC0; } value = value >> 1; } } int NeoPixelSPI::buildLEDMsg(LED_CONFIG_S * ledConfigs, int numLEDs) { byte header [HEADER_BITS] = {0}; byte footer [FOOTER_BITS] = {0}; //Keeps track where we are in our message int writeIndex = 0; //Write Header - just gives us a clean off period prior to sending data for(int i = 0; i < HEADER_BITS; i++) { outputString[writeIndex] = header[i]; writeIndex++; } //Add in the data for all the LEDs for(int i = 0; i < numLEDs; i++) { if(ledConfigs[i].type == LED_TYPES_RGB || ledConfigs[i].type == LED_TYPES_RGBW) { /// Red - Green - Blue byteToSPI(&outputString[writeIndex], ledConfigs[i].red); writeIndex += 8; byteToSPI(&outputString[writeIndex], ledConfigs[i].green); writeIndex += 8; byteToSPI(&outputString[writeIndex], ledConfigs[i].blue); writeIndex += 8; } if(ledConfigs[i].type == LED_TYPES_W || ledConfigs[i].type == LED_TYPES_RGBW) { /// White byteToSPI(&outputString[writeIndex], ledConfigs[i].white); writeIndex += 8; } } //Write Footer for(int i = 0; i < FOOTER_BITS; i++) { outputString[writeIndex] = footer[i]; writeIndex++; } return writeIndex; }
Here is some example code utilizing the library
/* Arduino Portenta neopixelSPI example */ #include "mbed.h" #include "Arduino.h" #include "neoPixelSPI.h" using namespace mbed; /********************************************************************* * Constants *********************************************************************/ const unsigned long LED_FLIP_PERIOD = 500; //milliseconds // SPI-LED Driver Definitions #define NUMBER_LEDS 10 /********************************************************************* * Global Variables *********************************************************************/ // When was the lsat time the LED was flipped. unsigned long previousLEDSwitch = 0; SPI spi(PC_3, PC_2, PI_1); NeoPixelSPI neoPixelSpi(&spi, NUMBER_LEDS); LED_CONFIG_S leds [NUMBER_LEDS]; /********************************************************************* * Application *********************************************************************/ void setup() { //Configure debug LEDs pinMode(LEDR, OUTPUT); pinMode(LEDG, OUTPUT); pinMode(LEDB, OUTPUT); digitalWrite(LEDR, LOW); digitalWrite(LEDG, HIGH); digitalWrite(LEDB, HIGH); neoPixelSpi.setup(); //Prepopulate LED Data leds[0].red = 255; leds[0].green = 255; leds[0].blue = 255; leds[1].red = 255; leds[1].green = 0; leds[1].blue = 0; leds[2].red = 0; leds[2].green = 255; leds[2].blue = 0; leds[3].red = 0; leds[3].green = 0; leds[3].blue = 255; leds[4].red = 255; leds[4].green = 255; leds[4].blue = 0; leds[5].red = 255; leds[5].green = 0; leds[5].blue = 255; leds[6].red = 0; leds[6].green = 255; leds[6].blue = 255; leds[7].red = 127; leds[7].green = 127; leds[7].blue = 127; leds[8].red = 255; leds[8].green = 255; leds[8].blue = 255; leds[9].red = 255; leds[9].green = 255; leds[9].blue = 255; //Turn off Red LED to indicate Setup Complete. digitalWrite(LEDR, HIGH); } void loop() { // What is the current time? unsigned long currentTimeMS = millis(); /////////////////////LED UPDATE////////////////////// if(currentTimeMS - previousLEDSwitch > LED_FLIP_PERIOD) { neoPixelSpi.transfer(leds, NUMBER_LEDS); digitalWrite(LEDB, !digitalRead(LEDB)); previousLEDSwitch = currentTimeMS; } }
I hope this works for you! If you notice any issues, feel free to comment.