RoboPro: Programmieren des Robo-Interface in C

Inhalt

  • Vorwort
  • Wie beginne ich?
  • Namenskonventionen (Variablen)
  • Portierbarkeit von C-Programmen
  • Multitasking
  • Erste Programme
    • Vereinfachung des I/O-Zugriffs
    • Debugging über die serielle Schnittstelle

Vorwort

Obwohl die von ft angebotene grafische Programmierumgebung RoboPro durchaus ihren Reiz hat (ich selbst bin nach anfänglicher Skepsis inzwischen total begeistert davon) gibt es einige Projekte, die die Möglichkeiten von RoboPro sprengen. Obwohl man diese Aussage relativ sehen muss, denn es ist sogar gelungen, einen “Rubiks Cube” (Zauberwürfel) mit RoboPro zu lösen - und das ohne PC-Unterstützung im Stand-alone Betrieb.

Wenn man einen Microcontroller (µC) direkt in C programmieren will, muss man sich zunächst über einiges im Klaren sein: Die Programmierung eines µC unterscheidet sich in vielen Dingen von der Programmierung eines PC.

Zwar können viele Algorithmen übernommen werden, aber der wichtigste Teil eines Interface, nämlich die Hardware, unterscheidet sich ganz gewaltig. Ausserdem ist das Betriebssystem nicht so absturzsicher, wie das eines PC.

Ferner gibt es auch Unterschiede zwischen verschiedenen Systemen. Ich möchte hier das weit verbreitete RCX in Verbindung mit NQC (“Not Quite C”) als Vergleich heranziehen. Der größte Unterschied zwischen dem Robo-Interface und dem RCX besteht darin, dass im Robo-Interface grundsätzlich echter Maschinencode ausgeführt wird (auch in Verbindung mit RoboPro), während das RCX von Haus aus einen Bytecode ausführt (dies lässt sich mit anderen Betriebssystemen ändern, allerdings funktioniert dann kein NQC mehr). Dies hat Vor- und Nachteile: Der Vorteil eines Bytecode-Interpreters ist die Absturzsicherheit, denn der Interpreter überprüft die Plausibilität von Daten und kann im Notfall korrigierend eingreifen. Der Nachteil: Nur die Funktionen, die in diesem Bytecode integriert sind, können auch verwendet werden. Dies ist z.B. der Grund, weshalb mit NQC keine Gleitkommaarithmetik möglich ist. Beim RoboInterface ist dies prinzipiell vom Compiler abhängig, der von ft unterstützte originale Renesas-Compiler vom Hersteller des im Robo-Interface eingebauten µC ist in der Lage, Gleitkommaartihmetik zu unterstützen.

Ich möchte aber auch nicht den großen Nachteil verschweigen: Dadurch, dass “echter” Maschinencode ausgeführt wird, können Programmfehler wesentlich dramatischere Auswirkungen haben, als bei einem Bytecode-Interpreter. Es gibt für das Betriebssystem nur begrenzte Möglichkeiten, hier einzugreifen. Aber was heisst eigentlich “Betriebssystem” bei einem µC? Wer sich mit µC auch abseits vom Robo-Interface beschäftigt, wird sehen, dass üblicherweise kein Betriebssystem in dem Sinne vorhanden ist, wie man es von einem PC kennt. Eigentlich schreibt man sich das selbst - denn es entspricht dem Anwenderprogramm. Beim Robo-Interface ist das Betriebssystem eigentlich nur eine Art Bibliothek, die die Verbindung zur Hardware vereinfacht. So werden die Schnittstellen initialisiert, Timer zur Verfügung gestellt etc., viel mehr auch nicht. Sobald ein Anwenderprogramm die Kontrolle übernommen hat, ist es verantwortlich für den richtigen Umgang mit der Hardware. Deshalb möchte ich auch nochmal die Warnung des Robo-Interface Entwicklers verdeutlichen:

Ein Fehler im Programm kann im ungünstigsten Fall einen Hardwaredefekt hervorrufen. Im günstigsten Fall hängt sich das Interface einfach nur auf.

Dies liegt vor allem an den Unzulänglichkeiten von C. Das, was C so schnell macht, ist gleichzeitig das größte Problem: Es gibt keinerlei Kontrollmechanismen. Variablen mit unterschiedlichen Datentypen können bunt gemischt werden, mit ungewissem Ausgang, bei Arrays kann ohne weiteres der Index überschritten werden, so dass andere Variablen oder wichtige Datenbereiche überschrieben werden. Über die Gefahren von Pointern garnicht zu reden.

Also: Wer das Robo-Interface direkt in C programmieren möchte, sollte genau wissen, was er tut. Vor allem sollte er wissen, was ein C-Compiler mit dem Code macht und notfalls auch mal etwas Assembler-Kenntnisse mitbringen, um den generierten Code zu überprüfen oder in einem Simulator laufen zu lassen.

Zum Erlernen der Programmiersprache C ist das Robo-Interface definitiv nicht zu empfehlen.

Auch Umsteiger von NQC sollten sich darüber im Klaren sein, dass NQC nur eine Teilmenge von C darstellt und bei weitem nicht so fehleranfällig ist.

Und zum Schluß noch etwas in eigener Sache:

Die hier von mir vorgestellten Programme wurden getestet und so weit als möglich überprüft. Dennoch muss ich jegliche Schadenersatzansprüche ablehnen - jeder muss selbst wissen, was er tut. Dies ist definitiv keine Anleitung für Anfänger in der C-Programmierung.

Wer die Programmierung eines µC in C erlernen will, dem möchte ich eine andere Vorgehensweise empfehlen:

Zunächst C lernen. Es gibt einen kostenlosen C-Compiler von Borland für DOS. Das reicht vollkommen, denn Windows-Anwendungen laufen auf einem µC sowieso nicht ;). Dann das Programmieren von z.B. einem Atmel-µC. Der kostet ca. 2-5 Euro, ein Programmiergerät nochmal 5 Euro und die restliche Software (Programmiersoftware und C-Compiler) ist ebenfalls kostenlos erhältlich. Auch bei diesen µC wird man zum fehlerfreien Programmieren “erzogen”. Entweder das Programm funktioniert - oder nicht. Wenn wirklich etwas schief geht, sind die Verluste auch eher zu verschmerzen als die Reparatur am Robo-Interface…

Noch ein Wort zu C++: Die meisten Compiler für µC unterstützen kein C++, da der Funktionsumfang von ANSI-C zur Programmierung eines µC im allgemeinen vollkommen ausreicht.

Wie beginne ich?

Die vom Entwickler des Robo-Interface mitgelieferte Anleitung zur Installation des Compilers, compilieren vorhandener Projekte und anlegen neuer Projekte ist vorbildlich. Deshalb werde ich dies auch garnicht weiter ausführen.

Nur ein paar Tipps zur Einrichtung des Renesas-Compilers:

Wer nicht unbedingt die Projekte auf C:Workspace speichern will, kann unter Setup -> Options -> Workspace das Defaultverzeichnis wechseln.

Um mit der Hardware des Robo-Interface in Kontakt zu treten, gibt es die sogenannte “Transfer-Area”. Eine Beschreibung der Transfer-Area befindet sich in der Anleitung von ft.

Namenskonventionen

Um Programme von unterschiedlichen Programmierern verwenden zu können, sollte man sich auf gemeinsame Richtlinien zur Vergabe von Variablennamen einigen. Zwar unterscheidet sich die vom Robo-Interface Entwickler verwendete Verfahrensweise auch von meiner eigenen, aber mir erscheint es sinnvoll, diese Namenskonventionen zu übernehmen, denn dadurch ist es in zukunft einfacher, fremde Programmteile zu integrieren.

Mitgeliefert werden auch Verkürzungen Variablendeklarationen, so wird z.B. anstelle von “unsigned char” “UCHAR” verwendet. Diese Definitionen befinden sich in der Datei “ke_c.h”.

Bei der Vergabe von Variablennamen stehen die ersten zwei Zeichen für den Variablentyp, z.B.:

ucVariable = unsigned char
ulVariable = unsigned long

Portierbarkeit von C-Programmen

Dies ist ein ewiges Dilemma. Zuerst einmal: C-Code ist eingeschränkt portabel. Spätestens bei Zugriff auf spezielle Hardware ist es vorbei mit der Portabilität. Zwar hat sich der Robo-Interface Entwickler offensichtlich viel Mühe gegeben, das Ganze zu vereinfachen, indem identische Transfer-Areas für PC und Interface verwendet werden, aber die Initialisierungsroutinen unterscheiden sich fundamental. Eine weitere böse Fußangel sind die Datentypen. Der Datentyp int kann bei verschiedenen Prozessoren unterschiedliche Längen haben (nein, richtig gelesen, das ist tatsächlich so und entspricht ANSI-C). So besteht auf einem C-Compiler für den PC ein int Datentyp aus 4 Bytes, auf dem Renesas-Compiler nur aus 2 Bytes. Insofern kann es durchaus sein, dass Programme, die auf einem PC einwandfrei funktionieren, dies auf dem Robo-Interface nicht tun.

Auch bei Pointern gibt es Unterschiede: Während die C-Compiler für den PC zumeist den richtigen Datentyp wählen, ist bei dem Renesas-Compiler das Schlüsselwort “far” notwendig, damit kein 16-Bit-Pointer generiert wird, sondern ein 20-Bit-Pointer, der den gesamten Speicherbereich des Robo-Interface adressieren kann.

Multitasking

Der Renesas C-Compiler unterstützt von Haus aus kein Multitasking, deshalb ist es auch nicht so ohne weiteres möglich, dies zu implementieren. Jeder Thread muss seinen eigenen Stack bekommen, damit nach einer Rückkehr von einem anderen Thread die aktuell verwendeten lokalen Variablen noch existieren - und das modifizieren des Stackpointers ist eine recht gefährliche Sache.

Hier haben die Entwickler von ft aber Abhilfe versprochen, in absehbarer Zeit soll das Betriebssystem entsprechende Subroutinen zur Verfügung stellen.

Momentan besteht die Möglichkeit, ein kooperatives Multitasking zu verwenden. Der Unterschied zum präemptiven Multitasking besteht darin, dass jeder Thread selbst dafür verantwortlich ist, zum Hauptprogramm zurückzukehren, so dass der nächste Thread aufgerufen werden kann. Beim Präemptiven Multitasking werden die Threads nach einer gewissen Zeit automatisch umgeschaltet, was etwas mehr Sicherheit ergibt, falls einer der Threads sich aufhängt.

Jedes der beiden Systeme hat seine Vor- und Nachteile.

Präemptives Multitasking:

+ Die Threads können ganz normal programmiert werden - Die Zeit für jeden Thread wird gleichmäßig verteilt (es gibt auch Ausnahmen, aber im Prinzip ist dies das Konzept)

Kooperatives Multitasking:

+ Jeder Thread kann sich soviel Rechenzeit nehmen, wie er braucht - Genau dies ist der Nachteil: Schlecht programmierte Threads können das System instabil machen

Ein Beispiel zu einem kooperativen Multitasking werde ich irgendwann nachliefern.

Erste Programme

Einen guten Ansatz, sich mit der Programmierung des Robo-Interface vertraut zu machen, ist die Analyse der mitgelieferten Demo-Programme. Sinnvoll für die ersten Schritte: Einen neuen Workspace anlegen, ein vorhandenes Demo-Programm dort hineinkopieren und dann einzelne, kleinere Änderungen vornehmen oder auch kleinere Erweiterungen vornehmen. Wie schon erwähnt - Man bekommt keine Fehlermeldungen, wenn etwas schiefgeht.

Um einen Programmteil zu finden, der Ärger macht, gibt es z.B. die Möglichkeit, bestimmte Ausgänge mit LEDs zu bestücken und diese beim Erreichen bestimmter Programmteile ein- oder auszuschalten.

Da dies nicht gerade sehr Benutzerfreundlich ist, nutze ich inzwischen die Möglichkeit, per serieller Schnittstelle Nachrichten an den PC zu senden. Hierzu gibts später mehr.

Insgesamt gesehen erzieht die Programmierung von µC zu fehlerfreiem programmieren, denn eine Fehlersuche gestaltet sich teilweise sehr lästig. Von vornherein gut durchdachte und geplante Algorithmen ersparen so manchen Abend Fehlersuche.

Vereinfachung des I/O-Zugriffs

Berechtigterweise wurde im Forum moniert, dass der Zugriff auf die Ein- und Ausgänge sehr kryptisch ist.

Willkommen bei C!

Nein, im Ernst: Es gibt natürlich jederzeit die Möglichkeit, dies mit selbstgeschriebenen Funktionen zu ändern.

Bei der Programmierung von µC muss man sich mit einem Thema beschäftigen, mit dem man sonst nur sehr selten konfrontiert wird: Bitmanipulationen, in µC-Kreisen auch “Bitschubsen” genannt ;)

Aus dem o.g. Grund gibts nun einen Crash-Kurs in Bitarithmetik.

Ein Bit ist die kleinste Informationseinheit innerhalb eines Computers, die entweder 0 oder 1 sein kann. Da man damit noch nicht viel anfangen kann, werden die Bits zusammengefasst, z.B. zu einem Byte (= 8 Bit). Zu beachten ist, dass die Bits innerhalb eines Bytes mit 0 - 7 durchnummeriert werden.

Warum brauchen wir das? Beim Robo-Interface (wie auch bei vielen anderen Interfaces auch) werden die Ein- und Ausgänge durch einzelne Bits repräsentiert. Wenn ich eine Lampe an Ausgang 1 anschließe, dann leuchtet sie, wenn Bit 0 in sTrans.M_Main auf “1” gesetzt wird. (Vorsicht: Die Ausgänge werden von 1-8 gezählt, die Bits von 0-7).

Hinzu kommt noch, dass die Ausgänge mit unterschiedlicher Ausgangsspannung angesteuert werden können (das stimmt so nicht 100%, aber der Effekt ist der gleiche…). Das heißt also: Eine Lampe kann dunkler oder heller leuchten, ein Motor schneller oder langsamer laufen. Dies wird mit dem Array sTrans.MPWM_Main[] festgelegt. Dieses Array hat 8 Elemente, jedes Element kann Werte von 0 (= Stufe 1) bis 7 (= max. Spannung) annehmen. Hierbei ist dringend zu beachten, dass der Array-Index niemals ausserhalb des Wertebereichs 0..7 liegen darf. Sollte aus irgendeinem Grund z.B. der Index 8 verwendet werden, landet man im nachfolgenden Datenbereich - und das ist sTrans.MPWM_Sub1. Somit wird dann der Spannungswert eines der Extension-Module geändert. Bei anderen Indizies sieht es entsprechend aus.

Aus Sicherheitsgründen empfehle ich deshalb beim Zugriff auf die Geschwindigkeitsarrays folgendes Konstrukt:

sTrans.MPWM_Main[ucIndex & 0x07] = ucSpeed

Durch den Operator & wird die Variable ucIndex mit 7 Binär “UND” verknüpft. Die Zahl 7 ist Binär 00000111, deshalb sind im Ergebnis die oberen 5 Bits immer 0 - das heißt, der Index kann nur Werte 0 - 7 annehmen, egal, welcher Wert tatsächlich in ucIndex steht.

Bei dem Wert für die Ausgangsspannung wäre diese Einschränkung des Wertebereichs ebenfalls angebracht, aber in diesem Sonderfall wird dies bereits vom Betriebssystem entsprechend verarbeitet. Werte größer als 7 werden als 7 interpretiert.

Wenn ich also Ausgang 1 einschalten will, muss Bit 0 gesetzt werden, bei Ausgang 2 Bit 1 usw. - aber wie realisiere ich so etwas in C ? Schließlich möchte ich ja bei einem Funktionsaufruf einfach die Nummer des Ausgangs angeben und nicht eine binäre Zahl.

Hierfür verwenden wir den Operator «. Er verschiebt einen Wert bitweise. Einige Beispiele sollen dies verdeutlichen:

1<<0 = 1 (00000001)
1<<1 = 2 (00000010)
1<<2 = 4 (00000100)
1<<3 = 8 (00001000)

usw.

Wenn ich also das zweite Argument durch eine Variable ersetze, bekomme ich genau diese Funktionalität, die ich brauche.

Somit ergibt sich für das Einschalten eines bestimmten Ausgangs folgender Code:

sTrans.M_Main |= 1<<ucNummer;

Für einen Wertebereich für ucNummer von 0 bis 7. Auch in diesem Fall sollte der Wert von ucNummer auf den richtigen Wert geprüft werden,obwohl hier speziell kein Problem zu erwarten ist.

Ein weiterer Operator, der hier zum ersten Mal verwendet wird, ist der Oder-Operator |. Dieser Operator stellt sicher, dass bereits vorher gesetzte Bits nicht wieder gelöscht werden. Würde man ucNummer ohne den Oder-Operator zuweisen, würde nur der momentan gewünschte Ausgang gesetzt werden.

Beispiel:

00010000
00000001

——– Oder

00010001

Wie aber kann ich nun einen eingeschalteten Ausgang wieder gezielt löschen, ohne die anderen zu beeinflussen? Hierzu können wir den Und-Operator & verwenden, außerdem brauchen wir den Not-Operator ~. Not kommt hier nicht von Notfall, sondern von engl. “Nicht”.

sTrans.M_Main &=~ 1<<ucNummer;

Der Not-Operator dreht sämtliche Bits um. Aus 00010000 wird 11101111. Mit dem Und-Operator wird dann gezielt das Bit für den entsprechenden Ausgang gelöscht.

Beispiel:

00010001
11101111

——– Und

00000001

Erweitert man nun den Code noch so weit, dass auch die Spannungswerte angegeben werden und neben dem RoboInt auch die Extension-Module angesprochen werden können, sieht es dann so aus:

void output (UCHAR ucNummer, UCHAR cSpeed)
{
UCHAR ucBitmask, ucBit;
ucNummer --; // Wertebereich von ucNummer auf 1-32 umwandeln
if (cSpeed > 8)
cSpeed = 8;
ucBit = ucNummer & 0x07;
ucBitmask = (1<<ucBit); // Eine ucBitmaske berechnen
if (ucNummer < 32) // Sicherheitsabfrage. Falls die Nummer zu groß ist, passiert nichts.
{
switch (ucNummer >> 3) // ucNummer Modulo 8
{
case 0:
sTrans.MPWM_Main[ucBit] = cSpeed - 1;
if (cSpeed == 0)
sTrans.M_Main &= ~ucBitmask;
else
sTrans.M_Main |= ucBitmask;
break;
case 1:
sTrans.MPWM_Sub1[ucBit] = cSpeed - 1;
if (cSpeed == 0)
sTrans.M_Sub1 &= ~ucBitmask;
else
sTrans.M_Sub1 |= ucBitmask;
break;
case 2:
sTrans.MPWM_Sub2[ucBit] = cSpeed - 1;
if (cSpeed == 0)
sTrans.M_Sub2 &= ~ucBitmask;
else
sTrans.M_Sub2 |= ucBitmask;
break;
case 3:
sTrans.MPWM_Sub3[ucBit] = cSpeed - 1;
if (cSpeed == 0)
sTrans.M_Sub3 &= ~ucBitmask;
else
sTrans.M_Sub3 |= ucBitmask;
break;
}
sTrans.MPWM_Update = 0x02; // Update PWM values one time
}
}

Hier gibts den kompletten Workspace mit den Subroutinen zur Ansteuerung der Motoren und Abfrage der Eingänge als ZIP-Datei.

Debugging über die serielle Schnittstelle

Über die Programmierung via C hat man die Möglichkeit, sogenannte “Messages” über die serielle Schnittstelle zu senden. Indem wir diese Messages verwenden, eröffnen wir uns einen Weg, Informationen über den aktuellen Programmablauf vom Interface zu bekommen. Was das Ganze perfekt macht: Der umgekehrte Weg funktioniert auch. Wenn man also entsprechende Subroutinen schreibt, kann man sich z.B. Speicherabbilder und ähnliches herunterladen bzw. aktiv in den Programmablauf eingreifen. Vorausgesetzt natürlich, dass man genügend oft einen Breakpoint einbaut.

Der einzige Nachteil der Messages ist das vorgegebene Protokoll: Es werden grundsätzlich 7 Bytes übertragen. Das erste Byte ist immer 0x90, dann folgt die Hardware-ID (frei wählbar), eine Sub-ID (frei wählbar) und 4 Datenbytes. In einem Test habe ich einen beliebigen Text übertragen können, indem ich den Text in 4 Byte große Häppchen aufgeteilt und gesendet habe. Allerdings muss das Programm auf der PC-Seite die Steuercodes herausfiltern.

Einen Ansatz hierzu bietet dieses Programmbeispiel to be continued….

(thkais)


Erstellt von Thomas Kaiser (thkais).
Hochgeladen von thkais am 9.4.2006.

Hinweis: Wir vertrauen auf die Sachkunde und Sorgfalt unserer Nutzer. Trotzdem könnten sich Fehler eingeschlichen haben. Eine Haftung für die Richtigkeit der Inhalte können wir nicht übernehmen.