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:


AXI Interconnect Diagram from Xilinx UG585


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:


System Level Address Map showing AXI_GP port addresses


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:

screenshot of the completed block design in Vivado


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.

screenshot showing AXI GP0 enabled in ZYNQ7 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:

screenshot of AXI3 protocol on ZYNQ7


to AXI4Lite protocol on the AXI GPIO controller IP:

screenshot of AXI4Lite protocol on GPIO controller



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.

screenshot of AXI GPIO GPIO port settings for AND gate


The NOT GPIO requires GPIO setting to a 1bit wide output and GPIO2 setting to a 1bit wide input.

screenshot of AXI GPIO GPIO port settings for NOT gate


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:

slice configuration for gate Input A


and for logic gate Input B we will use bit 1 on the input:

slice configuration for gate Input B


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.

screenshot of AXI GPIO showing unmapped addresses

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.

screenshot showing AXI GPIO mapped to addresses

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")

screenshot from Jupyter notebooks


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']


screenshot of Jupyter notebook


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'))


screenshot from Jupyter notebooks

Looks good !


The AxiGPIO class will be used to access the AXI GPIO controllers


from pynq.lib import AxiGPIO


screenshot from Jupyter notebooks

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']

screenshot from Jupyter notebooks

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

screenshot from Jupyter notebooks

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


screenshot from Jupyter notebooks

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_input[0:2].write(0x1) # set AND_input_A high AND_input_B low
print(f"AND output_C: {}")
AND_input[0:2].write(0x2) # set AND_input_A low AND_input_B high
print(f"AND output_C: {}")
AND_input[0:2].write(0x3) # set AND_input_A high AND_input_B high
print(f"AND output_C: {}")

screenshot from Jupyter notebooks

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_input[0:2].write(0x1) # set OR_input_A high OR_input_B low
print(f"OR output_C: {}")
OR_input[0:2].write(0x2) # set OR_input_A low OR_input_B high
print(f"OR output_C: {}")
OR_input[0:2].write(0x3) # set OR_input_A high OR_input_B high
print(f"OR output_C: {}")


screenshot from Jupyter notebooks

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_input[0:2].write(0x1) # set XOR_input_A high XOR_input_B low
print(f"XOR output_C: {}")
XOR_input[0:2].write(0x2) # set XOR_input_A low XOR_input_B high
print(f"XOR output_C: {}")
XOR_input[0:2].write(0x3) # set XOR_input_A high XOR_input_B high
print(f"XOR output_C: {}")


screenshot from Jupyter notebooks

Test the NOT gate


NOT_input[0:1].write(0x0) # set NOT_input_A low
print(f"NOT output_B: {}")
NOT_input[0:1].write(0x1) # set NOT_input_A high
print(f"NOT output_B: {}")


screenshot of Jupyter notebook

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.