Introduction: VHDL Basys3: Connect 4 Game

About: Electrical Engineer student at Cal Poly


This is a Connect 4 Digital Logic Game designed in VHDL using the Vivado Software and programmed to the Basys3 Board. The construction and design of this project is intermediate, but newcomers can copy the steps and build the digital game.

The game operates like the Connect 4 game. Players can move their cursor across the screen using the left and right buttons found on the board. Pressing the middle button on the board will cause the player to place their marker on that column and then it will become the next player's turn. Once a player wins, the game can be reset by pressing the up button on the board.

Step 1: Quick Details and Materials

Quick Technical Details:

  • Utilizes three sets of the PMOD connections on the board (JA,JB, JC)
    • 8 Pins (Excluding Vcc & GND Pins) used for each PMOD connector
    • JA - Control of Rows
    • JB - Control of Green Columns
    • JC - Control of Red Columns
  • Screen clock operates at 960Hz
    • Only 8 LEDs are on at a given time. Screen refreshes at a fast enough clock speed that the illusion is given that more than 8 LEDs are on at a given time.
  • Button clock operates at 5Hz; Optionally can be fined tuned by editing VHDL code.
  • Internal resistance of Darlington Arrays is sufficient to prevent LED burn-out

The game is constructed using the following components and tools:

  • (1) Basys3 Board
  • (2) LED Matrix Bi-color 8x5:
  • (2) ULN2803 - Darlington Transistor Arrays - Datasheet
  • Spools of Wire
  • Jumper Wires
  • Wire Stripper
  • Breadboards (Large Square should be enough)
  • Multimeter and Power Supply (Troubleshooting)

Step 2: Connecting the Hardware


The wiring of the project can be extremely convoluted, please take your time and verify that all the connections are correct one set at a time.

The project involves using two LED screens but are combined to form one large screen. This can be accomplished by connect all the rows to the same point. Because each screen is bi-color, the red and green rows of one screen must also be tied to the red and green rows of the other screen. By doing this, we can control all the rows with only 8 pins. The other 16 pins are used to control the columns of display. The 8 pins for the can be connected directly via jumper cables to the pmod connectors. Pmod connections first go to the input of the ULN2083A and the output of the ULN2083A is connected directly to the column on the screen. Because the design is an 8x8, some columns will physically not be connected.

  • JA: Row connections: Row 1 to JA:1 to Row 8 for JA:10.
  • JA: Red Column connections:
  • JC: Green Column connections

Please refer to the images posted to know which pins correspond to which rows/columns.

Note: The transistors have built in resistances, so the LEDs do not require additional resistance to be connected to them in series.

Step 3: Technical Explanation: Screen

The screen is operating on the persistence of vision. The screen is refreshing so fast, that the human eye can not visibly detect that some LEDs are rapidly being turned off and on. In fact, by slowing down the display clock, one can notice the flashing.

The display turns on all eight rows according to the data stored for those rows, and the display turns on one column. Then it quickly transitions to the next data entry for the eight rows and turns on the next column -- while having all other columns off. This process this continues at an fast enough clock speed that the flickering of the LED becomes unnoticeable.

Data storage for the display is initialized immediately after architecture in the VHDL file in the following manner:

signal RedA, RedB, RedC, RedD, RedE, RedF, RedG, RedH : std_logic_vector (7 downto 0) := "00000000"; 
signal GreenA, GreenB, GreenC, GreenD, GreenE, GreenF, GreenG, GreenH : std_logic_vector  (7 downto 0) := "00000000"; -- Row Data depending on column: GREEN

The following a small snippet of the process that control the LED display matrix.

-- Process that Controls LED display matrix
display : process (ColCLK) -- 0 - 16 to refresh both the 8X8 RED and 8x8 GREEn matrix variable RowCount : integer range 0 to 16 := 0; begin if (rising_edge(ColCLK)) then if (RowCount = 0) then DORow <= RedA; -- Row Data for corresponding Column DOCol <= "1000000000000000"; -- Column Trigger -- Repeat this code for all the way down to "0000000000000001"
-- Change to RedB,RedC...GreenA, GreenB...GreenH

At the end of the GreenH, right before the process terminates this snippet is included to reset the RowCount back to zero.

if (RowCount = 15) then -- Restart refresh from column A
RowCount := 0; else RowCount := RowCount + 1; -- Shift through columns end if;

Now, to explain the clock which is in the sensitivity list of the display process. The Basys3 board has an internal clock operating at 100MHz. For our purposes, this is too fast of a clock so we will need to divide this clock to a 960Hz clock using the following process.

-- Clock process operating at 960Hz
CLKDivider : process (CLK) variable clkcount : integer range 0 to 52083 := 0; begin if (rising_edge(CLK)) then clkcount := clkcount + 1; if (clkcount = 52083) then ColCLK <= not(ColCLK); clkcount := 0; end if; end if; end process;

Step 4: Technical Explanation: Changing the Information Displayed

In the VHDL code, the information or data that will be displayed to the screen is controlled by the cursor process, which has a different clock in its sensitivity list. This code was called BtnCLK, a clock designed to minimize debouching of the buttons when they are pressed. This is included so that if a button is pressed, the cursor on the top row doesn't move very rapidly across the columns.

-- Clock process operating at 5 Hz.
ButtonCLK : process (CLK) variable btnclkcount : integer range 0 to 10000001 := 0; begin if (rising_edge(CLK)) then if (btnclkcount = 10000000) then btnclkcount := 0; BtnCLK <= not(BtnCLK); else btnclkcount := btnclkcount + 1; end if; end if; end process;

With the BtnCLK signal output of this process, we can now explain the cursor process. The cursor process only has BtnCLK in its sensitivity list but in the code block, the state of the buttons are checked and this will cause the data for the RedA, RedB...GreenH to change. Here is a snippet of the cursor code, which includes the reset block and the block for the first column.

cursor : process (BtnCLK)
variable OCursorCol : STD_LOGIC_VECTOR (2 downto 0) := "000"; -- OCursorCol keeps track of previous column variable NCursorCol : STD_LOGIC_VECTOR (2 downto 0) := "000"; -- NCursorCol sets new cursor column begin --RESET condition (UP Button) --Board is cleared for game to restart if (rising_edge(BtnCLK)) then if (RST = '1') then RedA <= "00000000"; RedB <= "00000000"; RedC <= "00000000"; RedD <= "00000000"; RedE <= "00000000"; RedF <= "00000000"; RedG <= "00000000"; RedH <= "00000000"; GreenA <= "00000000"; GreenB <= "00000000"; GreenC <= "00000000"; GreenD <= "00000000"; GreenE <= "00000000"; GreenF <= "00000000"; GreenG <= "00000000"; GreenH <= "00000000"; end if; -- Case statements depending on Current column of RED or GREEN cursor case (OCursorCol) is -- When Column A when "000" => if (Lbtn = '1') then NCursorCol := "111"; -- Column H elsif (Rbtn = '1') then NCursorCol := "001"; -- Column B elsif (Cbtn = '1') then NCursorCol := OCursorCol; -- Column stays the same NTurnState <= not(TurnState); -- Triggers next player's turn -- Checks current column from bottom to top and turns on first LED that is not on. Color depends on current player's cursor color. for ck in 7 downto 1 loop if (RedA(0) = '1') and (RedA(ck) = '0') and (GreenA(ck) = '0') then RedA(Ck) <= '1'; RedA(0) <= '0'; EXIT; end if;
                        if (GreenA(0) = '1') and (RedA(ck) = '0') and (GreenA(ck) = '0') then
                            GreenA(Ck) <= '1';
                            GreenA(0) <= '0';
                        end if;
                    end loop;
                end if;
                -- Case that turns on LEDs on cursor row/column.
                    -- Color depends on current Player's turn.
                    -- Location depends on Button presses/ Old location
                case (TurnState) is
                    when '0' => -- Red Player
                        GreenA(0) <= '0';
                        if (NCursorCol = OCursorCol) then -- If nothing was pressed
                            RedA(0) <= '1';
                        elsif (NCursorCol = "111") then --  If Lbtn was pressed
                            RedH(0) <= '1';
                            RedA(0) <= '0';
                        elsif (NCursorCol = "001") then -- Iff Rbtn was pressed
                            RedB(0) <= '1';
                            RedA(0) <= '0';
                        end if;
                    when '1' => -- Green Player
                        RedA(0) <= '0';
                        if (NCursorCol = OCursorCol) then
                            GreenA(0) <= '1';
                        elsif (NCursorCol = "111") then
                            GreenH(0) <= '1';
                            GreenA(0) <= '0';
                        elsif (NCursorCol = "001") then
                            GreenB(0) <= '1';
                            GreenA(0) <= '0';
                        end if;
                 end case;

Note, the first case statement called: OCursorCol (which stands for Old Cursor Column) is the beginning of the finite state machine. Each column of the display is treated as its own state in the FSM. There are 8 columns so a 3-bit binary number set was used to identify each column as a state. How the FSM moves between state is dependent on the button which is pressed. In the snippet above, if the left button is pressed, the FSM will move to "111" which would be the last column of the display. If the right button is pressed, the FSM will move to "001" which would be the second column of the display.

If the middle button is pressed, the FSM will NOT move to a new state but will instead trigger a change in the TurnState signal, which is a one-bit signal to note which player's turn it is. Additionally, the middle button will run a code block that checks if there is an empty row at at the very bottom all the way to the top. It will try to place a marker in the lowest, unfilled row. Remember, this is a connect four game.

In the nested case statement called: TurnState, we alter what the cursor color is and which column on the first row we want to change the data for so that the display process can reflect the change.

We repeat this basic code for the remaining seven cases. The FSM diagram can be helpful to understand how the states are changing.

Step 5: Code

This is the functional code for the Connect 4 that can be compiled in VHDL using the Vivado Software.

A constraint is also provided to allow you to get the game up and running.

We provided a block diagram which explains how the inputs and outputs of each process are interconnected.