Snickerdoodle Black + piSmasher Bare Metal Bring Up in Vitis

Getting the Snickerdoodle Black FPGA board with piSmasher baseboard up and running in Xilinx's new IDE, Vitis.

Starting with version 2019.2, Vivado's corresponding software development IDE switched from XSDK to Vitis. As I've been adjusting to the new design flow, I've found some great new perks as I've been converting all of the projects for my preferred FPGA development boards. I started off with the Ultra96 booting bare metal from flash memory, now the Snickerdoodle Black with it's handy piSmasher baseboard booting from an SD card has been my second Vitis workflow experience. I'm a Vitis fan so far (despite the fact autocorrect on all of my platforms is determined to replace 'Vitis' with 'Virus' every time I type it).

Step 1: Hardware Design in Vivado

The design workflow in Vivado hasn't changed at all in 2019.2. The first thing to do is install the board definition files for the Snickerdoodle if it hasn't already been done, and then create a new project targeting the Snickerdoodle Black.

A block design is the first thing to create in a new Vivado project, and since I'm using the piSmasher baseboard, I added a few extra things in addition to the Zynq processing system. One of the main components of the piSmasher is the USB hub that is routed to MIO pins 28 - 39 of the MIO, and the dual Ethernet ports routed through the programmable logic via the EMIO.

Again, I also added some simple block memory accessed via the AXI BRAM interface for future use to quickly and easily share data between the various Arm cores and any other state machines that might I have running in the programmable logic.

To save I/O pins, the piSmasher uses the Reduced Gigabit Media Interface which Xilinx provides an IP block to convert the full Gigabit Media Interface down from. The two GMII to RGMII IP blocks require some configuration. Both of the GMII to RGMII converters are fed from external reference clocks for the gmii_clk. They also need different PHY addresses, which can be any value between 1 - 31. The first GMII to RGMII IP block needs to have 'Include Shared Logic in Core' under the Shared Logic tab. The clkin clock of the first GMII to RGMII IP block also needs to be fed from a 200 MHz clock source so that that IP core can generate the necessary 125MHz, 100MHz, and 25 MHz clocks for the various Ethernet link speeds. The FCLK_CLK3 output from the Zynq PS after applying the Snickerdoodle board presets is set to 200 MHz to use for this purpose.

The second GMII to RGMII IP block needs to have 'Include Shared Logic in Example Design' under the Shared Logic tab. This allows for the first GMII to RGMII IP block to feed in a reference clock to the second to keep the two interfaces in sync.

Once the block design is complete, add a new constraints file for the Ethernet interfaces:

#-------------------------------------------------------------------------------
# Ethernet 0
#-------------------------------------------------------------------------------
# Clock
set_property PACKAGE_PIN L16 [get_ports ETH0_CLK125]; # JA2.35
set_property IOSTANDARD LVCMOS18 [get_ports ETH0_CLK125]
create_clock -add -period 8.000 -name eth0_clk125 [get_ports ETH0_CLK125]
# MDIO
set_property PACKAGE_PIN L17 [get_ports ETH0_MDIO_mdc]; # JA2.37
set_property PACKAGE_PIN J15 [get_ports ETH0_MDIO_mdio_io]; # JA2.4
set_property IOSTANDARD LVCMOS18 [get_ports [list ETH0_MDIO_mdc ETH0_MDIO_mdio_io]]
# RGMII
set_property PACKAGE_PIN V15 [get_ports {ETH0_RGMII_rd[0]}]; # JB1.32
set_property PACKAGE_PIN W15 [get_ports {ETH0_RGMII_rd[1]}]; # JB1.30
set_property PACKAGE_PIN Y16 [get_ports {ETH0_RGMII_rd[2]}]; # JB1.26
set_property PACKAGE_PIN Y17 [get_ports {ETH0_RGMII_rd[3]}]; # JB1.24
set_property PACKAGE_PIN U19 [get_ports ETH0_RGMII_rx_ctl]; # JB1.36
set_property PACKAGE_PIN U18 [get_ports ETH0_RGMII_rxc]; # JB1.38
set_property IOSTANDARD LVCMOS18 [get_ports [list {ETH0_RGMII_rd[*]} ETH0_RGMII_rx_ctl ETH0_RGMII_rxc]]
create_clock -add -period 8.000 -name eth0_rgmii_rxclk [get_ports ETH0_RGMII_rxc]
set_property PACKAGE_PIN V12 [get_ports {ETH0_RGMII_td[0]}]; # JB1.14
set_property PACKAGE_PIN W13 [get_ports {ETH0_RGMII_td[1]}]; # JB1.12
set_property PACKAGE_PIN T12 [get_ports {ETH0_RGMII_td[2]}]; # JB1.8
set_property PACKAGE_PIN U12 [get_ports {ETH0_RGMII_td[3]}]; # JB1.6
set_property PACKAGE_PIN T15 [get_ports ETH0_RGMII_tx_ctl]; # JB1.18
set_property PACKAGE_PIN U13 [get_ports ETH0_RGMII_txc]; # JB1.17
set_property IOSTANDARD LVCMOS18 [get_ports [list {ETH0_RGMII_td[*]} ETH0_RGMII_tx_ctl ETH0_RGMII_txc]]
set_property SLEW FAST [get_ports [list {ETH0_RGMII_td[*]} ETH0_RGMII_tx_ctl ETH0_RGMII_txc]]
#-------------------------------------------------------------------------------
# Ethernet 1
#-------------------------------------------------------------------------------
# Clock
set_property PACKAGE_PIN K17 [get_ports ETH1_CLK125]; # JA2.38
set_property IOSTANDARD LVCMOS18 [get_ports ETH1_CLK125]
create_clock -add -period 8.000 -name eth1_clk125 [get_ports ETH1_CLK125]
# MDIO
set_property PACKAGE_PIN J18 [get_ports ETH1_MDIO_mdc]; # JA1.35
set_property PACKAGE_PIN T19 [get_ports ETH1_MDIO_mdio_io]; # JB1.4
set_property IOSTANDARD LVCMOS18 [get_ports [list ETH1_MDIO_mdc ETH1_MDIO_mdio_io]]
# RGMII
set_property PACKAGE_PIN Y14 [get_ports {ETH1_RGMII_rd[0]}]; # JB1.31
set_property PACKAGE_PIN W14 [get_ports {ETH1_RGMII_rd[1]}]; # JB1.29
set_property PACKAGE_PIN U17 [get_ports {ETH1_RGMII_rd[2]}]; # JB1.25
set_property PACKAGE_PIN T16 [get_ports {ETH1_RGMII_rd[3]}]; # JB1.23
set_property PACKAGE_PIN U15 [get_ports ETH1_RGMII_rx_ctl]; # JB1.37
set_property PACKAGE_PIN U14 [get_ports ETH1_RGMII_rxc]; # JB1.35
set_property IOSTANDARD LVCMOS18 [get_ports [list {ETH1_RGMII_rd[*]} ETH1_RGMII_rx_ctl ETH1_RGMII_rxc]]
create_clock -add -period 8.000 -name eth1_rgmii_rxclk [get_ports ETH1_RGMII_rxc]
set_property PACKAGE_PIN R14 [get_ports {ETH1_RGMII_td[0]}]; # JB1.13
set_property PACKAGE_PIN P14 [get_ports {ETH1_RGMII_td[1]}]; # JB1.11
set_property PACKAGE_PIN T10 [get_ports {ETH1_RGMII_td[2]}]; # JB1.7
set_property PACKAGE_PIN T11 [get_ports {ETH1_RGMII_td[3]}]; # JB1.5
set_property PACKAGE_PIN T14 [get_ports ETH1_RGMII_tx_ctl]; # JB1.20
set_property PACKAGE_PIN V13 [get_ports ETH1_RGMII_txc]; # JB1.19
set_property IOSTANDARD LVCMOS18 [get_ports [list {ETH1_RGMII_td[*]} ETH1_RGMII_tx_ctl ETH1_RGMII_txc]]
set_property SLEW FAST [get_ports [list {ETH1_RGMII_td[*]} ETH1_RGMII_tx_ctl ETH1_RGMII_txc]]

Finally, generate an HDL wrapper that is auto-managed by Vivado then run synthesis, implementation, and generate a bitstream.

Step 2: Export XSA and Launch Vitis

Once the bitstream as been successfully generated, export it using the option under File > Export > Export Hardware... and check the option to include the bitstream. I always leave the export file path to the default location which is within the Vivado project file structure.

Before launching Vitis, I like to create a folder within the Vivado project file structure for my Vitis workspace.

To launch Vitis, go to Tools > Launch Vitis and select the workspace to launch from. In my case, I select the folder I just created in my Vivado project file structure.

Step 3: Create Platform Project

Unlike XSDK, Vitis does not automatically import the hardware platform when first launched. Instead, a platform project has to be created from the XSA that is generated when the hardware is exported from Vivado.

After naming the project, select the option to create from hardware specification (XSA).

Browse to the XSA file generated from Vivado and click 'Finish'.

Step 4: Create Application Project

Once the hardware platform is set up, you are free to create new application projects for any desired bare metal applications. The really awesome thing about Vitis, is that the bare metal application for Zynq's FSBL has already been generated with the platform project so you can immediately focus on creating your custom bare metal applications.

For now, we'll create a hello world bare metal application.

Select the custom platform just created in Vivado.

Select the Hello World template and click 'Finish'.

Finally, build the project.

Step 5: Prep and Load SD Card

I prep my SD card as recommend in UG1144 (for the purposes of future use with PetaLinux) by creating a 60MB fat32 partition for the boot and an ext4 partition for the of the space on the SD card root file system.

For Linux, I create mounting points in the /media/ directory to mount the SD card partitions to and then copy the necessary files over.

Since we're just implementing a bare metal application here, only the BOOT.BIN binary file needs to be copied to the boot (fat32) partition of the SD card.

sudo mkdir /media/BOOT/ 
sudo mount /dev/sdc1 /media/BOOT/

sudo cp <project directory>/<Vitis workspace>/<vitis project name>_system/Debug/sd_card/BOOT.BIN /media/BOOT/

sudo umount /media/BOOT/

Vitis automatically generates the boot binary file using the ELF files for the hello world application, FSBL, and the hardware bitstream. AND generates an instructional README.txt file.

Step 6: Boot Snickerdoodle

Insert the SD card into the Snickerdoodle, launch your preferred serial console application, and you'll see the Snickerdoodle greet you with a Hello World!

The Snickerdoodle Black + piSmasher baseboard is a platform best utilized by implementing an OS using PetaLinux or an embedded version of Ubuntu. Starting with this hardware platform will provide the platform to build everything off of for the Snickerdoodle going forward.

Whitney Knitter
Working as a full-time engineer, but making time for the fun projects at home.
Related articles
Sponsored articles
Related articles