Hello World on the Ultra96 Using Vitis

Get up and running with one of the most powerful hobbyist FPGA boards on the market.

The Ultra96 is a unique offering in the FPGA hobbyist arena as it is the only sub-$500 development platform for the Zynq UltraScale+ MPSoC. Packing in an Arm A53 quad-core 64-bit processor, and an Arm R5 dual-core 32-bit processor in with a GPU and high speed peripherals such as PCIe, USB 3.0, SATA 3.1, display port, and Gigabit Ethernet. This is all of course encompassed by the programmable logic of the FPGA. The Ultra96 adds on top of this 2 GBs of DDR4 RAM, a Microchip WiFi+Bluetooth module, mini-display port, USB 3.0 ports, a high speed GPIO expansion header for CSI and DSI interfaces, and a low speed GPIO expansion header for standard peripherals.

The Ultra96 board is a great platform for a wide array of applications such as machine learning, IoT, video processing, endless robotics projects, and so on. It's such a little powerhouse of a board that it really means the sky is the limit for hobbyists.

I'm very excited to have finally gotten my hand on this awesome little board, and given Xilinx's recent announcement of their new IDE development platform (that I had the honor of attending this past October), I decided a proper bring-up tutorial was in order.

Part One: Hardware Design in Vivado

Since Vivado 2019.2 and Vitis are so new, I had decided to build the hardware design from scratch. First thing is to download and install the board files from Avnet's GitHub here. Then create a new project targeting the Ultra96 board (if you have purchased the board recently, then you have the second version, V2, of the board).

Once the project loads, select the 'Create Block Design' option under the IP Integrator. In the block design, the bare minimum that you must add is the Zynq UltraScale+ MPSoC IP block. Vivado will automatically offer 'Designer Assistance' after the MPSoC block is added. Running this will apply all of the relevant board settings for the Ultra96 from the board files previously installed.

Pro tip: if the Zynq MPSoC IP block doesn't show up in the catalog, go to the 'Settings' tab under Project Manager and verify that the Ultra96 is set as the Project Device. The Zynq IP blocks only show up in the IP catalog when the appropriate Zynq part(s) have been set as the Project Device. I'm not sure if it's a glitch in Vivado 2019.2 or what, but after I selected the Ultra96 board in the initial project creation steps I had to re-select it again after I created the block design.

I also added an AXI GPIO block for the low speed GPIO expansion header. The Ultra96 board has a mini CPU fan controlled by GPIO that I added a separate, single bit wide, AXI GPIO block for.

The extra BRAM via AXI is a personal design preference of mine. Throwing extra block RAM into the fabric like this is a way to quickly and easily share data between the various Arm cores and any other state machines that might be running in the programmable logic. This isn't necessary just to get the Ultra96 up and running initially, so it can be added later once you know you have a use for it.

Now while the MPSoC already has two UART controllers built into it that the Ultra96 utilizes via the MIO, they only contain the TX and RX lines. The Microchip ATWILC3000 WiFi+Bluetooth module utilizes a UART interface that also requires the flow control RTS and CTS lines. This is so the Microchip module knows when the Zynq processing system is ready to accept data and can stop the data flow if an error occurs. Adding a separate AXI UART 16550 and simply making the RTS and CTS lines external to assign to the Microchip ATWILC3000 WiFi+Bluetooth module via the constraints takes care of it!

Once you have everything added to the block design you want, select the Regenerate Layout option and then validate the design. Once the design is validated with no errors or critical warnings, navigate to the Sources tab and right click on the block design in the design sources hierarchy. Select 'Create HDL Wrapper...' and the option to let Vivado manage it.

While the majority of the Ultra96's peripherals are routed through the MIO, the GPIO for the low speed header and the fan, as well as the Microchip UART flow control lines are routed through the programmable logic so they need a constraints file to specify the pins they are mapped to. Select the 'Add Sources' option in the Project Manager and create a new constraints file. You can find my constraints file in my GitHub repo for the project here.

Going back to the GPIO for the fan control, the default drive strength of 12 is too high to the bank the pin is routed to so it has to be set lower. Which is why you'll see the 'set_property DRIVE 8' line in my constraints file.

Once the constraints file is added, run synthesis, implementation, and generate a bitstream. A bitstream is the final product of the hardware design in Vivado, and what you need to build the software on top of in Vitis and PetaLinux.

Correct all errors and/or critical warnings if any popped up during synthesis or implementation, then export the hardware local to the project by selecting File > Export > Export Hardware...

Be sure to check the box to include the bitstream as well.

At this point, the hardware design is complete and it's time to transition to the Vitis SDK to develop the boot software and bare metal applications for the design. Launch Vitis by going to Tools > Launch Vitis and selecting your desired workspace. I personally like to create a folder within my Viviado project file structure titled 'workspace' to place my Vitis workspace into so I can keep everything for a design all in one place.

Part Two: Software Design in Vitis

With your brand new Vitis workspace, the first thing to do is create a new Platform project. Unlike it's predecessor SDK, Vitis does not automatically import the hardware platform when launched locally to the Vivado project. Instead, a platform project has to be manually created and the desired XSA hardware specification package must be selected. There are preset XSA packages for the major Xilinx development boards such as the ZC706, ZCU102, etc. which you can select to build your platform project on, but we want to use the custom XSA we just created in Vivado.

After selecting the option to create a new Platform Project, select the option to 'Create from hardware specification (XSA)', and browse to the location of where you exported hardware to from Vivado.

The great thing about Vitis, is that it reads your XSA hardware specification and automatically generates all of the boot applications and files you need based upon what your targeted processor is. In this project, Vitis reads from the XSA that the targeted processor is a Zynq MPSoC, so it then generates the ZynqMP FSBL and PMU firmware applications automatically.

Once the hardware platform is created, you are free to create/add application projects on top of it. Select File > New > Application Project and you'll see a variety of templates for bare metal applications and others you can choose from. Select the custom XSA platform we just created as the hardware platform and the Hello World template then click 'Finish'.

By default, the BSPs always connect the standard input/output console to ps_uart_0, but the Ultra96 has it's serial console routed to ps_uart_1. You'll need to modify the BSP for the standalone application on the Cortex A53, ZynqMP FSBL, and the PMU firmware to connect the standard input/output console to ps_uart_1 in order to see the printed output.

Next I recommend modifying the Helloworld main function to put the "Hello World" print out in an infinite while loop to make sure you don't miss the print out while trying to set up a serial console application to view it:

Save any modified files and build the project, then create a debug configuration to launch and run the application on the Ultra96. I always use the System Project Debug option as I've historically found it to work the best for me.

The JTAG pins are located on the bottom of the Ultra96 board to the right of the power plug and under the low speed GPIO expansion header and the UART lines are the four pins located on the top of the board immediately to the left of the low speed GPIO expansion header. While you can connect to these pins with flying leads and use your own USB to RS232 and Xilinx JTAG programmer, I strongly recommend investing in Avent's USB-to-JTAG/UART Pod.

Connect to the UART and JTAG on the Ultra96 and configure the boot mode switches for booting from JTAG by turning both switches to the ON position.

Once connected, apply power to the Ultra96 board by plugging in the 12V supply and pressing the power button (SW4).

Launch the debug configuration you created and wait for Vitis to switch to the Debug Perspective.

Once in the Debug Perspective, navigate to the Vitis Serial Terminal and select the green '+' button. Select the serial port the Ultra96 has attached to on your computer, set the baud rate to 115200, and open the port. You'll then see the "Hello World" printing out from the Arm A53 processor on the Ultra96.

As with all my blog posts, you can find the corresponding project repository on GitHub.

Whitney Knitter
All thoughts/opinions are my own and do not reflect those of any company/entity I currently/previously associate with.
Latest articles
Sponsored articles
Related articles
Latest articles
Read more
Related articles