01signal.com

Accessing Xillybus' device files

Introduction

This page is based upon Xillybus' programming guide for Linux. For a more comprehensive view on the topics below, it's recommended to refer to this guide. There is also a similar guide for Microsoft Windows.

If you haven't done the "Hello, World" test yet, it's recommended to do that first.

The communication with the Xillybus IP core takes place by means of device files on the host. These device files are accessed like a regular file. However, unlike a normal file, a device file doesn't represent data that is stored on a disk: Reading and writing to a device file results in I/O operations instead.

Accordingly, it's possible to access Xillybus' device files with virtually all programming languages. It's also possible to use Linux command-line utilities for this purpose. So one may wonder why there is a need to discuss this topic at all: If you know how to correctly read and write to files, you know how to work with Xillybus.

Indeed, it's possible to write programs that access regular files without a detailed knowledge of the API. This is however not enough for working with I/O: The interaction with hardware creates situations that rarely happen with a regular file. As a programmer, it's necessary to know how to handle these situations with the help of the API.

The vast majority of this page is therefore dedicated to topics that are relevant for accessing regular files as well. The difference with device files is that they are less forgiving about mistakes when using the API.

A lack of understanding of the API may lead to mainly two kinds of confusions:

Programming languages and operating systems

With Linux, any tool or programming language that can access a file works with Xillybus.

When Windows is used, there is no problem with common programming languages, such as C, C++, C#, Python, Perl and anything that comes with Cygwin. However, some tools (e.g. MATLAB) may refuse to work with Xillybus' device files: These tools detect that the device file is not a regular file, and consider that as an error. There are workarounds for this situation, which are usually to use extensions that are intended for low-level I/O.

The discussion below is based upon the C language, however the topics are relevant for all programming languages.

Example code

There are coding examples available for download at Xillybus' website. These examples are written in C, and show how to use the low-level API correctly.

In order to download the examples, go to the web page that you downloaded the demo bundle from (i.e. the page for Xillybus or for the page for XillyUSB).

If you're using Linux, download the Linux driver. The example code is included in the same .tar.gz file.

If you're using Windows, download the Xillybus package for Windows.

Either way, the example code is inside the demoapps/ subdirectory. Note that in this code, the amount of data that is read or written in each I/O operation is small, because a small buffer is allocated (128 bytes). The purpose of this is only to make the code simpler. In a real application, a larger buffer is recommended (32 kBytes is usually a good choice).

The differences between Linux and Windows are small but important. In particular:

That said, the principles behind the example code are exactly the same.

Buffered file I/O

Programming languages usually offers two separate APIs for accessing files: One high-level API, and one low-level API. The high-level API is more commonly used, because it's easier to work with.

For example, in the C language, the high-level API consists of fopen(), fread(), fwrite(), fprintf(), fclose() etc. The low-level API consists of open(), read(), write(), close() etc. The difference between these two APIs is not just a small difference in the functions' names: The high-level API provides user-space RAM buffers. These buffers are implemented by the C run-time library. Don't confuse them with DMA buffers, which are controlled by the driver in the kernel.

The most important difference is the behavior of fwrite(): The result of a call to this function can be that the data is stored in a user-space buffer. The transmission to the FPGA may be delayed until later. In fact, the data can remain in fwrite()'s buffer indefinitely, until the file is closed. This can look like a bug with Xillybus, because the data was written to the file, and yet nothing happened.

It's therefore recommended to use the low-level (non-buffering) API. This is usually possible, even though most programming languages promote the use of the high-level API. If the tool or programming language doesn't support a low-level API, use the API that is available instead. In this case, it's important to remember that there is no control on when the I/O takes place. This is nevertheless good enough in many applications, for example for data acquisition.

Once again, buffered I/O should not be confused with Xillybus' buffers. Most importantly, the data will never get stuck indefinitely because of Xillybus' buffers. This is explained further below in the context of zero-length write().

Reading from a device file: The basics

The example code for reading from a device file is streamread.c. This program can be found in the demoapps/ directory. However, the code that I'm going to show below comes from a different program: memread.c (from the same directory). This program is intended for a different purpose, but it contains a function that is named allread(). It's more convenient to demonstrate a few topics with this function.

Let's say that a file has been opened with this command:

int fd, len;
char *buf;

fd = open("/dev/xillybus_read_32", O_RDONLY);

Now we want to read @len bytes from a file into a buffer. But no partial result should be allowed: We want a function that always reads the required amount of data.

This is what allread() does, when it's used like this:

allread(fd, buf, len);

This function is defined as follows:

void allread(int fd, unsigned char *buf, int len) {
  int received = 0;
  int rc;

  while (received < len) {
    rc = read(fd, buf + received, len - received);

    if ((rc < 0) && (errno == EINTR))
      continue;

    if (rc < 0) {
      perror("allread() failed to read");
      exit(1);
    }

    if (rc == 0) {
      fprintf(stderr, "Reached read EOF\n");
      exit(1);
    }

    received += rc;
  }
}

Note that this function always reads the requested number of bytes. If this is not possible, the function causes the program to terminate. This might be too drastic for a normal usage scenario. allread() should be treated as a simple demonstration of how to access a file with the low-level API.

Let's explain this function. The first interesting part is this:

rc = read(fd, buf + received, len - received);

@received is equal to zero during the first loop. So this row is equivalent to this:

rc = read(fd, buf, len);

read() attempts to read @len bytes from the file descriptor (@fd) and store the data in the buffer (@buf).

As for a Xillybus device file: read() waits for up to 10 ms if the requested amount of data (@len bytes) is not available. After this short time period, the function returns with less data than required (but at least one byte). If there is no data at at all, read() waits indefinitely until data arrives from the FPGA (but there are exceptions, more on this below). This behavior is specific to Xillybus' driver (but is nevertheless compliant with the standard API).

If read() was able to read something, @rc equals the number of bytes that were read. This means that @rc is a positive number, and all if-statements in the loop are skipped. So this happens next:

received += rc;

Consequently, @received always contains the total number of bytes that have been read so far. Note that it's perfectly legal and normal that @rc is smaller than @len (the number of bytes that were requested).

The while-loop continues until @received (the total number of bytes that have been read) reaches @len. This is reflected in the while statement:

while (received < len) { ... }

So more data is read if necessary:

rc = read(fd, buf + received, len - received);

This time, the starting point in the buffer is moved with @received. The number of requested bytes is also reduced with the same number. These adjustments merely reflect that this is a repeated attempt to read data.

When read() doesn't read anything

So far, I've focused on what happens when read() is able to read data. There are however three situations where read() doesn't read anything. Each of these situations is handled by its own if-statement.

POSIX signals

When you press CTRL-C to stop a program, the operating system sends a POSIX signal to the process. This is the mechanism that makes the program terminate. The same things happens when you use the "kill" command for the same purpose. But there are also a lot of other types of signals which should be ignored in most situations.

So what happens if the process receives a signal in the middle of a function call to read()? According to Linux' conventions, read() must return control to the main program immediately. If read() was able to read data before this happened, nothing unusual happens: @rc will contain the number of bytes, and there will be no indication that a signal has been received.

But if no new data has arrived, @rc will be a negative number, and @errno will equal EINTR. The standard way to handle this situation is as shown in the code: Behave as if nothing happened, and try again.

if ((rc < 0) && (errno == EINTR))
  continue;

This doesn't mean that the signal is ignored: For example, if the reason for the signal was that the user pressed CTRL-C, the program will terminate as usual. There is another mechanism that takes care of that. The purpose of this if-statement is to handle signals that are not intended to cause anything dramatic. The continue-statement ensures that nothing weird will happen if the process receives a signal of this sort.

For example, if the process is stopped with CTRL-Z, this if-statement is necessary to ensure that the program continues running when the execution is resumed. In addition, there are several other signals that can arrive without any human intervention.

A real error

Quite naturally, something can go wrong while attempting to read data. When this happens, @rc will be a negative number, and @errno's value will be something else than EINTR. The example code simply reports this error, and terminates the program:

if (rc < 0) {
  perror("allread() failed to read");
  exit(1);
}

EOF

If read() can't supply any data because the end of the file has been reached, this function returns the value zero. This is of course true for a regular file. But also Xillybus has the capability to declare that the data stream has ended. The behavior is the same.

The relevant code is this:

if (rc == 0) {
  fprintf(stderr, "Reached read EOF\n");
  exit(1);
}

This too is treated as an error, and causes the termination of the program: It means that the EOF was reached before the requested amount of data (@len bytes) was read. This if-statement can be reached only if @received is smaller than @len.

Recall that the idea behind allread() was that it always reads the required amount of data. If this isn't possible, this function stops the computer program.

Relevance to other programming languages

The example code above was written in C, but it demonstrates a few important points that are relevant regardless of the programming language.

Writing to a device file

The low-level API for writing to a file is almost the same as reading from a file. To demonstrate this, this is the function named allwrite(), which can be found in streamwrite.c:

void allwrite(int fd, unsigned char *buf, int len) {
  int sent = 0;
  int rc;

  while (sent < len) {
    rc = write(fd, buf + sent, len - sent);

    if ((rc < 0) && (errno == EINTR))
      continue;

    if (rc < 0) {
      perror("allwrite() failed to write");
      exit(1);
    }

    if (rc == 0) {
      fprintf(stderr, "Reached write EOF (?!)\n");
      exit(1);
    }

    sent += rc;
  }
}

Compare it with allread() that is shown above: There are only three differences:

So in principle, there is no difference between writing and reading.

Having said that, note that @rc should never be zero, because there is no meaning to an EOF when writing to a file. According to the POSIX standard, @rc can only be zero when write() was requested to write zero bytes. But this never happens in this while loop.

To summarize, allwrite() always writes the number of bytes that are requested. The only alternative is to terminate the process. In other words, suppose that a device file has been opened in the following way:

int fd, len;
char *buf;

fd = open("/dev/xillybus_write_32", O_WRONLY);

Writing @len bytes from @buf is done as follows:

allwrite(fd, buf, len);

Everything said above about allread() applies to allwrite() as well. This includes the relevance to other programming languages.

Zero-length write

It is allowed to make a function call to write() with zero bytes. The standard API doesn't say what will happen in that specific case. But obviously, this means that no data will be written.

A function call of this sort has a special meaning for a Xillybus device file: Writing zero bytes means to request a flush. To understand the meaning of this, let's first look at what happens when data is written to a device file.

Let's assume that the device file is an asynchronous stream. This term is explained briefly on a different page, and in more detail in the documentation.

When data is written to a device file (with write() ), the Xillybus driver stores this data in a RAM buffer. It's possible that some or all of this data is sent to the FPGA immediately. But generally speaking, an amount of data may remain in the buffer, and the program that performed the function call continues nonetheless. The purpose of this mechanism is to improve performance, in particular if there are many function calls to write().

So when is the data in the driver's RAM buffer sent to the FPGA? There are four possible situations:

So the data is never stuck in the driver's buffer for too long. This is because the data is always sent to the FPGA within a time period of 10 ms. But in some applications, even this delay isn't acceptable. In that case, a zero-length write() can be used to request that any remaining data is sent immediately.

This is how to do this in the C language:

write(fd, NULL, 0);

Note that the address to the buffer is NULL. This is OK, because the number of bytes to write is zero. But this function call doesn't guarantee that the request is successful. Even though it's very likely to succeed, this is the correct way to do it:

while (1) {
  rc = write(fd, NULL, 0);

  if ((rc < 0) && (errno == EINTR))
    continue; // Interrupted. Try again.

  if (rc < 0) {
    perror("flushing failed");
    break;
  }

  break; // Flush successful
}

All this was said for an asynchronous stream. If the device file is a synchronous stream, the data is always sent to the FPGA immediately as a result of a function call to write(). In addition to that, write() waits until the data has reached the FPGA before returning (a zero-length write() doesn't do that).

So a zero-length write() is only relevant for asynchronous streams. This feature should not be used unless necessary, because it slows down the communication with the FPGA.

Summary

As already mentioned, almost everything written above is correct for accessing any file. Only a couple of topics were specific to Xillybus.

It's important to follow these guidelines in order to ensure consistent behavior while communicating with the FPGA. Programs that are written without considering these topics are likely to have occasional failures. These failures often appear to be a problem with the FPGA or the driver. Proper programming techniques will hence save a lot of confusion and unnecessary efforts.

Copyright © 2021-2024. All rights reserved. (b4b9813f)