Games Controller

Home

To play a video game we need a game controller, a means of controlling our space ship, or bat :).

Serial Port

Figure 1 : serial port

The first game controller implemented was the existing serial port, to send ASCII characters to the FPGA from the keyboard i.e. WASD, W=Up, D=Fire and S=Down. This is fine for testing and is a useful tool for debugging i.e. allows you to print debug messages, variable values to the screen etc, but to develop a standalone SimpleCPU based games console we need some sort of game pad \ controller. Short video of this controller being used with the Pong game is available here: (Link).

Rotary Switch

Figure 2 : FPGA Board

One of the FPGA boards used to prototype the VGA controller has a rotary switch, as shown in figure 2. This input hardware is very similar to the hardware used in one of the controllers used in the Atari 2600 i.e. the Driving Controller. The Atari 2600 (Link) is the mother of all video game consoles, and for its age come with a surprisingly wide range of different controllers, as shown in figure 3.

Figure 3 : game controllers, joystick (left), paddle (middle), driving (right)

There were actually a couple more controllers in the Atari catalogue, i believe there was also a small keypad and a game pad. Looking at the controllers in figure 3, its may look like that the paddle and driving controllers are the same i.e. rotary position controllers. However, the paddle is analogue (continuous) and the joystick and driving controllers are digital (discrete). To help in this discussion consider the circuit diagrams of the Atari 2600 shown in figure 4.




Figure 4 : Atari 2600 schematics, interface (top), main board (bottom)

The pong paddle is implemented using a variable resister (top right schematic), producing an analogue voltage proportional to position i.e. on pins 5 and 9 (left and right 9-way D-connectors), for player 1 and player 2. Therefore, the paddle controller's rotational range is approximately 260 degrees, the rotational range of a typical variable resistor. These analogue signals are processed by the TIA chip (TV Interface Adaptor). This IC must have some sort of analogue to digital converter (ADC). Not sure how this was implemented, documentation for this IC is a little hard to find. However, knowing that the TIAs screen resolution was 160 x 192 pixels, the ADC resolution was probably less that 8bits i.e. the number of pixel positions that need to be represented: 8bits=256, 7bits=128, 6bits=64, 5bits=32. For a simple game like Pong, 5bits is probably sufficient, owing to its limited movement range. The joystick and driving controllers are implemented using switches. The joystick uses simple push-to-make switches (top left schematic), whilst the driving controller uses a rotary switch (top middle schematic). Therefore, the driving controller does not have rotational stop\limit i.e. you can keep rotating the dial forever. The rotary switch on the FPGA board is described in this document (Link) and shown in figure 5.



Figure 5 : rotary switch

The rotary switch has two switches: A and B. When rotated anti-clockwise i.e. to the left, the cam\teeth attached to the drive shaft closes switch B first then switch A. When rotated clockwise i.e. to the right, the cam\teeth attached to the drive shaft closes switch A first then switch B. Therefore, by detecting which switch closes first the hardware can determine direction, and by counting the number of pulses generated by these switch it can calculate rotation\distance. The hardware to process these signals (VHDL) is shown in figure 6 (taken from the Xilinx documentation). Note, i do like the switch debounce hardware used.






Figure 6 : rotary switch hardware

Using these designs a schematic component can be created allowing the simpleCPU to interface to the rotary switch, as shown in figure 7. The VHDL used to implement the left_right_leds component is shown below (taken from Xilinx reference doc), providing the outputs sw_event and sw_direction. It also provides the sw_pressed signal which is a third switch, triggered when you push down on the rotary switch, rather than rotating it (this will be the Fire button). As the rotary switch's state is polled it is only read once per main loop iteration, therefore, the sw_event signal is used to trigger a SR flip-flop, constructed from a D-type flip-flop. This ensures small position changes are not missed. The state of the push switch is not latched as it is assumed this will be held down for a longer period of time. This SR flip-flop is reset when the processor reads the state of the switch. Note, on reflection the SR flip-flop is not really needed as the main loop iteration time is relatively quick.




Figure 7 : rotary switch schematic

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;

entity left_right_leds is
Port (             
   clk             : in std_logic;
   clr             : in std_logic;

   rotary_a       : in std_logic;
   rotary_b       : in std_logic;
   rotary_press    : in std_logic;   

   sw_event         : out std_logic;
   sw_direction   : out std_logic;
   sw_pressed     : out std_logic;

   led             : out std_logic_vector(7 downto 0) );
end left_right_leds;


architecture Behavioral of left_right_leds is

    signal      rotary_a_in : std_logic;
    signal      rotary_b_in : std_logic;
    signal  rotary_press_in : std_logic;
    signal        rotary_in : std_logic_vector(1 downto 0);
    signal        rotary_q1 : std_logic;
    signal        rotary_q2 : std_logic;
    signal  delay_rotary_q1 : std_logic;
    signal     rotary_event : std_logic;
    signal      rotary_left : std_logic;

    signal      led_pattern : std_logic_vector(7 downto 0):= "00010000"; --initial value puts one LED on near the middle.
    signal        led_drive : std_logic_vector(7 downto 0);

begin

  rotary_filter: process(clk, clr)
  begin
    if clr='1'
   then 
      rotary_a_in     <= '0';
      rotary_b_in     <= '0';
      rotary_press_in <= '0';
      sw_pressed      <= '0';   
      rotary_q1       <= '0';
      rotary_q2       <= '0';
     
    elsif clk'event and clk='1' 
   then

      --Synchronise inputs to clock domain using flip-flops in input/output blocks.
      rotary_a_in     <= rotary_a;
      rotary_b_in     <= rotary_b;
      rotary_press_in <= rotary_press;
      sw_pressed      <= rotary_press;

      --concatinate rotary input signals to form vector for case construct.
      rotary_in <= rotary_b_in & rotary_a_in;

      case rotary_in is

        when "00" =>   rotary_q1 <= '0';         
                       rotary_q2 <= rotary_q2;
 
        when "01" =>   rotary_q1 <= rotary_q1;
                       rotary_q2 <= '0';

        when "10" =>   rotary_q1 <= rotary_q1;
                       rotary_q2 <= '1';

        when "11" =>   rotary_q1 <= '1';
                       rotary_q2 <= rotary_q2; 

        when others => rotary_q1 <= rotary_q1; 
                       rotary_q2 <= rotary_q2; 
      end case;

    end if;
  end process rotary_filter;

  direction: process(clk, clr)
  begin
    if clr='1'
    then
      delay_rotary_q1 <= '0';
      rotary_event    <= '0';
      rotary_left     <= '0';
      sw_event        <= '0';
      sw_direction    <= '0';
      
    elsif clk'event and clk='1' 
    then

      delay_rotary_q1 <= rotary_q1;
      
      if rotary_q1='1' and delay_rotary_q1='0' 
      then
        rotary_event <= '1';
        rotary_left <= rotary_q2;
        sw_event <= '1';
        sw_direction <= rotary_q2;
       else
        rotary_event <= '0';
        rotary_left <= rotary_left;
        sw_event <= '0';
        sw_direction <= rotary_left;
      end if;

    end if;
  end process direction;


  led_display: process(clk, clr)
  begin
    if clr='1'
    then
      led_pattern <= "00010000";
      led_drive   <= "00010000";
      
    elsif clk'event and clk='1' then

      if rotary_event='1' then
        if rotary_left='1' then 
          led_pattern <= led_pattern(6 downto 0) & led_pattern(7); --rotate LEDs to left
         else
          led_pattern <= led_pattern(0) & led_pattern(7 downto 1); --rotate LEDs to right
        end if;
      end if;

      if rotary_press_in='0' then
        led_drive <= led_pattern; 
       else
        led_drive <= led_pattern xor "11111111";  
      end if;

      --Ouput LED drive to the pins making use of the output flip-flops in input/output blocks.
      led <= led_drive; 

    end if;
  end process led_display;

end Behavioral;

Short video of this controller being used with the Pong game is available here: (Link).

Game pad

To make the game console more flexible i.e. not limited to FPGA boards with rotary switches, i needed a generally available games controller. Therefore, Googling for an off the shelf solution, a pre-made unit that doesn't cost tooo much, resulted in a lot of PC based controllers that use an USB interface. Designing the interface hardware for these i'm guessing will be tooo much fun, but watching so Youtube videos on the USB protocol i don't think it would be impossible. An alternative is to use retro gaming console controllers. Reading around the SNES controller's hardware looked simple and they only cost £ 5 :), shown in figure 8.


Figure 8 : SNES controller

This controller supports 12 switches and communicates using a serial protocol. Googling around on the web the protocol seems to be as shown in figure 9. Also found some discussion here: (Link). The online resources say that the SNES controller uses +5V, which reflects the voltages used in the 6502 based hardware the SNES was based on. However, the FPGA and most modern hardware uses +3.3V. Solutions, could uses a level shifter to convert the 3.3V outputs up to +5V, and the +5V input down to +3.3V, but as the ICs used in this replica controller work at +3.3V we can simply power the controller from +3.3V and remove the issue.



Figure 9 : SNES plug (top), protocol (bottom)

The protocol initially latches\captures button data by pulsing the latch input, then each data bit is shifted out on a falling clock edge. The hardware used to interface to this controller is shown below.

-- =============================================================================================================
-- *
-- * Copyright (c) Mike
-- *
-- * File Name: snes_controller.vhd
-- *
-- * Version: V1.0
-- *
-- * Release Date:
-- *
-- * Author(s): M.Freeman
-- *
-- * Description: SNES controller interface
-- *
-- * Conditions of Use: THIS CODE IS COPYRIGHT AND IS SUPPLIED "AS IS" WITHOUT WARRANTY OF ANY KIND, INCLUDING,
-- *                    BUT NOT LIMITED TO, ANY IMPLIED WARRANTY OF MERCHANTABILITY AND FITNESS FOR A
-- *                    PARTICULAR PURPOSE.
-- *
-- * Notes:
-- *
-- =============================================================================================================

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use ieee.std_logic_arith.all;
use ieee.std_logic_unsigned.all;

entity snes_controller is
Port ( 
    clk          : in  STD_LOGIC;
    clr          : in  STD_LOGIC;
    addr         : in  STD_LOGIC;
    data_in      : in  STD_LOGIC_VECTOR (15 downto 0);
    data_out     : out STD_LOGIC_VECTOR (15 downto 0);
    ce           : in  STD_LOGIC;
    we           : in  STD_LOGIC;
    snes_latch_o : out STD_LOGIC;
    snes_clk_o   : out STD_LOGIC;
    snes_data_in : in  STD_LOGIC);
end snes_controller;

architecture snes_controller_arch of snes_controller is

  TYPE state_type IS (SA0, SA1, SA2, SA3, SA4, SA5);
  SIGNAL present_state, next_state : state_type;
  
  SIGNAL data_packet : STD_LOGIC_VECTOR (11 downto 0);
  SIGNAL data_ce     : STD_LOGIC;
  SIGNAL data_rst    : STD_LOGIC; 
  
  SIGNAL delay_count     : STD_LOGIC_VECTOR (5 downto 0);
  SIGNAL delay_count_tc  : STD_LOGIC; 
  SIGNAL delay_count_rst : STD_LOGIC;
  SIGNAL delay_count_ce : STD_LOGIC;  
  
  SIGNAL bit_count     : STD_LOGIC_VECTOR (3 downto 0);
  SIGNAL bit_count_tc  : STD_LOGIC; 
  SIGNAL bit_count_rst : STD_LOGIC; 
  SIGNAL bit_count_ce  : STD_LOGIC; 
    
  SIGNAL busy : STD_LOGIC; 
  
begin

  data_out_bus_mux : PROCESS( addr, busy, data_packet )
  BEGIN
    if addr='0'
    then
      data_out <= "000000000000000" & busy;
    else
      data_out <= "0000" & data_packet;
    end if;
  END PROCESS; 
  
  shift_register : PROCESS ( clk, clr )
  BEGIN
    if clr='1'
    then
      data_packet <= (others => '0');
    elsif clk='1' and clk'event
    then
      if data_ce='1'
      then
        if data_rst='1'
        then
          data_packet <= (others => '0');
        else
          data_packet <= data_packet(10 downto 0) & snes_data_in;
        end if;
      end if;
    end if;
  END PROCESS; 
  
  delay_counter_sync : PROCESS ( clk, clr )
  BEGIN
    if clr='1'
    then
      delay_count <= (others => '0');
    elsif clk='1' and clk'event
    then
      if delay_count_ce='1'
      then
        if delay_count_rst='1'
        then
          delay_count <= (others => '0');  
        else
          delay_count <= delay_count + 1;
        end if;
      end if;
    end if;
  END PROCESS;   
  
  delay_counter_comb : PROCESS ( delay_count )
  BEGIN
    if delay_count = "11111"
    then
      delay_count_tc <= '1';
    else
      delay_count_tc <= '0';
    end if;
  END PROCESS;  
  
  bit_counter_sync : PROCESS ( clk, clr )
  BEGIN
    if clr='1'
    then
      bit_count <= (others => '0');
    elsif clk='1' and clk'event
    then
      if bit_count_ce='1'
      then
        if bit_count_rst='1'
        then
          bit_count <= (others => '0');  
        else
          bit_count <= bit_count + 1;
        end if;
      end if;
    end if;
  END PROCESS;  
   
  bit_counter_comb : PROCESS ( bit_count )
  BEGIN
    if bit_count = "1100"
    then
      bit_count_tc <= '1';
    else
      bit_count_tc <= '0';
    end if;
  END PROCESS;  
  
  controller_sync : PROCESS( clk, clr )
  BEGIN
    IF clr='1'
    THEN
      present_state <= SA0;
    ELSIF clk'event and clk='1'
    THEN
      present_state <= next_state;
    END IF;
  END PROCESS;

  controller_comb: PROCESS( present_state, ce, we, 
                            delay_count_tc, bit_count_tc,
                            data_ce, data_rst  )
  BEGIN
    snes_latch_o <= '0';
    snes_clk_o   <= '1';
       
    delay_count_rst <= '1';
    delay_count_ce  <= '1';  
      
    bit_count_rst   <= '1';
    bit_count_ce    <= '1';    
    
    data_rst  <= '0';
    data_ce   <= '0';
    
    busy      <= '0';
    
    CASE present_state IS
      WHEN SA0 =>
        if ce='1' and we='1'
        then 
          next_state <= SA1; 
        else
          next_state <= SA0;  
        end if;
        
      -- LATCH PULSE
      
      WHEN SA1 =>
        delay_count_rst <= '0';
        delay_count_ce  <= '1';   
        
        bit_count_rst <= '0';
        bit_count_ce  <= '0';   
                     
        data_rst  <= '1';
        data_ce   <= '1';
        
        snes_latch_o <= '1';
        snes_clk_o   <= '1';        
        
        busy <= '1';
        
        if delay_count_tc='1'
        then
          next_state <= SA2; 
        else
          next_state <= SA1;  
        end if;
        
      -- WAIT TO START
        
      WHEN SA2 =>
        delay_count_rst <= '0';
        delay_count_ce  <= '1';   
          
        bit_count_rst <= '0';
        bit_count_ce  <= '0';   
                       
        data_rst  <= '0';
        data_ce   <= '0';
          
        snes_latch_o <= '0';
        snes_clk_o   <= '1';        
          
        busy <= '1';
          
        if delay_count_tc='1'
        then
          next_state <= SA3; 
        else
          next_state <= SA2;  
        end if;   
        
      -- CLK LOW, CAPTURE DATA
          
      WHEN SA3 =>
        delay_count_rst <= '0';
        delay_count_ce  <= '1';   
            
        bit_count_rst <= '0';
        bit_count_ce  <= '1';   
                         
        data_rst  <= '0';
        data_ce   <= '1';
            
        snes_latch_o <= '0';
        snes_clk_o   <= '0';        
            
        busy <= '1';

        next_state <= SA4;  
        
      -- WAIT CLK LOW 
             
      WHEN SA4 =>
        delay_count_rst <= '0';
        delay_count_ce  <= '1';   
            
        bit_count_rst <= '0';
        bit_count_ce  <= '0';   
                         
        data_rst  <= '0';
        data_ce   <= '0';
            
        snes_latch_o <= '0';
        snes_clk_o   <= '0';        
            
        busy <= '1';
            
        if delay_count_tc='1'
        then
          next_state <= SA5; 
        else
          next_state <= SA4;  
        end if;         
          
      -- WAIT CLK HIGH
    
      WHEN SA5 =>
        delay_count_rst <= '0';
        delay_count_ce  <= '1';   
                
        bit_count_rst <= '0';
        bit_count_ce  <= '0';   
                             
        data_rst  <= '0';
        data_ce   <= '0';
                
        snes_latch_o <= '0';
        snes_clk_o   <= '1';        
                
        busy <= '1';
                
        if delay_count_tc='1'
        then
          if bit_count_tc='1'
          then
            next_state <= SA0; 
          else  
            next_state <= SA3; 
          end if;
        else
          next_state <= SA5;  
        end if;              
  
      WHEN OTHERS =>
        next_state <= SA0;  --default condition
            
    END CASE;
  END PROCESS;  

end snes_controller_arch;

Using this design a schematic component can be created allowing the simpleCPU to interface to the SNES controller, as shown in figure 10.


Figure 10 : SNES component

Short video of this controller being used with the Pong game is available here: (Link).

Creative Commons Licence

This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.

Contact email: mike@simplecpudesign.com

Back