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:
Components
- 3mm Diffused Blue LEDs x 512 count
- 1 HD74HC138P 3-to-8 decoder
- Used to select/power a layer of the cube.
- 8-16 NPN BJTs/NFETs
- Used to ensure the necessary current to each layer.
- 4 MCP23017 16-bit I/O Expanders (I2C)
- 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.
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.




