Lab 3: Adding custom hardware block

This text is based on teaching materials from Xilinx (www.xilinx.com).

Contents

Contents. 2

Introduction. 2

Setup. 2

Step 1: Creating new project 3

Step 2: Creating new hardware block. 4

Step 3: Adding new IP to the design. 13

Step 4: Implementing the design and programming FPGA. 21

Step 5: Modifying IP block. 25

Step 6: Writing software for 7-segment LED hardware block. 26

Conclusions. 27

Results required to complete the exercise. 27

Change history. 27

Introduction

This lab guides you through the process of adding custom hardware block, written in HDL language to the microprocessor system using AXI bus. You will add the hardware controlling the 7-segment LED display which will multiplex signals for the display. The HDL code of the 7-segment LED display controller will be supplied for you, you only need to encapsulate this HDL code into AXI slave interface, connect it to the system and run basic software. After completing this lab, you will be able to create and add custom hardware peripheral blocks to the programmable system and write a simple driver.

Setup

For this lab, you will need a Xilinx Nexys A7 FPGA Board and a USB cable. Connect the USB cable to the PC and the Nexys A7 Board to connector PROG/UART. With this cable, the programming of the FPGA will be done, as well as serial communication between the board and PC.

Step 1: Creating new project

1. Open the project created in the lab1 and save it as a new project, for example lab3.

 

Step 2: Creating new hardware block

1. From the menu, select Tools/Create and Package New IP:

The wizard for creating new block starts, select Create New AXI peripheral and click Next:

In the next window you can add your name of the new IP block and possible the comment, then click Next:

In Add Interfaces window change the number of registers to 8. This is the number of 32-bit memory locations that will be available by the processor for read and write in this block. The LED display has 8 digits, so it is convenient to store the value displayed on each digit in a separate register. Click Next.

Additional task for volunteers: It is a waste of the resources to use eight 32-bit registers in this case, because each register is 32-bit wide and you need at most 8-bits per digit. Create the IP block which realizes a better use of the hardware resources. If you decide to make this task for volunteers, please first do this normally as described below, then incrementally change and improve your design.

In the last window which summarizes the process of adding new IP block, leave the option Add IP to the repository and just click Finish. Your new block will be added to IP catalog.

2. In the Flow Navigator select IP Catalog, then in the main window find your new block in User Repository/AXI Peripheral section. Right click on it and select Edit in IP Packager:

3. This will open a new instance of Vivado with temporary project just for editing your IP block. In the next window click OK:

In the temporary Vivado project, which you can notice it on the window tab, for example:

In the Sources window there will be two HDL files:

· led7segment_v1_0_S00_AXI_inst – this HDL file contains the logic of AXI interface and custom registers. You will add your logic here and insert additional 8-bit output ports digit_o and segment_o for LED display.

· led7segment_v1_0 - this HDL file only contains the embedded led7segment_v1_0_S00_AXI_inst block, you will add here only the ports digit_o and segment_o that come from led7segment_v1_0_S00_AXI_inst and need to be routed to the outside of the design.

This is the block diagram of the IP at the beginning:

Your target is to obtain the following structure:

4. First you need to open led7segment_v1_0_S00_AXI_inst and add library at the beginning of the code (just after the line use ieee.std_logic_unsigned.all;), because you will be adding std_logic_vectors as unsigned numbers:

use ieee.std_logic_unsigned.all;

5. Then add two ports in port declaration section:

-- Users to add ports here
digit_o       : out std_logic_vector(7 downto 0);
segment_o     : out std_logic_vector(7 downto 0);
-- User ports ends

The code in this lab is written in VHDL, if you use Verilog, you need to convert the syntax.

6. Then define the new signals which will be needed by your logic, in VHDL you will add this between architecture and begin keywords.

signal digit_select          : std_logic_vector(0 to 2);
signal mhertz_count          : std_logic_vector(5 downto 0);
signal khertz_count          : std_logic_vector(9 downto 0);
signal mhertz_en             : std_logic;
signal khertz_en             : std_logic;

7. Add the custom logic code to the body of your architecture:

-- Add user logic here
----------------------------------------
-- Clock Dividers
----------------------------------------
GEN_1MHZ : process (S_AXI_ACLK) is
begin
   if rising_edge(S_AXI_ACLK) then
         if S_AXI_ARESETN = '0' then
               mhertz_count <= (others => '0');
               mhertz_en <= '0';
         else
               mhertz_count <= mhertz_count + 1;
               if mhertz_count = "110010" then
                     mhertz_en <= '1' ;
                     mhertz_count <= (others => '0');
               else
                     mhertz_en <= '0';
               end if;
         end if;
   end if;
end process GEN_1MHZ;

GEN_1KHZ : process (S_AXI_ACLK) is
begin
   if rising_edge(S_AXI_ACLK) then
         if S_AXI_ARESETN = '0' then
               khertz_count <= (others => '0');
               khertz_en <= '0';
         else
               if mhertz_en = '1' then
                     khertz_count <= khertz_count + 1;
                     if khertz_count = "1111101000" then
                          khertz_en <= '1';
                          khertz_count <= (others => '0');
                     end if;
               else
                     khertz_en <= '0';
               end if;
         end if;
   end if;
end process GEN_1KHZ;
----------------------------------------
-- User logic display update
----------------------------------------
CYC_DISP_PROC : process( S_AXI_ACLK ) is
begin
   if rising_edge(S_AXI_ACLK) then
         if S_AXI_ARESETN = '0' then
               digit_select <= (others => '0');
               digit_o      <= (others => '0');
               segment_o    <= (others => '0');
         else
               if khertz_en = '1' then
                     digit_select <= digit_select + 1 ;
                     case digit_select is
                          when "000" => segment_o <= slv_reg0(7 downto 0);  digit_o <= "11111110";
                          when "001" => segment_o <= slv_reg1(7 downto 0);  digit_o <= "11111101";
                          when "010" => segment_o <= slv_reg2(7 downto 0);  digit_o <= "11111011";
                          when "011" => segment_o <= slv_reg3(7 downto 0);  digit_o <= "11110111";
                          when "100" => segment_o <= slv_reg4(7 downto 0);  digit_o <= "11101111";
                          when "101" => segment_o <= slv_reg5(7 downto 0);  digit_o <= "11011111";
                          when "110" => segment_o <= slv_reg6(7 downto 0);  digit_o <= "10111111";
                          when "111" => segment_o <= slv_reg7(7 downto 0);  digit_o <= "01111111";
                          when others => null;
                     end case;
               end if;
         end if;
   end if;
end process CYC_DISP_PROC;
-- User logic ends

8. Now open file led7segment_v1_0 and add also two ports to the block led7segment_v1_0:

-- Users to add ports here
digit_o       : out std_logic_vector(7 downto 0);
segment_o     : out std_logic_vector(7 downto 0);
-- User ports ends

9. Modify the declaration of the component used in the architecture, because now the component contains two additional ports digit_o and segment_o (the change is indicated in yellow in the code below):

-- component declaration
component myip_v1_0_S00_AXI is
   generic (
         C_S_AXI_DATA_WIDTH      : integer   := 32;
         C_S_AXI_ADDR_WIDTH      : integer   := 5
   );
   port (
         digit_o       : out std_logic_vector(7 downto 0);
         segment_o     : out std_logic_vector(7 downto 0);
         S_AXI_ACLK  : in std_logic;
         S_AXI_ARESETN     : in std_logic;
         S_AXI_AWADDR      : in std_logic_vector(C_S_AXI_ADDR_WIDTH-1 downto 0);
         S_AXI_AWPROT      : in std_logic_vector(2 downto 0);
         S_AXI_AWVALID     : in std_logic;
         S_AXI_AWREADY     : out std_logic;
         S_AXI_WDATA : in std_logic_vector(C_S_AXI_DATA_WIDTH-1 downto 0);
         S_AXI_WSTRB : in std_logic_vector((C_S_AXI_DATA_WIDTH/8)-1 downto 0);
         S_AXI_WVALID      : in std_logic;
         S_AXI_WREADY      : out std_logic;
         S_AXI_BRESP : out std_logic_vector(1 downto 0);
         S_AXI_BVALID      : out std_logic;
         S_AXI_BREADY      : in std_logic;
         S_AXI_ARADDR      : in std_logic_vector(C_S_AXI_ADDR_WIDTH-1 downto 0);
         S_AXI_ARPROT      : in std_logic_vector(2 downto 0);
         S_AXI_ARVALID     : in std_logic;
         S_AXI_ARREADY     : out std_logic;
         S_AXI_RDATA : out std_logic_vector(C_S_AXI_DATA_WIDTH-1 downto 0);
         S_AXI_RRESP : out std_logic_vector(1 downto 0);
         S_AXI_RVALID      : out std_logic;
         S_AXI_RREADY      : in std_logic
   );
end component myip_v1_0_S00_AXI;

10. Finally map those ports in component instantiation in port map section:

digit_o       => digit_o,
segment_o     => segment_o,

This concludes the changes in HDL code.

11. In the Packager window, accept all changes by clicking Merge changes for all categories:

12. Having merged all the changes, click Re-Package IP button. Ensure that you enable creating the archive of IP.

You can now close the temporary project with your IP block:

You can easily open it again later, if you need to make more changes.

Step 3: Adding new IP to the design

1. Open the block diagram of your system and add newly created IP by clicking +:

and search for your block:

2. When the block has been added, run Connection Automation wizard:

3. Leave all setting unchanged and click OK:

4. The new block has been connected to AXI bus, but it still has unconnected digit_o and segment_o ports. You need to connect them manually, by right-clicking on each port and selecting Make External:

5. Change the names of the external ports to remove the trailing _0 from the names:

Your block diagram should look like this:

6. When you finish all the changes in block diagram, it is a good idea to perform validation:

7. Finally you need to add the constraints to route your new ports digit_o and sement_o to the appropriate pins of ther FPGA that are connected in PCB to 7-segment LED display. Right click on Constraints in Sources window with Hierarchy view and select Add Sources:

Then select Add or create constraints and click Next:

Then create new constraint file:

Give it your name and click OK:

 

At the end click Finish:

Open the newly created (empty) constraint file:

And paste the following constraints:

set_property -dict { PACKAGE_PIN T10   IOSTANDARD LVCMOS33 } [get_ports { segment_o[7] }]; #segment CA 
set_property -dict { PACKAGE_PIN R10   IOSTANDARD LVCMOS33 } [get_ports { segment_o[6] }]; #segment CB
set_property -dict { PACKAGE_PIN K16   IOSTANDARD LVCMOS33 } [get_ports { segment_o[5] }]; #segment CC
set_property -dict { PACKAGE_PIN K13   IOSTANDARD LVCMOS33 } [get_ports { segment_o[4] }]; #segment CD
set_property -dict { PACKAGE_PIN P15   IOSTANDARD LVCMOS33 } [get_ports { segment_o[3] }]; #segment CE
set_property -dict { PACKAGE_PIN T11   IOSTANDARD LVCMOS33 } [get_ports { segment_o[2] }]; #segment CF
set_property -dict { PACKAGE_PIN L18   IOSTANDARD LVCMOS33 } [get_ports { segment_o[1] }]; #segment CG
set_property -dict { PACKAGE_PIN H15   IOSTANDARD LVCMOS33 } [get_ports { segment_o[0] }]; #segment DP
 
set_property -dict { PACKAGE_PIN U13   IOSTANDARD LVCMOS33 } [get_ports { digit_o[7] }];  #digit 7
set_property -dict { PACKAGE_PIN K2    IOSTANDARD LVCMOS33 } [get_ports { digit_o[6] }];  #digit 6
set_property -dict { PACKAGE_PIN T14   IOSTANDARD LVCMOS33 } [get_ports { digit_o[5] }];  #digit 5
set_property -dict { PACKAGE_PIN P14   IOSTANDARD LVCMOS33 } [get_ports { digit_o[4] }];  #digit 4
set_property -dict { PACKAGE_PIN J14   IOSTANDARD LVCMOS33 } [get_ports { digit_o[3] }];  #digit 3
set_property -dict { PACKAGE_PIN T9    IOSTANDARD LVCMOS33 } [get_ports { digit_o[2] }];  #digit 2
set_property -dict { PACKAGE_PIN J18   IOSTANDARD LVCMOS33 } [get_ports { digit_o[1] }];  #digit 1
set_property -dict { PACKAGE_PIN J17   IOSTANDARD LVCMOS33 } [get_ports { digit_o[0] }];  #digit 0

Please check, if the port names of digit_o and segment_o in your block diagram are exactly the same as in constrains file. At the end, save the file.

Step 4: Implementing the design and programming FPGA

1. Now you can generate the bitstream by clicking Generate Bitstream in PROGRAM AND DEBUG section of Flow Navigator. If there are no errors, you will see the write_bitstream Complete message at the top right corner of your screen:

You can now program FPGA with new hardware, but remember, that when programming FPGA from Vivado, the software is not sent to the system.

If you have problems with programming and you cannot manually reset the board (remote lab), check this document: Remote reset by JTAG.

2. To program the FPGA, click Open Hardware Manager in Flow Navigator:

Then click Open target and Auto Connect:

If you have more than one prototype board connected to your PC, instead of using Auto Connect, you should follow the procedure given here: Working with multiple FPGAs connected to the PC.

If the board Nexys A7 is connected and the power is on, you should see the FPGA detected in Hardware window. To program the FPGA, you can click Program device at the top or from the context menu:

And confirm by clicking Program:

In the Tcl Console at the bottom of the screen, the message:

INFO: [Labtools 27-1434] Device xc7a100t (JTAG device index = 0) is programmed with a design that has no supported debug core(s) in it.

confirms that programming was successful. On the Nexys A7 board you should see that the 7-segment LED display is constantly on:

 

 

 

Step 5: Modifying IP block

This section describes the procedure of modifying your IP. If you need not to modify your IP, you can skip this section, but remember you might need to modify IP later.

1. Go to the IP Catalog, right-click your block and select Edit in IP Packager. After the changes, if your block is already included in the design, the changes will not be effective in the block diagram until you update the IP by using menu Reports/Report IP Status. Then in window at the bottom click Rerun:

2. Select the blocks needed to be updated and click Upgrade Selected button:

3. After upgrade, the system will ask you if you want to prepare output product. This step will perform initial synthesis of your block, which can speed up the synthesis. If you want, you can Generate output product, but if you skip this step, the synthesis will be done automatically later. Generating output product speeds up multiple synthesis of the design, but has no influence on the implementation time.

Step 6: Writing software for 7-segment LED hardware block

Now you will write the software for controlling the hardware block for 7-segment LED display you created in this lab.

1. Export hardware design to SDK, including the bitstream and Launch SDK. Now you can close Vivado, for further part of this lab you will be working in Xilinx SDK.

The 7-segment LED display block has 8 registers, starting from its base address on the AXI bus. The base address of this block is defined as macro in the file xparameters.h. Writing to the base address and its consecutive seven locations will result in writing to the registers of this block. For example, if you define the pointer to 32-bit unsigned integer:

   u32 *ledptr;

and assign the base address to this pointer:

   ledptr = (u32 *)LED7SEGMENT_BASEADDR;

you will be able to write to the hardware i-th register (where i=0…7):

*(ledptr + i) = dig;

where dig is 32-bit integer value to be written to the register.

LED7SEGMENT_BASEADDR is a macro containing the base address of the 7-segment LED display block, for example:

#define LED7SEGMENT_BASEADDR XPAR_LED7SEGMENT_0_S00_AXI_BASEADDR

This macro has been defined, because if you would have multiple references to the base address of this block, in case of hardware change, you would need to modify the software only in the line where the macro is defined, not at each reference to the base address.

2. Write the function:

void dispLED(u32 Val, u8 DP )

which will display in decimal format any 8-digit number Val to the LED display, DP is the position of the decimal point (from 0 to 7). You can use modulo operator (%) for calculating the value for each digit.

For example, if you want to display the number 12345.678 on the LED display, you would use:

dispLED(12345678, 3)

3. Test this function together with the delay function:

void timerDelay(int delay_ms)

from lab2, for example in the code as below:

u32 cnt=0;

    while (1) {

         xil_printf("cnt=%d\n\r", cnt);

         dispLED(cnt++, 0);

         timerDelay(1000);

    }

Present this code as the result of this lab.

Conclusions

You have created the custom hardware block with AXI interface and you have added it to your system. You also have written the software to control this new hardware.

Results required to complete the exercise

· Answer the questions in the test: https://docs.google.com/forms/d/e/1FAIpQLSdRa_v-us1LZMLrWG8MwwyKmvsJHGPagZvSGbJWrMNIVbuXZw/viewform?usp=sf_link

· Present the source codes and the operation of the LED display on Nexys board together with the timer on serial port console.

Change history

Adaptation to version Vivado 2018.3: M. Wójcikowski (08/2020).