Introduction
Recently I came across a bit of a blast from the past when one of my followers on X/Twitter had been asked by his daughter to make a simple embedded system that could play the google chrome dinosaur game. Their solution was simple and impressive using two embedded system favourites, ESP32 and Python.
Seeing this reminded me that a few years ago I developed a solution for this game which used a different approach to that followed by many. My first dino game project used PYNQ in combination with a web camera and image processing algorithms, to control the dinosaur.
However, upon seeing the simple implementation, it got me thinking that it would be fun to try and create an FPGA based solution which could play the game using this simple approach.
The simplest way to play the game is to use a light dependent resistor (LDR), connected the screen. The LDR changes resistance from the white back ground to the black cactus's, as they scroll by. By detecting this change in resistance we are able to know when to make the Trex jump. To make the Trex jump we use a servo to press a button normally either the space or up arrow on a keyboard.
To detect the cactus we need to correctly detect the resistance change from white to black background. As we will see this can be achieved pretty simply by a potential divider. However, the change in voltage will not be sufficient to register as a logic level (well not without some analogue design). As such we need to be able to detect the change in voltage using another method. The simplest of these is to use a ADC to measure the voltages.
An FPGA based solution can leverage the internal ADC provided by the device as it is a static (ish) signal we will be monitoring for the resistance change. Similarly the servo device and decision logic can be implemented very simply within the logic. We do not need to have processors, or even state machines to be able to play the game.
To implement the project I selected a AMD Spartan™ 7 device, we could implement this in the smallest XC7S6 device. However, the XC7S6 devices do not contain an XADC as such we will be using the Arty S7-50 as that contains a XADC and is easily accessible.
The front end design is basically a resistor potential divider, R1 is the LDR while R2 is a fixed resistor. How we design this will result in different voltages being output to the XADC when exposed to black or white pixels.
I bought a pack of LDR from Amazon which provide several different LDRs which have differing resistance ranges.
I selected a LDR from those purchased which had a range of resistance between 2 and 5K Ohms. My plan was to drive it from the 3v3 available on the Arty S7 shield connector.
Using a little empirical measurements with a multi meter and a selected LDR held to the screen of the game. I was able to determine the resistance on the white background was approx. 2k2 Ohms and the measurement on the black cactus was approx. 4K Ohms.
This makes sense as the LDR resistance should decrease with increased illumination.
XADC ChallengeMy plan is to use the XADC to measure the voltage output by the voltage divider however, we need to be sure the input to the XADC does not get stressed or damaged.
To ensure this is the case, we can simulate the design in multisim live to show the predicted outputs of the resistor network. In reality I did a quick calculation in my log book to show no risk to the XADC input range, but the simulation shows it nicely.
Voltage output in normal operation e.g. white background
Voltage output when cactus detected
As the LDR will drop in resistance when exposed to sun light or bright office lights make sure your LDR is taped to the screen before you connect the input to the VP input on the FPGA.
FPGA DesignThe next step of the development is to create a AMD Vivado™ Design Suite project, targeting the Arty S7 board. While I am selecting the Arty S7-50 board we can do this with any AMD device which has a XADC / Sysmon and has the VP/VN and at least one other pin for the Servo Drive.
Once the project is open create a new IP Integrator block diagram
With the block diagram created we need to add onto the diagram the XADC Wizard and configure it to output its data using AXI Stream and only to output Vp/Vn on that stream, also ensure it is configured for unipolar operation as shown below.
Enable the AXI Stream and disable the configuration interface
Set the averaging for 256 samples
Selectthe Vp/Vn Channel
Summary
The next step is to write the controller for the servo.
Of course to do this we need to understand how servos function, typically there drive position is updated every 20 ms. Servos have a limited range of movement between 0 and 180 degrees, normally the servo is driven with a 1.5 ms pulse every 20 ms and that will maintain the servo in the 90 degree position. Reducing that pulse to 0.5 ms will drive the servo to 0 degrees, increasing the width will drive the servo to 180 degrees.
If we drive it anywhere between 0.5 ms and 2.5 ms we get the associated movement.
To press the key on the keyboard all we need to do is keep the servo in the nominal position and then drive it to either extreme.
One challenge however is we need to be sure the movement is sufficent to press the key and not momentary e.g. it does not just last for one 20 ms cycle.
As such the code below, detects the change of resistance based on the XADC value and then drives the servo to the drive position for at least 100 ms. This ensure the key is actually pressed.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity servo is port(
i_clk : in std_logic;
i_data : in std_logic_vector(15 downto 0);
i_val : in std_logic;
o_pwm : out std_logic
);
end entity;
architecture rtl of servo is
constant upper_width : integer := 21;
constant max_count : integer := 2_000_000;
signal s_counter : unsigned(upper_width-1 downto 0):=(others=>'0');
signal s_wave_pos : unsigned(3 downto 0):=(others=>'0');
signal s_op_cnt : unsigned(upper_width-1 downto 0):=(others=>'0');
signal s_delay_reg : std_logic_vector(4 downto 0):=(others=>'0');
begin
process(i_clk)
begin
if rising_edge(i_clk) then
if s_counter = max_count - 1 then
s_counter <= (others=>'0');
s_delay_reg <= s_delay_reg(s_delay_reg'high - 1 downto 0) & '0';
if unsigned(i_data) < 30000 then
s_delay_reg <= (others => '1');
s_op_cnt <= to_unsigned(100_000,upper_width);
elsif s_delay_reg = "00000" then
s_op_cnt <= to_unsigned(150_000,upper_width);
end if;
else
s_counter <= s_counter + 1;
end if;
end if;
end process;
o_pwm <= '1' when s_counter < s_op_cnt else '0';
end architecture;
This code needs to be added into the block diagram and the AXI stream from the XADC connected to the i_data input. To ensure the data streams over the AXI Stream in the block diagram I tied the t_ready signal high.
The completed diagram looks like below
I also added in a clocking wizard and a ILA to debug the ADC interface. The clocking wizard enables different boards with different clock frequencies to provide the 100 MHz reference needed. We can use the locked output also to provide the reset for the XADC.
When it comes to the XDC file the pinning is as below.
set_property IOSTANDARD LVCMOS33 [get_ports clk_in]
set_property PACKAGE_PIN E3 [get_ports clk_in]
set_property IOSTANDARD LVCMOS33 [get_ports o_pwm_0]
set_property PACKAGE_PIN G13 [get_ports o_pwm_0]
With that we can build the project and generate the bit stream.
To connect the servo to the board I used a PmodCon1 this enabled me to connect the
Cable ConnectionThe simple cable constructed which contained the LDR connected to the resistor, with the measurement point between the resistor and LDR. Is connected as follows on the Arty Board
The connections can be seen on the board
To test the module I used a multi arm gripper to be able to select the position of the LDR sensor against the screen
The servo is connected to the keyboard space bar.
Running this while playing the game, shows the solution works pretty well
Wrap UpThis project has been a fun project it shows how we can have a little fun with a FPGA project. This project provides us simple way to learn about using XADCs and servos.
The project can be found here on github
Comments