The goal of this blog series is to master the Xilinx Zynq.

I'm using the PWM design of my previous posts, and now switch to the raw AXI memory map interface between ARM and FPGA.

In the previous post, I used AXI GPIO, the first step to memory mapped interface between the Linux and FPGA parts. Now I'm using the pure memory map (MMIO).


There isn't a lot of difference between the previous blog and this one. In fact, the previous one was a little but easier to use.

But this one is our stepping stone for fast memory access. A raw interface between Linux managed memory and the FPGA fabric.

The gateway to use direct memory access (DMA) between the two worlds later ...


Software Changes


The FPGA design does not change. I'm using the exact same VHDL and Vivado bitstream. Latest source on github:

The interface on the software side changes.


The AXI GPIO design from the last post allowed to address slices of our 12 bit memory mapped register.

We could access and write the lowest 8 bits for duty cycle programming, and the upper 4 bits to set the dead time.

This came at the cost that it's a lower speed interface.


In this post, I'm using the raw memory. It's faster to start, and it will be orders of magnitude faster for designs that need to exchange a lot of data. And allows DMA.

If you build algorithm accelerators in FPGA (e.g.: real time image decoding, real time data encryption), this is what you need.


This is the Python code to work with the direct memory mapping. Review the previous post to see the differences.

As indicated earlier, the bitstream is exact the same as the previous one using AXI GPIO:


from pynq import Overlay


Get the memory mapped address of our interface between ARM and PWM FPGA block


pwm_address = ol.ip_dict['axi_gpio_pwm']['phys_addr']


Set the direction of the memory area from ARM to FPGA


image: the dictionary shows bit width and offset of the different registers we can (and have to) address to set values and direction


from pynq import MMIO
RANGE = 8 # Number of bytes; 8/4 = 2x 32-bit locations which is all we need for this example
pwm_register = MMIO(pwm_address, RANGE) 
# Write 0x00 to the tri-state register at offset 0x4 to configure the IO as outputs.
pwm_register.write(0x4, 0x0) # Write 0x0 to location 0x4; Set tri-state to output


Test it:


duty_i = 40
band_i = 7
pwm_register.write(0x0, (band_i << 8) + duty_i)


This is where you lose a little of the functionality of the lower speed AXI GPIO approach of the previous post.

You write the full register, instead of being able to address bits 0 - 7 and 8 - 11 separately. You gain speed.



Pynq - Zync - Vivado series
Add Pynq-Z2 board to Vivado
Learning Xilinx Zynq: port a Spartan 6 PWM example to Pynq
Learning Xilinx Zynq: use AXI with a VHDL example in Pynq
VHDL PWM generator with dead time: the design
Learning Xilinx Zynq: use AXI and MMIO with a VHDL example in Pynq
Learning Xilinx Zynq: port Rotary Decoder from Spartan 6 to Vivado and PYNQ
Learning Xilinx Zynq: FPGA based PWM generator with scroll wheel control
Learning Xilinx Zynq: use RAM design for Altera Cyclone on Vivado and PYNQ
Learning Xilinx Zynq: a Quadrature Oscillator - 2 implementations
Learning Xilinx Zynq: a Quadrature Oscillator - variable frequency
Learning Xilinx Zynq: Hardware Accelerated Software
Automate Repeatable Steps in Vivado
Learning Xilinx Zynq: Try to make my own Accelerated OpenCV Function - 1: Vitis HLS
Learning Xilinx Zynq: Try to make my own Accelerated OpenCV Function - 2: Vivado Block Design
Learning Xilinx Zynq: Logic Gates in Vivado
Learning Xilinx Zynq: Interrupt ARM from FPGA fabric
Learning Xilinx Zynq: reuse and combine components to build a multiplexer