RkBlog

Using GPIO, USB and HDMI on the PYNQ-Z2 board

2020-05-31

In this article I'll go over some additional PYNQ-Z2 features like GPIO, USB and HDMI functionalities. If you are new to PYNQ FPGA accelerated edge-computing platform then you can go check out my introduction tutorial.

USB-UART

USB port on the PYNQ-Z2 board is connected to the CPU (Processing System - PS) so we can use it like a regular USB host port. One of ways to use it is to connect a USB-Serial adapter or a microcontroller board if we need such features.

Although PYNQ-Z2 does have GPIO headers it may be beneficial to move more complex I/O functionality of this type to a dedicated microcontroller board - like one based on MicroPython, CircuitPython, Zerynth, TinkerForge or Arduino - especially when we are using more complex components that use dedicated libraries (and those are often provided to the most popular platforms only).

Serial, RS232 and alike are also an industry standard for connecting to and controlling various devices. It may be that your camera or other device providing video or audio for PYNQ board has such connector and if so we could use USB-serial adapter to connect to it and control it alongside our data processing code - turn it on via bit of Python code instead of manually pressing a button.

For this example I used a generic USB-UART adapter and a LCD display with a Hobbytronics serial controller:

Serial LCD display connected to PYNQ-Z2
Serial LCD display connected to PYNQ-Z2

Power your board, connect to it via SSH or via serial connection:

sudo screen /dev/ttyUSB1 115200

Install pySerial library:

sudo pip3 install pyserial

Now you can run showcased below code either as file via the console or in the Jupyter notebook. First off lets start with a handy class that will make it easier to operate on the display - this code is based on the API provided by the Hobbytronics controller:

import time

from datetime import datetime

import serial


class LCD:
    END = chr(0xFF)

    def __init__(self, device, rows, columns):
        self.commands = {
            'display_string': chr(1),
            'set_cursor_position': chr(2),
            'clear_line': chr(3),
            'clear': chr(4),
            'set_lcd_type': chr(5),
            'backlight': chr(7)
        }
        self.device = device
        self.rows = rows
        self.columns = columns
        self.connection = None

    def configure(self):
        self.connection = serial.Serial(
            self.device, 9600, timeout=5, bytesize=serial.EIGHTBITS, parity=serial.PARITY_NONE,
            stopbits=serial.STOPBITS_ONE, xonxoff=False)
        time.sleep(1)
        self.execute_command('set_lcd_type', chr(self.rows) + chr(self.columns))
        self.clear()

    def clear(self):
        self.execute_command('clear', '')

    def clear_line(self, line):
        self.execute_command('clear_line', chr(line))

    def set_backlight(self, brightness):
        self.execute_command('backlight', chr(brightness))

    def set_cursor_position(self, row, column):
        self.execute_command('set_cursor_position', chr(row) + chr(column))

    def display_string(self, string):
        self.execute_command('display_string', string)

    def close(self):
        self.connection.close()

    def execute_command(self, command, payload):
        full_command = self.commands[command] + payload + self.END
        full_command = full_command.encode('iso-8859-1')
        self.connection.write(full_command)

And example usage - a clock/date display:

lcd = LCD("/dev/ttyUSB0", rows=2, columns=16)  # change to your serial device ID if needed
lcd.configure()
lcd.set_backlight(25)

while True:
    day = datetime.now().strftime('%d, %b %Y')
    current_time = datetime.now().strftime('%H:%M:%S')

    lcd.set_cursor_position(1, 1)
    lcd.display_string(day)
    lcd.set_cursor_position(2, 1)
    lcd.display_string(current_time)
    time.sleep(1)

This code will run on PYNQ board or on your PC, laptop, whatever that can run pyserial library and has a USB port.

For fun you could use such LCD to display PYNQ resource usage or system logs.

GPIO - running a stepper motor

So now lets try to use PYNQ-Z2 GPIO that is provided on the board. The layout of the headers may look like Arduino Uno or Raspberry Pi but functionality may change based on overlay (FPGA configuration) used.

For digital pin functionality we can use arduino_io.Arduino_IO module. Digital pin can be either high (1) or low (0). As stated in the documentation the A0 and subsequent pins are digital pins number 14 and up.

For this example I used a low power Chinese 28BYJ-48 stepper motor with ULN2003 controller. This kit is easy to get, although you can use a LED + resistor to test this GPIO as well.

To drive this stepper motor you need 4 GPIO that will turn high in specific order over and over again.

On the PYNQ-Z2 image you can find few example notebooks in base/arduino folder. The first thing to do wherever you are using Arduino_IO or other Arduino module is to configure given pins for that purpose:

import time

from pynq.overlays.base import BaseOverlay
from pynq.lib.arduino import arduino_io

base = BaseOverlay('base.bit')

p1 = arduino_io.Arduino_IO(base.iop_arduino.mb_info, 14, 'out')
p2 = arduino_io.Arduino_IO(base.iop_arduino.mb_info, 15, 'out')
p3 = arduino_io.Arduino_IO(base.iop_arduino.mb_info, 16, 'out')
p4 = arduino_io.Arduino_IO(base.iop_arduino.mb_info, 17, 'out')

Now we can play with pins high/low state to drive the motor:

def set_pins(pins, value):
    for pin in pins:
        pin.write(value)


steps = [
    [p1],
    [p1, p2],
    [p2],
    [p2, p3],
    [p3],
    [p3, p4],
    [p4],
    [p1, p4]
]

pins = [p1, p2, p3, p4]
current_step = 0


for i in range(0, 4000):
    high_pins = steps[current_step]
    set_pins(pins, 0)
    set_pins(high_pins, 1)
    current_step += 1
    if current_step == len(steps):
        current_step = 0
    time.sleep(0.01)
Stepper motor running from PYNQ-Z2

You can check examples in the Jupyter notebook as well as bootcamp notebooks for more examples.

Stepper motor working with PYNQ-Z2 GPIO
Stepper motor working with PYNQ-Z2 GPIO
Low power stepper motor can be controlled and powered via PYNQ-Z2 board
Low power stepper motor can be controlled and powered via PYNQ-Z2 board

GPIO - emulating button presses on an external device

If you are running a audio/video project with PYNQ and the camera doesn't have a serial port then you can still control it via digital GPIO. What can make the magic happen is a optocoupler or a small SSR - solid state relay… and the camera has to have physical buttons used to control or power it on/off.

SSR or an optocoupler will close a circuit when current is put to the control side. Both circuits are separated as the control circuit actually turns a small LED on while the load side reacts to light and closes the circuit. Thanks to this both control and load sides can operate on different voltages.

The camera or other device you want to control will likely have physical buttons and you can solder wires to both sides of a button and connect it to the load side of a SSR/optocoupler. The line will not be closed by default - button not pressed. To close it and emulate a button press the control side pin has to be set to high similarly how it was done in the stepper motor example - set it high for a second or two and the set it low - that should give you a button press effect without actually pressing the button.

Buttons on the PCB
Camera buttons with wires soldered
Solid state relay LAA110
LAA110 solid state relay with two relays in one package

HDMI - working with cameras

The HDMI ports on the PYNQ-Z2 board are connected to the FPGA part (PL - programmable logic) and can be used for HDMI stream processing. You will need a camera with HDMI out and a HDMI display to see the end result. For PYNQ-Z2 a 720p 30FPS camera is advised although my 1080p 60FPS camera also managed to work, although processing was done with half the frame rate.

The default image for PYNQ-Z2 has a set of notebooks showcasing HDMI processing capabilities. First of all there is getting_started/5_base_overlay_video.ipynb notebook that showcases HDMI handling and basic processing.

Reading a frame from a HDMI input in the base_overlay_video notebook
Reading a frame from a HDMI input in the base_overlay_video notebook

Next you can check base/video notebooks that showcase USB webcam and HDMI camera processing features. Also element14 webinars showcase how to create a custom overlay (FPGA configuration) in Vivado with specific accelerated processing pipeline - but that's getting close to FPGA programming.

Microscope camera with HDMI out using C-mount lens
Microscope camera with HDMI out using C-mount lens
HDMI in for camera, HDMI out for a HDMI display
HDMI in for camera, HDMI out for a HDMI display
PYNQ video processing setup
PYNQ video capture and processing setup
Block diagram of a custom HDMI processing overlay for PYNQ created in Vivado
Block diagram of a custom HDMI processing overlay for PYNQ created in Vivado

HDMI - working with PC HDMI input

If you don't have a camera or want specific input image (like a list of faces for a face detection algorithm) you can use a HDMI output from a PC/laptop. Just connect it to the input on the PYNQ-Z2 board and connect PYNQ output to the HDMI display. To be able to use the PC you while feeding PYNQ with the video stream you will have to configure it to mirror image on two displays (laptop screen + PYNQ or PC with 2 displays, one directly connected to it and the other via PYNQ). With a mirror setup the resolution may be set to like 720p due to PYNQ board not handling high resolutions.

Dual display setup with PYNQ-Z2
Dual display setup with PYNQ-Z2
Laplacian gradient filter
Laplacian gradient filter
Gray-scale applied live to the HDMI stream
Gray-scale applied live to the HDMI stream

You can either open specific image or a video in a browser or locally and then execute the code in the Jupyter notebook that will do some actions on the HDMI video stream.

Comment article