From d22622a4022d5c41a0b5bc69a78079b5c1feb3e5 Mon Sep 17 00:00:00 2001 From: Paint Your Dragon Date: Fri, 6 Jan 2012 23:36:12 -0800 Subject: [PATCH] LEDstream for LPD8806 now compatible with stock WS2801 version --- .../LEDstream_LPD8806/LEDstream_LPD8806.pde | 273 +++++++++--- .../Adalight_LPD8806/Adalight_LPD8806.pde | 414 ------------------ 2 files changed, 202 insertions(+), 485 deletions(-) delete mode 100644 Processing/Adalight_LPD8806/Adalight_LPD8806.pde diff --git a/Arduino/LEDstream_LPD8806/LEDstream_LPD8806.pde b/Arduino/LEDstream_LPD8806/LEDstream_LPD8806.pde index 5ceba50..1fc79f0 100644 --- a/Arduino/LEDstream_LPD8806/LEDstream_LPD8806.pde +++ b/Arduino/LEDstream_LPD8806/LEDstream_LPD8806.pde @@ -1,39 +1,65 @@ -// Arduino "bridge" code between host computer and LPD8806-based digital -// addressable RGB LEDs (e.g. Adafruit product ID #306). Intended for -// use with USB-native boards such as Teensy or Adafruit 32u4 Breakout; -// works on normal serial Arduinos, but throughput is severely limited. -// LED data is streamed, not buffered, making this suitable for larger -// installations (e.g. video wall, etc.) than could otherwise be held -// in the Arduino's limited RAM. - -// The LPD8806 latch condition is indicated through the data protocol, -// not through a pause in the data clock as with the WS2801. Buffer -// underruns are thus a non-issue and the code can be vastly simpler. -// Data is merely routed from serial in to SPI out. +// Arduino bridge code between host computer and LPD8806-based digital +// addressable RGB LEDs (e.g. Adafruit product ID #306). LED data is +// streamed, not buffered, making this suitable for larger installations +// (e.g. video wall, etc.) than could otherwise be contained within the +// Arduino's limited RAM. Intended for use with USB-native boards such +// as Teensy or Adafruit 32u4 Breakout; also works on normal serial +// Arduinos (Uno, etc.), but speed will be limited by the serial port. // LED data and clock lines are connected to the Arduino's SPI output. -// On traditional Arduino boards, SPI data out is digital pin 11 and -// clock is digital pin 13. On both Teensy and the 32u4 Breakout, -// data out is pin B2, clock is B1. LEDs should be externally -// powered -- trying to run any more than just a few off the Arduino's -// 5V line is generally a Bad Idea. LED ground should also be -// connected to Arduino ground. +// On traditional Arduino boards (e.g. Uno), SPI data out is digital pin +// 11 and clock is digital pin 13. On both Teensy and the 32u4 Breakout, +// data out is pin B2, clock is B1. On Arduino Mega, 51=data, 52=clock. +// LEDs should be externally powered -- trying to run any more than just +// a few off the Arduino's 5V line is generally a Bad Idea. LED ground +// should also be connected to Arduino ground. + +// Elsewhere, the WS2801 version of this code was specifically designed +// to avoid buffer underrun conditions...the WS2801 pixels automatically +// latch when the data stream stops for 500 microseconds or more, whether +// intentional or not. The LPD8806 pixels are fundamentally different -- +// the latch condition is indicated within the data stream, not by pausing +// the clock -- and buffer underruns are therefore a non-issue. In theory +// it would seem this could allow the code to be much simpler and faster +// (there's no need to sync up with a start-of-frame header), but in +// practice the difference was not as pronounced as expected -- such code +// soon ran up against a USB throughput limit anyway. So, rather than +// break compatibility in the quest for speed that will never materialize, +// this code instead follows the same header format as the WS2801 version. +// This allows the same host-side code (e.g. Adalight, Adavision, etc.) +// to run with either type of LED pixels. Huzzah! #include +// A 'magic word' precedes each block of LED data; this assists the +// microcontroller in syncing up with the host-side software and latching +// frames at the correct time. You may see an initial glitchy frame or +// two until the two come into alignment. Immediately following the +// magic word are three bytes: a 16-bit count of the number of LEDs (high +// byte first) followed by a simple checksum value (high byte XOR low byte +// XOR 0x55). LED data follows, 3 bytes per LED, in order R, G, B, where +// 0 = off and 255 = max brightness. LPD8806 pixels only have 7-bit +// brightness control, so each value is divided by two; the 8-bit format +// is used to maintain compatibility with the protocol set forth by the +// WS2801 streaming code (those LEDs use 8-bit values). +static const uint8_t magic[] = { 'A','d','a' }; +#define MAGICSIZE sizeof(magic) +#define HEADERSIZE (MAGICSIZE + 3) +static uint8_t + buffer[HEADERSIZE], // Serial input buffer + bytesBuffered = 0; // Amount of data in buffer + // If no serial data is received for a while, the LEDs are shut off // automatically. This avoids the annoying "stuck pixel" look when // quitting LED display programs on the host computer. static const unsigned long serialTimeout = 15000; // 15 seconds +static unsigned long lastByteTime, lastAckTime; void setup() { - int i, c; - unsigned long - lastByteTime, - lastAckTime, - t; + byte c; + int i, p; - Serial.begin(115200); // 32u4 ignores BPS, runs full speed + Serial.begin(115200); // 32u4 will ignore BPS and run full speed // SPI is run at 2 MHz. LPD8806 can run much faster, // but unshielded wiring is susceptible to interference. @@ -43,59 +69,164 @@ void setup() { SPI.setDataMode(SPI_MODE0); SPI.setClockDivider(SPI_CLOCK_DIV8); // 2 MHz + // Issue dummy byte to "prime" the SPI bus. This later simplifies + // the task of doing useful work during SPI transfers. Rather than + // the usual issue-and-wait-loop, code can instead wait-and-issue -- + // with other operations occurring between transfers, the wait is + // then shortened or eliminated. The SPSR register is read-only, + // so this flag can't be forced -- SOMETHING must be issued. + SPDR = 0; + + // Issue initial latch to LEDs. This flushes any undefined data that + // may exist on powerup, and prepares the LEDs to receive the first + // frame of data. Actual number of LEDs isn't known yet (this arrives + // later in frame header packets), so just latch a large number: + latch(10000); + // Issue test pattern to LEDs on startup. This helps verify that - // wiring between the Arduino and LEDs is correct. Not knowing the - // actual number of LEDs connected, this sets all of them (well, up - // to the first 25,000, so as not to be TOO time consuming) to green, - // red, blue, then off. Once you're confident everything is working - // end-to-end, it's OK to comment this out and reprogram the Arduino. - uint8_t testcolor[] = { 0x80, 0x80, 0x80, 0xff, 0x80, 0x80 }; - for(char n=3; n>=0; n--) { - for(c=0; c<25000; c++) { - for(i=0; i<3; i++) { - for(SPDR = testcolor[n + i]; !(SPSR & _BV(SPIF)); ); + // wiring between the Arduino and LEDs is correct. Again not knowing + // the actual number of LEDs, this writes data for an arbitrarily + // large number (10K). If wiring is correct, LEDs will all light + // red, green, blue on startup, then off. Once you're confident + // everything is working end-to-end, it's OK to comment this out and + // re-upload the sketch to the Arduino. + const uint8_t testColor[] = { 0x80, 0x80, 0xff, 0x80, 0x80, 0x80 }, + testOffset[] = { 1, 2, 0, 3 }; + for(c=0; c<4; c++) { // for each test sequence color... + for(p=0; p<10000; p++) { // for each pixel... + for(i=0; i<3; i++) { // for each R,G,B... + while(!(SPSR & _BV(SPIF))); // Wait for prior byte out + SPDR = testColor[testOffset[c] + i]; // Issue next byte } } - for(c=0; c<400; c++) { - for(SPDR=0; !(SPSR & _BV(SPIF)); ); - } + latch(10000); + if(c < 3) delay(250); } - Serial.print("Ada\n"); // Send ACK string to host - SPDR = 0; // Dummy byte out to "prime" the SPI status register - - lastByteTime = lastAckTime = millis(); - - // loop() is avoided as even that small bit of function overhead - // has a measurable impact on this code's overall throughput. - - for(;;) { - t = millis(); - if((c = Serial.read()) >= 0) { - while(!(SPSR & (1< 1000) { - Serial.print("Ada\n"); // Send ACK string to host - lastAckTime = t; // Reset counter - } - // If no data received for an extended time, turn off all LEDs. - if((t - lastByteTime) > serialTimeout) { - for(c=0; c<32767; c++) { - for(SPDR=0x80; !(SPSR & _BV(SPIF)); ); - } - for(c=0; c<512; c++) { - for(SPDR=0; !(SPSR & _BV(SPIF)); ); - } - lastByteTime = t; // Reset counter - } - } - } + Serial.print("Ada\n"); // Send ACK string to host + lastByteTime = lastAckTime = millis(); // Initialize timers } +// Program flow is simpler than the WS2801 code. No need for a state +// machine...instead, software just alternates between two conditions: +// a header-seeking mode (looking for the 'magic word' at the start +// of each frame of data), and a data-forwarding mode (moving bytes +// from serial input to SPI output). A proper data stream will +// consist only of alternating valid headers and valid data, so the +// loop() function is simply divided into these two parts, and repeats +// forever. + +// LPD8806 pixels expect colors in G,R,B order vs. WS2801's R,G,B. +// This is used to shuffle things around later. +static const uint8_t byteOrder[] = { 2, 0, 1 }; + void loop() { - // Not used. See note in setup() function. + uint8_t i, hi, lo, byteNum; + int c; + long nLEDs, remaining; + unsigned long t; + + // HEADER-SEEKING BLOCK: locate 'magic word' at start of frame. + + // If any data in serial buffer, shift it down to starting position. + for(i=0; i= 0) { // Data received? + buffer[bytesBuffered++] = c; // Store in buffer + lastByteTime = lastAckTime = t; // Reset timeout counters + } else { // No data, check for timeout... + if(timeout(t, 10000) == true) return; // Start over + } + } + + // Have a header's worth of data. Check for 'magic word' match. + for(i=0; i 0) + nLEDs = remaining = 256L * (long)hi + (long)lo + 1L; + bytesBuffered = 0; // Clear serial buffer + byteNum = 0; + + // DATA-FORWARDING BLOCK: move bytes from serial input to SPI output. + + // Unfortunately can't just forward bytes directly. The data order is + // different on LPD8806 (G,R,B), so bytes are buffered in groups of 3 + // and issued in the revised order. + + while(remaining > 0) { // While more LED data is expected... + t = millis(); + if((c = Serial.read()) >= 0) { // Successful read? + lastByteTime = lastAckTime = t; // Reset timeout counters + buffer[byteNum++] = c; // Store in data buffer + if(byteNum == 3) { // Have a full LED's worth? + while(byteNum > 0) { // Issue data in LPD8806 order... + i = 0x80 | (buffer[byteOrder[--byteNum]] >> 1); + while(!(SPSR & _BV(SPIF))); // Wait for prior byte out + SPDR = i; // Issue new byte + } + remaining--; + } + } else { // No data, check for timeout... + if(timeout(t, nLEDs) == true) return; // Start over + } + } + + // Normal end of data. Issue latch, return to header-seeking mode. + latch(nLEDs); } + +static void latch(int n) { // Pass # of LEDs + n = ((n + 63) / 64) * 3; // Convert to latch length (bytes) + while(n--) { // For each latch byte... + while(!(SPSR & _BV(SPIF))); // Wait for prior byte out + SPDR = 0; // Issue next byte + } +} + +// Function is called when no pending serial data is available. +static boolean timeout( + unsigned long t, // Current time, milliseconds + int nLEDs) { // Number of LEDs + + // If condition persists, send an ACK packet to host once every + // second to alert it to our presence. + if((t - lastAckTime) > 1000) { + Serial.print("Ada\n"); // Send ACK string to host + lastAckTime = t; // Reset counter + } + + // If no data received for an extended time, turn off all LEDs. + if((t - lastByteTime) > serialTimeout) { + long bytes = nLEDs * 3L; + latch(nLEDs); // Latch any partial/incomplete data in strand + while(bytes--) { // Issue all new data to turn off strand + while(!(SPSR & _BV(SPIF))); // Wait for prior byte out + SPDR = 0x80; // Issue next byte (0x80 = LED off) + } + latch(nLEDs); // Latch 'all off' data + lastByteTime = t; // Reset counter + bytesBuffered = 0; // Clear serial buffer + return true; + } + + return false; // No timeout +} + diff --git a/Processing/Adalight_LPD8806/Adalight_LPD8806.pde b/Processing/Adalight_LPD8806/Adalight_LPD8806.pde deleted file mode 100644 index b24aefe..0000000 --- a/Processing/Adalight_LPD8806/Adalight_LPD8806.pde +++ /dev/null @@ -1,414 +0,0 @@ -// "Adalight" is a do-it-yourself facsimile of the Philips Ambilight concept -// for desktop computers and home theater PCs. This is the host PC-side code -// written in Processing, intended for use with a USB-connected Arduino -// microcontroller running the accompanying LPD8806 (NOT WS2801) LED -// streaming code. Requires one or more strips of Digital Addressable RGB -// LEDs (Adafruit product ID #306, and a 5 Volt power supply (such as -// Adafruit #276). You may need to adapt the code and the hardware -// arrangement for your specific display configuration. -// Screen capture adapted from code by Cedrik Kiefer (processing.org forum) - -import java.awt.*; -import java.awt.image.*; -import processing.serial.*; - -// CONFIGURABLE PROGRAM CONSTANTS -------------------------------------------- - -// Minimum LED brightness; some users prefer a small amount of backlighting -// at all times, regardless of screen content. Higher values are brighter, -// or set to 0 to disable this feature. - -static final short minBrightness = 120; - -// LED transition speed; it's sometimes distracting if LEDs instantaneously -// track screen contents (such as during bright flashing sequences), so this -// feature enables a gradual fade to each new LED state. Higher numbers yield -// slower transitions (max of 255), or set to 0 to disable this feature -// (immediate transition of all LEDs). - -static final short fade = 75; - -// Pixel size for the live preview image. - -static final int pixelSize = 20; - -// Depending on many factors, it may be faster either to capture full -// screens and process only the pixels needed, or to capture multiple -// smaller sub-blocks bounding each region to be processed. Try both, -// look at the reported frame rates in the Processing output console, -// and run with whichever works best for you. - -static final boolean useFullScreenCaps = true; - -// Serial device timeout (in milliseconds), for locating Arduino device -// running the corresponding LEDstream code. See notes later in the code... -// in some situations you may want to entirely comment out that block. - -static final int timeout = 5000; // 5 seconds - -// PER-DISPLAY INFORMATION --------------------------------------------------- - -// This array contains details for each display that the software will -// process. If you have screen(s) attached that are not among those being -// "Adalighted," they should not be in this list. Each triplet in this -// array represents one display. The first number is the system screen -// number...typically the "primary" display on most systems is identified -// as screen #1, but since arrays are indexed from zero, use 0 to indicate -// the first screen, 1 to indicate the second screen, and so forth. This -// is the ONLY place system screen numbers are used...ANY subsequent -// references to displays are an index into this list, NOT necessarily the -// same as the system screen number. For example, if you have a three- -// screen setup and are illuminating only the third display, use '2' for -// the screen number here...and then, in subsequent section, '0' will be -// used to refer to the first/only display in this list. -// The second and third numbers of each triplet represent the width and -// height of a grid of LED pixels attached to the perimeter of this display. -// For example, '9,6' = 9 LEDs across, 6 LEDs down. - -static final int displays[][] = new int[][] { - {0,12,6} // Screen 0, 12 LEDs across, 6 LEDs down -//,{1,12,6} // Screen 1, also 12 LEDs across and 6 LEDs down -}; - -// PER-LED INFORMATION ------------------------------------------------------- - -// This array contains the 2D coordinates corresponding to each pixel in the -// LED strand, in the order that they're connected (i.e. the first element -// here belongs to the first LED in the strand, second element is the second -// LED, and so forth). Each triplet in this array consists of a display -// number (an index into the display array above, NOT necessarily the same as -// the system screen number) and an X and Y coordinate specified in the grid -// units given for that display. {0,0,0} is the top-left corner of the first -// display in the array. -// For our example purposes, the coordinate list below forms a ring around -// the perimeter of a single screen, with a one pixel gap at the bottom to -// accommodate a monitor stand. Modify this to match your own setup: - -static final int leds[][] = new int[][] { - {0, 5,5}, {0, 4,5}, {0, 3,5}, {0, 2,5}, {0, 1,5}, {0, 0,5}, // Bottom edge, left half - {0, 0,4}, {0, 0,3}, {0, 0,2}, {0, 0,1}, // Left edge - {0, 0,0}, {0, 1,0}, {0, 2,0}, {0, 3,0}, {0, 4,0}, {0, 5,0}, // Top edge, left half - {0, 6,0}, {0, 7,0}, {0, 8,0}, {0, 9,0}, {0,10,0}, {0,11,0}, // Top edge, right half - {0,11,1}, {0,11,2}, {0,11,3}, {0,11,4}, // Right edge - {0,11,5}, {0,10,5}, {0, 9,5}, {0, 8,5}, {0, 7,5}, {0, 6,5}, // Bottom edge, right half - -/* Hypothetical second display has the same arrangement as the first. - But you might not want both displays completely ringed with LEDs; - the screens might be positioned where they share an edge in common. -, {1, 5,5}, {1, 4,5}, {1, 3,5}, {1, 2,5}, {1, 1,5}, {1, 0,5}, // Bottom edge, left half - {1, 0,4}, {1, 0,3}, {1, 0,2}, {1, 0,1}, // Left edge - {1, 0,0}, {1, 1,0}, {1, 2,0}, {1, 3,0}, {1, 4,0}, {1, 5,0}, // Top edge, left half - {1, 6,0}, {1, 7,0}, {1, 8,0}, {1, 9,0}, {1,10,0}, {1,11,0}, // Top edge, right half - {1,11,1}, {1,11,2}, {1,11,3}, {1,11,4}, // Right edge - {1,11,5}, {1,10,5}, {1, 9,5}, {1, 8,5}, {1, 7,5}, {1, 6,5}, // Bottom edge, right half -*/ -}; - -// GLOBAL VARIABLES ---- You probably won't need to modify any of this ------- - -static final int latchLen = (leds.length + 63) / 64; -byte[] serialData = new byte[(leds.length + latchLen) * 3]; -short[][] ledColor = new short[leds.length][3], - prevColor = new short[leds.length][3]; -byte[][] gamma = new byte[256][3]; -int nDisplays = displays.length; -Robot[] bot = new Robot[displays.length]; -Rectangle[] dispBounds = new Rectangle[displays.length], - ledBounds; // Alloc'd only if per-LED captures -int[][] pixelOffset = new int[leds.length][256], - screenData; // Alloc'd only if full-screen captures -PImage[] preview = new PImage[displays.length]; -Serial port; -DisposeHandler dh; // For disabling LEDs on exit - -// INITIALIZATION ------------------------------------------------------------ - -void setup() { - GraphicsEnvironment ge; - GraphicsConfiguration[] gc; - GraphicsDevice[] gd; - int d, i, totalWidth, maxHeight, row, col, rowOffset; - int[] x = new int[16], y = new int[16]; - float f, range, step, start; - - dh = new DisposeHandler(this); // Init DisposeHandler ASAP - - // Open serial port. As written here, this assumes the Arduino is the - // first/only serial device on the system. If that's not the case, - // change "Serial.list()[0]" to the name of the port to be used: - port = new Serial(this, Serial.list()[0], 115200); - // Alternately, in certain situations the following line can be used - // to detect the Arduino automatically. But this works ONLY with SOME - // Arduino boards and versions of Processing! This is so convoluted - // to explain, it's easier just to test it yourself and see whether - // it works...if not, leave it commented out and use the prior port- - // opening technique. - // port = openPort(); - // And finally, to test the software alone without an Arduino connected, - // don't open a port...just comment out the serial lines above. - - // Initialize screen capture code for each display's dimensions. - dispBounds = new Rectangle[displays.length]; - if(useFullScreenCaps == true) { - screenData = new int[displays.length][]; - // ledBounds[] not used - } else { - ledBounds = new Rectangle[leds.length]; - // screenData[][] not used - } - ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); - gd = ge.getScreenDevices(); - if(nDisplays > gd.length) nDisplays = gd.length; - totalWidth = maxHeight = 0; - for(d=0; d 0) totalWidth++; - if(displays[d][2] > maxHeight) maxHeight = displays[d][2]; - } - - // Precompute locations of every pixel to read when downsampling. - // Saves a bunch of math on each frame, at the expense of a chunk - // of RAM. Number of samples is now fixed at 256; this allows for - // some crazy optimizations in the downsampling code. - for(i=0; i= 4) && - ((ack = s.readString()) != null) && - ack.contains("Ada\n")) { - return s; // Got it! - } - } - // Connection timed out. Close port and move on to the next. - s.stop(); - } - - // Didn't locate a device returning the acknowledgment string. - // Maybe it's out there but running the old LEDstream code, which - // didn't have the ACK. Can't say for sure, so we'll take our - // changes with the first/only serial device out there... - return new Serial(this, ports[0], 115200); -} - - -// PER-FRAME PROCESSING ------------------------------------------------------ - -void draw () { - BufferedImage img; - int d, i, j, o, c, weight, rb, g, sum, deficit, s2; - int[] pxls, offs; - - if(useFullScreenCaps == true ) { - // Capture each screen in the displays array. - for(d=0; d> 24) & 0xff) * weight + - prevColor[i][0] * fade) >> 8); - ledColor[i][1] = (short)(((( g >> 16) & 0xff) * weight + - prevColor[i][1] * fade) >> 8); - ledColor[i][2] = (short)((((rb >> 8) & 0xff) * weight + - prevColor[i][2] * fade) >> 8); - - // Boost pixels that fall below the minimum brightness - sum = ledColor[i][0] + ledColor[i][1] + ledColor[i][2]; - if(sum < minBrightness) { - if(sum == 0) { // To avoid divide-by-zero - deficit = minBrightness / 3; // Spread equally to R,G,B - ledColor[i][0] += deficit; - ledColor[i][1] += deficit; - ledColor[i][2] += deficit; - } else { - deficit = minBrightness - sum; - s2 = sum * 2; - // Spread the "brightness deficit" back into R,G,B in proportion to - // their individual contribition to that deficit. Rather than simply - // boosting all pixels at the low end, this allows deep (but saturated) - // colors to stay saturated...they don't "pink out." - ledColor[i][0] += deficit * (sum - ledColor[i][0]) / s2; - ledColor[i][1] += deficit * (sum - ledColor[i][1]) / s2; - ledColor[i][2] += deficit * (sum - ledColor[i][2]) / s2; - } - } - - // Apply gamma curve and place in serial output buffer - serialData[j++] = gamma[ledColor[i][1]][1]; // G - serialData[j++] = gamma[ledColor[i][0]][0]; // R - serialData[j++] = gamma[ledColor[i][2]][2]; // B - // Update pixels in preview image - preview[d].pixels[leds[i][2] * displays[d][1] + leds[i][1]] = - (ledColor[i][0] << 16) | (ledColor[i][1] << 8) | ledColor[i][2]; - } - - if(port != null) { - port.write(serialData); // Issue data to Arduino - // You *might* need to comment out the above line and use - // the following code instead. Long writes fail for some - // unknown reason. RXTX lib? Processing? Java? OS? Hardware? -// for(i=0; i serialData.length) j = serialData.length; -// port.write(Arrays.copyOfRange(serialData,i,j)); -// } - } - - // Show live preview image(s) - scale(pixelSize); - for(i=d=0; d