« take me back please!

Getting Started with the Linux Kernel and the Digilent Zybo/Xilinx Zynq

I’m a big fan of embedded systems. And I’m a big fan of FPGAs. Of course, I am also a big fan of the Linux kernel, so you can probably imagine my excitement when the Xilinx Zynq was announced in 2011. For those of you not in the know (although that is quite unlikely considering that you are here reading this), the Zynq is a dual core ARM processor with an FPGA on the same piece of silicon. Or alternatively, the Zynq is an FPGA with a dual core ARM processor right next to it. I guess it depends on what your final application is.

While great in principle, the Zynq had very little documentation available when it was initially released with the Zedboard. Despite having borrowed a pre-production board to play with, I quickly became frustrated and moved onto other projects.

Fast forward to today (March 2015) and there is plenty of documentation out there, as well as many development boards available. One of these boards with relatively low cost and wide availability is the Digilent Zybo.

Although I have only just started playing with it, it’s a great board for learning purposes. Given it was a bit tricky to get going, I thought that I would post some information about how to get it running, as well as some tips and tricks that I’ve picked up along the way.


Before we begin

In order to get the most out of your Zynq board, you need to install Xilinx Vivado. It’s quite a step up from the previous tool, ISE, it’s just a shame that even slightly older parts don’t work with it (The spartan 6 being a notable exception). Oh well. I run Vivado (and ISE) on an Ubuntu 14.04 Trusty Tahr LTS virtual machine in VMware Fusion. If you do this, make sure you have a few gigs of RAM available to the VM.

It would also be advantageous to get a non-free version so that you have access to more of the advanced functionality. You can get a copy fairly cheaply through Digilent (US$10 I think) when you purchase the Zybo, although this version will be locked to the device family that is on the Zybo (as of writing, this is a Zynq 7010 which is the lowest spec chip in the range). Be sure to install the Xilinx SDK as well, it’s a different install option in the Vivado installer.

The Zybo has no HDMI transmitter like the Zedboard. This guide will not involve setting up a graphical user interface.

I should also mention that almost all of my Linux knowledge and all of my FPGA knowledge is self-taught. So there might be some mistakes here and there. Sorry! :) I may have also glossed over some topics where I felt the detail was not relevent.

Some books and tutorials

For some of this, I will just be paraphrasing the work of others. There are two great tutorials I’ve found on the internet on building an embededded Linux image for the Zynq and a book on the Zynq in general:

One slight downside of the Zynq Book is that I found it a little light with respect to Linux integration. It provides a great overview of the Zynq though, and I highly recommend it.

Digilent also provides some tutorials, but they are a bit lacking and are mostly “do this, now do this” with little explanation. I should also mention that the Digilent source code on Github is quite significantly out of date as far as cutting edge embedded development is concerned, so I won’t be referencing any of it. But I’m not trying to slag Digilent here, they are a great company producing interesting products.

Xilinx has a wonderful wiki full of information about putting Linux on the Zynq. You should totally look at it if you get stuck.

Petalinux is a toolset from Xilinx designed to make installing Linux on a Zynq board super easy. I’m more interested in learning how to deploy embedded Linux, so using a vendor specific tool to do setups is not really appropriate. I have met the guy who started the microblaze port which turned into Petalinux and he was an awesome dude, so I’m sure it’s great.

If you just want Ubuntu running

One thing I noticed is that nobody provided any Linux images ready to go for the Zybo. This seems like a mega no-brainer to me, so I put one together using Ubuntu 14.04. I haven’t really tested it too much, but it appears to be working.

You can get the image here (dropbox link) or here (slower hosted link). It’s a ~150MB download which decompresses to 8GB, so you’ll need an 8GB microSD card. Installing it is simple using a unix based operating system (assuming your SD card is located at /dev/disk3):

$ gunzip ubuntu-zybo-14.04.img.gz
$ sudo dd if=ubuntu-zybo-14.04.img of=/dev/disk3 bs=512

If you are using Windows, you can use a tool like 7zip to decompress the image and Win32 Disk Imager to put the image on the card.

Some information about the image:


And now, the real reason you are here…

…is of course to build a linux image for the Zybo. All of these steps assume that you are running Ubuntu 14.04 LTS with Vivado 2014.4. It will probably work with later versions too (but these are the latest at the time of writing).

Step 0: An overview

Step 1: First Stage Boot Loader (FSBL) and FPGA bitfile generation

The first stage boot loader (FSBL) is the program which takes the device from running the boot ROM, sets the critical hardware up and passes execution to the “real program”. In our case, this will be a second-stage bootloader, which will then load and pass execution to the Linux Kernel.

Why do we need two bootloaders?

If you are familiar with microcontroller development, you should know that the memory of the devices is usually quite limited. Having more than a few hundred kilobytes of memory is unheard of (for now!); this is because it is made of SRAM which sits on the same silicon “chip” as the CPU. SRAM is used here because it is very high speed and very simple to access. Unfortunately though, SRAM is also very expensive and physically large so it doesn’t scale well when you need a lot. As of today, a 450MHz 144Mbit SRAM is US$331 from digikey for a single IC.

To increase the amount of memory available to a CPU, it is common to use much cheaper but more complicated DRAM. In fact, DRAM is so complicated that when most processors start up, they aren’t even able to access it as their special CPU to DRAM interface is not yet configured. On the Zynq, this is one of the many primary functions of the FSBL: configuring and enabling the DRAM controller so that the second stage bootloader can access all of that delicious memory.

Problems like this are usually specific to a single processor family, so if we want to have software run on multiple platforms, we need to have a way of abstracting them to look like the same thing to our code. So in general, the purpose of the FSBL is to configure and/or perform this abstraction.

If we are being 100% super techically correct, we don’t need two bootloaders. In fact, we don’t even need one (use any microcontroller and you’ll see why). But when these systems get more complicated, having bootloaders allows us to decouple the pieces so that we can fiddle with and test them individually, as well as separating the boot process into lower level platform specific actions (like setting up the DRAM controller) and higher level actions (like passing arguments to the kernel) which will be the same across one or many architectures.

Bitfile generation

No need to reinvent the wheel, you should just follow this tutorial.

Create the FSBL

Again, Greg has got us covered here on his blog.

The only difference I could find in my 2014.04 version was that instead of right clicking the block diagram and choosing to “Export hardware for SDK”, you instead go to “File->Export->Export Hardware…”. The default settings will be fine, so click ok, then click “File->Launch SDK”. Again, the default options are fine.

Step 2: Patching and compiling u-boot (the second stage boot loader)

Hooray for Greg! But we have to change a few things that he didn’t mention.

First, I’m going to assume like me that you’d like to be able to configure u-boot from the microSD before it starts trying to boot. If this is the case and my patch has not been accepted (yet, hopefully!), before compiling u-boot you need to edit include/configs/zynq-common.h like this:

Find the line:


Add this line above it:


Next, find this line:

        "env import -t ${loadbootenv_addr} $filesize\0" \

Add this line below it:

    "preboot=if env run loadbootenv; then env run importbootenv; fi\0" \

Then compile as outlined in the tutorial above.

Step 3: Building a device tree binary (dtb)

In the days of old, developers needed to do one of two things to get a new device to work with Linux. Either they needed to rewrite a good section of the kernel and then recompile it (like for adding USB support), or they needed to hardcode identifying device strings in the kernel.

For the latter to work, devices would have to have some way of identifying themselves, like having an ROM burned with the text “I AM A V123 SOUNDCARD” in a standard place on a standard interface. Then, when the kernel booted up, it simply read all the ROMs on all of the peripherals (it knew how to do this because standards like ISA/PCI/PCIe/USB define exactly how devices should behave so that they are discovered). If it found “I AM A V123 SOUNDCARD”, it would then know that it should treat the peripheral like a V123 soundcard (yes, I made that up) and load the corresponding driver.

Unfortunately, devices and their interconnects have since become a lot more complicated. In order to prevent everyone needing to compile custom kernels to change minor pieces of hardware, the device tree system was born. It is essentially file containing a markup language which describes which hardware is attached to where in a system, so that all devices don’t have to be able to identify themselves. Of course, we need this file so that the kernel knows what it has to work with on the Zybo, and it is a key part of connecting to the FPGA fabric in the kernel.

Making the tree

Unfortunately, Greg didn’t ever get around to putting up his device tree post. So here is my methodology. I make no guarantees that it is complete, but it works for me!

First, you need to create a device tree BSP in Xilinx SDK. First, you’re going to have to follow this excellent wiki page by xilinx. You should have a device tree BSP project at the end of it. Unfortunately, as of writing this, the device tree does not include any ethernet phy or physical layer transciever information, so we need to add that ourselves if we want to use networking.

Create a new file in your device tree project called ethernet_fix.dts. The name doesn’t really matter, I just chose that one. Inside this file, put the following:

/include/ "system.dts"

&gem0 {
    local-mac-address = [00 0a 35 00 00 00];
    phy-mode = "rgmii-id";
    status = "okay";
    phy-handle = <&phy0>;
    xlnx,eth-mode = <0x1>;
    xlnx,has-mdio = <0x1>;
    xlnx,ptp-enet-clock = <0x6750918>;
    ps7_ethernet_0_mdio: mdio {
        #address-cells = <1>;
        #size-cells = <0>;
        phy0: phy@1 {
            compatible = "realtek,RTL8211E";
            device_type = "ethernet-phy";
            reg = <1>;
        } ;

The first line of the file copies all of the hardware definitions from the system.dts tree which is automatically generated by the Xilinx SDK. Next, we create a definition for gem0, which is a Zynq-ish way of saying the first ethernet controller. This will override the configuration from system.dts. If you are curious, take a look at the gem0 section in there and look at what it is missing.

Now that we have a complete device tree structure, we need to compile the device tree into a binary device tree. This makes the file size smaller and easier for computers to understand. Using your terminal, first install the standard device tree compiler:

$ sudo apt-get install device-tree-compiler

And then compile your device tree (look in your Xilinx SDK workspace folder, for me it was located at /home/jeremy/xilinx_workspace/zynq_test/zynq_test.sdk/device_tree_bsp_0/ethernet_fix.dts) using the following command

$ dtc -I dts -O dtb /path/to/structure.dts -o /path/to/output/system.dtb 

system.dtb (note the extension .dtb) will be your compiled device tree.

Step 4: Configuring and compiling the Linux Kernel

Yet again, we pass over to Greg’s tutorial. This step is fairly straightforward and nothing extra needs to be configured.

Step 5a: Creating the image using a loopback device

We’re going to create our disk image using a loopback device, because writing to an SD card can be painfully slow. In this particular case, it isn’t an issue because the root filesystem is so small. But it’s a good thing to know about.

Step 5b: Configuring the Ubuntu 14.04 image

You can get the official ARM supported Ubuntu base image from here. Download it and extract it somewhere using

tar xvf ubuntu-trusty-14.04-armhf.com-20140603.tar.xz

Sorry, the following are just notes for me. I’ll write them up properly later.

create /etc/init/ttyPS0.conf to make a serial terminal at bootup:

start on stopped rc or RUNLEVEL=[12345]
stop on runlevel [!12345]

exec /sbin/getty -L 115200 ttyPS0 vt102

add the following line to /etc/securetty


edit /etc/fstab to look like this:

/dev/mmc.blk0p2 /   ext4    relatime,errors=remount-ro  0   1

You’re done!

Step 6b: Burning the image to an SD card

Step 7: Controlling some custom GPIOs via the FPGA and Linux

A simple python script to drive the GPIOs:


#!/usr/bin/env python3

gpio_root = '/sys/class/gpio'
gpio_name = 'gpiochip902'

############ actual code below here

import os
import time

def get_gpio_count(gpio_root, gpio_name):
    """Returns thet number of gpios available on the port"""
    with open(os.path.join(gpio_root, gpio_name, 'ngpio')) as f:
        return int(f.read().rstrip())

def export_gpio(gpio_root, gpio_name, gpio_number):
    """Instructs the kernel to load and configure the driver for the gpio pin"""
    # discover information about the GPIOs
    gpio_count = get_gpio_count(gpio_root, gpio_name)
    if gpio_number < 0 or gpio_number > gpio_count-1:
        raise RuntimeError("Specified GPIO is out of range")

    with open(os.path.join(gpio_root, gpio_name, 'base')) as f:
        gpio_base = int(f.read().rstrip())

    gpio_addr = gpio_base + gpio_number
    gpio_folder = "gpio" + str(gpio_addr)

    if not os.path.exists(os.path.join(gpio_root, gpio_folder)):
        with open(os.path.join(gpio_root, 'export'), 'w') as f:

    return gpio_folder, gpio_addr

def set_gpio(gpio_root, gpio_name, gpio_number, value):
    """Turns a gpio on or off"""
    gpio_folder, gpio_addr = export_gpio(gpio_root, gpio_name, gpio_number)

    with open(os.path.join(gpio_root, gpio_folder, 'direction'), 'w') as f:
    with open(os.path.join(gpio_root, gpio_folder, 'value'), 'w') as f:
        f.write(str(1 if (value != 0) else 0))

if __name__ == "__main__":
    count = get_gpio_count(gpio_root, gpio_name)

    # first we turn them all on
    for i in range(count):
        set_gpio(gpio_root, gpio_name, i, 1)

    # now we make a cool pattern!
    ticker = 0
    direction = 1
    while 1:
        for i in range(count):
            if ticker == i:
                set_gpio(gpio_root, gpio_name, i, 1)
                set_gpio(gpio_root, gpio_name, i, 0)

        ticker += direction
        if ticker == 0 or ticker == 3:
            direction *= -1



Yes, some parts might be empty or slim. It’s coming soon!