Archive for November 13th, 2015

Hard Drive Platter Mood Light: Neopixel Firmware

Having accumulated a pile of useless hard drives, it seemed reasonable to harvest the platters and turn them into techie mood lights (remember mood lights?). Some doodling showed that four of Adafruit’s high-density Neopixel strips could stand up inside the 25 mm central hole, completely eliminating the need to putz around with PWM drivers and RGB LEDs: one wire from an Arduino Pro Mini and you’re done:

const byte PIN_NEO = 6;				// DO - data out to first Neopixel

The firmware creates three sine waves with mutually prime periods, then updates the RGB channels with raised-sine values every 10 ms. The PdBase constant defines the common conversion from milliseconds to radians:

const float PdBase = 0.05 * TWO_PI / 1000.0;	// scale time in ms to radians

The leading 0.05 = 1/20 means the sine wave will repeat every 20 s = 20000 ms.

Dividing that period by three small primes produces an RGB pattern that will repeat every 5x11x17 = 935 PdBase cycles = 18.7×103 s = 5.19 h:

const float Period[] = {PdBase/5.0,PdBase/11.0,PdBase/17.0};		// mutually prime periods

That’s languid enough for me, although I admit most of the colors look pretty much the same. Obviously, you can tune for best picture by dinking with a few constants.

A Phase array sets the starting phase to 3π/2 = -90 degrees:

float Phase[] = {3.0 * HALF_PI,3.0 * HALF_PI,3.0 * HALF_PI};		// sin(3π/2 ) = -1, so LEDs are off

Jiggling those starting phases produces a randomized initial color that’s close to dark:

	MillisNow = MillisThen = millis();
	randomSeed(MillisNow + analogRead(6) + analogRead(7));
	printf("Phases: ");
	for (byte i=0; i<3; i++) {
		Phase[i] += random(-1000,1000) * HALF_PI / 1000.0;
		printf("%d ",(int)(Phase[i]*RAD_TO_DEG));
	}
	printf(" deg\r\n");

With all that in hand, converting from time to color goes like this:

uint32_t SineColor(unsigned long t) {
byte rgb[3];

	for (byte i=0; i<3; i++) {
			rgb[i] = Intensity[i]/2.0 * (1 + sin(t * Period[i] + Phase[i]));
	}
	return strip.Color(rgb[0],rgb[1],rgb[2]);
}

The rest of the code scales neatly with the strip length defined in the magic instantiation:

Adafruit_NeoPixel strip = Adafruit_NeoPixel(12, PIN_NEO, NEO_GRB + NEO_KHZ800);

Although the colors change very slowly, shifting them all one chip toward the end of the 144 Neopixel strip at each update produces a noticeable difference that reassured me this whole mess was working:

		for (int i=strip.numPixels()-1; i>0; i--) {
			c = strip.getPixelColor(i-1);
			strip.setPixelColor(i,c);
		}

		c = SineColor(MillisNow);
		strip.setPixelColor(0,c);
		strip.show();

And with that in hand, It Just Worked…

However, it’s worth noting that each Neopixel draws a bit over 60 mA at full white, which works out to a smidge under 9 A for a 144 LED strip. Because they’re PWM devices, the LEDs are either full-on or full-off, so the peak current can actually be 9 A, regardless of any reduced duty cycle to limit the intensity.

The Adafruit driver includes an overall intensity control, but I added an Intensity array with separate values for each channel:

float Intensity[] = {128.0,128.0,128.0};							// pseudo current limit - PWM is always full current

That would allow throttling back the blue LEDs a bit to adjust the overall color temperature, but that’s definitely in the nature of fine tuning.

The Adafruit Neopixel guide recommends a honkin’ big cap right at the strip, plus a 470 Ω decoupling resistor at the first chip’s data input. I think those attempt to tamp down the problems caused by underpowered supplies and crappy wiring; running it at half intensity produced a maximum average current just under the supply’s 3 A limit.

The complete Arduino source code:

// Neopixel mood lighting for hard drive platter sculpture
// Ed Nisley - KE4ANU - November 2015

#include <Adafruit_NeoPixel.h>

//----------
// Pin assignments

const byte PIN_NEO = 6;				// DO - data out to first Neopixel

const byte PIN_HEARTBEAT = 13;		// DO - Arduino LED

//----------
// Constants

const int UPDATEMS = 10ul - 4ul;		// update LEDs only this many ms apart minus loop() overhead

const float PdBase = 0.05 * TWO_PI / 1000.0;	// scale time in ms to radians

const float Period[] = {PdBase/5.0,PdBase/11.0,PdBase/17.0};		// mutually prime periods
float Phase[] = {3.0 * HALF_PI,3.0 * HALF_PI,3.0 * HALF_PI};		// sin(3π/2 ) = -1, so LEDs are off
float Intensity[] = {128.0,128.0,128.0};							// pseudo current limit - PWM is always full current

//----------
// Globals

unsigned long MillisNow;
unsigned long MillisThen;

Adafruit_NeoPixel strip = Adafruit_NeoPixel(12, PIN_NEO, NEO_GRB + NEO_KHZ800);

uint32_t FullWhite = strip.Color(255,255,255);
uint32_t FullOff = strip.Color(0,0,0);

//--- figure color from time in ms

uint32_t SineColor(unsigned long t) {
byte rgb[3];

	for (byte i=0; i<3; i++) {
			rgb[i] = Intensity[i]/2.0 * (1 + sin(t * Period[i] + Phase[i]));
	}
	return strip.Color(rgb[0],rgb[1],rgb[2]);
}

//-- Helper routine for printf()

int s_putc(char c, FILE *t) {
  Serial.write(c);
}

//------------------
// Set the mood

void setup() {
	
uint32_t c;

	pinMode(PIN_HEARTBEAT,OUTPUT);
	digitalWrite(PIN_HEARTBEAT,LOW);	// show we arrived

	Serial.begin(57600);
	fdevopen(&s_putc,0);				// set up serial output for printf()

	printf("Mood Light with Neopixels\r\nEd Nisley - KE4ZNU - November 2015\r\n");
	
/// set up Neopixels
	
	strip.begin();
	strip.show();
	
// lamp test: run a brilliant white dot along the length of the strip
	
	printf("Lamp test: walking white\r\n");
	
	strip.setPixelColor(0,FullWhite);
	strip.show();
	delay(500);
	
	for (int i=1; i<strip.numPixels(); i++) {
		digitalWrite(PIN_HEARTBEAT,HIGH);
		strip.setPixelColor(i-1,FullOff);
		strip.setPixelColor(i,FullWhite);
		strip.show();
		digitalWrite(PIN_HEARTBEAT,LOW);
		delay(500);
	}
	
	MillisNow = MillisThen = millis();
	randomSeed(MillisNow + analogRead(6) + analogRead(7));
	printf("Phases: ");
	for (byte i=0; i<3; i++) {
		Phase[i] += random(-1000,1000) * HALF_PI / 1000.0;
		printf("%d ",(int)(Phase[i]*RAD_TO_DEG));
	}
	printf(" deg\r\n");
	
	c = SineColor(MillisNow);
	printf("Initial time: %08lx -> color: %08lx\r\n",MillisNow,c);
	
	for (int i=0; i<strip.numPixels()-1; i++) {
		strip.setPixelColor(i,c);
	}
	
	strip.show();
	
}

//------------------
// Run the mood

void loop() {
	
byte r,g,b;
uint32_t c;

	MillisNow = millis();
	if ((MillisNow - MillisThen) > UPDATEMS) {
		digitalWrite(PIN_HEARTBEAT,HIGH);
		
		for (int i=strip.numPixels()-1; i>0; i--) {
			c = strip.getPixelColor(i-1);
			strip.setPixelColor(i,c);
		}

		c = SineColor(MillisNow);
		strip.setPixelColor(0,c);
		strip.show();

		MillisThen = MillisNow;
		digitalWrite(PIN_HEARTBEAT,LOW);
	}
}

12 Comments