8x8x8 LED Cube

For our embedded systems design project, we built an 8x8x8 LED Cube with a playable game of 3-Dimensional "Snake" using the mbed microcontroller.

About

In case you haven't yet done the math, an 8x8x8 LED cube consists of 512 indidual LEDs, all soldered together in a pattern that allows you to light any desired LED. It is immediately obvious that each LED cannot have it's own pin, since the mbed does not support such a number of digital output pins. To handle this problem, we connect each LED's VDD leg in a column to form 64 8-long LED columns. Even with 64 columns, we decided to use I2C with I/O Expanders in orders to address all 64 columns, since the mbed still does not support this number of digital out pins. That takes care of trying to light any single LED in a column. To select the exact layer belong to the LED we want to turn on, we send GND (logic 0) to that layer. In order to do so, the GND leg of each LED in a layer must be connected, and then each of the 8 layers will be connected to the output of a 3-to-8 decoder, which will then be controlled by some digital out pins on the mbed. Here are some pictures that will help you visualize what is meant by the connections I have described:

/media/uploads/aismail3/layers.jpg /media/uploads/aismail3/columns2.jpg /media/uploads/aismail3/columns.jpg

Components

  • 3mm Diffused Blue LEDs x 512 count
  • 1 HD74HC138P 3-to-8 decoder
  1. Used to select/power a layer of the cube.
  • 8-16 NPN BJTs/NFETs
  1. Used to ensure the necessary current to each layer.
  • 4 MCP23017 16-bit I/O Expanders (I2C)
  1. Used for powering one of the 64 columns
  • mbed microcontroller w/ an external 5v power supply
  • Solder/Soldering iron
  • Solid wire
  • PATIENCE

Schematic

This is the schematic of a simplified version: a 2x3x2 cube. Each layer has 6 (2 x 3) LEDs, with 2 layers total.

/media/uploads/aismail3/ckt.png

Code

main.cpp (The Snake game)

#include "mbed.h"
#include "LinkedList_ll/linkedlist_ll.h"
#include "LEDCube/LEDCube.h"
 
Serial pc(USBTX, USBRX); // tx, rx
I2C my2c(p28, p27);
LEDcube myCube(my2c, p23, p22, p21, 0x40, 4);
 
void initializeSnake(LLRoot * master);
void resetSnake(LLRoot * master);
void moveSnake(void);
void generateFruit(LLRoot * master);
void printSnakeLayer(const LLRoot * master, char layer);
void printSnakeCube(const LLRoot * master);
void getSnakeInput(void);
void setSnakeFlag(void);
 
LLRoot Snake;
Ticker tick;
Timer  sysClock;
 
InterruptIn b_xpos(p5);
InterruptIn b_xneg(p6);
InterruptIn b_ypos(p7);
InterruptIn b_yneg(p8);
InterruptIn b_zpos(p9);
InterruptIn b_zneg(p10);
 
char key, reset;
char errcode = 0;
char snakeFlag = 0;
char moveFlag = 0;
 
int main() {
     
    sysClock.reset();
    sysClock.start();
    my2c.frequency(400000);
 
    b_xpos.fall(&getSnakeInput);
    b_xpos.mode(PullUp);
    b_xneg.fall(&getSnakeInput);
    b_xneg.mode(PullUp);
    b_ypos.fall(&getSnakeInput);
    b_ypos.mode(PullUp);
    b_yneg.fall(&getSnakeInput);
    b_yneg.mode(PullUp);
    b_zpos.fall(&getSnakeInput);
    b_zpos.mode(PullUp);
    b_zneg.fall(&getSnakeInput);
    b_zneg.mode(PullUp);
    wait(0.2);
         
    initializeSnake(&Snake); // sets ticker to SPEED
     
    if(LLDEBUG) printListLL(&Snake);
    while(1) {
 
        myCube.lightCube(CUBE_DELAY);
        if(snakeFlag) {
            snakeFlag = 0;
            moveSnake();
        }
    }
     
    //freeListLL(&Snake);
    //printf("$ Program complete ... how did we get here?\r\n");
 
}
 
void setSnakeFlag(void) {
    snakeFlag = 1;
}
 
void initializeSnake(LLRoot * master) {
    /* Start Snake at 3 in length
     * x, y, z:layer
     */
       
    //if(master != NULL)      // Uh oh, non-empty snake?
    //    freeListLL(master); // No matter
     
    initializeLL(master);
    __disable_irq();
    addToHeadLL(master, 0, 0, 0);
    __enable_irq();
    // BEAMMEUPSCOTTY
    myCube.plotPoint(0, 0, 0);
    master->direction = XPOS;
    //master->length = 1;
    
    generateFruit(master);
     
    // Note: What if fruit is generated on snake?
    // The user will not be able to see it until all snake nodes have gone
    // over it
     
    tick.attach(&setSnakeFlag, SPEED);
}
 
void generateFruit(LLRoot * master) {
    if(master == NULL) return;
     
    /* Generation data for fruit between 0-7 
     * http://c-faq.com/lib/randrange.html
     */
      
      
    sysClock.stop();
     
    srand (sysClock.read_us());     // Seed random generator
    //sysClock.reset();
     
    master->fruit_x = (char) (rand() / (RAND_MAX / CUBEDIM + 1));
    master->fruit_y = (char) (rand() / (RAND_MAX / CUBEDIM + 1));
    master->fruit_z = (char) (rand() / (RAND_MAX / CUBEDIM + 1));
    myCube.plotPoint(master->fruit_x, master -> fruit_y, master->fruit_z);
    sysClock.start();
}
 
void resetSnake(LLRoot * master) {
    if(master == NULL) return;
     
    if(LLDEBUG) printf("###### GAME RESET; Setting snake back to 0,0,0 ######\r\n\r\n");
     
    tick.detach();
    freeListLL(master);
     
    myCube.clearCube();
    initializeSnake(master);
     
     
    //sysClock.reset();
    if(LLDEBUG) printListLL(master);
 
}
 
void getSnakeInput(void) {
    //LLRoot * master = &Snake;
    char current_dir = Snake.direction;
     
    /* Determines which button was pressed (out of 6), and prevents
     * the snake from turning in the opposite direction
     * Ex: when going x dir, cannot turn -x
     */
    if(moveFlag) {
        if(b_xpos == 0 && current_dir != XNEG) Snake.direction = XPOS;
        else if(b_xneg == 0 && current_dir != XPOS) Snake.direction = XNEG;
        else if(b_ypos == 0 && current_dir != YNEG) Snake.direction = YPOS;
        else if(b_yneg == 0 && current_dir != YPOS) Snake.direction = YNEG;
        else if(b_zpos == 0 && current_dir != ZNEG) Snake.direction = ZPOS;
        else if(b_zneg == 0 && current_dir != ZPOS) Snake.direction = ZNEG;
        moveFlag = 0;    
    }
     
    // else unknown data condition
}
 
/* May require the use of a ticker.attach in order to update the snake every
 * x seconds. <http://mbed.org/handbook/Ticker>
 */
void moveSnake(void) {
/*
 * NOTE: This is part of an ISR called by the Ticker
 * So no printf, limited use of malloc.
 * If issues occur, split up function
 */
 /* TODO: test mbed ISR, determine how much memory allocation,
  * max time for ISR, etc
  * REMOVE PRINTF ONCE MIGRATION OCCURS
  */
    LLRoot * master = &Snake;
    LLNode * tmp;
    char sHead_x, sHead_y, sHead_z;
    char apple_x, apple_y, apple_z;
    signed char XDIR, YDIR, ZDIR;
    char count;
     
    if(master == NULL) return;
     
    moveFlag = 1;
    errcode = 0;
     
    /* Careful usage required, stale data can occur with these variables */
    sHead_x = master->head->x;
    sHead_y = master->head->y;
    sHead_z = master->head->z;
    apple_x = master->fruit_x;
    apple_y = master->fruit_y;
    apple_z = master->fruit_z;
     
    XDIR = YDIR = ZDIR = 0;
    switch(master->direction) {
        case XPOS:
            XDIR = 1;
            break;
        case XNEG:
            XDIR = -1;
            break;
        case YPOS:
            YDIR = 1;
            break;
        case YNEG:
            YDIR = -1;
            break;
        case ZPOS:
            ZDIR = 1;
            break;
        case ZNEG:
            ZDIR = -1;
            break;
             
        default:
            break;
    }
     
    /* Update snake nodes */
    __disable_irq();
    addToHeadLL(master, sHead_x + XDIR, sHead_y + YDIR, sHead_z + ZDIR);
    __enable_irq();
    // BEAMMEUPSCOTTY
    myCube.plotPoint(sHead_x + XDIR, sHead_y + YDIR, sHead_z + ZDIR);
    if(LLDEBUG) printf("debug: adding [%d %d %d]\r\n", sHead_x + XDIR, sHead_y + YDIR, sHead_z + ZDIR);
     
    /* Boundary/Self checking */
    count = 0;
    tmp = master->head;
    while(tmp != NULL) {
        ++count;
         
        /* Check head coordinates against all other nodes */
        if(count > 1) {
            if((master->head->x == tmp->x) && (master->head->y == tmp->y) && (master->head->z == tmp->z)) {
                if(LLDEBUG) printf("debug: conflict with snake head: [%d %d %d] ... resetting game\r\n", master->head->x, master->head->y, master->head->z);
                resetSnake(master);
                reset = 1;
                errcode = 1;
                break;
            }
        }
         
        // TODO: flash cube face on wall hit
        // in if statements
         
        if(tmp->x > BOUNDARY) {
            resetSnake(master);
            reset = 1; 
            errcode = 3;
            // TODO: light right wall
            break;
        }
        if(tmp->x < 0) {
            resetSnake(master);
            reset = 1; 
            errcode = 3;
            // TODO: light left wall
            break;
        }
        if(tmp->y > BOUNDARY) {
            resetSnake(master);
            reset = 1;
            errcode = 3;
            // TODO: light back wall
            break;
        }
        if(tmp->y < 0) {
            resetSnake(master);
            reset = 1;
            errcode = 3;
            // TODO: light front wall
            break;
        }
        if(tmp->z > BOUNDARY) {
            resetSnake(master);
            reset = 1;
            errcode = 3;
            // TODO: light top wall
            break;
        }
        if(tmp->z < 0) {
            resetSnake(master);
            reset = 1;
            errcode = 3;
            // TODO: light bottom wall
            break;
        }
        tmp = tmp->next;
    }
    //master->length = count - 1;
 
    if(reset) { reset = 0; return; }
 
    /* Fruit detection */
    if((sHead_x == apple_x) && (sHead_y == apple_y) && (sHead_z == apple_z)) {
        generateFruit(master);
        errcode = 2;
        if(LLDEBUG) printf("debug: keeping tail [%d %d %d]\r\n", master->tail->x, master->tail->y, master->tail->z);
    }else{
        /* If we detect fruit, there is no need to remove the tail node.
         * Otherwise we can remove it to produce the snake movement
         */
        // BEAMMEUPSCOTTY
         
        myCube.clearPoint(master->tail->x, master->tail->y, master->tail->z);
        if(LLDEBUG) printf("debug: removing [%d %d %d] because no fruit detected\r\n", master->tail->x, master->tail->y, master->tail->z);
        __disable_irq();
        removeTailLL(master);
        __enable_irq();
    }
     
    if(LLDEBUG) printListLL(master);       /* Display LL header and node data */
}
 
void printSnakeCube(const LLRoot * master) {
    int i;
     
    if(master == NULL) return;
     
    for(i = 0; i < 8; i++) {
        printf("~~~~~LAYER%d~~~~~\r\n", i);
        printSnakeLayer(master, i);
    }
    printf("ERRCODE: %d\r\n", errcode);
}
 
void printSnakeLayer(const LLRoot * master, char layer) {
    LLNode * node = master->head;       /* Begin at the head node */
    char board[8][8];
    int x, y;
         
    if(master == NULL) return;
     
    for(x = 0; x < 8; ++x)
        for(y = 0; y < 8; ++y)
            board[x][y] = '-';
 
    if(layer == master->fruit_z)
        board[master->fruit_y][master->fruit_x] = 'x';
         
    while(node != NULL) {               /* If the node exists, pull the data from it */
        if(node->z == layer) {
            board[node->y][node->x] = 'o';
        }
        node = node->next;
    }
     
    if(layer == master->head->z)
        board[master->head->y][master->head->x] = 's';
         
    for(y = 7; y >= 0; --y) {
        for(x = 0; x < 8; ++x) {
            printf("%c", board[y][x]);
        }
        printf("\r\n");
    }
 
}

LEDCube.cpp

/*
    LEDcube.cpp    
*/
 
#include "mbed.h"
#include "LEDCube.h"
 
LEDcube::LEDcube(I2C &i2c, PinName a, PinName b, PinName c, char deviceAddress, char numChip) : _i2c(i2c), _decode0(a), _decode1(b), _decode2(c){
         
    _numChips = numChip;
    _baseWrite = deviceAddress & 0xFE;  // low order bit = 0 for write
    _baseRead = deviceAddress | 0x01;   // low order bit = 1 for read 
     
    clearCube();
   
    _init();
}
 
void LEDcube::clearCube(){
    int x;
    int y;
     
    for(x = 0; x < 8; x++)
        for(y = 0; y < 8; y++)
            cubeData[x][y] = 0x00; // Previously 0xff
}
 
void LEDcube::plotPoint(signed char x, signed char y, signed char z){
    if(x < 0 || x > 7) return; // Ignore bad data, snake function will catch it
    if(y < 0 || y > 7) return; // so we are ok to just return
    if(z < 0 || z > 7) return;
    if(x == 1 && y == 7 && z == 4)
        return;
    cubeData[z][x] = cubeData[z][x] | (0x01 << y);
}
void LEDcube::clearPoint(signed char x, signed char y, signed char z){
    if(x < 0 || x > 7) return;
    if(y < 0 || y > 7) return;
    if(z < 0 || z > 7) return;
    cubeData[z][x] = cubeData[z][x] & ((0x01 << y)^0xff);
} 
 
void LEDcube::drawDiamond(char x, char y, char z, char x2, char y2, char z2){
    if(x > x2 || y > y2 || z > z2)
        return;
    char Xtravel;
    char Ztravel;
     
    char tempX;
         
    char xmid = (x2 + x)/2;
    char ymid = (y2 + y)/2;
    char zmid = (z2 + z)/2;
     
    for(Xtravel = x; Xtravel < x2; Xtravel++){
        tempX = xmid - abs(xmid - Xtravel);
        for(Ztravel = z; Ztravel < z2; Ztravel++){
            if( Ztravel >= (zmid - tempX) && Ztravel <= (zmid + tempX)){
                plotPoint(Xtravel, ymid + tempX - abs(Ztravel - zmid), Ztravel);
                plotPoint(Xtravel, ymid - tempX + abs(Ztravel - zmid), Ztravel);
            }
        }
    }    
}
 
void LEDcube::explodeDiamond(char x, char y, char z){
    clearCube();
    drawDiamond(x - 1, y - 1, z - 1, x + 1, y + 1, z + 1);
    lightCube(CUBE_DELAY);
    ////////////////////
    wait(.3);
    ////////////////////
    clearCube();
    drawDiamond(x - 2, y - 2, z - 2, x + 2, y + 2, z + 2);
    lightCube(CUBE_DELAY);
    clearCube();
    //////////////
    wait(.3);
    /////////////
}
 
void LEDcube::shiftArray(){
    int x, z;
    char tempArr[8];
     
    for(z = 0; z < 8; z++)
        tempArr[z] = cubeData[z][7];
       
    for(x = 7; x < 1; x--){
        for(z = 7; z < 0; z--){
            cubeData[z][x] = cubeData[z][x - 1];
        }
    }
     for(z = 0; z < 8; z++)
        cubeData[z][0] = tempArr[z];     
}
 
void LEDcube::lightLED(int x, int y){
    char chip;
    char port = GPIOB;
    char byte;
     
    if(x > 7 || x < 0)
        x = 0;
    if(y > 7 || y < 0)
        y = 0;
         
    byte = ((0x01 << y)^0xff);    
    chip = x >> 1;
         
    if( x % 2 == 1){
        port = GPIOA;
        byte = reverse_byte(byte);
    }
 
    _write(chip, port, byte);
}
 
void LEDcube::lightPort(int x, char byte){
    char chip;
    char port = GPIOB;
     
    if(x > 7 || x < 0)
        x = 0;
         
    chip = x >> 1;
         
    if( x % 2 == 1){
        port = GPIOA;
        byte = reverse_byte(byte);
    }
     _write(chip, port, byte); 
}
 
void LEDcube::lightCube(double myWait){
    int layer;
    int x;
    for(layer = 0; layer < 8; layer++){
        _decode0 = layer & 0x01;
        _decode1 = layer & 0x02;
        _decode2 = layer & 0x04;
 
        // CHANGE LAYER OR SOMETHING
        for(x = 0; x < 8; x++){           
           lightPort(x, cubeData[layer][x]);    //A bit faster by removing for loop and using 2nd _write function
        }
         
        wait(myWait);
         
        // WAITING
        for(x = 0; x < 8; x++){           
           lightPort(x, 0x00); // Previously 0xff
        }
    }
}
 
void LEDcube::_init() {
    char i, j;
     
    if(DEBUG) printf("Begin initialization\n\r");
    for(i = 0; i < _numChips; i++){
        _write(i, IOCONA, 0x20); // Set to non-sequential registers
        _write(i, IODIRA, 0x00); // Set Port A as outputs
        _write(i, IODIRB, 0x00); // Set Port B as outputs 
    }
     
    for(i = 0; i < 8; i++)
        for(j = 0; j < 8; j++)
            cubeData[j][i] = 0x00;
}
 
void LEDcube::_write(char chip, char address, char byte) {
    char data[2];
 
    data[0] = address;
    data[1] = byte;
   
    _i2c.write((_baseWrite + (chip << 1)), data, 2);    // Write data to selected Register
    wait(myDELAY);
}
 
void LEDcube::_write(char chip, char address, char portA, char portB) {
    char data[3];
 
    data[0] = address;
    data[1] = portA;
    data[2] = portB;
        
    _i2c.write((_baseWrite + (chip << 1)), data, 3);    // Write data to selected Register
    wait(myDELAY);
}
 
unsigned char reverse_byte(unsigned char x)
{
    static const unsigned char table[] = {
        0x00, 0x80, 0x40, 0xc0, 0x20, 0xa0, 0x60, 0xe0,
        0x10, 0x90, 0x50, 0xd0, 0x30, 0xb0, 0x70, 0xf0,
        0x08, 0x88, 0x48, 0xc8, 0x28, 0xa8, 0x68, 0xe8,
        0x18, 0x98, 0x58, 0xd8, 0x38, 0xb8, 0x78, 0xf8,
        0x04, 0x84, 0x44, 0xc4, 0x24, 0xa4, 0x64, 0xe4,
        0x14, 0x94, 0x54, 0xd4, 0x34, 0xb4, 0x74, 0xf4,
        0x0c, 0x8c, 0x4c, 0xcc, 0x2c, 0xac, 0x6c, 0xec,
        0x1c, 0x9c, 0x5c, 0xdc, 0x3c, 0xbc, 0x7c, 0xfc,
        0x02, 0x82, 0x42, 0xc2, 0x22, 0xa2, 0x62, 0xe2,
        0x12, 0x92, 0x52, 0xd2, 0x32, 0xb2, 0x72, 0xf2,
        0x0a, 0x8a, 0x4a, 0xca, 0x2a, 0xaa, 0x6a, 0xea,
        0x1a, 0x9a, 0x5a, 0xda, 0x3a, 0xba, 0x7a, 0xfa,
        0x06, 0x86, 0x46, 0xc6, 0x26, 0xa6, 0x66, 0xe6,
        0x16, 0x96, 0x56, 0xd6, 0x36, 0xb6, 0x76, 0xf6,
        0x0e, 0x8e, 0x4e, 0xce, 0x2e, 0xae, 0x6e, 0xee,
        0x1e, 0x9e, 0x5e, 0xde, 0x3e, 0xbe, 0x7e, 0xfe,
        0x01, 0x81, 0x41, 0xc1, 0x21, 0xa1, 0x61, 0xe1,
        0x11, 0x91, 0x51, 0xd1, 0x31, 0xb1, 0x71, 0xf1,
        0x09, 0x89, 0x49, 0xc9, 0x29, 0xa9, 0x69, 0xe9,
        0x19, 0x99, 0x59, 0xd9, 0x39, 0xb9, 0x79, 0xf9,
        0x05, 0x85, 0x45, 0xc5, 0x25, 0xa5, 0x65, 0xe5,
        0x15, 0x95, 0x55, 0xd5, 0x35, 0xb5, 0x75, 0xf5,
        0x0d, 0x8d, 0x4d, 0xcd, 0x2d, 0xad, 0x6d, 0xed,
        0x1d, 0x9d, 0x5d, 0xdd, 0x3d, 0xbd, 0x7d, 0xfd,
        0x03, 0x83, 0x43, 0xc3, 0x23, 0xa3, 0x63, 0xe3,
        0x13, 0x93, 0x53, 0xd3, 0x33, 0xb3, 0x73, 0xf3,
        0x0b, 0x8b, 0x4b, 0xcb, 0x2b, 0xab, 0x6b, 0xeb,
        0x1b, 0x9b, 0x5b, 0xdb, 0x3b, 0xbb, 0x7b, 0xfb,
        0x07, 0x87, 0x47, 0xc7, 0x27, 0xa7, 0x67, 0xe7,
        0x17, 0x97, 0x57, 0xd7, 0x37, 0xb7, 0x77, 0xf7,
        0x0f, 0x8f, 0x4f, 0xcf, 0x2f, 0xaf, 0x6f, 0xef,
        0x1f, 0x9f, 0x5f, 0xdf, 0x3f, 0xbf, 0x7f, 0xff,
    };
    return table[x];
}

Videos

Pictures


Please log in to post comments.