Continuing on from the previous PYNQ-Z2 Workshop blogs, this blog continues to investigate the topics covered in the Element 14 PYNQ-Z2 Workshop series led by Adam Taylor, where we took an introductory look at the PYNQ-Z2 development board produced by the Tul Corporation.
In this blog I explore using AXI GPIO from within Jupyter Notebooks to interact with logic designs implemented within the FPGA fabric.
In a previous blog PYNQ-Z2 Workshop - PS GPIO I created four logic gates using Verilog to use as custom IP in the FPGA fabric of the ZYNQ device. I reuse these custom IP blocks in this design but instead of using PS GPIO to interact with them from Jupyter notebooks, AXI GPIO will be used instead.
AXI is a point-to-point protocol for communicating between the processing system (PS) and the programable logic (PL) of the device. One side is set up as a master, the other a slave. In the ZYNQ device there are 9 AXI PS-PL interfaces:
- 4x high performance (AXI_HP)
- 1x cache-coherent (AXI_ACP)
- 4x general purpose (AXI_GP)
and we can see seven of them with the master originating in the PL side here in the Xilinx UG585 documentation, upper left to upper right:
The two remaining AXI_GP interfaces which have the master originating in the PS side, are shown lower centre and these are the ones we will be using in this blog.
AXI_GP consists of four general purpose ports conforming to the AXI 3 interface specification (two master ports and two slave ports) each with a 32bit data width. In the above diagram the master ports can be seen in the Master Interconnect for Slave Peripherals interconnect switch and they connect to General Purpose AXI Controllers in the PL Logic. They are referred to as M_AXI_GP0 and M_AXI_GP1 on the PS side and as we can see from the Xilinx UG585 documentation, they are mapped at the following address ranges:
Multiple AXI GPIO controllers (AXI slaves) can be implemented in the FPGA fabric, and in this design four are used, one per logic gate. These will need to be connected to AXI masters in an intermediate AXI interconnect which then connects back to M_AXI_GP0.
Each AXI GPIO controller has two channels, and often in examples only one of them is used for simplicity and/or demonstration purposes, however here we will use the channel 0 as a GPIO output to connect to the inputs of the logic gate and channel 1 as a GPIO input to connect to the output of the logic gate.
Here is the completed block design which will be turned into an overlay for use with the PYNQ framework:
Starting the design in Xilinx Vivado
Starting with the ZYNQ7 Processing system we use the Master AXI GP0 output (M_AXI_GP0) to connect to the PL.
This needs to be enabled in the ZYNQ7 Processing System configuration.
M_AXI_GP1 interface should be left disabled as we aren't using it in this design.
As AXI is a point-to-point protocol and as we have four AXI GPIO controllers we must first use an AXI Interconnect to create four Master AXI ports, one for each controller. The AXI Interconnect also provides the necessary conversion to convert from the AXI3 protocol on the PS:
to AXI4Lite protocol on the AXI GPIO controller IP:
Each AXI GPIO controller needs its second channel enabling and its GPIO ports configuring.
The AND, OR and XOR GPIOs require GPIO setting to a 2bit wide output, and GPIO2 setting to a 1bit wide input.
The NOT GPIO requires GPIO setting to a 1bit wide output and GPIO2 setting to a 1bit wide input.
Slice blocks are then used to split the 2bit outputs into two separate 1bit outputs to be used with the 1bit wide inputs to the AND, OR, and XOR logic gates.
For logic gate Input A we will use bit 0 on the input:
and for logic gate Input B we will use bit 1 on the input:
The NOT gate does not require a slice block as it is already a 1bit output which can be directly connected to the 1bit input.
Concat blocks aren't required on the logic gate outputs this time as they are being connected to 1bit inputs on the AXI GPIO controller blocks.
Before we can make use of the AXI GPIO controllers in the PS, they require their offset addresses configuring as by default they appear as Unmapped Slaves.
Addresses can be assigned automatically, but it is always worth checking that they fall within the correct range as per the documentation. For M_AXI_GP0 we need them to be within the address range of 0x4000_0000 to 0x7FFF_FFFF.
The clocks and reset lines need to be to be connected, this can either be done manually or via automation.
This completes the FPGA design and if it is all connected up correctly, then this will allow the input states of the four gates (LUTs) defined in the PL to be configured by Jupyter in the PS and the output states to be read all using the AXI GPIO interconnects.
The final steps required in Vivado were to Validate Design, Create HDL Wrapper, Run Synthesis & Implementation, and Generate the Bitstream. The resulting files were copied over onto the PYNQ-Z2 dev board ready to be loaded into the PYNQ framework as a custom overlay.
Continuing the design in Jupyter Notebooks
Within the Jupyter environment a new notebook was created and within the IPython environment the newly created overlay was loaded.
from pynq import Overlay axi_gpio_design = Overlay("./bitstream/logic_gpio_axi_overlay.bit")
We can query the physical addresses for the AXI GPIO controllers from the IP dictionary
and_address = axi_gpio_design.ip_dict['axi_gpio_and']['phys_addr'] or_address = axi_gpio_design.ip_dict['axi_gpio_or']['phys_addr'] xor_address = axi_gpio_design.ip_dict['axi_gpio_xor']['phys_addr'] not_address = axi_gpio_design.ip_dict['axi_gpio_not']['phys_addr']
and print them out to check they match what is expected
print("Physical address of axi_gpio_and GPIO controller: 0x" + format(and_address,'02x')) print("Physical address of axi_gpio_or GPIO controller: 0x" + format(or_address,'02x')) print("Physical address of axi_gpio_xor GPIO controller: 0x" + format(xor_address,'02x')) print("Physical address of axi_gpio_not GPIO controller: 0x" + format(not_address,'02x'))
Looks good !
The AxiGPIO class will be used to access the AXI GPIO controllers
from pynq.lib import AxiGPIO
Create dictionaries for each AXI GPIO controller from the main IP dictionary
and_dict = axi_gpio_design.ip_dict['axi_gpio_and'] or_dict = axi_gpio_design.ip_dict['axi_gpio_or'] xor_dict = axi_gpio_design.ip_dict['axi_gpio_xor'] not_dict = axi_gpio_design.ip_dict['axi_gpio_not']
Set up instances for the outputs (attached to the inputs of the logic gates under test). These were attached to the first channel (GPIO) of the AXI GPIO controller.
AND_input = AxiGPIO(and_dict).channel1 OR_input = AxiGPIO(or_dict).channel1 XOR_input = AxiGPIO(xor_dict).channel1 NOT_input = AxiGPIO(not_dict).channel1
Set up instances for the inputs (attached to the outputs of the logic gates under test). These were attached to the second channel (GPIO2) of the AXI GPIO controller.
AND_output_C = AxiGPIO(and_dict).channel2 OR_output_C = AxiGPIO(or_dict).channel2 XOR_output_C = AxiGPIO(xor_dict).channel2 NOT_output_B = AxiGPIO(not_dict).channel2
That is AXI GPIO set up and ready to use.
Test the AND gate
AND_input[0:2].write(0x0) # set AND_input_A low AND_input_B low print(f"AND output_C: {AND_output_C.read()}") AND_input[0:2].write(0x1) # set AND_input_A high AND_input_B low print(f"AND output_C: {AND_output_C.read()}") AND_input[0:2].write(0x2) # set AND_input_A low AND_input_B high print(f"AND output_C: {AND_output_C.read()}") AND_input[0:2].write(0x3) # set AND_input_A high AND_input_B high print(f"AND output_C: {AND_output_C.read()}")
Test the OR gate
OR_input[0:2].write(0x0) # set OR_input_A low OR_input_B low print(f"OR output_C: {OR_output_C.read()}") OR_input[0:2].write(0x1) # set OR_input_A high OR_input_B low print(f"OR output_C: {OR_output_C.read()}") OR_input[0:2].write(0x2) # set OR_input_A low OR_input_B high print(f"OR output_C: {OR_output_C.read()}") OR_input[0:2].write(0x3) # set OR_input_A high OR_input_B high print(f"OR output_C: {OR_output_C.read()}")
Test the XOR gate
XOR_input[0:2].write(0x0) # set XOR_input_A low XOR_input_B low print(f"XOR output_C: {XOR_output_C.read()}") XOR_input[0:2].write(0x1) # set XOR_input_A high XOR_input_B low print(f"XOR output_C: {XOR_output_C.read()}") XOR_input[0:2].write(0x2) # set XOR_input_A low XOR_input_B high print(f"XOR output_C: {XOR_output_C.read()}") XOR_input[0:2].write(0x3) # set XOR_input_A high XOR_input_B high print(f"XOR output_C: {XOR_output_C.read()}")
Test the NOT gate
NOT_input[0:1].write(0x0) # set NOT_input_A low print(f"NOT output_B: {NOT_output_B.read()}") NOT_input[0:1].write(0x1) # set NOT_input_A high print(f"NOT output_B: {NOT_output_B.read()}")
The outputs of these four gates would all appear to be behaving as expected. Success !
That completes an overview and test of AXI GPIO on the PYNQ-Z2 board.
Top Comments