The Zynqberry is an FPGA development board offering up Xilinx's baseline Zynq-7000 series FPGA chip in a Raspberry Pi formfactor. Ever since the Zynqberry found its way into my FPGA dev board lineup, it's been begging for me to hack into my previous Raspberry Pi projects to migrate them over.
Looking into my past Raspberry Pi projects, many of them are centered around using Adafruit's variant of MicroPython, CircuitPython to work with shields such as the PiOLED, Radio Bonnet, and InkyPhat to name a few. After looking through the framework of these projects I narrowed down four main steps needed to transfer these projects over to the Zynqberry.
Step 1: Enable the Zynq's SPI and I2C interfaces and route via EMIO to the appropriate pins of the Zynqberry's 40-pin header (J8).
Step 2: Enable the I2C smbus and SPIdev kernel drivers in the PetaLinux project.
Step 3: Create a GPIO function class library Python package for the Zynqberry.
Step 4: Modify the core CircuitPython packages to add the Zynqberry framework.
Overall, I decided to create an embedded Linux image for the Zynqberry to essentially make it appear as a Raspberry Pi to the base CircuitPython library in the hopes that I can then freely install any other packages that use CircuitPython without having to modify them to work specifically with the Zynqberry.
CircuitPython's structure itself can be defined overall by two main Python3 packages: Adafruit Platform Detect and Adafruit Blinka. Adafruit Platform Detect is the first to run to provide a best-guess of the platform and it's built to work on a range of single-board computers. When implemented on a Linux system, the package works by querying various files such as /proc/cpuinfo and /proc/device-tree to map out the hardware of the board. Adafruit Blinka takes over after Platform Detect to define board/chip specific pin identities, analog and digital input/output pins using pin identities from board/microcontroller packages (RPI.GPIO in the case of the Raspberry Pi), and software/hardware driven interfaces for I2C, SPI, and UART. Based on the information provided by Platform Detect, the Blinka package then calls the board specific package to provide the necessary GPIO functions class (ie - RPI.GPIO).
As a proof of concept, I decided to start with my Adafruit PiOLED shield as it only requires an I2C interface. To do an initial prove-in of the SPI interface with the SPIdev driver, I created a custom PetaLinux application to implement the SPIdev loopback test. As a starting point for this project, I'll be using the Vivado and PetaLinux project from my last post that I like to refer to as Pi OS.
The PiOLED is a monochrome 128x32 OLED, with sharp white pixels utilizing I2C as its communication interface so it doesn't consume too much of the GPIO resources available on the 40-pin header of a Raspberry Pi (and Zynqberry).
Starting with the base Zynqberry Vivado design from my last project post, I went back to the block design in Vivado and enabled the I2C0 interface of the Zynq via the Zynq's EMIO:
I also enabled the two SPI interfaces routed through EMIO for future use since pretty much all Raspberry Pi shields use an I2C interface and/or a SPI interface. Since I'm dedicating these pins to the respective I2C and SPI interfaces, I narrowed the GPIO interface down from 24 bits to 17 bits.
I saw many recommendations to only make the needed pins for my SPI topology external in the block design, so I only brought out the output SCLK, output MOSI, input MISO, and output SS signals to route to the corresponding package pins. The Zynq TRM also recommended that the input SS signal be tied high (a constant IP set to 1).
I chose to connect the SPI pins to SPI1 to avoid conflicts in the kernel as I noticed the QSPI flash node in the Zynqberry's device tree uses SPI0 as its alias.
Generate a new bitstream and export the hardware platform again from the File > Export > Export Hardware... option in Vivado. Be sure to check the option to 'Include Bitstream' then import the new hardware into the existing PetaLinux project for Pi OS with the following command:
petalinux-config --get-hw-description ./<path to exported XSA file>
After importing the new hardware platform into the PetaLinux project, a few extra drivers need to be enabled in the Pi OS kernel. CircuitPython uses the SMBus protocol for I2C and the SPIdev driver for SPI interfaces.
The PiOLED boasts a SSD1306, a single-chip CMOS OLED/PLED driver with controller for organic/polymer light emitting diode dot-matrix graphic display system. There is a dedicated kernel driver for it that also needs to be enabled in the PetaLinux project:
You can find the pathways for desired drivers in the kernel configuration editor by typing '/' at any time to bring up the search bar. It will also tell you any dependencies that also need to be enabled in the kernel for the driver.
Since the PiOLED only has an I2C interface, I wanted something to test the SPI interface to verify the routing and base setup in the embedded Linux image. The SPIdev driver has a commonly implemented loopback test that can be found written in almost any language. In Xilinx's Github account, I found a version written in C that I copied the main code from into a custom application in the Pi OS PetaLinux project.
petalinux-create --type apps --template c --name spidev-test --enable
So that the embedded Linux image knows the SPI interface is compatible with the SPIdev driver, the following node needs to be added to the device tree:
&spi1 {
#address-cells=<1>;
#size-cells=<0>;
status = "okay";
num-cs = <1>;
is-decoded-cs = <0>;
spidev0: spidev@0 {
compatible = "spidev";
/* Max SPI clock frequency via Zynq EMIO is 25MHz */
spi-max-frequency = <25000000>;
/* slave number - CS */
reg = <0>;
/* Set SPI mode = 0 */
spi-cpol = <0>;
spi-cpha = <0>;
};
};
In my last post, I created a dedicated Adafruit recipe in the meta-user layer of the Pi OS PetaLinux project as a place to store the customized CircuitPython packages and test Python scripts. The bitbake recipe from the Adafruit recipe currently only copies the Python script files and tarballs for the Python packages to the home directory in the embedded Linux image, meaning they still need to be installed upon first boot. As this project evolves, I'll eventually figure out how to configure the bitbake recipe such that it installs the Python packages when I build the PetaLinux project.
While the CircuitPython packages only needed some modifications to add the Zynqberry framework, I had to create the equivalent RPI.GPIO Python package for the Zynqberry from scratch. Luckily, I found a very similar Python package already existed for the Pynq board in the Jupyter Notebooks workflow. I stole the ps.py and gpio.py scripts as a starting point for the Zynqberry.GPIO Python package. This provided me the baseline functionality of Linux's Sysfs API for the Zynq-7000 GPIO routed via EMIO in Python.
Since there are so many drivers and packages out there that work from Raspberry Pi's BCM numbering scheme, I decided to just hardcode the translation from the BCM numbering to Zynq EMIO into the Zynqberry.GPIO Python package. This in hopes that the package will be more portable to future projects with minimal extra hacking.
The Zynq-7000 chip has 118 GPIO pins available, 54 in the MIO and the remaining 64 in EMIO. GPIO[0] in the MIO starts at GPIO number 906 in Linux's Sysfs API, so GPIO[0] in the EMIO starts at GPIO number 960. Since I'm leaving BCM2 and BCM3 from the Raspberry Pi GPIO numbering scheme dedicated to the I2C interface, this means that the first GPIO pin in the Zynq's EMIO is BCM4, or Linux GPIO number 960. I mapped out the full EMIO numbering in the Linux Sysfs GPIO driver in the table below:
The actual hardcoding of BCM to Zynq GPIO numbering is done in the gpio.py script for the respective input and output functions in the GPIO class:
Following the guide of how to create a Python package from a project found here, I created the tarball for Zynqberry.GPIO and copied it into the Adafruit recipe in the Pi OS PetaLinux project. The font5x8.bin file is the font definition for writing text to the PiOLED that needs to be in the same directory as any scripts that want to write test to the PiOLED, while the other scripts are some that I have created for some other shields I'm currently proving in on the Zynqberry.
Moving on to the CircuitPython package modifications, I downloaded the full projects for Adafruit Platform Detect and Adafruit Blinka from https://pypi.org/ in order to modify the original source files then simply repackage them to get new tarballs to add to my Adafruit recipe in the PetaLinux project.
I found that the Adafruit Platform Detect Python package already has hooks to identify the Pynq board, so the structure to detect the Zynq-7000 chip from the /proc/cpuinfo is already there. I will admit, I got a little lazy here and just replaced all of the hooks for the Pynq board with those for the Zynqberry. (There were several places that needed modification, if you're interested you can find these modified source files in my project repository under the python_packages directory)
While the Platform Detect package had hooks to recognize the Pynq board, the Adafruit Blinka package did not. While it could recognize the Zynq-7000 chip, the 'generic_linux' GPIO class did not work with the base numbering scheme for the EMIO. So I added a new option under Blinka's microcontroller source directory for the Zynqberry's EMIO.
I then added the pointer to it for when Blinka detects a Zynq-7000 chip:
I did catch a break in that the SPI and I2C scripts from the generic_linux directory did work with Zynq, so I just added a copy of them to the zynqberry_emio directory:
Since the pinout for the 40-pin header of the Zynqberry matches that of the Raspberry Pi and I hardcoded the BCM to Zynq GPIO numbering into Zynqberry.GPIO, I was able to copy the Raspberry Pi's pin.py pinout script and simply change the imported package from RPI.GPIO to my custom Zynqberry.GPIO package:
Under the board directory in Adafruit Blinka, I also added a directory for the Zynqberry containing the same pin.py pinout script.
Back in the main source files directory of the Adafruit Blinka package, scripts such as busio.py, digitalio.py, bitbangio.py, board.py, etc. simply needed to be updated to see the new directories and source files added defining the Zynqberry.
Again, refer to the python_packages directory of the linked Github repository if you'd like to explore all of the modifications I made to the Adafruit Platform Detect and Adafruit Blinka packages.
I repacked my newly customized CircuitPython packages to create new tarballs to add to the Adafruit recipe in PetaLinux by running the following command from the same directory within the package that contained its setup.py script:
python3 setup.py sdist bdist_wheel
At this point, the PetaLinux project is ready to be rebuilt and the boot binary image repackaged using the new bitstream from Vivado:
petalinux-build
petalinux-package --boot --fsbl ./images/linux/zynq_fsbl.elf --fpga ./images/linux/system.bit --u-boot ./images/linux/u-boot.elf --force
Copy the new device tree blob, kernel image, and root filesystem to the appropriate partitions on the SD card and reflash the QSPI memory of the Zynqberry with the new BOOT.BIN.
Install the SD card back in the Zynqberry and power cycle the board.
After verifying a stable internet connection to the Zynqberry (I like to use ping google.com), use the Python3 package manager to install the customized Python packages from the local tarballs in the home folder. It's important that the custom packages be installed in the order listed below as they have dependencies on each other that the Python3 package manager (pip3) will attempt to resolve by installing the same packages from the PyPi repository that won't have the Zynqberry customizations.
pip3 install ./Adafruit-PlatformDetect-2.4.0
pip3 install ./Adafruit-Blinka-4.1.0
pip3 install ./Zynqberry.GPIO-0.0.1
The generic CircuitPython package for the PiOLED can now be installed from the network the same as would be done on a Raspberry Pi:
pip3 install adafruit-circuitpython-ssd1306
As a first hardware check before running the test script for the PiOLED, I wanted to verify that the Zynqberry was able to see the OLED on the I2C0 bus. Using the i2cdetect command:
sudo i2cdetect -y -r 0
Demonstrated the Zynqberry could see the PiOLED on the I2C0 bus at address 0x3C.
I then ran the test script I created for the PiOLED (SSD1306_Zynqberry_test.py) as a final step in proving the I2C bus on the Zynqberry worked with the Adafruit CircuitPython library and PiOLED shield.
I don't think I'll ever get to a point in my career where that first "Hello World" blink on some sort isn't extremely satisfying.
You'll notice the jumper wire on the header pins of the MOSI and MISO SPI signals. This is for the SPIdev loopback test application.
First, I queried the system to verify the SPIdev device was recognized from the device tree:
ls /dev/spi*
Then I ran the SPIdev loopback test on the SPIdev device with the verbose flag set to see that the bytes were actually being clocked out of the MOSI pin and read back in on the MISO pin.
/usr/bin/spidev-test -D /dev/spidev1.0 -v
At this point, Pi OS for the Zynqberry has working I2C and GPIO interfaces compatible with Adafruit's CircuitPython. Even though the SPIdev interface is working and the hardware layout is proven in, the next step for Pi OS will be to verify it works with CircuitPython. I'm very excited to get my Radio Bonnet running on the Zynqberry as it's one of my go-to Raspberry Pi shields.
Comments