The goal

The Xillybus Linux mini-distro is not just a general-purpose system, but it's also a starter kit for developing peripherals based upon the Xillybus IP core. As such, its merits are best discovered by working with it in a real design. The initial FPGA code includes the simplest possible implementation, as a basis to work on. You may want to look at Xillybus main page to get an idea of what the Xillybus IP core is about.

In short, Xillybus makes a seamless connection between data from and to the FPGA and classic Linux device files. It allows FPGA logic to drop data into a standard FIFO, and have the data read by a userspace application (on the Linux machine) directly from a device file. The Xillybus IP core and the Linux driver make sure that things work in a sensible way. Of course, there's also the other direction: A Linux userspace application can write data to a device file, and the FPGA then sees the data appearing on a standard FIFO interface.

In the downloaded bundle, the FPGA merely loops the data it receives back to the host. This makes the demonstration somewhat unimpressive, but also makes it a good starting point for developing peripherals of your own. This loopback is implemented by having two FIFOs connected between two pairs of interfaces with the core. Any data sent to the core is stored in those FIFOs and looped back to the host. A RAM is also connected to the core as well, demonstrating addressed memory-like access.

The tests shown below merely trigger off communication with the FPGA. Things will become really interesting once you hook up something else instead of the existing FIFOs and RAM.

This guide is based upon examples shown with command-line interface, which is the only possibility on Microblaze Linux.

The trivial loopback test

The idea about this test is to verify that the loopback indeed works. The easiest way is using the UNIX command-line utility "cat".

Open two telnet connections to the card, if you have network set up.

On the first telnet connection, type at command prompt (don't type the "#", it's the prompt):

# cat /dev/xillybus_read_8

This makes the "cat" program print out anything it reads from the xillybus_read_8 device file. Nothing is expected to happen at this stage.

If you don't have telnet connection (and hence not two command line sessions), add a "&" sign at the end of the command above, so that the process runs in the background.

On the second telnet connection, type

# cat > /dev/xillybus_write_8

Notice the ">" sign, which tells "cat" to send anything typed to xillybus_write_8.

Now type some text on the second telnet connection, and press ENTER. The same text will appear in the first connection. The reason nothing is sent until ENTER is pressed, is that the standard input is designed not to bother applications with every character.

Either "cat" can be halted with a CTRL-C (if a process is in the background, bring it to the foreground with the "fg" command). Other trivial file operations will work likewise. For example, with the first terminal still reading, type

# date > /dev/xillybus_write_8

in the second telnet.

This works as long as both ends of the FIFO are connected to the core. Changes in the FPGA code will of course change the outcome of the operations described above.

Note that there is no risk for data overflow or underflow by the core on the FIFOs, since the core respects the hardware 'empty' and 'full' signals. When necessary, the Xillybus driver forces the application to wait until the FIFO is ready for I/O (by classic blocking = forcing the application to sleep).

There is another pair of device files with a FIFO inbetween, /dev/xillybus_read_32 and /dev/xillybus_write_32. These device files work with a 32-bit word granularity, and so does the FIFO in the FPGA. Attempting the same test as above will result in similar behavior, with one difference: All hardware I/O runs in chunks of 4 bytes, so when the input hasn't reached a round boundary of 4-byte chunks, the last byte(s) will remain untransmitted.

Host applications

There are four simple C programs in the "demoapps" the subdirectory of the application compilation kit. Their compiled binaries are in the distro's /usr/bin directory, which is in the default execution path, so these programs can be run directly from shell prompt on the Microblaze system. The source files are

  • Makefile -- This file contains the rules used my the "make" utility to crosscompile the four programs.
  • streamread.c  -- Read from a file, send data to standard output
  • streamwrite.c -- Read data from standard input, send to file
  • memread.c -- Read data after seeking. Demonstrates how to access a memory interface in the FPGA
  • memwrite.c -- Write data after seeking. Also demonstrates how to access a memory interface in the FPGA

The purpose of these programs is to show the correct coding style and serve as basis for writing custom applications. They are not individually documented here, as they are very simple and all follow common UNIX file access practice, for which there is plenty of tutorials on the web.

Note that these programs use the plain open(), read(), write() etc. functions rather than fopen(), fread(), fwrite() set, since the latter may cause unexpected behavior due to data caching in the C library level.

Execution

Let's redo the simple loopback example with two telnet connections with the C programs. After compiling, type in the first connection:

# streamread /dev/xillybus_read_8

This is the part that reads from the device file. Note that this executes the application in /usr/bin.

And then, in the second telnet:

# streamwrite /dev/xillybus_write_8

This will work more or less like with "cat", only "streamwrite" attempts to work on a character-by-character basis, and not wait for ENTER. This is done in a function called config_console(), which can be disregarded: It's there merely for the immediate effect of typing.

Memory interface

The memread and memwrite are more interesting, because they demonstrate something which can't be shown with simple command-line utilities: Accessing memory on the FPGA by seeking the device file. Note that in the evaluation kit, only xillybus_mem_8 is seekable. It's also the only device file which can be opened both for read and for write.

Before writing to the memory, let's look at its current situation 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

Your output may be different: It reflects the FPGA's RAM which may contain other values to begin with (most likely because of previous games with it).

The -C and -v flag told hexdump to use the output format shown. The "-n 32" part asked for the first 32 bytes only. The memory array is just 32 bytes long, so there is no point reading more.

A word about what happened: hexdump opened /dev/xillybus_mem_8 and read 32 bytes from it. Every seekable file starts at position zero by default when opened, so the output is the first 32 bytes in the memory array.

Now let's change the value of the memory at address 3 to 170 (0xaa in hex):

# memwrite /dev/xillybus_mem_8 3 170

And read the entire array again:

# 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 quite evidently, it worked. The memread programs demonstrates how to read individual bytes from the RAM. Its main interest is how it's written, though.

The magic in memwrite.c happens where it says "lseek(fd, address, SEEK_SET)". With this command, the FPGA's address is set, causing any subsequent reads or writes start from that position (and increment as the data flows).

If multithreading is employed in the target application, it's important to make sure some kind of mutex is held before the lseek() call and released only after all I/O, which expects a certain address, is finished. Unlike a regular memory access, the file-modeled access involves at least two operations, whose atomicity should be ensured with locks.

And just a final note: The current FPGA code involved a RAM, but if the RAM's interface is exchanged with registers, things get much better: This gives a simple way to set and read registers, and hence control the FPGA through basic file operations.

Next recommended reading: FPGA IP Core's signal API