After checking your conductive thread circuit for shorts, upload the NeoPixel test code (refer to the FLORA NeoPixel tutorial if necessary). If any of your connections are flaky, reinforce them with conductive thread. Once all your pixels are working flawlessly, load the code below, which will make a chasing pac-man-inspired animation appear across the suspenders.
// "Retro gaming" suspenders using Adafruit NeoPixel LEDs.
// by Phillip Burgess for Adafruit Industries
#include <Adafruit_NeoPixel.h>
#define LED_PIN 6
#define N_LEDS 30
#define N_SPRITES 5 // 4 ghosts + 1 mouth
#define PILL_INDEX (N_LEDS - 5) // Position of 'power pill' along strip
#define PILL_R 255 // Power pill is slightly pink-ish,
#define PILL_G 184 // not pure white.
#define PILL_B 151
#define DOT_R (PILL_R / 3) // Dots are same hue as power pill,
#define DOT_G (PILL_G / 3) // just dimmer.
#define DOT_B (PILL_B / 3)
#define DOT_FADE_SPEED 2 // New screenful of dots fades in this fast
uint8_t gamma[] PROGMEM = { // Gamma correction table for LED brightness
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x01,0x01,
0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x02,0x02,0x02,0x02,0x02,0x02,0x02,
0x02,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x04,0x04,0x04,0x04,0x04,0x05,0x05,0x05,
0x05,0x06,0x06,0x06,0x06,0x07,0x07,0x07,0x07,0x08,0x08,0x08,0x09,0x09,0x09,0x0A,
0x0A,0x0A,0x0B,0x0B,0x0B,0x0C,0x0C,0x0D,0x0D,0x0D,0x0E,0x0E,0x0F,0x0F,0x10,0x10,
0x11,0x11,0x12,0x12,0x13,0x13,0x14,0x14,0x15,0x15,0x16,0x16,0x17,0x18,0x18,0x19,
0x19,0x1A,0x1B,0x1B,0x1C,0x1D,0x1D,0x1E,0x1F,0x20,0x20,0x21,0x22,0x23,0x23,0x24,
0x25,0x26,0x27,0x27,0x28,0x29,0x2A,0x2B,0x2C,0x2D,0x2E,0x2F,0x30,0x31,0x32,0x32,
0x33,0x34,0x36,0x37,0x38,0x39,0x3A,0x3B,0x3C,0x3D,0x3E,0x3F,0x40,0x42,0x43,0x44,
0x45,0x46,0x48,0x49,0x4A,0x4B,0x4D,0x4E,0x4F,0x51,0x52,0x53,0x55,0x56,0x57,0x59,
0x5A,0x5C,0x5D,0x5F,0x60,0x62,0x63,0x65,0x66,0x68,0x69,0x6B,0x6D,0x6E,0x70,0x72,
0x73,0x75,0x77,0x78,0x7A,0x7C,0x7E,0x7F,0x81,0x83,0x85,0x87,0x89,0x8A,0x8C,0x8E,
0x90,0x92,0x94,0x96,0x98,0x9A,0x9C,0x9E,0xA0,0xA2,0xA4,0xA7,0xA9,0xAB,0xAD,0xAF,
0xB1,0xB4,0xB6,0xB8,0xBA,0xBD,0xBF,0xC1,0xC4,0xC6,0xC8,0xCB,0xCD,0xD0,0xD2,0xD5,
0xD7,0xDA,0xDC,0xDF,0xE1,0xE4,0xE7,0xE9,0xEC,0xEF,0xF1,0xF4,0xF7,0xF9,0xFC,0xFF };
Adafruit_NeoPixel strip = Adafruit_NeoPixel(N_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800);
// 100% of animation logic is in a timer interrupt, hence all globals are volatile.
// Lots of sub-pixel smoothing shenanigans are going on in an attempt to better
// translate the smooth animation of the original game into this coarse 1D format.
// Image is buffered independently of the NeoPixel color data, to allow for
// blending, gamma correction and horizontal flip on alternate passes.
volatile uint32_t pixel[N_LEDS][3];
// Game character sprites are always 1 pixel wide here to simplify certain code.
// Horizontal coordinates/speeds are in 1/256 pixel units.
volatile struct {
int16_t x; // Current position
int16_t prevX; // Position in prior frame
int8_t speed;
int16_t x1, x2; // Span covered by prior-to-current movement
uint8_t r, g, b, a; // Color & opacity
uint8_t moveDuringPause; // If set, sprite is immune to animation pauses
} sprite[N_SPRITES];
#define SPRITE_RED 0 // Ghost indices
#define SPRITE_PINK 1
#define SPRITE_CYAN 2
#define SPRITE_ORANGE 3
#define SPRITE_MOUTH 4 // Mouth index
volatile uint8_t
dotState[N_LEDS], // 1 = dot enabled, 0 = eaten
pillBlink = 0, // Power pill blink counter
pauseCounter = 0, // Counter for 'chomp-a-ghost' pause
dotFade, // Counter as dots fade into view
flip = 1; // If bit 1 set, flip playfield
volatile uint16_t
ghostTime = 0; // Countdown timer for blue ghosts
//----------------------------------------------------------------------------------
void setup() {
memset((void *)pixel, 0, sizeof(pixel)); // Clear screen buffer
memset((void *)dotState, 0, sizeof(dotState)); // Set all dots to 'off'
reset_animation(); // Init all other values
strip.begin();
// Set up Timer1 interrupt: Mode 14 (fast PWM, top=ICR1), 1:8 prescale, OC1A/B off
TCCR1A = _BV(WGM11);
TCCR1B = _BV(WGM13) | _BV(WGM12) | _BV(CS11);
ICR1 = F_CPU / 8 / 64; // 64 Hz
TIMSK1 |= _BV(TOIE1); // Enable Timer1 overflow interrupt
sei(); // Enable global interrupts
}
//----------------------------------------------------------------------------------
// This is called once at program start, and periodically each time the animation
// needs to reset. The playfield is flipped horizontally on each call, for variety.
void reset_animation() {
uint8_t i, j, temp;
sprite[SPRITE_RED].r = 255; // Red ghost color
sprite[SPRITE_RED].g = 0;
sprite[SPRITE_RED].b = 0;
sprite[SPRITE_PINK].r = 255; // Pink ghost color
sprite[SPRITE_PINK].g = 184;
sprite[SPRITE_PINK].b = 222;
sprite[SPRITE_CYAN].r = 0; // Cyan ghost color
sprite[SPRITE_CYAN].g = 255;
sprite[SPRITE_CYAN].b = 222;
sprite[SPRITE_ORANGE].r = 255; // Orange ghost color
sprite[SPRITE_ORANGE].g = 184;
sprite[SPRITE_ORANGE].b = 71;
sprite[SPRITE_MOUTH].r = 255; // Mouth color
sprite[SPRITE_MOUTH].g = 255;
sprite[SPRITE_MOUTH].b = 0;
// Ghost positions aren't perfectly spaced; they're a bit scrambled for
// variety. Mouth starts off left side, ghosts are further left of that.
sprite[SPRITE_MOUTH].x = -400; // Mouth position
sprite[SPRITE_RED].x = sprite[SPRITE_MOUTH].x - 800; // Red ghost position
sprite[SPRITE_PINK].x = sprite[SPRITE_RED].x - 440; // Pink ghost position
sprite[SPRITE_CYAN].x = sprite[SPRITE_PINK].x - 500; // Cyan ghost position
sprite[SPRITE_ORANGE].x = sprite[SPRITE_CYAN].x - 530; // Orange ghost position
for(i=0; i<N_SPRITES; i++) {
sprite[i].prevX = sprite[i].x;
sprite[i].moveDuringPause = false;
sprite[i].a = 255; // Opaque
sprite[i].speed = 14;
}
sprite[4].speed = 13; // Mouth moves a little slower when eating dots
flip++; // Reverse layout of display
// Flip residual screen dots to match new layout
for(i=0, j=N_LEDS-1; i<N_LEDS/2; i++, j--) {
temp = dotState[i];
dotState[i] = dotState[j];
dotState[j] = temp;
}
dotFade = 0; // Fade in new screenload of dots
}
//----------------------------------------------------------------------------------
// Nothing happens in loop() -- everything is in the interrupt handler below.
void loop() { }
//----------------------------------------------------------------------------------
// Timer1 overflow interrupt handler
ISR(TIMER1_OVF_vect) {
uint8_t i;
uint16_t fracX, weightL, weightR, inv, w;
int8_t x1, x2;
// First thing is to push data for the PRIOR frame to the strip. This is done
// so the animation timing will be rock steady. Some frames may take slightly
// different times to process than others, and putting the show() at the end
// would cause a slightly different refresh time for each.
strip.show();
// If new screenload of dots is fading into view, that's the only processing
// that occurs -- no sprite animation or collision detection, etc. happens.
if(dotFade < 255) {
for(i=0; i<N_LEDS; i++) { // For each dot...
if(dotState[i] == 1) { // Already something here?
pixel[i][0] = DOT_R; // Yep, show uneaten 'on' dot.
pixel[i][1] = DOT_G;
pixel[i][2] = DOT_B;
} else { // Fade new dot into view
if(i == PILL_INDEX) { // Is it the power pill?
if(pillBlink & 16) { // Blinking 'on' right now?
w = (uint16_t)dotFade + 1; // Blend factor
pixel[i][0] = (PILL_R * w) >> 8;
pixel[i][1] = (PILL_G * w) >> 8;
pixel[i][2] = (PILL_B * w) >> 8;
} else {
pixel[i][0] = pixel[i][1] = pixel[i][2] = 0; // Blink off
}
} else { // Regular dot, not power pill
w = (uint16_t)dotFade + 1; // Blend factor
pixel[i][0] = (DOT_R * w) >> 8;
pixel[i][1] = (DOT_G * w) >> 8;
pixel[i][2] = (DOT_B * w) >> 8;
}
}
}
// Increment dot fade counter
if(dotFade >= (255 - DOT_FADE_SPEED)) { // About to hit/exceed max?
dotFade = 255; // Set to max value, we're done
for(i=0; i<N_LEDS; i++) dotState[i] = 1; // Make all dots edible
} else {
dotFade += DOT_FADE_SPEED; // Not maxed, keep counting up
}
} else {
//------------------------------------------------------------------------------
// Dots are 'active,' not fading into view. Do the full 'game' logic...
// Update sprite positions
for(i=0; i<N_SPRITES; i++) {
sprite[i].prevX = sprite[i].x;
if((pauseCounter == 0) || sprite[i].moveDuringPause) {
sprite[i].x += sprite[i].speed;
}
}
if(pauseCounter) { // Is animation paused? (During a ghost chomp)
if(--pauseCounter == 0) { // Count down to resume. Done?
sprite[SPRITE_MOUTH].a = 255; // Mouth = opaque
for(i=0; i<4; i++) { // Set non-opaque ghosts (being chomped) to eye color
if(sprite[i].a == 255) continue; // Is opaque - don't change
sprite[i].r = sprite[i].g = sprite[i].b = 255; // White
sprite[i].a = 96; // Mostly translucent
sprite[i].speed = 48; // Eyes move fast
sprite[i].moveDuringPause = 1; // Eyes move when others are paused
}
}
} else { // Animation isn't paused...do collision tests...
// Calc X extents of each sprite since prior frame
for(i=0; i<N_SPRITES; i++) {
if(sprite[i].x >= sprite[i].prevX) {
sprite[i].x1 = sprite[i].prevX;
sprite[i].x2 = sprite[i].x;
} else {
sprite[i].x1 = sprite[i].x;
sprite[i].x2 = sprite[i].prevX;
}
}
// Check mouth motion against dots, eating any that are crossed
x1 = sprite[SPRITE_MOUTH].x1 >> 8;
x2 = sprite[SPRITE_MOUTH].x2 >> 8;
if((x1 >= 0) && (x2 < N_LEDS)) {
if(x1 < 0) x1 = 0;
if(x2 >= N_LEDS) x2 = N_LEDS - 1;
if(dotState[PILL_INDEX]) { // Is power pill uneaten?
if((x1 <= PILL_INDEX) && (x2 >= PILL_INDEX)) { // Eating it now?
// Switch to 'chasing ghosts' mode. Turn around, a little faster
sprite[SPRITE_MOUTH].speed = -14;
for(i=0;i<4;i++) {
sprite[i].speed = -10; // Turn around, slow down
sprite[i].r = sprite[i].g = 33; // Blue ghost
sprite[i].b = 255;
}
ghostTime = 64 * 10; // Countdown timer
}
}
for(i=x1; i<=x2; i++) dotState[i] = 0; // Erase eaten dot(s)
}
// If in ghost-chasing mode, check mouth collision against eligible ghosts
if(ghostTime) {
if(--ghostTime == 0) {
reset_animation();
} else {
for(i=0; i<4; i++) {
if(sprite[i].a != 255) continue; // Don't compare against ghost eyes
if((sprite[SPRITE_MOUTH].x1 <= sprite[i].x2) &&
(sprite[SPRITE_MOUTH].x2 >= sprite[i].x1)) { // Ate a ghost!
pauseCounter = 48; // Make animation pause (except eyes)
sprite[SPRITE_MOUTH].a = 0; // Mouth disappears momentarily
sprite[i].r = 0;
sprite[i].g = sprite[i].b = 255;
sprite[i].a = 220; // Ghost is replaced with 'point' display
}
}
if(ghostTime < (64 * 6)) { // Make ghosts blink blue/white toward end
for(i=0; i<4; i++) {
if(sprite[i].a != 255) continue; // Ghost is in eye state; ignore
if(ghostTime & 32) {
sprite[i].r = sprite[i].g = 222;
} else {
sprite[i].r = sprite[i].g = 33;
}
}
}
}
}
}
// Draw background dots into pixel[] array
for(i=0; i<N_LEDS; i++) {
if(dotState[i] == 1) {
if(i == PILL_INDEX) {
if(pillBlink & 16) {
pixel[i][0] = PILL_R;
pixel[i][1] = PILL_G;
pixel[i][2] = PILL_B;
} else {
pixel[i][0] = pixel[i][1] = pixel[i][2] = 0;
}
} else {
pixel[i][0] = DOT_R;
pixel[i][1] = DOT_G;
pixel[i][2] = DOT_B;
}
} else {
pixel[i][0] = 0;
pixel[i][1] = 0;
pixel[i][2] = 0;
}
}
// Overlay sprites
for(i=0; i<N_SPRITES; i++) {
x1 = sprite[i].x >> 8; // Left pixel
x2 = x1 + 1; // Right pixel
if((x2 >= 0) && (x1 < N_LEDS)) { // Gross clipping
fracX = sprite[i].x & 255; // Sub-pixel position (0-255)
weightL = 1 + ((256 - (uint16_t)fracX) * sprite[i].a) >> 8; // Left pixel weight (1-256)
weightR = 1 + (( 1 + (uint16_t)fracX) * sprite[i].a) >> 8; // Right pixel weight (1-256)
if(x1 >= 0) { // Process left pixel
inv = 257 - weightL; // 1-256
pixel[x1][0] = ((sprite[i].r * weightL) + (pixel[x1][0] * inv)) >> 8;
pixel[x1][1] = ((sprite[i].g * weightL) + (pixel[x1][1] * inv)) >> 8;
pixel[x1][2] = ((sprite[i].b * weightL) + (pixel[x1][2] * inv)) >> 8;
}
if(x2 < N_LEDS) { // Process right pixel
inv = 257 - weightR; // 1-256
pixel[x2][0] = ((sprite[i].r * weightR) + (pixel[x2][0] * inv)) >> 8;
pixel[x2][1] = ((sprite[i].g * weightR) + (pixel[x2][1] * inv)) >> 8;
pixel[x2][2] = ((sprite[i].b * weightR) + (pixel[x2][2] * inv)) >> 8;
}
}
}
}
pillBlink++;
// Apply gamma correction / flip to pixel data while passing to NeoPixel lib...
for(i=0; i<N_LEDS; i++) {
x1 = (flip & 1) ? N_LEDS - 1 - i : i;
strip.setPixelColor(i,
pgm_read_byte(&gamma[pixel[x1][0]]),
pgm_read_byte(&gamma[pixel[x1][1]]),
pgm_read_byte(&gamma[pixel[x1][2]]));
}
}
Page last edited June 04, 2013
Text editor powered by tinymce.