Software API
Low-level UART (Serial) Functions
Note: If you are connected to the FPGA via the VLAB, then the VLAB runs a serial terminal directly and you can communicate with your FPGA through the VLAB client. You can ignore this bit.
Xilinx SDK has a serial terminal that can be used to communicate with your design. It is in the bottom panel under the heading "SDK Terminal". However, this terminal is line buffered, which means that nothing is sent until you have written a line of text and clicked "Send". If you want to communicate using individual keypresses, you need to use a better serial terminal program. We recommend using GNU screen. You can connect screen by opening a terminal and typing
screen /dev/ttyUSB1 115200
Note that sometimes the FPGA appears on a different device node, so you might have to try /dev/ttyUSB0
or /dev/ttyUSB2
.
Code examples
Most of the time you can use the serial/UART just by using the C standard library. Functions like printf and scanf will work with the UART to output and input accordingly. However you might find that lower-level access to the UART hardware is useful. You can use the functions in xuartps_hw.h
to do this. For example:
Low-level UART use
#include "xuartps_hw.h"
int main(void) {
if(XUartPs_IsReceiveData(STDIN_BASEADDRESS)) { //If the user has pressed a key
char byte = XUartPs_RecvByte(STDIN_BASEADDRESS); //Read it in
XUartPs_SendByte(STDOUT_BASEADDRESS, byte); //And send it out
}
}
These functions let you read and write individual bytes.
Measuring Time
The ARM core contains a monotonically increasing counter, which can be used to measure time in the system without controlling a full countdown timer manually (detailed below). The timer increases at half the ARM clock frequency (i.e. every two clock cycles).
Time can be accessed using the XTime
functions, as follows:
XTime Example
#include "xtime_l.h"
int main() {
XTime startTime, endTime, executionTime;
XTime_GetTime(&startTime);
// Perform execution here
XTime_GetTime(&endTime);
executionTime = endTime - startTime;
float timeInSecs = 1.0 * executionTime / COUNTS_PER_SECOND;
}
Countdown Timer
The ARM system has an internal timer which can be used to measure execution times. An example of doing this is shown below:
ARM internal timer
#include <stdio.h>
#include <xscutimer.h>
int main() {
int i;
XScuTimer timer;
XScuTimer_Config *timercfg;
timercfg = XScuTimer_LookupConfig(XPAR_SCUTIMER_DEVICE_ID);
XScuTimer_CfgInitialize(&timer, timercfg, timercfg->BaseAddr);
XScuTimer_LoadTimer(&timer, 500000000);
XScuTimer_Start(&timer);
for(i = 0; i < 10; i++) {
printf("This is something which takes time.\n");
}
XScuTimer_Stop(&timer);
int val = XScuTimer_GetCounterValue(&timer);
printf("Timer value: %d\n", val);
return 0;
}
Note that the timer is a countdown timer. xparameters.h
includes a #define
called XPAR_CPU_CORTEXA9_0_CPU_CLK_FREQ_HZ
which is the current clock rate. This can be used to convert clock cycles to time.
A common use of the timer is to trigger a periodic interrupt. The code sample below shows how to set this up.
Using the timer
Important
The ethernet framework below also makes use of this timer!
Ethernet
The ARM cores can use the Zybo's Ethernet connection to send and receive messages over the network. To use the Ethernet, do the following:
In Vitis, double click your project's
.prj
file and select Navigate to BSP Settings, then Modify BSP Settings.Tick the
lwip211
(Lightweight IP) library. (Note: this may be a higher number if a more recent version has been released.)In the list on the left, under standalone, click
lwip211
. This shows the settings for the library.Expand
dhcp_options
and setlwip_dhcp
totrue
.
This will bring in the Lightweight IP library, and set it to obtain an IP address by DHCP when your system boots.
Add the following two platform files to your project (or replace them if they already exist). They set up various parts of the system and initialise the hardware.
Create a
main.c
and follow the code structure as in the examples below.
If you are working in C++ then rename platform.c
to platform.cpp
and the tools should automatically use the correct compilation. You will need to wrap the LWIP includes at the top of platform.cpp in an extern C declaration to tell the C++ compiler to expect a C library. Like this:
Using the Ethernet
The following code structure shows examples of how to use the ethernet:
Ethernet main.c
Important things to note:
The above code is just to show sample usage, and will not compile as it is.
You must use a unique MAC address. In EMBS these are listed on the EMBS Student Network page.
Sending and receiving requires a packet buffer (
pbuf
). You must remember to free these after using them.Sending and receiving also requires Protocol Control Blocks (PCBs). While you can remove these when you've finished using them, we recommend re-using them if you're going to send or receive more than once.
After setting up any handlers you must call
handle_ethernet()
.
If you don't have DHCP
The default ethernet code uses DHCP to automatically obtain an IP address from the network, based on your MAC address. If DHCP requests aren't working, it often means you're not connected to the network correctly, or you have a problem with your code. There could also be network issues, so ask a demonstrator if unsure.
If you're sure that you shouldn't be using DHCP (e.g. if you're not using the EMBS network), you can use a manual IP address as follows:
Set up the application and BSP as above.
Right click your BSP and click Board Support Package Settings. In the left-hand column, under
standalone
, clicklwip202
.Expand
dhcp_options
and setdhcp_does_arp_check
andlwip_dhcp
both to false.
Now you must provide an IP address and subnet mask manually, as below:
Manually specifying IP address
Sharing Memory Between HLS and the ARM
To share a large amount of data between the ARM cores and an HLS component you will use main system memory. The Zybo Z7 has 1GB of main DDR memory which can be accessed from an HLS component by using an AXI Master interface on the HLS core.
Look at this diagram. It helps to understand how the system is laid out.
The ARM cores read and write data from main memory. Your HLS core is controlled by the ARM over its slave interface, but it can also access main memory via its master interface. For this reason, you should see why it doesn't make sense to ask "how do I pass data from the ARM core to HLS?". The data is always in memory, instead the ARM core simply needs to tell the FPGA where to look for it.
We can see therefore that the HLS core and the ARM cores are reading and writing from the same memory. Therefore we will declare a segment of that memory that we can use for sharing. The easiest way to do this is to declare a global array, then pass the address of the shared memory into the HLS component using XToplevel_Set_ram
:
Declare a segment of shared memory
In HLS we can read and write from RAM address 0 and it will be offset by the value we passed in with XToplevel_Set_ram
to access the shared memory:
Using the address in HLS
In the example above we declared 4000 bytes to use as shared memory between HLS and the ARM cores. This is not only "input" data, it is shared data. If your algorithm needs to read in some input data and produces a chunk of output data, you can arrange it all in the array accordingly. For example, imagine a problem which takes in 400 bytes and produces 400 bytes:
Returning lots of data
Bulk reads and writes with memcpy
(include string.h
) are faster than reading individual words. For example:
Use of memcpy
Both the loop and the call to memcpy
do the same thing, but memcpy
is much faster because HLS will use what is called a burst transfer to copy in data at a faster rate. You can also memcpy
data out to RAM.
Remember that the system contains caches! If you simply write data and do nothing else the ARM will write and read from its caches, which are not visible to the HLS component. Also any memory changed by HLS will not invalidate the ARM's cache lines so you may not see the updates. You must flush the caches when you want to force the ARM to write to or read from system memory. For example:
Caching
This code uses Xil_DCacheFlush()
and Xil_DCacheInvalidate()
to flush changes from the cache to main memory and re-read from main memory into cache. Xil_DCacheFlushRange()
and Xil_DCacheInvalidateRange()
can also be used to specify regions of memory that have changed.
If you are having issues which you suspect are cache-related you can completely disable caches by calling Xil_DCacheDisable()
, but this will make your code a lot slower.
Using C Maths Functions
Functions such as sin
and floor
are defined in the standard C header math.h
. If you use this you may find that the compiler does not include the maths library by default, resulting in errors like:
undefined reference to `sin'
To fix this:
In SDK, right click your application project and select
Properties
Go to
C/C++ Build | Settings
In the
Tool Settings
tab, underARM v7 gcc linker
clickLibraries
Click the Add button and enter
m