Skip to content

Commit

Permalink
Support for Trinket/Gemma (see strandtest.ino)
Browse files Browse the repository at this point in the history
  • Loading branch information
PaintYourDragon committed Jun 29, 2014
1 parent 8a14896 commit dbc0d83
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 85 deletions.
180 changes: 102 additions & 78 deletions LPD8806.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,47 @@ a 'latch' anyway.
Tested. Confirmed. Fact.
*/


#include "SPI.h"
#include "LPD8806.h"

#ifdef __AVR_ATtiny85__

// Teensy/Gemma-specific stuff for hardware-assisted SPI @ 2 MHz

#if(F_CPU > 8000000L)
#define SPI_DELAY asm volatile("rjmp .+0"); // Burn 2 cycles
#elif(F_CPU > 4000000L)
#define SPI_DELAY asm volatile("nop"); // Burn 1 cycle
#else
#define SPI_DELAY // Run max speed
#endif

#define SPIBIT \
USICR = ((1<<USIWM0)|(1<<USITC)); \
SPI_DELAY \
USICR = ((1<<USIWM0)|(1<<USITC)|(1<<USICLK)); \
SPI_DELAY

static void spi_out(uint8_t n) {
USIDR = n;
SPIBIT
SPIBIT
SPIBIT
SPIBIT
SPIBIT
SPIBIT
SPIBIT
SPIBIT
}

#else

// All other boards support Full and Proper Hardware SPI

#include <SPI.h>
#define spi_out(n) (void)SPI.transfer(n)

#endif

/*****************************************************************************/

// Constructor for use with hardware SPI (specific clock/data pins):
Expand Down Expand Up @@ -107,117 +144,111 @@ void LPD8806::begin(void) {

// Change pin assignments post-constructor, switching to hardware SPI:
void LPD8806::updatePins(void) {
hardwareSPI = true;
pinMode(datapin, INPUT); // Restore data and clock pins to inputs
pinMode(clkpin , INPUT);
datapin = clkpin = 0;
hardwareSPI = true;
// 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 LPD8806::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) {
#ifdef __AVR_ATtiny85__
DDRB &= ~(_BV(PORTB1) | _BV(PORTB2));
#else
SPI.end();
#endif
} else {
pinMode(datapin, INPUT); // Restore prior data and clock pins to inputs
pinMode(clkpin , INPUT);
}
}
datapin = dpin;
clkpin = cpin;
clkport = dataport = 0;
clkpinmask = datapinmask = 0;

#if defined(__AVR_ATmega168__) || defined(__AVR_ATmega328P__) || defined (__AVR_ATmega328__) || defined(__AVR_ATmega8__) || (__AVR_ATmega1281__) || defined(__AVR_ATmega2561__) || defined(__AVR_ATmega2560__) || defined(__AVR_ATmega1280__)
#ifdef __AVR__
clkport = portOutputRegister(digitalPinToPort(cpin));
clkpinmask = digitalPinToBitMask(cpin);
dataport = portOutputRegister(digitalPinToPort(dpin));
datapinmask = digitalPinToBitMask(dpin);
#endif

if(begun == true) { // If begin() was previously invoked...
// If previously using hardware SPI, turn that off:
if(hardwareSPI == true) SPI.end();
startBitbang(); // Regardless, now enable 'soft' SPI outputs
} // 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!
// If previously begun, enable 'soft' SPI outputs now
if(begun == true) startBitbang();

hardwareSPI = false;
}

#ifndef SPI_CLOCK_DIV8
#define SPI_CLOCK_DIV8 4
#endif

// Enable SPI hardware and set up protocol details:
void LPD8806::startSPI(void) {
#ifdef __AVR_ATtiny85__
PORTB &= ~(_BV(PORTB1) | _BV(PORTB2)); // Outputs
DDRB |= _BV(PORTB1) | _BV(PORTB2); // DO (NOT MOSI) + SCK
#else
SPI.begin();
SPI.setBitOrder(MSBFIRST);
SPI.setDataMode(SPI_MODE0);

SPI.setClockDivider(SPI_CLOCK_DIV8); // 2 MHz
// SPI bus is run at 2MHz. Although the LPD8806 should, in theory,
// work up to 20MHz, the unshielded wiring from the Arduino is more
// susceptible to interference. Experiment and see what you get.

#if defined(__AVR_ATmega168__) || defined(__AVR_ATmega328P__) || defined (__AVR_ATmega328__) || defined(__AVR_ATmega8__) || (__AVR_ATmega1281__) || defined(__AVR_ATmega2561__) || defined(__AVR_ATmega2560__) || defined(__AVR_ATmega1280__)
#if defined(__AVR__) || defined(CORE_TEENSY)
SPI.setClockDivider(SPI_CLOCK_DIV8);
#else
SPI.setClockDivider((F_CPU + 1000000L) / 2000000L);
#endif
#endif

// Issue initial latch/reset to strip:
SPDR = 0; // Issue initial byte
for(uint16_t i=((numLEDs+31)/32)-1; i>0; i--) {
while(!(SPSR & (1<<SPIF))); // Wait for prior byte out
SPDR = 0; // Issue next byte
}
#else
SPI.transfer(0);
for(uint16_t i=((numLEDs+31)/32)-1; i>0; i--) {
SPI.transfer(0);
}
#endif
for(uint16_t i=((numLEDs+31)/32); i>0; i--) spi_out(0);
}

// Enable software SPI pins and issue initial latch:
void LPD8806::startBitbang() {
pinMode(datapin, OUTPUT);
pinMode(clkpin , OUTPUT);
if (dataport != 0) {
// use low level bitbanging when we can
*dataport &= ~datapinmask; // Data is held low throughout (latch = 0)
for(uint16_t i=((numLEDs+31)/32)*8; i>0; i--) {
*clkport |= clkpinmask;
*clkport &= ~clkpinmask;
}
} else {
// can't do low level bitbanging, revert to digitalWrite
digitalWrite(datapin, LOW);
for(uint16_t i=((numLEDs+31)/32)*8; i>0; i--) {
digitalWrite(clkpin, HIGH);
digitalWrite(clkpin, LOW);
}
#ifdef __AVR__
*dataport &= ~datapinmask; // Data is held low throughout (latch = 0)
for(uint16_t i=((numLEDs+31)/32)*8; i>0; i--) {
*clkport |= clkpinmask;
*clkport &= ~clkpinmask;
}
#else
digitalWrite(datapin, LOW);
for(uint16_t i=((numLEDs+31)/32)*8; i>0; i--) {
digitalWrite(clkpin, HIGH);
digitalWrite(clkpin, LOW);
}
#endif
}

// Change strip length (see notes with empty constructor, above):
void LPD8806::updateLength(uint16_t n) {
uint8_t latchBytes = (n + 31) / 32;
if(pixels != NULL) free(pixels); // Free existing data (if any)
numLEDs = n;
n *= 3; // 3 bytes per pixel
numBytes = n + latchBytes;
if(NULL != (pixels = (uint8_t *)malloc(numBytes))) { // Alloc new data
memset( pixels , 0x80, n); // Init to RGB 'off' state
memset(&pixels[n], 0 , latchBytes); // Clear latch bytes
} else numLEDs = numBytes = 0; // else malloc failed
uint8_t latchBytes;
uint16_t dataBytes, totalBytes;

numLEDs = numBytes = 0;
if(pixels) free(pixels); // Free existing data (if any)

dataBytes = n * 3;
latchBytes = (n + 31) / 32;
totalBytes = dataBytes + latchBytes;
if((pixels = (uint8_t *)malloc(totalBytes))) { // Alloc new data
numLEDs = n;
numBytes = totalBytes;
memset( pixels , 0x80, dataBytes); // Init to RGB 'off' state
memset(&pixels[dataBytes], 0 , latchBytes); // Clear latch bytes
}
// 'begun' state does not change -- pins retain prior modes
}

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

// This is how data is pushed to the strip. Unfortunately, the company
// that makes the chip didnt release the protocol document or you need
// to sign an NDA or something stupid like that, but we reverse engineered
// this from a strip controller and it seems to work very nicely!
void LPD8806::show(void) {
uint8_t *ptr = pixels;
uint16_t i = numBytes;
Expand All @@ -226,31 +257,24 @@ void LPD8806::show(void) {
// bytes vs. latch data, etc. Everything is laid out in one big
// flat buffer and issued the same regardless of purpose.
if(hardwareSPI) {
while(i--) {
#if defined(__AVR_ATmega168__) || defined(__AVR_ATmega328P__) || defined (__AVR_ATmega328__) || defined(__AVR_ATmega8__) || (__AVR_ATmega1281__) || defined(__AVR_ATmega2561__) || defined(__AVR_ATmega2560__) || defined(__AVR_ATmega1280__)
while(!(SPSR & (1<<SPIF))); // Wait for prior byte out
SPDR = *ptr++; // Issue new byte
#else
SPI.transfer(*ptr++);
#endif
}
while(i--) spi_out(*ptr++);
} else {
uint8_t p, bit;

while(i--) {
p = *ptr++;
for(bit=0x80; bit; bit >>= 1) {
if (dataport != 0) {
#ifdef __AVR__
if(p & bit) *dataport |= datapinmask;
else *dataport &= ~datapinmask;
*clkport |= clkpinmask;
*clkport &= ~clkpinmask;
} else {
if (p&bit) digitalWrite(datapin, HIGH);
else digitalWrite(datapin, LOW);
#else
if(p & bit) digitalWrite(datapin, HIGH);
else digitalWrite(datapin, LOW);
digitalWrite(clkpin, HIGH);
digitalWrite(clkpin, LOW);
}
#endif
}
}
}
Expand Down
9 changes: 6 additions & 3 deletions LPD8806.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ class LPD8806 {
LPD8806(void); // Empty constructor; init pins & strip length later
void
begin(void),
show(void),
setPixelColor(uint16_t n, uint8_t r, uint8_t g, uint8_t b),
setPixelColor(uint16_t n, uint32_t c),
show(void),
updatePins(uint8_t dpin, uint8_t cpin), // Change pins, configurable
updatePins(void), // Change pins, hardware SPI
updateLength(uint16_t n); // Change strip length
Expand All @@ -32,11 +32,14 @@ class LPD8806 {
numLEDs, // Number of RGB LEDs in strip
numBytes; // Size of 'pixels' buffer below
uint8_t
*pixels, // Holds LED color values (3 bytes each) + latch
clkpin , datapin, // Clock & data pin numbers
*pixels, // Holds LED color values (3 bytes each) + latch bytes
clkpin , datapin; // Clock & data pin numbers
#ifdef __AVR__
uint8_t
clkpinmask, datapinmask; // Clock & data PORT bitmasks
volatile uint8_t
*clkport , *dataport; // Clock & data PORT registers
#endif
void
startBitbang(void),
startSPI(void);
Expand Down
5 changes: 2 additions & 3 deletions examples/LEDbeltKit/LEDbeltKit.pde
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include "SPI.h"

// Example to control LPD8806-based RGB LED Modules in a strip!
// NOTE: WILL NOT WORK ON TRINKET OR GEMMA due to floating-point math
/*****************************************************************************/

#if defined(USB_SERIAL) || defined(USB_SERIAL_ADAFRUIT)
Expand All @@ -19,8 +20,6 @@ int clockPin = 15;
// The LED strips are 32 LEDs per meter but you can extend/cut the strip
LPD8806 strip = LPD8806(32, dataPin, clockPin);



void setup() {
// Start up the LED strip
strip.begin();
Expand Down Expand Up @@ -252,4 +251,4 @@ uint32_t Wheel(uint16_t WheelPos)
break;
}
return(strip.Color(r,g,b));
}
}
2 changes: 2 additions & 0 deletions examples/LEDbeltKit_alt/LEDbeltKit_alt.pde
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
// ANIMATION. See the 'strandtest' sketch for a simpler and less RAM-
// intensive example that can process more LEDs (100+).

// ALSO: NOT COMPATIBLE WITH TRINKET OR GEMMA for way too many reasons.

// Example to control LPD8806-based RGB LED Modules in a strip; originally
// intended for the Adafruit Digital Programmable LED Belt Kit.
// REQUIRES TIMER1 LIBRARY: http://www.arduino.cc/playground/Code/Timer1
Expand Down
1 change: 1 addition & 0 deletions examples/longstrandtest/longstrandtest.pde
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include "SPI.h"

// Simple test for 160 (5 meters) of LPD8806-based RGB LED strip
// Not compatible with Trinket/Gemma due to limited RAM

/*****************************************************************************/

Expand Down
9 changes: 8 additions & 1 deletion examples/strandtest/strandtest.ino
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
#include "LPD8806.h"
#include "SPI.h"
#include "SPI.h" // Comment out this line if using Trinket or Gemma
#ifdef __AVR_ATtiny85__
#include <avr/power.h>
#endif

// Example to control LPD8806-based RGB LED Modules in a strip

Expand All @@ -26,6 +29,10 @@ LPD8806 strip = LPD8806(nLEDs, dataPin, clockPin);
//LPD8806 strip = LPD8806(nLEDs);

void setup() {
#if defined(__AVR_ATtiny85__) && (F_CPU == 16000000L)
clock_prescale_set(clock_div_1); // Enable 16 MHz on Trinket
#endif

// Start up the LED strip
strip.begin();

Expand Down

0 comments on commit dbc0d83

Please sign in to comment.