Nathan Sweeney
Published © GPL3+

FPGA snake!

This is my first project! I designed hardware and software to run the classic 'snake' arcade game on a 24x24 NeoPixel array - FPGA based.

BeginnerFull instructions provided6 hours298
FPGA snake!

Things used in this project

Hardware components

Sundance VCS3 FPGA board
×1
RGB LED Pixel Matrix, NeoPixel NeoMatrix
RGB LED Pixel Matrix, NeoPixel NeoMatrix
×9
Kailh Mechanical Key Switches
×1
Anycubic Kobra 3 Max
×1

Software apps and online services

Vivado Design Suite
AMD Vivado Design Suite
Vitis Unified Software Platform
AMD Vitis Unified Software Platform

Hand tools and fabrication machines

Soldering iron (generic)
Soldering iron (generic)

Story

Read more

Custom parts and enclosures

LED cover

24 x 24 grid diffuser for the pixel array (Solidworks 2020)

Button + board / fan case

Portable tray

Schematics

Push Button Schematic

Block Diagram schematic for button solder design

Code

Numbers header

C/C++
Stored numbers for score display on death
#include <stdio.h>
#include "xil_printf.h"

// 3x5 font, digits 0-9
static const uint8_t font3x5[10][5] = {
    {0b111,0b101,0b101,0b101,0b111}, // 0
    {0b010,0b110,0b010,0b010,0b111}, // 1
    {0b111,0b001,0b111,0b100,0b111}, // 2
    {0b111,0b001,0b111,0b001,0b111}, // 3
    {0b101,0b101,0b111,0b001,0b001}, // 4
    {0b111,0b100,0b111,0b001,0b111}, // 5
    {0b111,0b100,0b111,0b101,0b111}, // 6
    {0b111,0b001,0b001,0b001,0b001}, // 7
    {0b111,0b101,0b111,0b101,0b111}, // 8
    {0b111,0b101,0b111,0b001,0b111}  // 9
};

Snake source code

C/C++
Source code in Vitis to play the snake game
#include <stdio.h>
#include "xbram_hw.h"
#include "xil_printf.h"
#include "xparameters.h"
#include "xbram.h"
#include "sleep.h"
#include "xgpio.h"
#include "numbers.h"


// ============ LED MATRIX VARIABLES ============
#define PANEL_X 8
#define PANEL_Y 8
#define EDGE_NUM 3
#define WIDTH (PANEL_X * EDGE_NUM)   // 24
#define HEIGHT (PANEL_Y * EDGE_NUM)  // 24
#define LED_NUM (WIDTH * HEIGHT)     // 576

XBram Bram;
u32 BramBase = XPAR_XBRAM_0_BASEADDR;
u16 ledMap[HEIGHT][WIDTH]; // ledMap 2D array
static u32 leds[LED_NUM]; // LED matrix state array


// ============ GPIO VARIABLES ============
#define GPIO_CHANNEL 1

XGpio Gpio;
u32 GpioBase = XPAR_XGPIO_0_BASEADDR;

#define BTN_UP_MASK    0x01 // GPIO0
#define BTN_DOWN_MASK  0x02 // GPIO1
#define BTN_LEFT_MASK  0x04 // GPIO2
#define BTN_RIGHT_MASK 0x08 // GPIO3


// ============ GAME SETTINGS ============
#define TICK_MS 120 // game tick speed - 120 is an ideal middle ground between gpio sensitivity and speed
#define MAX_SNAKE_LENGTH (WIDTH * HEIGHT) // full board is max points


// ============ GAME STATE VARIABLE SETUP ============
typedef struct {int x, y; } Point;
typedef enum {UP = 0, DOWN = 1, LEFT = 2, RIGHT = 3} Direction; // define directions

static Point snake[MAX_SNAKE_LENGTH]; // initialise snake array
static int snakeLength = 0;           // initial snake length is zero
static Direction dir = RIGHT;         // initial direction is right
static Point food;                    // initialise food location
static int alive = 1;                 // state to check end of game


// ============ USEFUL FUNCTIONS ============
static inline void colour_clear(u32 colour) { // clears the whole frame to a colour
    for(int i = 0; i < LED_NUM; i++) {
        leds[i] = colour;
    }
}

static inline void pix_set(int x, int y, u32 colour) { // sets one pixel colour using ledMap to translate (x,y) to hardware index
    if((unsigned)x < WIDTH && (unsigned)y < HEIGHT) {
        leds[ ledMap[y][x] ] = colour;
    }
}

static u32 rng_state = 0x12345678u;
static inline u32 xor_shift32(void) { // random placement function (for food) - pulled from internet
    u32 x = rng_state;
    x ^= x << 13;
    x ^= x >> 17;
    x ^= x << 5;
    rng_state = x ? x : 0x6AC690C5u;
    return rng_state;
}


// ============ LED MAPPING FUNCTION ============
void build_led_map(void) { // sorts out daisy-chain serpentine wiring of physical boards

    int ledex = 0; // LED index

    for(int panelX = 0; panelX < 3; panelX++) { // panelX is x position of the panel within the 3x3 matrix (0, 1, 2 from L - R), col is the x position of the column
        // middle column reads right to left bottom to top
        int reversed = (panelX == 1);
        
        if(!reversed) {
            for(int panelY = 0; panelY < 3; panelY++) { // panelY is y position of the panel within the 3x3 matrix (0, 1, 2 from T - B). row is the y position of the row
                for(int row = 0; row < PANEL_X; row++) {
                    // left and right panel columns read left to right top to bottom
                    for(int col = 0; col < PANEL_Y; col++) {
                        int X = panelX * PANEL_X + col; // sorts out which LED column is being accessed
                        int Y = panelY * PANEL_Y + row; // sorts out which LED row is being accessed
                        ledMap[Y][X] = ledex++; // builds ledMap from top left corner, Y axis positive downwards, X axis positive left to right
                    }
                }
            }
        } 
        else {
            for(int panelY = EDGE_NUM - 1; panelY >=0; panelY--) { // for middle column, panelY is y position of the panel within the 3x3 matrix (0, 1, 2 from B - T). row is the y position of the row
                for(int row = PANEL_Y - 1; row >= 0; row--) { // bottom to top for middle column of panels
                    // right to left for middle column of panels
                    for(int col = PANEL_X - 1; col >= 0; col--) {
                        int X = panelX * PANEL_X + col;
                        int Y = panelY * PANEL_Y + row;
                        ledMap[Y][X] = ledex++;
                    }
                }
            }

        }
    }

}


// ============ SEND FRAME TO LEDS ============
void show_frame(u32 *frame) { // writes the current game frame to BRAM, taken into LED driver
    u32 EffectiveAddr = 0x04; // first LED data memory position
    for (int led = 0; led < LED_NUM; led++) {
        XBram_WriteReg(BramBase, 0, 0x240); // tell ip to expect 576 LEDs
        XBram_WriteReg(BramBase, EffectiveAddr, frame[led]); // write LED data for one pixel
        EffectiveAddr += 4; // increment Bram address
        XBram_WriteReg(BramBase, 0, 0); // force FSM reset
    }

}


// ============ GPIO I/P CHECK AND DEBOUNCE ============
static Direction pendingDir = RIGHT; //updated by gpio i/p, applied per clock pulse

static void read_input(void) {

    u32 raw = XGpio_DiscreteRead(&Gpio, GPIO_CHANNEL); // read gpio input (if any)

    // make sure 180 degree turns are impossible - 90 is max per tick
    if (raw & BTN_UP_MASK)    { if (dir != DOWN)  pendingDir = UP; }
    if (raw & BTN_DOWN_MASK)  { if (dir != UP)    pendingDir = DOWN; }
    if (raw & BTN_LEFT_MASK)  { if (dir != RIGHT) pendingDir = LEFT; }
    if (raw & BTN_RIGHT_MASK) { if (dir != LEFT)  pendingDir = RIGHT; }
}


// ============ GAME LOGIC FUNCTIONS ============
static void spawn_food(void) { // food spawn function
    while(1) {
        int x = (int)(xor_shift32() % WIDTH);
        int y = (int)(xor_shift32() % HEIGHT); // choose random position
        int collision = 0;
        for (int pix = 0; pix < snakeLength; pix++) { // check this isnt in the snake
            if(snake[pix].x == x && snake[pix].y == y) {
                collision = 1;
                break;
            }
        }
        if(!collision) {
            food.x = x, food.y = y; // if not, place food here
            return;
        }
    }
}

static void death_screen(void) { // flash red pulses on death
    for(int k = 0; k < snakeLength; k++) {
        pix_set(snake[k].x, snake[k].y, 0x00043300); // set snake pixels
    }
    show_frame(leds);
    usleep(500000);
    for(int k = 0; k < snakeLength; k++) {
        pix_set(snake[k].x, snake[k].y, 0x00021f00); // snake dies out in steps
    }
    show_frame(leds);
    usleep(200000);
    for(int k = 0; k < snakeLength; k++) {
        pix_set(snake[k].x, snake[k].y, 0x00000f00);
    }
    show_frame(leds);
    usleep(150000);
    for(int k = 0; k < snakeLength; k++) {
        pix_set(snake[k].x, snake[k].y, 0x00000200);
    }
    show_frame(leds);
    usleep(100000);
}

static void draw_digit(int digit, int x, int y, u32 colour) { // draw digit 0-9 to display score upon death
    if(digit < 0 || digit > 9) return;
    for(int row = 0; row < 5; row++) {
        for(int col = 0; col < 3; col++) {
            if(font3x5[digit][row] & (1 << (2 - col))) { // from numbers.h header - font3x5 is binary layout of numbers on pixel display
                pix_set(x + col, y + row, colour);
            }
        }
    }
}

static void show_score(int score) { // write out the numbers on the board (drawing the frame with the score on it)
    char buf[6];
    snprintf(buf, sizeof(buf), "%d", score); // convert number to string
    
    colour_clear(0x00000000); // clear board
    
    int len = strlen(buf);
    int startX = (WIDTH - (len * 4 - 1)) / 2; // center horizontally
    int startY = (HEIGHT - 5) / 2;            // center vertically
    
    for(int i = 0; i < len; i++) {
        draw_digit(buf[i] - '0', startX + i * 4, startY, 0x00001133); // blue numbers
    }
    show_frame(leds);
}

static void reset_game(void) {

    // reset game to start point
    snakeLength = 3;
    int x0 = WIDTH / 2;
    int y0 = HEIGHT / 2;
    snake[0].x = x0;     snake[0].y = y0; // build 3 pixel snake in 2x2 array
    snake[1].x = x0 - 1; snake[1].y = y0;
    snake[2].x = x0 - 2; snake[2].y = y0;
    dir = RIGHT; // starting direction is right
    pendingDir = dir;
    alive = 1;
    spawn_food();
}

static void draw_game_frame(void) {
    colour_clear(0x00000000); // clear panel
    for(int pix = 0; pix < snakeLength; pix++) {
        pix_set(snake[pix].x, snake[pix].y, 0x00220001); // set snake pixels
    }
    pix_set(food.x, food.y, 0x00003300); // set food pixel
    show_frame(leds); // output this game frame to show_frame function
}

static void wait(void) {
    while(1) {
        u32 pend = XGpio_DiscreteRead(&Gpio, GPIO_CHANNEL); // read gpio input (if any)
        if(pend & BTN_RIGHT_MASK) { return; }
        else { draw_game_frame(), usleep(1000 * TICK_MS); }
    }
}

static void game_tick(void) {
    dir = pendingDir; // update direction every tick, checking for gpio inputs

    Point head = snake[0];
    switch(dir) { // switch operator to save memory
        case UP:    head.y -= 1; break; // move up
        case DOWN:  head.y += 1; break; // move down
        case LEFT:  head.x -= 1; break; // move left
        case RIGHT: head.x += 1; break; // move right
    }

    // check for collisions
    if(head.x < 0 || head.x >= WIDTH || head.y < 0 || head.y >= HEIGHT) {
        alive = 0; // wall collision this frame
        return;
    }
    for(int pix = 0; pix < snakeLength; pix++) {
        if(snake[pix].x == head.x && snake[pix].y == head.y) {
            alive = 0; // self collision this frame
            return;
        }
    }

    // shift snake along one pixel
    if (snakeLength < MAX_SNAKE_LENGTH) {
        for(int i = snakeLength; i > 0; i--) {
            snake[i] = snake[i - 1];
        }
    }
    else {
        for(int i = snakeLength - 1; i > 0; i--) {
            snake[i] = snake[i - 1];
        }
    }
    snake[0] = head; // reinstate where head is

    if(head.x == food.x && head.y == food.y) {  // check if snake eats food this frame
        if(snakeLength < MAX_SNAKE_LENGTH) {
        snakeLength++;
        spawn_food();
        }
    }
}


// ============ COMBINED LOGIC FUNCTION (MAIN) ============
int main() {

    build_led_map(); // build LED 2D lookup table

    // ============ INIT BRAM ============
    XBram_Config *ConfigPtr;
    ConfigPtr = XBram_LookupConfig(BramBase);
    XBram_CfgInitialize(&Bram, ConfigPtr, ConfigPtr->CtrlBaseAddress);

    // ============ INIT GPIO ============
    XGpio_Initialize(&Gpio, GpioBase);
    XGpio_SetDataDirection(&Gpio, GPIO_CHANNEL, 0xf); // set direction as inputs


    // ============ GAME LOOP ============
    rng_state ^= (u32)BramBase ^ 0x9E3779B9u; // random seed
    reset_game(); // reset to start position to begin
    wait(); // hold frame until right button pressed to start the game

    while(1) {
        read_input(); // loop checking input from buttons

        if(alive) {
            game_tick(); // run a tick of the game
            draw_game_frame(); // draw the current game frame - which then writes this frame to LEDs with show_frame
        }
        else {
            xil_printf("Game Over! Score: %d\r\n", snakeLength - 3);            
            death_screen(); // flash death screen
            show_score(snakeLength - 3); // pixel score
            usleep(2000000); // hold score for 2s
            reset_game(); // if collision occurs, reset
            wait(); // hold frame until right button pressed to restart the game
        }

        usleep(TICK_MS * 1000); // game tick speed (change TICK_MS to increase or decrease this)
    }

return 0;
}

NeoPixel VHDL

Full credit to Adam Taylor, GitHub FPGA LED driver (I have edited line 58 to increase RAM address to from 0 to 4096 to drive up to 1024 LEDs).

Credits

Nathan Sweeney
1 project • 5 followers
Cambridge University Engineering Undergraduate
Thanks to Adam Taylor.

Comments