4 Example host applications
4.1 General
There are four or five simple C programs that demonstrate how to access Xillybus’ device files. These programs can be found in the the compressed file that contains the host driver for Xillybus / XillyUSB (available for download on the website). Refer to the “demoapps” directory, which consists of the following files:
-
Makefile – This file contains the rules that are used by the “make” utility for the purpose of the programs’ compilation.
-
streamread.c – Reads from a file, sends data to standard output.
-
streamwrite.c – Reads data from standard input, sends to file.
-
memread.c – Reads data after performing seek. Demonstrates how to access a memory interface in the FPGA.
-
memwrite.c – Writes data after performing seek. Demonstrates how to access a memory interface in the FPGA.
The purpose of these programs is to show correct coding style. They can also be used as a basis for writing your own programs. However, neither of these programs is intended for use in a real-life application, in particular because these programs don’t perform well with high data rates. See chapter 5 for guidelines on achieving high bandwidth performance.
These programs are very simple, and merely demonstrate the standard methods for accessing files on a Linux computer. These methods are discussed in detail in the Xillybus host application programming guide for Linux. For these reasons, there are no detailed explanations about these programs here.
Note that these programs use the low-level API, e.g. open(), read(), and write(). The more well-known API (fopen(), fread(), fwrite() etc.) is avoided, because it relies on data buffers that are maintained by the C runtime library. These data buffers may cause confusion, in particular because the communication with the FPGA is often delayed by the runtime library.
Those who download the driver for PCIe will find a fifth program in the “demoapps” directory: fifo.c. This program demonstrates the implementation of a userspace RAM FIFO. This program is rarely useful, because the device file’s RAM buffers can be configured to be sufficient for almost all scenarios. fifo.c is hence useful only for very high data rates, and when the RAM buffer needs to very large (i.e. several gigabytes).
This program is not included along with XillyUSB’s driver, because the data rates that may require fifo.c are not possible with XillyUSB.
4.2 Editing and compilation
If you’re experienced with compilation of programs in Linux, you may skip to the next section: The compilation of Xillybus’ example programs is done with “make” in the usual way.
First and foremost, change directory to where the C files are:
$ cd demoapps
To run a compilation of all five programs, just type “make” at shell prompt. The following transcript is expected:
$ make gcc -g -Wall -O3 memwrite.c -o memwrite gcc -g -Wall -O3 memread.c -o memread gcc -g -Wall -O3 streamread.c -o streamread gcc -g -Wall -O3 streamwrite.c -o streamwrite gcc -g -Wall -O3 -pthread fifo.c -o fifo
The five rows that start with “gcc” are the commands that are requested by “make” in order to use the compiler. These commands can be used for compilation of the programs separately. However, there is no reason to do so. Just use “make”.
On some systems, the fifth compilation (of fifo.c) may fail if the POSIX threads library isn’t installed (e.g. in some installations of Cygwin). This error can be ignored if you don’t have an intention to use fifo.c.
The “make” utility runs compilation only on what is necessary. If only one file is changed, “make” will request the compilation of only that file. So the normal way to work is to edit the file you want to edit, and then use “make” for a recompilation. No unnecessary compilation will take place.
Use “make clean” in order to remove the executables that were generated by a previous compilation.
As mentioned above, the Makefile contains the rules for the compilation. The syntax of this file is not simple, but fortunately it is often possible to make changes to this file by using just common sense.
A Makefile relates to the files that are in the same directory as the Makefile itself. It is therefore possible to make a copy of the entire directory, and work on the files that are inside this replica. The two copies of the directory will not interfere with each other.
It is also possible to add a C file and easily change the Makefile, so that “make” also runs a compilation of this new file. For example, suppose that memwrite.c is copied to a new file, which is named mygames.c. This can be done with the GUI interface or with command line:
$ cp memwrite.c mygames.c
The next step is to edit the Makefile. There are many text editors and numerous ways to run each of them. On most systems, it is possible to start a GUI editor from the shell prompt by typing “gedit” or “xed”. However, it’s easier to find a GUI text editor in the computer desktop’s menus. There are also many text editors that work inside the terminal window, for example vim, emacs, nano and pico.
Which editor to use is a matter of taste and personal experience. For example, this command can be used to start editing the Makefile:
$ xed Makefile &
The ’&’ at the end of the command tells the shell to not wait until the program finishes: The next shell prompt appears immediately. This is suitable for starting GUI applications, among others.
The row that should be changed in Makefile is:
APPLICATIONS=memwrite memread streamread streamwrite
This row is changed to:
APPLICATIONS=memwrite memread streamread streamwrite mygames
The compilation of mygames.c will take place on the next time you type “make”.
4.3 Running the programs
The simple loopback example that is shown in section 3.3 can be done with two of the example programs.
Let’s assume that “demoapps” is already the current directory and that a compilation has already been done with “make’.
Type this in the first terminal:
$ ./streamread /dev/xillybus_read_8
This is the program that reads from the device file.
Note that the command begins with “./”: It is necessary to explicitly point at the directory of the executable. In this example, the expression “./” is used to request the current directory.
And then, in the second terminal window:
$ ./streamwrite /dev/xillybus_write_8
This works more or less like with the example with “cat”. The difference is that “streamwrite” doesn’t wait for ENTER before sending the data to the device file. Instead, this program attempts to operate separately on each character. In order to achieve this, the program uses a function called config_console(). This function is used only for the purpose of an immediate response to typing on the keyboard. This has nothing to do with Xillybus.
The examples above relate to Xillybus for PCIe / AXI. With XillyUSB, the names of the device files have a slightly different prefix. For example, xillyusb_00_read_8 instead of xillybus_read_8.
IMPORTANT:
The I/O operations that are performed by streamread and streamwrite are inefficient: In order to make these programs simpler, the size
of the I/O buffer is only 128 bytes. When a high data rate is required, larger buffers should be used. See section 5.3.
4.4 Memory interface
The memread and memwrite programs are more interesting, because they demonstrate how to access memory on the FPGA. This is achieved by making function calls to lseek() on the device file. There is a section in Xillybus host application programming guide for Linux that explains this API in relation to Xillybus’ device files.
Note that in the demo bundle, only xillybus_mem_8 allows seeking. This device file is also the only one that can be opened for both read and for write.
Before writing to the memory, the existing situation can be observed by using the hexdump utility:
$ hexdump -C -v -n 32 /dev/xillybus_mem_8 00000000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000020
This output is the first 32 bytes in the memory array: hexdump opened /dev/xillybus_mem_8 and read 32 bytes from this device file. When a file that allows lseek() is opened, the initial position is always zero. Hence the output consists of the data in the memory array, from position 0 to position 31.
It’s possible that your output will be different: This output reflects the FPGA’s RAM, which may contain other values. In particular, these values may be different from zero as a result of previous experiments with the RAM.
A few words about hexdump’s flags: The format of the output that is shown above is the result of “-C” and “-v”. “-n 32” means to show first 32 bytes only. The memory array is just 32 bytes long, so it’s pointless to read more than so.
memwrite can be used to change a value in the array. For example, the value at address 3 is changed to 170 (0xaa in hex format) with this command:
$ ./memwrite /dev/xillybus_mem_8 3 170
In order to verify that the command worked, it’s possible to repeat the hexdump command from above:
$ hexdump -C -v -n 32 /dev/xillybus_mem_8 00000000 00 00 00 aa 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000020
So evidently, the command worked.
In memwrite.c, the important part is where it says “lseek(fd, address, SEEK_SET)”. This function call changes the position of the device file. Consequently, this changes the address of the array’s element that is accessed inside the FPGA. The subsequent read operation or write operation starts from this position. Each such access increments the position according to the number of bytes that were transferred.
A device file that allows seeking is also useful for easily sending configuration commands to the FPGA. As already mentioned, when a file that allows lseek() is opened, the initial position is always zero. This also holds true for a command like in this example:
$ echo -n 010111 > /dev/xillybus_mem_8
Note the “-n” part in the “echo” command. This prevents “echo” from adding a newline character at the end of its output.
This command writes the ASCII code of “0” (the value 0x30) to the address zero. Likewise, the value 0x31 is written to address 1, and so on. So this simple “echo” command can be used to set the values of several registers at once.
This is a convenient method, because the implementation on the FPGA is simple. For example, suppose that only the characters “0” and “1” are intended for use with the “echo” command. Accordingly, only bit 0 is important. This is an example of a register that obtains its value from the third byte of the “echo” command:
reg my_register;
always @(posedge bus_clk)
if (user_w_mem_8_wren && (user_mem_8_addr == 2))
my_register <= user_w_mem_8_data[0];
This method is relevant in particular for running tests while developing the FPGA’s logic.
