SPI, I2C, UART on PYNQ: a PL approach

There are two methods for using devices on FPGA for PYNQ: the integrated microprocessor (PS side) and the programmable logic (PL side). In order to maximize the use of the FPGA, this article shows how you can use the programmable logic for common devices such as SPI, I2C and UART.  

We have used this flow also for SMART-IO Project. If you have lost the article about SMART-IO, follow this link!

You can find every design in this article as Github project in the MakarenaLabs Github company.

Design with Vivado for PYNQ

In order to create your programmable logic system, you need to create a Vivado design that includes the target device. Vivado has specific IP for the devices, called LogiCore IP:

  • for SPI you can choose AXI Quad SPI;
  • also for I2C you can choose AXI IIC Bus Interface;
  • then for UART you can choose AXI UART Lite.
You may notice that every device has an AXI bus connection as peripheral, so you can connect it to an axi peripheral block. When you use the Vivado block design system, Vivado gives you some usefull automations that helps you to connect every blocks to your central system (like Zynq7000 processing system or Zynq Ultrascale + processing system).

Here below we will show you some examples of Vivado block design composition with SPI, I2C and UART design.

SPI block design PYNQ
SPI block design PYNQ
I2C block design PYNQ
I2C block design PYNQ
UART block design PYNQ
UART block design PYNQ

You may notice that all the design are similar: there is a connection between processing system and ps7 axi peripheral through a master AXI GP, that connect the block to the device IP block. Only SPI has an additional IP block that is the clocking wizard, that helps you to set the right SPI clock (downsampling or upsampling the processing system clock).

Connection to the physical pin

After the block design composition, you need to connect the block pin interface to the physical interface of your board. In order to do that, you need two things: the constraint file, that allows Vivado to “route” the blocks pin to the FPGA pins, and the schematics of your board.

For example, we want to connect the UART interface to PMODB pins, that we are sure that there is a connection between them and FPGA pins. The procedure is quite easy: we need to choose specific pins of PMODB that represent all the pins of a common UART (i.e. Rx and Tx), then we will obtain the relative pin name of the FPGA.

 

The schematics

Let’s start using as example of the PYNQ Z2 schematic. You can find the schematics in this link:

https://dpoauwgwqsy2x.cloudfront.net/Download/TUL_PYNQ_Schematic_R12.pdf

So, let’s check the schematics:

PMODB Schematics

The pins 5 and 11 are Ground connection, while 6 and 12 are VCC 3.3V connection. Remember that you will need to connect the Ground pin to your external board, in order to share the ground for both PYNQ Z2 board and the other board. You also need to know that the VCC reference is 3.3V, so if the other device works with a different voltage, the communication will be unstable or impossible to understand!

The pins 1, 2, 3, 4, 7, 8, 9, 10 are free, so you can use it freely. So, take the pin 1 and 2 for Rx and Tx, you see that pins 1 and 2 have labels JB1_P and JB1_N, so these are the names of the link between PMODB and FPGA. Now, we need to find in the schematics where are JB1_P and JB1_N, in order to obtain the real name of the FPGA pins connected to these links.

Two hours later meme

Ok, fortunately you can use in the most of the schematics the CTRL+F command (find text), but always is a bit struggling!

So, let’s check the links for the JB1_P and JB1_N:

FPGA pins schematics

You see that there is a connection between JB1_P and W14 pin of the FPGA and between JB1_N and Y14 pin, so we did it! We will use them on Vivado in order to create the constraint file

The constraints file

In the constraint file of Vivado you need to specify what are the connections between the IP block and the choosen physical pin of PFGA.


set_property -dict {PACKAGE_PIN W14 IOSTANDARD LVCMOS33} [get_ports uart_rtl_rxd]
set_property -dict {PACKAGE_PIN Y14 IOSTANDARD LVCMOS33} [get_ports uart_rtl_txd]

As we decided before, we connect the package pin (“real” FPGA pin) W14 an Y14 to the design pin of Rx and Tx, identified by uart_rtl_rxd and uart_rtl_txd. These names are the standard name given by Vivado, but if you need different names you can simply double-click on the pin port and you can set the new name in the menu.

After that, you can launch the bitstream generation and wait. Then, you can export the hardware design in order to obtain the XSA file that contains bitstream and hardware description (.bitstream and .hwh files). PYNQ framework will use these files in order to download the design on the FPGA.

You can use the same procedure for all the devices which are shown before.

The MMIO objects



Ok, now we have the bitstream for the FPGA, but how we can use it directly on PYNQ framework?

Due the fact that the devices are AXI slave for the processing system, when you download the bitstream on the FPGA, PYNQ recognizes the devices as MMIO object.

The MMIO class allows the user to access addresses of system memory, and you can use read() and write() methods directly to the device. On PYNQ you can use the MMIO in two way:

  • explicit flavour, so you need to istantiate a new MMIO object assigning the base address and range addresses obtainable by Vivado address editor
  • implicit flavour, so you get the specific device after the overlay istantiation

In particular, the explicit and implicit flow are shown below (with a GPIO device example):


#EXPLICIT 
from pynq import MMIO

address = 0x41200000
XGPIO_DATA_OFFSET = 0x0

class GPIO:
    def __init__(self, address):
        # Setup axi core
        self.gpio = MMIO(address, 0x10000, debug=False)
        self.address = address

    def write(self, signal):
        # Send signal via GPIO using defined offset
        self.gpio.write(XGPIO_DATA_OFFSET, signal)
        
gpio = GPIO(address)
gpio.write(0)


#IMPLICIT
from pynq import Overlay
ol = Overlay("pynqz2_gpio.bit")
gpio = ol.axi_gpio_0
gpio.write(0)

There isn’t a “right way” to choose the explicit or implicit flavour, they are both valid.

So, using the MMIO you can write your own device driver for the specific application. This isn’t a pedantic article, so we have prepared all the code in our github repository 😉

You will find here SPI on PL with PYNQ code, also I2C on PL with PYNQ code, then UART on PL with PYNQ code and last but not least GPIO on PL with PYNQ code. Lots of stuffs for new adventurers 😉

https://github.com/MakarenaLabs/Common-PL-Devices-on-PYNQ