Charlie BramptonMika Gudipati
Published

Motion-Controlled FPGA Snake

FPGA-based design of the arcade game, ‘Snake’ - running on a 24x24 NeoPixel array, controlled using a joystick / accelerometer setup.

BeginnerFull instructions provided10 hours61
Motion-Controlled FPGA Snake

Things used in this project

Hardware components

VCS³ Core
Sundance Multiprocessor Technology Ltd. VCS³ Core
×1
RGB LED Pixel Matrix, NeoPixel NeoMatrix, 8x8, 64 LEDs
×9
5V Supply (for panels)
×1
JTAG Pod (Digilent JTAG-HS3)
×1
USB serial
×1
Anycubic Kobra 3 max (3D Printer)
×1
Custom VCS-3 Power Board
*Built in-house by Sundance Multiprocessor Technology Ltd.
×1

Software apps and online services

AMD Vivado Design Suite (2024.2)
AMD Vitis Unified IDE (2024.2)
A serial terminal (e.g. PuTTY, minicom, screen)
VCS-3 Board Files
Solidworks (for 3D prints)

Story

Read more

Custom parts and enclosures

Full bottom plate to fit board + joystick setup

Square Cross-Section Reinforced Bar

5mm Connector

Bottom section of the holder

Fan slots for the holder

Cutout for holder fan slots

Schematics

General Design

Hardware Block Design (Vivado)

Code

snake.c

C/C++
Copy and paste into your Vitis app component source, named "snake.c".
/*
 * =============================================================================
 *  Motion-Controlled Snake for the Sundance VCS-3
 * =============================================================================
 *
 *  A 24x24 WS2812 LED matrix game controlled by flicking a joystick, using the
 *  ICM-20948 IMU integrated on the VCS-3 module.
 *
 *  Adapted from Nathan Sweeney's FPGA Snake
 * =============================================================================
 */

#include <stdio.h>
#include <string.h>
#include "xbram_hw.h"
#include "xil_printf.h"
#include "xparameters.h"
#include "xbram.h"
#include "sleep.h"
#include "xgpio.h"
#include "numbers.h"
#include "xspips.h"
#include "xstatus.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]; // maps (x,y) to a hardware LED index - see build_led_map()
static u32 leds[LED_NUM]; // current frame, in hardware LED order


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

XGpio Gpio;
u32 GpioBase = XPAR_XGPIO_0_BASEADDR;

XSpiPs Spi; // PS SPI0 - the ICM-20948 IMU is hard-wired to this on the VCS-3

#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 5 // input poll interval in ms - NOT the snake speed, see GAME_MOVE_TICKS
#define MAX_SNAKE_LENGTH (WIDTH * HEIGHT) // full board is max points

// Loop passes between snake movements. 24 x TICK_MS(5) = 120 ms, which is the
// original keypad version's speed, kept deliberately.
#define GAME_MOVE_TICKS 24
static int game_move_counter = 0;


// ============ 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]; // snake[0] is the head
static int snakeLength = 0;           // initial snake length is zero
static Direction dir = RIGHT;         // direction currently being moved
static Point food;                    // initialise food location
static int alive = 1;                 // state to check end of game


// ============ IMU SETTINGS ============

// Threshold in raw counts. The ICM-20948 defaults to +/-250 dps, i.e. 131
// counts per deg/s, so 1800 is roughly 14 deg/s.
//
// This value is set by the NOISIER axis, not the average. Measured on this
// rig, X wobbles by +/-1200 even when held still while Y stays within +/-500,
// so a threshold suited to Y produces phantom left/right turns.
// History: 5000 (nothing registered) -> 3000 -> 1800.
#define GYRO_THRESHOLD 1800  // prev 3000

// Lockout after an accepted turn, in LOOP PASSES. 40 x 5 ms = 200 ms.
// Stops one flick registering as several turns, and absorbs the joystick's
// spring-back, which generates rotation in the opposite direction.
#define GYRO_COOLDOWN 40 // prev 8

static int gyro_cooldown = 0;

// gyro zero offset (calibrated at startup)
static int gyro_zero_x = 0;
static int gyro_zero_y = 0;

// gyro movement confirmation
static int gyro_x_count = 0;
static int gyro_y_count = 0;

// Consecutive above-threshold samples needed before a turn is accepted.
// 6 x TICK_MS(5) = 30 ms.
//
// This matters more than the threshold. At 30 it demanded 150 ms of
// continuously sustained rotation, which a natural flick never achieves - the
// gesture accelerates, peaks and decelerates, dropping below threshold partway
// through and resetting the count.
#define GYRO_CONFIRM_COUNT 6 //30

// Consecutive above-threshold samples needed to start a game.
// 6 x TICK_MS(5) = 30 ms of continuous rotation. Raise if the game starts on
// incidental knocks; lower if a deliberate flick is ignored.
#define START_GYRO_CONFIRM 6 //10

// Redraw the idle frame every N polls instead of every poll.
// 20 x 5 ms = 10 Hz. Previously this redrew at TICK_MS, i.e. 200 Hz - a full
// frame is 1728 AXI writes, so redrawing on every pass is 24x the work.
#define WAIT_DRAW_TICKS 20

// Set to 1 only if a keypad is physically attached.
// With no keypad connected these GPIO inputs float, and floating CMOS inputs
// drift high - so the code sees a permanently pressed button and the game
// starts on its own the instant wait() is entered.
#define ALLOW_BUTTON_START 0


// ============ 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 ============
// Note the control register is rewritten and the FSM reset once per PIXEL
// rather than once per frame. That is unusual - you would normally expect
// reset, then 576 data writes, then a single "go" - but it is what this
// NeoPixel IP expects, so it is left alone.
//
// Cost: 3 AXI writes x 576 LEDs = 1728 writes per frame. Worth remembering
// before calling this on a fast loop.
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
    }
}


// ============ KEYPAD INPUT (ORIGINAL) ============
static Direction pendingDir = RIGHT; // direction queued for the next snake move

// The original's input path. Not called in this build - the joystick replaces
// it - but retained as documentation and as the route back if buttons are ever
// refitted. Expect an "unused function" warning.
//
// Worth noting it is stateless: a button is a level, either pressed now or
// not, so there is nothing to remember between calls. Gesture recognition is
// not stateless, which is why read_gyro_input() below needs counters and this
// does not.
static void read_input(void)
{
    u32 raw = XGpio_DiscreteRead(&Gpio, GPIO_CHANNEL);

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


// ============ GYRO CALIBRATION ============

// Captures the gyro's zero offset by averaging 50 samples over ~500 ms.
//
// IMPORTANT: the board must be still for that whole window. If it is moved or
// held tilted during calibration the datum is wrong for the entire session -
// one direction becomes hypersensitive and its opposite unreachable, which
// looks exactly like badly chosen thresholds and cannot be fixed by tuning
// them. Hence the prompt before this is called, and hence recalibrating after
// every death.
//
// The printed datum is a useful sanity check: values differing wildly between
// runs mean the calibration was disturbed.
static void calibrate_gyro(void)
{
    u8 gyro_tx[7] = {0};
    u8 gyro_rx[7] = {0};

    int sum_x = 0;
    int sum_y = 0;

    for(int i = 0; i < 50; i++)
    {
        gyro_tx[0] = 0x80 | 0x33;

        XSpiPs_PolledTransfer(
            &Spi,
            gyro_tx,
            gyro_rx,
            7);

        short gyro_x =
            (short)(((u16)gyro_rx[1] << 8) | gyro_rx[2]);

        short gyro_y =
            (short)(((u16)gyro_rx[3] << 8) | gyro_rx[4]);

        sum_x += gyro_x;
        sum_y += gyro_y;

        // The gyro's low bits are genuine electrical noise, so they make a
        // good entropy source for food placement. See main() for why the
        // original's seed was not one.
        rng_state ^= ((u32)gyro_x << 16) ^ (u32)gyro_y;

        usleep(10000);
    }

    gyro_zero_x = sum_x / 50;
    gyro_zero_y = sum_y / 50;

    xil_printf("Gyro zero X=%d Y=%d\r\n",
               gyro_zero_x,
               gyro_zero_y);
}


// ============ GYRO INPUT ============

// Polls the gyro and converts a sustained rotation into a queued turn.
//
// Positive X rate commands RIGHT, negative X commands LEFT; positive Y
// commands UP, negative Y commands DOWN. These signs depend on how the board
// is mounted to the joystick. If all four directions come out inverted, that
// is a single sign flip from the mounting orientation - swap each direction
// together with its guard, and flip the matching sign in wait() too.
//
// Each guard names the OPPOSITE of the direction being requested, because its
// job is to block an illegal 180-degree reversal. If a guard ever names the
// same direction it is setting, something has been half-edited.
static void read_gyro_input(void)
{
    u8 gyro_tx[7] = {0};
    u8 gyro_rx[7] = {0};

    // Gyro X,Y,Z registers start at 0x33
    gyro_tx[0] = 0x80 | 0x33;

    XSpiPs_PolledTransfer(
            &Spi,
            gyro_tx,
            gyro_rx,
            7);

    short gyro_x =
        (short)(((u16)gyro_rx[1] << 8) | gyro_rx[2]);

    short gyro_y =
        (short)(((u16)gyro_rx[3] << 8) | gyro_rx[4]);

    gyro_x -= gyro_zero_x;
    gyro_y -= gyro_zero_y;

    // Discard implausible samples. A signed 16-bit value cannot exceed 32767,
    // yet occasional torn SPI reads return more than that.
    if (gyro_x > 30000 || gyro_x < -30000) return;
    if (gyro_y > 30000 || gyro_y < -30000) return;

    // Prevent one long rotation from causing multiple turns
    if(gyro_cooldown > 0)
    {
        gyro_cooldown--;
        return;
    }

    // X axis controls left/right
    if(gyro_x > GYRO_THRESHOLD)
    {
        gyro_x_count++;

        if(gyro_x_count >= GYRO_CONFIRM_COUNT)
        {
            if(dir != LEFT)
            {
                pendingDir = RIGHT;
                gyro_cooldown = GYRO_COOLDOWN;
            }

            gyro_x_count = 0;
        }
    }
    else if(gyro_x < -GYRO_THRESHOLD)
    {
        gyro_x_count++;

        if(gyro_x_count >= GYRO_CONFIRM_COUNT)
        {
            if(dir != RIGHT)
            {
                pendingDir = LEFT;
                gyro_cooldown = GYRO_COOLDOWN;
            }

            gyro_x_count = 0;
        }
    }
    else
    {
        // Only a reading back inside the threshold band resets the count.
        gyro_x_count = 0;
    }

    // Y axis controls up/down
    if(gyro_y > GYRO_THRESHOLD)
    {
        gyro_y_count++;

        if(gyro_y_count >= GYRO_CONFIRM_COUNT)
        {
            if(dir != DOWN)
            {
                pendingDir = UP;
                gyro_cooldown = GYRO_COOLDOWN;
            }

            gyro_y_count = 0;
        }
    }
    else if(gyro_y < -GYRO_THRESHOLD)
    {
        gyro_y_count++;

        if(gyro_y_count >= GYRO_CONFIRM_COUNT)
        {
            if(dir != UP)
            {
                pendingDir = DOWN;
                gyro_cooldown = GYRO_COOLDOWN;
            }

            gyro_y_count = 0;
        }
    }
    else
    {
        gyro_y_count = 0;
    }
}


// ============ SHARED GYRO HELPER ============

/*
 * Raw gyro read, zero-corrected on X and Y. Z has no calibrated zero
 * so it is returned raw. Same registers and byte order as
 * read_gyro_input().
 *
 * Used by wait(); read_gyro_input() predates it and does its own read.
 */
static void gyro_read_xyz(int *x, int *y, int *z)
{
    u8 tx[7] = {0};
    u8 rx[7] = {0};

    tx[0] = 0x80 | 0x33;

    XSpiPs_PolledTransfer(&Spi, tx, rx, 7);

    short raw_x = (short)(((u16)rx[1] << 8) | rx[2]);
    short raw_y = (short)(((u16)rx[3] << 8) | rx[4]);
    short raw_z = (short)(((u16)rx[5] << 8) | rx[6]);

    *x = (int)raw_x - gyro_zero_x;
    *y = (int)raw_y - gyro_zero_y;
    *z = (int)raw_z;
}


// ============ GAME LOGIC FUNCTIONS ============
static void spawn_food(void) { // food spawn function
    while(1) {
        // Uses the upper bits: xorshift32's low bits are its weakest, and
        // taking two consecutive low-bit outputs as (x,y) produces visible
        // lattice structure in the spawn positions.
        int x = (int)((xor_shift32() >> 16) % WIDTH);
        int y = (int)((xor_shift32() >> 16) % 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;
        }
    }
}

// Death animation. Blocks for ~950 ms and repaints the panel four times.
//
// Worth knowing while debugging: if turns are misfiring and killing the snake
// more often than the player realises, this reads from the outside as
// intermittent lag and unexplained LED flashing - a symptom that looks like a
// hardware fault. If you see that, shorten this temporarily before
// investigating the panel.
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; // three-segment snake, head first, laid out horizontally
    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;

    // The original had none of these to reset, because button input is
    // stateless. Gesture recognition is not: a partial confirmation count or
    // an active cooldown surviving into the next game causes intermittent
    // faults that only show up from the second game onward.
    //
    // Rule of thumb: every static added to the input handler is a line owed
    // to this function.
    game_move_counter = 0;
    gyro_x_count = 0;
    gyro_y_count = 0;
    gyro_cooldown = 0;

    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
}


// ============ FLICK-TO-START IDLE SCREEN ============

// Holds the idle screen until the player flicks right to start.
//
// The start gesture is deliberately identical to an in-game turn: the action
// that starts the game should be the action used to play it. No separate
// return-to-neutral step is needed, because the cooldown set on exit ignores
// input for 200 ms and the reversal guards in read_gyro_input() block the
// spring-back anyway.
static void wait(void)
{
    int held = 0;
    int draw_counter = 0;
    int gx, gy, gz;

    xil_printf("Rotate right to start.\r\n");

    while (1) {
#if ALLOW_BUTTON_START
        if (XGpio_DiscreteRead(&Gpio, GPIO_CHANNEL) & BTN_RIGHT_MASK) {
            break;
        }
#endif

        gyro_read_xyz(&gx, &gy, &gz);

        xor_shift32();   /* advance RNG - human timing is the entropy */

        /*
         * Positive X rate is RIGHT, matching read_gyro_input(). If the game
         * starts on a LEFT flick, this sign and the ones in read_gyro_input()
         * are both inverted - change them together, not separately.
         */
        if (gx > GYRO_THRESHOLD) {
            held++;
            if (held >= START_GYRO_CONFIRM) {
                break;
            }
        }
        else {
            held = 0;
        }

        if (draw_counter == 0) {
            draw_game_frame();
        }
        draw_counter = (draw_counter + 1) % WAIT_DRAW_TICKS;

        usleep(TICK_MS * 1000);
    }



    /* Clear the input state so play starts clean, and hold off input long
     * enough to cover the tail of the starting flick. */
    gyro_x_count = 0;
    gyro_y_count = 0;
    gyro_cooldown = GYRO_COOLDOWN;

    xil_printf("Go.\r\n");
}


static void game_tick(void) {
    dir = pendingDir; // apply the queued direction; this is the only place dir changes

    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;
    }
    // Inherited behaviour: this checks every segment including the tail tip,
    // which is about to vacate its square on this very tick. Most Snake
    // implementations allow moving into that square; here it kills you. Left
    // as the original had it, since changing it alters the game's feel.
    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

    // ============ INIT IMU SPI ============
    // The IMU is on the VCS-3 module itself, wired to SPI0 (MIO64/67/68/69).
    // Applying the Vivado board preset configures the bus - nothing to wire.
    XSpiPs_Config *SpiConfig;

    SpiConfig = XSpiPs_LookupConfig(XPAR_XSPIPS_0_BASEADDR);

    if(SpiConfig == NULL)
    {
        xil_printf("SPI lookup failed\r\n");
        while(1);
    }

    XSpiPs_CfgInitialize(
            &Spi,
            SpiConfig,
            SpiConfig->BaseAddress);

    XSpiPs_SetOptions(
            &Spi,
            XSPIPS_MASTER_OPTION |
            XSPIPS_FORCE_SSELECT_OPTION);

    // Conservative; the ICM-20948 supports up to 7 MHz for data reads.
    XSpiPs_SetClkPrescaler(
            &Spi,
            XSPIPS_CLK_PRESCALE_64);

    XSpiPs_SetSlaveSelect(
            &Spi,
            0);

    // Wake IMU
    // PWR_MGMT_1 (0x06) = 0x01 clears the sleep bit and selects the best
    // available clock source. The device boots asleep.
    u8 wake_tx[2];
    u8 wake_rx[2];

    wake_tx[0] = 0x06;
    wake_tx[1] = 0x01;

    XSpiPs_PolledTransfer(
            &Spi,
            wake_tx,
            wake_rx,
            2);

    xil_printf("IMU ready\r\n");

    // Let the IMU settle before sampling. Reading immediately after wake
    // returns values indistinguishable from a wiring fault.
    usleep(500000);
    calibrate_gyro();


    // ============ GAME LOOP ============
    // Both operands here are compile-time constants, so this line does not
    // actually seed anything - it XORs a fixed value into a fixed value, and
    // every power-up produced the identical food sequence. Real entropy now
    // comes from gyro noise in calibrate_gyro() and from human reaction time
    // in wait(). Kept only so the original's structure remains recognisable.
    rng_state ^= (u32)BramBase ^ 0x9E3779B9u; // NOT a random seed - see above
    reset_game(); // reset to start position to begin
    wait(); // hold frame until a right flick starts the game

    while(1) {

        read_gyro_input(); // every TICK_MS

        if(alive) {
            game_move_counter++;

            if (game_move_counter >= GAME_MOVE_TICKS) {
                game_move_counter = 0;

                game_tick(); // every GAME_MOVE_TICKS passes, i.e. every 120 ms
                draw_game_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

            xil_printf("Keep board still - calibrating gyro...\r\n");
            calibrate_gyro();
            xil_printf("Calibration complete\r\n");

            wait(); // hold frame until a right flick restarts the game
        }

        usleep(TICK_MS * 1000); // input poll interval
    }

    return 0;
}

numbers.h

C Header File
Copy and paste into your Vitis app component source, named "numbers.h".
#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
};

Credits

Charlie Brampton
1 project • 0 followers
Engineering Undergraduate at the University of Cambridge
Mika Gudipati
1 project • 0 followers
Engineering student at Uni of Bristol

Comments