SILICA Xilinx Zynq ZedBoard Vivado Workshop Ver1.0
SILICA Xilinx Zynq ZedBoard Vivado Workshop Ver1.0
SILICA Xilinx Zynq ZedBoard Vivado Workshop Ver1.0
Rich Griffin
Xilinx Embedded Specialist, Silica EMEA
Page 1
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
Introduction
Welcome to the Zynq beginners workshop. The purpose of this document is to give
you a hands-on introduction to the Zynq-7000 SoC devices, and also to the Xilinx
Vivado Design Suite. Throughout the course of this guide you will learn about the
Zynq SoC solution step-by-step, and gain the knowledge and experience you need to
create your own designs.
However you want it to! But the following notes might help:
Please feel free to experiment beyond the guidance of the exercises – it is the
fastest way to learn. This is not going to be a regimented "follow the steps in the
instructions" style of material. The aim is to learn by doing, getting it wrong, and
then fixing it.
Please work on your own if you can, you'll learn more by doing than by watching
someone else use the tools. If you do work with someone else, please try to get
equal hands-on time. You won’t learn much by watching.
Everyone will be work at their own speed when following this guide. Depending
on previous knowledge and experience, some people will progress quickly and
others will take longer to master the concepts. The idea is to progress through
the exercises, learning how to use the Xilinx embedded processor tools simply by
"getting your hands dirty". Each exercise will introduce new concepts of
embedded processor design and will enable you to learn new skills. The
exercises will become more challenging as the workshop progresses, and will
gradually offer less step-by-step guidance.
If you’re working though this material at an organised event, there are more
exercises listed here than most people will be able to cover during one day. This
has been done quite intentionally so that you will have something more to
explore in your own time if you so choose. If you are working through this
material alone, then you can obviously set your own schedule and pace.
Have fun!
Prerequisites
These exercises have been designed to guide you through the usage of the Xilinx
tools at every step of the way, and teach the basic principles of the code that we’ll
be writing in C and VHDL. That said, it would be advantageous if you have a basic
understanding of both languages, just so that you don’t feel like you’re living in a
world of peculiar-looking hieroglyphics.
Page 2
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
Agenda
Exercise 1 - Getting something (anything!) working.
"I just want to see the board work. How do I do it?"
Use of the Block Diagram tool.
"print" is your friend. We will learn how to use it for debugging.
Exercise 3 - Debugging.
Why is this important?
How does it help us?
What options do we have for debugging?
Use the debugger to step through your Flashing LED design.
Page 3
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
Preparation
The starter kit board has its own in-built JTAG configuration cable, which simply
requires that the user connect a USB lead between their laptop and the board. A
power supply for the board is also provided. We shall be using the RS232 UART in
our experiments, which is connected to a USB socket on the board via a Cypress
CY7C64225 USB-UART bridge. You will therefore need two USB to MicroUSB cables,
and the power supply provided with the board.
The Xilinx design tools are designed to cater for both hardware and software
engineers. The Xilinx FPGA and Zynq SoC devices are extremely flexible and so
there is a lot of functionality in the toolset, which is spread across different
applications.
Vivado – The top level design environment for the hardware designer. Use this tool
to create the contents of your Programmable Logic, and to create the embedded
processor section of the design. We will use Vivado to configure our settings for the
Zynq “Processing System” section of the design.
SDK – The Software Development Kit. A tool for software engineers, allowing the
user to develop C code, generate BSPs, and test their code using the debugger.
This workshop uses the ZedBoard and the cables which are supplied in the box. For
Exercise 9, you will also need a “MAX31723PMB1” temperature sensor from Maxim.
Page 4
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
This exercise has a triple purpose. Firstly, it will check that Xilinx tools have been
correctly installed. The second and main part of the exercise will be to build a very
basic processor system using the Xilinx Vivado tools, to help create a design which
will then be synthesised, implemented, and downloaded to the demonstration board
via a programming cable. Lastly, the exercise will enable the user to test the
ZedBoard to check that it is correctly powered and configured. If successful, the
user will see messages displayed on the terminal connected to the UART socket on
the board.
As with all development boards, there are supplied reference designs which are
provided for the user to see the board in action out of
the box. However, the purpose of this session is for you
to do it, so we’ll create our own design rather than use
the supplied reference designs.
Page 5
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
2. Connect the second USB lead to the “PROG” socket next to the power connector
on the board. Connect the other end of the USB lead to a spare USB port on
your PC.
6. Depending on whether your PC has previously been used with this board, you
may see an automated installation wizard run on your PC for the USB UART
drivers. If this happens, permit the wizard to look for files on the internet and
the cable should be installed automatically. (N.B. The wizard may re-run several
times before the cable is correctly installed. This is normal so please persevere)
7.3. Choose “VHDL” as your target language from the drop down list. If you have
a particular preference for Verilog, you can choose it here but the guidance
Page 6
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
later on in this workshop will need to be modified using your own skills. Click
“Next” several times until you see the “Default Part” screen.
7.4. Click the “Boards” option in the “Specify” area. Choose “Zynq-7000” from the
“Family” filter drop-down. Choose “All” from the “Board Rev” drop down box.
7.5. Check to see which version of the ZedBoard you have by examining the
revision markings next to the bar code sticker on the board.
7.6. Choose the correct version of the “ZedBoard Zynq Evaluation and
Development Kit” option from the list of known boards. Click “Next”, and
then click “Finish”.
8. You will now see an empty Vivado project. Maximise the Vivado window if it is
not already filling the screen, so that you can properly explore this tool.
8.1. On the left of the screen you will see the Flow Navigator pane. This is where
you control the flow of the design and where you will click to run Synthesis,
Implementation, and generate a configuration bitstream.
8.2. In the middle of the screen near the top, you will see the “Sources” pane.
This is where you will see design sources that you will add to your project.
Currently there are no Design Sources in this project so the list will be empty.
You will also see the design constraints lists here, which is also empty. Don’t
be confused by the “constrs_1” sub folder; this represents that you have a
default “Constraint Set” present in the project, but there are no constraints
files in it yet.
8.3. On the right of the screen you will see the Project Summary pane. This is
where you will see statistics about your project; how big it is, how many
resources you are using in your Zynq device, and a summary from the
various report files. There is little information in this pane at the moment,
because we have not yet done anything.
8.4. At the bottom of the screen you will see a series of tabs which show reports,
errors, warnings, messages, a log of recent events, and the progress of any
open designs in the “Design Runs” tab. There is also a TCL console in this
area which allows power users to enter advanced custom commands.
9. Xilinx Zynq devices offer users the ability to combine custom logic blocks with
embedded processor systems. We are going to add a source file to the project
which represents the Embedded Processor section of our design in block diagram
form.
9.1. On the left of the screen in the Flow Navigator pane, click the “Create Block
Design” icon.
9.2. Leave the name of the block diagram at the default setting, and click “OK”.
Page 7
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
9.3. After a few seconds, the pane on the right of the screen will open into the
Block Design mode (shown in the blue bar at the top of the pane), and an
empty block diagram will be shown. Note also the green “designer
assistance” bar at the top of the pane, which is prompting you to start your
design by adding some IP to the block diagram.
10. You will now configure the hardened Processing System section of the Zynq
device.
10.1. In the green designer assistance bar at the top of the page,
click the “Add IP”. If the designer assistance bar is not visible,
you can open the “Add IP” window using its icon on the left of the
block diagram pane.
10.2. Choose the “ZYNQ7 Processing System” entry from the list,
either by scrolling down to locate it, or by typing the first few characters into
the search box at the top of the box. Double-click the IP in the list to add it
to the block diagram, or drag it to the block diagram with your mouse.
10.3. Note the change to the green designer assistance bar at the top of the
block diagram, which is now offering the “Run Block Automation” feature.
Vivado has detected that there is an un-configured block on the diagram and
is offering the user the chance to connect some required pins on the symbol.
Click the “Run Block Automation” link in the designer assistance bar, followed
by the instance name that appears for the symbol you have added to the
diagram. Leave the “Apply Board Preset” box ticked, which will apply the
required settings for the ZedBoard, and click OK.
10.4. Review how the DDR & Fixed IO connections are made automatically.
The Vivado tools are “board aware” and have also now made a number of
changes to the internal configuration of the “Zynq7 Processing System” block.
You will notice that a number of additional connections have appeared on the
block including clocks, resets, timer outputs, and bus interfaces which allow
the connection of additional AXI IP blocks (soft peripherals in the
Programmable Logic fabric).
Page 8
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
10.5. Double click the “Zynq7 Processing System” block to open the
customisation window. This is where changes can be made to the Processing
System section of a Zynq device, and there are a large number of settings
available. We are going to keep our design very simple at first, so we will
disable the AXI bus interfaces that connect to the Programmable Logic.
10.6. The diagram of the Zynq device shown on this window is interactive;
anything in bright green can be clicked to adjust the settings. Click the “32b
GP AXI Master Ports” block at the bottom of the diagram. Remove the tick
mark from the box labelled “M AXI GP0 interface” to disable the bus port,
then click “OK”. The customisation window will close and you will return to
the Block Diagram editor. Note that the AXI ports on the “Zynq7 Processing
System” tile disappear, leaving just the clock, reset, and timer outputs.
(Note: We could also have disabled these outputs in the customisation
settings, but in our design we will simply leave them disconnected)
10.7. This completes the settings for the embedded processing
section of the design. Click the “Validate Design” icon to the left
of the block diagram to check for any errors. You should see a
“Validation successful” message.
10.8. Save the block design by choose “File Save Block Design”
from the menus.
11. Click the “Sources” tab in the “Design Hierarchy” pane in the middle of the
screen. You will now see your block design source in the “Design Sources” folder.
Click the name of the embedded source that you have created (with its yellow
icon and .bd file suffix) to select it. This module contains all of the settings that
you configured in the block diagram editor, and you can return to it any time by
double-clicking this source.
12. We now need to create a top level VHDL / Verilog wrapper file to go around this
block design. Normally this top level source file would be custom created by the
designer, but as a short-cut we can make the Vivado tools do this for us.
12.1. Right-click the block design source file and choose “Create HDL
Wrapper” from the resulting menu. Choose to “Let Vivado manage wrapper
and auto-update”, and click OK. The tools will automatically create a VHDL
or Verilog file which will have the same name as your block design source,
but with the suffix “_wrapper” appended to the name. If you wish, you can
double click to open and review this top level file in the editing pane on the
right of the screen. This wrapper will serve at the top level HDL file for our
design.
13. We will now synthesise, implement, and create a downloadable bitstream for this
design.
13.1. On the left of the screen in the “Flow Navigator” pane you will see
menu items for “Run Synthesis”, “Run Implementation” and “Generate
Bitstream”. We could run all of these steps in turn, but the Vivado tool is
capable of managing the dependences automatically.
Page 9
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
14. That’s it, you’ve now built the hardware and you have a Bitstream which can be
used to configure the Programmable Logic (PL) section of the device. In this case
the bitstream is just a wrapper, because all of the functionality in this first simple
design is contained within the hardened Processing System (PS) section of the
device. You will see a diagram of the Zynq device appear on the right of the
screen. The boxes represent different design regions of the Programmable Logic,
and the bright orange section in the top left is the hardened Processing System
which contains the memory controllers and ARM processor cores.
14.1. The settings you made using the block design tool were of huge
importance, and all of this information has been included in our design.
Almost all of these settings were applied automatically by the tool because
we chose the “ZedBoard” when we created the project. Vivado knows how
to correctly configure the DDR controller, clocks, IO pins, and many other
settings, so a huge amount of design time has been saved here. For custom
boards the user would need to manually configure these settings to match
their board.
15. We now need to start writing some software to run on one of the ARM Cortex A9
processors, and to do that we must use the Software Development Kit (SDK).
Page 10
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
16.1. Choose “File Export Export Hardware for SDK...” from the menus.
16.2. Tick the “Launch SDK” box from the pop up, leave the other settings as
the defaults (all ticked), and click OK.
After a few seconds you will see a new design tool appear. The Software
Development Kit (SDK) is based upon the “Eclipse” tools which are fast becoming
an industry standard for software development environments across the world. If
you are new to Eclipse, please take a few moments to familiarise yourself with
the layout of the SDK.
If the SDK does not appear, you may have forgotten to open the implemented
design, or you may have closed the block design before performing the export
operation. The block design must be open, and the implemented design must be
open, in order for the export operation to succeed.
Page 11
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
16.3. On the left of the screen is the Project Explorer pane. This is where
you will see all of your software projects, Board Support Packages, and
Hardware Platforms listed, including all of the files which represent the
project. At this stage you will see only a hardware platform, which is
depicted by the yellow folder icon containing a microchip. This hardware
platform has been automatically imported from the Vivado tool and describes
the hardware settings which are applicable for the ZedBoard that we’re using
today.
16.4. In the middle of the screen we can see the code and file editing pane.
This is where you will make changes to source files, and edit your C code. At
this stage you should see a .XML file open, and this represents the hardware
for the ZedBoard that has been imported. Feel free to explore this file; you
will see that it contains all of the hardware base address and high address
settings for the various peripherals on the Zynq device.
16.5. On the right of the screen is the Outline pane. This will provide an
overview of your software code later on, but is blank for the moment.
16.6. At the bottom of the screen you will see a series of tabs that operate in
much the same way as they did in the Vivado tool. The tabs show you a
console which will provide messages, warnings, and status updates. There is
a Tasks tab which shows you what the SDK is currently doing (perhaps in the
background), and a Terminal which allows you to interact with serial ports on
the board.
17. We will start our software development by creating a Board Support Package
(BSP). BSPs contain drivers, libraries, and essentially anything else which will
allow your software applications to access features on the hardware. In many
cases it is the user’s responsibility to create a BSP from scratch, or import one
that someone else has created. The Xilinx tools differ massively from this way of
working, and are capable of creating a BSP for you automatically. This is a huge
benefit and will save a lot of time during your software development.
17.1. From the menus choose “File New Board Support Package”.
Accept all of the defaults and click “Finish”.
17.2. A BSP settings window will appear, giving you the option to adjust what
will be contained within the BSP. We are going to keep it simple, so please
just click “OK”.
17.3. In the Console tab at the bottom of the screen you will see the compiler
and a special utility called “Library Generator” working to create your BSP.
You will ultimately see the message “Build Finished”, which will indicate that
the BSP is complete. That’s it! You have just created a fully featured BSP
which would normally require weeks of engineering time.
17.4. In the Project Explorer pane on the left of the screen you will see that
your “standalone_bsp_0” BSP has appeared in the list of sources. Expand
this project by clicking on the arrow to the left of the name and continue to
expand the yellow folders until you see the “include” folder. Open this folder
and you will see a list of header (.h) files which represent all of the drivers
that you will need to develop your software.
Page 12
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
17.5. As an example of the available drivers, open the “xuartps.h” header file
by double-clicking it, and explore the contents of the file. You will find a
selection of functions which allow you to
configure and control the UART. Near the
bottom of the file you will see functions that
Send and Receive bytes to/from the UART. At
this stage also review the “Outline” tab on the
right of the screen. For large complex source
files, the Outline tab can help you to navigate
through the source file with ease. Try clicking a
few of the items in the outline tab, and note
how the view in the source code window jumps
to that section of the code.
17.6. You will find that all of the function
prototypes in the header file are documented
and provided in C source code (in the associated
“libsrc” directory of the BSP). This is a very
useful resource for software developers, and will
save huge amounts of time when writing your
code.
17.7. Close the header file, and collapse the BSP
folder to tidy up the Project Explorer pane.
19. Switch on the ZedBoard, if it is not already. Check that you can see the green
“Power” LED on the left of the board.
20. We will now configure the built-in UART Terminal to listen to UART traffic coming
from the board.
Page 13
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
23. That’s it! Although there were a lot of steps to explain in this first exercise, you
have taken a Zynq device and configured it to run a simple software application,
starting completely from scratch. Congratulations!
Further experiments
The "print" C code function can be used to display text to the standard output device
(which in our case is the UART). The syntax for "print" is as follows:
Page 14
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
The “\n\r” is a special tag which tells the STDOUT device to send a line feed (\n) to
the UART, followed by a carriage return (\r). Therefore to send a blank line to the
UART we would use the function call:
print("\n\r");
When you have made your code modifications, the application can be run on the
board using the “Right-click Run as Launch on Hardware (GDB)” method, the
same as you did before.
Page 15
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
We have now verified that the board and the relevant connections are correct. In
this exercise we will explore the use of software drivers to control an output pin on
the Zynq device to drive an LED. As we saw at the end of Exercise 1, the Vivado
tools have the ability to maintain several software applications designed for use on
the same processor hardware, so we must first create a new software application in
SDK, by choosing File New Application Project from the menus. Give the
application the name of “Exercise_02”, and select the existing “standalone_bsp_0”
board support package in the same way that you did in the previous exercise. On
the second screen, choose the “Hello World” template again. We could create an
Empty Application, but it’s actually easier to create a “Hello World” template and
then modify it, because the template also includes some basic setup for the UART.
Expand the new “Exercise_02” application in the Project Explorer pane, and then also
expand the “src” folder. Right-click on the “hello_world.c” source file and choose
“Rename” from the menu. Give the source file a new name, such as “exercise_02.c”.
Note that the SDK will guide you to choose and appropriate filename extension for
this type of source file.
Open the “exercise_02.c” source file by double-clicking it. You will see that there are
some comments at the top of the source file, and the basic code required to print a
hello world message to the UART. Note also the “init_platform()” function; this is the
basic setup for the UART that is useful to us. Modify the message printed to the
UART by adjusting the print statement to a printf statement, and remove the
declaration for the print function.
#include <stdio.h>
#include "platform.h"
int main()
{
init_platform();
printf("Hello World\n\r");
return 0;
}
This printf statement will provide us with a simple sanity check when we run the
application later on, and reassure us that the
application is executing correctly on the ARM
processor core.
Page 16
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
LD0 to LD7, but these are all connected to the PL (Programmable Logic) side of the
Zynq device, and we have not yet configured that part of the device to do anything.
Instead we shall use “LD9” for our exercise, and this is connected to pin 7
(PS_MIO7) of the 54 MIO pins which are accessible on the “PS” (Processing System)
side of the Zynq device.
We will now find the appropriate driver to use from the BSP that we created in
Exercise 1. Expand the BSP in the Project Explorer, and navigate down to the
contents of the “include” directory. All Xilinx drivers begin with the letter “X” and
then have a name which reflects their functionality. We’re looking for the GPIO
driver for pins connected to the PS, so we need to look for the “xgpiops.h” header
file (X GPIO PS).
Open the “xgpiops.h” header file and review the code. You will probably become
very rapidly overwhelmed by the quantity and complexity of the code. This is where
the “Outline” pane becomes useful on the right of the screen. All #define
statements (Macro definitions) in the code are listed with a “#” icon. All function
prototypes are shown with circular icons with a
green hatch pattern. All structs are shown with a
circular blue “S” icon. All type definitions are shown
with a yellow circular “T” icon. The Outline view is
also interactive. Try clicking to select various items
in the Outline pane, and you will see that the view
in the editor window jumps to that section of the
code.
#include “xgpiops.h”
#include “xparameters.h”
The second #include line (xparameters.h) will give you access to many of the
predefined “#define” macros which are provided in the Xilinx BSP. Take a moment
to view the contents of this file, and to do this we will introduce another feature of
the SDK. Although we have added the #include line for xparameters.h, we might
not have any idea where the file is located in the tree of sources. In fact it doesn’t
matter, because the SDK will find it for us. Hold down the “Ctrl” key on your
keyboard and hover your mouse over the “#include” line in your source. Notice how
the line of C has become a link. Click the link and the xparameters.h source file will
be opened in the editor.
Scrolling through the file you’ll find that there are lines which define a “Device ID” to
each of the peripherals in the system. The one that interests us for this exercise is
“XPAR_PS7_GPIO_0_DEVICE_ID” which you will find roughly halfway down the file.
If you find it difficult to locate, you can use the “Outline” view to make this easier. If
Page 17
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
it is still too hard to find, then try sorting the Outline view
alphabetically by toggling the A-Z sort feature header bar.
Xilinx drivers all use the same principles of operation and they require
that the driver be initialised before use. To avoid confusion, we will
explore the concepts of driver initialisation before we write any code.
All Xilinx drivers have a “struct” (structure) which holds all of the various setup
values which will be needed by the peripheral. A “struct” is merely a collection of
variables / data types, wrapped and bundled together in such a way that allows us to
move many variables around using just one name.
To declare an instance of this struct in C, we could use the following line of code.
Fruit My_Fruit_Collection;
Worse still, imagine that you wanted to expand the software to count the fruit in two
different shops; you’d need to double the number of lines of code just to declare the
variables. Whereas with a struct, you’d simply go from one line of code to two:
Fruit My_Fruit_Collection_SHOP_A;
Fruit My_Fruit_Collection_SHOP_B;
To access the individual variables inside the struct, you simply use the “->” notation
in C. Here is an example showing how you’d assign two different variables inside
two different structs:
My_Fruit_Collection_SHOP_A->Lemon_Count = 5;
My_Fruit_Collection_SHOP_B->Banana_Count = 3;
Page 18
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
Fruit
*My_Fruit_Collection_SHOP_B
Banana_Count = 3;
Apple_Count = ????;
Lemon_Count = ????;
XGpioPs my_Gpio
typedef struct {
XGpioPs_Config GpioConfig;
u32 IsReady;
XGpioPs_Handler Handler;
void *CallBackRef;
} XGpioPs;
Xilinx drivers work in the same way. The struct represents the various operating
parameters, and in the case of complex peripherals there may be a very large
number of variables inside the struct. Fortunately in the case of our GPIO it is
relatively simple and there are only a few variables. Here is the struct which controls
the GPIO in the Processing System of a Zynq device. In this example we have
declared an instance of the struct and called it “my_Gpio”.
There are four variables inside the struct. The first is called “GpioConfig” and is of
data type “XGpioPs_Config”. This data type is actually another struct (yes, it’s
possible to have a struct inside a struct!), and we will use it to configure our
“my_Gpio” instance.
The remaining three variables are all of different data types. It’s not necessary to
understand what everything does, but it is not difficult to believe that it would be a
tricky task manually configure everything inside the struct. For this reason, Xilinx
supplies a function in C which does the job for us, and it is called
“XGpioPs_CfgInitialize”. The function automatically configures everything for us,
because all of the variables inside are uninitialised when the struct is declared. The
function requires three inputs; the instance of “my_Gpio” that we declared, the
GPIO_Config struct, and a base address (which can be easily extracted from the
GPIO_Config struct). The output of the function is a status value which lets us know
whether the initialisation was successful. Here is a graphical description of how it
works; we can see the three required items being passed into the
XGpioPs_CfgInitialize function, and the initialised struct coming out (plus a status
value).
Page 19
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
XGpioPs_Config
XGpioPs my_Gpio GPIO_Config
XGpioPs_CfgInitialize
Status
XGpioPs my_Gpio
GpioConfig = ;
IsReady = ;
Handler = ;
*CallBackRef = ;
But there’s a problem! The “GPIO_Config” struct hasn’t been initialised yet, either.
So our first step is to use another automated function called “XGpioPs_LookupConfig”
to achieve this task. In essence, the output of one function is therefore simply fed
into the other.
Again, let’s represent this in a diagram for clarity. The important thing to note is that
the only piece of information that we’ve had to enter manually is the “Device ID”,
and even that comes from the xparameters.h file:
Page 20
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
XGpioPs_Config
GPIO_Config
DeviceID = ????;
BaseAddr = ????;
Device
ID
XGpioPs_LookupConfig
XGpioPs_Config
XGpioPs my_Gpio GPIO_Config
GpioConfig = ????;
DeviceID = ;
IsReady = ????; BaseAddr = ;
Handler = ????;
*CallBackRef = ????;
Base
Address
XGpioPs_CfgInitialize
Status
XGpioPs my_Gpio
GpioConfig = ;
IsReady = ;
Handler = ;
*CallBackRef = ;
Page 21
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
Let’s start putting this into C code. The first task is to declare two structs; a
temporary configuration struct called “XGpioPs_Config”, and the second struct is the
instance that will be used to control the GPIO, called “XGpioPs”. You will see these
listed in the “Outline” view of SDK when you select the “xgpiops.h” in your editor
(yellow circular icon with a “T” logo).
Return to your “execise_2.c” source file and start adding some code above the
“init_platform()” line. At this stage we will also introduce an incredibly powerful
feature of the code editor called “Content Assist”. This feature will save you huge
amounts of typing once you learn how to use it. Start typing the first few characters
of “XGpioPs_Config” and then hold down the “Ctrl” key on your keyboard and press
the space-bar. A pop-up screen will appear giving you a list of possibilities, which
has the same appearance as the Outline pane. Scroll down the list of possibilities (or
continue typing the next letters to filter the list) and choose the “XGpioPs_Config”
item from the list using either the mouse or the arrow keys on the keyboard and
pressing enter. Note that the coloured icons are shown here too, to make it easier
for you. Finish off the line manually by adding a name for this instance, and making
the code match the following line. Notice the use of the “*” because this is a
pointer, and of course the semicolon at the end of the line.
XGpioPs_Config *GPIO_Config;
Immediately below the line you’ve just typed, declare an instance of “XGpioPs” and
also an integer called “Status” that we will use for some error checking later on.
XGpioPs my_Gpio;
int Status;
To initialize the GPIO driver, we need to provide it with three pieces of information
when we use the function call in our code. The instance name of the driver which in
our case is “my_Gpio”, the configuration settings for the GPIO (we have created a
placeholder for this in the form of “GPIO_Config” but not done anything with it yet),
and the base address for the GPIO peripheral in question. The second and third
parameters can be easily provided using a function call which you may have seen
when you reviewed the Outline pane for the “xgpiops.h” file. As we saw in the
diagrams, the function is called “XGpioPs_LookupConfig”, and just needs to have one
parameter passed to it (the “DEVICE ID”) which we will find in the xparameters.h file
(automatically generated for the design by Xilinx).
Below the “printf()” line, add the following to your code to look up the settings for
the GPIO configuration and store them in your “GPIO_Config” placeholder.
Experiment using the Content Assist feature by pressing Ctrl+<Space> to help you
write the code without lots of typing:
GPIO_Config = XGpioPs_LookupConfig(XPAR_PS7_GPIO_0_DEVICE_ID);
“GPIO_Config” is our instance name for the “XGpioPs_Config” struct data type that
we declared earlier. As we saw previously, structs are essentially a series of smaller
data types (variables, usually) which are grouped together under one name to make
Page 22
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
life easier for the user. In this particular case the “XGPioPs_Config” struct contains
two variables; a 16-bit device ID called “DeviceId”, and a 32-bit base address called
“BaseAddr”. SDK will allow you to find out the definition of any function or data type
that you choose by using the “Open Declaration” feature from the Right-Click menu
(this is the same as holding down “Ctrl” and clicking on the object). Add the
following line to initialize the driver, remembering once again to use Content Assist to
ease the strain your fingers:
You can now begin to see how structs work. We are recovering the base address for
the GPIO using the “GPIO_Config->BaseAddr” notation. Essentially this is like using
an ordinary variable name, but we’re telling the compiler to look inside the
“GPIO_Config” struct for a variable called “BaseAddr” by using the “->” syntax.
You’ll notice that the initialisation function returns a value which we are capturing in
our variable called “Status”. We could add some code here to check that the
initialisation was successful (Success = a returned value of “0”), or alternatively we
could check the value in the debugger at runtime if we experience problems. To
keep our code as simple as possible we’ll choose the latter option, and we’ll trust that
the function call works as it should.
That’s it! The GPIO driver is now initialised, and we can now use it to control the
LED on our board. If we want to perform operations on the GPIO in our code, we
can now do so by using the “my_Gpio” instance that we have just created and
initialized.
When we configured the Zynq device using the Platform Studio tool in Exercise 1, the
imported settings in the XML file for the ZedBoard included a GPIO peripheral. The
GPIO is perhaps the simplest of all peripherals, in that it allows us to control a series
of wires from software. Once again, review the driver header file for the GPIO and
you will see that there are functions which help you to set the pins as inputs or
outputs, and also functions which allow you to read and write from those pins.
(Hint: Use the “Ctrl + <click>” operation on #include line for the GPIO’s header file)
We’re only interested in outputting a value to either illuminate or extinguish the LED,
so we only need two functions:
These functions allow us to send values to the various registers within the GPIO
peripheral. The first line will set the direction of the GPIO to be an output, but only
for a specific pin. In our case this will be pin 7 which is where the LED is connected.
The second line allows us to write our chosen value to the pin; either “0” or “1”.
Add the two lines of code to your application, filling in the blanks with the correct
information. The first parameter should be a reference to your instance of the
“my_Gpio” struct, so add a “&” character before the name of the struct to tell the
Page 23
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
compiler that you are using a reference (References are a more advanced concept of
the C language... don’t worry about it, just trust me!). The pin number is easy; 7.
The data direction is unknown to us at the moment so we need to find this
information. Go back to the “xgpiops.h” header file and click on the
“XGpioPs_SetDirectionPin” function in the Outline pane. The editor will jump to the
location of this function in the file. Now move your mouse to the editor window, and
hover the mouse pointer over the highlighted function name. A helper window will
pop up, showing you key information about the function. In this information you will
see that a value of “0” makes the GPIO and input, and the value of “1” makes the
GPIO an output. Excellent; we now have enough information to complete the code
in our application. So let’s add the code:
XGpioPs_SetDirectionPin(&my_Gpio, 7, 1);
XGpioPs_WritePin(&my_Gpio, 7, 1);
That’s it. We have changed the direction of pin 7 to an output (output = 1) using
the first line, and the second line writes the value of “1” to the pin to switch on the
LED. Save the C source file, and the application will be compiled automatically.
Check for any errors in the C code which will be highlighted and marked with a red
cross.
Run the code on the board using the “Run as...” feature that you explored in
Exercise 1. You should see the LED “LD9” illuminate. You’ll find it to the right of the
OLED display near the bottom of the board.
Change the code to send a “0” to the GPIO pin to switch the LED off, and then run
the code on the board again using “Run as...”. Be sure to change only the
“XGpioPs_WritePin” line of code and not the “XGpioPs_SetDirectionPin” line. We only
want to change the value we’re sending to the UART, not change the direction of the
port!
Now that you have established basic control over an LED, let’s make it flash.
Let’s create a loop in software using a “while” loop. Edit your code to match the
following:
XGpioPs_SetDirectionPin(&my_Gpio, 7, 1);
while(1)
{
XGpioPs_WritePin(&my_Gpio, 7, 0);
XGpioPs_WritePin(&my_Gpio, 7, 1);
}
This code makes an endless loop which repeatedly turns the LED on and off. Run
the code on your board and see what happens.
Page 24
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
Really? Are you sure? The LED might be on, but is it as bright as it used to
be?
If you’re correct, then you will have realised that the LED is being switched on and
off extremely quickly. So quickly, in fact, that it has the appearance of being dimmer
than before. This is the way that LEDs are dimmed using digital control; and it’s
called Pulse Width Modulation. The brightness of an LED can be varied by changing
the percentage of the time the LED is on versus off. Provided that the flash rate is
fast enough, the human eye can’t tell the difference.
For our purposes it would be interesting to have the software insert a delay between
the “on” command and the “off” so that we can drive the LED on and off at a speed
that we can realistically observe. Here is an example of a delay function:
As you can see, this is a simple loop which instructs the processor to print a full-stop
to the UART 2000 times. Naturally, we are not at all interested in generating 2000
full-stops on the terminal, but doing so will take the processor a finite amount of
time to complete the task, hence the delay. For any expert software engineers
reading this; yes we know that this delay is phenomenally crude, but this is basic
stuff for the moment. It gets more advanced later!
The iterations are counted by using a variable called “Status”. We used “Status”
simply because we’d already declared it for a previous use and so it was convenient.
In real designs, you’d probably declare another variable specifically to be used in the
loop. We are just being lazy here, to save some typing.
Page 25
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
Exercise 3 - Debugging
In this exercise we shall look at the concepts behind debugging a piece of C code.
This is important for all software development, because we are all human and
therefore software rarely works first time. The ARM Cortex-A9 processors in the
Zynq devices are supplied with a very advanced debug and trace interface, but we
will simply cover some basic debugging techniques.
We shall now create a project to show the steps required for debugging. The finished
version of Exercise 2 is an ideal design for this exercise. Create a new software
application called “Exercise_03” using standard cut and paste operations, based on
Exercise 2. Once you’ve copied the files, experiment with changing the filename
from “exercise_02.c” to “exercise_03.c” to match your new project (and you might
want to change the printf message inside the file too!). The rename feature can be
found in the right-click menu.
To debug the code on the processor we shall be using another tool called "GDB"
(The GNU Debugger). GDB is built into the SDK, and can be accessed by Right-
Clicking on the project name and choosing “Debug as” from the menu rather than
“Run as”.
Right click on the “Exercise_03” software project, and choose “Debug as Launch
on Hardware (System Debugger)” from the menu. After a few seconds you may see
a pop-up message asking you whether you want to switch to the “debug
perspective”. Choose “Yes”.
The windows will all re-arrange themselves, and you will now be in the “Debug
perspective” rather than the “C/C++ Perspective”. A “Perspective” in the Eclipse
SDK tool is the name given to a layout of the various panes on the screen. The
layout of the screen in a SDK tool is critical to making the software engineer more
Page 26
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
With the debug perspective open, you will notice that the code editing window has
become smaller and has moved to the left of the screen. The Project Explorer
window has vanished altogether; we no longer need to concentrate on different
applications because we’re debugging just one. In the code editing window, you will
see that there is a line of code that is highlighted in green, and also marked with a
blue arrow in the left margin. This is
showing you that the processor has been
halted at this stage of the application, and
that the software execution is under control
of the GDB debugger. The highlighted line
of code is GDB telling you “I am about to
execute this line of code, but I’ve not done it
yet”. The pane above the code editing
window shows you that the Exercise_03.elf
file is being debugged, and that debug
control is being handled by something called
“TCF”. TCF is the Target Communication
Framework system, and is the utility which
manages the connection between the GDB
debug engine, and the JTAG cable. You will be able to see that the code is halted
inside the “main” function. This display will update to show when you’re inside a
function, and the hierarchy will be shown to demonstrate which functions called each
other as you step through your code.
Hover your mouse over the yellow single-step arrow buttons and locate the one
marked “Step Into”. Click this button (perhaps a few times, depending on your code
that you copied from Exercise 2) and you will “Step Into” the init_platform() function.
Observe what happens in the “Debug” pane when you descend into the function.
You will see the “_start()” function underneath the “main” function, which is also
underneath the “init_platform” function. This will tell you that “_start” has called
“main”, which in turn has called “init_platform”, and therefore tells you which
functions you passed through on the way to your current point in the code. In our
Page 27
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
In the code editing pane, scroll down a few lines and find the line of code which sets
the direction of the GPIO port. In the blue margin to the left of this line, double click
your mouse and you will see a blue dot with a tick mark appear. This is a
breakpoint, and tells GDB that you wish to always stop at this point. Now click the
green arrow “resume” button, and the debugger will execute all of your code
between the current position and the breakpoint that you have just set. This is a
useful way to execute a lot of your application, but only stop at the section you wish
to debug.
At this stage we will turn our attention to the Variables pane which you will see in
the top right of the screen. You will see all of the local variables which are relative to
this function. It’s a simple application, so you should only see three or four listed.
One of the variables is “Status” and hopefully its value will be “0”. This proves that
the “XGpioPs_CfgInitialize” function completed and returned no errors. This is good
news. In the “Variables” pane you should also see the “GPIO_Config” placeholder
which you created in Exercise 2. Recall that this is actually a struct, which is a
collection of variables under one name.
Expand the hierarchy of the
“GPIO_Config” struct by clicking on the
white arrow to the left of the name. You
will now see the two variables contained
within the struct, and their values. The
“GPIO_Config -> BaseAddr” variable
probably seems to be a meaningless number, but that’s because we’re looking at the
decimal value rather than the hexadecimal value. A second column exists in the
Variables table which shows the hexadecimal equivalent of the number. Now the
number should make more sense. This is the base address of the GPIO in the Zynq
device memory map.
You can also expand the “my_Gpio” instance in the Variables pane. You will see that
it’s actually a struct containing another struct, and therefore there will be multiple
Page 28
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
levels of hierarchy that you can explore. You may see that some of the variables are
highlighted in yellow. This occurs when the variable has been updated since the last
breakpoint or during the last single-step operation. This feature of the debugger can
be very useful because it draws your attention to things that change during your
debugging session.
But the Variables pane is not the only place where you can explore the value of
variables. In the code editing window, hover your mouse over the name of a
variable or a struct. A pop up helper will be displayed that will give you the same
information. Simple, but useful.
Add another breakpoint inside your “for” delay loop which prints the “.” characters to
the UART. Click the “Resume” button and the code will advance to inside this loop.
Click “Resume” a few more times, and observe the change to the variable which
counts the iterations of this loop. You will see the variable incrementing with each
click of “Resume”. Single-stepping our way out of this loop could be a very tedious
process, and would require 2000 mouse clicks. But what if there was a fault in the
code just before the last iteration of the loop? The debugger can help us in these
cases, because we can dynamically change the values of variables in the middle of
executing the code.
Inside the “Variables” pane, click the value for your loop variable and manually
change it to “1997”. Now click the “Resume” button and notice that within a few
clicks we have left the loop. If you find yourself back in the loop but with the loop
variable set to zero, it’s because you have exited the loop and re-entered it on the
next iteration of the entire application. By using this feature, we have manually
updated the value of a variable to set up a specific condition in the software. This
can be used to debug a known set of events which might be causing a problem in
your application.
That’s it – you’ve just mastered the basics of single step debugging. Feel free to
play around in the debugger to familiarise yourself with the features that are
available.
Select the “Breakpoints” tab in the top right of the screen, and you will see a list of
the breakpoints that are currently set. Experiment with the tick boxes to the left of
the breakpoints. Set up various breakpoints in your code, and then enable and
disable them to see how the debugger works.
Page 29
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
In this exercise we manually adjusted the value of our loop variable to a given value
(1997). This was fine for our application, but in a more complex software project
this method could seriously affect the correct operation of the design.
Select the “Breakpoints” tab in the top right of the screen, and you will see a list of
the breakpoints that are currently set. Right-click on the breakpoint which applies to
your loop, and then choose “Breakpoint Properties...”
Work out, perhaps by using the Help system built into SDK, how to set up a
conditional breakpoint that only stops when the value of the loop variable reaches
“1997”. This might not be immediately obvious, so don’t get too annoyed with it if
you can’t find the answer.
When you have finished debugging your code, click the C/C++ button in the top
right of the screen to return to the code editing perspective.
Page 30
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
In the previous exercises we have confined our efforts to just the hardened
“processing system” (PS) section of the Zynq device. However, Zynq is much more
than just a processor, and it provides a large quantity of FPGA “programmable logic”
(PL) that can be configured by the user.
Several use models for the PL; the user can write and design custom code in a
hardware description language such as VHDL or Verilog, they can use a high level
language synthesis tool to convert C code into hardware (such as Vivado HLS), or
they can add items from an existing catalogue of IP. To expand our design we will
choose the latter option, adding some existing IP from the catalogue.
Close the SDK tool, and re-open Vivado if you have closed it. In the “Sources” pane,
expand the hierarchy of sources until you can see the block design that you created
in Exercise 1. Double click this source and the block diagram editor will be invoked
so that you can make edits to the design.
Note that the design only contains one item, the “Zynq7 Processing System”, which
is the IP that represents the hardened “PS” section of the Zynq device. We will now
expand this into the programmable logic “PL” section of the Zynq device.
Specifically, we will add functionality to control the eight LEDs, eight DIP switches,
and one of the PMOD expansion sockets.
Double click the “Zynq7 Processing System” block to open the customisation window.
Click on the green box at the bottom of the Zynq diagram marked “32b GP AXI
Master Ports”, and then enable the “M AXI GP0 Interface”. This will provide an AXI
port which can be used to connect additional peripherals in the programmable logic
part of the Zynq device. Select the “Interrupts” screen on the left of the window,
enable “Fabric Interrupts” and also expand the “Fabric Interrupts” hierarchy tree,
expand the “PL-PS Interrupt Ports” sub-tree, and then enable the “IRQ_F2P[15:0]”
feature. This makes some interrupt lines available to peripherals in the
programmable logic so that the interrupt lines on our two new peripherals can be
connected. Click “OK”.
Page 31
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
We now need to set the parameters for these two IPs. Double click the AXI GPIO
block to open its customisation window. Select the “IP Configuration” tab, and note
that peripheral has two channels “GPIO” and
“GPIO 2”. This allows two sets of GPIO
signals to be controlled by just one GPIO
peripheral. Adjust the width of the first
channel, “GPIO”, to 16 bits (8 for the DIP
switches and 8 for the LEDs) and then click
“OK”
Using the “Designer Assistance” feature in the green bar at the top of the block
design, run the “connection automation” feature and choose “/axi_gpio_0/S_AXI”
from the dialogue. When the “clock connection” drop down box appears, leave it set
to “Auto” and click “OK”. Repeat the designer assistance process for the
“/axi_quad_spi_0/AXI_LITE” connection. Note how the two new peripherals have
been automatically connected to the Zynq processing system block, and the Vivado
tools have added AXI interconnects, reset controllers, and clock nets, without the
user having to make any manual connections.
We will now connect the inputs and outputs of the GPIO and SPI peripherals to
external ports on our block design. Run the designer assistance feature again,
selecting “/axi_gpio_0/GPIO”, and then choose “custom” from the board part
dialogue that will appear. Click “OK” to confirm. Run the design assistance feature
yet again, and select “/axi_quad_spi_0/SPI_0” from the dialogue.
The SPI peripheral requires a reference clock connection, which must be connected
manually. Using your mouse, click and draw a new net from the “ext_SPI_clk” port
on the SPI block to the existing clock nets which are connected throughout the
design including the “s_axi_aclk” port on the SPI block. Note how the diagram
enhances the viable clock nets in bold to guide you to make an acceptable clock
connection.
Lastly, we will need to use the SPI controller in interrupt enabled mode, so we must
connect the interrupt output from the peripheral to the interrupt controller. Using
your mouse, click and draw a new net from the “IP2INTC_Irpt” connection on the
SPI block to the “IRQ_F2P” port on the Zynq 7 Processing System block. Note the
Page 32
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
small green tick mark which appears on the interrupt input port of the Zynq 7
Processing system block, which guides you to make this connection.
Select the “Address Editor” tab of the block design, and expand the hierarchy for the
“processing_system7_0” instance and also for the “Data” connections. You will see
the base address and high address settings for the two new peripherals that we’ve
just added to the design. The tools have automatically allocated addresses to the
new peripherals, making them suitable for use with the Zynq device.
We also need to work around a problem related to an expansion module which will
be used later. There is a bug on the expansion module board which means that one
of the SPI signals (the slave select) must be inverted in order to make it work.
Happily we are using a programmable logic device and therefore small modifications
like this are fairly simple.
If you remember from Exercise 1, we created a top level VHDL file called
“<your_design_name>_wrapper.vhd”. This is now out of date because we have
added additional ports to the block design for the new GPIO and SPI peripherals.
Vivado automatically maintains this file, so we must first allow the tools to update
the wrapper file. In the “Flow Navigator” pane on the left of the screen, run the
“Elaborate Design” step of the design flow, which will cause the top level VHDL
wrapper to be updated.
Open the top level VHDL wrapper file and check that the SPI and GPIO ports are
visible in the entity declaration. To implement the modification for the inverted SPI
SS signal, we must first save our own version of the wrapper file because it is
currently being maintained automatically by the Vivado tools. Choose File Save
File As... from the menus. Save the file as “top.vhd” in the default directory that is
presented in the dialogue.
We must now replace the automated wrapper VHDL with our own version. Right-
click on the top level wrapper in the list of Design Sources, and choose “Replace
File...”. Choose the top.vhd and click OK.
To implement the modification, open top.vhd and scroll down to the bottom of the
file until you find an instantiation of an IOBUF primitive which handles the
spi_0_ss_io signal. Add an inverter to the input of this buffer by adding the keyword
“not” to the port assignment, as shown below.
Page 33
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
The new ports for the SPI and GPIO pins are not currently allocated to any specific
IO pins on the Zynq device, so we must create and edit a constraints file to do this.
Click “Add Sources” from the “Project Manager” section of the “Flow Navigator” pane
on the left of the screen. Choose “Add or Create Constraints” and click “Next”. Click
the “Create File”... button and name the file “my_constraints”. Click “OK” and then
“Finish”.
Expand the “Constraints” folder in the “Sources” pane and locate the new constraints
file that you have just created. Double-click on the “my_constraints.xdc” file to open
it. At this stage you would normally consult the ZedBoard user guide to research the
pin locations for the LEDs and DIP switches, but it has been done for you. Cut and
paste the following constraints into the XDC file, or copy them from the supplied
resource files:
set_property PACKAGE_PIN AA11 [get_ports spi_0_io0_io]
set_property PACKAGE_PIN Y10 [get_ports spi_0_io1_io]
set_property PACKAGE_PIN AA9 [get_ports spi_0_sck_io]
set_property PACKAGE_PIN Y11 [get_ports {spi_0_ss_io[0]}]
In the interests of clarity, four of these constraints relate to pin locations for the SPI
interface, sixteen relate to pin locations for the GPIO (8 DIP switches & 8 LEDs), and
the remaining four specify IO standards and pull down resisters for both interfaces
(using wildcards in the constraints to save some typing). More specifically, the SPI
pin location constraints relate to socket “JA1” which can be found on the bottom left
of the board. An additional module will be plugged into this socket in a later
exercise, which features a sensor with a SPI interface.
Page 34
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
That completes the changes to this design. From the Vivado Flow Navigator pane,
click “Run Synthesis”, “Run implementation” and then finally “Generate Bitstream” to
complete the design. Take a few minutes to stretch your legs and top up your
caffeine levels. The next exercise will require your concentration.
When the Bitstream has been generated, please re-export the design into SDK by
choosing “File Export Export Hardware for SDK...” from the menus, check the
“Launch SDK” box, and click “OK”. Remember that the block design must be open,
and the implemented design must also be open, in order for the export process to
succeed.
When the SDK opens, you may notice furious activity from the Library Generator and
compiler tools. This is because the tools will automatically detect the changes you
have made to the hardware, and will update the BSP, and all of your software
applications for you without any fuss. It just works.
Page 35
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
It is time to develop a more complex design, which will allow you to practice using
the tools in preparation for your future embedded processor projects. In this
exercise, we will look at how to make the processor read values from user inputs (in
our case some DIP Switches) and then use that information to control some outputs
(in our case some LEDs). Once again this will require the use of the supplied
software drivers, which have been produced by the Library Generator tool when we
created a BSP. All software drivers read or write to an address in memory, which in
turn is mapped to a register in a peripheral. The job of a peripheral is to make
hardware registers appear as if they are memory, so that the processor believes that
it is simply reading from and writing to RAM. The fact that the peripheral adds extra
functionality to these registers for controlling UARTs, GPIO, and other functions is
completely hidden from the processor.
On our board, the eight DIP switches are connected to the least significant bits of the
GPIO (i.e. bits 7 down to 0), and the LEDs are connected to the eight next most
significant bits of the GPIO (i.e. bits 15 down to 8).
For our first example, we shall make the processor read the DIP switches and output
their state, bit by bit, to the LEDs. Obviously this could be done by wiring the switch
to the LED in FPGA programmable logic, but we will use software for the purposes of
our learning.
Your software
Examine the driver created for the GPIO by Library Generator by examining the
appropriate header file. The GPIO we want with the Xilinx GPIO. (But not the
one in the PS). The underlines are a “subtle” hint!
If you cannot find the header file that you’re looking for, it might be that the
BSP has not been automatically updated for some reason. Try right-clicking
on the BSP in the list of projects and choosing ”Board Support Package
Settings”, change to the “drivers” screen, and then adjust the setting in the
“Driver” column drop down menus for the “axi_gpio_0” to “gpio”. While you
are there, you can also check / update the driver for the “axi_quad_spi_0”
peripheral to “spi”. Click “OK” and the BSP will be re-built, and after a few
seconds you should see an updated list of header files in the “include”
directory.
Page 36
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
Identify the struct that must be declared for the instance of the GPIO. Again, the
hint here is that we’re declaring an instance of the Xilinx GPIO.
We will need to configure the GPIO, so identify the struct that will be used to
store the Xilinx GPIO configuration information. Also identify a function that will
lookup the details of the Configuration.
The function to lookup the config information will require a device ID, so locate
this in the xparameters.h file.
Locate the function that will initialise the configuration, using the config struct
that you found just now.
Find a function that sets the data direction on the GPIO so that the pins for the
DIP switches and LEDs are inputs and outputs, as appropriate. This is slightly
more complicated than last time, because you will need to use a bit mask to set
the direction, rather than doing it pin by pin. There is also a reference to a
“channel”, and this should be set to “1”. The soft GPIO that we built in the
programmable logic (PL), had the possibility of two channel operation. We only
implemented one channel (the default), so the channel number is always “1” for
our purposes.
Use the “Open Declaration” feature in the right-click menu examine functions in
greater detail, and this will help you to determine how to use them. Xilinx
documents each of their driver functions in comments above the function in
question. Also find a function that writes data to the GPIO.
Once you have researched the functions mentioned above, write a piece of
software in C that will check the value of the DIP switches, make modifications to
the data values, and then write the same value back to the LEDs.
The biggest hint here is to remember that the LEDs and DIP switches are
attached to the same GPIO, so therefore you must consider the positions of each
bit in the register and how it will impact the value of the variables that are read
and written.
Here is a starting point for the code, you will need to fill in the gaps. This code is
also available in the supplied files:
#include <stdio.h>
#include "platform.h"
#include "xgpio.h"
#include "xparameters.h"
#include “stdio.h”
int main()
{
XGpio_Config *GPIO_Config;
XGpio my_Gpio;
Page 37
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
init_platform();
printf("Exercise 5\n\r");
Big Gotcha!!
Now that we have created some soft peripherals in the programmable logic, it is vital that
you program the bitstream into the Programmable Logic before you run the software. To
do this, use the menus in SDK; Xilinx Tools Program FPGA, and click “Program” to
accept the defaults. The Bitstream will be loaded into the device, and the blue “DONE”
LED will illuminate on the ZedBoard to indicate that the bitstream was successfully
programmed. Remember to download the bitstream again if you power-cycle the board
for any reason.
Modify your C code to set up the drivers for both the PL based GPIO, and also
the PS based GPIO. This will require you to create different instances of the
various structs, and duplicate the lookup and initialisation function calls to
setup both GPIOs.
Write some code to control the ninth LED “LD9”. Make LD9 illuminate only
when four DIP switches or more are in the “on” position.
Page 38
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
To succeed with this challenge, you will need to think about the use of
hexadecimal mask values to check the status of each individual DIP switch.
When you detect that a switch is on, you will need to increment a variable
which counts the number of switches that are in the “on” position.
Essentially you will need to perform an AND gate operation in software, using
the variable that contains the value that you read from the DIP switches. The
syntax for a bitwise AND operation in the C language is “&”.
Before you start, ask yourself “How would I do this in hardware using logic
gates?”
Page 39
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
This exercise will look at ways to read from and write to memory. We shall begin by
exploring why this is so important. Memory is not only used to store software in an
embedded system; quite often the user will have a piece of memory as the interface
between the embedded processor system and some custom hardware in the FPGA
design. The Xilinx dual port BlockRAMs are very often used in this way, so it is
important that we explore how to access memory from our processor software. The
On Chip Memory (OCM) in Zynq also behaves like a dual port BlockRAM memory.
External (e.g. DDR) memory works in much the same way, the only difference being
that the memory controller interprets the request from the processor and generates
a transaction for the external memory which uses the correct protocol of signals used
by the DIMM or device.
To do this, we shall once again use some drivers that have been generated by the
Library Generator. The drivers are not associated with any peripheral, but are
nevertheless made available to EDK users in all processor designs. Let us look again
in the "include" directory under the name of the processor instance. Here we can
see that Library Generator has created some other
header files (.h) that do not relate to any particular
peripheral. Let us take a look at the "xil_io.h" header
file. In it we will see various functions designed
specifically for reading from and writing to memory, in
various byte widths.
However, if we wish to write four half-word wide (16 bit) data values to four memory
addresses starting at the same location, we must write the first to DDR_BASEADDR
+ 0, the second to DDR_BASEADDR + 2, the third to DDR_BASEADDR + 4, and the
last to DDR_BASEADDR + 6.
It therefore follows that when writing word wide (32 bit) data values, we must do so
on 4 byte boundaries; 0x0, 0x4, 0x8, and 0xC.
Page 40
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
As you can see from the xil_io.h header file, there are functions for reading from
memory in 8 bit, 16 bit, and 32 bit transactions. The same applies for writing to
memory in the same width variants. The exact syntax is very simple to use; here are
the function templates for writing:
Xil_Out8(memory_address, 8_bit_value);
Xil_Out16(memory_address, 16_bit_value);
Xil_Out32(memory_address, 32_bit_value);
8_bit_value = Xil_In8(memory_address);
16_bit_value = Xil_In16(memory_address);
32_bit_value = Xil_In32(memory_address);
Here is an example, showing how you might access the Zynq On-Chip Memory
(OCM):
int main(void)
{
int result1; // integers are 32 bits wide!
int result2; // integers are 32 bits wide!
return(0);
}
Write some software that will write eight data values, each one byte wide
with the following values; 0xAB, 0xFF, 0x34, 0x8C, 0xEF, 0xBE, 0xAD, 0xDE.
The values should be written to consecutive addresses, starting at the base
address of the Zynq OCM region.
Leave a gap in the OCM memory of 2 "words" in size following the values you
have just written. Then write four "half-word" data values at the next
Page 41
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
Add some code that will write the value of 0x00000000 (32 bits of data) to
address 0xE000A204.
Add code that will read two words of data starting from the base address of
the OCM region, placing the values into integer variables called "word1" and
"word2".
Add code that will read a word (32 bits) of data from address 0xE000A064,
placing the result into an integer variable called “word3”. Type this address
in your code carefully – it’s special, and it is NOT the same as the one we
used just now. A lot of people get this wrong.
Using the following lines of code, display the contents of word1, word2, and
word3 to the UART.
Run the software on the board using “Run as Launch on Hardware (GDB)”.
Check that the output on the UART terminal screen is as you’d expect for
word1 and word2. Some of the data might seem to be around the wrong
way. It’s not, but consider why this might be the case. Think about how the
addresses are arranged in the memory map for data values larger than 8 bits.
Does the value of “word3” mean anything to you? It probably doesn’t; but
try running the software on the board again, but this time press and hold
push buttons “PB1” and/or “PB2” on the board while you download the
application. What happens to the value of “word3”?
Note: If you are reading the value of “0x00000000” for word3, then you
have typed the wrong address into your code. Please go back and check
carefully.
Page 42
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
Page 43
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
Although loops like the one shown here are effective for implementing delays, they
are extremely inefficient in terms of processor time, which is often a precious
resource in embedded designs. All of the time that we spent wastefully going
around in the loop doing nothing, could be better spent executing real code and
producing useful results. Also, when using our rudimentary loops we had no real
control over the amount of delay time that would be produced by the delay loop.
We could adjust the size of the loop, but this is a trial and error process and has no
precise correlation to predictable and measurable units of time.
In this exercise we shall look at how it is possible to accurately measure time using a
hardware-based timer peripheral. This will give us much greater flexibility in our
designs, considerably more control, and will allow us to avoid wastage of the
processor's time.
A timer, just like any other processor peripheral, is a block of hardware which has
been added to the bus structure using memory mapped registers. Timers can work
in many ways but we shall first look at using a countdown timer in "polled mode".
To explain the process in its most basic form, we shall use the processor to write a
value into one of the registers within the timer and then starting it running. The
timer will decrement once per clock cycle until it reaches zero, and then stop. While
it is counting down we shall "poll" the timer from the processor, which is simply the
art of repeatedly asking the timer if it has finished. If it were a human conversation
it would go something like this:
PROCESSOR: Load the value of 10,000 into your register and start running.
TIMER: OK. I'm doing that.
PROCESSOR: Have you reached zero yet?
TIMER: No.
PROCESSOR: Have you reached zero yet?
TIMER: No.
PROCESSOR: Have you reached zero yet?
TIMER: No.
PROCESSOR: Have you reached zero yet?
TIMER: Yes.
PROCESSOR: Good, I can start doing something else.
Page 44
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
This scheme will allow us to measure time quite accurately; we know the value that
was loaded into the timer, and we know the frequency of the clock connected to the
timer. Therefore we know precisely how long it will be before we reach 0x00000000,
and that can be checked by the processor to insert a known delay into our software.
The Xilinx tools have already generated device driver functions in the BSP for writing
the values into the timer's registers, enabling and disabling the timer, and for
checking the value currently held within the timer. We can use these functions to
control the timer from our code, in the same way that we did for the other
peripherals.
There are various timers in a Zynq device, but we shall use the one built into the
Snoop Control Unit (don’t worry about the details of the SCU for a moment – but
please just believe me that there’s a timer in there!).
In the “xparameters.h” file you will find a reference to the SCU timer called
“XPAR_PS7_SCUTIMER_0_DEVICE_ID”. You will need this in your code.
Open the header file in the BSP “include” directory called “xscutimer.h”. Browse
through the available functions and identify functions which do the following:
Also identify the pre-defined struct which is used to store the settings for the timer
driver, and the struct used for configuration.
Using the xscutimer.h header file as a reference, write some code which does the
following:
Look up the config for the timer and the initialize it.
Load a suitable value into the Timer which reflects a period of one second in
real time. (HINT: The SCU Timer runs at half the frequency of the processor,
and in xparameters.h you will find some #define statements which define the
frequency of the CPU clock).
Start the timer.
Inside an infinite software loop, constantly check the value of the timer’s
counter register. When it reaches zero:
Print something to the UART. An incrementing counter value would be a
good idea, so you can see the number of seconds which have elapsed.
Page 45
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
Restart the timer from the original value so that the timer keeps running,
reaching zero repeatedly at a frequency of 1Hz.
Page 46
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
We have seen from the last exercise that peripheral based timers can be very useful
for accurately measuring time, and represent a much more elegant solution than
using variable delay loops in software. Although we have cured the problems of
inaccuracy, in the previous exercise we are still polling the timer which is an
inefficient use of processor time. To cure this inefficiency, we can use interrupts.
An interrupt is simply an event which causes the processor to stop what it is doing,
make a record of the stage it had reached and the contents of all its internal
registers, and then begin work on a new task by executing a different piece of
software. The alternate task usually performs a specific function which is related to
handling the event which caused the interrupt, and thus we call the alternate piece
of software an "interrupt handler". An interrupt handler is written in C code just like
any other function, and we then assign the interrupt handler function to the
peripheral using some special C functions supplied in the BSP. The Xilinx BSP
automates the rest of the flow, and our software will return the processor to the
original task once the interrupt handler has executed to completion.
People often get confused about interrupts, and don’t always understand why they
are helpful in software design. The easiest way to understand interrupts is to liken
them to a simple household scenario. Imagine the front door of your house, and
imagine you’re sitting inside the house watching TV. How do you know if there is
somebody waiting at your door with the pizza that you
ordered? One method is that you could routinely get up,
walk to the door, and check. If you check very regularly,
then you stand a good chance of finding your pizza at the
exact moment it arrives, but you’ll spend a lot of wasted
time going back and forth, you’ll miss most of your TV
show, and your neighbours will think you’re odd. If you
check less often then you will waste less of your time, you’ll
see more TV, but you might need to be quite lucky in order
to get your pizza before it gets cold. This is “polling”; a
constant checking process that requires a lot of manual
work. All of the same principles apply in software.
Here is an example of a piece of code that could write a line of text to the UART
when an interrupt was generated. At the moment we will not worry about how or
Page 47
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
why the interrupt was generated, but simply study it as a textbook example of how
the code might appear in C:
while(1)
{
print("I’m watching TV\n\r");
}
return(0);
}
Interrupts can be generated by many different peripherals, but for this exercise we
will be generating interrupts from the Snoop Control Unit Timer peripheral that we
used in the previous exercise. In the last exercise we were operating the timer in
polled mode, although the timer peripheral is in fact capable of producing interrupts.
The Zynq documentation states that an interrupt is generated by the timer whenever
it reaches zero, so we shall use this feature to our advantage.
In order to use interrupts, we need to use a few more driver functions to enable the
interrupt functionality on the timer, to enable interrupts on the ARM processor, and
also to set up the interrupt controller. For the beginner, this process can get a bit
scary, but I’ll walk you through it. You’re going to need coffee for this next bit. No
really... please get coffee!
Let’s look at the software flow for an interrupt. We start in the main part of the
code, and then when an interrupt occurs we jump first into a supplied general
purpose interrupt handler, and then again into your custom interrupt handler. The
supplied interrupt handler does all of the important tasks such as saving the state of
the processor, and tidying up any mess that is caused by the interruption. All you
have to do is to write the code that you want to execute when the interrupt occurs.
Page 48
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
Interrupt
Occurs
In a processor like the ARM Cortex-A9, all interrupts are managed by, and connected
via, the general interrupt controller. So our first task is to create instances of the
general interrupt controller (GIC) and the timer, and initialise them both. This is
done in the same way as before, using the “Lookup_Config” and “CfgInitialize”
functions.
XScuTimer my_Timer;
XScuTimer_Config *Timer_Config;
XScuGic my_Gic;
XScuGic_Config *Gic_Config;
Gic_Config = XScuGic_LookupConfig(XPAR_PS7_SCUGIC_0_DEVICE_ID);
XScuGic_CfgInitialize(&my_Gic, Gic_Config, Gic_Config->CpuBaseAddress);
Timer_Config = XScuTimer_LookupConfig(XPAR_PS7_SCUTIMER_0_DEVICE_ID);
XScuTimer_CfgInitialize(&my_Timer, Timer_Config, Timer_Config->BaseAddr);
We should now consider the layout of our processor system, and understand what
needs to be done to configure it correctly. We shall start with a block diagram which
shows a simplified view of a Zynq device at power-up. Please note the following
things, which are all important to the success of this exercise:
The Cortex-A9 CPU cores have internal exception handing logic, and this is
disabled and initialised at power-up.
There is one (standard) interrupt pin on the Cortex-A9 core, and there is a
master interrupt handler which the CPU executes when receiving any interrupt
request (IRQ). The handler is unassigned.
The General Interrupt Controller (GIC) has the ability to manage many
interrupt inputs, and has a table which allows interrupt handlers to be
assigned to each incoming interrupt.
Each interrupt input on the GIC has an enable “switch” that is disabled by
default.
Each peripheral / interrupt source has an output enable “switch”, that is
disabled by default.
Page 49
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
IRQ
Page 50
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
Next we need to initialise the exception handling features on the ARM processor.
This is done using a function call from the “xil_exception.h” header file.
Xil_ExceptionInit();
1
When an interrupt occurs, the processor first has to interrogate the interrupt
controller to find out which peripheral generated the interrupt. Xilinx provide an
interrupt handler to do this automatically, and it is called
“XScuGic_InterruptHandler”. To use this supplied handler, we have to assign it to
the interrupt controller. The syntax is pretty scary, but it’s the same for all designs
so it can just be copied and pasted for every design that you create. The only item
that needs to be changed is the name of the GIC instance at the end of the function
(in our case “&my_Gic”).
Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_IRQ_INT,
2
(Xil_ExceptionHandler)XScuGic_InterruptHandler, &my_Gic);
We now need to assign our interrupt handler, which will handle interrupts for the
timer peripheral. In our case, the handler is called “my_timer_interrupt_handler”.
It’s connected to a unique interrupt ID number which is represented by the
“XPAR_SCUTIMER_INTR”. You’ll find a list of these IDs in the “xparameters_ps.h”
header file, and they cover all of the peripherals in the PS which generate interrupts.
If you were dealing with an interrupt which came from a peripheral in the PL, you’d
find a similar list in the “xparameters.h” header file.
3
XScuGic_Connect(&my_Gic, XPAR_SCUTIMER_INTR,
(Xil_ExceptionHandler)my_timer_interrupt_handler, (void *)&my_Timer);
The next task is to enable the interrupt input for the timer on the interrupt controller.
Interrupt controllers are flexible, so you can enable and disable each interrupt to
decide what gets through and what doesn’t.
XScuGic_Enable(&my_Gic, XPAR_SCUTIMER_INTR); 4
Next, we need to enable the interrupt output on the timer.
XScuTimer_EnableInterrupt(&my_Timer); 5
Finally, we need to enable interrupt handling on the ARM processor. Again, this
function call can be found in the “xil_exception.h” header file.
Xil_ExceptionEnable();
6
That’s the setup completed for the interrupt controller and timer. Let’s review our
work, and see what changes we’ve made to the system diagram. Use the yellow
numbers to cross-reference each line of C code to the block diagram.
Page 51
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
1 6
Cortex-A9 core(s) Exception handling logic = INITIALISED & ENABLED
2
IRQ
Page 52
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
Now we need to write an interrupt handler, which is the function that will be called
when the interrupt occurs. We’ve already decided on a name because we used it
above; “my_timer_interrupt_handler”.
This is what the basic structure of our timer interrupt handler looks like:
We can clearly see the “CallBackRef” that is passed into the function, and we can use
it to our advantage. Suppose we wanted to control the timer as part of our interrupt
handler code? Without the CallBackRef we couldn’t do it, because the original
instance of “my_Timer” doesn’t exist in the scope of this function. (We declared it in
main() and therefore it cannot be directly used in our interrupt handler because our
handler is a different function)
But we can declare a local copy of the “my_Timer” instance, and assign it the
information provided by the “CallBackRef” :
The instance “my_Timer_LOCAL” is now an exact copy of the one in our “main”
function, and we can control the timer in the same way as we did before. Any
changes to “my_Timer_LOCAL” will also apply to the original copy of “my_Timer”,
because they are linked together using a pointer.
Page 53
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
if (XScuTimer_IsExpired(my_Timer_LOCAL))
{
xil_printf("We are in the interrupt handler!!\n\r”);
}
}
The last task is to clear the interrupt condition in the timer peripheral. If we don’t do
this, then the interrupt condition will still be active. That means that the moment we
exit from our interrupt handler, the processor will jump straight back into it again.
The main section of our application would therefore never be executed. Again there
is a supplied function call in the timer’s header file to do this, so we need to add it to
our interrupt handler :
XScuTimer_ClearInterruptStatus(my_Timer_LOCAL);
Add your code back in from the previous exercises to make the eight LEDs (LD0 –
LD7) show the values on the eight DIP switches, at the same time that the
messages on the UART are getting displayed. If you do it correctly, then the
messages will continue to be displayed at 1Hz no matter what you are doing with
the rest of the code.
Also make LD9 flash at precisely 1Hz, using interrupt control. Think carefully
about this before you start hacking your code. It’s complicated.
I don’t want to scare you, but you’re already becoming quite proficient with Zynq. The basics that
you’ve covered so far are the foundations for the vast majority of Zynq embedded design projects.
You understand how the Xilinx drivers work, you understand how interrupts work, and you understand
how peripherals are added in the PL. You can do a lot with that!
Page 54
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
You're nearly there! We have looked at all of the basic techniques that would be
needed to create an embedded processor system with Zynq. For this final exercise
we will go beyond the pins of the Zynq device, and expand the design to
communicate with an external device.
You may recall that in our previous exercises we added two peripherals to the PL
section of the Zynq device, and one of those peripherals (the GPIO) we used
immediately. The other one, the SPI controller, was connected to some external pins
but we didn’t do anything with it. In this exercise we will explore the use of the
incredibly popular SPI protocol, and use it to control a device on an external “PMOD”
daughter board.
SPI (Serial Peripheral Interface) is a very well known standard interface in the digital
electronics world, that allows master and slave devices to communicate using just
four wires; SCLK, MOSI, MISO, and SS. SCLK is (perhaps obviously!) the clock
connection. MOSI is an abbreviation for “Master Out Slave In”. MISO is a similar
abbreviation for “Master In Slave Out”. SS is an abbreviation for “Slave Select”, and
is an active-low connection. Technically speaking, therefore, the signal should be
named “nSS” but it hardly ever is.
SPI is a simple protocol, whereby data is
sent and received in a loop. SPI has no
specific concept of “read” and “write”,
but merely understands the concept of
data “transfers”. For every byte the
master sends to the slave, the slave
always sends a byte back again. As a
result of this system, it is up to the
master to “know” what data is expected
back from any given slave, so that
irrelevant dummy bytes (often 0x00 or
0xFF) can be ignored. For example, if
the master is
sending a command to the slave, but nothing is expected
back, then the master must be programmed to ignore any
bytes that come back. If the master sends a byte and the
slave is configured to reply immediately, then the master
must actually send two bytes (one for the command and
one dummy byte) because the slave will reply with its
data during byte number two.
PMOD is an abbreviation for “Peripheral MODule” and was designed by Digilent Inc
(http://www.digilentinc.com). Digilent hold the trademark for “PMOD”, and details of
the licensing agreement use can be found at
http://www.digilentinc.com/Pmods/licensing.cfm .
Page 55
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
For this experiment we shall be using a temperature sensing PMOD board from
Maxim, called the “MAX31723PMB1”. The board is very simple, and consists merely
of a MAX31723 device mounted to a 6 pin PMOD board. Four of the pins are used
for the SPI connections, and the remaining two are
used for Vcc and GND. The MAX31723 can also be
used in a 3-pin I2C mode, but we will stick to using SPI
for the purposes of our workshop today.
MOSI
Byte 0 Byte 1
(Address) (Dummy)
Master Slave
(Zynq) (MAX31723)
MISO
Byte 1 Byte 0
(Data) (Dummy)
We will read two registers from the MAX31723; requiring four bytes to be transferred
in total (two address bytes and two dummy bytes). The TEMPLSB register is at
address 0x01, and the TEMPMSB register is at address 0x02. Using the data from
these two registers, it is possible to calculate the current temperature. The TEMP MSB
byte represents the temperature in whole units (1 degree C, 2 degrees C... 100
degrees C, etc) in two’s compliment binary, and TEMPLSB represents fractions of a
degree (the bits in the binary word represent ½ degree C, ¼ degree C, ⅛ degree C,
and so on).
Page 56
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
Before reading the temperature registers, we must enable the device using the
configuration register which is at address 0x80. Conveniently for us, the simplest
and most useful mode to get a basic temperature reading can be achieved by
clearing all of the bits in the configuration register, so we will write the value of 0x00
to it at the beginning of our software application.
The design is already ready to go, because all of the pin location constraints that we
added to the .XDC file in our previous exercise were sufficient to route the SPI
connections to PMOD socket “JA1” on the ZedBoard (next to the DIP switches). Plug
the MAX31723PMB1 module into the upper row of pins on connector JA1 with the
jumpers on the module facing upwards.
We will use the device in interrupt mode, and due to the complexity of the C code a
template is supplied for you. The code follows all of the same rules that we saw
from our previous exercises, using the same principles to set up the driver and
initialise interrupts.
The code starts a transfer and then sets up a global variable and a while loop that
halts execution until the SPI transfer has completed. The SPI peripheral can
generate an interrupt for a number of reasons (completion of a transfer, error
conditions, etc) and so the interrupt handler has to check to see what event caused
the interrupt before taking any action. As a result, the interrupt handler in this
exercise is slightly more advanced; because it first has to do some checking. If the
interrupt condition is the desired result (transfer complete) then the handler sets the
global variable back to the original value and the main section of the application can
continue.
Global variables are also used to implement two buffers for the SPI peripheral; one
read buffer and one write buffer. These buffers are then passed to the SPI drivers
and are sent / received, byte by byte, during an SPI transfer.
Page 57
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
I have left some useful features in the code (unused) to help you. These
include functions like “display_buffers()” which could be called to help you
debug what you’re doing.
This is harder than it looks. Make good use of caffeine!
The supplied code uses some calls from the math.h library, and you will need to
make a small change to the properties of your software project to tell the linker to
include the maths library. The error message you will see from the compiler is
“Undefined reference to ’pow’”. Follow these steps carefully to correct the
problem. If different compiler error messages are seen, then it’s a fault in your code
and not mine. Have fun!
Right-click on the “Exercise 9” project in the Project Explorer, and choose
“C/C++ Build Settings”.
Choose the “ARM gcc linker Libraries” section.
Click the “Add...” button in the “Libraries” pane at the top. (The button is the
one with the green “+” sign on the document icon)
Type “m” in the entry field and then click “OK”.
Click “OK” to return to the main SDK window.
When your code is up and running, you can experiment with making the temperature
sensor more precise. Examine the command that you sent to the MAX31723’s
configuration register. You will see that it contains a value called
“MAX31723_THERMOMETER_RESOLUTION_9BIT_MODE”.
Page 58
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
We now have a design that works on the board, and is advanced enough for us to
call it “finished”. We now want to make the ZedBoard boot automatically from
power on.
To make this happen, we need to create a “First Stage Boot Loader” (FSBL), and
then combine our application software, the FSBL, and the Programmable Logic
hardware bitstream into one flash image file. Happily, Xilinx have made this a very
easy flow and it will allow you to get your board booting without any trouble.
From the Xilinx SDK tool, choose “File New Application Project” from the
menus. Give the project a name, for example “FSBL”, create a new Board Support
Package for this application, and click “Next”. At the bottom of the list of templates
choose “Zynq FSBL” and click “Finish”.
Note: The Xilinx FSBL requires the inclusion of some libraries to access the FAT file
system on the SD card. This is the reason that a new Board Support Package must
be generated, because the tools automatically include the required libraries.
The FSBL project will be created next to all of the other software application projects.
No modifications are necessary – the code just works.
Page 59
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
Open a Windows Explorer session (or an alternative file system application if you are
using a different operating system on your host machine), and navigate to the
location of the boot image that you have just created. You will see two files (.bif and
.bin). The .bif file contains the settings for the “Boot Image Generator” wizard, and
can be used if you ever want to re-create the boot image using the same settings as
before. The second file (.bin) is the binary image that is used if you are configuring
the Zynq device using a SD Card.
Page 60
Zynq Workshop for Beginners (ZedBoard) -- Version 1.0, July 2014
Rich Griffin, Silica EMEA
Page 61