Using the Photon SPI Bus - WS2801 Problems

So I have an existing and happily running project that uses a Spark Core on a custom PCB that controls a string of 150 WS2801 LEDs. The WS2801 uses both clock and data lines (SPI minus slave select), as opposed to WS2811/WS2812 (Neopixels) which relies on strict timing on the data line. When I loaded the code onto a Photon keeping everything else the same, it no longer works. I am aware that the Photon has a faster clock, but I’ve played with the clock speeds and have run the Core successfully with a higher clock speed than the lowest Photon speed. I’ve also gotten the code to run happily on an Arduino (which is where the project began and I ported it to the wonderful Spark/Particle ecosystem :slight_smile: ).

An interesting note is that the code does light up the LEDS with a consistent pattern, but it doesn’t change or do anything other than that. I also noticed in the differences of the Photon that a second SPI bus was added. Is there something that must be done in the firmware to tell it which SPI bus to use? If not, how would it even be told to use the other bus? I’d prefer not to have to access the register directly if there is Spark/Particle code that can do it for me. I purchased and am awaiting delivery of a logic analyzer so I can actually see what’s going on as a DMM can only troubleshoot so far. I’ll update with my findings once I have tested it and compared the Core and Photon SPI lines, but thought I’d see if anyone else is having problems or successes with the Photon and SPI. Thanks!

@CHeil402, which version of system firmware are you running on the Photon? The latest is available here: https://github.com/spark/firmware/releases

Thanks for the help! Pardon my ignorance. But I’ve only updated via the Web IDE, and in settings it says I’m using Spark Firmware V0.3.4, so I assume that one? Is there another way to tell? And I further assume to update the firmware, I’d have to use the CLI to issue the commands and not by using the Web IDE?

EDIT: So I updated to V0.4.3 then reflashed the code via the Web IDE (does that revert back anything?). Now its a static repeatable pattern, but a different pattern. Hmm…

@CHeil402, having the latest system firmware removes some possible issues. Can you post your code? Is your hardware setup for the Photon the same as it was for the Core?

At this point I’ve dropped back to using essentially the base example for the WS2801 library. I am using the exact same hardware layout between the two.

/* ========================== Application.cpp =========================== */

#include "application.h"
#include "WS2801.h"

/*****************************************************************************
Example sketch for driving Adafruit WS2801 pixels on the Spark Core!
  Designed specifically to work with the Adafruit RGB Pixels!
  12mm Bullet shape ----> https://www.adafruit.com/products/322
  12mm Flat shape   ----> https://www.adafruit.com/products/738
  36mm Square shape ----> https://www.adafruit.com/products/683
  These pixels use SPI to transmit the color data, and have built in
  high speed PWM drivers for 24 bit color per pixel
  2 pins are required to interface
  Adafruit invests time and resources providing this open source code,
  please support Adafruit and open-source hardware by purchasing
  products from Adafruit!
  Written by Limor Fried/Ladyada for Adafruit Industries.
  BSD license, all text above must be included in any redistribution
*****************************************************************************/

// The colors of the wires may be totally different so
// BE SURE TO CHECK YOUR PIXELS TO SEE WHICH WIRES TO USE!

// SPARK CORE SPI PINOUTS
// http://docs.spark.io/#/firmware/communication-spi
// A5 (MOSI) Yellow wire on Adafruit Pixels
// A3 (SCK) Green wire on Adafruit Pixels

// Don't forget to connect the ground wire to Arduino ground,
// and the +5V wire to a +5V supply$

const int numPixel = 50;
const int numPixelX = 10;
const int numPixelY = 10;

uint8_t dataPin  = A5;    // Yellow wire on Adafruit Pixels
uint8_t clockPin = A3;    // Green wire on Adafruit Pixels

// Set the argument to the NUMBER of pixels.
Adafruit_WS2801 strip = Adafruit_WS2801(numPixel);
//Adafruit_WS2801 strip = Adafruit_WS2801(numPixelX, numPixelY);

// For 36mm LED pixels: these pixels internally represent color in a
// different format.  Either of the above constructors can accept an
// optional extra parameter: WS2801_RGB is 'conventional' RGB order
// WS2801_GRB is the GRB order required by the 36mm pixels.  Other
// than this parameter, your code does not need to do anything different;
// the library will handle the format change.  Example:
//Adafruit_WS2801 strip = Adafruit_WS2801(25, WS2801_GRB);

void setup() {
    //Spark.function("color", color);
    strip.begin();
}

void loop() {
  // Some example procedures showing how to display to the pixels
  
  colorWipe(Color(255, 0, 0), 50);
  colorWipe(Color(0, 255, 0), 50);
  colorWipe(Color(0, 0, 255), 50);
  rainbow(20);
  rainbowCycle(20);
}

void rainbow(uint8_t wait) {
  int i, j;
   
  for (j=0; j < 256; j++) {     // 3 cycles of all 256 colors in the wheel
    for (i=0; i < strip.numPixels(); i++) {
      strip.setPixelColor(i, Wheel( (i + j) % 255));
    }  
    strip.show();   // write all the pixels out
    delay(wait);
  }
}

// Slightly different, this one makes the rainbow wheel equally distributed 
// along the chain
void rainbowCycle(uint8_t wait) {
  int i, j;
  
  for (j=0; j < 256 * 5; j++) {     // 5 cycles of all 25 colors in the wheel
    for (i=0; i < strip.numPixels(); i++) {
      // tricky math! we use each pixel as a fraction of the full 96-color wheel
      // (thats the i / strip.numPixels() part)
      // Then add in j which makes the colors go around per pixel
      // the % 96 is to make the wheel cycle around
      strip.setPixelColor(i, Wheel( ((i * 256 / strip.numPixels()) + j) % 256) );
    }  
    strip.show();   // write all the pixels out
    delay(wait);
  }
}

// fill the dots one after the other with said color
// good for testing purposes
void colorWipe(uint32_t c, uint8_t wait) {
  int i;
  
  for (i=0; i < strip.numPixels(); i++) {
      strip.setPixelColor(i, c);
      strip.show();
      delay(wait);
  }
}

/* Helper functions */

// Create a 24 bit color value from R,G,B
uint32_t Color(byte r, byte g, byte b)
{
  uint32_t c;
  c = r;
  c <<= 8;
  c |= g;
  c <<= 8;
  c |= b;
  return c;
}

//Input a value 0 to 255 to get a color value.
//The colours are a transition r - g -b - back to r
uint32_t Wheel(byte WheelPos)
{
  if (WheelPos < 85) {
   return Color(WheelPos * 3, 255 - WheelPos * 3, 0);
  } else if (WheelPos < 170) {
   WheelPos -= 85;
   return Color(255 - WheelPos * 3, 0, WheelPos * 3);
  } else {
   WheelPos -= 170; 
   return Color(0, WheelPos * 3, 255 - WheelPos * 3);
  }
}

I can post my more specific app, but that would have more to troubleshoot and this basic example should work fine.

@CHeil402, what divisor are you using with SPI.setClockDivider()?

@peekay123 Right now it’s 128 and have tried 256 to no avail. Here’s the library cpp file:

//-----------------------------------------------//
// SPARK CORE Adafruit_WS2801 LIBRARY & EXAMPLE  //
//===============================================//
// Copy this into a new application at:          //
// https://www.spark.io/build and go nuts!       //
// Read comments in APPLICATION.CPP section for  //
// Hookup details!                               //
//-----------------------------------------------//
// Technobly / BDub - Jan 9th, 2014              //
//===============================================//

/*****************************************************************************
Example sketch for driving Adafruit WS2801 pixels on the Spark Core!
  Designed specifically to work with the Adafruit RGB Pixels!
  12mm Bullet shape ----> https://www.adafruit.com/products/322
  12mm Flat shape   ----> https://www.adafruit.com/products/738
  36mm Square shape ----> https://www.adafruit.com/products/683
  These pixels use SPI to transmit the color data, and have built in
  high speed PWM drivers for 24 bit color per pixel
  2 pins are required to interface
  Adafruit invests time and resources providing this open source code,
  please support Adafruit and open-source hardware by purchasing
  products from Adafruit!
  Written by Limor Fried/Ladyada for Adafruit Industries.
  BSD license, all text above must be included in any redistribution
*****************************************************************************/

/* ========================== Adafruit_WS2801.cpp =========================== */

#include "WS2801.h"

#if PLATFORM_ID == 0 // Core
  #define pinLO(_pin) (PIN_MAP[_pin].gpio_peripheral->BRR = PIN_MAP[_pin].gpio_pin)
  #define pinHI(_pin) (PIN_MAP[_pin].gpio_peripheral->BSRR = PIN_MAP[_pin].gpio_pin)
#elif PLATFORM_ID == 6 // Photon
  #include "pinmap_impl.h"
  STM32_Pin_Info* PIN_MAP = HAL_Pin_Map(); // Pointer required for highest access speed
  #define pinLO(_pin) (PIN_MAP[_pin].gpio_peripheral->BSRRH = PIN_MAP[_pin].gpio_pin)
  #define pinHI(_pin) (PIN_MAP[_pin].gpio_peripheral->BSRRL = PIN_MAP[_pin].gpio_pin)
#else
  #error "*** PLATFORM_ID not supported by this library. PLATFORM should be Core or Photon ***"
#endif
// fast pin access
#define pinSet(_pin, _hilo) (_hilo ? pinHI(_pin) : pinLO(_pin))

// Example to control WS2801-based RGB LED Modules in a strand or strip
// Written by Adafruit - MIT license
/*****************************************************************************/

// Constructor for use with hardware SPI (specific clock/data pins):
Adafruit_WS2801::Adafruit_WS2801(uint16_t n, uint8_t order) {
  rgb_order = order;
  alloc(n);
  updatePins();
}

// Constructor for use with arbitrary clock/data pins:
Adafruit_WS2801::Adafruit_WS2801(uint16_t n, uint8_t dpin, uint8_t cpin, uint8_t order) {
  rgb_order = order;
  alloc(n);
  updatePins(dpin, cpin);
}

// Constructor for use with a matrix configuration, specify w, h for size of matrix
// assumes configuration where string starts at coordinate 0,0 and continues to w-1,0, w-1,1
// and on to 0,1, 0,2 and on to w-1,2 and so on. Snaking back and forth till the end.
// other function calls with provide access to pixels via an x,y coordinate system
Adafruit_WS2801::Adafruit_WS2801(uint16_t w, uint16_t h, uint8_t dpin, uint8_t cpin, uint8_t order) {
  rgb_order = order;
  updatePins(dpin, cpin);
}

// Allocate 3 bytes per pixel, init to RGB 'off' state:
void Adafruit_WS2801::alloc(uint16_t n) {
  begun   = false;
  numLEDs = ((pixels = (uint8_t *)calloc(n, 3)) != NULL) ? n : 0;
}

// via Michael Vogt/neophob: empty constructor is used when strand length
// isn't known at compile-time; situations where program config might be
// read from internal flash memory or an SD card, or arrive via serial
// command.  If using this constructor, MUST follow up with updateLength()
// and updatePins() to establish the strand length and output pins!
// Also, updateOrder() to change RGB vs GRB order (RGB is default).
Adafruit_WS2801::Adafruit_WS2801(void) {
  begun     = false;
  numLEDs   = 0;
  pixels    = NULL;
  rgb_order = WS2801_RGB;
  updatePins(); // Must assume hardware SPI until pins are set
}

// Release memory (as needed):
Adafruit_WS2801::~Adafruit_WS2801(void) {
  if (pixels != NULL) {
    free(pixels);
  }
}

// Activate hard/soft SPI as appropriate:
void Adafruit_WS2801::begin(void) {
  if(hardwareSPI == true) {
    startSPI();
  } else {
    pinMode(datapin, OUTPUT);
    pinMode(clkpin , OUTPUT);
  }
  begun = true;
}

// Change pin assignments post-constructor, switching to hardware SPI:
void Adafruit_WS2801::updatePins(void) {
  hardwareSPI = true;
  datapin     = clkpin = 0;
  // If begin() was previously invoked, init the SPI hardware now:
  if(begun == true) startSPI();
  // Otherwise, SPI is NOT initted until begin() is explicitly called.

  // Note: any prior clock/data pin directions are left as-is and are
  // NOT restored as inputs!
}

// Change pin assignments post-constructor, using arbitrary pins:
void Adafruit_WS2801::updatePins(uint8_t dpin, uint8_t cpin) {

  if(begun == true) { // If begin() was previously invoked...
    // If previously using hardware SPI, turn that off:
    if(hardwareSPI == true) SPI.end();
    // Regardless, now enable output on 'soft' SPI pins:
    pinMode(dpin, OUTPUT);
    pinMode(cpin, OUTPUT);
  } // Otherwise, pins are not set to outputs until begin() is called.

  // Note: any prior clock/data pin directions are left as-is and are
  // NOT restored as inputs!

  hardwareSPI = false;
  datapin     = dpin;
  clkpin      = cpin;
  ////clkport     = portOutputRegister(digitalPinToPort(cpin));
  ////clkpinmask  = digitalPinToBitMask(cpin);
  ////dataport    = portOutputRegister(digitalPinToPort(dpin));
  ////datapinmask = digitalPinToBitMask(dpin);
}

// Enable SPI hardware and set up protocol details:
void Adafruit_WS2801::startSPI(void) {
    SPI.begin();
    SPI.setBitOrder(MSBFIRST);
    SPI.setDataMode(SPI_MODE0);
    SPI.setClockDivider(SPI_CLOCK_DIV128); // 1 MHz max, else flicker (set to 72MHz/128 = 562.5kHz)
}

uint16_t Adafruit_WS2801::numPixels(void) {
  return numLEDs;
}

// Change strand length (see notes with empty constructor, above):
void Adafruit_WS2801::updateLength(uint16_t n) {
  if(pixels != NULL) free(pixels); // Free existing data (if any)
  // Allocate new data -- note: ALL PIXELS ARE CLEARED
  numLEDs = ((pixels = (uint8_t *)calloc(n, 3)) != NULL) ? n : 0;
  // 'begun' state does not change -- pins retain prior modes
}

// Change RGB data order (see notes with empty constructor, above):
void Adafruit_WS2801::updateOrder(uint8_t order) {
  rgb_order = order;
  // Existing LED data, if any, is NOT reformatted to new data order.
  // Calling function should clear or fill pixel data anew.
}

void Adafruit_WS2801::show(void) {
  uint16_t i, nl3 = numLEDs * 3; // 3 bytes per LED
  uint8_t bit;
  
    if(true) {
        for(i=0; i<nl3; i++) {
            SPI.transfer(pixels[i]);
        }
    } 
    else {
        for(i=0; i<nl3; i++ ) {
          for(bit=0x80; bit; bit >>= 1) {
            if(pixels[i] & bit) pinSet(datapin, HIGH);
            else                pinSet(datapin, LOW);
            pinSet(clkpin, HIGH);
            pinSet(clkpin, LOW);
          }
        }
    }

  delay(1); // Data is latched by holding clock pin low for 1 millisecond
}

// Set pixel color from separate 8-bit R, G, B components:
void Adafruit_WS2801::setPixelColor(uint16_t n, uint8_t r, uint8_t g, uint8_t b) {
  if(n < numLEDs) { // Arrays are 0-indexed, thus NOT '<='
    uint8_t *p = &pixels[n * 3];
    // See notes later regarding color order
    if(rgb_order == WS2801_RGB) {
      *p++ = r;
      *p++ = g;
    } else {
      *p++ = g;
      *p++ = r;
    }
    *p++ = b;
  }
}

// Set pixel color from 'packed' 32-bit RGB value:
void Adafruit_WS2801::setPixelColor(uint16_t n, uint32_t c) {
  if(n < numLEDs) { // Arrays are 0-indexed, thus NOT '<='
    uint8_t *p = &pixels[n * 3];
    // To keep the show() loop as simple & fast as possible, the
    // internal color representation is native to different pixel
    // types.  For compatibility with existing code, 'packed' RGB
    // values passed in or out are always 0xRRGGBB order.
    if(rgb_order == WS2801_RGB) {
      *p++ = c >> 16; // Red
      *p++ = c >>  8; // Green
    } else {
      *p++ = c >>  8; // Green
      *p++ = c >> 16; // Red
    }
    *p++ = c;         // Blue
  }
}

// Query color from previously-set pixel (returns packed 32-bit RGB value)
uint32_t Adafruit_WS2801::getPixelColor(uint16_t n) {
  if(n < numLEDs) {
    uint16_t ofs = n * 3;
    // To keep the show() loop as simple & fast as possible, the
    // internal color representation is native to different pixel
    // types.  For compatibility with existing code, 'packed' RGB
    // values passed in or out are always 0xRRGGBB order.
    return (rgb_order == WS2801_RGB) ?
      ((uint32_t)pixels[ofs] << 16) | ((uint16_t) pixels[ofs + 1] <<  8) | pixels[ofs + 2] :
      (pixels[ofs] <<  8) | ((uint32_t)pixels[ofs + 1] << 16) | pixels[ofs + 2];
  }

  return 0; // Pixel # is out of bounds
}

OK, something else is going on here. I took out all the potential issues and removed all libraries… This works as it should, so now I have to figure out what the problem is. I don’t know why it would work with the Core and not the Photon which is even more interesting. Can’t wait for the logic analyzer.

#include "application.h";

void setup() {
    SPI.begin();
    SPI.setBitOrder(MSBFIRST);
    SPI.setDataMode(SPI_MODE0);
    SPI.setClockDivider(SPI_CLOCK_DIV128);
}

void loop() {
    int i;
    
    for(i=0; i<3; i++) {
        //Red
        SPI.transfer(0xFF);
        SPI.transfer(0x00);
        SPI.transfer(0x00);
        //Green
        SPI.transfer(0x00);
        SPI.transfer(0xFF);
        SPI.transfer(0x00);
        //Blue
        SPI.transfer(0x00);
        SPI.transfer(0x00);
        SPI.transfer(0xFF);
    }
    
    delay(1000); // Data is latched by holding clock pin low for 1 millisecond
    
    for(i=0; i<3; i++) {
        //Yellow
        SPI.transfer(0xFF);
        SPI.transfer(0xFF);
        SPI.transfer(0x00);
        //Cyan
        SPI.transfer(0x00);
        SPI.transfer(0xFF);
        SPI.transfer(0xFF);
        //Magenta
        SPI.transfer(0xFF);
        SPI.transfer(0x00);
        SPI.transfer(0xFF);
    }
    
    delay(1000); // Data is latched by holding clock pin low for 1 millisecond
    
    for(i=0; i<3; i++) {
        //Blue
        SPI.transfer(0x00);
        SPI.transfer(0x00);
        SPI.transfer(0xFF);
        //Red
        SPI.transfer(0xFF);
        SPI.transfer(0x00);
        SPI.transfer(0x00);
        //Green
        SPI.transfer(0x00);
        SPI.transfer(0xFF);
        SPI.transfer(0x00);
    }
    
    delay(1000); // Data is latched by holding clock pin low for 1 millisecond
}

@CHeil402, I only have neopixels so I can’t test :cry: I will however run the code above and scope it to see if there any fundamental differences between a Core and a Photon. I’ll try to get that done tonight. BTW, which analyzer did you buy?

@CHeil402, ran your code on a Photon with system firmware 0.4.3rc2 and code compiled on Particle DEV (0.4.1 I believe). I shortened the delays to fits things better on both my scope and logic analyzer. One thing I discovered is that the SPI root clock is 60MHz, not 120MHz. Given this, I also set a trigger pin to go high at the start of the sequence, then low at the end so I could trigger properly. Here is the first RGB set, which is exactly as expected:

The other two sets are correct. One thing to note is the fact that the MOSI line stays at whatever is the last bit to go out is until another byte is send. The clock in this case is DIV64 and is correct if calculated with 60MHz root clock.

So, I don’t see any issues at this point.

3 Likes

@peekay123 I bought this one from eBay which is supposed to be compatible with the Saleae software for a fraction of the cost. Hopefully being cheap doesn’t come back to bite me. http://www.ebay.com/itm/190956566562

One thing is for sure, the 0.4.3rc2 firmware is definitely more stable than what I had before as it connects faster. Interesting that the SPI root clock is 60 MHz; hopefully once the documentation makes it way back up that gets noted. Looking at your screenshot makes it look like you ran the bottom code (most recent code snippet) since it repeats RGBx3. This is the only code that I was able to get to work on the Photon. It was when I was using the library that it broke on the Photon but worked on the Core, so something’s definitely going on. It would be one thing if the library didn’t work on either. But now that I at least know that the SPI bus works on the Photon I can start to rebuild the library from scratch and see where it breaks… and once I get the logic analyzer I’ll be able to take a look at the original code.

If you use the WS2801 library that’s on the Web IDE with the first code snippet that’s what breaks (even though the main library that I used was a trimmed version of it).

Thanks so much for your help, sorry I didn’t answer sooner. I’ll keep you posted on my progress.

@CHeil402, I’ll look at your code and fun it on the analyzer to see if there is anything weird. Having a Core and a Photon makes things simpler.

FYI, for the logic analyzer, you may want to get one that’s faster than 24MHz at some point. With 8 bits, that’s 3Mbits/s on each channel meaning the fastest signal you could capture is about 1.5MHz (at best). This is the one I was considering:

:smile:

Hi @peekay123,

Is the SPI bus on the photon exclusively for the user code, or is the SPI bus shared with system firmware? And if so what is your opinion about accessing the SPI from an interrupt routine?

@marcus, the SPI bus IS dedicated to the user and CAN be accessed in an ISR. The existing code blocks while sending or receiving until a full byte is sent/received though I believe there will be support for SPI DMA in a future version of firmware. :smile:

tnx!

1 Like

Sorry I’ve been a little absent, but I haven’t had any time to play with this. I can confirm that the SPI was working as it should on the Photon, noting that the SPI clock is half of the system clock. I believe when I looked at this a month ago the issue was actually with how the Photon handled the calloc function compared to the Core. Sorry I haven’t had any more time to further diagnose this, but when I would send any number of generated bytes to the SPI it would function properly, but when I used the library it would always initialize to the same pattern when calloc should initialize to an array of zeros. See the below snippet from the library of the WS2801…

Spark_WS2801::Spark_WS2801(unsigned int w, unsigned int h) {
    alloc(w * h);
    width = w;
    height = h;
}

// Allocate 3 bytes per pixel, init to RGB 'off' state:
void Spark_WS2801::alloc(unsigned int n) {
    begun   = false;
    numLEDs = ((pixels = (char *)calloc(n, 3)) != NULL) ? n : 0;
}

Okay,
A little status report about the SPI bus behaviour. I have a CC1101 module connected and tested it for 5 days continuously. Everything works very good and stable. I tried different clock dividers and they all functioned perfectly.
When you work on a breadboard, I would not advise the highest SPi frequencies.

CHeil402, I’m curious if you ever solved the problem using the WS2801 strip with the Photon? I’ve just recently tried setting this up and I’m having what seems like the exact same problem you had. I’ve used the strip fine both on an Arduino and on one of those early spark 1" square devices, I forget the exact part number right now.

Thanks

After building with the latest firmware it now works. It must have been a bug that got addressed whatever it was. Make sure you’re building with the latest firmware in the IDE.