diff options
| -rw-r--r-- | lpd8806/lib/.holder | 0 | ||||
| -rw-r--r-- | lpd8806/src/LPD8806.cpp | 296 | ||||
| -rw-r--r-- | lpd8806/src/LPD8806.h | 46 | ||||
| -rw-r--r-- | lpd8806/src/sketch.ino | 74 |
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 +} + |
