In our previous training we demonstrated how to: Create a Vivado design containing multiple AXI GPIO blocks. And Control these AXI GPIOs in Vitis using the Memory-Mapped I/O (MMIO) concept:
https://www.hackster.io/fpgaps/axi-gpio-memory-mapped-i-o-mmio-read-write-using-c-pointer-0db8a9
In this training, we will take it a step further by explaining: How to access and interact with peripherals directly in a Jupyter Notebook.
Learn how to interface with AXI GPIO blocks using both high-level GPIO classes and low-level MMIO access methods. The video demonstrates:
- Setting up your Jupyter environment and uploading necessary bitstream files
- Exploring Overlay attributes and IP block access in PYNQ
- Implementing GPIO control using both AXI GPIO class and MMIO methods
- Creating an interactive LED control system using DIP switches
You can find the full training video here:
Brief Overview of Vivado designThe Vivado design of this project contains 5 AXI GPIOs:
- AXI GPIO 0: is connected to User LEDs, ARM core blinks the user LEDs in circular manner by sending the shifted values to this AXI GPIO.
- AXI GPIO 1: is connected to the user DIP switch. In polling mode, ARM core reads the status of the DIP switch and change the blinking status based on the
- AXI GPIO 2, 3, 4: to gain more experience, there is an adder in the PL side that its inputs are connected to the two AXI GPIOs. ARM core sends data to the PL side and can read back the calculation results over AXI GPIO 3, that is connected to the output of adder.
you can download the Jupyter Notebook from following GitHub repository:
https://github.com/FPGAPS/PYNQ_AXI_GPIO_MMIO
Steps to Upload the Overlay and Driver Code- Prepare the Jupyter Environment:
- Open Jupyter Notebook and create a new folder. Rename it as per your preference.
- Upload Required Files:
- Navigate to the Vivado project folder and locate the.hwh file. Upload this file into the newly created folder in Jupyter.
- Locate the generated bitstream file (usually.bit) and upload it to the same folder.
- Important: Ensure that the.hwh and.bit files have the same base name. If needed, rename them to match your desired naming convention.
- Add the Notebook File:
- Download the provided notebook file from GitHub and upload it to the same directory.
At this point, everything is ready to begin.
Overview of the Provided Driver Code
- At the beginning of the code, various modules are imported, including the mmio module.
# Various modules are imported
from pynq import Overlay
from pynq.lib import AxiGPIO
from pynq import MMIO
import time
- The Overlay class is used to program the programmable logic (PL) of the FPGA. The PL side will be programmed with the provided bitstream.
- This process utilizes the.hwh file to initialize and configure all required PYNQ functions, enabling you to interact with the design effortlessly.
# Probram the PL with the provided bitstream
# utilizes the .hwh file to initialize and configure all required PYNQ
ol = Overlay("design_1.bit")
Exploring the Overlay Attributes
When you place a question mark (?) in front of the Overlay object in a Jupyter Notebook, it displays all the available attributes within the loaded overlay.
# Exploring the Overlay Attributes
ol?
Key Information: IP Blocks
- One of the most important sections in the attribute list is IP Blocks.
- This section lists all the connected Intellectual Property (IP) blocks in the design that are accessible to the ARM core.
- It provides an easy way to identify and interact with the IPs from your Vivado design.
Example:
In the example below, the overlay includes four AXI GPIOs. Their names and associated classes are listed, making it straightforward to locate them in the Vivado block design. We will use these names later to create the AXI GPIO class with read and write access.
In PYNQ, there are several ways to interact with AXI GPIO peripherals. One of the simplest and most convenient is using the AXI GPIO class.
AXI GPIO ClassFeatures of the AXI GPIO Class:
- Provides methods for reading, writing, and handling interrupts from external general-purpose peripherals.
- Allows you to interact with GPIO blocks by referencing their names from the IP Blocks list displayed in the overlay attributes.
For complete information you can refer to the following PYNQ documentation page:
https://pynq.readthedocs.io/en/v2.4/pynq_libraries/axigpio.html
for example here we create an object of AXI GPIO class to control the user LEDs:
# Define an AXI GPIO object
# The read() and write() methods are used to read and write data on a channel (all of the GPIO).
led_ip = ol.axi_gpio_0
and here we create another AXI GPIO object to read the DIP switch:
# Define an AXI GPIO object for accessing the DIP switch
dip_sw_ip = ol.axi_gpio_1
AXI GPIO Class read and write:
Using the read() and write() Methods:
- The write() method requires two parameters:
- Offset: The address offset of the GPIO register.
- Value: The data to be written.
- For an AXI GPIO, the Address Space Offset for Channel 1 Data Register is always 0.
- This means to access or modify the data of an AXI GPIO, you need to perform read() and write() operations at the base address of the AXI GPIO with an offset of 0.
Following line of codes write into the AXI GPIO 0 and turn on the first two LEDs:
# Offset: The address offset of the GPIO register.
ADDRESS_OFFSET = 0
# Value: The data to be written.
Value = 0x03
# turn on first two LEDs
led_ip.write(ADDRESS_OFFSET,Value)
- The read() method only has offset register as input and returns the read value.
For example following code returns the read value from user DIP switch:
read_value = dip_sw_ip.read(ADDRESS_OFFSET)
print("DIP Switch mode:",read_value)
MMIO ClassThe MMIO class allows a Python object to access addresses in the system memory mapped. In particular, registers and address space of peripherals in the PL can be accessed.
For complete information you can refer to the following PYNQ documentation page:
https://pynq.readthedocs.io/en/v2.1/pynq_libraries/mmio.html
Define an MMIO object:To define an MMIO object, you need two parameters: the base address of the peripheral you want to access and its address range. Both of these parameters can be found in the Vivado Address Editor or the hwh file.
In the following section of the code, I listed all the AXI GPIO addresses as obtained from the Vivado Address Editor. The address range for all GPIOs is the same, set to 64K.
# AXI GPIO 0 base address
AXI_GPIO_0_BASE_ADDRESS = 0xA0000000
# AXI GPIO 1 base address
AXI_GPIO_1_BASE_ADDRESS = 0xA0010000
# AXI GPIO 2 base address
AXI_GPIO_2_BASE_ADDRESS = 0xA0020000
# AXI GPIO 3 base address
AXI_GPIO_3_BASE_ADDRESS = 0xA0030000
# AXI GPIO 4 base address
AXI_GPIO_4_BASE_ADDRESS = 0xA0040000
# Address range
ADDRESS_RANGE = 64000
Using these addresses, I created an MMIO object for each AXI GPIO.
# MMIO Object to control AXI GPIO 0
gpio_leds = MMIO(AXI_GPIO_0_BASE_ADDRESS, ADDRESS_RANGE)
# MMIO Object to control AXI GPIO 1
gpio_dips = MMIO(AXI_GPIO_1_BASE_ADDRESS, ADDRESS_RANGE)
# MMIO Object to control AXI GPIO 2
AXI_GPIO_2_mmio = MMIO(AXI_GPIO_2_BASE_ADDRESS, ADDRESS_RANGE)
# MMIO Object to control AXI GPIO 3
AXI_GPIO_3_mmio = MMIO(AXI_GPIO_3_BASE_ADDRESS, ADDRESS_RANGE)
# MMIO Object to control AXI GPIO 4
AXI_GPIO_4_mmio = MMIO(AXI_GPIO_4_BASE_ADDRESS, ADDRESS_RANGE)
The MMIO class provides read and write methods to interact with the peripherals. For Example: The following code demonstrates how to send values to an Adder implemented in the PL by interfacing with AXI GPIO 2 and AXI GPIO 3, and then reading the calculated value back:
# send values to the Adder implemented in the PL and read back the result
for i in range(3):
# write value to AXI GPIO 2
inp_1 = 2*i
AXI_GPIO_2_mmio.write(ADDRESS_OFFSET,inp_1)
# write value to AXI GPIO 3
inp_2 = 3*i
AXI_GPIO_3_mmio.write(ADDRESS_OFFSET,inp_2)
# read the AXI GPIO4
results_out = AXI_GPIO_4_mmio.read(ADDRESS_OFFSET)
print(inp_1 , " + ", inp_2, " = ", results_out)
In the second part, the program reads the DIP switch inputs in polled mode. This means the system continuously checks for input changes in a loop and applies corresponding adjustments.
Each of the 4 DIP switch bits controls a specific LED behavior:
- Bit 0: Controls the shift direction. Toggle this switch to change whether the LEDs shift left or right.
- Bit 1: Adjusts the speed of the LED shift. When set, the shift slows down.
- Bit 2: Acts as a pause button. Setting this bit stops the LED shifting until it’s toggled back.
- Bit 3: Turns all LEDs on or off. When set, the LEDs are off regardless of the shift pattern.
# Initialize variables
init_shift_value = 0b0001
LED_Shiffer_Value = init_shift_value & 0b1111
shift_direction = 1 # 1 for right, 0 for left
usecond_sleep = 125000 / 1e6 # Convert microseconds to seconds for Python's sleep
stop_shift = 0 # Will stop the shift if this is 1
off_leds = 0 # Turn off all LEDs if this value is 1
# Track the previous DIP switch value
previous_dip_switch_value = -1 # Start with an invalid value to ensure it prints the first time
for i in range(10000):
# Write the value to the User LEDs
if off_leds == 0:
gpio_leds.write(0x00, LED_Shiffer_Value)
else:
gpio_leds.write(0x00, 0)
# Delay in the loop (in seconds)
time.sleep(usecond_sleep)
# Read the User DIP switch in polled mode
dip_switch_value = gpio_dips.read(0x00) & 0b1111
# If the DIP switch value changes, print the new value
if dip_switch_value != previous_dip_switch_value:
print(f"New DIP switch value: {bin(dip_switch_value)}")
previous_dip_switch_value = dip_switch_value # Update the tracked value
# Update parameters based on the DIP switch value
shift_direction = dip_switch_value & 0b0001 # First bit controls the direction
usecond_sleep = 125000 * (1 if (dip_switch_value & 0b0010) == 0 else 2) / 1e6 # Second bit controls the speed
stop_shift = (dip_switch_value & 0b0100) # Third bit to stop or run
off_leds = (dip_switch_value & 0b1000) # Fourth bit to turn off or on
# Shift the LEDs based on the selected direction
if stop_shift == 0:
if shift_direction == 0: # Left shift
LED_Shiffer_Value = (LED_Shiffer_Value << 1) & 0b1111
init_shift_value = 0b0001
else: # Right shift
LED_Shiffer_Value = (LED_Shiffer_Value >> 1) & 0b1111
init_shift_value = 0b1000
# Reset the value if it shifts out completely
if LED_Shiffer_Value == 0:
LED_Shiffer_Value = init_shift_value & 0b1111
Each time that the DIP switch is changed, the program also prints the new status of the DIP switch:
After running this section, here’s what to expect:
- Flipping the first switch changes the LED shift direction.
- The second switch slows down the LED shifting.
- The third switch pauses the shifting.
- The fourth switch turns off all LEDs.
Comments