FPGA & CPU Shared Memory

This example shows how to transfer data from programmable logic to the processing system by using shared memory. Data will be written to the memory only by using FPGA resources. From the CPU prospective data will just appear in the desired memory location.

This can be achieved by using AXI master interface. In the simplest case AXI master interface allows to write 32-bit word to 1024 memory locations. Depending on the chip that you are using, it is possible to use multiple AXI master interfaces to write data asynchronously to different memory locations.

Comment: this guide requires some basic Verilog / Vivado knowledge. I suggest you start with ULTRA96 tutorial 1 & 2.

Comment: PYNQ tutorials can be found here.

Step 1 – Reserve DDR memory region (optional)

  • You don’t need to do that if you want to run your application bare metal (without OS).
  • If you want to transfer data to the CPU running Linux, you should reserve DDR memory region from Linux. Otherwise, Linux will use all the available memory for its runtime and it will be impossible to access memory from the PL side. 
  • Memory reservation is explained in “How to Reserve DDR region in Xilinx Petalinux ULTRA96”.

Step 2 – Create FPGA Project

Zynq PS block after block connection automation
  • Create a new Vivado 2018.2 project targeting the board of your choice (for example ULTRA96).
  • Create a new block design.
  • Add ZYNQ UltraScale PS block and run block automation.
  • Double click on the PS block and deselect AXI master interfaces in PS-PL configuration window.
  • Enable AXI HP0 FPD.
PS-PL Interface Configuration
  • Your Zynq PS block should look as follows:
Zynq PS Block with AXI HP Slave Port

Create new AXI master IP

  • Next step is to create IP with AXI Master Interface.
  • Go to “Tools -> Create and Package New IP”.
  • Name yourIP and Select “Create a new AXI4peripheral”
  • Select Interface and interface mode as shown below.
AXI Master Interface Configuration
  • Add the IP to the repository.
  • Add new IP to the block design.
  • Add AXI interconnect IP to the block design. This block will take care of data synchronization between 2 different interfaces.
  • Interconnect IPs as shown below.
Block Design Interconnection
  • Double click on your AXI Master IP.
  • Set AXI burst length to 1.
  • Set AXI target slave base address. 
AXI Master IP Block Configuration

Comment: slave base address is should point to the memory region that you want to use to save the data. You should choose the same base address in the “Address Editor window” (HP0_DDR_LOW). Also, you can set the range to (4KByte -> 1024 32-bit words).

Address Editor

Comment:in the next few steps we will edit AXI master interface to allow the user to write data to the memory. This can be achieved by adding “data_in” port and “write_next” port. “data_in” port will be used to send user data to the memory. Positive edge in “write_next” port will indicate that the AXI Master is ready to write new data to the memory cell.

  • Right click on your AXI Master IP and choose “Edit in IP Packager”
  • This will open a new window. AXI Master IP has 2 layers:
    • HDL Wrapper that listsall the parameters and interconnects the signals inside the IP.
    • AXI Master instance contains software that performsAXI read-write.
  • Go to Sources > Libraries tab,and edit both design sources as shown below.
  • Add following lines to both sources after “//Users to add ports here” (line 25 in *_v1_0.v, 33 in*_ v1_0_M00_AXI.v)
    • input wire [31:0] data_in,
    • output wire write_next,
IP HDL Wrapper Update 1
  • Add following lines to HDL Wrapper (after line 140in *_v1_0.v)
    • .data_in(data_in),
    • .write_next(write_next)
IP HDL Wrapper Update 2
  • Edit M00_AXI_inst as shown in the picture below (“Write Data Generator” function):
Add “data_in” port
  • Define “write_next” port as shown below
“write_next” port definition
  • Merge all changes in the “Package IP” tab, close the window and update the IP in the block design.
Package IP Window
  • After changes your AXI master IP should look as follows:
Updated AXI Master IP

Signals are explained in the table below.

SignalDescription
[31:0]data_in32-bit user data input.
m00_axi_init_axi_txnInput that awaits for the positive edge to begin memory writing process. This port can be connected to the button or controlled by Verilog source.
write_nextOutput. Positive edge indicates that new 32-bit word is ready to be written to the memory.
m00_axi_txn_doneOutput. Positive edge indicates that transmission has finished. Can be used to start a new transmission and/or to count transmissions.
  • To finish this example, we will write 32-bit word to the memory address 0x5e000000. The transmission will be initiated with a delayed impulse.
  • Create 2 new Verilog sources “Word_Generator” and “start_tx”.
  • Verilog code for the Word Generator is shown below.
Code for “Word_Generator”
  • Verilog code for transmission generator is shown below.

Comment: transmission generator creates a single rising edge after 255 clock cycles.

Code for the Transmission Generator
  • IP blocks should be interconnected as shown below.
Complete Block Design
  • Create a HDL Wrapper.
  • Generate Bitstream.

Step 3 – Create Python application in Jupiter Notebooks in PYNQ

  • Generate Bitstream
  • Go to “File” -> “Export Block Design”
  • Go to your PYNQ SD card /jupyter_notebooks and create a new folder
  • Go to your project folder and copy the following files in the new folder
    • “design_1”.
    • go to “/<project_name> .runs/impl_1”and copy “design_1_wrapper.bit”.
  • Open Jupyter Notebooks in your browser, go to your folder and create a Python file.
  • You can read the memory with MMIO function.
    Set “IP_BASE_ADDRESS” to correspond the address in the Vivado project.
    If you run the code as shown below (“design_1” instead of “design_2”) you will see the bit word that you have defined in your Vivado project.
Memory Read in Jupiter Notebooks

Step 4 – Create C application in Xilinx SDK

  • Generate Bitstream
  • Go to “File” -> “Export Hardware” (Include bitstream).
  • Go to “File” -> Launch SDK
  • Wait for your hardware platform to import
  • Create a new “Hello World” project
  • Update “helloworld.c” with code below.
  • Program FPGA and run your application.
  • You should see your 32-bit word in your serial monitor
#include <stdio.h>
#include "xil_cache.h"
#include "xparameters.h"
#include "xil_cache.h"
#include "xil_io.h"

int main()
{
   init_platform();
   Xil_DCacheDisable();
   Data1 = Xil_In32(0x40000000);
   Data2 = Xil_In32(0x40000004);
   Data3 = Xil_In32(0x40000008);
   printf("Data1: %x\n", Data1);
   printf("Data2: %x\n", Data2);
   printf("Data3: %x\n", Data3);
   cleanup_platform();
   return 0;
}

Comment: replace 0x40000000 with the memory location specified in your FPGA program.