Learn how to master AXI GPIO and memory mapped I/O on Zynq UltraScale+ devices in this tutorial!
This tutorial walks you through creating a complete hardware-software project that demonstrates how to control peripherals from ARM cores using memory-mapped I/O.
The Vitis application read the DIP switch in polling mode and controls the LED shift pattern.
In Part 1, you'll learn how to:
- Set up a Vivado block design with Zynq UltraScale+ IP
- Configure multiple AXI GPIO blocks for LED control and DIP switch input
- Implement a 32-bit adder in programmable logic that its ports connected to AXI GPIOs
- Review the "Address Editor" in Vivado, and compare it with "Addressing View" option in block design
- Generate and export hardware for Vitis
Part 2 covers the software implementation in Vitis where you'll discover:
- How to use memory-mapped I/O with C pointers to access peripherals
- Working with Xil_Out32 and Xil_In32 functions for reading/writing to hardware
- Creating an interactive LED pattern controlled by DIP switches
- Understanding memory addressing and hardware abstraction
You can find the complete Video here:
Memory-mapped I/O (MMIO)Memory-mapped I/O (MMIO) is a technique that allows a computer's central processing unit (CPU) to communicate with peripheral devices by using special memory addresses and memory read and write instructions:
Here are some benefits of memory-mapped I/O:
- Simplifies I/O operations: MMIO makes it easier to control I/O devices by allowing the CPU to speak directly to them.
- Improves performance: MMIO can lead to significant performance improvements.
- Unifiesmemory and I/O: MMIO treats I/O and memory similarly, which can make software design easier.
MMIO is one of several ways to perform input/output (I/O) between a CPU and peripheral devices.
Create Vivado DesignAdd ZYNQ IP to the block design
Create a new project and select ZCU104 from board list. With our project set up, ladd the ZYNQ Ultrascale+ IP. You'll see a banner appear at the top of the diagram suggesting to run block automation. Click on "Run Block Automation, " select "Apply Board Preset, " and click "OK." This process configures the IP with preset parameters that match the ZCU104 board layout.
AXI GPIO for User LED Control
Add an AXI GPIO to the block design to control the four user LEDs. Set the direction to All Output and configure the bit width to 4 bits. Expose the AXI GPIO output as an external port, rename the created port to LED_control. During I/O planning, these ports will be assigned to the correct pins.
AXI GPIO for User DIP Switch
Next, add another AXI GPIO for reading the user DIP switch. Set its direction to Input and configure its bit width to 4 bits. Make the input as external port, and rename it to dip switch.
To gain further practice, add two additional AXI GPIOs.
- For each of these, set the bit width to 32 and change the direction to Output.
- Then, add another AXI GPIO, set its bit width to 32, and configure its direction to Input.
For some additional functionality, let's use these GPIOs for simple arithmetic. Add an Adder IP block to the design, change the adder’s input width to 32 bits and remove the clock enable option. Connect the outputs of the first two AXI GPIOs to the inputs of the Adder. Then, route the output of the Adder to the final AXI GPIO.
Since only one master port is enough for ARM cores, Double click on ZYNQ IP and remove the second High performance master port:
At this point, we can let Vivado handle the connections automatically. Run the Auto Connection Automation feature, which will streamline the process. This will automatically add a Processor System Reset and an AXI Interconnect to the design.
Vivado will then connect the Zynq IP Master port to the AXI Interconnect, and from there, to all the AXI GPIOs. This setup allows the Processing System (PS) to act as the Master, providing full access to the AXI GPIOs, which function as Slaves.
Let’s highlight the clock and reset signal for better vision, as you can see the whole AXI network is running with the same clock and reset signal.
PS - PL Clock
Inside the PS section of the Zynq UltraScale+, multiple PLLs are available to generate and manage various clock signals. For instance, one of these PLLs can be configured to provide a 100 MHz clock signal to the Programmable Logic for synchronization and operation.
Processor System Reset
You cannot directly connect the PS reset to other peripherals. The Processor System (PS) reset is specifically designed to provide a synchronized reset signal aligned with the AXI clock. It takes the reset signal from the PS, synchronizes it with the provided clock, and then distributes it to all connected peripherals.
Addresses of Connected PeripheralsAfter you finished your design, "Validate" it. Once the design is validated, Vivado will automatically assign addresses to the AXI GPIOs. This process, known as Memory Mapping, allows the ARM cores, acting as masters, to access all connected peripherals using their specific addresses.
For example, when the ARM core needs to read from or write to AXI GPIO 0, it will reference the unique address assigned to that peripheral. This mapping ensures seamless communication between the Processing System and the peripherals.
For better understanding, you can change the view style to “Addressing View” in the Vivado design.
This mode simplifies the diagram by removing additional connections like clocks and resets, focusing instead on how each master is connected to specific slaves. It clearly shows which master has access to which slaves based on their address mapping.
For example, in this design, the Processing System (PS) has access to all AXI GPIOs through their unique addresses.
Now, we are ready to procced to synthesis. Add the top wrapper and allow the Vivado to manage and handle it.
Run the Synthesis design and wait until is finished.
Assigning the I/O ports: Open Synthesized design and Open I/O ports.at this stage, refer to the datasheet or user manual for your board to assign the correct input and output pins. If you are working with an evaluation board you can also use the board files to find those pins.
Assign the LED Pins: Based on the ZCU104 board, assign the appropriate pins for the LED outputs. These should be connected to the specific pins listed in the board's documentation.
Connect the DIP Switch Pins: Locate the user DIP switch pins in the documentation and connect them accordingly.
Once the pins are assigned, save your I/O connections as an XDC file. If you're working with the ZCU104, I've also shared a pre-configured XDC file in the GitHub repository for convenience.
Generate bitstream and Export XSAWith everything set, you’re now ready to generate the bitstream. From the left-hand toolbar, select Generate Bitstream and wait for the process to complete.
Exporting the Hardware: The last step in Vivado is to export our hardware design for use in Vitis. Go to "File" > "Export" > "Export Hardware, " and make sure to check the "Include bitstream" option.
This process creates a.xsa (Xilinx Support Archive) file, which includes the hardware description, PS configuration, and PL bitstream. This file is the bridge between our hardware design in Vivado and our software development in Vitis.
Vitis DesignCreate new project: Now that we have our hardware design, let's switch over to Vitis to develop our software. Launch Vitis IDE and create a new application project.
When prompted, select the XSA file we just exported from Vivido and create a new platform.
complete all the steps, and use the "Hello World" template as our starting point. This gives us a basic structure to work with and ensures that our development environment is set up correctly.
Provided Driver Code
You can download the code from our GitHub repository.
https://github.com/FPGAPS/AXI-GPIO-memory-map
Simply copy the code and replace the content of the “helloworld.c” file.
Now, let’s take a closer look at the different sections of this driver code.
Address Definitions:
The code defines macros for the base addresses of each AXI GPIO peripheral. Each GPIO module is assigned a unique memory address. these macros are derived from "xparameters.h" header file, and correspond to the addresses in implemented hardware design in Vivado.
//-------------------------------------------------
// AXI GPIOs addresses here: You can also find them
#define AXI_GPIO_0 XPAR_AXI_GPIO_0_BASEADDR //0xA0000000 LED control
#define AXI_GPIO_1 XPAR_AXI_GPIO_1_BASEADDR //0xA0010000 DIP switch
#define AXI_GPIO_2 XPAR_AXI_GPIO_2_BASEADDR //0xA0020000 First input for Adder
#define AXI_GPIO_3 XPAR_AXI_GPIO_3_BASEADDR //0xA0030000 Second input for Adder
#define AXI_GPIO_4 XPAR_AXI_GPIO_4_BASEADDR //0xA0040000 Adder's output
"xparameters.h" header is a very important file, and it Contains the base addresses of AXI peripherals, mapped from Vivado. You can find the address of different slave elements in this file.
Xil Out and Xil In Functions:
At the core of our code there are two key functions:
- Xil_Out32: Sends 32-bit data to a specific hardware address. This is how we send control or data values to the peripherals.
- Xil_In32: Reads 32-bit data from a hardware address. This is how we retrieve information, such as the state of the DIP switches.
These functions abstract the complexity of memory-mapped I/O, making it easy to interact with the hardware.
Let’s take a closer look at these two functions to understand how they work. You can find the implementation of this two functions in xil I/O C file.
/*****************************************************************************/
static INLINE void Xil_Out32(UINTPTR Addr, u32 Value)
{
/* write 32 bit value to specified address */
#ifndef ENABLE_SAFETY
volatile u32 *LocalAddr = (volatile u32 *)Addr;
*LocalAddr = Value;
#else
XStl_RegUpdate(Addr, Value);
#endif
}
/*****************************************************************************/
static INLINE u32 Xil_In32(UINTPTR Addr)
{
return *(volatile u32 *) Addr;
}
As you can see, Xil_Out32 defines a volatile pointer, assigns it the address of a peripheral, and then uses that pointer to write a value to the specified address.
Similarly, Xil_In32 defines a volatile pointer, but in this case, it reads data from the peripheral's address using the same pointer.
First Part: Interfacing with the Adder Module
The first part of the main code interacts with the adder module implemented in the PL. All the transactions, at the first part is done with the pointers.
- We write values to the AXI GPIO connected to the adder.
- Then, we read back the result and display it in the terminal.
All the transactions, at the first part is done with the pointers.
Here, the AXI GPIO serves as an interface, allowing data to flow between the Processing System (PS) and the Programmable Logic (PL).
//------------------ First practice the arithmetic in PL side!----------------
volatile u32* GPIO_2_address = (u32*) XPAR_AXI_GPIO_2_BASEADDR;
volatile u32* GPIO_3_address = (u32*) XPAR_AXI_GPIO_3_BASEADDR;
volatile u32* GPIO_4_address = (u32*) XPAR_AXI_GPIO_4_BASEADDR;
u32 a, b, c;
for (a = 1, b = 2; a < 6; a = a + 2, b = b + 3) {
*GPIO_2_address = a;
*GPIO_3_address = b;
c = *GPIO_4_address;
xil_printf("Arithmetic Calculation in PL: %d + %d = %d \r\n", a, b, c);
xil_printf("**********************************\r\n");
}
Second Part: DIP Switch Control in Polled Mode
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.
- Bit1: 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.
Build and Run the Project:
Now that the code is ready, it’s time to build the project file run it on the ARM core.
- Ensure your board is connected to the PC, and the boot mode is set to JTAG.
- Use the Launch Hardware feature in Vitis to load the bitstream and application onto the board.
Once the program is running:
- First, you’ll see the results from the adder printed in the terminal.
- The program will pause and prompt you to press Enter.
- After pressing Enter, the LED control section begins.
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.
Enjoy experimenting with the controls and watching the system respond in real-time!
Comments