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]]));
  }
}

This guide was first published on Jun 05, 2013. It was last updated on Jun 05, 2013.

This page (Program it) was last updated on Jun 04, 2013.

Text editor powered by tinymce.