summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYuval Adam <yuv.adm@gmail.com>2013-05-16 21:02:19 +0300
committerYuval Adam <yuv.adm@gmail.com>2013-05-16 21:02:19 +0300
commit2ff68176a3fa5b4390abcf2ae28276c4510f95f5 (patch)
treeaa778f7f16996fedc9aa73a90a2d8c5ca8e45b69
parent8111ed7c07a31dd99a323a0a29f9e99dd54c6d80 (diff)
Funky LPD8806 stuff
-rw-r--r--lpd8806/lib/.holder0
-rw-r--r--lpd8806/src/LPD8806.cpp296
-rw-r--r--lpd8806/src/LPD8806.h46
-rw-r--r--lpd8806/src/sketch.ino74
4 files changed, 416 insertions, 0 deletions
diff --git a/lpd8806/lib/.holder b/lpd8806/lib/.holder
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/lpd8806/lib/.holder
diff --git a/lpd8806/src/LPD8806.cpp b/lpd8806/src/LPD8806.cpp
new file mode 100644
index 0000000..b6632d9
--- /dev/null
+++ b/lpd8806/src/LPD8806.cpp
@@ -0,0 +1,296 @@
+/*
+Arduino library to control LPD8806-based RGB LED Strips
+Copyright (C) Adafruit Industries
+MIT license
+
+Clearing up some misconceptions about how the LPD8806 drivers work:
+
+The LPD8806 is not a FIFO shift register. The first data out controls the
+LED *closest* to the processor (unlike a typical shift register, where the
+first data out winds up at the *furthest* LED). Each LED driver 'fills up'
+with data and then passes through all subsequent bytes until a latch
+condition takes place. This is actually pretty common among LED drivers.
+
+All color data bytes have the high bit (128) set, with the remaining
+seven bits containing a brightness value (0-127). A byte with the high
+bit clear has special meaning (explained later).
+
+The rest gets bizarre...
+
+The LPD8806 does not perform an in-unison latch (which would display the
+newly-transmitted data all at once). Rather, each individual byte (even
+the separate G, R, B components of each LED) is latched AS IT ARRIVES...
+or more accurately, as the first bit of the subsequent byte arrives and
+is passed through. So the strip actually refreshes at the speed the data
+is issued, not instantaneously (this can be observed by greatly reducing
+the data rate). This has implications for POV displays and light painting
+applications. The 'subsequent' rule also means that at least one extra
+byte must follow the last pixel, in order for the final blue LED to latch.
+
+To reset the pass-through behavior and begin sending new data to the start
+of the strip, a number of zero bytes must be issued (remember, all color
+data bytes have the high bit set, thus are in the range 128 to 255, so the
+zero is 'special'). This should be done before each full payload of color
+values to the strip. Curiously, zero bytes can only travel one meter (32
+LEDs) down the line before needing backup; the next meter requires an
+extra zero byte, and so forth. Longer strips will require progressively
+more zeros. *(see note below)
+
+In the interest of efficiency, it's possible to combine the former EOD
+extra latch byte and the latter zero reset...the same data can do double
+duty, latching the last blue LED while also resetting the strip for the
+next payload.
+
+So: reset byte(s) of suitable length are issued once at startup to 'prime'
+the strip to a known ready state. After each subsequent LED color payload,
+these reset byte(s) are then issued at the END of each payload, both to
+latch the last LED and to prep the strip for the start of the next payload
+(even if that data does not arrive immediately). This avoids a tiny bit
+of latency as the new color payload can begin issuing immediately on some
+signal, such as a timer or GPIO trigger.
+
+Technically these zero byte(s) are not a latch, as the color data (save
+for the last byte) is already latched. It's a start-of-data marker, or
+an indicator to clear the thing-that's-not-a-shift-register. But for
+conversational consistency with other LED drivers, we'll refer to it as
+a 'latch' anyway.
+
+* This has been validated independently with multiple customers'
+ hardware. Please do not report as a bug or issue pull requests for
+ this. Fewer zeros sometimes gives the *illusion* of working, the first
+ payload will correctly load and latch, but subsequent frames will drop
+ data at the end. The data shortfall won't always be visually apparent
+ depending on the color data loaded on the prior and subsequent frames.
+ Tested. Confirmed. Fact.
+*/
+
+
+#include "SPI.h"
+#include "LPD8806.h"
+
+/*****************************************************************************/
+
+// Constructor for use with hardware SPI (specific clock/data pins):
+LPD8806::LPD8806(uint16_t n) {
+ pixels = NULL;
+ begun = false;
+ updateLength(n);
+ updatePins();
+}
+
+// Constructor for use with arbitrary clock/data pins:
+LPD8806::LPD8806(uint16_t n, uint8_t dpin, uint8_t cpin) {
+ pixels = NULL;
+ begun = false;
+ updateLength(n);
+ updatePins(dpin, cpin);
+}
+
+// via Michael Vogt/neophob: empty constructor is used when strip 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 strip length and output pins!
+LPD8806::LPD8806(void) {
+ numLEDs = numBytes = 0;
+ pixels = NULL;
+ begun = false;
+ updatePins(); // Must assume hardware SPI until pins are set
+}
+
+// Activate hard/soft SPI as appropriate:
+void LPD8806::begin(void) {
+ if(hardwareSPI == true) startSPI();
+ else startBitbang();
+ begun = true;
+}
+
+// Change pin assignments post-constructor, switching to hardware SPI:
+void LPD8806::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 LPD8806::updatePins(uint8_t dpin, uint8_t cpin) {
+
+ 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__)
+ 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!
+
+ hardwareSPI = false;
+}
+
+#ifndef SPI_CLOCK_DIV8
+ #define SPI_CLOCK_DIV8 4
+#endif
+
+// Enable SPI hardware and set up protocol details:
+void LPD8806::startSPI(void) {
+ 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__)
+
+ // 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
+}
+
+// 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);
+ }
+ }
+}
+
+// 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
+ // '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;
+
+ // This doesn't need to distinguish among individual pixel color
+ // 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
+ }
+ } else {
+ uint8_t p, bit;
+
+ while(i--) {
+ p = *ptr++;
+ for(bit=0x80; bit; bit >>= 1) {
+ if (dataport != 0) {
+ if(p & bit) *dataport |= datapinmask;
+ else *dataport &= ~datapinmask;
+ *clkport |= clkpinmask;
+ *clkport &= ~clkpinmask;
+ } else {
+ if (p&bit) digitalWrite(datapin, HIGH);
+ else digitalWrite(datapin, LOW);
+ digitalWrite(clkpin, HIGH);
+ digitalWrite(clkpin, LOW);
+ }
+ }
+ }
+ }
+}
+
+// Convert separate R,G,B into combined 32-bit GRB color:
+uint32_t LPD8806::Color(byte r, byte g, byte b) {
+ return ((uint32_t)(g | 0x80) << 16) |
+ ((uint32_t)(r | 0x80) << 8) |
+ b | 0x80 ;
+}
+
+// Set pixel color from separate 7-bit R, G, B components:
+void LPD8806::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];
+ *p++ = g | 0x80; // Strip color order is GRB,
+ *p++ = r | 0x80; // not the more common RGB,
+ *p++ = b | 0x80; // so the order here is intentional; don't "fix"
+ }
+}
+
+// Set pixel color from 'packed' 32-bit GRB (not RGB) value:
+void LPD8806::setPixelColor(uint16_t n, uint32_t c) {
+ if(n < numLEDs) { // Arrays are 0-indexed, thus NOT '<='
+ uint8_t *p = &pixels[n * 3];
+ *p++ = (c >> 16) | 0x80;
+ *p++ = (c >> 8) | 0x80;
+ *p++ = c | 0x80;
+ }
+}
+
+// Query color from previously-set pixel (returns packed 32-bit GRB value)
+uint32_t LPD8806::getPixelColor(uint16_t n) {
+ if(n < numLEDs) {
+ uint16_t ofs = n * 3;
+ return ((uint32_t)(pixels[ofs ] & 0x7f) << 16) |
+ ((uint32_t)(pixels[ofs + 1] & 0x7f) << 8) |
+ (uint32_t)(pixels[ofs + 2] & 0x7f);
+ }
+
+ return 0; // Pixel # is out of bounds
+}
diff --git a/lpd8806/src/LPD8806.h b/lpd8806/src/LPD8806.h
new file mode 100644
index 0000000..126ef1c
--- /dev/null
+++ b/lpd8806/src/LPD8806.h
@@ -0,0 +1,46 @@
+#if (ARDUINO >= 100)
+ #include <Arduino.h>
+#else
+ #include <WProgram.h>
+ #include <pins_arduino.h>
+#endif
+
+class LPD8806 {
+
+ public:
+
+ LPD8806(uint16_t n, uint8_t dpin, uint8_t cpin); // Configurable pins
+ LPD8806(uint16_t n); // Use SPI hardware; specific pins only
+ 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),
+ updatePins(uint8_t dpin, uint8_t cpin), // Change pins, configurable
+ updatePins(void), // Change pins, hardware SPI
+ updateLength(uint16_t n); // Change strip length
+ uint16_t
+ numPixels(void);
+ uint32_t
+ Color(byte, byte, byte),
+ getPixelColor(uint16_t n);
+
+ private:
+
+ uint16_t
+ 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
+ clkpinmask, datapinmask; // Clock & data PORT bitmasks
+ volatile uint8_t
+ *clkport , *dataport; // Clock & data PORT registers
+ void
+ startBitbang(void),
+ startSPI(void);
+ boolean
+ hardwareSPI, // If 'true', using hardware SPI
+ begun; // If 'true', begin() method was previously invoked
+};
diff --git a/lpd8806/src/sketch.ino b/lpd8806/src/sketch.ino
new file mode 100644
index 0000000..21b865f
--- /dev/null
+++ b/lpd8806/src/sketch.ino
@@ -0,0 +1,74 @@
+#include "LPD8806.h"
+#include "SPI.h"
+
+// Simple test for 160 (5 meters) of LPD8806-based RGB LED strip
+
+/*****************************************************************************/
+
+// Number of RGB LEDs in strand:
+int nLEDs = 160;
+
+// Chose 2 pins for output; can be any valid output pins:
+int dataPin = 2;
+int clockPin = 3;
+
+// First parameter is the number of LEDs in the strand. The LED strips
+// are 32 LEDs per meter but you can extend or cut the strip. Next two
+// parameters are SPI data and clock pins:
+LPD8806 strip = LPD8806(nLEDs, dataPin, clockPin);
+
+// You can optionally use hardware SPI for faster writes, just leave out
+// the data and clock pin parameters. But this does limit use to very
+// specific pins on the Arduino. For "classic" Arduinos (Uno, Duemilanove,
+// etc.), data = pin 11, clock = pin 13. For Arduino Mega, data = pin 51,
+// clock = pin 52. For 32u4 Breakout Board+ and Teensy, data = pin B2,
+// clock = pin B1. For Leonardo, this can ONLY be done on the ICSP pins.
+//LPD8806 strip = LPD8806(nLEDs);
+
+uint32_t SCHEME[] = {
+ strip.Color(117, 0, 27),
+ strip.Color(127, 43, 0),
+ strip.Color(0, 85, 57),
+ strip.Color(41, 111, 0)
+};
+
+uint32_t SCHEME2[] = {
+ strip.Color(99,0,62),
+ strip.Color(124,0,9),
+ strip.Color(53,5,85),
+ strip.Color(84,120,0)
+};
+
+void setup() {
+ // Start up the LED strip
+ strip.begin();
+
+ // Update the strip, to start they are all 'off'
+ strip.show();
+}
+
+void loop() {
+ colorChase();
+}
+
+// Chase one dot down the full strip. Good for testing purposes.
+void colorChase() {
+ int i;
+ uint32_t c;
+ uint8_t wait;
+
+ // Start by turning all pixels off:
+ for(i=0; i<strip.numPixels(); i++) strip.setPixelColor(i, 0);
+
+ // Then display one pixel at a time:
+ for(i=0; i<strip.numPixels(); i++) {
+ c = SCHEME2[random(4)];
+ strip.setPixelColor(i, c); // Set new pixel 'on'
+ // strip.show(); // Refresh LED states
+ // strip.setPixelColor(i, 0); // Erase pixel, but don't refresh!
+ // delay(random(30));
+ }
+
+ strip.show(); // Refresh to turn off last pixel
+}
+