01signal.com

Zugriff auf die Gerätedateien von Xillybus

Einführung

Diese Seite basiert auf dem Programmierhandbuch von Xillybus für Linux. Für eine umfassendere Betrachtung der folgenden Themen wird empfohlen, auf dieses Handbuch zurückzugreifen. Es gibt außerdem ein ähnliches Handbuch für Microsoft Windows.

Falls Sie den „Hello, World“-Test noch nicht durchgeführt haben, empfiehlt es sich, das zuerst zu tun.

Die Kommunikation mit dem IP-Core von Xillybus (IP core) erfolgt über Gerätedateien (device files) auf dem Host. Auf diese Gerätedateien wird wie auf eine reguläre Datei zugegriffen. Anders als bei einer normalen Datei repräsentieren Gerätedateien jedoch keine Daten, die auf einer Festplatte gespeichert sind: Das Lesen aus einer Gerätedatei und das Schreiben in eine Gerätedatei führt stattdessen zu I/O-Operationen.

Daher ist es möglich, auf die Gerätedateien von Xillybus mit praktisch allen Programmiersprachen zuzugreifen. Es ist ebenfalls möglich, Linux-Kommandozeilenprogramme für diesen Zweck zu verwenden. Man könnte sich also fragen, warum dieses Thema überhaupt behandelt werden muss: Wenn Sie wissen, wie man Dateien richtig liest und schreibt, wissen Sie auch, wie man mit Xillybus arbeitet.

In der Tat ist es möglich, Programme zu schreiben, die ohne detaillierte Kenntnis der API auf reguläre Dateien zugreifen. Für die Arbeit mit I/O reicht das jedoch nicht aus: Die Interaktion mit Hardware erzeugt Situationen, die bei einer regulären Datei selten auftreten. Als Programmierer muss man wissen, wie man diese Situationen mithilfe der API bewältigt.

Der weitaus größte Teil dieser Seite ist daher Themen gewidmet, die auch für den Zugriff auf reguläre Dateien relevant sind. Der Unterschied zu Gerätedateien besteht darin, dass sie Fehler bei der Verwendung der API weniger verzeihen.

Ein unzureichendes Verständnis der API kann zu zwei Hauptarten von Verwirrung führen:

Programmiersprachen und Betriebssysteme

Unter Linux funktioniert jedes Werkzeug oder jede Programmiersprache, die auf eine Datei zugreifen kann, auch mit Xillybus.

Unter Windows gibt es keine Probleme mit gängigen Programmiersprachen wie C, C++, C#, Python, Perl und allem, was mit Cygwin daherkommt. Einige Werkzeuge (z. B. MATLAB) weigern sich jedoch möglicherweise, mit den Gerätedateien von Xillybus zu arbeiten: Diese Werkzeuge erkennen, dass die Gerätedatei keine reguläre Datei ist, und werten das als Fehler. Es gibt Lösungen für diese Situation, die üblicherweise darin bestehen, Erweiterungen zu verwenden, die für die systemnahe I/O gedacht sind.

Die folgende Erörterung basiert auf der Sprache C, die Themen sind jedoch für alle Programmiersprachen relevant.

Beispielcode

Auf der Website von Xillybus stehen Codebeispiele zum Herunterladen bereit. Diese Beispiele sind in C geschrieben und zeigen, wie man die systemnahe API korrekt verwendet.

Um die Beispiele herunterzuladen, gehen Sie auf die Webseite, von der Sie das Demopaket (demo bundle) heruntergeladen haben (also die Seite für Xillybus oder die Seite für XillyUSB).

Wenn Sie Linux verwenden, laden Sie den Linux-Treiber herunter. Der Beispielcode ist in derselben .tar.gz-Datei enthalten.

Wenn Sie Windows verwenden, laden Sie das Xillybus-Paket für Windows herunter.

In beiden Fällen befindet sich der Beispielcode im Unterverzeichnis demoapps/. Beachten Sie, dass in diesem Code die Datenmenge, die bei jedem I/O-Vorgang gelesen oder geschrieben wird, klein ist, weil ein kleiner Puffer (128 Byte) reserviert wird. Das dient nur dazu, den Code einfacher zu gestalten. In einer realen Anwendung wird ein größerer Puffer empfohlen (32 kByte sind in der Regel eine gute Wahl).

Die Unterschiede zwischen Linux und Windows sind klein, aber wichtig. Insbesondere:

Abgesehen davon sind die Prinzipien hinter dem Beispielcode genau dieselben.

Gepufferte Datei-E/A

Programmiersprachen bieten normalerweise zwei getrennte APIs für den Zugriff auf Dateien an: eine High-Level-API und eine Low-Level-API. Die High-Level-API wird häufiger verwendet, weil sie einfacher zu handhaben ist.

In der Sprache C besteht die High-Level-API beispielsweise aus fopen(), fread(), fwrite(), fprintf(), fclose() usw. Die Low-Level-API besteht aus open(), read(), write(), close() usw. Der Unterschied zwischen diesen beiden APIs ist nicht nur ein kleiner Unterschied in den Funktionsnamen: Die High-Level-API stellt Puffer im Benutzerspeicher bereit. Diese Puffer werden von der C-Laufzeitbibliothek implementiert. Verwechseln Sie sie nicht mit DMA-Puffern, die vom Treiber im Kernel verwaltet werden.

Der wichtigste Unterschied ist das Verhalten von fwrite(): Das Ergebnis eines Aufrufs dieser Funktion kann sein, dass die Daten in einem Benutzerspeicher-Puffer gespeichert werden. Die Übertragung an das FPGA kann sich dadurch verzögern. Tatsächlich können die Daten unbegrenzt im Puffer von fwrite() verbleiben, bis die Datei geschlossen wird. Das kann wie ein Bug mit Xillybus aussehen, weil die Daten in die Datei geschrieben wurden und doch nichts passiert.

Es wird daher empfohlen, die systemnahe (nicht puffende) API zu verwenden. Das ist meist möglich, auch wenn die meisten Programmiersprachen die Verwendung der High-Level-API fördern. Wenn das Werkzeug oder die Programmiersprache keine Low-Level-API unterstützt, verwenden Sie stattdessen die verfügbare API. Dabei ist zu beachten, dass Sie keine Kontrolle darüber haben, wann die I/O stattfindet. Für viele Anwendungen, beispielsweise für die Datenerfassung (data acquisition), ist das dennoch gut genug.

Noch einmal: Gepufferte I/O darf nicht mit den Puffern von Xillybus verwechselt werden. Am wichtigsten ist, dass Daten wegen der Puffer von Xillybus niemals auf unbestimmte Zeit hängen bleiben. Das wird weiter unten im Zusammenhang mit dem Schreiben mit der Länge null erläutert.

Lesen aus einer Gerätedatei: Die Grundlagen

Der Beispielcode für das Lesen aus einer Gerätedatei ist streamread.c. Dieses Programm befindet sich im Verzeichnis demoapps/. Der Code, den ich unten zeige, stammt jedoch aus einem anderen Programm: memread.c (aus demselben Verzeichnis). Dieses Programm ist für einen anderen Zweck gedacht, enthält aber eine Funktion namens allread(). Es ist bequemer, einige Themen anhand dieser Funktion zu demonstrieren.

Nehmen wir an, eine Datei wurde mit diesem Befehl geöffnet:

int fd, len;
char *buf;

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

Nun wollen wir @len Bytes aus einer Datei in einen Puffer lesen. Dabei soll kein Teilergebnis akzeptiert werden: Wir wollen eine Funktion, die immer die benötigte Datenmenge liest.

Das macht allread(), wenn es so verwendet wird:

allread(fd, buf, len);

Diese Funktion ist wie folgt definiert:

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;
  }
}

Beachten Sie, dass diese Funktion immer die angeforderte Anzahl Bytes liest. Wenn das nicht möglich ist, führt die Funktion zum Programmabbruch. Das mag für ein normales Anwendungsszenario zu drastisch sein. allread() sollte als einfache Demonstration des Dateizugriffs mit der Low-Level-API betrachtet werden.

Nun erklären wir diese Funktion. Der erste interessante Teil ist dieser:

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

@received ist während der ersten Schleife gleich null. Diese Zeile ist also äquivalent zu:

rc = read(fd, buf, len);

read() versucht, @len Bytes aus dem Dateideskriptor (@fd) zu lesen und die Daten im Puffer (@buf) zu speichern.

Was eine Xillybus-Gerätedatei betrifft: read() wartet bis zu 10 ms, wenn die angeforderte Datenmenge (@len Bytes) nicht verfügbar ist. Nach dieser kurzen Zeitspanne kehrt die Funktion mit weniger Daten zurück als angefordert (aber mit mindestens einem Byte). Wenn überhaupt keine Daten vorhanden sind, wartet read() unbegrenzt, bis Daten vom FPGA eintreffen (es gibt jedoch Ausnahmen, dazu weiter unten mehr). Dieses Verhalten ist spezifisch für den Xillybus-Treiber (entspricht aber dennoch der Standard-API).

Wenn read() etwas lesen konnte, ist @rc gleich der Anzahl der gelesenen Bytes. Das bedeutet, dass @rc eine positive Zahl ist und alle if-Anweisungen in der Schleife übersprungen werden. Als Nächstes passiert also Folgendes:

received += rc;

@received enthält folglich immer die Gesamtzahl der bisher gelesenen Bytes. Beachten Sie, dass @rc durchaus kleiner als @len (die angeforderte Anzahl Bytes) sein darf; das ist völlig legal und normal.

Die while-Schleife läuft weiter, bis @received (die Gesamtzahl der gelesenen Bytes) @len erreicht. Das zeigt die while-Anweisung:

while (received < len) { ... }

Es werden also bei Bedarf weitere Daten gelesen:

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

Diesmal wird der Startpunkt im Puffer um @received verschoben. Die Anzahl der angeforderten Bytes wird ebenfalls um denselben Betrag reduziert. Diese Anpassungen spiegeln lediglich wider, dass es sich um einen erneuten Leseversuch handelt.

Wenn read() nichts liest

Bisher habe ich mich darauf konzentriert, was passiert, wenn read() Daten lesen kann. Es gibt jedoch drei Situationen, in denen read() nichts liest. Jede dieser Situationen wird durch eine eigene if-Anweisung behandelt.

POSIX-Signale

Wenn Sie STRG-C drücken, um ein Programm zu stoppen, sendet das Betriebssystem ein POSIX-Signal an den Prozess. Das ist der Mechanismus, der das Programm beendet. Dasselbe passiert, wenn Sie zu diesem Zweck den Befehl „kill“ verwenden. Es gibt aber auch viele andere Arten von Signalen, die in den meisten Situationen ignoriert werden sollten.

Was passiert also, wenn der Prozess mitten in einem Funktionsaufruf von read() ein Signal empfängt? Gemäß den Linux-Konventionen muss read() sofort die Kontrolle an das Hauptprogramm zurückgeben. Wenn read() vorher Daten lesen konnte, passiert nichts Ungewöhnliches: @rc enthält die Anzahl der Bytes, und es gibt keinen Hinweis darauf, dass ein Signal empfangen wurde.

Wenn jedoch keine neuen Daten eingetroffen sind, ist @rc eine negative Zahl, und @errno ist gleich EINTR. Die übliche Methode, mit dieser Situation umzugehen, ist im Code gezeigt: Tun Sie so, als wäre nichts passiert, und versuchen Sie es erneut.

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

Das bedeutet nicht, dass das Signal ignoriert wird: Wenn das Signal etwa daher rührt, dass der Benutzer STRG-C gedrückt hat, wird das Programm wie gewöhnlich beendet. Dafür gibt es einen anderen Mechanismus. Der Zweck dieser if-Anweisung ist es, Signale zu behandeln, die nichts Dramatisches auslösen sollen. Die continue-Anweisung stellt sicher, dass nichts Seltsames passiert, wenn der Prozess ein derartiges Signal empfängt.

Wenn der Prozess zum Beispiel mit STRG-Z angehalten wird, ist diese if-Anweisung notwendig, um sicherzustellen, dass das Programm weiterläuft, wenn die Ausführung fortgesetzt wird. Außerdem gibt es einige andere Signale, die ohne menschliches Zutun eintreffen können.

Ein echter Fehler

Natürlich kann beim Versuch, Daten zu lesen, etwas schiefgehen. Wenn das passiert, ist @rc eine negative Zahl, und der Wert von @errno ist etwas anderes als EINTR. Der Beispielcode meldet diesen Fehler einfach und beendet das Programm:

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

EOF

Wenn read() keine Daten liefern kann, weil das Ende der Datei erreicht wurde, gibt diese Funktion den Wert null zurück. Das gilt natürlich für eine reguläre Datei. Aber auch Xillybus kann erklären, dass der Datenstrom beendet ist. Das Verhalten ist dann dasselbe.

Der relevante Code ist dieser:

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

Auch das wird als Fehler behandelt und führt zum Programmabbruch: Es bedeutet nämlich, dass das EOF erreicht wurde, bevor die angeforderte Datenmenge (@len Bytes) gelesen wurde. Diese if-Anweisung kann nur erreicht werden, wenn @received kleiner als @len ist.

Erinnern Sie sich: Die Idee hinter allread() war, immer die benötigte Datenmenge zu lesen. Wenn das nicht möglich ist, stoppt diese Funktion das Computerprogramm.

Relevanz für andere Programmiersprachen

Der obige Beispielcode wurde in C geschrieben, er zeigt jedoch einige wichtige Punkte, die unabhängig von der Programmiersprache relevant sind.

Schreiben in eine Gerätedatei

Die Low-Level-API zum Schreiben in eine Datei ist fast dieselbe wie zum Lesen aus einer Datei. Als Demonstration dient die Funktion allwrite(), die in streamwrite.c zu finden ist:

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;
  }
}

Vergleichen Sie sie mit allread() weiter oben: Es gibt nur drei Unterschiede:

Grundsätzlich gibt es also keinen Unterschied zwischen Schreiben und Lesen.

Allerdings sollte @rc niemals null sein, denn ein EOF beim Schreiben in eine Datei ergibt keinen Sinn. Laut POSIX-Standard kann @rc nur dann null sein, wenn write() aufgerufen wird, um null Bytes zu schreiben. Das passiert in dieser while-Schleife jedoch nie.

Kurz gesagt: allwrite() schreibt immer die angeforderte Anzahl Bytes. Die einzige Alternative ist, den Prozess zu beenden. Nehmen wir also an, eine Gerätedatei wurde auf folgende Weise geöffnet:

int fd, len;
char *buf;

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

Das Schreiben von @len Bytes aus @buf geschieht folgendermaßen:

allwrite(fd, buf, len);

Alles, was oben über allread() gesagt wurde, gilt auch für allwrite(). Dies schließt die Relevanz für andere Programmiersprachen mit ein.

Schreiben mit der Länge null

Es ist zulässig, write() mit null Bytes aufzurufen. Die Standard-API sagt nicht, was in diesem speziellen Fall passieren wird. Offensichtlich bedeutet das, dass keine Daten geschrieben werden.

Ein derartiger Funktionsaufruf hat für eine Xillybus-Gerätedatei eine besondere Bedeutung: Das Schreiben von null Bytes bedeutet, einen Flush anzufordern. Um die Bedeutung davon zu verstehen, schauen wir uns zunächst an, was passiert, wenn Daten in eine Gerätedatei geschrieben werden.

Nehmen wir an, die Gerätedatei ist ein asynchroner Stream (asynchronous stream). Dieser Begriff wird auf einer anderen Seite kurz und in der Dokumentation ausführlicher erklärt.

Wenn Daten mit write() in eine Gerätedatei geschrieben werden, speichert der Xillybus-Treiber diese Daten in einem RAM-Puffer. Es ist möglich, dass einige oder alle dieser Daten sofort an das FPGA gesendet werden. Im Allgemeinen kann jedoch eine Datenmenge im Puffer verbleiben, während das Programm, das den Funktionsaufruf ausgeführt hat, dennoch fortfährt. Der Zweck dieses Mechanismus ist die Verbesserung der Leistung, insbesondere wenn es viele Funktionsaufrufe von write() gibt.

Wann werden also die Daten im RAM-Puffer des Treibers an das FPGA gesendet? Es gibt vier mögliche Situationen:

Die Daten bleiben also nie zu lange im Puffer des Treibers. Das liegt daran, dass die Daten immer innerhalb einer Zeitspanne von 10 ms an das FPGA gesendet werden. In manchen Anwendungen ist aber selbst diese Verzögerung nicht akzeptabel. In diesem Fall kann mit einem Schreibvorgang der Länge null angefordert werden, dass verbliebene Daten sofort gesendet werden.

So geht das in der Sprache C:

write(fd, NULL, 0);

Beachten Sie, dass die Adresse des Puffers NULL ist. Das ist in Ordnung, weil die Anzahl der zu schreibenden Bytes null ist. Dieser Funktionsaufruf garantiert jedoch nicht, dass die Anforderung erfolgreich ist. Auch wenn sie sehr wahrscheinlich erfolgreich ist, ist dies die korrekte Vorgehensweise:

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 dies wurde für einen asynchronen Stream gesagt. Wenn die Gerätedatei ein synchroner Stream (synchronous stream) ist, werden die Daten als Ergebnis eines write()-Aufrufs immer sofort an das FPGA gesendet. Außerdem wartet write(), bis die Daten das FPGA erreicht haben, bevor es zurückkehrt (ein Schreibvorgang der Länge null tut das nicht).

Ein Schreibvorgang mit der Länge null ist also nur für asynchrone Streams relevant. Dieses Feature sollte nicht unnötig verwendet werden, weil es die Kommunikation mit dem FPGA verlangsamt.

Zusammenfassung

Wie bereits erwähnt, gilt fast alles oben Geschriebene auch für den Zugriff auf eine beliebige Datei. Nur ein paar Themen waren spezifisch für Xillybus.

Es ist wichtig, diesen Richtlinien zu folgen, um ein konsistentes Verhalten bei der Kommunikation mit dem FPGA zu gewährleisten. Programme, die ohne Berücksichtigung dieser Themen geschrieben werden, werden wahrscheinlich gelegentlich ausfallen. Diese Ausfälle erscheinen oft als ein Problem mit dem FPGA oder dem Treiber. Gute Programmiertechniken ersparen einem also viel Verwirrung und unnötige Mühe.

Diese Seite wurde maschinell aus dem Englischen übersetzt. Im Zweifelsfall siehe den Originaltext.
Copyright © 2021-2026. All rights reserved. (dcc38493)