Schnittstelle der Library

Die Library setzt sich aus mehreren Modulen zusammen, die jeweils einer C-Datei entsprechen. Im Allgemeinen sind die Module relativ abgeschlossene Einheiten, die dem Programmierer den Zugriff auf einer bestimmten Ebene oder auf eine eng umrissene Funktionalität erlauben. Sie werden in den folgenden Abschnitten einzeln kurz skizziert.

Viele Eigenschaften sind bei allen Funktionen gleich. Zeiten werden immer in Mikrosekunden angebeben, die meisten Referenzparameter können auf NULL gesetzt werden, wenn das Ergebnis nicht von Bedeutung ist. Fast alle Funktionen liefern 0 beziehungsweise einen gültigen Zeiger zurück, wenn sie erfolgreich ausgeführt werden. Fehlerursachen werden dem Aufrufer über errno signalisiert, wie es auch bei der libc üblich ist.

Modul firmware

Auf unterster Ebene enthält das Modul firmware Konstanten und Funktionen, die bei der Kommunikation mit dem Mikrocontroller auf einer Leistungselektronik hilfreich sind.

  • Einige Konstanten decken die Parameter der seriellen Kommunikation (Baudrate, Magic, Befehlscodes, Größen der Pakete) ab, andere beschreiben die Adressen der Fehlerzähler im EEPROM oder wichtige Eckdaten im Timing der Firmware.
  • Eine Gruppe von Makros und Funktionen dient dazu, die Pakete des seriellen Protokolls auszuwerten. Opcodes können erstellt, zerlegt und umgeformt werden. Andere Funktionen berechnen und prüfen die Checksummen des Paketes oder dessen Inhalt auf Plausibilität.
  • Der Rest des Codes hilft bei der zeitlichen Ablaufsteuerung. Zwei Routinen warten für eine definierte Zeit (mit Suspendierung des Prozesses oder per Busy Waiting), ein Makro berechnet, wieviel Zeit zwischen zwei Aufrufen von gettimeofday vergangen ist.

Der Benutzer kommt mit dieser Funktionalität kaum direkt in Kontakt, sind für ihn vor allem die Konstanten für das Timing oder die Adressen der Fehlerzähler relevant.

Modul nodelink

Die gemeinsame Schnittstelle aller Nodelinks wird in dem gleichnamigen Modul definiert. Eine Struktur dient als Handle und enthält die nötigen Daten für alle Typen von Nodelinks. Das Modul definiert selber keine Routine zum Öffnen der Verbindung und Initialisieren der Struktur, dafür sind die spezialisierten Varianten in anderen Modulen zuständig. Der einmal erstellte Nodelink kann von den übrigen Funktionen dieses Moduls benutzt werden, um unabhängig von der Art des Links zu kommunizieren und die Verbindung wieder schließen zu können.

  int nodelink_close(struct nodelink *link);
  int nodelink_send(struct nodelink *link, unsigned char *request,
                    unsigned size);
  int nodelink_receive(struct nodelink *link, unsigned char *reply,
                       unsigned *size, unsigned opcode, unsigned timeout);
  int nodelink_command(struct nodelink *link,
                       unsigned char *request, unsigned requestsize,
                       unsigned char *reply, unsigned *replysize);

Die Funktion nodelink_send überträgt ein vorbereitetes Paket an die Leistungselektronik. Ihr Gegenstück nodelink_receive empfängt die Antwort mit einstellbarem Timeout. Das Paket muß den vorgegebenen Opcode enthalten, anderenfalls wird es verworfen. Dieser Mechanismus dient dazu, nur Antworten mit der korrekten Sequenznummer zu akzeptieren. Der Aufrufer kann auch OPCODE_ANY übergeben, in dem Fall wird jedes Paket akzeptiert. Timeouts und andere Fehler werden über den Rückgabewert angezeigt.

Häufig wird die Applikation nacheinander senden und empfangen wollen. Dafür existiert als Abkürzung die Funktion nodelink_command, die als Vereinfachung außerdem weniger Parameter benötigt. Opcodes und Paketgrößen werden aus dem zu sendenden Paket ermittelt, der Timeout ergibt sich aus dem Standardwert für die Verbindung, wie er im Nodelink gespeichert ist.


Verbundene Steuerknoten unter der Anlage

Auf einigen Netzwerken wie dem CAN-Bus sind nur sehr kleine Pakete erlaubt. Wenn eine serielle Schnittstelle auf der anderen Seite geöffnet werden soll, ist ihr vollständiger Name meistens zu lang, um in ein Paket zu passen. Daher wird statt des Namens ein entsprechender Code übermittelt. Zwei Funktionen bilden Code und Name aufeinander ab, die Liste der definierten Namen ist statisch im Modul gespeichert.

  int nl_devicecode(char *device, unsigned char *code);
  char *nl_devicename(unsigned char devicecode);

Es werden mit /dev/ttyS0 bis /dev/ttyS3 alle Schnittstellen der PC104-Rechner unterstützt.

Module nltty, nludp und nlcan

Die Kommunikationsroutinen der unterschiedlichen Nodelinks sind jeweils in eigenen Modulen implementiert. Diese enthalten neben Funktionen zur Ansteuerung des Mediums jeweils die vier Basisroutinen des Nodelinks (open, close, send und receive). Jede Kommunikation beginnt mit dem Öffnen der Verbindung.

  struct nodelink *nodelink_tty_open(char *device);
  struct nodelink *nodelink_udp_open(char *hostname, char *remotedevice);
  struct nodelink *nodelink_can_open(struct pcan_interface *iface,
                                     unsigned remotenode,
                                     char *remotedevice);

Die Parameter hängen von dem Medium ab. Bei lokalen seriellen Schnittstellen (tty) muß nur der Gerätename übergeben werden, Ethernet-Verbindungen (udp) erfordern zusätzlich den Namen des entfernten Rechners. Nodelinks über CAN können nur geöffnet werden, nachdem ein Handle für das lokale CAN-Interface zur Verfügung steht, zusätzlich werden die Nummer des entfernten Rechners und der Name der seriellen Schnittstelle auf der anderen Seite übergeben. Die so geöffneten Nodelinks können für die weitere Kommunikation benutzt werden.

  int nodelink_*_close(struct nodelink *link);
  int nodelink_*_send(struct nodelink *link, unsigned char *request,
                      unsigned size);
  int nodelink_*_receive(struct nodelink *link, unsigned char *reply,
                         unsigned *size, unsigned opcode, unsigned timeout);

Es ist egal, ob die Funktionen zum Senden, Empfangen und Schließen des eigenen Moduls oder die Gegenstücke aus dem Modul nodelink benutzt werden. Der Vorteil der zweiten Methode liegt darin, daß es für sie keine Rolle spielt, wie der Nodelink ursprünglich erzeugt wurde.

Kommunikation auf den Netzwerken

Die Verwaltung der Verbindungen auf den Bussystemen Ethernet und CAN verläuft nach einem einfachen Schema. Zum Öffnen und Schließen einer seriellen Schnittstelle auf der anderen Seite wird ein Paket mit dem Code des Devices gesendet. Der betreffende Rechner antwortet mit einem Resultatcode, der den Erfolg oder eventuelle Fehler signalisiert.

Die eigentlichen Datenpakete werden unverändert und nicht eingekapselt in beide Richtungen übertragen. Tritt auf dem entfernten Rechner ein Fehler auf, sendet er ein Paket mit einem entsprechenden Fehlercode. Das Protokoll unterscheidet Nachrichten anhand des ersten Bytes, Datenpakete beginnen mit dem Magic-Wert 0xF0, andere mit entsprechenden eigenen Nummern.

Die Kommunikation über Ethernet basiert auf UDP, der Server auf den PC104-Rechnern ist über Port 8250 erreichbar. CAN arbeitet bei 250 KBit/s, Verbindungen zu Rechner n werden durch Befehle an die CAN-ID n geöffnet und geschlossen. Der Server weist jeder Verbindung eine freie ID zu, unter welcher der Austausch der Datenpakete erfolgt.

Module nlsim und nllog

Wie anpassungsfähig das beschriebene Konzept ist, zeigen zwei spezielle Module. Die von nlsim erzeugten Nodelinks führen nicht zu echter Hardware sondern zu einem im Modul integrierten Simulator, der sich wie eine Leistungselektronik ohne Peripherie verhält. Programme können auf diese Weise die Kommunikation testen, ohne auf echte Hardware angewiesen zu sein.

  struct nodelink *nodelink_sim_open(struct nodesim *sim, unsigned delay);

Die Ausführungszeit der Befehle wird bei der Erstellung des Nodelinks angegeben. Sie beträgt in der Regel 10000 Mikrosekunden plus die Laufzeit durch eventuelle nachzuahmende Netzwerke.

Ein ähnlich wichtiges Werkzeug bietet das Modul nllog. Es kapselt einen bestehenden Nodelink in einen Mechanismus ein, der transparent alle durch ihn laufenden Pakete protokolliert.

  struct nodelink *nodelink_log_open(char *logfile, char *infotext,
                                     int mode, struct nodelink *tolog);

Die Ausgabe erfolgt in eine Textdatei, in der jedes Paket in einer Zeile dargestellt wird. Davor stehen ein Zeitstempel und ein frei wählbarer Infotext. Der Detailgrad kann auf mehreren Stufen bis hin zu textuell interpretierten Inhalten eingestellt werden.

Modul nodeapi

Das Erstellen von Befehlspaketen und das Interpretieren der Antworten sind lästige Aufgaben, die das Modul nodeapi dem Programmierer abnehmen kann. Seine Funktionen entsprechen genau dem API der Firmware. Dabei sind jedem Befehlscode drei Routinen zugeordnet, wie das folgende Beispiel für CMDCONNECT zeigt.

  int node_connect_send(struct nodelink *link, int reset);
  int node_connect_recv(struct nodelink *link, unsigned *channel,
                        unsigned timeout);
  int node_connect(struct nodelink *link, int reset, unsigned *channel);

Die erste Funktion erzeugt ein Befehlspaket und sendet es über den Nodelink. Der Parameter reset entspricht dem Flag im Paket und gibt an, ob die Peripherie zurückgesetzt werden soll. Das Antwortpaket wird von der zweiten Funktion empfangen und ausgewertet, die Routine setzt channel auf die übermittelte Kanalnummer. Die dritte der genannten Funktionen führt beides nacheinander aus. Dabei wird die im Nodelink gespeicherte Zeitschranke als Timeout benutzt.

Diese Aufteilung erlaubt es Programmen, auf zwei unterschiedliche Arten zu kommunizieren. Zunächst können sie mit einem Aufruf der dritten Funktion einen kompletten Befehl ausführen lassen. Dies ist vor allem dann interessant, wenn nur eine Leistungselektronik angesprochen werden soll. Bei vielen Nodelinks dauert es zu lange, den Vorgang mehrmals zu wiederholen. In solchen Fällen ist es sinnvoller, erst alle Befehle zu senden und dann die Antworten zu sammeln. Für diese Aufgabe sind die ersten beiden Funktionen konzipiert, die nebenläufige Ausführung mehrerer Befehle dauert kaum länger als ein einzelner Befehl.

Das Modul übernimmt intern auch die Verwaltung der Sequenznummern. Ein Eintrag in der Nodelink-Struktur ist ein Zähler, der mit jedem gesendeten Paket inkrementiert wird. Beim Empfang der Antwort wird der gespeicherte Zähler als Referenz benutzt. Die Anwendungen müssen daher die Befehle zum Senden und Empfangen alternierend benutzen, was auch genau der intuitiven Reihenfolge entspricht. Zu jedem der elf API-Befehle existieren in dem Modul die drei Funktionen, dazu kommen einige Konstanten für Signalfarben, Motormodi und ähnliches, mit denen Programme lesbarer gestaltet werden können.

Module kicking und oval

Die bisher vorgestellten Module reichen für eine einfache Steuerung der Bahn bereits aus. Das Programm kann einen Nodelink zu jedem beliebigen Steuerrechner unter der Bahn aufbauen und über die entsprechenden Befehle die Peripherie kontrollieren. Dabei stellt sich allerdings die Frage, welche Bauteile auf welchem Weg zu erreichen sind, denn deren Verkabelung folgt keinem berechenbaren Schema. Die Leitungen von der Oberseite der Bahn sind immer mit dem erstbesten Anschluß der nächstgelegenen Leistungselektronik verbunden.


Eine der beiden Kreuzungsweichen

Die Tabelle gibt die Zuordnungen der Anschlüsse auf die Peripherie an und zeigt auch, welche serielle Schnittstelle welches Rechners jeweils verantwortlich zeichnet. Sie liegt in Form einer CSV-Datei vor und kann so automatisch ausgewertet werden. Ein Skript generiert aus den Beschreibungen der Anlage und des Testovals die beiden Module kicking und oval.

Die kleinsten Bestandteile der Beschreibung sind die Mapping-Arrays. Jeder Eintrag in ihnen entspricht einer Zeile der CSV-Datei und damit einem Peripherieteil. Die Felder enthalten die Nummern der Leistungselektronik und des Anschlusses (node, connector) sowie die Bezeichnung auf dem Gleisplan (block, device).

  struct railway_mapping {
    unsigned node;
    unsigned connector;
    int block;
    int device;
  };

Die Header-Dateien der Module deklarieren Konstanten mit den Blocknamen, im Programmcode wird eine Struktur zur Beschreibung der Peripherie in mehreren Schritten definiert. Ihre Felder geben einige Eckdaten wie die Anzahl der Leistungselektroniken und die textuellen Namen der Blöcke an. Wichtiger sind jedoch die Variablen, welche die Anzahlen der Peripheriegeräte und die dazugehörenden Mapping-Arrays enthalten.

  struct railway_hardware {
    unsigned numnodes;
    unsigned numsignals;
    struct railway_mapping *signalmapping;
    unsigned numcontacts;
    struct railway_mapping *contactmapping;
    unsigned numtracks;
    struct railway_mapping *trackmapping;
    unsigned numpoints;
    struct railway_mapping *pointmapping;
    unsigned numlights;
    struct railway_mapping *lightmapping;
    unsigned numgates;
    struct railway_mapping *gatemapping;
    unsigned numgatesensors;
    struct railway_mapping *gatesensormapping;
    unsigned numbells;
    struct railway_mapping *bellmapping;
    unsigned numgatesignals;
    struct railway_mapping *gatesignalmapping;
    unsigned numblocks;
    char **blocknames;
  };

Will ein Programm beispielsweise auf das Signal am Ausgang von OC_ST_1 zugreifen, sucht es dazu im Mapping-Array signalmapping den Eintrag mit block=OC_ST_1 und device=1. Dabei werden maximal numsignals Einträge geprüft, bis der passende gefunden ist. Dessen Felder node und connector legen fest, an welcher Leistungselektronik und an welchem Ausgang das Signal angeschlossen ist. Jetzt muß nur noch der richtige Nodelink benutzt werden, um die Einstellung des Signals zu ändern oder auszulesen. Diesen Schritte muß der Programmierer nur selten selbst implementieren, normalerweise übernehmen die höheren API-Ebenen diese Aufgabe.

Modul railway

In der höchsten Hierarchiestufe der Library ist schließlich das Modul railway angesiedelt. Es abstrahiert von Nodelinks, Mapping-Arrays und ähnlichen Strukturen, um dem Benutzer einen möglichst einfachen Zugriff auf die Modellbahn zu ermöglichen. Es müssen nur am Anfang die Hardwarebeschreibung registriert und die Nodelinks angelegt werden, ab dann übernimmt ein im Hintergrund laufender Thread die Kommunikation mit der Hardware. Das Hauptprogramm arbeitet mit Funktionen, die ausschließlich auf ein Speicherabbild der Hardware zugreifen. Der Thread versendet laufend Befehlspakete vom Typ CMDGLOBAL, um den gespeicherten Status der Aktoren zu den Leistungselektroniken zu senden und bekommt im Austausch den Zustand der Sensoren für das Speicherabbild. Der Benutzer muß so sich nicht um die Kommunikation, das Timing oder die Reaktion auf Fehler kümmern. All diese Aufgaben werden transparent von dem Modul übernommen.

Verwaltung des Systems

Vor der Benutzung der Routinen muß eine Applikation die Datenstrukturen initialisieren. Dabei wird die Hardwarebeschreibung übergeben, hierbei handelt es sich um einen Zeiger auf eine der Strukturen kicking oder oval aus den gleichnamigen Modulen.

  struct railway_system *railway_initsystem(struct railway_hardware *hardware);

Als nächstes werden die Nodelinks zu allen an der Steuerung beteiligten Leistungselektroniken geöffnet und eingetragen. Dies kann manuell geschehen, dann müssen die Nodelinks nur noch mit der folgenden Funktion registriert werden.

  int railway_setlink(struct railway_system *railway, unsigned node,
                      struct nodelink *link);

Das Modul kann diese Aufgabe aber auch automatisch durchführen, solange nur ein Bussystem für die Steuerung benutzt werden soll. Eine Funktion stellt Nodelinks über den CAN-Bus her, es müssen nur der Name des lokalen CAN-Interfaces und der Name der verwendeten seriellen Schnittstelle auf den PC104-Knoten übergeben werden. railway_openlinks_udp funktioniert genauso, hier wird statt dem Namen des CAN-Interfaces ein Formatstring für den Hostnamen benutzt. Er muß an irgendeiner Stelle den Platzhalter %i enthalten, an seiner Stelle wird die Nummer des Knotens eingesetzt. Der String node%02i expandiert zu node00 und so weiter.

  int railway_openlinks_can(struct railway_system *railway, char *candevice,
                            char *device);
  int railway_openlinks_udp(struct railway_system *railway,
                            char *hostformat, char *device);

Wurden alle Nodelinks erfolgreich angelegt und registriert, startet railway_startcontrol das gesamte System. Der Funktion werden Grenzwerte für die Dauer einer Kommunikationsrunde übergeben. Ab diesem Moment kann die Hardware normal angesteuert werden.

  int railway_startcontrol(struct railway_system *railway,
                           unsigned mincycle, unsigned maxcycle);

Ob die Steuerung normal arbeitet oder durch nicht behebbare Fehler zusammengebrochen ist, läßt sich mit einem Aufruf der Funktion railway_alive erfragen.

  int railway_alive(struct railway_system *railway);

Nachdem das Programm seine Aufgabe beendet hat, fährt railway_stopcontrol die Steuerung wieder herunter. Auf Wunsch wird dabei die gesamte Peripherie zurückgesetzt.

  int railway_stopcontrol(struct railway_system *railway, int reset);

Die noch offenen Nodelinks kann der Aufrufer mit int railway_closelinks schließen lassen oder dies selbst übernehmen, falls er die Links selbst geöffnet hatte.

  int railway_closelinks(struct railway_system *railway);

Zum Abschluß wird die Datenstruktur des Interfaces aufgelöst und der Speicher freigegeben.

  int railway_donesystem(struct railway_system *railway);

Ansteuerung der Hardware

Eine Vielzahl von Funktionen steuert die Peripherieteile der Modellbahn. Dieser Abschnitt greift die wichtigsten exemplarisch heraus, im Modul sind noch wesentlich mehr vorhanden.

Zum Setzen eines Signals werden die Adresse in Form von Blockname und Nummer angegeben, dazu kommt eine Bitmaske mit den Signalfarben. Sie setzt sich aus den drei Konstanten RED, YELLOW und GREEN zusammen.

  void setsignal(struct railway_system *railway, int block, int signal,
                 int lights);

Ähnlich wird der Fahrstrom für einen Block geschaltet. Der Parameter mode wird auf OFF, FWD, REV oder BRAKE gesetzt, target gibt die Zielgeschwindigkeit an. Der Wert besteht aus einer der beiden Konstanten PWM (festes PWM-Tastverhältnis) und SPEED (Geschwindigkeitsregler aktiv) plus einem Wert von 0 bis 127.

  void settrack(struct railway_system *railway, int track, unsigned mode,
                unsigned target);

Die Kontakte werden über zwei Funktionen abgefragt. Mit getcontact kann ein bestimmter Kontakt ausgelesen werden, sie liefert dann einen der vier Werte NONE (nicht ausgelöst), FWD (wurde vorwärts von einem Zug überquert), REV (rückwärts) und UNI (ausgelöst, aber keine Richtung feststellbar) zurück. Ist der Parameter clear nicht Null, wird das Ereignis gleichzeitig aus dem Eingangspuffer gelöscht.

  unsigned getcontact(struct railway_system *railway, int block,
                      int contact, int clear);

Die Funktion scancontact funktioniert genauso, ihr kann für block und contact aber jeweils -1 übergeben werden. In diesem Fall sucht die Routine den ersten ausgelösten Kontakt, liefert dessen Zustand zurück und setzt die Parameter block und contact auf die richtige Adresse. Die Funktion eignet sich also besonders dafür, auf Ereignisse zu reagieren, die irgendwo in der Anlage aufgetreten sind.

  unsigned scancontact(struct railway_system *railway, int *block,
                       int *contact, int clear);

Die Kontakte dürfen nicht zu selten abgefragt werden. Ereignisse werden zwar lange gepuffert, dafür speichert das System aber nur die Richtung der aktuellsten Auslösung. Ältere liefern als Richtung grundsätzlich nur UNI.

Weichen kennen zwei Zustände, STRAIGHT und BRANCH. Sie können per setpoint für jeden Antrieb auf der ganzen Anlage gesetzt werden.

  void setpoint(struct railway_system *railway, int point, int state);

Ob sich ein Triebwagen auf einem Gleisabschnitt befindet, ermittelt die Funktion trackused. Falls dies der Fall ist, liefert getspeed auch dessen aktuelle Geschwindigkeit.

  int trackused(struct railway_system *railway, int track);
  unsigned getspeed(struct railway_system *railway, int track);

Die meisten Funktionen unterstützen Wildcards, um mehrere Bauteile gleichzeitig zu verändern. So schaltet beispielsweise der folgende Befehl alle Signale in allen Blöcken auf Rot.

  setsignal(railway,-1,-1,RED);

Diagnosefunktionen

Eine Gruppe von Funktionen des APIs beschäftigt sich mit der Diagnose von Systemfehlern, dazu werden die Fehlerzähler der Leistungselektroniken im Rahmen eines Diagnosezyklus gelesen und später ausgewertet. Den Zyklus leitet railway_diagnostics ein, das System muß dafür bereits online sein. Die normale Steuerung pausiert für die Dauer der Ausführung, die Steuerung sollte sich dann in einem sicheren Betriebszustand befinden.

  int railway_diagnostics(struct railway_system *railway, int function);

Als Diagnosefunktionen kommen DIAG_STUCKCONTACTS (festhängende Kontakte ermitteln und speichern), DIAG_DOWNLOADEEPROM (EEPROM auslesen und speichern) sowie DIAG_CLEAREEPROM (EEPROM zurücksetzen) in Frage. Vier Funktionen werten die so gesammelten Daten aus.

  int diagstuckcontact(struct railway_system *railway, int *block,
                       int *contact, int clear);
  int diagfailedcontact(struct railway_system *railway, int *block,
                        int *contact, int *first, int *second, int clear);
  int diagshutdowntrack(struct railway_system *railway, int *block,
                        int *count, int clear);
  int diagresetnode(struct railway_system *railway, int *node,
                    int *mclr, int *wdt, int *bod, int clear);

Alle von ihnen durchsuchen die im Diagnosezyklus ermittelten Daten nach Fehlern und ordnen diese Peripheriebauteilen auf der Modellbahn zu. Die Anwendung kann die Informationen dem Benutzer ausgeben und im Puffer wieder löschen. Der EEPROM kann anschließend durch einen eigenen Diagnosezyklus gelöscht werden.

Weitere Informationen

Die vorgestellten Funktionen stellen nur einen Ausschnitt aus dem API dar, die vollständige Dokumentation aller Routinen befindet sich im Quellcode. Erwähnenswert ist noch, daß im laufenden Betrieb auftretende Fehler von railway auf stderr ausgegeben werden. Dazu gehören sowohl Probleme bei der Kommunikation wie auch Fehler in der Bahnhardware.

Modul progbar

Die Library benutzt eine Fortschrittsanzeige für einige Aufgaben wie das Öffnen einer Gruppe von Nodelinks. Anwendungen können diesen Code in Form des Moduls progbar nutzen.

  Progress [oooo................]

Bei der Initialisierung werden die Breite der Anzeige in Zeichen sowie Minimum und Maximum der Werte angegeben, welche angezeigt werden sollen.

  void progbar_init(struct progbar *bar, unsigned width, int min, int max);

Danach kann die Anzeige mit progbar_show ausgegeben beziehungsweise aktualisiert werden.

  void progbar_show(struct progbar *bar, int value);

Das Modul benutzt das Backspace-Zeichen, um zuvor ausgegebenen Text zu überschreiben. Daher dürfen keine anderen Ausgaben zwischen zwei Aufrufen von progbar_show erfolgen.