The Path II Programmable training covered a great deal of Zynq MPSoC Hardware, Software and Petalinux content - covering almost everything required to build a Zynq MPSoC system: right from the logic running to the PL, to Petalinux and software applications that run on the PS. My plan was to work on a project that covered all of this, but this turned out to be more complicated that I originally thought it would be. To add to this, I've been very busy at work, so there have been a couple of weeks that have passed by without me doing anything related to PIIP.
While I won't be changing my project too much, I'm going to take the PYNQ route in hopes of finishing my project a little faster!
Setting up PYNQ on the Ultra96v2
I downloaded the PYNQ v2.4 pre-built image from https://github.com/Avnet/Ultra96-PYNQ/releases and flashed it to a SD card using Etcher. Connect a micro-USB cable to the 'USB 3 Device' header - the Ultra96 will enumerate as a COM port and Ethernet adapter. Navigate to 192.168.3.1 using a browser, and after logging in, you should see a Jupyter notebook:
The PYNQ image includes a couple of examples to demonstrate how to:
- download an overlay to the PL, and also measure the time taken to download it.
- get/set the fabric clocks generated by the PS
- connect to a WiFi network
- use a USB webcam
Creating PYNQ overlays
My plan for PYNQ is to use Python to talk to the custom logic that I've implemented in the PL. A lot of examples I've seen use pre-built overlays, but the first step for me is to learn how to import and use an overlay that has been generated by Vivado.
The tutorial uses Vivado HLS to create an IP that adds 2 numbers - HLS generates the IP, and all you need to do is point Vivado to the IP, and add it to your block design.
After the bitstream has been generated, export the block diagram's tcl file, and bitstream. Copy both of these to the ultra96 and rename them so that both have the same filenames.
In PYNQ, import the overlay (which downloads it), import the IP created by Vivado HLS, write to 2 registers (which are the inputs), and read from the third, which will contain the output. To create a more formal 'driver', import the DefaultIP class, and add the 'add' function as a driver API.
Now that I was done with this example, I decided to add a couple of more things to the design. My Verilog & VHDL is a little rusty, so I grabbed the sources for the IP core that I used in Path to Programmable Blog 14 - Project: A MiniZed WS2812 Controller. However, instead of adding it as an IP, I added the source files to the current design, and imported it as a module. The top file follows the naming convention that Vivado uses, allowing the AXI port 'snapping' to work as usual, and the biggest advantage of using it as a module instead of an IP is that it's a lot easier to modify the source code.
I made a couple of changes to expose the BRAM ports from my original design (since it was internal to the IP), and added a dual port BRAM & AXI BRAM controller - allowing the BRAM to be accessed by my IP, as well as Python code running in the PYNQ environment. The 'mmio' class allows access to the system memory space, so I added some code that wrote to the BRAM and read it back to test things out.
Now that I've figured this out, it's time to move on to implementing the second half of the project - some custom RTL that allows me to capture I/O pin states to BRAM (like a logic analyzer), as well as drive the IO pins according to the data in the BRAM (i.e. a pattern generator).