I have a project running 1400 apa102 pixels (strip) using an adafruit feather M0. It’s a fixed installation, so I’m not worried about fast POV stuff.
This is up and running using DMA SPI access at 4MHz (won’t run faster without flickering at the end of the strip).
I’m porting it to the FastLED library (so that I can use the excellent math/fading etc. Functions).
If I just define the clock and data pins, it’s slow (plain bit banging SPI I assume). The SPI port I’m using is defined as SPI1 (not 0), but there doesn’t seem to be a way to pass an SPI object to the class initializer (just pin based), and although there is a dma.h file, it’s empty. I don’t want to use SPI0, as that’s used for WiFi (on the WiFi version of this board -which I plan on switching to later).
So right now, I’m using my own routine to write to SPI1 via DMA (which is nice and fast), and FastLED routines to manipulate the led data.
Is there a straightforward way to add my DMA SPI function to the CFastLed class so that I can use colour correction, brightness dithering etc. Or a way for me to implement these in my own function?
Right now, I can only dim down to a (admittedly low) level, before the led’s go off. I don’t want to implement my own temporal dithering (when you already have it), so I’ve implemented the apa102 “global brightness” feature. Which works quite well, but feels like a bit of a hack.
Any suggestions, or is this the best way to do it at the moment?
Thanks,
Here’s the code, no code markers so the indentation is terrible - but here goes:
#include <SPI.h>
#include <Adafruit_WINC1500.h>
#include <PubSubClient.h>
#include <Adafruit_ZeroDMA.h>
#include <Adafruit_ASFcore.h>
#include “utility/dmac.h”
#include “utility/dma.h”
#include “wiring_private.h” // pinPeripheral() function
#include “Adafruit_FeatherOLED_WiFi.h”
#include “math.h”
#include “FastLED.h”
#define MAX_LEDS 1370 // Upper limit; OK to receive data for fewer - can go up to 1400 max, but 1370 covers the fence…
#define SPI_BUFFER_SIZE (4 + MAX_LEDS * 4 + ((MAX_LEDS / 2) + 7) / 8)
// Two equal-size SPI buffers are allocated; one’s being filled with new
// data as the other’s being issued via DMA.
uint8_t spiBuffer[2][SPI_BUFFER_SIZE];
uint8_t spiBufferBeingFilled = 0; // Index of currently-calculating buf
volatile bool spiReady = true; // True when SPI DMA ready for new data
uint8_t rgbBuf[MAX_LEDS * 3]; // 512 LEDs = 6144 bytes. (4608 now)
CRGB realleds = (CRGB)&rgbBuf[0];
CRGBSet leds(realleds, MAX_LEDS);
// Order of color bytes as issued to the DotStar LEDs. Current DotStars use
// BGR order (blue=first, green=second, red=last); pre-2015 DotStars use GBR
// order. THESE RELATE ONLY TO THE LED STRIP; OPC data is always RGB order.
#define DOTSTAR_BRIGHTNESS -1 //global (but per pixel) 5 bit brightness value - 111XXXXX where X is 0-31 brightness value
#define DOTSTAR_BLUEBYTE 0
#define DOTSTAR_GREENBYTE 1
#define DOTSTAR_REDBYTE 2
Adafruit_ZeroDMA myDMA; // For DMA transfers
// Called each time a DMA transfer finishes
void dma_callback(struct dma_resource* const resource) {
spiReady = true; // OK to issue next SPI DMA payload now!
}
//stuff …
// Declare second SPI peripheral ‘SPI1’:
SPIClass SPI1( // 11/12/13 classic UNO-style SPI
&sercom1, // → Sercom peripheral
12, // MISO pin
13, // SCK pin
11, // MOSI pin
SPI_PAD_0_SCK_1, // TX pad (for MOSI, SCK)
SERCOM_RX_PAD_3); // RX pad (for MISO)
// This function interpolates between two RGB input buffers, gamma- and
// color-corrects the interpolated result with 16-bit dithering and issues
// the resulting data to the GPIO port.
void magic(
uint8_t brightness, // global (per pixel) brightness value 0-31
uint8_t *rgbIn1, // First RGB input buffer being interpolated)
uint8_t w2, // interpolation factor (255 = no interpolation)
uint8_t spiBufferBeingFilled, //current spibuffer (DotStar-native order)
uint16_t numLEDs) { // Number of LEDs in buffer
uint8_t *fillBuf = &spiBuffer[spiBufferBeingFilled][0];
uint8_t *fillPtr = &spiBuffer[spiBufferBeingFilled][5]; // Skip 4-byte header + 1 byte pixel marker (pixel marker is really pixel brightness - use -1 position to fill it in)
uint8_t *prevPtr = &spiBuffer[1-spiBufferBeingFilled][5]; //previous data that was written (for interpolation)
if(numLEDs >= MAX_LEDS) numLEDs = MAX_LEDS-1; //just in case!
for(uint16_t pixelNum = 0; pixelNum < numLEDs; pixelNum++, fillPtr += 4, prevPtr +=4) {
//set global brightnes (for the whole pixel)
if(masked && !active[pixelNum])
fillPtr[DOTSTAR_BRIGHTNESS] = 0xE0; //off
else {
fillPtr[DOTSTAR_BRIGHTNESS] = brightness | 0xE0; //0-31 0 is actually off, 31 is max brightness - messes up fast PWM, but gives smooth brightness control
fillPtr[DOTSTAR_REDBYTE] = lerp8by8(prevPtr[DOTSTAR_REDBYTE],leds[pixelNum].r,w2);
fillPtr[DOTSTAR_GREENBYTE] = lerp8by8(prevPtr[DOTSTAR_GREENBYTE],leds[pixelNum].g,w2);
fillPtr[DOTSTAR_BLUEBYTE] = lerp8by8(prevPtr[DOTSTAR_BLUEBYTE],leds[pixelNum].b,w2);
}
}
primary_rgb = CRGB(fillBuf[DOTSTAR_REDBYTE+5], fillBuf[DOTSTAR_GREENBYTE+5], fillBuf[DOTSTAR_BLUEBYTE+5]); //set primary_rgb to current values for pixel 0
while(!spiReady); // Wait for prior SPI DMA transfer to complete
// Set up DMA transfer using the newly-filled buffer as source…
myDMA.setup_transfer_descriptor(
fillBuf, // Source address
(void *)(&SERCOM1->SPI.DATA.reg), // Dest address
SPI_BUFFER_SIZE, // Data count
DMA_BEAT_SIZE_BYTE, // Bytes/halfwords/words
true, // Increment source address
false); // Don’t increment dest
myDMA.start_transfer_job();
spiReady = false;
}
in setup
setup {
SPI1.begin(); // Init second SPI bus
pinPeripheral(11, PIO_SERCOM); // Enable SERCOM MOSI on this pin
pinPeripheral(13, PIO_SERCOM); // Ditto, SERCOM SCK
//SPI1.beginTransaction(SPISettings(12000000, MSBFIRST, SPI_MODE0));
//SPI1.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0));
SPI1.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0));
// Long DotStar stips may require reducing the SPI clock; if you see
// glitching, try setting to 8 MHz above. - had to reduce to 4MHz for 30m!
// Configure DMA for SERCOM1 (our ‘SPI1’ port on 11/12/13)
myDMA.configure_peripheraltrigger(SERCOM1_DMAC_ID_TX);
myDMA.configure_triggeraction(DMA_TRIGGER_ACTON_BEAT);
myDMA.allocate();
myDMA.add_descriptor();
myDMA.register_callback(dma_callback);
myDMA.enable_callback();
}
it’s a bit of a mess, I’m improving it as i go, and I don’t call “magic” directly, I call show_leds(); which calculates the interpolation factor etc, then calls magic with the correct buffers rotated in place etc.
actually here is show_leds();
void show_leds(bool direct) {
if(!display_blank) Relay_Control(true); //turn output power on (if not already on) and not blank display
uint32_t timeSinceFrameStart, t;
uint8_t w;
static uint32_t timeBetweenFrames, lastFrameTime;
//calculate interpolation weighting factor (w, 0-255), based on time since last frame (255 = no interpolation)
t = micros(); // Current time
timeSinceFrameStart = t - lastFrameTime; // Elapsed since data recv’d
w = (timeSinceFrameStart >= timeBetweenFrames) ? 255 :
(255L * timeSinceFrameStart / timeBetweenFrames);
if(direct) w = 255; //no interpolation if direct is set.
magic(brightness, rgbBuf, w, spiBufferBeingFilled, NUM_LEDS);
//swap spi buffers
spiBufferBeingFilled = 1 - spiBufferBeingFilled;
//record time of last frame
t = micros();
timeBetweenFrames = t - lastFrameTime;
lastFrameTime = t;
}
Thanks,