Hey guys! This will be a big jump from the last two tutorials. There is a prerequisite for this tutorial. The reader should know how a basic adder is coded in Verilog or if you don’t know you can find it out by quickly googling it. The primary focus of this tutorial is the Zynq PS-PL communication i.e. writing the operands to registers on which the programmable logic(PL) would perform the summation operation and write the result in a third register followed by the processing system(PS) accessing them. I learned the Vivado part of this tutorial from this youtube video. I thought it might be a good idea to document it in case I need it later. I have modified the second part where you have to write a c code to perform the writes on registers from the Xilinx SDK documentation.

Hardware and software specs used:

Vivado: 2018.1

OS: Ubuntu 18.04

Board: PYNQ-Z1

Step 1: Create your adder IP

Go to tools -> create and package new IP -> Create a new AXI4 peripheral

This will make an IP with 4 registers each of 32 bits. We only need three for our purpose as we will need two registers to be the operands of the summation and the third to store the results.

Once you click Finish a new vivado window will open up with a different project for the IP which you just created and are about to make some tweaks.

Open the selected file to edit it.

After you open the file, you will see that the 4 registers that you have made from the IP are as below:

Now verilog has some rule that only one process writes to something in a clock cycle(I don’t understand this part very clearly. It was mentioned in the above mentioned youtube video from which I learned how to do this) so since I am going to write the result of two operands in “slv_reg0”, I will therefore comment all the write statements on that register. To avoid any errors, you can simply use the following code:

`timescale 1 ns / 1 ps
module my_axi_adder_v1_0_S00_AXI #
 (
  // Users to add parameters here
// User parameters ends
  // Do not modify the parameters beyond this line
// Width of S_AXI data bus
  parameter integer C_S_AXI_DATA_WIDTH = 32,
  // Width of S_AXI address bus
  parameter integer C_S_AXI_ADDR_WIDTH = 4
 )
 (
  // Users to add ports here
// User ports ends
  // Do not modify the ports beyond this line
// Global Clock Signal
  input wire  S_AXI_ACLK,
  // Global Reset Signal. This Signal is Active LOW
  input wire  S_AXI_ARESETN,
  // Write address (issued by master, acceped by Slave)
  input wire [C_S_AXI_ADDR_WIDTH-1 : 0] S_AXI_AWADDR,
  // Write channel Protection type. This signal indicates the
      // privilege and security level of the transaction, and whether
      // the transaction is a data access or an instruction access.
  input wire [2 : 0] S_AXI_AWPROT,
  // Write address valid. This signal indicates that the master signaling
      // valid write address and control information.
  input wire  S_AXI_AWVALID,
  // Write address ready. This signal indicates that the slave is ready
      // to accept an address and associated control signals.
  output wire  S_AXI_AWREADY,
  // Write data (issued by master, acceped by Slave) 
  input wire [C_S_AXI_DATA_WIDTH-1 : 0] S_AXI_WDATA,
  // Write strobes. This signal indicates which byte lanes hold
      // valid data. There is one write strobe bit for each eight
      // bits of the write data bus.    
  input wire [(C_S_AXI_DATA_WIDTH/8)-1 : 0] S_AXI_WSTRB,
  // Write valid. This signal indicates that valid write
      // data and strobes are available.
  input wire  S_AXI_WVALID,
  // Write ready. This signal indicates that the slave
      // can accept the write data.
  output wire  S_AXI_WREADY,
  // Write response. This signal indicates the status
      // of the write transaction.
  output wire [1 : 0] S_AXI_BRESP,
  // Write response valid. This signal indicates that the channel
      // is signaling a valid write response.
  output wire  S_AXI_BVALID,
  // Response ready. This signal indicates that the master
      // can accept a write response.
  input wire  S_AXI_BREADY,
  // Read address (issued by master, acceped by Slave)
  input wire [C_S_AXI_ADDR_WIDTH-1 : 0] S_AXI_ARADDR,
  // Protection type. This signal indicates the privilege
      // and security level of the transaction, and whether the
      // transaction is a data access or an instruction access.
  input wire [2 : 0] S_AXI_ARPROT,
  // Read address valid. This signal indicates that the channel
      // is signaling valid read address and control information.
  input wire  S_AXI_ARVALID,
  // Read address ready. This signal indicates that the slave is
      // ready to accept an address and associated control signals.
  output wire  S_AXI_ARREADY,
  // Read data (issued by slave)
  output wire [C_S_AXI_DATA_WIDTH-1 : 0] S_AXI_RDATA,
  // Read response. This signal indicates the status of the
      // read transfer.
  output wire [1 : 0] S_AXI_RRESP,
  // Read valid. This signal indicates that the channel is
      // signaling the required read data.
  output wire  S_AXI_RVALID,
  // Read ready. This signal indicates that the master can
      // accept the read data and response information.
  input wire  S_AXI_RREADY
 );
// AXI4LITE signals
 reg [C_S_AXI_ADDR_WIDTH-1 : 0]  axi_awaddr;
 reg   axi_awready;
 reg   axi_wready;
 reg [1 : 0]  axi_bresp;
 reg   axi_bvalid;
 reg [C_S_AXI_ADDR_WIDTH-1 : 0]  axi_araddr;
 reg   axi_arready;
 reg [C_S_AXI_DATA_WIDTH-1 : 0]  axi_rdata;
 reg [1 : 0]  axi_rresp;
 reg   axi_rvalid;
// Example-specific design signals
 // local parameter for addressing 32 bit / 64 bit C_S_AXI_DATA_WIDTH
 // ADDR_LSB is used for addressing 32/64 bit registers/memories
 // ADDR_LSB = 2 for 32 bits (n downto 2)
 // ADDR_LSB = 3 for 64 bits (n downto 3)
 localparam integer ADDR_LSB = (C_S_AXI_DATA_WIDTH/32) + 1;
 localparam integer OPT_MEM_ADDR_BITS = 1;
 //----------------------------------------------
 //-- Signals for user logic register space example
 //------------------------------------------------
 //-- Number of Slave Registers 4
 reg [C_S_AXI_DATA_WIDTH-1:0] slv_reg0;
 reg [C_S_AXI_DATA_WIDTH-1:0] slv_reg1;
 reg [C_S_AXI_DATA_WIDTH-1:0] slv_reg2;
 reg [C_S_AXI_DATA_WIDTH-1:0] slv_reg3;
 wire  slv_reg_rden;
 wire  slv_reg_wren;
 reg [C_S_AXI_DATA_WIDTH-1:0]  reg_data_out;
 integer  byte_index;
 reg  aw_en;
// I/O Connections assignments
assign S_AXI_AWREADY = axi_awready;
 assign S_AXI_WREADY = axi_wready;
 assign S_AXI_BRESP = axi_bresp;
 assign S_AXI_BVALID = axi_bvalid;
 assign S_AXI_ARREADY = axi_arready;
 assign S_AXI_RDATA = axi_rdata;
 assign S_AXI_RRESP = axi_rresp;
 assign S_AXI_RVALID = axi_rvalid;
 // Implement axi_awready generation
 // axi_awready is asserted for one S_AXI_ACLK clock cycle when both
 // S_AXI_AWVALID and S_AXI_WVALID are asserted. axi_awready is
 // de-asserted when reset is low.
always @( posedge S_AXI_ACLK )
 begin
   if ( S_AXI_ARESETN == 1'b0 )
     begin
       axi_awready <= 1'b0;
       aw_en <= 1'b1;
     end 
   else
     begin    
       if (~axi_awready && S_AXI_AWVALID && S_AXI_WVALID && aw_en)
         begin
           // slave is ready to accept write address when 
           // there is a valid write address and write data
           // on the write address and data bus. This design 
           // expects no outstanding transactions. 
           axi_awready <= 1'b1;
           aw_en <= 1'b0;
         end
         else if (S_AXI_BREADY && axi_bvalid)
             begin
               aw_en <= 1'b1;
               axi_awready <= 1'b0;
             end
       else           
         begin
           axi_awready <= 1'b0;
         end
     end 
 end
// Implement axi_awaddr latching
 // This process is used to latch the address when both 
 // S_AXI_AWVALID and S_AXI_WVALID are valid.
always @( posedge S_AXI_ACLK )
 begin
   if ( S_AXI_ARESETN == 1'b0 )
     begin
       axi_awaddr <= 0;
     end 
   else
     begin    
       if (~axi_awready && S_AXI_AWVALID && S_AXI_WVALID && aw_en)
         begin
           // Write Address latching 
           axi_awaddr <= S_AXI_AWADDR;
         end
     end 
 end
// Implement axi_wready generation
 // axi_wready is asserted for one S_AXI_ACLK clock cycle when both
 // S_AXI_AWVALID and S_AXI_WVALID are asserted. axi_wready is 
 // de-asserted when reset is low.
always @( posedge S_AXI_ACLK )
 begin
   if ( S_AXI_ARESETN == 1'b0 )
     begin
       axi_wready <= 1'b0;
     end 
   else
     begin    
       if (~axi_wready && S_AXI_WVALID && S_AXI_AWVALID && aw_en )
         begin
           // slave is ready to accept write data when 
           // there is a valid write address and write data
           // on the write address and data bus. This design 
           // expects no outstanding transactions. 
           axi_wready <= 1'b1;
         end
       else
         begin
           axi_wready <= 1'b0;
         end
     end 
 end
// Implement memory mapped register select and write logic generation
 // The write data is accepted and written to memory mapped registers when
 // axi_awready, S_AXI_WVALID, axi_wready and S_AXI_WVALID are asserted. Write strobes are used to
 // select byte enables of slave registers while writing.
 // These registers are cleared when reset (active low) is applied.
 // Slave register write enable is asserted when valid address and data are available
 // and the slave is ready to accept the write address and write data.
 assign slv_reg_wren = axi_wready && S_AXI_WVALID && axi_awready && S_AXI_AWVALID;
always @( posedge S_AXI_ACLK )
 begin
   if ( S_AXI_ARESETN == 1'b0 )
     begin
//       slv_reg0 <= 0;
       slv_reg1 <= 0;
       slv_reg2 <= 0;
       slv_reg3 <= 0;
     end 
   else begin
     if (slv_reg_wren)
       begin
         case ( axi_awaddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] )
           2'h0:
             for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 )
               if ( S_AXI_WSTRB[byte_index] == 1 ) begin
                 // Respective byte enables are asserted as per write strobes 
                 // Slave register 0
//                 slv_reg0[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
               end  
           2'h1:
             for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 )
               if ( S_AXI_WSTRB[byte_index] == 1 ) begin
                 // Respective byte enables are asserted as per write strobes 
                 // Slave register 1
                 slv_reg1[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
               end  
           2'h2:
             for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 )
               if ( S_AXI_WSTRB[byte_index] == 1 ) begin
                 // Respective byte enables are asserted as per write strobes 
                 // Slave register 2
                 slv_reg2[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
               end  
           2'h3:
             for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 )
               if ( S_AXI_WSTRB[byte_index] == 1 ) begin
                 // Respective byte enables are asserted as per write strobes 
                 // Slave register 3
                 slv_reg3[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
               end  
           default : begin
//                       slv_reg0 <= slv_reg0;
                       slv_reg1 <= slv_reg1;
                       slv_reg2 <= slv_reg2;
                       slv_reg3 <= slv_reg3;
                     end
         endcase
       end
   end
 end
// Implement write response logic generation
 // The write response and response valid signals are asserted by the slave 
 // when axi_wready, S_AXI_WVALID, axi_wready and S_AXI_WVALID are asserted.  
 // This marks the acceptance of address and indicates the status of 
 // write transaction.
always @( posedge S_AXI_ACLK )
 begin
   if ( S_AXI_ARESETN == 1'b0 )
     begin
       axi_bvalid  <= 0;
       axi_bresp   <= 2'b0;
     end 
   else
     begin    
       if (axi_awready && S_AXI_AWVALID && ~axi_bvalid && axi_wready && S_AXI_WVALID)
         begin
           // indicates a valid write response is available
           axi_bvalid <= 1'b1;
           axi_bresp  <= 2'b0; // 'OKAY' response 
         end                   // work error responses in future
       else
         begin
           if (S_AXI_BREADY && axi_bvalid) 
             //check if bready is asserted while bvalid is high) 
             //(there is a possibility that bready is always asserted high)   
             begin
               axi_bvalid <= 1'b0; 
             end  
         end
     end
 end
// Implement axi_arready generation
 // axi_arready is asserted for one S_AXI_ACLK clock cycle when
 // S_AXI_ARVALID is asserted. axi_awready is 
 // de-asserted when reset (active low) is asserted. 
 // The read address is also latched when S_AXI_ARVALID is 
 // asserted. axi_araddr is reset to zero on reset assertion.
always @( posedge S_AXI_ACLK )
 begin
   if ( S_AXI_ARESETN == 1'b0 )
     begin
       axi_arready <= 1'b0;
       axi_araddr  <= 32'b0;
     end 
   else
     begin    
       if (~axi_arready && S_AXI_ARVALID)
         begin
           // indicates that the slave has acceped the valid read address
           axi_arready <= 1'b1;
           // Read address latching
           axi_araddr  <= S_AXI_ARADDR;
         end
       else
         begin
           axi_arready <= 1'b0;
         end
     end 
 end
// Implement axi_arvalid generation
 // axi_rvalid is asserted for one S_AXI_ACLK clock cycle when both 
 // S_AXI_ARVALID and axi_arready are asserted. The slave registers 
 // data are available on the axi_rdata bus at this instance. The 
 // assertion of axi_rvalid marks the validity of read data on the 
 // bus and axi_rresp indicates the status of read transaction.axi_rvalid 
 // is deasserted on reset (active low). axi_rresp and axi_rdata are 
 // cleared to zero on reset (active low).  
 always @( posedge S_AXI_ACLK )
 begin
   if ( S_AXI_ARESETN == 1'b0 )
     begin
       axi_rvalid <= 0;
       axi_rresp  <= 0;
     end 
   else
     begin    
       if (axi_arready && S_AXI_ARVALID && ~axi_rvalid)
         begin
           // Valid read data is available at the read data bus
           axi_rvalid <= 1'b1;
           axi_rresp  <= 2'b0; // 'OKAY' response
         end   
       else if (axi_rvalid && S_AXI_RREADY)
         begin
           // Read data is accepted by the master
           axi_rvalid <= 1'b0;
         end                
     end
 end
// Implement memory mapped register select and read logic generation
 // Slave register read enable is asserted when valid address is available
 // and the slave is ready to accept the read address.
 assign slv_reg_rden = axi_arready & S_AXI_ARVALID & ~axi_rvalid;
 always @(*)
 begin
       // Address decoding for reading registers
       case ( axi_araddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] )
         2'h0   : reg_data_out <= slv_reg0;
         2'h1   : reg_data_out <= slv_reg1;
         2'h2   : reg_data_out <= slv_reg2;
         2'h3   : reg_data_out <= slv_reg3;
         default : reg_data_out <= 0;
       endcase
 end
// Output register or memory read data
 always @( posedge S_AXI_ACLK )
 begin
   if ( S_AXI_ARESETN == 1'b0 )
     begin
       axi_rdata  <= 0;
     end 
   else
     begin    
       // When there is a valid read address (S_AXI_ARVALID) with 
       // acceptance of read address by the slave (axi_arready), 
       // output the read dada 
       if (slv_reg_rden)
         begin
           axi_rdata <= reg_data_out;     // register read data
         end   
     end
 end
// Add user logic here
 
 always @ (posedge S_AXI_ACLK) begin
    slv_reg0 <= slv_reg1 + slv_reg2;
end
// User logic ends
endmodule

The last section is also changed in the code where it says “add user logic here”. After making the changes to the code, save the file. Go to “Package IP” tab and select “review and package” -> Click Re-Package IP. This will add your IP to the repository. A dialogue box will appear asking to close the project -> click yes.

Step 2: Create the block design

Add Zynq processing system IP core and click the + sign. Click on “Run on block automation”.

Add our IP core by following the same step again. This time write “adder” in the search bar after clicking the + sign and you will see the name my_axi_adder_v1.0 in it. Click on it.

Click the “Run Connection Automation” -> OK.

You can hit the “regenerate layout” button to redraw it in an optimized manner(no connections are changed in this process) to make the block diagram a bit more clean.

Go to the address editor tab to see how to access the registers in your IP. Expand the contents in it.

You will see that my base address is 0x43C00000(could be different for you). I will be using this value while accessing the registers via SDK.

Step 3: Create an HDL wrapper for your design and generate bitstream.

Go to Sources -> right click on design_1.bd -> Create HDL wrapper -> Let Vivado manage wrapper and auto update -> OK.

Generate bitstream -> View Reports.

Export bitstream and launch SDK.

Go to file -> Export -> export hardware ->check include bitstream and click ok.

Program the FPGA.

Step 4: Launch SDK

File -> New -> Application Project.

In the project explorer, go to add_axi -> src -> Right click to make a new c file (call it testing.c). Replace the code with the following code. I wrote this code from this source. Read this to understand the following code.

/*
 * testing.c
 *
 *  Created on: Aug 17, 2018
 *      Author: prateek
 */
#include <stdio.h>
#include "xil_io.h"
#define CUSTOM_IP_BASEADDR 0x43C00000
#define REGISTER_1_OFFSET 0X00
#define REGISTER_2_OFFSET 0X04
#define REGISTER_3_OFFSET 0X08
// Function prototypes
void set_custom_ip_register(int baseaddr, int offset, int value);
int get_custom_ip_register(int baseaddr, int offset);
int main(){
 int temp1, temp2, temp3;
printf("writing to third register...");
 set_custom_ip_register(CUSTOM_IP_BASEADDR, REGISTER_3_OFFSET, 0XEE);
 printf("Done\n\r");
printf("writing to second register...");
 set_custom_ip_register(CUSTOM_IP_BASEADDR, REGISTER_2_OFFSET, 0XBB);
 printf("Done\n\r");
temp2 = get_custom_ip_register(CUSTOM_IP_BASEADDR, REGISTER_2_OFFSET);
 temp3 = get_custom_ip_register(CUSTOM_IP_BASEADDR, REGISTER_3_OFFSET);
 temp1 = get_custom_ip_register(CUSTOM_IP_BASEADDR, REGISTER_1_OFFSET);
printf("Register 2 = 0x%02X\n\r", temp2);
 printf("Register 3 = 0x%02X\n\r", temp3);
 printf("Register 1 = 0x%02X\n\r", temp1);
return 0;
}
void set_custom_ip_register(int baseaddr, int offset, int value){
 Xil_Out32(baseaddr + offset, value);
}
int get_custom_ip_register(int baseaddr, int offset){
 int temp = 0;
 temp = Xil_In32(baseaddr + offset);
 return(temp);
}

Step 5: Run the code on the hardware

Before running the code install miniterm on the your system. Configure it appropriately. Since in my case it is pynq board i.e. the reference manualmentions its details.

Click the run button on menu bar or goto Run -> Run -> launch on hardware (GDB) -> OK. See the output on the serial connection on the terminal.

This should give you the output and the inputs and you can also vary the inputs in the c code to check the results.

Happy Learning!