Introduction: FPGA Tic-Tac-Toe
Hello. I'm a first year computer engineering student at Cal Poly SLO whose currently taking Prof. Danowitz's CPE 133 (Digital Design) class, and this is my final project.
This Instructable will show you how to implement a Tic-Tac-Toe game in Verilog, on the Basys 3 FPGA board. The board's switches will be used as input, while output will be done over VGA (640x480).
Supplies
For the project, you'll need the following:
- Basys3 FPGA board
- VGA cable, monitor
- Installation of Xilinx Vivado
Step 1: Video Driver
The video driver is a slightly modified version of the one downloadable here (credit to embeddedthoughts.com).
From what I've ascertained, it outputs 480 horizontal lines in accordance with standard VGA timing. While each line is being drawn, the analog RGB levels can be changed to represent different pixels. Through this, an image is displayed on-screen.
`timescale 1ns / 1ps
// Video driver from embeddedthoughts.com
// https://embeddedthoughts.com/2016/07/29/driving-a-vga-monitor-using-an-fpga/
module vga_sync(
input wire clk, reset,
output wire hsync, vsync, video_on, p_tick,
output wire [9:0] x, y
);
// constant declarations for VGA sync parameters
localparam H_DISPLAY = 640; // horizontal display area
localparam H_L_BORDER = 48; // horizontal left border
localparam H_R_BORDER = 16; // horizontal right border
localparam H_RETRACE = 96; // horizontal retrace
localparam H_MAX = H_DISPLAY + H_L_BORDER + H_R_BORDER + H_RETRACE - 1;
localparam START_H_RETRACE = H_DISPLAY + H_R_BORDER;
localparam END_H_RETRACE = H_DISPLAY + H_R_BORDER + H_RETRACE - 1;
localparam V_DISPLAY = 480; // vertical display area
localparam V_T_BORDER = 10; // vertical top border
localparam V_B_BORDER = 33; // vertical bottom border
localparam V_RETRACE = 2; // vertical retrace
localparam V_MAX = V_DISPLAY + V_T_BORDER + V_B_BORDER + V_RETRACE - 1;
localparam START_V_RETRACE = V_DISPLAY + V_B_BORDER;
localparam END_V_RETRACE = V_DISPLAY + V_B_BORDER + V_RETRACE - 1;
// mod-4 counter to generate 25 MHz pixel tick
reg [1:0] pixel_reg;
wire [1:0] pixel_next;
wire pixel_tick;
always @(posedge clk, posedge reset)
if(reset)
pixel_reg <= 0;
else
pixel_reg <= pixel_next;
assign pixel_next = pixel_reg + 1; // increment pixel_reg
assign pixel_tick = (pixel_reg == 0); // assert tick 1/4 of the time
// registers to keep track of current pixel location
reg [9:0] h_count_reg, h_count_next, v_count_reg, v_count_next;
// register to keep track of vsync and hsync signal states
reg vsync_reg, hsync_reg;
wire vsync_next, hsync_next;
// infer registers
always @(posedge clk, posedge reset)
if(reset)
begin
v_count_reg <= 0;
h_count_reg <= 0;
vsync_reg <= 0;
hsync_reg <= 0;
end
else
begin
v_count_reg <= v_count_next;
h_count_reg <= h_count_next;
vsync_reg <= vsync_next;
hsync_reg <= hsync_next;
end
// next-state logic of horizontal vertical sync counters
always @*
begin
h_count_next = pixel_tick ?
h_count_reg == H_MAX ? 0 : h_count_reg + 1
: h_count_reg;
v_count_next = pixel_tick && h_count_reg == H_MAX ?
(v_count_reg == V_MAX ? 0 : v_count_reg + 1)
: v_count_reg;
end
// hsync and vsync are active low signals
// hsync signal asserted during horizontal retrace
assign hsync_next = h_count_reg >= START_H_RETRACE && h_count_reg <= END_H_RETRACE;
// vsync signal asserted during vertical retrace
assign vsync_next = v_count_reg >= START_V_RETRACE && v_count_reg <= END_V_RETRACE;
// video only on when pixels are in both horizontal and vertical display region
assign video_on = (h_count_reg < H_DISPLAY) && (v_count_reg < V_DISPLAY);
// output signals
assign hsync = hsync_reg;
assign vsync = vsync_reg;
assign x = h_count_reg;
assign y = v_count_reg;
assign p_tick = pixel_tick;
endmodule
Step 2: Cell Module
Each cell is implemented as a finite-state machine with three different states- N, X, and O. N corresponds to an output of 00, X an output of 01, and O an output of 11.
While in the N state, the cell is unset and won't display anything on the grid. However, if the corresponding switch is set and, depending on the turn, it will transition to X or O.
While in the X or O state, the cell displays its shape on the grid and outputs its status accordingly. All cell status outputs are two bits wide. The cell cannot transition from this state, unless the game is reset.
Nine individual cells are defined in the gameLogic module, corresponding to those on the grid.
`timescale 1ns / 1ps
typedef enum {N, X, O} states;
module Cell(
input clk,
input Sel,
input Turn,
input Reset,
output reg [1:0] State
);
states PS, NS;
always_ff @ (posedge clk) begin
if (Reset)
PS = N;
else
PS = NS;
end
always_comb begin
case(PS)
N : begin
State = 'b00;
if (Sel && ~Turn)
NS = X;
else if (Sel && Turn)
NS = O;
else
NS = PS;
end
X : begin
State = 'b01;
NS = PS;
end
O : begin
State = 'b11;
NS = PS;
end
default : begin
NS = N;
end
endcase
end
endmodule
Step 3: Video Elements
This module interfaces with the video driver, and is responsible for defining the shapes that get drawn. It operates in-sync with the video driver's 25 MHz clock, deciding which color pixel to draw (red, white, black) based on its vertical and horizontal coordinates.
Each shape has a conditional statement, defining the bounds in which it is drawn. Additionally, the representation of each cell is dependent on its current state (N, X, or O).
Several localparams are factored in to the drawing of each shape
- hRes - Horizontal screen resolution, 650
- vRes - Vertical screen resolution, 480
- hBorder - Horizontal border from edge of screen, 100
- vBorder - Vertical border from edge of screen, 20
- hLinePos1 - Position of first horizonal grid line, vBorder + 147
- hLinePos2 - Position of second horizontal grid line, (vRes - 20) - 147
- vLinePos1 - Position of first horizonal grid line, hBorder + 147
- vLinePos2 - Position of second horizontal grid line, (hRes - 100) - 147
- sqBorder - Border between squares and grid, on all sides - 40
- plsBorder - Horizontal border between vertical lign of plus sign and grid, 30
p_tick, hPos, and vPos are all taken from the vga_sync module. The conditional draw checks are all run within an always block, triggered by p_tick..
A mux at the end selects one of three colors to use (black, white, red).
`timescale 1ns / 1ps
module videoElements(
input clk,
input [17:0] Cells,
input[8:0] Color,
input Turn,
output hsync, vsync,
output [11:0] rgb
);
// Constants
localparam hRes = 640;
localparam vRes = 480;
localparam hBorder = 100;
localparam vBorder = 20;
localparam hLinePos1 = (vBorder) + 147;
localparam hLinePos2 = (vRes - 20) - 147;
localparam vLinePos1 = (hBorder) + 147;
localparam vLinePos2 = (hRes - 100) - 147;
localparam sqBorder = 40;
localparam [4:0] plsBorder = 30;
localparam lineWeight = 2;
// Internal registers
reg [1:0] pDisp;
// Internal wires
wire [9:0] hPos, vPos;
wire p_tick;
//wire plsBorder = 30;
// instantiate vga_sync
vga_sync vga_sync_unit (
.clk(clk), .reset(reset), .hsync(hsync),
.vsync(vsync), .video_on(video_on), .p_tick(p_tick),
.x(hPos), .y(vPos));
// Draw elements on screen
always @(posedge p_tick) begin
// Horizontal grid
if (hPos > (hBorder) && hPos < (hRes - hBorder) &&
((vPos > hLinePos1 - lineWeight && vPos < hLinePos1 + lineWeight) ||
(vPos > hLinePos2 - lineWeight && vPos < hLinePos2 + lineWeight)))
pDisp = 1;
// Vertical grid
else if (vPos > (vBorder) && vPos < (vRes - vBorder) &&
((hPos > vLinePos1 - lineWeight && hPos < vLinePos1 + lineWeight) ||
(hPos > vLinePos2 - lineWeight && hPos < vLinePos2 + lineWeight)))
pDisp = 1;
// Cell 1
else if (
((hPos > hBorder + sqBorder + (Cells[1] ? 0 : plsBorder) && hPos < vLinePos1 - sqBorder - (Cells[1] ? 0 : plsBorder) &&
vPos > vBorder + sqBorder && vPos < hLinePos1 - sqBorder) ||
(hPos > hBorder + sqBorder && hPos < vLinePos1 - sqBorder &&
vPos > vBorder + sqBorder + (Cells[1] ? 0 : plsBorder) && vPos < hLinePos1 - sqBorder - (Cells[1] ? 0 : plsBorder))) &&
Cells[0])
pDisp = {Color[0],1'b1};
// Cell 2
else if (
((hPos > vLinePos1 + sqBorder + (Cells[3] ? 0 : plsBorder) && hPos < vLinePos2 - sqBorder - (Cells[3] ? 0 : plsBorder) &&
vPos > vBorder + sqBorder && vPos < hLinePos1 - sqBorder) ||
(hPos > vLinePos1 + sqBorder && hPos < vLinePos2 - sqBorder &&
vPos > vBorder + sqBorder + (Cells[3] ? 0 : plsBorder) && vPos < hLinePos1 - sqBorder - (Cells[3] ? 0 : plsBorder))) &&
Cells[2])
pDisp = {Color[1],1'b1};
// Cell 3
else if (
((hPos > vLinePos2 + sqBorder + (Cells[5] ? 0 : plsBorder) && hPos < (hRes - hBorder) - sqBorder - (Cells[5] ? 0 : plsBorder) &&
vPos > vBorder + sqBorder && vPos < hLinePos1 - sqBorder) ||
(hPos > vLinePos2 + sqBorder && hPos < (hRes- hBorder) - sqBorder &&
vPos > vBorder + sqBorder + (Cells[5] ? 0 : plsBorder) && vPos < hLinePos1 - sqBorder - (Cells[5] ? 0 : plsBorder))) &&
Cells[4])
pDisp = {Color[2],1'b1};
// Cell 4
else if (
((hPos > hBorder + sqBorder + (Cells[7] ? 0 : plsBorder) && hPos < vLinePos1 - sqBorder - (Cells[7] ? 0 : plsBorder) &&
vPos > hLinePos1 + sqBorder && vPos < hLinePos2 - sqBorder) ||
(hPos > hBorder + sqBorder && hPos < vLinePos1 - sqBorder &&
vPos > hLinePos1 + sqBorder + (Cells[7] ? 0 : plsBorder) && vPos < hLinePos2 - sqBorder - (Cells[7] ? 0 : plsBorder))) &&
Cells[6])
pDisp = {Color[3],1'b1};
// Cell 5
else if (
((hPos > vLinePos1 + sqBorder + (Cells[9] ? 0 : plsBorder) && hPos < vLinePos2 - sqBorder - (Cells[9] ? 0 : plsBorder) &&
vPos > hLinePos1 + sqBorder && vPos < hLinePos2 - sqBorder) ||
(hPos > vLinePos1 + sqBorder && hPos < vLinePos2 - sqBorder &&
vPos > hLinePos1 + sqBorder + (Cells[9] ? 0 : plsBorder) && vPos < hLinePos2 - sqBorder - (Cells[9] ? 0 : plsBorder))) &&
Cells[8])
pDisp = {Color[4],1'b1};
// Cell 6
else if (
((hPos > vLinePos2 + sqBorder + (Cells[11] ? 0 : plsBorder) && hPos < (hRes - hBorder) - sqBorder - (Cells[11] ? 0 : plsBorder) &&
vPos > hLinePos1 + sqBorder && vPos < hLinePos2 - sqBorder) ||
(hPos > vLinePos2 + sqBorder && hPos < (hRes- hBorder) - sqBorder &&
vPos > hLinePos1 + sqBorder + (Cells[11] ? 0 : plsBorder) && vPos < hLinePos2 - sqBorder - (Cells[11] ? 0 : plsBorder))) &&
Cells[10])
pDisp = {Color[5],1'b1};
// Cell 7
else if (
((hPos > hBorder + sqBorder + (Cells[13] ? 0 : plsBorder) && hPos < vLinePos1 - sqBorder - (Cells[13] ? 0 : plsBorder) &&
vPos > hLinePos2 + sqBorder && vPos < (vRes - vBorder) - sqBorder) ||
(hPos > hBorder + sqBorder && hPos < vLinePos1 - sqBorder &&
vPos > hLinePos2 + sqBorder + (Cells[13] ? 0 : plsBorder) && vPos < (vRes - vBorder) - sqBorder - (Cells[13] ? 0 : plsBorder))) &&
Cells[12])
pDisp = {Color[6],1'b1};
// Cell 8
else if (
((hPos > vLinePos1 + sqBorder + (Cells[15] ? 0 : plsBorder) && hPos < vLinePos2 - sqBorder - (Cells[15] ? 0 : plsBorder) &&
vPos > hLinePos2 + sqBorder && vPos < (vRes - vBorder) - sqBorder) ||
(hPos > vLinePos1 + sqBorder && hPos < vLinePos2 - sqBorder &&
vPos > hLinePos2 + sqBorder + (Cells[15] ? 0 : plsBorder) && vPos < (vRes - vBorder) - sqBorder - (Cells[15] ? 0 : plsBorder))) &&
Cells[14])
pDisp = {Color[7],1'b1};
// Cell 9
else if (
((hPos > vLinePos2 + sqBorder + (Cells[17] ? 0 : plsBorder) && hPos < (hRes - hBorder) - sqBorder - (Cells[17] ? 0 : plsBorder) &&
vPos > hLinePos2 + sqBorder && vPos < (vRes - vBorder) - sqBorder) ||
(hPos > vLinePos2 + sqBorder && hPos < (hRes- hBorder) - sqBorder &&
vPos > hLinePos2 + sqBorder + (Cells[17] ? 0 : plsBorder) && vPos < (vRes - vBorder) - sqBorder - (Cells[17] ? 0 : plsBorder))) &&
Cells[16])
pDisp = {Color[8],1'b1};
// Background
else
pDisp = 0;
end
// output
assign rgb = (pDisp[0]) ? (pDisp[1] ? 'hF00 : 'hFFF) : 'h000;
endmodule
Step 4: Game State
This module uses another finite-state machine, which has two states (PLAY, WIN). While in the PLAY state, it checks for matches between rows, columns, and diagonals. The two-bit status of all cells is concatenated and inputted as an 18-bit value. Only three matching, non-zero cells in a row are valid.
Once in the WIN state, the game will stay in the WIN state until it is reset. The winState output will also be set to 1, preventing any further input from being taken.
The colors are outputted as a 9-bit binary number, with each digit corresponding to a single cell to be colored. This is sent into the videoElements module which uses them to color the individual cells red.
`timescale 1ns / 1ps
typedef enum {PLAY, WIN} gameStates;
module gameState(
input Reset,
input clk,
input [17:0] Cells,
output reg winState,
output reg [8:0] Color
);
gameStates PS, NS;
always_ff @ (posedge clk) begin
if (Reset)
PS = PLAY;
else
PS = NS;
end
always_comb begin
case(PS)
PLAY : begin
winState = 0;
// Horizontal
if (Cells[1:0] == Cells [3:2] && Cells [3:2] == Cells[5:4] && Cells[1:0] != 0) begin
Color = 'b000000111;
NS = WIN;
end
else if (Cells[7:6] == Cells [9:8] && Cells[9:8] == Cells[11:10] && Cells[7:6] != 0) begin
Color = 'b000111000;
NS = WIN;
end
else if (Cells[13:12] == Cells [15:14] && Cells[15:14] == Cells[17:16] && Cells[13:12] != 0) begin
Color = 'b111000000;
NS = WIN;
end
// Vertical
else if (Cells[1:0] == Cells [7:6] && Cells [7:6] == Cells[13:12] && Cells[1:0] != 0) begin
Color = 'b001001001;
NS = WIN;
end
else if (Cells[3:2] == Cells [9:8] && Cells[9:8] == Cells[15:14] && Cells[3:2] != 0) begin
Color = 'b010010010;
NS = WIN;
end
else if (Cells[5:4] == Cells [11:10] && Cells[11:10] == Cells[17:16] && Cells[5:4] != 0) begin
Color = 'b100100100;
NS = WIN;
end
// Diagonal
else if (Cells[1:0] == Cells [9:8] && Cells [9:8] == Cells[17:16] && Cells[1:0] != 0) begin
Color = 'b100010001;
NS = WIN;
end
else if (Cells[5:4] == Cells [9:8] && Cells[9:8] == Cells[13:12] && Cells[5:4] != 0) begin
Color = 'b001010100;
NS = WIN;
end
Color = 'b001010100;
NS = WIN;
end
else begin
Color = 0;
NS = PS;
end
end
WIN : begin
winState = 1;
NS = WIN;
end
default : begin
Color = 0;
winState = 0;
NS = PLAY;
end
endcase
end
endmodule
Step 5: Game Logic
The game logic module ties everything together- input, graphics, cells, and game state. It also contains a routine that synchronously alternates between turns (X and O).
All nine cells are individually defined. Input is taken within an always_comb block, with priority given to the highest-value switch. If winState = 1, input is blocked. The individual cell selected by the input block is defined in sqrSel.
hsync, vsync, and rgb are all outputted from the videoElements module. winState is outputted from the gameStates module. Switch input and Reset is all taken directly from the board, as defined in the constraints file.
`timescale 1ns / 1ps
module gameLogic(
// Video
input wire clk,
output wire hsync, vsync,
output wire [11:0] rgb,
output wire winState,
// Control
input [8:0] In,
input Reset
);
reg [8:0] sqrSel;
reg [17:0] Cells;
reg [8:0] prevIn;
reg [8:0] myIn;
reg Turn;
//wire winState;
wire [8:0] Color;
// Input decoding
always_comb begin
if(!winState) begin
if (In[6] && !Cells[16]) sqrSel = 'b100000000;
else if (In[7] && !Cells[14]) sqrSel = 'b010000000;
else if (In[8] && !Cells[12]) sqrSel = 'b001000000;
else if (In[3] && !Cells[10]) sqrSel = 'b000100000;
else if (In[4] && !Cells[8]) sqrSel = 'b000010000;
else if (In[5] && !Cells[6]) sqrSel = 'b000001000;
else if (In[0] && !Cells[4]) sqrSel = 'b000000100;
else if (In[1] && !Cells[2]) sqrSel = 'b000000010;
else if (In[2] && !Cells[0]) sqrSel = 'b000000001;
else sqrSel = 'b000000000;
end
end
always @ (negedge clk) begin
if (prevIn != myIn && myIn != 0)
Turn = ~Turn;
prevIn = myIn;
myIn = In;
end
// Win checking
gameState state (
.clk(clk), .Reset(Reset), .Cells(Cells), .winState(winState), .Color(Color)
);
// Video generator
videoElements VGA (
.clk(clk), .hsync(hsync), .vsync(vsync), .rgb(rgb), .Cells(Cells), .Color(Color)
);
// Board cells
Cell cell1 (
.clk(clk), .Sel(sqrSel[0]), .Turn(Turn), .Reset(Reset),
.State(Cells[1:0])
);
Cell cell2 (
.clk(clk), .Sel(sqrSel[1]), .Turn(Turn), .Reset(Reset),
.State(Cells[3:2])
);
Cell cell3 (
.clk(clk), .Sel(sqrSel[2]), .Turn(Turn), .Reset(Reset),
.State(Cells[5:4])
);
Cell cell4 (
.clk(clk), .Sel(sqrSel[3]), .Turn(Turn), .Reset(Reset),
.State(Cells[7:6])
);
Cell cell5 (
.clk(clk), .Sel(sqrSel[4]), .Turn(Turn), .Reset(Reset),
.State(Cells[9:8])
);
Cell cell6 (
.clk(clk), .Sel(sqrSel[5]), .Turn(Turn), .Reset(Reset),
.State(Cells[11:10])
);
Cell cell7 (
.clk(clk), .Sel(sqrSel[6]), .Turn(Turn), .Reset(Reset),
.State(Cells[13:12])
);
Cell cell8 (
.clk(clk), .Sel(sqrSel[7]), .Turn(Turn), .Reset(Reset),
.State(Cells[15:14])
);
Cell cell9 (
.clk(clk), .Sel(sqrSel[8]), .Turn(Turn), .Reset(Reset),
.State(Cells[17:16])
);
endmodule
Step 6: Constraints
The constraints file is a modified version of the one from embedded thoughts.com. A clock signal is routed into clk, the basys board's first 9 switches are mapped to In, RGB, hsync and vsync are mapped to VGA, and winState is mapped to the first LED. All these interface with gameLogic, mind you.
# Clock signal
# Credit to embeddedthoughts.com
# https://embeddedthoughts.com/2016/07/29/driving-a-vga-monitor-using-an-fpga/
set_property PACKAGE_PIN W5 [get_ports clk]
set_property IOSTANDARD LVCMOS33 [get_ports clk]
create_clock -add -name sys_clk_pin -period 10.00 -waveform {0 5} [get_ports clk]
# Switches
set_property PACKAGE_PIN V17 [get_ports {In[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {In[0]}]
set_property PACKAGE_PIN V16 [get_ports {In[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {In[1]}]
set_property PACKAGE_PIN W16 [get_ports {In[2]}]
set_property IOSTANDARD LVCMOS33 [get_ports {In[2]}]
set_property PACKAGE_PIN W17 [get_ports {In[3]}]
set_property IOSTANDARD LVCMOS33 [get_ports {In[3]}]
set_property PACKAGE_PIN W15 [get_ports {In[4]}]
set_property IOSTANDARD LVCMOS33 [get_ports {In[4]}]
set_property PACKAGE_PIN V15 [get_ports {In[5]}]
set_property IOSTANDARD LVCMOS33 [get_ports {In[5]}]
set_property PACKAGE_PIN W14 [get_ports {In[6]}]
set_property IOSTANDARD LVCMOS33 [get_ports {In[6]}]
set_property PACKAGE_PIN W13 [get_ports {In[7]}]
set_property IOSTANDARD LVCMOS33 [get_ports {In[7]}]
set_property PACKAGE_PIN V2 [get_ports {In[8]}]
set_property IOSTANDARD LVCMOS33 [get_ports {In[8]}]
set_property PACKAGE_PIN U16 [get_ports {winState}]
set_property IOSTANDARD LVCMOS33 [get_ports {winState}]
# Buttons
set_property PACKAGE_PIN U18 [get_ports {Reset}]
set_property IOSTANDARD LVCMOS33 [get_ports {Reset}]
#VGA Connector
set_property PACKAGE_PIN G19 [get_ports {rgb[8]}]
set_property IOSTANDARD LVCMOS33 [get_ports {rgb[8]}]
set_property PACKAGE_PIN H19 [get_ports {rgb[9]}]
set_property IOSTANDARD LVCMOS33 [get_ports {rgb[9]}]
set_property PACKAGE_PIN J19 [get_ports {rgb[10]}]
set_property IOSTANDARD LVCMOS33 [get_ports {rgb[10]}]
set_property PACKAGE_PIN N19 [get_ports {rgb[11]}]
set_property IOSTANDARD LVCMOS33 [get_ports {rgb[11]}]
set_property PACKAGE_PIN N18 [get_ports {rgb[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {rgb[0]}]
set_property PACKAGE_PIN L18 [get_ports {rgb[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {rgb[1]}]
set_property PACKAGE_PIN K18 [get_ports {rgb[2]}]
set_property IOSTANDARD LVCMOS33 [get_ports {rgb[2]}]
set_property PACKAGE_PIN J18 [get_ports {rgb[3]}]
set_property IOSTANDARD LVCMOS33 [get_ports {rgb[3]}]
set_property PACKAGE_PIN J17 [get_ports {rgb[4]}]
set_property IOSTANDARD LVCMOS33 [get_ports {rgb[4]}]
set_property PACKAGE_PIN H17 [get_ports {rgb[5]}]
set_property IOSTANDARD LVCMOS33 [get_ports {rgb[5]}]
set_property PACKAGE_PIN G17 [get_ports {rgb[6]}]
set_property IOSTANDARD LVCMOS33 [get_ports {rgb[6]}]
set_property PACKAGE_PIN D17 [get_ports {rgb[7]}]
set_property IOSTANDARD LVCMOS33 [get_ports {rgb[7]}]
set_property PACKAGE_PIN P19 [get_ports hsync]
set_property IOSTANDARD LVCMOS33 [get_ports hsync]
set_property PACKAGE_PIN R19 [get_ports vsync]
set_property IOSTANDARD LVCMOS33 [get_ports vsync]
set_property PACKAGE_PIN U18 [get_ports reset]
set_property IOSTANDARD LVCMOS33 [get_ports reset]
Step 7: Game Play
- The game starts out with an empty grid
- Players take turns flipping the first 9 switches on the Basys Board, one at a time
- Each time a switch is flipped, a turn is taken and the chosen cell is turned into an X or an O
- When a row of three matching cells is made (horizontal, vertical, diagonal), the cells are turned red and the game is won. No more moves can be made until the game is reset using the middle button on the control pad.
Watch the demo here