Introduction:
Working on FPGA often requires you to transfer data to your PC for better evaluation.
In this blog, we will learn to transfer data from the Arty S7-50 board to our PC using the xuartlite APIs in Vitis. I will be using the design that I created in my previous blog. This tutorial can be followed with default presets also as long as you have UartLite in your design.
Block Design:
Writing the code:
Open Vitis and create an empty C Application Project. Create a source file inside the "src" folder. Create "platform.h", "platform_config.h" and "platform.c" files in the "src" folder by first creating a temporary project with "Hello World" template and then copying these files from this temp project's "src" folder. You can delete this temp project after that.
We will need to include the "xuartlite.h" and the "xparameters.h" header files also into our project. Your workspace should look something like this:
Now, on the right hand side you will notice the names of header files in the "Outline" tab if the system was able to successfully locate them. You can open these files by double clicking on them in the "Outline" tab directly.
I would advise you to look into the "xparameters.h" and the "xuartlite.h" files for better understanding of the parameters and the functions that we are going to use.
The "xparameters.h" header file contains all the parameters like device ID, base addresses etc for various devices in our design.
The "xuartlite.h" header file contains the function prototypes that we will require to send data from the board to our PC.
Sending Side:
So, now we will use the following code to send some array elements over UART to the PC.
Code:#include "platform.h"
#include "xil_printf.h"
#include "xstatus.h"
#include "xparameters.h"
#include "xuartlite.h"
XUartLite uart;
u8 Tx[2][3] = {{75, 23, 56}, {43, 78, 89}};
int main() {
init_platform();
XUartLite_Initialize(&uart, XPAR_UARTLITE_0_DEVICE_ID);
for (int i=0; i<2; i++) {
XUartLite_Send(&uart, Tx[i], 3);
}
cleanup_platform();
return 0;
}
- We first need to instantiate the XUartLite driver (XUartLite uart).
- We will intialize an array with the data that we need to send to our PC (u8 Tx[2][3]).
- The XUartLite_Initialize() function is used to initialize the UART device with default configurations (the configurations of the UartLite block in the design). We need to pass a reference to the XUartLite instance (&uart) and the device ID (XPAR_UARTLITE_0_DEVICE_ID) as arguments to the function.
- The XUartLite_Send() function will send data over the serial connection. We need to pass a reference to the XUartLite instance (&uart), the base address of our array and the number of bytes that we need to send in one go.
- The maximum number of bytes that the XUartLite_Send() function can send at once is 16. So, after sending 16 bytes, we will need to change the base address (i.e. the second argument of the function) to the 17th byte's base address to send another 16 bytes.
- The array in our code has two rows with three elements each, so there are a total of 6 bytes that need to be sent. We will send one row at a time i.e. three bytes at a time. The for loop will iterate over rows and thus modify the base address (i.e. the second argument) to the starting address (or the base address) of each row while iterating.
Receiving Side:
Now, we have written the code to send the data. This code will run on the board. But we will need something to catch this data on the receiving end i.e. the PC. Well, there are some ways like serial monitors like Putty or Terra Term. We can also use the serial terminal in vitis itself.
But sometimes, we might need to process the data further. It would be better if we load it in some programming environment. So, we will use the "pyserial" package in python to capture the data.
Code:import serial
ser = serial.Serial()
ser.baudrate = 9600
ser.port = 'COM7'
ser.timeout = None
print(ser)
ser.open()
print(ser.isOpen())
for i in range(2):
x = ser.read(1)
y = ser.read(1)
z = ser.read(1)
print(int.from_bytes(x, 'big', signed=False))
print(int.from_bytes(y, 'big', signed=False))
print(int.from_bytes(z, 'big', signed=False))
print("\n")
ser.close()
- First of all we will get a serial instance.
- We can set the parameters according to our wish but it must match with the sending side. We will set the baud rate to be '9600'. In my case, the port to which my board is connected is 'COM7'. You need to use your port name in you code. Setting "timeout = None" will make the code to keep looking for data until it receives it. The function read() is used to keep looking for the bytes of data specified within its parenthesis. Giving "timeout" some value will mean that the read() function will wait only for that particular time and if it doesn't get the specified bytes of data in that time, it will stop looking for it and rest of the code will execute. Setting "timeout = None" will make it look for the data until it gets it no matter how much time it takes.
- We can view the current values of the parameters by printing the serial instance (i.e. print(ser)).
- Now, we need to open the port (ser.open()). We can check if our port is open or not by ser.isOpen() (True for open and False for close). Moreover, python will throw an error if the port is busy or opened by some other program. So, make sure that no other program is using this particular port before running the code.
- Now since we have opened the port, we can now look for the data. We need to capture a total of 6 bytes of data which will come in two groups of three bytes as we described in our Sending side code. So, we are running loop two times, in each iteration we will capture three bytes of data. To read data, we use ser.read(). '1' in bracket means that we are looking for one byte of data. The read() function will keep looking for data till it gets the whole byte. It will block the execution of the rest of the code till it gets its one byte. We have used three read() functions which will capture 1 byte each in each iteration and will assign these values to x, y and z respectively. You can capture all the three bytes at once also by ser.read(3).
- Now, return type of the read() function is a "bytes" object. We can convert it into int with 'int.from_bytes(x, 'big', signed=False)' and eventually into hex with 'hex(int.from_bytes(x, 'big', signed=False))' if we wish to.
- We will close the serial connection after receiving all the bytes with ser.close().
Running the Code:
We have written the code for both the sending side and the receiving side. Now we will run these to check if our code is working or not.
- Open vitis, open the application project, copy the code of sending side into the source file.
- Connect the board to the PC, program it, build the code and launch it on the hardware.
- Now open the command prompt and run the python code for the receiving side.
- After a few seconds of hardware getting programmed, we will start receiving data and it will be printed on the terminal.
Conclusion:
Using serial monitors to get data is a simple option. The data will be printed on the serial monitor. We can then copy this data and use it for further evaluation. But directly loading the data into the programming environment is sometimes preferable.
For example, if you need the data in another format rather than the format in which the data is being sent, then there are two options. First one is to use the serial monitors to get data, then copy the whole data into our programming workspace and initialize a variable with this data and then process it. The second option is to directly get the data through code and process it as soon as we receive it.
It should be kept in mind that UART protocol doesn't have acknowledgement bits. The sender will just keep sending the data without confirming if the receiver was to able to capture it or not. So, you should avoid doing heavy processing which takes some time to process while the data is being received otherwise you might miss some data.