CL3-E CANopen/USB Technisches Handbuch

Programmierung mit NanoJ

Einleitung

Der VMM (Virtual Machine Monitor) stellt eine geschützte Ausführungsumgebung innerhalb der Firmware zur Verfügung. In diese kann der Anwender eigene Programme (Benutzerprogramm, "User Program") laden. Diese können dann Funktionen in der Motorsteuerung auslösen, indem beispielsweise Einträge im Objektverzeichnis gelesen oder geschrieben werden.

Durch Verwendung von Schutzmechanismen wird verhindert, dass Benutzerprogramme die eigentliche Firmware zum Absturz bringen können. Im schlimmsten Fall wird lediglich die Ausführung des Benutzerprogramms mit einem im Objektverzeichnis hinterlegten Fehlercode abgebrochen.

Wenn das Benutzerprogramm einmal auf die Steuerung geladen hat, wird es nach dem Einschalten oder Neustarten der Steuerung automatisch ausgeführt.

Verfügbare Rechenzeit

Ein Benutzerprogramm erhält zyklisch im 1 ms Takt Rechenzeit (siehe auch nachfolgende Abbildung). Da durch Interrupts und Systemfunktionen der Firmware Rechenzeit verloren geht, stehen dem Benutzerprogramm (abhängig von Betriebsart und Anwendungsfall) nur ca. 30% - 50% dieser Zeit zur Verfügung. In dieser Zeit muss das Benutzerprogramm seine Arbeit erledigen und sich entweder beenden oder durch Aufruf der Funktion yield() die Rechenzeit abgegeben haben. Bei ersterem wird das Benutzerprogramm mit dem Beginn des nächsten 1 ms-Zyklus wieder neu gestartet, letzteres bewirkt eine Fortsetzung des Programms an dem der Funktion yield() nachfolgenden Befehl beim nächsten 1 ms-Zyklus.

Sofern das System feststellt, dass das Benutzerprogramm mehr als die ihm zugeteilte Zeit benötigt, wird dieses beendet und im Objektverzeichnis ein Fehlercode gesetzt. Bei der Entwicklung von Benutzerprogrammen ist daher speziell bei zeitintensiveren Aufgaben eine sorgfältige Überprüfung des Laufzeitverhaltens durchzuführen. So empfiehlt sich daher beispielsweise die Verwendung von Tabellen, anstatt einen Sinuswert über eine sin Funktion zu berechnen.

Hinweis
Sollte das NanoJ-Programm zu lange die Rechenzeit nicht abgeben, wird es vom Betriebssystem beendet. In diesem Fall wird in das Statuswort bei Objekt 2301h der VMM die Ziffer "4" eingetragen, im Fehlerregister der VMM bei Objekt 2302h wird die Ziffer "5" (Timeout) notiert.

Interaktion des Benutzerprogramms mit der Steuerung

Kommunikationsmöglichkeiten

Ein Benutzerprogramm hat mehrere Möglichkeiten, mit der Motorsteuerung zu kommunizieren:

  • Lesen und Schreiben von OD-Werten per PDO-Mapping
  • Direktes Lesen und Schreiben von OD-Werten über Systemcalls
  • Aufruf sonstiger Systemcalls (z. B. Debug-Ausgabe schreiben)

Über ein PDO Mapping werden dem Benutzerprogramm OD-Werte in Form von Variablen zur Verfügung gestellt. Bevor ein Benutzerprogramm seine 1 ms Zeitscheibe erhält, werden dazu von der Firmware die Werte aus dem Objektverzeichnis in die Variablen des Benutzerprogramms übertragen. Sobald nun das Benutzerprogramm Rechenzeit erhält, kann es diese Variablen wie gewöhnliche C-Variablen manipulieren. Am Ende der Zeitscheibe werden letztendlich die neuen Werte von der Firmware wieder automatisch in die jeweiligen OD-Einträge kopiert.

Um die Performance zu optimieren werden dabei 3 Arten von Mappings definiert: Input, Output und Input/Output (In, Out, InOut). Input Mappings lassen sich nur lesen und werden nicht zurück ins Objektverzeichnis übertragen. Output Mappings lassen sich nur schreiben. Input/Output Mappings erlauben hingegen Lesen und Schreiben.

Die gesetzten Mappings können über die Web-Oberfläche bei den Objekten 2310h, 2320h, und 2330h ausgelesen und überprüft werden. Für jedes Mapping sind maximal 16 Einträge erlaubt.

Über die Angabe der Linker-Section wird in NanoJEasy gesteuert, ob eine Variable im Input-, Output- oder Datenbereich abgelegt wird.

Ausführung eines VMM-Zyklus

Zusammengefasst besteht der Ablauf bei der Ausführung eines VMM-Zyklus hinsichtlich des PDO-Mapping aus folgenden 3 einfachen Schritten:

  1. Werte aus dem Objektverzeichnis lesen und in die Bereiche Inputs und Outputs kopieren.
  2. Benutzerprogramm ausführen.
  3. Werte aus den Bereichen Outputs und Inputs wieder zurück in das Objektverzeichnis kopieren.

Die Konfiguration der Kopiervorgänge lehnt sich an den CANopen Standard an.

Zusätzlich ist es auch möglich, über Systemcalls auf Werte des Objektverzeichnis zuzugreifen. Dies ist im Allgemeinen deutlich langsamer und daher sind Mappings vorzuziehen. Leider ist jedoch die Anzahl an Mappings begrenzt (jeweils 16 Einträge in In/Out/InOut). Es empfiehlt sich daher, häufig genutzte und veränderte OD-Werte zu mappen und auf weniger häufig genutzte OD-Einträge per Systemcall zuzugreifen. Eine Liste verfügbarer Systemcalls findet sich im Kapitel "Systemcalls".

Hinweis
Es wird dringend empfohlen, entweder per Mapping oder Systemcall mit od_write() auf ein und denselben OD-Wert zuzugreifen. Wird beides gleichzeitig verwendet, so hat der Systemcall keine Auswirkung.

OD-Einträge zur Steuerung und Konfiguration der VMM

OD-Einträge

Der VMM wird durch OD-Einträge im Objekt-Bereich 2300h bis 2330h gesteuert und konfiguriert.


OD-Index Name
2300h NanoJ Control
2301h NanoJ Status
2302h NanoJ Error Code
2303h Number Of Active User Program
2304h Table Of Available User Programs
2310h NanoJ Input Data Selection
2320h NanoJ Output Data Selection
2330h NanoJ In/output Data Selection

Beispiel

Um das Benutzerprogramm "TEST1.USR" auszuwählen und zu starten, kann z. B. folgende Sequenz benutzt werden:

  • Umbenennen der Datei "TEST1.USR" in "vmmcode.usr".
  • Kopieren der Datei "vmmcode.usr" über USB auf die Steuerung.
  • Starten des NanoJ Programms durch Beschreiben von Objekt 2300h, Bit 0 = "1" oder Neustart der Steuerung.
  • Überprüfen des Eintrags 2302h auf Fehlercode und des Objekts 2301h, Bit 0 = "1" (VMM läuft).
Hinweis
Aufgrund Limitierungen in der USB Implementation wird nach einem Neustart der Steuerung die Datei "VMMCODE.USR" auf eine Größe von 16kB gesetzt und das Erstelldatum auf den 13.03.2012 gestellt.

Um ein laufendes Programm anzuhalten: Beschreiben des Eintrags 2300h mit dem Bit 0-Wert = "0".

NanoJEasyV2

Installation und Benutzung

Einleitung

Mit NanoJEasyV2 lässt sich Programmierung, Upload und Steuerung eines Benutzerprogramms bewerkstelligen.

Installation

Gehen Sie zur Installation wie folgt vor:

  1. Entpacken Sie die Datei "NanoJEasyV2.zip" in einen Ordner Ihrer Wahl.

  2. Starten Sie das Programm über die Datei "NanoJEasy.exe".

Programmieren von Benutzerprogrammen

Aufbau Benutzerprogramm

Ein Benutzerprogramm besteht aus mindestens zwei Anweisungen:

  1. der Präprozessoranweisung #include "wrapper.h"
  2. der Funktion void user(){}

In der Funktion void user() lässt sich der auszuführende Code hinterlegen.

Die Dateinamen der Benutzerprogramme dürfen nicht länger als acht Zeichen sein und drei Zeichen im Suffix enthalten, zum Beispiel ist der Dateiname "main.cpp" zulässig, hingegen "einLangerDateiname.cpp" nicht.

Beispiel

Programmieren eines Rechtecksignals in das Objekt 2500h:01h

  1. Kopieren Sie folgenden Text in den Editor von NanoJEasy und speichern Sie diese Datei unter dem Namen "main.cpp" ab.

    // file main.cpp
    map S32 outputReg1 as inout 0x2500:1
    #include "wrapper.h"
    
    // user program
    void user()
    {
      U16 counter = 0;
      while( 1 ) 
      {
        ++counter; 
    
        if( counter < 100 )
         InOut.outputReg1 = 0;
        else if( counter < 200 )
          InOut.outputReg1 = 1;
        else
          counter = 0;
    
        // yield() 5 times (delay 5ms)
        for(U08 i = 0; i < 5; ++i )
          yield();
      }
    }// eof

  2. Wenn das Programm fehlerfrei übersetzt wurde:

Aufbau eines Mappings

Einleitung

Mit dieser Methode lässt sich eine Variable im NanoJ-Programm direkt mit einem Eintrag im Objektverzeichnis verknüpfen. Das Anlegen des Mappings muss dabei am Anfang der Datei stehen - noch vor der #include "wrapper.h"-Anweisung. Lediglich ein Kommentar oberhalb des Mappings ist erlaubt.

Tipp

Benutzen Sie das Mapping, falls Sie den Zugriff auf ein Objekt im Objektverzeichnis häufiger benötigen, wie beispielsweise das Controllword 6040h oder das Statusword 6041h.

Für den einzelnen Zugriff auf Objekte bieten sich eher die Funktionen od_write() und od_read() an (siehe Abschnitt "Zugriff auf das Objektverzeichnis")

Deklaration des Mappings

Die Deklaration des Mappings gliedert sich dabei folgendermaßen:

map <TYPE> <NAME> as <input|output|inout> <INDEX>:<SUBINDEX>

Dabei gilt:

  • <TYPE>

    Der Datentyp der Variable, also U32, U16, U08, S32, S16 oder S08.

  • <NAME>

    Der Name der Variable, wie sie später im Benutzerprogramm verwendet wird.

  • <input|output|inout>

    Die Schreib- und Leseberechtigung einer Variable: Eine Variable kann entweder als input, output oder inout deklariert werden. Damit wird festgelegt, ob eine Variable lesbar (input), schreibbar (output) oder beides ist (inout) und über welche Struktur sie im Programm angesprochen werden muss.

  • <INDEX>:<SUBINDEX>

    Index und Subindex des zu mappenden Objektes im Objektverzeichnis.

Jede deklarierte Variable wird im Benutzerprogramm über eine der drei Strukturen "In", "Out" oder "InOut" angesprochen, je nach definierter Schreib- und Leserichtung.

Beispiel eines Mappings

Beispiel eines Mappings und der zugehörigen Variablenzugriffe:

map U16 controlWord as output 0x6040:00
map U08 statusWord as input 0x6041:00
map U08 modeOfOperation as inout 0x6060:00

#include "wrapper.h"

void user()
{
  [...]
  Out.controlWord = 1;
  U08 tmpVar = In.statusword;
  InOut.modeOfOperation = tmpVar;
  [...]
}

Eventuelle Fehlerquelle

Eine mögliche Fehlerquelle ist ein schreibender Zugriff mittels der Funktion od_write() auf ein Objekt im Objektverzeichnis, welches gleichzeitig als Mapping angelegt wurde. Nachfolgend aufgelisteter Code ist fehlerhaft:

map U16 controlWord as output 0x6040:00
#include " wrapper.h"
void user()
{
 [...]
  Out.controlWord = 1;
  [...]
  od_write(0x6040, 0x00, 5 ); // der Wert wird durch das Mapping überschrieben
  [...]
}

Die Zeile mit dem Befehl od_write(0x6040, 0x00, 5 ); ist wirkungslos. Wie in der Einleitung beschrieben, werden alle Mappings am Ende jeder Millisekunde in das Objektverzeichnis kopiert.

Damit ergibt sich folgender Ablauf:

  • Die Funktion od_write schreibt den Wert "5" in das Objekt 6040h:00h.

  • Am Ende des 1 ms-Zyklusses wird das Mapping geschrieben, welches ebenfalls das Objekt 6040h:00h beschreibt, allerdings mit dem Wert "1".

  • Somit wird - aus Sicht des Benutzers - der od_write-Befehl wirkungslos.

Systemcalls

Einleitung

Mit Systemcalls ist es möglich, in der Firmware eingebaute Funktionen direkt aus einem Benutzerprogramm aufzurufen. Da eine direkte Codeausführung nur in dem geschützten Bereich der Sandbox möglich ist, wird dies über sogenannte Cortex-Supervisor-Calls (Svc Calls) realisiert. Dabei wird mit dem Aufruf der Funktion ein Interrupt ausgelöst und die Firmware hat so die Möglichkeit, temporär eine Codeausführung außerhalb der Sandbox zuzulasssen. Der Entwickler des Benutzerprogramms muss sich jedoch um diesen Mechanismus nicht kümmern - für ihn sind die Systemcalls wie ganz normale C-Funktionen aufrufbar. Lediglich die Datei "wrapper.h" muss - wie üblich - eingebunden werden.

Zugriff auf das Objektverzeichnis

  • void od_write (U32 index, U32 subindex, U32 value)

    Diese Funktion schreibt den übergebenen Wert an die angegebene Stelle in das Objektverzeichnis.

    index Index des zu schreibenden Objektes im Objektverzeichnis
    subindex Subindex des zu schreibenden Objektes im Objektverzeichnis
    value Zu schreibender Wert
    Hinweis
    Es wird dringend empfohlen, nach dem Aufruf eines od_write() die Prozessorzeit mit yield() abzugeben. Der Wert wird zwar sofort ins OD geschrieben. Damit die Firmware jedoch davon abhängige Aktionen auslösen kann, muss diese Rechenzeit erhalten und somit das Benutzerprogramm beendet oder mit yield() angehalten worden sein.

  • U32 od_read (U32 index, U32 subindex)

    Diese Funktion liest den Wert an der angegebenen Stelle aus dem Objektverzeichnis und gibt ihn zurück.

    index Index des zu lesenden Objektes im Objektverzeichnis
    subindex Subindex des zu lesenden Objektes im Objektverzeichnis
    Rückgabewert Inhalt des OD-Eintrags
    Hinweis
    Aktives Warten auf einen Wert im Objektverzeichnis sollte immer mit einem yield() verbunden werden.

    Beispiel:

    while (od_read(2400,2) != 0) // wait until 2400:2 is set
    { yield(); }

Prozesssteuerung

  • void yield()

    Diese Funktion gibt die Prozessorzeit wieder an das Betriebssystem ab. Das Programm wird in der nächsten Zeitscheibe wieder an der Stelle nach dem Aufruf fortgesetzt.

  • void sleep (U32 ms)

    Diese Funktion gibt die Prozessorzeit für die angegebene Zahl an Millisekunden an das Betriebssystem ab. Das Benutzerprogramm wird anschließend an der Stelle nach dem Aufruf fortgesetzt.

    ms Zu wartende Zeit in Millisekunden

Debug-Ausgabe

Die folgenden Funktionen geben einen Wert in die Debug Konsole aus. Sie unterscheiden sich lediglich anhand des Datentyps des zu übergebenden Parameters.

  • bool VmmDebugOutputString (const char *outstring)
  • bool VmmDebugOutputInt (const U32 val)
  • bool VmmDebugOutputByte (const U08 val)
  • bool VmmDebugOutputHalfWord (const U16 val)
  • bool VmmDebugOutputWord (const U32 val)
  • bool VmmDebugOutputFloat (const float val)
Hinweis
Die Debug Ausgaben werden zunächst in einen eigenen Bereich des Objektverzeichnis geschrieben und dann von dort von der Web-Oberfläche ausgelesen. Dieser OD-Eintrag hat den Index 2600h und ist 64 Zeichen lang. In Subindex 0 ist immer die Anzahl der bereits geschriebenen Zeichen enthalten.

Ist der Puffer vollgeschrieben, so schlägt VmmDebugOutputxxx() zunächst fehl, das Benutzerprogramm wird dann nicht weiter ausgeführt und hält an der Stelle der Debug Ausgabe an. Erst wenn die Web-Oberfläche den Puffer ausgelesen hat und danach Subindex 0 wieder zurückgesetzt hat, wird das Programm wieder fortgesetzt und VmmDebugOutputxxx() kehrt ins Benutzerprogramm zurück.

Debug-Ausgaben dürfen daher nur während der Testphase bei der Entwicklung eines Benutzerprogramms verwendet werden.

▶   weiter

Inhalt