1.4.2.9. Ping pong

This is a continuation of the project → vga image

1.4.2.9.1. Building the Project

  • Move to folder RedPitaya-FPGA/prj/Examples.

  • Uncomment the line “set project_name Vga_game” and comment out all files in the make_project.tcl file.

  • Open Vivado and in Vivado Tcl Console navigate to the base folder: RedPitaya-FPGA/prj/Examples.

    Logo
  • Then run the script by typing into the following command into the TCL console. If the TCL console is not open got to Tools → Run Tcl Script:

Take a moment to examine the block design.

If the Block Design is not open, click on Flow => Open Block Design from the top menu or select Open Block Design on the left-hand side of the window (under IP INTEGRATOR). When you are ready, click Generate Bitstream at the bottom-left part of the window to generate a bitstream file.

After you confirm that both Synthesis and Implementation will be executed beforehand, the longer process starts. After successful completion of synthesis, implementation, and bitstream generation, the bit file can be found at Examples/Led_blink/tmp/Led_blink/Led_blink.runs/impl_1/system_wrapper.bit.

This will make the full project, if you want to follow through with this tutorial you will need to install Xilinx Vitis. Afterward, run the Vga_draw project and continue with the tutorial.

1.4.2.9.2. Step-by-step tutorial

After the previous project started working, I wanted to be able to change the picture pattern, picture size, and location on the screen. I added a few more ports which will be later controlled through AXI_GPIO, with the Vitis application.

entity picture is
    Port (
        clk50: in STD_LOGIC;
        data_position: in unsigned(16 downto 0);
        Offset: in unsigned(15 downto 0);
        size: in unsigned(15 downto 0);
        data_in: in STD_LOGIC;
        hst: out unsigned(10 downto 0);
        vst: out unsigned(9 downto 0);
        rgb: out STD_LOGIC_VECTOR(2 downto 0));
end picture;
  • clk50 - 50 MHz clock

  • data_position - position to write the data + Write enable bit

  • Offset - picture location on the screen

  • size - picture size

  • data_in - “1” or “0” that is written in an array

  • hst, vst - current position on the screen

  • rgb - signal to be displayed on the screen

I created and connected another block design. As seen in the picture below:

Logo

1.4.2.9.2.1. Block diagram explained

If you are doing block design for the first time, I recommend the Zynq book as a good starting point because it explains the basic steps of how to build a project in Vivado.

Ok, let’s explain the block diagram:

  1. First, we start with the Zynq7 processing system, these are the brains.

  2. I added three AXI_GPIO blocks

    • The first one (axi_gpio_0) is connected to on-board LEDs, and it is there just to check if the program is running

    • The second one (axi_gpio_1) has two outputs, the first output controls the position on the screen, and the second output controls the picture size.

    • The third one (axi_gpio_2) also has two outputs, the first output is controlling where and when we write our data to the array, and on the second output, the actual data is coming in.

  1. picture IP (picture_0) is used to save the picture and to read from the array

  2. VGA IP (VGA_0) is used to set signals to synchronize the screen and output the data.

  3. The last important IP is Clocking Wizard (clk_wiz_0). Linux sets the clock on the FCLK_CLK0 port to 125 MHz, but we need a 50 MHz clock so we place a Clocking Wizard to lower the clock frequency to the desired rate.

Picture IP and VGA IP are located in RedPitaya-FPGA/prj/Examples/Vga_draw

1.4.2.9.2.2. Setting 50 MHz clock

First, we need to set the source clock from the ZYNQ7 IP.

Logo

After setting the 125 MHz clock we have to divide it to get 50 MHz that we use. For this, we need the Clocking Wizard.

Logo
Logo

Run Synthesis, Implementation and Generate Bitstream for the project Vga_draw.

1.4.2.9.2.3. Exporting hardware

Go to File → Export → Export Hardware.

Logo

Use a fixed platform type.

Logo

Select Include bitstream.

Logo

Complete the instructions and note the location of the file. In my case, the file is named design_1_wrapper (after the top module of the project).

1.4.2.9.2.4. Creating Vitis platform project

Start Vitis

Logo

Press → Create Platform Project. Set the project name and choose Create from hardware specification (XSA) Then point to the generated xsa file (Do not forget to specify the operating system as Linux):

Logo

And press finish.

The last step is building:

Logo

Now we can use the resulting platform to write a program.

1.4.2.9.2.5. Creating Vitis application project

Go to File → New → Application project. Click next and select the platform you just created

Logo

Press next and set the project name (Ping_pong). Leave the rest of the parameters by default. When prompted to choose a domain template, check if it matches the created platform and click next. The next step is choosing a template - I have chosen an Empty Linux Application.

Next we need to copy the main.c file from Vga_draw into our application project - in the Explorer sub-window right click on the project and select Import Sources…, navigate to RedPitaya-FPGA/prj/Examples/Vga_draw/Vitis_sources and click OK. Then check the main.c file and click Finish.

The main.c file should now be visible in the src folder of the Explorer sub-window.

We need to add the math.h library, so right click on the project -> Properties and then add the m library to the project.

Logo

The project should compile.

1.4.2.9.2.6. Vitis code explained

For every AXI_GPIO we have to define its address and its size as shown below

static unsigned long addr;
static unsigned long addr_2;
static unsigned long addr_3;

addr = 0x41200000;
addr_2 = 0x41220000;
addr_3 = 0x41210000;

This is how we define dual ports. The second port is shifted by 0x0008.

data_position = map_base_2 + (addr_2 & MAP_MASK_2);
data_in = map_base_2 + ((addr_2 + 0x0008) & MAP_MASK_2);

1.4.2.9.2.7. How to run an application on Red Pitaya

For running the program on Red Pitaya I used Winscp (Windows) or the terminal (Linux), to transfer the .bit file from Vivado and the .elf file from SDK on the board.

Then connect to the RedPitaya via Putty/terminal.

Instructions on changing the FPGA image:

Please note that you need to change the forward slashes to backward slashes on Windows.

  1. Open Terminal or CMD and go to the .bit file location.

cd <Path/to/RedPitaya/repository>/prj/v0.94/project/repitaya.runs/impl_1
  1. Send the file .bit (red_pitaya_top.bit is the default name) to the Red Pitaya with the scp command.

scp red_pitaya_top.bit root@rp-xxxxxx.local:/root
  1. Now establish an SSH communication with your Red Pitaya and check if you have the copy red_pitaya_top.bit in the root directory.

redpitaya> ls
  1. Load the red_pitaya_top.bit to xdevcfg with

redpitaya> cat red_pitaya_top.bit > /dev/xdevcfg

After the FPGA image has been changed type in the following:

chmod +x <file_name.elf>
./ <file_name.elf>

1.4.2.9.2.8. Creating an IP core with an AXI bus

Open the Vga_draw project with Vivado.

Let’s create an IP core for drawing a rectangle. The core must draw a rectangle according to the specified parameters; coordinates (x, y), and size along both axes.

Logo

To create an IP core, go to tools → Create and Package New Ip:

Logo

Select Create AXI4 Peripheral.

Next, we give a name to our block, its version, and its description:

Logo

Vivado automatically generates code for working on the AXI bus, we just need to specify the number of registers we need when creating the IP core. I have specified 5 registers:

Logo

Next, select the item to add IP to the repository.

Logo

Now one can edit the IP core, go to the IP directory and look for the created core (Window -> IP Catalog):

Logo
Logo

A new project will be created, which we can use to start writing logic.

1.4.2.9.2.9. Writing an IP core to draw a rectangle in Verilog

Let’s create a new Verilog file named RectPic.v. This module will describe drawing a rectangle - the module’s inputs are:

module RectPic #
(
    parameter integer SCREEN_HEIGHT = 600,
    parameter integer SCREEN_WIDTH  = 800
)
(
    input wire clk,
    input wire [10 : 0] hst,
    input wire [9 : 0] vst,
    input wire [15 : 0] block_posx,  // The position of the rectangle on the x-axis
    input wire [15 : 0] block_posy,  // The position of the rectangle on the y-axis
    input wire [15 : 0] block_sizex,  // The size of the rectangle on the x-axis
    input wire [15 : 0] block_sizey,  // The size of the rectangle on the y-axis
    input wire [2 : 0] draw_color,    // The color with which the object will be drawn
    input wire [2 : 0] rgb_i,         // Sent to rgb_o when there is nothing to draw
    output reg [2 : 0] rgb_o
);

Drawing a rectangle is pretty straightforward. To do this, it is enough to determine that the displayed pixel is inside the screen and inside the rectangle. The process of drawing a rectangle is written below:

always @(posedge clk)
begin
    if ((hst < SCREEN_WIDTH) &&  (vst < SCREEN_HEIGHT)) // inside the screen
        if ((hst > block_posx) && (hst < (block_posx + block_sizex)))
            if ((vst > block_posy) && (vst < (block_posy + block_sizey)))
                rgb_o <= draw_color;
            else
                rgb_o <= rgb_i;
        else
            rgb_o <= rgb_i;
    else
        rgb_o <= 3'b000;
end
endmodule

Now we need to describe the IO for the BlockImage_v1_0 top-level module. After the line: Users to add ports here.

input wire [10 : 0] hst,
input wire [9 : 0] vst,
input wire [2 : 0] rgb_i,
output wire [2 : 0] rgb_o,

Also, these ports need to be added to BlockImage_v1_0_S00_AXI - After the line: Users to add ports here.

input wire [10 : 0] hst,
input wire [9 : 0] vst,
input wire [2 : 0] rgb_i,
output wire [2 : 0] rgb_o,

Add the parameters that will be required in the future to BlockImage_v1_0 and BlockImage_v1_0_S00_AXI:

// Users to add parameters here
parameter integer SCREEN_HEIGHT = 600,
parameter integer SCREEN_WIDTH  = 800,

parameter integer RESET_POSX = 10,
parameter integer RESET_POSY = 10,
parameter integer RESET_SIZEX = 10,
parameter integer RESET_SIZEY = 10,
parameter integer RESET_COLOR = 1,

Now let’s connect the BlockImage_v1_0 and the BlockImage_v1_0_S00_AXI module (find and change the following in BlockImage_v1_0.v):

// Instantiation of Axi Bus Interface S00_AXI
BlockImage_v1_0_S00_AXI # (
    .SCREEN_HEIGHT(SCREEN_HEIGHT),
    .SCREEN_WIDTH(SCREEN_WIDTH),
    .RESET_POSX(RESET_POSX),
    .RESET_POSY(RESET_POSY),
    .RESET_SIZEX(RESET_SIZEX),
    .RESET_SIZEY(RESET_SIZEY),
    .RESET_COLOR(RESET_COLOR),

    .C_S_AXI_DATA_WIDTH(C_S00_AXI_DATA_WIDTH),
    .C_S_AXI_ADDR_WIDTH(C_S00_AXI_ADDR_WIDTH)
) BlockImage_v1_0_S00_AXI_inst (
.hst(hst),
.vst(vst),
.rgb_i(rgb_i),
.rgb_o(rgb_o),
.S_AXI_ACLK(s00_axi_aclk),
.S_AXI_ARESETN(s00_axi_aresetn),
.S_AXI_AWADDR(s00_axi_awaddr),
...etc...

Let’s go to the BlockImage_v1_0_S00_AXI file and rename the registers according to their purpose:

//-- Number of Slave Registers 5
reg [C_S_AXI_DATA_WIDTH-1:0]        posx;
reg [C_S_AXI_DATA_WIDTH-1:0]        posy;
reg [C_S_AXI_DATA_WIDTH-1:0]        sizex;
reg [C_S_AXI_DATA_WIDTH-1:0]        sizey;
reg [C_S_AXI_DATA_WIDTH-1:0]        draw_color;

Set default values for registers:

always @( posedge S_AXI_ACLK )
begin
if ( S_AXI_ARESETN == 1'b0 )
    begin
    posx <= RESET_POSX;
    posy <= RESET_POSY;
    sizex <= RESET_SIZEX;
    sizey <= RESET_SIZEY;
    draw_color <= RESET_COLOR;

In the same process rename all the instances of slv_regN (N: 0-4) with the appropriate new register (so replace each instance of slv_reg0 with posx, slv_reg1 with posy, and so on). Do this wherever an error is displayed after you renamed the registers in the file.

The last thing left to do is to connect RectPic:

// Add user logic here
RectPic #
(
    SCREEN_HEIGHT,
    SCREEN_WIDTH
) pic_inst (
    .clk(S_AXI_ACLK),
    .hst(hst),
    .vst(vst),
    .block_posx(posx),
    .block_posy(posy),
    .block_sizex(sizex),
    .block_sizey(sizey),
    .draw_color(draw_color),
    .rgb_i(rgb_i),
    .rgb_o(rgb_o)
);

Optionally, you can write tests for RectPic and the top-level module in the same project before packaging. After all these procedures, you can pack the project into the IP kernel. Click Edit packaged IP and complete all steps (Click Merge changes from File Groups Wizard in all instances where there is no checkmark):

Logo

Press Re-Package IP.

1.4.2.9.2.10. Writing an IP core to draw a circle in Verilog

Let’s create an AXI IP core named CircleImage, we only need 3 registers, but I left 4.

Drawing a circle is not an easy task for FPGA. One of the simpler solutions is to use a block of memory and load a circle image into it (Vga_draw lesson), but we will go the simpler way and create an array immediately with a circle drawing inside. Let’s create a CircPic.v file:

Ports:

module CircPic #
    (
        parameter integer SCREEN_HEIGHT     = 600,
        parameter integer SCREEN_WIDTH      = 800
    )
    (
    input wire clk,
    input wire [10 : 0] hst,
    input wire [9 : 0] vst,
    input wire [15 : 0] block_posx,  // The position of the rectangle on the x-axis
    input wire [15 : 0] block_posy,  // The position of the rectangle on the y-axis
    input wire [2 : 0] draw_color,
    input wire [2 : 0] rgb_i,
    output reg [2 : 0] rgb_o
    );

The drawing process is similar to RectPic:

reg [31 : 0] mem [31 : 0];

initial begin
    mem[0]  = 32'b00000000000011111111000000000000;
    mem[1]  = 32'b00000000011111111111111000000000;
    mem[2]  = 32'b00000001111111111111111110000000;
    mem[3]  = 32'b00000011111111111111111111000000;
    mem[4]  = 32'b00000111111111111111111111100000;
    mem[5]  = 32'b00001111111111111111111111110000;
    mem[6]  = 32'b00011111111111111111111111111000;
    mem[7]  = 32'b00111111111111111111111111111100;
    mem[8]  = 32'b00111111111111111111111111111100;
    mem[9]  = 32'b01111111111111111111111111111110;
    mem[10] = 32'b01111111111111111111111111111110;
    mem[11] = 32'b01111111111111111111111111111110;
    mem[12] = 32'b11111111111111111111111111111111;
    mem[13] = 32'b11111111111111111111111111111111;
    mem[14] = 32'b11111111111111111111111111111111;
    mem[15] = 32'b11111111111111111111111111111111;
    mem[16] = 32'b11111111111111111111111111111111;
    mem[17] = 32'b11111111111111111111111111111111;
    mem[18] = 32'b11111111111111111111111111111111;
    mem[19] = 32'b11111111111111111111111111111111;
    mem[20] = 32'b01111111111111111111111111111110;
    mem[21] = 32'b01111111111111111111111111111110;
    mem[22] = 32'b01111111111111111111111111111110;
    mem[23] = 32'b00111111111111111111111111111100;
    mem[24] = 32'b00111111111111111111111111111100;
    mem[25] = 32'b00011111111111111111111111111000;
    mem[26] = 32'b00001111111111111111111111110000;
    mem[27] = 32'b00000111111111111111111111100000;
    mem[28] = 32'b00000011111111111111111111000000;
    mem[29] = 32'b00000001111111111111111110000000;
    mem[30] = 32'b00000000011111111111111000000000;
    mem[31] = 32'b00000000000011111111000000000000;
end

always @(posedge clk)
begin
    if ((hst < SCREEN_WIDTH) &&  (vst < SCREEN_HEIGHT)) // inside the screen
        if ((hst >= block_posx) && (hst < (block_posx + 32)))
            if ((vst >= block_posy) && (vst < (block_posy + 32)))
    if (mem[vst - block_posy][hst - block_posx])
        rgb_o <= draw_color;
    else
        rgb_o <= rgb_i;
            else
                rgb_o <= rgb_i;
        else
            rgb_o <= rgb_i;
    else
        rgb_o <= 3'b000;
end
endmodule

As in the previous paragraph, add the necessary ports and rename the registers for their purpose (this time we only have 3 registers). Do not forget to pack the project into an IP core.

1.4.2.9.2.11. Writing an IP core to work with the keyboard

Create another empty IP core where you will need to add an input for the buttons:

input wire [3 : 0] keys,

And write the values at the keys input to register 0 at each clock signal:

always @( posedge S_AXI_ACLK )
    begin
      if ( S_AXI_ARESETN == 1'b0 )
        begin
          slv_reg0 <= 4'b1111;
          slv_reg1 <= 0;
          slv_reg2 <= 0;
          slv_reg3 <= 0;
        end
      else begin
            slv_reg0 = keys;
        if (slv_reg_wren)

In my case, with the buttons connected and not pressed, the keys port has the value 4’b1111. When you press one of the buttons, its corresponding bit will be equal to 0.

Scheme of one of the buttons:

Logo

1.4.2.9.2.12. Connecting IP cores to the processor

Now you can change the Vga_draw project with the newly added cores or open the project Vga_game where everything is already done.

We add the resulting IPs to the main project and connect them to the AXI bus. In total, you need 4 BlockImage (2 paddles and 2 counters), 1 CircleImage (1 ball), and a keyboard. We connect rgb_o to rgb_i of each of the cores. The order is not very important, as it only affects which object is drawn on top of the other. The resulting diagram:

Logo

Setting up addressing:

Logo

1.4.2.9.2.13. Writing game code in C++

The complete game code is located in RedPitaya-FPGA/prj/Examples/Vga_game/Vitis_sources. In the following chapters, we will discuss what the important parts of the code do.

All classes Rectangle, Keyboard, and Ball - describe work with the corresponding IP cores, constructors take a file descriptor as input, and an address in memory for the corresponding IP cores.

1.4.2.9.2.13.1. Keyboard class

Since there are no debounce mechanisms for the button inputs, they will have to be processed programmatically. The algorithm is quite simple, it is enough for us to poll the keyboard at a certain frequency, less than the duration of the bounce. In our case, the polling rate of the keyboard is 60Hz.

Processing of clicks is done in the Process method of the Keyboard class. The purpose of this method is to return the button number and its state. The above algorithm is good, but the current implementation is not capable of handling simultaneous key presses within a single loop. I suggest doing it yourself, but what we did is enough for the game to be operational.

The order of the keys is set by the position of the button in the class enum Keys, so the buttons can be soldered incorrectly.

1.4.2.9.2.13.2. Rectangle class

Quite a simple class, the functionality of which boils down to writing coordinates and sizes in the corresponding registers.

1.4.2.9.2.13.3. Ball class

A distinctive feature of this class is racket collision detection. Collision handling is performed in the Process method of this class, objects that need to be detected as an argument are passed. Also, this method implicitly detects collisions with screen borders.

1.4.2.9.2.13.4. Players score

To simplify the code, the score is displayed through the Rectangle class, its width corresponds to the player’s score.

1.4.2.9.2.14. Building

Copy the c ++ code to RedPitaya, and compile:

g++  -std=c++11 -o vga_game.o vga_game.cpp

1.4.2.9.2.15. First run

Downloading Bitstream and compiling the code is described in the previous lessons.

If you have followed the steps this far - congratulations, you have just made a simple version of a pong game. To run it connect to your Red Pitaya with a browser and launch your new application.

When loading a bitstream, all blocks with the default size and position will be outputs:

Logo

After starting the program, all the figures will be displayed in their places:

Logo

1.4.2.9.3. Author & Source

Original author: Jaka Koren

Original lesson: link