Roboter programmieren - Vorlesungen
Transcription
Roboter programmieren - Vorlesungen
1 Roboter programmieren Axel-Tobias Schreiner Fachbereich Mathematik-Informatik Universität Osnabrück Dieser Band enthält Kopien der OH-Folien, die in der Vorlesung verwendet werden. Diese Information steht außerdem im World-Wide-Web online zur Verfügung. Programmtexte werden maschinell in diesen Text eingefügt. Zur Betrachtung auf anderen Systemen gibt es den Text auch als PDF-Dokument. Mit dem Acrobat Reader von Adobe kann der Text damit auf Windows-Systemen ausgedruckt werden. Der Band stellt kein komplettes Manuskript der Vorlesung dar. Rudimentäre Programmierkenntnisse werden vorausgesetzt. Inhalt 0 1 2 3 4 5 6 7 8 9 10 11 Einführung Robotic Invention System fischertechnik Remote Procedure Calls Bytecodes Not Quite C P-brick Script Java im RCX pbForth legOS Handy Board IPC@CHIP 1 3 13 27 39 55 75 83 85 107 117 123 2 Literatur Dieser Text wird in der Blue Box mit Adobe Framemaker, Illustrator, PhotoShop und Distiller sowie mit Glyphix unter MacOS X Server entwickelt. Er befindet sich im Web. Der Text ist, soweit vorhanden, direkt mit Artikeln und Dokumentation der verschiedenen Systeme verbunden. Es gibt schon verschiedene Bücher über LEGO Mindstorms. Zu den verwendeten Programmiersprachen, C, C++, Forth und Java, existieren viele Bücher. Die folgenden sind sehr nützlich. Soweit vorhanden, befinden sie sich in der Lehrsammlung. Baum Ellis/Stroustrup Flanagan Flanagan Flanagan Kernighan&Ritchie Knudsen 1-893115-09-7 0-201-51459-1 1-56592-487-8 1-56592-488-6 1-56592-371-5 3-446-15497-3 1-56592-692-7 Definitive Guide to LEGO Mindstorms The annotated C++ Reference Manual Java in a Nutshell (3rd Edition) Java Foundation Classes in a Nutshell Java Examples in a Nutshell Programmieren in C Unofficial Guide to LEGO Mindstorms Robots 3 1 Robotic Invention System Die LEGO-Gruppe hat in Zusammenarbeit mit dem Massachusetts Institute of Technology 1998 das Robotics Invention System (RIS) als ersten der LEGO Mindstorms Baukästen produziert. Das RIS wird für die Altersgruppe ab 12 Jahre ohne Programmiererfahrung empfohlen. Im Rahmen des LEGO Technic Programms erschien zuerst der CyberMaster, der dem RCX nahe verwandt ist und über Funk kontrolliert wird. Im CyberMaster bilden Motoren und Computer eine Einheit. 1999 folgte das Droid Developer Kit, ebenfalls für die Altersgruppe ab 9 Jahren, mit dem MicroScout, einem Modul mit Motor, Licht-Sensor und Tongenerator, mit dem Star Wars Modelle gebaut werden sollen. Das Kit enthält eine CD mit einer multimedialen Einführung in das System, benötigt aber zum Betrieb keinen PC. Ebenfalls 1999 erschien das Robotics Discovery Kit mit dem Scout, der ebenfalls für 9-jährige gedacht ist und über eine Infrarot-Fernbedienung beeinflußt wird. Vor kurzem erschien bei LEGO im Internet ein Development Kit zum Scout, mit dem er vom PC aus im sogenannten LEGO Assembler programmiert werden kann. Der Scout verwendet eine ähnliche Peripherie wie der RCX, seine Software ist aber deutlich eingeschränkt. Zentraler Bestandteil des RIS ist ein Hitachi H8 Microcomputer in einem größeren LEGO-Baustein, der sogenannte Programmable Brick oder RCX, zunächst in der Version 1.0. Der RCX wird über eine serielle Schnittstelle und den sogenannten Infrared Tower in der Regel von einem PC aus kontrolliert und programmiert. Normalerweise lädt man Programme in den RCX und betreibt sie dann autonom, deshalb eignet sich das System vorwiegend zum Bau mobiler Roboter. 4 Für Windows liefert LEGO im RIS eine mit dem Macromedia Director implementierte, grafische Programmierumgebung mit Internet-Anschluß, die auf Ideen von Seymour Pappert (learning by doing) fußt. Einige sehr primitive Roboter-Baupläne sind auf Papier in der Constructopedia vorgegeben, dazu einige Lösungsvorschläge für Konstruktionsprobleme. Auf Wunsch der Kunden mußten 1999 in der Version 1.5 mehr komplette Baupläne geliefert werden. Die Programmierumgebung beginnt zwingend mit einer multimedialen Führung durch das ganze System, in deren Verlauf Soft- und Hardware vorgestellt und ausprobiert werden. Sie stellt dann eine Reihe von Aufgaben (challenges), die durch Zukauf weiterer Baukästen mit Motiven wie Sport, Kreaturen und Mars-Erforschung erweitert werden. Zu den Aufgaben gibt es Hilfestellungen und Checklisten, aber keine Musterlösungen. Die Freiheitsgrade des Benutzers sind leider zum Beispiel im Baukasten Mars-Erforschung sehr eingeschränkt worden. So wie von LEGO geliefert ist das RIS eine ansprechende, moderne Programmierumgebung für Anfänger. LEGO hat einen Modul spirit.ocx dokumentiert, mit dem man den RCX wesentlich anspruchsvoller, zum Beispiel von Visual Basic aus, programmieren kann. Im Internet kam es außerdem zu einer weitgehenden Disassemblierung des RCX, die in die Entwicklung der verschiedensten Programmiersprachen und -umgebungen für den RCX mündete. Diese, etwas unfreiwillige, Offenlegung des Systems stellt seinen eigentlichen Reiz für die etwas Fortgeschritteneren dar. 5 1.1 Trusty Videos: AVI mp4 (649k), cinepak (8.4m); Quicktime (3.5m). Knudsens Trusty treibt zwei große, griffige Räder mit je einem Motor an. Das dritte Rad ist klein und leicht drehbar gelagert. Man steuert Trusty durch Variieren der MotorGeschwindigkeiten und -Laufrichtungen — geradeaus zu fahren ist schwierig. An Stelle des Licht-Sensors trägt Trusty eine Variante der Touch-Sensoren von Knudsens Hank: 6 Algorithmus Der linke Motor ist so an A und der rechte Motor so an C am RCX angeschlossen, daß sich Trusty vorwärts bewegt, wenn sie in die gleiche Richtung laufen. 1 A 3 C Fährt Trusty vorne links gegen ein Hindernis, schließt sich der Touch-Sensor 1. Dann sollen zunächst beide Motoren umgekehrt laufen, damit sich Trusty rückwärts bewegt. Anschließend soll A vorwärts und C noch rückwärts laufen, damit sich Trusty auf der Stelle nach rechts dreht. In der neuen Richtung, also einige Grad im Uhrzeigersinn gedreht, soll schließlich auch C wieder vorwärts laufen. Fährt Trusty vorne rechts gegen ein Hindernis, schließt sich der Touch-Sensor 2. Analog soll dann Trusty zurückfahren, nach links drehen, und wieder vorwärts fahren. Nach der 11. Kollision bleibt Trusty stehen. 1.2 Robotics Invention System RIS ist eine grafische Oberfläche unter Windows, mit der der RCX auf der Basis von stark eingeschränkten, spezialisierten Struktogrammen (vergleiche DIN 66261) programmiert wird. Da man Kontrollstrukturen nicht direkt schachteln kann, würde man vielleicht so codieren: 7 Oberfläche RIS unterscheidet verschiedene Benutzer nach Namen. Von diesem Schirm aus erreicht man alle anderen. Im Setup kontrolliert man RCX-Parameter und schaltet spezielle Sensoren ein. Von hier erreicht man (zuerst zwangsweise) das Trainingsprogramm sowie die Programmierumgebung und den Programmspeicher. Das Trainingsprogramm führt interaktiv in (fast) alle Aspekte der Programmierung ein. 8 Im Programmierbereich gibt es ein Dialogfeld Programme werden in Dateien gespeichert; zur interaktiven Kontrolle der RCX-Ein- und - der Zugriff ist leider linear. Ausgänge. Durch Rechts-Click kann man fast alle Blöcke Challenges enthalten mehr oder weniger umdrehen und Parameter setzen. anspruchsvolle Aufgaben, Hinweise, Tests und ein Logbuch für Resultate. 9 Sprachumfang addiert 1 zu einem Zähler, der abgefragt werden kann. läßt den RCX piepsen. schaltet einige der Ausgaben ab. schaltet einige der Ausgaben ein. schaltet einige der Ausgaben für einige Zehntelsekunden ein — auch zufällige Länge möglich. Programmkopf — daran hängen alle aktiven Blöcke. setzt den Zähler auf Null. löscht empfangene Zahl, die abgefragt werden kann; erst danach kann wieder eine empfangen werden. löscht Zeitzähler, der abgefragt werden kann. vertauscht Polarität einiger Ausgaben, kehrt also Motor-Laufrichtung um. verschickt eine Zahl an andere RCX, die diese empfangen und abfragen können. 10 setzt Polarität — also Laufrichtung — einiger Ausgaben. setzt Pulsbreite — also Umdrehungszahl — einiger Ausgaben. läßt den RCX mit einer gewissen Frequenz einige Zehntelsekunden tönen. läßt den RCX einige Zehntelsekunden warten; Motoren laufen weiter. Es gibt fünf Bedingungen: TouchSensor, Licht-Sensor, Zähler, Zeitzähler und Zahl von einem anderen RCX. Im Setup kann man dazu noch den Rotation-Sensor und den Temperatur-Sensor aktivieren. abhängig von der Helligkeit eines LichtSensors wird der linke oder rechte Bereich mit Anweisungen ausgeführt. Die Stacks kann man bisher nur über Makros (my commands) schachteln. wiederholt einen Bereich mit Anweisungen einige Male. Die Anzahl Wiederholungen kann auch in einem Intervall zufällig gewählt werden. wiederholt einen Bereich mit Anweisungen; ein Abbruch ist nicht möglich. 11 wiederholt einen Bereich mit Anweisungen falls und solange der Licht-Sensor einen bestimmten Wertbereich anzeigt. wartet, bis ein Touch-Sensor aktiviert ist. Mit Makros kann man Bereiche mit Anweisungen benennen und wiederverwenden. Man kann damit auch Stacks schachteln. Bis zu 9 Bedingungen kann man auch als Watcher einsetzen. Mit dem Licht-Watcher wird einer von zwei Bereichen mit Anweisungen ausgeführt, abhängig vom Wert eines Licht-Sensors. Mit dem Touch-Watcher wird einer von zwei Bereichen mit Anweisungen ausgeführt, abhängig vom Wert eines Touch-Sensors. Beide Bereiche können verwendet werden. Aktivierte Watcher kann man nicht inaktivieren. Da man Stacks einfügen kann, ergeben sich komplizierte Bedingungen. Auf der Basis der Sensor Watcher kann man ein Event-getriebenes Programm für Trusty konstruieren, das auch nach einigen Sekunden selbst zur Ruhe kommt; siehe rebound2. Diese Programmiertechnik gilt als wesentlich natürlicher... 12 13 2 fischertechnik Weihnachten 1965 wollte Artur Fischer seinen Kunden etwas anderes als Schwarzwälder Schinken und Kirschwasser schenken — und durch die begeisterte Reaktion der Kunden kam es zu den fischertechnik-Baukästen, die im technischen Bereich wesentlich anspruchsvoller und präziser sind als LEGO Technic. Firmen wie Staudinger entwickeln große, modulare Modelle zur CIM-Simulation auf der Basis von fischertechnik. Schon 1984 wurde bei fischertechnik ein erstes Universal-Interface angeboten, und zwar zum Anschluß an eine parallele Schnittstelle, die damals weitverbreitet war. Modelle konnten mit Computern wie dem Commodore C64, einem Atari ST oder auch einem IBM PC angesteuert werden. 1997 erschien das sogenannte Intelligent Interface zum Anschluß an eine serielle Schnittstelle, das einen Intel 80C32 Microprocessor enthält und ähnliche Eingaben und Ausgaben erlaubt — allerdings mit etwas anderen elektrischen Eigenschaften — wie das parallele Interface. Beide Geräte werden heute mit der gleichen grafischen Oberfläche LLWin angesteuert, die an das industriell verwendete System iCon-L angelehnt ist. Für das serielle Interface kennt man ein Byte-Protokoll, mit dem man das Interface von einem Computer aus steuern kann, der über eine serielle Schnittstelle verfügt. LLWin-Programme kann man auch in das serielle Interface laden und dort autonom betreiben; damit ist der Bau mobiler Roboter möglich. Im Gegensatz zu LEGO’s RCX ist das Innenleben der seriellen Schnittstelle von fischertechnik nicht näher bekannt — es ist anscheinend nicht möglich, die von LLWin verwendete Firmware direkt anzusteuern, zu umgehen oder gar zu ersetzen. Dies, und der in Deutschland im Gegensatz zu anderen Ländern wesentlich teurere und weniger verbreitete Internet-Zugang, führte bisher leider dazu, daß das technisch eigentlich interessantere System eine wesentlich kleinere ‘‘Fan-Gemeinde’’ hat. 14 2.1 Schwenkroboter Videos: AVI mp4 (579k), cinepak (7m); Quicktime (2.7m). Das erste Modell aus dem Baukasten Industry Robots (30408) betreibt eine Drehscheibe mit einem Motor M1 und zwei Endabschaltern E1 und E2. M1 E3 E4 E1 E2 M2 Auf der Drehscheibe befindet sich eine Greifzange mit dem Motor M2, einem Endabschalter E3, der die offene Stellung der Zange registriert, und einem Impulsschalter E4, der die Umdrehungen des Schneckengetriebes zum Schließen der Zange zählt. 15 Algorithmus Links steht ein Objekt, das nach rechts gebracht werden soll. Als Ausgangsposition läßt man zuerst M2 nach links drehen, bis E3 geschlossen wird, und dann M1 nach links, bis E1 geschlossen ist; damit umgibt die offene Zange das Objekt. Dann schließt man die Zange mit M2 nach rechts, wobei E4 als Zähler relativ zum Objekt kalibriert werden muß. Danach dreht M1 nach rechts, um das Objekt zu schwenken, bis E2 geschlossen ist. Zuletzt dreht M2 noch nach links, bis E3 geschlossen ist, dann ist das Objekt frei. Je nach Einbettung (Förderbänder?) kann die Zange leer zurückfahren oder das Objekt erneut greifen und zurückbringen etc. LLWin LLWin ist eine grafische Oberfläche unter Windows, mit der fischertechnik-Modelle auf der Basis von spezialisierten Flußdiagrammen gesteuert werden. Die Oberfläche ist insgesamt gewöhnungsbedürftig; insbesondere der Editor läßt sich leider nicht umgehen, da eine andere Repräsentation der Programme nicht offengelegt wurde. Das Hauptprogramm von schwenk.mdl enthält die im Algorithmus beschriebenen Schritte. START und END begrenzen einen Prozeß. Es kann mehr als einen Prozeß geben; alle Prozesse werden parallel ausgeführt. Unterprogramme wie Home definiert man auf eigenen Arbeitsblättern, die man mit Subprogram|Copy erzeugt und zum Editieren mit Edit|Subprogram erreicht. Anweisungen wie Beep oder Wait verursachen bestimmte Effekte. Parameter kann man nachträglich nochmals durch Shift-Rechts-Clicken mit der Maus editieren. Alternativ gibt es unter Run|Init einen Modus, bei dem man durch einfaches Clicken die Parameter editieren und dafür auch zu Unterprogrammen wechseln kann (zurück mit Rechts-Click). Mit dem Terminal-Block kann man Programme während Run|Start steuern. Die oberen Zeilen zeigen Ausgaben an die Motoren und auf den Display an. Darunter sind Werte EA..ED und Schalter E17..E26, die man interaktiv beeinflussen und im Programm abfragen kann. EA kalibriert hier die Greifzange für Fässer. STOP unterbricht die Programmausführung; Run|Stop bricht sie vollständig ab. RESET setzt alle Prozesse auf START zurück. Hat man die nötigen Blöcke mit Edit|Insert Block... angelegt und mit der Maus positioniert, kann man sie im zeitlichen Ablauf verbinden: Rechts-Click auf einen Anschluß, Links-Clicks auf Zwischenpositionen und Rechts-Click auf den Anschluß am folgenden Block oder auf eine bestehende Verbindung. Bei Fehlern drückt man auf Del und löscht dann durch Clicken. 16 Es empfiehlt sich, Unterprogramme einzuführen, die ihrerseits weitere Unterprogramme verwenden dürfen: Home öffnet zuerst die Greifzange und dreht sie dann zur linken Plattform. Dafür müssen nur die zuständigen Unterprogramme aufgerufen werden. Open läßt mit einem Motor-Block M2 nach links (ccw) drehen und wartet mit einem Input-Branch-Block, bis sich der Schalter E3 schließt. Dann wird M2 abgeschaltet (off). Weil die Greifzange damit am Anschlag steht, setzt man die Variable VAR1 auf Null. Es gibt 99 Variablen. Close läßt M2 nach rechts (cw) drehen und benützt den Schalter E4 in einem Position-Block dazu, die Variable VAR1 bis zum Endwert in EA hochzuzählen. Dann wird M2 wieder abgeschaltet. Mit dem Position-Block kann man verschiedene Schleifen programmieren. ToLeft läßt M1 nach links drehen, bis E1 sich meldet. Über Options|Setup Interface... kann man das Zeitintervall beeinflussen, das zwischen zwei Anfragen eines Schalters abgewartet wird. Typisch sind 10 Millisekunden (100 Hertz). ToRight läßt M1 nach rechts drehen, bis E2 sich meldet. Falls sich die Motoren nicht in der gewünschten Richtung drehen, muß man ihre Anschlüsse im Modell vertauschen. Zum Test ruft man Options|Check Interface... auf — die Motoren kann man mit Links- und Rechts-Click betreiben. 17 Sprachumfang Ohne sekundäres Interface gibt es die Schalter E1..E8, die Terminal-Schalter E17..E26, die Motorzustände E31..E34 nach links (ccw) und E41..E44 nach rechts (cw), die Terminal-Werte EA..ED, die Variablen VAR1..VAR99, die Konstanten -999..9999 sowie die Ausgaben M1..M4. Die analogen Eingaben EX..EY muß man per Prozeß in eine Variable kopieren, um sie zu verarbeiten. Beep läßt’s beim PC piepsen. Compare vergleicht eine Variable mit einer Variablen, einem TerminalWert oder einer Konstanten. DecVariable verringert den Wert einer Variablen um 1. Display zeigt eine Variable, eine Konstante, eine analoge Eingabe oder einen Terminal-Wert in einem der beiden DIsplay-Felder an. Edge wartet, bis ein Schalter oder Terminal-Schalter von 1 auf 0 wechselt. EndTask beendet einen Prozeß. IncVariable erhöht den Wert einer Variablen um 1. InputBranch testet einen Schalter oder Terminal-Schalter. Lamp schaltet eine Ausgabe ein oder aus. Message gibt einen kurzen Text im Display im Terminal aus. Motor schaltet eine Ausgabe nach links (ccw) oder rechts (cw) oder aus (off). Position erhöht oder verringert eine Variable bei jeder Änderung eines Schalters oder Terminal-Schalters um 1 und wartet, bis ein Endwert (Konstante, Variable oder Terminal-Wert) erreicht ist. Reset gibt einem Schalter die Möglichkeit, alle Prozesse auf Start zurückzusetzen. Start beginnt einen Prozeß. Alle Prozesse starten gleichzeitig und laufen parallel ab. Sie können sich über Variablen synchronisieren. Stop gibt einem Schalter die Möglichkeit, alle Ausgaben abzuschalten, wobei das Programm nicht terminiert wird. Text ist ein Kommentar. Variable weist einer Variablen eine Konstante, eine Variable, eine analoge Eingabe oder einen Terminal-Wert zu. Wait suspendiert einen Prozeß für eine konstante Anzahl Sekunden. Wird ein negativer Wert angegeben, limitiert er einen zufälligen Wert. Als Beispiel für die parallele Verarbeitung aller Prozesse kann man zum Beispiel bei Close den nebenstehenden Prozeß hinzufügen. Obgleich der Prozeß quasi in Close versteckt ist, wird er sofort gestartet. Man kann damit einmal(!) mit dem ersten Terminal-Schalter eine Lampe einund ausschalten, die an M4 angeschlossen ist; danach ist der Prozeß beendet. 18 2.2 Servo Servos (31495) werden im Universalfahrzeug (30481) zum Schalten und Steuern verwendet. Ein Servo enthält einen sehr kräftigen Motor, der über zwei Pins der kleinen Buchse seine Position als Analogwert zurückmeldet. Schließt man den Motor als M1 und die Rückmeldung als EX an, kann man mit dem Diagnose-Panel die Grenzwerte bestimmen, die EX annimmt. Die Schaltung des Fahrzeugs hat drei Positionen. In den Terminal-Werten EA..ED legt man versuchsweise Bereiche fest, die beim Schalten im Programm servo.mdl erreicht werden müssen. Ein Prozeß kopiert möglichst oft EX in eine Variable und zum Display. Unter Options|Setup Interface... kann man die Zykluszeit des Programms einstellen und außerdem festlegen, nach wieviel Zyklen jeweils der Analogwert kontrolliert wird. Das muß sehr häufig geschehen. Ein Unterprogramm prüft zum Beispiel, ob EX genügend groß ist. Falls nicht, wird der Motor ganz kurz in die richtige Richtung gedreht. Es ist ziemlich klar, daß man auf diese Weise den Lastwagen kaum interaktiv steuern kann — die Rückmeldung ist einfach nicht schnell und präzise genug, und im Gegensatz zum RCX kann man bei fischertechnik die Motoren nicht mit reduzierter Leistung langsamer betreiben. 19 2.3 Knickarm-Roboter Videos: AVI mp4 (1.8m), cinepak (21m); Quicktime (9.1m). Das letzte Modell aus dem Baukasten Industry Robots (30408) verwendet insgesamt vier Motoren und für jeden einen Endabschalter und einen Impulsschalter. M3 M2 M1 Die Greifzange wird — vermutlich aus Gewichtsgründen — über eine lange Welle mit Gelenken angetrieben. Ihr Motor M4 und Impulsschalter E8 befinden sich in der Nähe der anderen Motoren, ihr Endabschalter E7 ist an der Zange angebracht. Es gibt folgende Motoren und Schalter: Motor Endabschalter Impulsschalter Motor links/rechts... M1 E1 E2 rotiert Drehscheibe links/rechts M2 E3 E4 schiebt Arm zurück/vor M3 E5 E6 hebt/senkt Arm mit Greifzange M4 E7 E8 öffnet/schließt Greifzange über Gelenkwelle 20 Detailansichten Greifzange mit E7 Armantrieb mit E5 und M3 Auflager des Arms E3 und darunter M2 Drehscheibe mit E1 und E2 Alle Motoren am Arm 21 Programmierung Der Videofilm zeigt mit einem LLWin-Programm von fischertechnik, wie der Knickarm-Roboter zwei Objekte holt und aufeinandertürmt. Im weiteren Verlauf werden die Objekte vertauscht. Ausgehend von der durch die Endabschalter definierten Home-Position (alle Motoren ganz nach links) muß man dazu einige Punkte im Raum um die Objekte kalibrieren und nacheinander ansteuern. Man könnte viele Variablen verwenden, aber die Einrichtung eines solchen Programms ist doch sehr mühsam. Es ist außerdem wenig effizient, weil es schwierig ist, mehrere Motoren gleichzeitig zu betreiben, um einen Punkt ‘‘schräg’’ anzusteuern. Teach-In Zu dem speziellen Modell gibt es eine grafische Teach-In-Oberfläche, mit der man die Motoren durch Links- und Rechts-Click steuern kann. Zum Schließen der Greifzange muß man die Anzahl Impulse eingeben. Hat man signifikante Punkte erreicht, speichert man sie ab. Dadurch entsteht eine Textdatei: Arbeitsraum: A1:250 A2:160 A3:116 A4:025 Programm: 1.M1:025 M2:000 M3:090 M4:000 2.M1:000 M2:000 M3:000 M4:000 3.M1:025 M2:037 M3:113 M4:000 4.M1:025 M2:037 M3:113 M4:018 Die zweite Zeile definiert die unter Options|Working Limits einstellbaren maximalen Werte, die die Impulszähler für die Motoren erreichen dürfen. Die Datei kann mit dem Programm auch abgespielt werden — dabei werden durchaus mehrere Motoren parallel betrieben. Teach-In demonstriert die Grenzen von geschlossenen Systemen wie LLWin. Konstruieren und Abspielen der Textdatei ist in einer normalen Programmiersprache trivial, aber es gibt eben keinen Weg von der Programmiersprache zu LLWin. Die Textdatei könnte auch aus einem CAD-System stammen. 22 2.4 Trainingsroboter Videos: AVI mp4 (1.1m), cinepak (15.6m); Quicktime (3.3m). Der Trainingsroboter wurde sehr früh mit dem Universal-Interface verkauft. Damals wurden Lichtschranken an Stelle der Impulsschalter verwendet, die jedoch mit dem seriellen Interface nicht schnell genug gelesen werden können. Die Greifzange hatte ein sehr feines Gewinde und mußte zeitabhängig gesteuert werden. Man kann den Roboter mit Teilen aus dem Baukasten Industry Robots modernisieren. Bei der Greifzange bringt man den Motor senkrecht an, um die Balance zu verbessern. Es ist mechanisch ganz instruktiv, die verschiedenen Konstruktionen zu vergleichen. M3 M2 M1 M4 Aus dem Schema erkennt man, daß sich die Effekte von M2 und M3 gegenseitig beeinflussen. 23 Es gibt folgende Motoren und Schalter: Motor Endabschalter Impulsschalter Motor links/rechts... M1 E1 E2 rotiert Drehscheibe links/rechts M2 E3 E4 neigt Arm zurück/vor M3 E5 E6 hebt/senkt Arm mit Greifzange M4 E7 E8 öffnet/schließt Greifzange Der modifizierte Trainingsroboter kann mit dem Teach-In-Programm für den Knickarm-Roboter betrieben werden. Der Videofilm zeigt, daß der Trainingsroboter zwar eine höhere Reichweite hat, aber durch die gegenseitige Beeinflussung der beiden Motoren relativ schwierig präzise zu bewegen ist. Details M2 und E3 M3 und E5 von unten: E6 und E4 M1 mit E2 und E1 M4 und E7, E8 liegt dazwischen 24 2.5 Pneumatik Fischer und Lego liefern Zylinder, die mit Druckluft bewegt werden können. Zur Erzeugung der Druckluft verbindet man einen Zylinder exzentrisch mit einem Rad, das von einem Motor angetrieben wird. Mit einem geeigneten Ventil wirkt der Zylinder wie eine Fahrradpumpe. Die Druckluft speichert man in einem kleinen Behälter. Im Baukasten Pneumatic Robots (34948) gibt es von Fischer drei elektrisch betriebene 3/2 Magnetventile, die jeweils 3 Anschlüsse (P für Druckluft, A zum Anschluß eines Zylinders und R zur Belüftung) und 2 Zustände (ohne Strom A zu R, mit Strom P zu A) besitzen. Mit zwei Dioden kann man zwei Ventile so an einen Interface-Ausgang anschließen, daß je nach ‘‘Motor’’-Drehrichtung eines der Ventile offen bleibt und das andere geschlossen wird. Sortierer Ein Modell der Pneumatic Robots verwendet eine Reflex-Lichtschranke zur Steuerung eines pneumatisch betriebenen Verteilers, der schwarze und weisse Räder (meistens :) sortiert. Videos: AVI mp4 (0.8m), cinepak (10.6m); Quicktime (6.2m). 25 Ein Motor mit Exzenter treibt einen sehr flachen Schieber, der die Räder zur Lichtschranke und dahinter zum Verteiler transportiert. E2 markiert den hinteren Endpunkt des Schiebers. Dort muß der Motor M4 angehalten werden, bis der Verteiler ein Rad zur Seite geschoben hat. M4 E2 Die Drehrichtung von M4 ist so gewählt, daß der Exzenter beim Vorschieben gegen das hier rot markierte Bauteil drückt und nicht umgekehrt, denn sonst verklemmt sich der Schieber. Die Lichtschranke besteht aus einer Glühbirne und einer Fotozelle E1. Die Birne wird so justiert, daß weiße Räder erkannt werden. Reflex-Lichtschranke E1 Der Verteiler benötigt drei Magnetventile, um zwei Zylinder vor- und zurückzubewegen, wodurch drei Positionen entstehen. Mit Hilfe von Dioden kann man zwei Ventile an M1 anschließen und durch die ’’Drehrichtung’’ des Ausgangs steuern. M3 steuert das Ventil, das den Schieber zentriert. Pneumatischer Verteiler M1 (Dioden), M3 left home right Der rechte Zylinder ist fest eingebaut, der linke bewegt sich und trägt den Verteiler. Das LLWin-Programm sort.mdl regelt im Wesentlichen zeitliche Abläufe. 26 27 3 Remote Procedure Calls Das RIS beruht auf einer Windows-spezifischen Active-X-Control spirit.ocx, deren Benutzung LEGO bewußt offengelegt hat. Man kann Funktionen in diesem Modul unter Windows mit Visual Basic oder anderen Sprachen aufrufen und damit mit einer prozeduralen Schnittstelle mit dem RCX interaktiv kommunizieren sowie Programme laden und ausführen lassen. Quelle Applikation Programmiersprache spirit.ocx Infrarot Codes Interpreter Hardware 3.1 Visual Basic for Applications Hanley und Hearne’s Buch Lego Mindstorms Programming with Visual Basic erklärt sehr ausführlich, wie man spirit.ocx ansteuert. Auf diese Weise erhält man schnell Oberflächen, die mit dem RCX kommunizieren. In einem Anhang wird skizziert, wie man Visual C++ zum gleichen Zweck verwendet. Microsoft liefert Visual Basic auch als Teil von Applikationen wie Word — in diesem Fall kann man zwar Programme nur als Teil von Dokumenten speichern, aber für Experimente mit spirit.ocx reicht dies völlig. Knudsen schildert im achten Kapitel, wie man speziell mit Visual Basic for Applications unter Word umgeht. In Word97 erreicht man mit Extras|Makros|Visual BasicEditor das VBA-System. Dort legt man mit Einfügen|UserForm ein Fenster (Form) an. Dann muß man einmal unter Extras|Weitere Steuerelemente... die Spirit Control finden — vorausgesetzt, RIS ist bereits installiert. Anschließend aktiviert man die Ansicht| Werkzeugsammlung und zieht von dort den LEGOBaustein in die Form. Im Ansicht|Eigenschaftenfenster ändert man Namen und Titel der Form und andere Eigenschaften|Sub aS. Zieht man noch einen Knopf in die Form und ändert seinen Namen und die Caption, erhält man über Ansicht|Code schließlich ein Fenster, in dem man die Reaktion auf einen Knopfdruck programmieren kann: Private Sub Play_Click() With MyForm.Spirit1 .InitComm .DeleteAllTasks .PlaySystemSound 0 .CloseComm End With End Sub ’ ’ ’ ’ ’ ’ Methodenkopf vereinfacht Codierung reserviert und initialisiert COM1 legt RCX still piepst gibt Verbindung frei 28 Mit Testen|Kompilieren könnte man überprüfen, ob Syntaxfehler vorliegen. Wenn die Form selektiert ist, startet Ausführen|UserForm die Event-Schleife. Ist der RCX eingeschaltet und in Reichweite, dann muß er auf Knopfdruck piepsen. Entweder schließt man die Form oder man wählt Ausführen|Beenden, um wieder in den Editor zurückzukehren. Knudsen erklärt, daß man auch durch Einfügen|Modul zu einem Programmfenster kommt. Dort kann man praktisch den gleichen Programmtext eintragen: Sub Hello() With ... End Sub Selektiert man ein Unterprogramm in diesem Fenster, dann startet Ausführen|Sub genau dieses Unterprogramm— die Form bleibt dabei unsichtbar. Form und Modul können mit dem Word-Dokument gespeichert oder über Datei|Datei exportieren... einzeln abgelegt und mit Datei importieren... eingelesen werden. Visual Basic Zu Visual Basic gibt es eine ausführliche Online-Dokumentation. Im Wesentlichen wird man Oberflächen zusammenstellen und auf Ereignisse reagieren. Die nötigen Methodenköpfe generiert der Editor, mögliche Ereignisse kennt er auch. Wichtige Anweisungen sind zum Beispiel: variable = ausdruck prozedur ausdruck , ausdruck objekt.methode ausdruck , ausdruck If bedingung Then anweisung ... ElseIf bedingung Then anweisung ... Else anweisung ... End If Do ’ Zuweisung an generische Variable ’ Aufruf einer Prozedur ’ Aufruf einer Methode ’ Entscheidung, auch einzeilig ohne End anweisung ... Loop ’ Schleife, auch mit While oder Until ’ Körper, mit Exit Do ’ Ende, auch mit While oder Until For variable = ausdruck To ausdruck anweisung ... Next ’ Schleife, auch mit Step ’ Körper, mit Exit For ’ Ende, auch mit variable With objekt anweisung ... End With ’ vereinfacht Zugriff auf Objekt End ’ beendet Ausführung Anweisungen werden mit : getrennt und mit _ auf der nächsten Zeile fortgesetzt. Kommentare beginnen mit einem einfachen Anführungszeichen. 29 Spirit Methoden Zur RCX-Programmierung muß man in einer Form ein Spirit-Objekt anlegen. Diesem Objekt schickt man dann Nachrichten, die von LEGO ausführlich dokumentiert wurden; dabei wird man die With-Anweisung verwenden. Außerdem sollte man den Modul RCXdata.bas importieren, der nützliche Konstanten definiert. Nachrichten wie .InitComm oder .CloseComm steuern nur das Objekt. Die meisten Nachrichten wirken sich auf den RCX aus — dort werden dann Anweisungen entweder sofort ausgeführt oder als Programme gespeichert. Es gibt fünf Programme mit je bis zu 10 Tasks und bis zu 8 Unterprogrammen. Task 0 wird vom Run-Knopf am RCX gestartet. Unterprogramme können nur von einer Task aufgerufen werden. Ein Programm wählt man mit .SelectPrgm. Die Nachrichten .BeginOfTask oder .BeginOfSub leiten die Speicherung einer Task oder eines Unterprogramms im RCX ein, bei .EndOfTask oder .EndOfSub wird der Code dann übertragen. Entscheidend für den Umgang mit spirit.ocx ist, daß man die nachfolgend abgebildeten Tabellen aus der LEGO Dokumentation versteht. Sehr viele Prozeduren erlauben als Argument wahlweise eine Konstante, eine Variable, den Wert eines Eingangs, der noch verschieden skaliert werden kann, den Wert einer Ausgangs usw. Das Argument wird dann durch zwei Zahlen dargestellt: source entscheidet, um welche Art von Wert es sich handelt — die möglichen Werte sind in RCXdata.bas als Konstanten definiert: Public Public Public Public Public Public Public Public Public Public Public Public Public Public Public Public Const Const Const Const Const Const Const Const Const Const Const Const Const Const Const Const VAR = 0 TIMER = 1 CON = 2 MOTSTA = 3 RAN = 4 TACC = 5 TACS = 6 MOTCUR = 7 KEYS = 8 SENVAL = 9 SENTYPE = 10 SENMODE = 11 SENRAW = 12 BOOL = 13 WATCH = 14 PBMESS = 15 Je nach source sind dann verschiedene Wertebereiche für die zweite Zahl möglich — und die stehen in den Tabellen. SendPBMessage ( Source, Number ) R SelectDisplay( Source, Number ) 0 - 31 0 - 31 0 - 31 0 - 31 0 - 31 While( Source1, Number1, RelOp, Source2, Number2 ) ) If ( Source1, Number1, RelOp, Source2, Number2 Wait ( Source, Number ) • 0 - 31 Loop( Source, Number ) • • 0-3 • 0-3 • 0 - 31 SumVar( VarNo, Source, Number ) SubVar( VarNo, Source, Number ) DivVar ( VarNo, Source, Number ) MulVar ( VarNo, Source, Number ) SgnVar( VarNo, Source, Number ) AbsVar ( VarNo, Source, Number ) AndVar ( VarNo, Source, Number ) OrVar ( VarNo, Source, Number ) 0-3 0-3 0 - 31 • • 0-31 R DatalogNext( Source, Number ) R • 0 • (1) (0) 0-31 Timer Var. SetVar( VarNo, Source, Number ) Poll( Source, Number ) SetEvent( Source, Number, Time ) ClearEvent( Source, Number ) C ClearTachoCounter( MotorList ) On( MotorList ) Off ( MotorList ) Float( MotorList ) SetFwd( MotorList ) SetRwd( MotorList ) AlterDir ( MotorList ) SetPower( MotorList, Source, Number ) Command • • 0, 1, 2 -32768 32767 0 - 255 -32768 32767 0-6 • • • • • 0,1,2 132767 0, 1, 2 -32768 32767 • • • (3) Motor Status • • • 0-7 (2) Const. • • • 1 - 32767 • 1 - 255 • 1 - 32767 • • • 0-7 Random No. (4) • • • • • 0, 1 • • 0,1 0, 1 • • • • • 0, 1 • • 0,1 0, 1 • • • C(6) C(5) • Tacho Speed Tacho Counter • • • • 2 • • 2 2 • • • C(7) Motor Current • • • • • • • • x • • • 0 - 255 R(8) PrgmNo • • 0, 1, 2 • 0, 1, 2 • • 0, 1, 2 0, 1, 2 • • • (9) SensorValue • • • • 0, 1, 2 • • 0, 1, 2 0, 1, 2 • • • (10) Sensor Type Source(Number in cells below) • • • • 0, 1, 2 • • 0, 1, 2 0, 1, 2 • • • (11) Sensor Mode • • • • 0, 1, 2 • • 0, 1, 2 0, 1, 2 • • • R(12) Sensor Raw • • • • 0, 1, 2 • • 0, 1, 2 0, 1, 2 • • • R(13) Sensor Bool. • • 0 • 0 • • 0 0 • • • R(14) Watch • • • • 0 • • 0 0 • • • PBMessage R(15) • • • • • • • • 0 • • • C(16) AGC • • • • 0 • • • • • • • • • • 0 - 31 0 - 31 • • • 0, 1 • VarNo • 0, 1, 2 MotorList • • • • 0-3 • • • • • • • > < = <> 0 1 2 3 RelOp • • • • • • • • • 0-10.000 mS • • Time 30 • PBPowerDownTime( Time ) (immidiateRetries, downloadRetries) C OnWaitDifferent ( MotorList, Number0, Number1, Number2, Time ) C OnWait ( MotorList, Number, Time ) R PBTxPower( Number ) R UploadDatalog( From, Size ) R SetDatalog( Size ) R SetWatch( Hours, Minutes ) -7 -> 7 -7 -> 7 0-1 • • • 0-4 • 0–3 ClearTimer( Number ) [100mS !!] 0-255 [100mS !!] 0-255 • • • • • 1 – 255[min] 0=forever [10mS] 1-255 • • 0–5 0, 1, 2 • • Time PlayTone( Frequency, Time ) PlaySystemSound( Number ) SetSensorType ( Number, Type ) ClearSensorValue( Number ) SetSensorMode ( Number, Mode, Slope ) R SelectPrgm( Number ) R -7 -> 7 R: 0 - 7 C Drive( Number0, Number1 ) C: 0 – 3 Gosub( Number ) R: 0 - 9 C: 0 - 3 Number BeginOfSub( Number ) DeleteSub( Number ) BeginOfTask( Number ) DeleteTask( Number ) StartTask( Number ) ResumeTask( Number ) StopTask( Number ) Command • • • • • • • • • • • • • • • • • • R: 0 – 4 • • • 0: None 1: Switch 2: Temperature 3: Reflection 4: Angle 5: ID0 Switch@ 6: ID1 Switch@ 7: ID2 Switch@ Only use for Poll of Sensor-Type (CyberMaster) @) Type • 1-20,000 • • • • Freq • • • • • • • • • • 0 – 31 • • • 0: Absolute 1-31: Dynamic Slope • • • • • • • • • • SetRetransmitRetries R: 0 - 7 C: 0 - 4 • • • 0: Raw 1: Boolean 2: Trans. Counter 3: Period Counter 4: Percent 5: Celsius 6: Fahrenheit 7: Angle Mode • • • • 0 – available Memory • • • • • • • • • • From • • • 1 - 50 1 - available Memory 0 ~ delete log area • • • • • • • • • Size • • • • • 0 - 23 • • • • • • • • Hours • • • • • 0 - 59 • • • • • • • • Min. 0, 1, 2 0, 1, 2 • • • • • • • • • • • • Motorlist 0-32767 • • • • • • • • • • • • • • ImmidiateRetries 1-32767 • • • • • • • • • • • • • • DownloadRetries 31 32 Beispiel: Trusty trusty.doc enthält eine Form mit einem Spirit-Objekt und einem Knopf, dessen ClickMethode das im ersten Kapitel vorgestellte Programm rebound2 an Trusty schickt: wenn Trusty ein Hindernis berührt, weicht er zurück, dreht und versucht dann wieder vorwärts zu laufen. Die Teile dieses Programms werden editiert, indem man für die verschiedenen Objekte die fettgedruckten Nachrichten wählt und definiert. Im allerersten Abschnitt kann man eine Reihe von nützlichen Konstanten vereinbaren; weitere Konstanten führt man ein, indem man RCXdata.bas als Modul laden läßt. Private Private Private Private Private Private Const Const Const Const Const Const Counter = 0 Forward = 0 Backup = 1 Left = 1 Right = 2 Limit = 3 ' ' ' ' ' ' react 10 times subroutine: go fwd subroutine: turn task: left bumper task: right bumper task: 10 times Private Sub UserForm_Activate() With Trusty.Spirit1 If .InitComm And .TowerAndCableConnected And _ .TowerAlive And .PBAliveOrNot Then Trusty.Load.Enabled = True: Exit Sub Else Trusty.Message.Caption = "RCX or Tower down" End If End With End Sub Private Sub UserForm_Deactivate() With Trusty.Spirit1 .CloseComm End With End Sub Diese beiden Methoden nehmen die Verbindung zum IR-Tower auf und versuchen festzustellen, ob Trusty überhaupt erreichbar ist. Falls ja, wird der Load-Knopf in der Form aktiviert — dann kann man ein Programm laden lassen. 33 Private Sub Load_Click() With Trusty.Spirit1 Trusty.Load.Enabled = False .SelectPrgm SLOT_5 .StopAllTasks ' safety... .BeginOfTask MAIN ' start Trusty forward .PlaySystemSound 2 .SetSensorType SENSOR_1, SWITCH_TYPE .SetSensorType SENSOR_3, SWITCH_TYPE .SetSensorMode SENSOR_1, BOOL_MODE, 0 .SetSensorMode SENSOR_3, BOOL_MODE, 0 .SetVar Counter, CON, 0 .StartTask Left .StartTask Right .StartTask Limit .GoSub Forward .On "02" .EndOfTask .BeginOfTask Left ' act on left bumper .Loop CON, 0 ' forever .If BOOL, SENSOR_1, EQ, CON, 1 .GoSub Backup .AlterDir "0" .GoSub Forward .EndIf .EndLoop .EndOfTask .BeginOfTask Right ' act on right bumper .Loop CON, 0 ' forever .If BOOL, SENSOR_3, EQ, CON, 1 .GoSub Backup .AlterDir "2" .GoSub Forward .EndIf .EndLoop .EndOfTask .BeginOfTask Limit ' act on Counter .While VAR, Counter, LT, CON, 10 .Wait CON, 1 ' 10 msec .EndWhile .PlaySystemSound 3 .PBTurnOff .EndOfTask .BeginOfSub Forward ' motors forward, count up .Wait CON, 50 ' 500 msec .SetFwd "02" .SetPower "02", CON, 4 .SumVar Counter, CON, 1 .EndOfSub .BeginOfSub Backup ' back off .AlterDir "02" .SetPower "02", CON, 1 .Wait CON, 10 ' 100 msec .EndOfSub .Wait CON, 500 ' 5 sec for download to complete .PlaySystemSound 1 Trusty.Load.Enabled = True End With End Sub 34 Als Feedback wird der Load-Knopf während der Übertragung inaktiviert und danach wieder aktiviert. Programm SLOT_5 wird überschrieben. Das neue Programm funktioniert wie die WatcherVersion aus Kapitel 1, allerdings werden aus den Watcher-Bausteinen Tasks mit EndlosSchleifen. Das Programm funktioniert, weil die Tasks abwechselnd ausgeführt werden. MAIN ist Task 0, wird also beim Start von Programm 5 gestartet. Hier konfiguriert man die Eingänge als binäre Schalter und initialisiert die Variable Counter auf 0. Anschließend startet man die nötigen Tasks und schließlich wird Trusty vorwärts in Marsch gesetzt. Die Tasks Left und Right kümmern sich um die Schalter. Ist ein Schalter gedrückt, marschiert Trusty zuerst rückwärts und dreht dann auf der Stelle. Prinzipiell könnte man Variablen als Unterprogramm-Parameter verwenden und damit den Code von Left und Right zusammenfassen, aber da die Motoren als Strings spezifiziert werden müssen, wird der resultierende Code nicht einfacher. Limit wartet, bis Counter genügend groß ist, und schaltet dann den RCX ab. Forward und Backup sind Unterprogramme, die die Motoren steuern und Counter hochzählen. Sie sind den Makro-Bausteinen aus Kapitel 1 nachempfunden. Fazit Mit Visual Basic kommt man schnell zu einer Oberfläche, mit der man leicht auf Eingaben vom Benutzer reagieren kann — das liegt daran, daß der Editor das Anlegen von Methoden ganz gut unterstützt. Der Umgang mit spirit.ocx ist eher unangenehm, und das hat wenig mit Visual Basic zu tun. Die Spezifikation von Werten über source ist zwar mächtig, ist aber selbst mit den definierten Konstanten reichlich mühsam zu lesen und zu schreiben. Da man viele Anweisungen wahlweise speichern oder sofort ausführen lassen kann, und da es eine sehr flexible Methode .Poll gibt, mit der man eine Vielzahl von Werten aus dem RCX holen kann (hier ist source sehr praktisch), kann man den RCX durchaus auch fernsteuern. Dies geht im Prinzip sogar Event-getrieben, denn .SetEvent veranlaßt den RCX, regelmäßig die Variable 0 zu testen und Änderungen als Events an spirit.ocx zu berichten, die mit einer Methode VariableChange empfangen werden können. 35 3.2 MindControl Eric Brok’s MindControl ist in Visual Basic programmiert und verwendet spirit.ocx, um den RCX interaktiv zu steuern und Programme zu laden. Die Programme haben eine etwas eigenwillige Syntax für die Anweisungen der LEGO-Dokumentation und benutzen verschiedene vordefinierte Wörter, um die Befehle sinnvoller darzustellen. Über File|Download kann man eingebaute und im Dateisystem gespeicherte Programme an den RCX schicken. Das Panel selbst ist recht praktisch, um den RCX interaktiv zu bedienen. 3.3 BrickCommand Brian Cwikla’s BrickCommand ist ebenfalls in Visual Basic programmiert. Auch mit diesem Programm kann man die üblichen Anweisungen als Text bearbeiten und an den RCX schicken. Es gibt Präprozessor-Anweisungen, um Konstanten zu vereinbaren. Die Details kann man aus den Programmbeispielen im Katalog samples lernen. BrickCommand hat verschiedene, sehr praktische Panels, mit denen man .Poll ausprobieren und Musik ausprobieren kann. 36 3.4 Gordon’s Brick Programmer Malcolm S. Powell’s Gordon’s Brick Programmer beruht ebenfalls auf spirit.ocx und ist auch in Visual Basic programmiert. GBP stellt aber ein RCX-Programm als attributierten Baum dar und hat folglich ein völlig anderes Konzept: Ähnlich wie in SmallTalk kann man Teilbäume selektieren und sofort ausführen lassen. Das System verfügt außerdem über sehr detaillierte Hilfen und Dokumentation. 3.5 LOGO Richard C. Maynard hat UCB Logo mit den Vokabeln von spirit.ocx erweitert. Der Zugriff erfolgt über C++. Das Paket BrainStorm enthält sehr knappe Hinweise zur Installation, eine Dokumentation der neuen Wörter und ein paar Beispielprogramme, die allerdings auf COM2 zugreifen. Die Einbettung in LOGO ist nicht so nützlich, wie man zunächst denkt, denn man ist nach wie vor an die Strukturen von spirit.ocx gebunden. 37 3.6 FishFace Ulrich Müller hat verschiedene Pakete im Internet publiziert, mit denen man auch die fischertechnik-Schnittstellen von Visual Basic und anderen Sprachen aus programmieren kann. Zentral sind zwei Module InOut32.dll und FishF532.dll, die man nach Windows\System kopiert und registriert: c> \windows\system\regsvr32 FishF32.dll Anschließend kann man zum Beispiel mit der Visual Basic Control Creation Edition das Projekt FishP532.vbp öffnen und durch Run|Start das folgende Diagnose-Panel anzeigen: Ein Manual führt am Beispiel einer Ampelsteuerung schrittweise in die Programmierung ein. Parallel dazu lädt man das Projekt FishKurs.vbp und wählt bei Project|Properties als Startup Object nacheinander die verschiedenen Forms mit immer komplizierteren Steuerungen. 38 39 4 Bytecodes Fischer hat für das Intelligent Interface ein sehr einfaches Byte-Protokoll offengelegt, mit dem man von einem Computer über eine serielle Schnittstelle das Interface kontrollieren kann. Die Dokumentation des Universal Interface enthält Zeichnungen der nötigen Signalfolgen. Der RCX wurde nach allen Regeln der Kunst disassembliert. Im Internet gibt es unter anderem Beschreibungen der Bytecodes und des Paket-Protokolls, mit dem man den RCX direkt über eine serielle Schnittstelle und den IR-Tower ansteuern kann. 4.1 Java Communications API Eine serielle Schnittstelle ist bidirektional und byte-orientiert. In Java müßte man sie als java.io.RandomAccessFile ansprechen, aber man hat dann keine Methoden, mit denen man die Parameter der Schnittstelle (Geschwindigkeit, Bits per Zeichen, Parität, zeitlicher Abstand der Zeichen) einstellen oder andere Signale zwecks Flußkontrolle abfragen kann. Außerdem scheint die Implementierung von RandomAccessFile unter Windows keinen Zugriff auf Geräte wie COM1 zu erlauben. Das Communications API dient bei Java dazu, plattformunabhängig auf serielle und parallele Schnittstellen zuzugreifen. Es gibt eine Implementierung von Sun für Windows und Solaris. Im Internet findet man auch eine Implementierung für Linux. Es zeigt sich, daß die Implementierung von Sun für Windows sehr langsam ist. Die LinuxImplementierung unterstützt nur serielle Schnittstellen, scheint aber schnell genug zu sein, um das Intelligent Interface von Fischer oder den IR-Tower zu betreiben. Architektur Das Communications API definiert ein Interface CommDriver sowie abstrakte Basisklassen ParallelPort und SerialPort, die plattformabhängig implementiert werden müssen. Dazu programmiert man die nötigen Systemaufrufe in C und verbindet sie mit dem Java Native Interface. Ein Benutzerprogramm führt etwa folgende Schritte durch: CommPortIdentifier.getPortIdentifiers() portId.getName() portId.getPortType() portId.open(...) port.getInputStream() port.getOutputStream() port.setSerialPortParams(...) liefert Enumeration. liefert Namen wie COM1 oder /dev/ttya. liefert PORT_SERIAL oder PORT_PARALLEL. liefert CommPort (entweder SerialPort oder ParallelPort) für exklusiven Zugriff. liefern Streams für Byte-Eingabe und Ausgabe. und viele andere Methoden manipulieren Kommunikationseigenschaften getPortIdentifiers() greift auf den CommDriver zu, dessen Klassenname in der Datei javax.comm.properties eingetragen ist. Koexistenz mehrerer CommDriver ist so nicht möglich. Es gibt auch noch Listener-Schnittstellen, mit denen man die Ports beobachten kann. Das Communications API ist reichlich auf die derzeitigen PC-Schnittstellen zugeschnitten, aber es trennt maschinen-abhängige und -unabhängige Aspekte recht gut. 40 Windows Zur Installation packt man die Datei javacomm20-win32.zip von Sun aus und installiert wie in der Dokumentation beschrieben. Für das JDK 1.2.2 handelt es sich um eine installierte Erweiterung mit native Methoden, das heißt, man kopiert zum Beispiel C> copy win32com.dll \jdk1.2.2\jre\bin C> copy comm.jar \jdk1.2.2\jre\lib\ext C> copy javax.comm.properties \jdk1.2.2\jre\lib Diese Implementierung ist sehr langsam. code/4/ft/comm enthält eine Implementierung, die sich genau zum Betrieb beider Interfaces von fischertechnik eignet. Man kann die Implementierung mit Visual C++ 4.2 und dem JDK 1.2.2 übersetzen und dann an Stelle der Sun-Implementierung installieren, zum Beispiel: C> cd ft\comm C> nmake -f nmakefile install oder von Hand: C> C> C> C> copy copy copy echo ft.comm.dll \jdk1.2.2\jre\bin\ft.comm.dll comm.jar.x86 \jdk1.2.2\jre\lib\ext\comm.jar ft.comm.jar \jdk1.2.2\jre\lib\ext Driver=ft.comm.Driver >\jdk1.2.2\jre\lib\javax.comm.properties Das JDK 1.2.2 ist nötig, denn das JDK 1.3.0 kann ft.ScaledView nicht übersetzen. Linux Zur Installation packt man zunächst die Datei javacomm20-x86.tar.Z von Sun aus und installiert Archiv und Konfiguration bei JDK 1.2.2 als installierte Extension, zum Beispiel: # cp comm.jar /usr/local/java/jre/lib/ext/comm.jar # cp javax.comm.properties /usr/local/java/jre/lib/javax.comm.properties Anschließend packt man die Datei rxtx-1.3-13.tar.gz für Linux aus und übersetzt und installiert wie in der Datei INSTALL beschrieben — dabei muß sich java/bin auf PATH befinden, zum Beispiel: # cd rxtx-1.3-13; mkdir build; cd build # ../configure --prefix=/usr # make install Mit dieser Implementierung kann man über serielle Schnittstellen wie /dev/ttyS0 zum Beispiel mit dem Intelligent Interface von fischertechnik oder mit dem IR-Tower und dem RCX kommunizieren. 41 4.2 Java und fischertechnik Folgendes hexadezimale Byte-Protokoll ist für das Intelligent Interface offengelegt: Motorzustand setzen, digitale Eingänge lesen c5 mm Motorzustand setzen, tt x1 x2 digitale Eingänge und EX lesen c9 mm Motorzustand setzen, tt y1 y2 digitale Eingänge und EY lesen c1 mm tt Soll ein Java-Programm auf äußere Einflüsse reagieren, muß man die Eingänge möglichst häufig lesen und in einem Objekt im Programm speichern, das das Modell repräsentiert. Die wesentliche Klasse ist ft.Interface. Eine Instanz ist über das Communications API mit einer Schnittstelle verbunden und hält den Zustand des angeschlossenen Geräts in einer Reihe von Objekten innerer Klassen. Ein Thread sorgt in der Instanz dafür, daß der Zustand möglichst aktuell gehalten wird. Mit dem Hauptprogramm kann man die Geschwindigkeit der Aktualisierung testen. Eine Vielzahl von Parametern des Pakets sind über eine Properties-Datei ft.properties kontrollierbar, die sich im aktuellen Katalog befinden muß, deshalb kann man die Leistung mit oder ohne Zugriff auf die analogen Eingänge leicht messen: C> java ft.Interface COM1 100 8 true 1 100 in 20050 msec, 4 Hz Mit dem Communications API von Sun kann ein Java-Programm nur 4 mal pro Sekunde den Zustand der Eingänge erfahren — das reicht natürlich nicht. ft.comm Es ist relativ einfach, den Teil des Communications API für serielle Schnittstellen unter Windows zu implementieren, den man braucht, um darüber von Java aus das Intelligent Interface zu betreiben. ft.comm.Driver implements javax.comm.CommDriver initialize() wird von CommPortIdentifier aufgerufen und lädt aus der Properties-Datei ft.properties unter anderem eine Liste von Namen wie COM1, die als Port-Namen hinterlegt werden und die unter Windows als Dateinamen zum Zugriff auf serielle Schnittstellen geeignet sein müssen. getCommPort() wird später von CommPortIdentifier aufgerufen und muß zu einem Port- Namen dann ein Port-Objekt liefern. ft.comm.SerialPort extends javax.comm.SerialPort SerialPort() erhält den Port-Namen und konstruiert zwei Byte-Ströme zum Zugriff auf die Schnittstelle, die dann getInputStream() und getOutputStream() als Resultat liefern. Alle anderen Methoden sind trivial implementiert und verbieten so weit als möglich alle anderen Operationen. Die Übertragungsparameter sind unveränderlich 9600 Baud, 8 Bit, keine Parität und 1 Stop-Bit. Der wesentliche Geschwindigkeitsvorteil gegenüber der Referenzimplementierung von Sun entsteht offenbar dadurch, daß man für diesen speziellen Zweck auf alle Listener verzichten kann. 42 ft.comm.SerialInputStream extends java.io.InputStream SerialInputStream() erhält den Port-Namen und notiert einen Windows-Handle zum Zugriff auf die Schnittstelle. Die verschiedenen read-Methoden liefern Bytes. ft.comm.SerialOutputStream extends java.io.OutputStream SerialOutputStream() erhält den Port-Namen und notiert einen Windows-Handle zum Zugriff auf die Schnittstelle. Die verschiedenen write-Methoden akzeptieren Bytes. ft.comm.dll Die wesentlichen Methoden dieser Klassen werden in einer Bibliothek mit dem Java Native Interface in C implementiert und verwenden Windows Funktionen. Die C-Deklarationen für die Java-Methoden kann man sich von javah generieren lassen. Die Bibliothek wird von ft.comm.Driver beim ersten Zugriff dynamisch geladen. ft.comm.ParallelPort extends javax.comm.ParallelPort Es stellt sich heraus, daß man die Implementierung so konstruieren kann, daß das Universal Interface aus der Sicht von Java unter Windows das gleiche Byte-Protokoll verarbeitet. Unter Windows kann man also mit der gleichen Software wahlweise das Intelligent Interface oder das Universal Interface betreiben. Der einzige Unterschied liegt im Port-Namen für den Zugriff und in der Geschwindigkeit. ParallelPort() erhält den Port-Namen und konstruiert zwei Byte-Ströme zum Zugriff auf die Schnittstelle, die dann getInputStream() und getOutputStream() als Resultat liefern. Der Port-Name muß die hexadezimale Basisadresse einer parallelen Schnittstelle sein, zum Beispiel 378 oder 3bc. Alle anderen Methoden sind trivial implementiert und verbieten so weit als möglich alle anderen Operationen. ft.comm.ParallelOutputStream extends java.io.OutputStream ParallelOutputStream() wird zuerst aufgerufen, erhält den Port-Namen und notiert einen Index zum Zugriff auf die Schnittstelle. Die verschiedenen write-Methoden akzeptieren Bytes. ft.comm.ParallelInputStream extends java.io.InputStream ParallelInputStream() erhält den Port-Namen und notiert einen Index zum Zugriff auf die Schnittstelle. Die verschiedenen read-Methoden liefern Bytes. 43 parallel.c Die wesentlichen Methoden dieser Klassen werden ebenfalls mit dem Java Native Interface in C implementiert und sind Teil von ft.comm.dll. Eine write-Methode muß zwei Bytes schreiben und ruft ftOutput() mit der Port-Adresse und dem zweiten Byte auf. Diese Funktion schreibt Bytes so in das Daten-Register der Schnittstelle, daß folgendes Zeitdiagramm auf den Datenleitungen entsteht: loadOut loadIn dataOut clock triggerX triggerY Der Zeittakt hängt dabei von einer Funktion ftIdle() ab und kann in der Properties-Datei ft.properties beeinflußt werden. Liest eine read-Methode ein Byte, wird ftInput() mit der Port-Adresse aufgerufen. Diese Funktion schreibt Bytes so in das Daten-Register der Schnittstelle und beobachtet das busyBit des Status-Registers, daß folgendes Zeitdiagramm auf den Datenleitungen entsteht: loadOut loadIn dataOut clock triggerX triggerY busy Das beobachtete Byte wird als Resultat geliefert. Man muß den Zeittakt so einstellen, daß die Eingänge E1 und E8 korrekt erkannt werden. 44 Liest eine read-Methode 3 Bytes, wird zuerst ftDigital() und dann ftAnalog() mit der Port-Adresse und einem Wert aufgerufen, der vom letzten Kommando bei ftOutput() abhängt und die Leitung triggerX oder triggerY wählt. Diesmal wird folgendes Zeitdiagramm bearbeitet: loadOut loadIn dataOut clock triggerX triggerY busy Das Resultat entsteht, indem die Zeit beobachtet wird, bis busy wieder im Ruhezustand ist. Dabei wird ebenfalls ftIdle() aufgerufen, aber mit einem größeren Wert, der ebenfalls in ft.properties beeinflußt werden kann. Performance Mit ft.Interface kann man die Geschwindigkeit messen, mit der das Objekt und die Schnittstelle aktualisiert werden: Computer Toshiba 430CDT Pentium 120 MHz System Windows 98 SE JDK 1.2.2_005 HotSpot 2.0rc2 Linux 2.2.13 JDK 1.2.2 (Sun) Sony PCG-F190 Windows 98 SE Pentium II 366 MHz JDK 1.3.0 HotSpot 2.0rc2 Comm API Sun ft.comm rxtx-1.3-13 Sun ft.comm Interface seriell seriell parallel, idle 1 seriell Leistung 4 Hz 97 Hz 298 Hz 99 Hz seriell seriell parallel, idle 10 5 Hz 130 Hz 4500 Hz Die analogen Eingänge sind beim Universal Interface nur mühsam zu messen. Sie blieben bei diesem Test unberücksichtigt. Die mögliche Geschwindigkeit ist beim Universal Interface so hoch, daß man in ft.properties einen Taktgeber vorsehen sollte — idle zu vergrößern bedeutet nur, daß viel CPU-Zeit vergeudet wird. 45 ft Aufbauend auf dem Communications API baut man nach dem Model-View-Controller-Prinzip eine Klassenhierarchie auf, mit der man Modelle beobachten und steuern kann. Model wickelt auf Anfrage einen Algorithmus ab; kapselt Zustand, weiß nichts von Umgebung. View kommuniziert mit Umgebung; stellt Werte dar, die vom Programm oder von der Umgebung geliefert werden, kann Eingaben in Anfragen verwandeln. Controller koordiniert Ablauf; sorgt für Datenfluß von View über Model zu View. In diesem Fall speichert ein Model-Objekt den aktuellen Zustand eines Modells, der über ein Interface beobachtet wird. View- und Controller-Objekte beobachten das Model. Ein Controller wickelt eine Ablaufsteuerung ab, indem er das Model dazu veranlaßt, seinen Zustand zu ändern. ft.Constants Dieses interface vereinbart eine Reihe von konstanten Masken, die als Argumente der verschiedenen Methoden angegeben werden sollten. ft.Interface Dies ist das Model. Eine Instanz ist über das Communications API mit einer Schnittstelle verbunden und hält den Zustand des angeschlossenen Geräts in einer Reihe von inneren Model-Objekten. run() sorgt in der Instanz dafür, daß der Zustand insgesamt möglichst aktuell gehalten und der Observer-Thread benachrichtigt wird. Über ft.properties kann ein weiterer Thread erzeugt werden, der eine untere Zeitschranke für einen Aktualisierungszyklus setzt. Mit main() kann man die Geschwindigkeit der Aktualisierung testen. Eine Vielzahl von Parametern des Pakets sind über ft.properties kontrollierbar, deshalb kann man auch zum Beispiel die Leistung bei Zugriff auf die analogen Eingänge leicht messen — das kostet beim Intelligent Interface etwa 20%. Die Klassenmethode open(portname) liefert eine Interface-Instanz für eine Schnittstelle wie COM1. Ein Interface kann Observer benachrichtigen, wenn sich der Zustand seines Geräts ändert oder ein Aktualisierungszyklus beendet ist. Verschiedene Observer können jederzeit mit addObserver(mask, observer) angeschlossen oder mit removeObserver(mask, observer) entfernt werden. Wenn Observer angemeldet sind, benachrichtigt sie nacheinander ein Dämon-Thread. Die Observer sollten möglichst schnell reagieren, damit dieser Thread einigermaßen aktuell bleibt; sie sollten aber die Aktualisierung der InterfaceInstanz selbst nicht beeinflussen können. Ein Observer muß ein spezifisches interface implementieren, das jeweils eine edgeMethode definiert, mit der er benachrichtigt wird. 46 ft.Interface.MotorObserver edge(iface, motor, state) wird aufgerufen, wenn sich die Laufrichtung eines Motors ändert, und berichtet die neue Laufrichtung. set(motors, state) legt in ft.Interface eine neue Laufrichtung für einen oder mehrere Motoren fest. Vermutlich sollte eine asynchrone get-Methode definiert werden. ft.Interface.DigitalObserver edge(iface, sensor, state, count) wird aufgerufen, wenn sich der Zustand eines digitalen Eingangs ändert, und berichtet den neuen Zustand und den aktuellen Wert eines Zählers, der jedesmal geändert wird, wenn sich der Zustand ändert. set(sensors, count, delta) setzt in ft.Interface für einen oder mehrere digitale Eingänge den aktuellen Zählerwert und den Wert, der bei einer Änderung addiert oder subtrahiert wird. setDelta(sensors, delta) setzt nur den Änderungswert. Aktuellen Zustand und Zähler eines Eingangs liefern isSet(sensor) und isAt(sensor) in ft.Interface. ft.Interface.AnalogObserver edge(iface, sensor, value) wird aufgerufen, wenn sich der skalierte Wert eines analogen Eingangs ändert, und berichtet den aktuellen, skalierten Wert des Eingangs. getScale(sensor) liefert in ft.Interface die Koeffizienten a und b, mit denen ein analoger Eingang linear skaliert wird. setScale(sensors, a, b) setzt in ft.Interface diese Koeffizienten für einen oder mehrere Eingänge. Ein analoger Eingang wird nur abgefragt, wenn ein AnalogObserver dafür angemeldet ist. Das Universal Interface wird wesentlich langsamer, wenn man analoge Eingänge berücksichtigen muß. In einem Zyklus wird mindestens der Motor-Zustand gesetzt und der Zustand der digitalen Eingaben abgefragt. setQuery(count) setzt in ft.Interface die Anzahl Zyklen, innerhalb derer der Zustand der analogen Eingaben einmal abgefragt wird. getQuery() liefert in ft.Interface diese Anzahl; typisch ist der Wert 10, das heißt, daß die analogen Eingaben nur mit etwa 10 Hz aktualisiert werden. Werden sie ständig aktualisiert, sinkt die Leistung insgesamt um etwa 20%. ft.Interface.CycleObserver edge(iface, cycles) wird aufgerufen, wenn ein Aktualisierungszyklus beendet ist, und berichtet den aktuellen Wert des Zyklenzählers. set(cycles) setzt in ft.Interface den aktuellen Zählerwert. Vermutlich sollte eine asynchrone get-Methode definiert werden. 47 Grafische Bausteine Eine Reihe von JavaBeans auf der Basis des Abstract Window Toolkit können als Observer angemeldet werden. Sie sind Views oder auch Controller und berücksichtigen Konfigurationsparameter in der Properties-Datei ft.properties. ft.TouchView stellt den Zustand einer digitalen Eingabe dar. ft.EdgeView enthält eine TouchView und zeigt außerdem den aktuellen Zählerstand an. ft.EdgeButton erweitert EdgeView; man kann den Zählerwert eingeben und über einen Knopf kontrollieren, ob jeweils aufwärts oder abwärts gezählt werden soll. ft.MotorView stellt die Laufrichtung eines Motors dar. ft.MotorButton erweitert MotorView; man kann per Maus-Click einen Motor links- oder rechts-laufen lassen. ft.StepMotor erweitert MotorButton und kombiniert einen Motor mit zwei digitalen Eingängen; eine TouchView zeigt den Nullpunkt und eine EdgeView zeigt die aktuelle Position des Motors. Man kann per Maus-Click einen Motor links- oder rechts-laufen lassen, allerdings nur zwischen dem Nullpunkt und einem Grenzwert. ft.Stepper erweitert StepMotor; mit home() und position(count) kann man positionieren, isRunning() zeigt, ob der Ausgang noch beschäftigt ist. Ein Stepper schickt sich notifyAll(), wenn er nicht mehr beschäftigt ist. ft.AnalogView stellt den Zustand einer analogen Eingabe dar. ft.ScaledView erweitert AnalogView und enthält einen Knopf mit einem modalen Dialog, mit dem man die Skalierung und die Zyklusrate sehen und ändern kann. 48 ft.Diagnose extends java.awt.Panel Diese Klasse implementiert ein Panel, das mit EdgeButton-, MotorButton- und ScaledView-Objekten ein Interface beobachtet und kontrolliert.: C> java ft.Diagnose COM1 $ java ft.Diagnose /dev/ttyS0 In ft.properties kann man die Anzeige der analogen Eingänge ausblenden. Mit main() kann man Modelle manuell betreiben und kritische Werte kalibrieren. Diagnose könnte in einer Web-Seite als Applet betrieben werden, wenn man dem Applet Zugriff auf das Communications API ermöglicht. Insgesamt könnte man eine Umgebung realisieren, bei der Steuerungen in Web-Seiten als Hilfesystem eingebettet sind. ft.industry.Record extends java.awt.Panel Diese Klasse implementiert ein Panel, das mit StepMotor-Objekten ein Interface beobachtet und kontrolliert. Drückt man auf record, wird die aktuelle Position in die StandardAusgabe geschrieben — Record ist eine rudimentäre, nicht konfigurierbare Teach-In Applikation. C> java ft.industry.Record COM1 1 1 1 1 17 9 9 7 9 9 9 7 Mit main() kann man ein Modell manuell betreiben und ein Skript erstellen. ft.industry.Play extends java.awt.Panel Diese Klasse implementiert ein Panel, das mit Stepper-Objekten ein Interface beobachtet und kontrolliert. record gibt es hier nicht, denn die Stepper sollen von der Standard-Eingabe her positioniert werden — Play ist ein rudimentärer, nicht konfigurierbarer Abspieler für Record-Skripte. C> java ft.industry.Record COM1 >skript C> java ft.industry.Play COM1 <skript Mit main() kann man ein Modell durch ein Skript betreiben. 49 Steuerungen Ein objekt-orientiertes Steuerprogramm für ein Fahrzeug wie Trusty sollte Java mit Objekten und Methoden kombinieren, die sich auf Ein- und Ausgänge beziehen. Das kann ungefähr so aussehen: 4/ft/mobile/Trusty.java // Trusty.java package ft.mobile; import ft.Controller; import ft.Interface; /** simple control program for a moving robot that can back off. */ public class Trusty extends Controller { public Trusty (Interface iface) { super(iface); } /** m1, m2 run forward. if e3 (or e4) is open, back off, turn away, and go forward again. */ public void run () { m1.left(); m2.left(); for (int count = 0; count < 10; ) if (!e3.isOn()) { turn(m1, m2, e1); ++ count; } else if (!e4.isOn()) { turn(m2, m1, e2); ++ count; } m1.off(); m2.off(); } /** use impulse counter to back off, turn away from motor a. */ protected void turn (Output a, Output b, Input t) { a.right(); b.right(); t.count(8); a.left(); t.count(2); b.left(); } } $ java ft.Controller /dev/ttyS0 ft.mobile.Trusty Damit das funktioniert, stellt ft.Controller entsprechende Objekte und Methoden bereit, lädt im Hauptprogramm ein Interface-Objekt mit einem eigenen Thread sowie eine Instanz des Steuerprogramms und führt run() erst dann im main-Thread aus, wenn dies mit dem start-Knopf freigegeben wird. 50 Auch der Sortierer (Pneumatik) kann in Java programmiert werden: 4/ft/pneumatic/Sorter.java /** process about 5 wheels. */ public void run () { if (diodes) left(M2); // start compressor and lamp if (isOff(E2)) { // retract piston: wait 1 edge left(M4); e2.count(1); off(M4); } sleep(recover); // additional pressure recovery period moveSlider(OFF); // center slider for (int wheel = 0; wheel < 7; ++ wheel) { boolean white = isOn(E1); cyclePiston(); moveSlider(white ? LEFT : RIGHT); moveSlider(OFF); } off(Mall); } /** run piston back and forth once -- E2 must go off and back on. */ protected void cyclePiston () { right(M4); e2.count(2); off(M4); } /** move slider. */ protected void moveSlider (int how) { sleep(recover); switch (how) { case LEFT: left(M1); break; case RIGHT: if (diodes) right(M1); else right(M2); break; case OFF: left(M3); } sleep(press); off(M1|M3); if (!diodes) off(M2); } /** sleep in tenths of seconds. */ protected void sleep (int tsec) { try { Thread.sleep(tsec*100); } catch (InterruptedException e) { } } Hier sieht man einen weniger objekt-orientierten Programmierstil, bei dem die Aus- und Eingänge zumeist mit Masken durch Methoden in ft.Controller angesprochen werden. 51 Sorter liest Optionen in ft.properties, die die Reaktionszeiten der Zylinder festlegen, und berücksichtigt, ob die Ventile über Dioden angeschlossen sind. 4/ft/pneumatic/Sorter.java /** true if wired with diodes */ protected boolean diodes; /** pressure time for slider */ protected int press; /** recovery time for compressor */ protected int recover; static { Interface.class.getName(); } // load properties { String prefix = getClass().getName(); press = Integer.getInteger(prefix+".press", 10).intValue(); recover = Integer.getInteger(prefix+".recover", 20).intValue(); diodes = Boolean.getBoolean(prefix+".diodes"); } } Fazit Man kann Geräte nach dem Model-View-Controller-Muster objekt-orientiert programmieren. Interface agiert als Model, die grafischen Bausteine sind Views. Ein Steuerprogramm für ein spezielles Gerät muß man als Controller ansehen, der sich als Observer am Interface anmeldet und dann das Interface beeinflußt. Passive Views können gleichzeitig als Observer angemeldet sein und den Zustand eines Geräts schildern. Durch die Koppelung über Thread-Synchronisation und das Observer-Modell erhält man eine geringe Systembelastung und eine hohe Wiederverwendbarkeit der Klassen. Für ein Beispiel wie den Schwenkroboter oder ein Fahrzeug mit Sensoren sollte man untersuchen, ob man den Controller in weitere JavaBeans zerlegen kann, die man vielleicht nur mit einer einfachen Skripting-Sprache (Properties, endlicher Automat als Tabelle etc.) konfigurieren müßte. Interface ist im Code auf ein Intelligent oder Universal Interface ohne zusätzlichen Adapter zugeschnitten. Es ließe sich leicht auf einen Adapter mit weiteren Eingaben und Ausgaben erweitern — wenn offengelegt wäre, wie die überhaupt angesteuert werden. Verschiedene Erweiterungen sind denkbar. Man kann Interface verteilen, das heißt, daß man den Interface-Thread in einem mobilen Prozessor abwickelt und die Observer über ein Protokoll (Funk, IR, 10baseT etc.) verbindet. Man sollte dann aber, ähnlich wie beim Skalieren der analogen Eingaben, eine programmierbare Filterung der Observer-Events vorsehen, um das Protokoll zu entlasten. Da jedoch die Systemlast durch Interface für das Intelligent Interface sehr klein ist, ist unklar, wann sich das überhaupt lohnt. Vermutlich kann man Interface über das Communications API auch mit dem RCX verbinden — wenn man die Konfiguration von Interface etwas variabler gestaltet. Gelingt das, müßten Views und Controller austauschbar auch für LEGO zu verwenden sein... 52 4.3 RCX Internals Russell Nelson pflegt die LEGO Mindstorms Internals im Internet, über die inoffizielle Informationen zum RCX zugänglich sind. Dort kann man eine rudimentäre Beschreibung des Paket-Protokolls finden, das zwischen Computer und IR-Tower über eine serielle Schnittstelle abgewickelt wird. Eine Anfrage enthält einen Kopf (0x55 0xff 0x00), der vermutlich den IR-Empfänger aufwecken soll. Dann folgen Daten, wobei jedem Byte sein Komplement folgt. Den Abschluß bildet ein Byte mit der Summe aller Daten-Bytes (ohne Komplemente) und ein Byte mit dem Komplement der Summe. Das Paket enthält gleich viele 1- und 0-Bits — das bietet vermutlich Vorteile zur Entstörung der IR-Übertragung. Eine Antwort wiederholt die Anfrage und enthält dann wieder Kopf, Daten mit Komplementen, und Summe der Daten mit Komplement. Da die Übertragung sehr langsam ist (2400 Baud, 8 Datenbits, ungerade Parität und ein Stopbit), ist das Protokoll insgesamt träge. Kekoa Proudfoot hat seinen RCX zerlegt, analysiert und die Details im Internet zugänglich gemacht. Man findet dort eine Beschreibung der Hard- und Firmware und insbesondere eine Liste der Bytecodes, die man an den RCX mit LEGO’s normaler Firmware schicken kann. Eine Anfrage enthält als erstes Byte einen Befehl wie setmotor (0x21) und davon abhängig Argumente, zum Beispiel 0x82, um den mittleren Motor (0x02) einzuschalten (0x80). Die Daten der Antwort enthalten als erstes Byte das Komplement des Befehls-Bytes und dann je nach Befehl einige Argumente. Beispielsweise erhält man zum Befehl getvalue (0x12) mit den Argumenten senraw (0x0c) und Eingang 1 (0x00) Werte wie 0x2e 0x00 oder 0xfd 0x03 — hier enthält die Antwort einen short-Wert, dessen insignifikantes Byte zuerst übertragen wird. Aus der Befehlstabelle kann man die Länge einer Anfrage und der zugehörigen Antwort eindeutig bestimmen. Kommt keine vollständige Antwort, wiederholt man die gleiche Anfrage. Zwei aufeinanderfolgende Anfragen dürfen daher nicht das gleiche Befehls-Byte enthalten. Um das zu erreichen, komplementiert man das Bit 0x08 in diesem Byte — beide Codes sind äquivalent. 53 rcx.RCXLoader Bei Dario Laverde’s rcx.RCXLoader gibt man in einem Textfeld hexadezimal Bytes ein, die dann an den RCX übertragen werden. Die möglichen Bytes findet man in einer Tabelle. C> echo port = COM1 >parameters.txt C> java rcx.RCXLoader Da das Programm in Java geschrieben ist und das Java Communications API verwendet, kann man es auf verschiedenen Plattformen verwenden. Die Klasse rcx.RCXPort könnte man auch anders einsetzen. rcxport.RCXPort Scott B. Lewis’ rcxport.RCXPort verwendet ebenfalls das Java Communications API dazu, Bytes an den RCX zu übertragen. Dieses System enthält jedoch Klassen, um Programme und Tasks zu modellieren. Von der Kommandozeile werden die Bytes als Programm übertragen und sofort ausgeführt. C> java rcxport.RCXPort -raw 51 3 Opening port COM1...done. Raw codes: 51 3 Downloading program 5 to RCX...done. In diesem Fall hört man dann eine Melodie. lego.comm lego.comm.OutputStream ist ein FilterOutputStream, der jeweils einen Befehl per write- Methode akzeptiert und ein codiertes Paket weiterreicht. lego.comm.InputStream ist ein FilterInputStream, der den zugehörigen OutputStream kennen muß und per read-Methode die Antwortdaten abliefert. lego.Test baut eine Verbindung zu einem SerialPort auf, fügt die lego.comm-Ströme ein und überträgt eine Byte-Folge von der Kommandozeile. In der Properties-Datei lego.properties kann man eine Ablaufverfolgung verlangen. Die Datei enthält auch Namen für einige Codes; außerdem kann man Bytes dezimal, oktal oder hezadezimal sowie mit dem Suffix L auch short-Werte angeben. Man sieht speziell in InputStream, wie schwierig es ist, mit defekten Antworten wirklich zuverlässig fertig zu werden. 54 55 5 Not Quite C David Baum hat eine Sprache Not Quite C (nqc) implementiert, die die Möglichkeiten der LEGO-Firmware im RCX wesentlich besser erschließt, als dies das RIS vermag. nqc beruht auf Bytecodes und ist für Windows, Linux und MacOS verfügbar. Von Mark Overmars gibt es für Windows eine integrierte Entwicklungsumgebung RCX Command Center dazu. 5.1 Minerva Knudsen’s Minerva (Videos: AVI mp4 (438k), cinepak (7.1m); Quicktime (4.6m)) sucht mit einem Lichsensor nach dunklen Bausteinen, greift sie und bringt sie zum Ausgangspunkt zurück. Minerva verwendet nur zwei Motoren: Motor C treibt eine Schnecke, die sich lose auf einer Achse verschieben kann. In der schwarz gezeichneten Stellung bewegen sich die Räder entgegengesetzt (die linken Zahnräder berühren sich an der grün gezeichneten Stelle). Kehrt man die Laufrichtung des Motors um, weicht die Schnecke aus und erreicht die blau gezeichnete Stellung. Dann werden die Räder in der gleichen Richtung angetrieben. Schnecke Schnecke [Die Maße sich nicht exakt und alles wurde eben dargestellt.] Das Bild zeigt die Schnecke fast in der oberen Position. 56 Schnecke Antrieb fürGreifer Motor A treibt über eine Schnecke und ein Zahnrad eine Achse, die über einige Kegelräder die Greifer führt. Klappen die Greifer zusammen, fixieren sie den Greiferarm auf der Achse und der Arm wird angehoben. Kehrt man die Laufrichtung des Motors um, senkt sich der Arm, bis er auf den gleichzeitig angehobenen Exzentern ruht. Dreht sich der Motor weiter, drehen sich die Kegelräder und geben die Greifer wieder frei. [Greiferarm und Exzenter befinden sich auf der gleichen Seite.] Exzenter Navigation Minerva mißt die Fahrzeit, bis sie vom Startpunkt aus einen Baustein findet. Um 180 Grad zu drehen, läßt Knudsen den Motor eine typische Zeit lang laufen. Das funktioniert sehr schlecht, wenn Minerva schon am Ausgangspunkt einen Baustein sieht und folglich das Getriebe nicht umschalten muß. In einem Artikel hat Knudsen erklärt, wie man mit dem Rotations-Sensor und einem Differential die Fahrtrichtung kontrollieren kann: In dieser Anordnung von Kegelrädern drehen sich die linke und rechte Achse genau gleich schnell in entgegengesetzten Richtungen. Bewegen sich die Achsen in gleicher Richtung, müßte sich das mittlere Kegelrad mit den anderen um die beiden Achsen drehen. Ein Differential ist ein Gehäuse, das insbesondere das mittlere Kegelrad tragen und sich auf den beiden äußeren Achsen drehen kann. Das Differential steht still, wenn sich die äußeren Achsen gleich schnell entgegengesetzt drehen. Es dreht sich insbesondere, wenn sich die äußeren Achsen gleich drehen — das wird im Auto zum Antreiben benützt, 57 Rotation Man kann die Anordnung als Meßgerät benützen, wenn man ein Rad von Minerva direkt und das andere genau umgekehrt mit den äußeren Achsen des Differentials verbindet und so die Differenz der Reifenumdrehungen mißt, wenn Minerva dreht. Einbau des Differentials, außen mit längeren Trägern. Einbau des Rotations-Sensors, RCX höher. 58 5.2 Programmierung Minerva navigiert mit Hilfe von Zeitmessungen. Außerdem wird der Lichtsensor zuerst kalibriert. Man kann dies zwar mit der LEGO-Firmware, aber nicht mehr mit dem RIS programmieren. Knudsen zeigt, wie man Minerva mit nqc betreibt: 5/minerva.nqc /* Main program for Minerva. */ #define #define #define #define #define PIECES COMPASS MOTION ARM GRABBER 5 SENSOR_1 OUT_C SENSOR_3 OUT_A // // // // // Collect five pieces. Rotation sensor on input 1. Drive motor on output C. Arm sensors on input 3. Arm motor on output A. task main() { // Configure sensors, _init sets motors to full power. SetSensor(COMPASS, SENSOR_ROTATION); SetSensor(ARM, SENSOR_LIGHT); calibrate(); int i = 0; while (i < PIECES) { retrieve(i); ++ i; } Off(GRABBER + MOTION); } nqc hat einen Präprozessor mit #include, #define und bedingter Übersetzung. Es gibt Makros mit und ohne Parameter. Makros mit Parametern agieren ähnlich wie Prozeduren, verwenden jedoch Textersatz für die Argumente, liefern ein Resultat, erlauben je nach Kontext kein return und auch keine lokalen Variablen. Ein Programm besteht aus bis zu 10 Tasks. main wird implizit gestartet, die anderen kontrolliert man mit den Anweisungen start taskname ; stop taskname ; nqc verwaltet Namen für bis zu 32 globale und lokale int-Variablen mit Blockstruktur in Tasks und Prozeduren. Die meisten Konstanten und vordefinierten Funktionen stammen aus einer Datei rcx2.nqh. Es gibt die üblichen numerischen Operationen ++ -- - ~ * / % + - << >> & ^ | && || sowie abs() und sign() aber ~ % << >> ^ && || müssen sich auf Konstanten beziehen. Konstanten können dezimal und hexadezimal angegeben werden. Bedingungen und numerische Ausdrücke werden unterschieden. Für Bedingungen gibt es true false und die üblichen Vergleiche == != < <= > >= ! && ||. Zuweisungen sind Anweisungen; es gibt die kombinierten Zuweisungen += -= *= /= &= |= ||= (absoluter Wert) +-= (Vorzeichen). 59 Außer start und stop gibt es folgende Anweisungen: int name , name = expression , ... ; name assignment expression ; name ( arguments... ); if ( condition ) statement if ( condition ) statement else statement switch ( expression ) { case constant : ... default : ... } while ( condition ) statement until ( condition ) statement do statement while ( condition ); repeat ( expression ) statement break ; continue ; return ; nqc unterstützt Prozeduren, die im Code expandiert werden und weitere Prozeduraufrufe enthalten dürfen. 5/minerva.nqc /* Calibrate the light sensor. */ #define SAMPLES int threshold; 10 // Readings to average. void calibrate() { // Take an average light reading. release(); // Arm is up when sensor tries to find something. int i = 0, sum = 0; while (i < SAMPLES) { sum += ARM; Wait(10); ++ i; } threshold = sum / SAMPLES; } void turn(const int& ticks) { // Use rotation sensor and differential to check turn. ClearSensor(COMPASS); OnRev(MOTION); until (COMPASS >= ticks); Off(MOTION); } Prozeduren können Parameter haben: mit den Typen int und const int übergibt man Werte, die vor Übergabe einmal berechnet werden; bei const int kann nur ein konstanter Wert übergeben werden. Für int& und const int& verweist der Parameter auf den Argumentwert, das heißt, bei int& kann nur eine Variable übergeben werden, die in der Prozedur geändert werden kann, bei const int& darf der Parameter nicht verändert werden und der Argumentwert wird für jeden Zugriff neu berechnet. 60 Ein Programm kann bis zu acht parameterlose Unterprogramme enthalten, die sich nicht gegenseitig aufrufen dürfen. 5/minerva.nqc /* Subprograms to operate the grab arm. */ sub grab() { // Run the motor until we hit the limit switch. OnFwd(GRABBER); complete(); } sub release() { // Run the motor until we hit the limit switch. OnRev(GRABBER); complete(); } void complete() { // Keep running until we hit the switch. until (ARM == 100); // Back off from the switch. Toggle(GRABBER); until (ARM != 100); Off(GRABBER); } Minerva verwendet nur einen Eingang, an dem ein Touch- und ein Licht-Sensor parallel angeschlossen sind. Der Licht-Sensor liefert normalerweise Werte zwischen 30 und 70. Wird der Touch-Sensor geschlossen, weil der Greifarm angehoben ist, erhält man am Eingang den Wert 100. Damit anschließend wieder der Licht-Sensor gelesen werden kann, muß man den Greifarm nach dem Anheben etwas absenken. Man sollte Prozeduren zur Strukturierung eines Programms verwenden, und Unterprogramme nur dann, wenn man bei mehrfachen Aufrufen wirklich Code einspart. 61 Minervas eigentliche Arbeit besteht im Suchen und Aufheben eines Bausteins. Dies kann man jetzt aber relativ leicht aus den einzelnen Schritten kombinieren: 5/minerva.nqc /* Retrieve one piece. */ #define TRIGGER #define BACK (-5) 14 // Units relative to threshold to sense piece. // Ticks to turn 180 degrees. void retrieve(const int& i) { // Drive forward until we see something. OnFwd(MOTION); ClearTimer(0); // Measure time to return. until (ARM < threshold + TRIGGER); Wait(20); // Move up on it a little. int returnTime = Timer(0); Off(MOTION); grab(); // Turn around and drive back. turn(BACK); OnFwd(MOTION); ClearTimer(0); until (Timer(0) >= returnTime); Off(MOTION); release(); PlaySound(3); // Announce success. // Turn around into new direction. turn(BACK * (PIECES-i) / PIECES); } Die Navigation auf der Basis eines Timers ist natürlich nicht sehr präzise. Man sollte außerdem größere Anstrengungen unternehmen, um verstreute Bauteile zu finden. 62 5.3 nqc unter Windows oder Linux nqc ist ein Compiler und Lader und wird von der Kommandozeile aus bedient. Für Windows gibt es ein binäres Archiv, für Linux ist zum Beispiel nqc 2.0 bei Suse 6.3 enthalten — um die Quellen zu übersetzen, benötigt man zum Beispiel die Pakete egcs und gpp. C> nqc nqc version 2.1 r1 (built Feb 19 2000, 18:46:16) Copyright (C) 1998-2000 David Baum. All Rights Reserved. Usage: nqc [options] [actions] [ - | filename ] [actions] - : read from stdin instead of a source_file Options: -1: use NQC 1.x compatability mode -T<target>: target can be RCX, CM or Scout -d: download program -n: prevent the system file (rcx.nqh) from being included -D<sym>[=<value>] : define macro <sym> -E[<filename>] : write compiler errors to <filename> (or stdout) -I<path>: search <path> for include files -L[<filename>] : generate code listing to <filename> (or stdout) -O<outfile>: specify output file -S<portname>: specify serial port -U<sym>: undefine macro <sym> Actions: -run: run current program -pgm <number>: select program number -datalog | -datalog_full: upload datalog -near : set IR to near mode -far : set IR to far mode -watch <time> | now : set RCX time -firmware <filename> : download firmware -firmfast <filename> : download firmware at quad speed -sleep <timeout> : set RCX sleep timeout -msg <number> : send IR message to RCX -raw <data> : format data as a packet and send to RCX -remote <value> <repeat> : send a remote command to the RCX -clear : erase all programs and datalog in RCX $ RCX_PORT=/dev/ttyS0 nqc -d -pgm 3 minerva.nqc Downloading Program:...............complete Battery Level = 8.5 V 63 Implementierung nqc erzeugt eine binäre Datei. Den generierten Code kann man mit -L darstellen lassen. Der Präprozessor spielt eine sehr wichtige Rolle, denn die RCX-spezifischen Vokabeln werden mit Makros und Prozeduren realisiert. Mit dem gcc-Präprozessor kann man betrachten, was eigentlich übersetzt wird. Für die Task main in minerva.nqc task main() { SetSensor(COMPASS, SENSOR_ROTATION); SetSensor(ARM, SENSOR_LIGHT); SetPower(GRABBER + MOTION, OUT_FULL); calibrate(); ergibt sich ausschnittsweise $ cat *h *c | gcc -E void SetSensorMode(__sensor sensor, const int mode) { asm { 0x42, &sensor : 0x03000200, mode }; } void SetSensorType(__sensor sensor, const int type) { asm { 0x32, &sensor : 0x03000200, (type) }; } void SetSensor(__sensor sensor, const int tm) { SetSensorType(sensor, tm>>8); SetSensorMode(sensor, tm); } void SetDirection(const int o, const int d) { asm { 0xe1, (o) + (d) }; } void SetPower(const int o, const int &p) { asm { 0x13, (o), &p : 0x1000015}; } void Fwd(const int o) { SetDirection(o, 0x80 ); } void _init() { SetPower((1 << 0) + (1 << 1) + (1 << 2) , 7 ); Fwd((1 << 0) + (1 << 1) + (1 << 2) ); } task main() { SetSensor(@(0x90000 + ( 0 )) , ((( 4 )<<8) + ( 0xe0 )) SetSensor(@(0x90000 + ( 2 )) , ((( 3 )<<8) + ( 0x80 )) SetPower((1 << 0) + (1 << 2) , 7 ); calibrate(); ); ); und daraus wird ausschnittsweise *** 000 004 006 009 012 015 018 022 Task 0 = main pwr ABC, 7 dir ABC, Fwd InType 0, Angle InMode 0, Angle InType 2, Light InMode 2, Percent pwr AC, 7 calls 1 13 e1 32 42 32 42 13 17 07 87 00 00 02 02 05 01 02 07 04 e0 03 80 02 07 Man sieht genau, wie die Symboltabelle mit Funktionen und den Bytecodes geladen wird, und daß sich der Compiler vor allem mit Namen und Kontrollstrukturen beschäftigen muß, sowie mit einer asm-Anweisung mit abenteuerlicher Struktur. 64 5.4 Subsumption nqc erlaubt die Implementierung komplexerer Steuerprogramme. Von Rodney Brooks stammt die Idee, verschiedene, primitive Steuerungen gleichzeitig zu betreiben und durch Prioritäten zu entscheiden, welche zum Zug kommt. Minerva möchte normalerweise vorwärts fahren: 5/subsume.nqc /* Move forward. */ #define CRUISE #define TIMED #define BLACKED #define BUMPED #define TOLD int command = 0; (1 (1 (1 (1 (1 << << << << << 0) // Bits define priorities... 1) // ...must match task decide. 2) 3) 4) task cruise() { command |= CRUISE; } void doCruise() { OnFwd(MOTION); } Mit einer Task mit Wait() kann man Minervas Aktionsradius zeitlich begrenzen: 5/subsume.nqc /* Turn 90 degrees after a while. */ #define #define RADIUS BACK 500 14 task timed() { Wait(RADIUS); command |= TIMED; } void doTimed() { stop timed; turn(BACK/2); command &= ~TIMED; start timed; } // 1/100 secs before TIMED. // Ticks to turn 180 degrees. 65 Vorrangig kann man den Licht-Sensor dazu benützen, eine markierte Arena nicht zu verlassen: 5/subsume.nqc /* Turn around at black line. */ int threshold; #define TRIGGER (-7) // Relative to threshold. task blacked() { until (ARM < threshold + TRIGGER); command |= BLACKED; } void doBlacked() { stop blacked; stop timed; turn(BACK); command &= ~(BLACKED|TIMED); start blacked; start timed; } Bringt man noch einen Touch-Sensor an, kann Minerva auf Anstoßen zum Beispiel durch erschreckten Stillstand reagieren: 5/subsume.nqc /* Wait for bumper. */ #define SHOCK 300 // Ticks to wait if BUMPED. task bumped() { until (BUMP == 1); command |= BUMPED; } void doBumped() { stop bumped; stop timed; Off(MOTION); Wait(SHOCK); until (BUMP == 0); // Wait for bumper reset. command &= ~(BUMPED|TIMED); start bumped; start timed; } 66 Schickt man Minerva eine Nachricht, muß sie anhalten und singen: 5/subsume.nqc /* Wait for message. */ #define #define STOP START 1 3 // Message numbers for TOLD. task told() { do { ClearMessage(); until (Message() > 0); } while (Message() != STOP); command |= TOLD; } void doTold() { stop told; stop timed; Off(MOTION); PlaySound(2); do { ClearMessage(); until (Message() > 0); } while (Message() != START); PlaySound(3); command &= ~(TOLD|TIMED); start told; start timed; } Sie fährt erst bei einer weiteren Nachricht wieder los. Entscheidend ist, daß der RCX diese Tasks wirklich parallel betreibt (preemptive multitasking). main kann dann Prioritäten implementieren und das Verhalten einer Task bevorzugen: 5/subsume.nqc /* Startup and debugging. */ task main() { SetSensor(COMPASS, SENSOR_ROTATION); SetSensor(ARM, SENSOR_LIGHT); SetSensor(BUMP, SENSOR_TOUCH); calibrate(); start start start start start cruise; timed; blacked; bumped; told; while(true) { debug(command); if (false) ; else if ((command else if ((command else if ((command else if ((command else doCruise(); } } & & & & TOLD) != 0) doTold(); BUMPED) != 0) doBumped(); BLACKED) != 0) doBlacked(); TIMED) != 0) doTimed(); 67 /* Debugging procedure, assumes OUT_B unused. */ void debug (const int& value) { SelectDisplay(DISPLAY_OUT_B); SetPower(OUT_B, value-1); On(OUT_B); } debug() demonstriert einen bösen Trick, mit dem man kleine Zahlen im Display anzeigen kann. Die restlichen Prozeduren und Unterprogramme stammen aus minerva.nqc. 68 5.5 Synchronisation Navigator ist der Rover 2 aus dem LEGO Bausatz 9736 Exploration Mars, erweitert um einen Rotations-Sensor zur Navigation und ausgestattet mit zwei Touch-Sensoren. Alternativ könnte man Trusty mit einem Rotations-Sensor ausbauen. Videos: AVI mp4 (174k), cinepak (3.7m); Quicktime (2m). Navigator ist mit Subsumption programmiert und wendet sich wie Trusty von Hindernissen ab. Zusätzlich werden aber Messages ausgewertet: 1 und 3 halten jeweils den linken oder rechten Motor an, bis die Message nochmals eintrifft, das heißt, Navigator dreht sich oder bleibt sogar stehen. Die Messages werden außerdem mit Sounds quittiert. Message 2 macht die im Rotations-Sensor über alle Drehungen akkumulierte Zahl rückgängig, das heißt, Navigator bewegt sich dann wieder in der ursprünglichen Richtung. Das eigentliche Programmierproblem besteht darin, daß es verschiedene Tasks gibt, die alle — mit oder ohne Subsumption — auf eine Message warten. Eine Message muß deshalb zuverlässig allen Interessenten zugestellt werden, bevor die nächste empfangen wird. 69 Algorithmus Der Algorithmus für die Zustellung wird zunächst als Java-Klasse beschrieben und später so gut wie möglich in nqc implementiert. Ein Messenger-Objekt arbeitet als Thread, der Nachrichten an Kunden zustellt: 5/Messenger.java /** distributes a message to up to 32 clients. */ public class Messenger implements Runnable { /** loop to distribute messages: wait for all clients to post done, mark message as empty, wait for next message, post all clients. */ public synchronized void run () { try { for (;;) { empty = true; notifyAll(); while (empty) wait(); done = 0; notifyAll(); while (done != all) wait(); } } catch (InterruptedException e) { } } Ein Auftraggeber wartet durch set(), bis er eine Nachricht hinterlegen kann, die dann sicher an alle Kunden ausgeliefert wird: 5/Messenger.java /** ok to set message. */ protected boolean empty; /** current message. */ protected int message; /** wait until there is room for a message. */ public synchronized void waitForEmpty () throws InterruptedException { while (!empty) wait(); } /** set message: wait until there is room for a message, then set it. @param msg to set. */ public synchronized void set (int message) throws InterruptedException { waitForEmpty(); this.message = message; empty = false; notifyAll(); } Ein Kunde erhält mit login() ein Identifikations-Bit, und gibt es mit logout() wieder frei: 70 5/Messenger.java /** mask of clients. */ protected int all; /** current completions. */ protected int done; /** become a client. @return handle to be used to wait for a message. @throws RuntimeException if no more clients can be accepted. */ public synchronized int login () { for (int handle = 1; handle != 0; handle <<= 1) if ((handle & all) == 0) { all |= handle; done |= handle; return handle; } throw new RuntimeException("no more clients"); } /** validate a handle: one bit, set in all. @param handle to be validated. @throws IllegalArgumentException if handle is invalid. */ public synchronized void validate (int handle) { if ((all & handle) != 0) for (int bit = 1; bit != 0; bit <<= 1) if (bit == handle) return; throw new IllegalArgumentException("bad handle"); } /** cease to be a client. @param handle to be recycled. @throws IllegalArgumentException if handle is invalid. */ public synchronized void logout (int handle) { validate(handle); all &= ~handle; done &= ~handle; } Mit dem Bit kann er durch get() auf eine Nachricht warten und durch done() anzeigen, daß er die Nachricht fertig bearbeitet hat: 5/Messenger.java /** retrieve next message: wait for handle to be cleared in done, take message. @param handle obtained from login(). @return next message. @throws IllegalArgumentException if handle is invalid. */ public synchronized int get (int handle) throws InterruptedException { validate(handle); while ((done & handle) != 0) wait(); return message; } /** done with message: post handle in done. @param handle obtained from login(). @throws IllegalArgumentException if handle is invalid. */ public synchronized void done (int handle) { validate(handle); done |= handle; notifyAll(); } } 71 Entscheidend sind die Identifikations-Bits. Eine Variable all enthält je ein Bit für jeden Kunden. Zu Beginn einer Zustellung wird eine andere Variable done auf 0 gesetzt. get() wartet darauf, daß das relevante Bit in done 0 ist, done() setzt es auf 1. Die Zustellung ist offensichtlich beendet, wenn done und all wieder gleich sind. Da die verschiedenen Tasks (in Java Threads) parallel ausgeführt werden, muß man den Zugriff auf die Variablen all und done so absichern, daß eine Task eine Änderung komplett durchführen kann, bevor eine andere Zugriff erhält. Messenger wird als Monitor verwendet und alle Zugriffe sind mit synchronized abgesichert. Messenger kann mit Demo getestet werden. Clickt man auf einen der drei Knöpfe im SenderPanel, werden alle inaktiviert und eine Zahl wird über einen Thread mit set() an Messenger geschickt, damit der Event-Thread dabei nicht blockiert. Jedes der Receiver-Textfelder hat einen Thread, der mit get() eine Nachricht holt, auf einen Click in das Feld wartet, und die Nachricht mit done() quittiert. Merkt der Sender-Thread mit waitForEmpty(), daß der Messenger frei ist, werden die Knöpfe wieder aktiviert. 72 Implementierung In nqc empfängt eine Task die Messages und stellt sie Interessenten zu: 5/navigator.nqc // message receivers #define breakW (1 << 3) #define musicW (1 << 4) #define fixW (1 << 5) #define ALL // bits for all message receivers combined (breakW|musicW|fixW) int ready = 0; // bit(s) set for task requiring callback int message = 0; // published message int complete = 0; // current completions // distribute messages to up to 16 clients: wait for next message, // post all clients, wait for all clients to post complete. task messenger () { while (true) { ClearMessage(); do message = Message(); while (message == 0); complete = 0; // atomic until (complete == ALL); // atomic } } // wait for next message. void get (const int& handle) { until ((handle & complete) == 0); } // mark completion. void done (const int& handle) { complete |= handle; } // might loop once too many // atomic Man kann sich mit nqc -L davon überzeugen, daß die als atomic markierten Anweisungen in einzelne Bytecodes übersetzt werden. Die Firmware kann die Ausführung eines Bytecodes nicht unterbrechen, das heißt, daß der Zugriff auf complete im Sti von synchronized erfolgt. 73 Die Bremsen werden unabhängig von Subsumption mit einem trivialen endlichen Automaten bearbeitet: 5/navigator.nqc // message 1/3: left/right break pedal task breaks () { int left = 1, right = 1; while (true) { get(breakW); switch (message) { case 1: doBreak(leftM, left); break; case 3: doBreak(rightM, right); break; } done(breakW); } } void doBreak (const int& motor, int& state) { if (state != 0) { Off(motor); state = 0; } else { On(motor); state = 1; } } Die Musik spielt unabhängig dazu als Klient von messenger: 5/navigator.nqc // message 1/3: provide accompaniment task music () { while (true) { get(musicW); if (message == 1) PlaySound(2); if (message == 3) PlaySound(3); done(musicW); } } Die Kurskorrektor erfolgt durch Subsumption — allerdings nur, wenn beide Motoren in Betrieb sind. Merkwürdigerweise kann man in nqc 2.1 den Zustand der Motoren nicht abfragen, aber man kann eigene Makros vereinbaren; @() kombiniert Source und Wert für einen RCX-Zugriff: 5/navigator.nqc // macros for motor state #define MOTOR_A 0 #define MOTOR_B 1 #define MOTOR_C 2 #define isOn(n) ((@(0x30000 + (n)) & 0x80) #define isOff(n) ((@(0x30000 + (n)) & 0xc0) #define isFloat(n) ((@(0x30000 + (n)) & 0xc0) #define isFwd(n) ((@(0x30000 + (n)) & 0x08) #define isRev(n) ((@(0x30000 + (n)) & 0x08) #define Power(n) (@(0x30000 + (n)) & 0x07) #define leftM #define rightM #define moving != == == != == 0) 0x40) 0x00) 0) 0) OUT_C // drive motors on outputs C and A OUT_A (isOn(MOTOR_A) && isOn(MOTOR_C)) 74 Mit moving stellt fix fest, ob die Motoren laufen. Dies erfolgt im Subsumption-Thread, bevor die Nachricht quittiert wird, kann also nicht durch eine andere Nachricht unterbrochen werden. 5/navigator.nqc // message 2: compensate breaking task fix () { while (true) { get(fixW); if (message == 2) break; done(fixW); } ready |= fixW; } void doFix () { stop fix; if (moving) { if (COMPASS > 1) { Toggle(rightM); until (abs(COMPASS) <= 1); Toggle(rightM); } else if (COMPASS < -1) { Toggle(leftM); until (abs(COMPASS) <= 1); Toggle(leftM); } } done(fixW); ready &= ~fixW; start fix; } Die Kurskorrektur akkumuliert ganz von selbst, wenn man den Rotations-Sensor COMPASS nur beim Start löscht. Man muß allerdings die Drehungen beim Zurücksetzen von einem Hindernis dann relativ berechnen: 5/navigator.nqc // watches for collision at right task right () { until (rightB != 1); ready |= rightW; } void doRight () { stop right; backup(COMPASS, COMPASS + turnT/4, rightM, leftM); ready &= ~rightW; start right; } // backup from bumper: reverse motors a and b, // reverse motor a until COMPASS shows ticks, then reverse motor b void backup (int low, int high, const int& a, const int& b) { Toggle(a + b); Wait(backupT); Toggle(a); until (COMPASS < low || COMPASS > high); Toggle(b); } 75 6 P-brick Script Im Frühjahr 2000 hat LEGO eine erweiterte Firmware für den RCX vorgestellt. Diesmal wurden die Bytecodes als LEGO Assembler (LASM) offengelegt. Außerdem gibt es eine ScriptSprache mit Compiler, die auch zum Abspeichern von Programmen aus grafischen Oberflächen dienen soll. Leider ist auch diesmal das Paket-Protokoll nicht offengelegt und die Lizenz warnt davor, daß das Protokoll patentiert werden soll. Da das System auf dynamischen Bibliotheken beruht, die für Windows in C++ implementiert wurden, dürfte das die Akzeptanz negativ beeinflussen. 6.1 ATLClient ATLClient ist eine rudimentäre grafische Oberfläche für Windows, die auf dem neuen System beruht. Zur Installation packt man die Archive aus und integriert die Patches. Man kann Bibliotheken, Firmware, Quellen und Definitionsdateien auf verschiedene Unterkataloge verteilen, wenn man die Datei ATLClient.ini entsprechend editiert. Sie muß sich im gleichen Katalog wie ATLClient — oder im Windows Katalog — befinden. Ebenfalls dort wird nach der dynamischen Bibliothek atl.dll gesucht, die jedoch zu C++ gehört und von LEGO nicht geliefert wird. Will man P-brick Script verwenden, muß man zuerst die neue Firmware in den RCX laden. Sie soll mit alten Programmen kompatibel sein. Anschließend lädt man ein Programm ins Fenster, übersetzt es, und schickt es zum RCX. 76 6.2 Sprachumfang Groß- und Kleinschreibung werden nicht unterschieden. Es gibt beide Arten von JavaKommentaren. Es gibt sehr viele reservierte Wörter. Anweisungen benötigen keine Trenner und müssen nicht auf einzelnen Zeilen stehen. In der Regel gilt declare before use. Abgesehen von der hier skizzierten Programmierung gibt es eine Reihe von Anweisungen, mit denen man direkt mit dem RCX kommunizieren kann. Programmstruktur Die verschiedenen Definitionen müssen ungefähr in der angegebenen Reihenfolge erfolgen. Eine Datei enthält ein Programm: program name { #include <file> const name = number var name [= number] sensor name on 1|2|3 name is unknown|switch|temperature|light|rotation as raw|boolean|transition|periodic|percent|celsius|fahrenheit|angle output name on 1|2|3 timer name // in 1/10 sec counter name [= number] event name when event-condition macro name (parm-name, ...) { commands } main { commands } watcher name monitor event-name, ... { commands } [restart on event] task name { commands } } Sensoren und Outputs müssen benannt und dabei mit einem Port verknüpft werden. Sensoren muß man außerdem konfigurieren. Bereiche wie low und high definiert man durch Zuweisung and Sensor-Properties. Makros werden inline oder als Unterprogramme expandiert. watcher und task werden explizit gestartet und angehalten. Man kann erlauben, daß ein watcher durch einen beobachteten Event unterbrochen wird. Es gibt 32 globale und lokale Variablen sowie insgesamt 10 Tasks. Events beziehen sich entweder auf Eigenschaften von Sensoren oder auf Vergleiche von Werten. Falls nötig, wird implizit eine Task erzeugt, die die Events bewacht und verteilt. event-condition: condition sensor-name . pressed|released|low|normal|high|click|doubleclick condition: value =|<|>|<> value value is [not] number|variable-name .. number|variable-name value: number|variable-name|sensor-name|timer-name|counter-name|message sensor-name . type|raw|value output-name . power|status Eine number wird dezimal oder mit 0x hexadezimal angegeben; statt dessen kann man auch den Namen einer Konstanten verwenden. 77 Anweisungen Es gibt sehr viele Anweisungen: man kann mit Variablen rechnen, Tasks kontrollieren, Events explizit verursachen, Nachrichten verschicken und Motoren und Sounds steuern: local name [= number] clear message|variable-name|timer-name|counter-name|sensor-name name = random number [to number] name =|+=|-=|*=|/=|&=||= expression with + - * / & | ( ) abs() sgn() display value // beobachtet Wert im RCX Display start main|task-name|watcher-name stop main|tasks|task-name|watcher-name wait number|variable-name|random number [to number]// in 1/100 sec fire event-name, ... trigger event-name, ... calibrate (sensor-name) // unbedingt // falls erfüllt // propagiert Bereiche zu Events? send number|variable-name // message on [output-name ...] [for number|random number [to number]]// in 1/100 sec off|forward|fd|backward|bk|reverse [output-name ...] direction|dir [output-name ...] [output-name ...] power [output-name ...] number|variable-name|random number [to number] sound on|off|number tone number|variable-name for number Motoren und Sound gelten als Ressourcen, die eine Task je nach Prioriät einer anderen wegnehmen kann. Mit try kann die betroffene Task die Ausgabe wiederholen, abbrechen oder insgesamt neu beginnen oder aufhören: priority number try { commands } retry|abort|restart|stop on fail Es gibt Kontrollstrukturen für Entscheidungen und Schleifen: if condition { commands } [else { commands }] select value { when number | number .. number { commands } ... [else { commands }] } while condition { commands } repeat number|random number [to number] { commands } forever { commands } In einem watcher oder in monitor gibt es auch Events als Bedingungen: if event-name, ... { commands } [else { commands }] monitor event-name, ... { commands } retry|abort|restart|stop on event repeat { commands } until event-name, ... wait until event-name, ... try und monitor sowie die speziellen Formen repeat und wait until kann man jeweils nicht schachteln. monitor kann durch einen erneuten Event neu gestartet oder abgebrochen werden; alternativ kann man auch die Task neu beginnen oder abbrechen. 78 6.3 Subsumption und Synchronisation Die nahezu frei definierbaren Events, watcher und monitor legen nahe, daß man in P-brick Script Event-orientiert programmieren sollte. Das in Synchronisation diskutierte Beispiel kann bei gleicher Funktionalität etwa so aussehen: 6/navigator.rcx2 // subsumption and synchronization in P-brick Script program Navigator { const turnT = 60 // rotation ticks for 360 degrees const backupT = 100 // clock ticks to backup sensor compass on 1 sensor rightB on 2 sensor leftB on 3 compass is rotation as angle rightB is switch as boolean leftB is switch as boolean output rightM on 1 output leftM on 3 // motor assignments timer beater event event event event event event rightBumped when rightB.released leftBumped when leftB.released beat when beater > 10 // 1 second leftBreak when message = 1 rightBreak when message = 3 fix when message = 2 var a var b event turned when compass is not a..b var motors = 3 // both motors forward Zuerst definiert man empirisch ermittelte Konstanten und beschreibt, an welchen Ports welche Art von Sensoren und Motoren hängen. Da die Touch-Sensoren bei ungehinderter Fahrt aktiv sind, legt man für die Events fest, daß die Sensoren losgelassen werden. Mit dem timer soll häufig ein Ton ausgegeben werden; beat ist der zugehörige Event. Die Nachrichten kann man als verschiedene Events definieren. Die Zustellung erfolgt sichtlich so, daß jeder Event jeder Task zugestellt wird. turned zeigt, daß man einen Event parametrisieren kann. In motors wird der Motorzustand modelliert, auf den die Bremsnachrichten einwirken und der mit niedriger Priorität herbeigeführt wird. 79 Das Hauptprogramm muß vor den anderen Tasks definiert werden. Es wird implizit gestartet: 6/navigator.rcx2 // startup main { global power [leftM rightM] 8 global forward [leftM rightM] // global on [leftM rightM] seems to have no effect clear clear clear sound beater compass message on start start start start start avoids // prio breaks start squeals fixes beats idles 3 // // // // prio prio prio prio 5 6 7 8 } Wenn eine Task auf irgendeinen Output zugreift, steuert sie offenbar dann alle an. global scheint die Effekte der normalen Motor-Befehle global zu kontrollieren: global on/off kontrolliert, ob on/off überhaupt wirkt; global revert kehrt die Richtung der anderen Befehle um; global power setzt das Maximum. Bevor man Töne hören kann, muß man sound einschalten. Das Hauptprogramm startet einige Tasks. avoids kümmert sich um Hindernisse: 6/navigator.rcx2 // bumper watcher: avoid contact watcher avoids monitor leftBumped, rightBumped { priority 3 // relatively high try { if leftBumped { a = compass - turnT/4 b = compass backup(leftM, rightM) } else { a = compass b = compass + turnT/4 backup(rightM, leftM) } } abort on fail } restart on event Dieser watcher wird durch jeden interessanten Event sofort neu angestoßen. Die Priorität ist sehr hoch, das heißt, avoids schafft es eher als die anderen Tasks, die Motoren zu kontrollieren. Je nach Event wird in a und b der Bereich für den turned-Event festgelegt, in dem backup() die Drehung vornehmen soll. 80 Ein Makro wie backup() muß vor dem Hauptprogramm definiert werden: 6/navigator.rcx2 // backup from bumper: reverse motors a and b, // reverse motor a until compass shows ticks, then reverse motor b macro backup (a, b) { on [a b] reverse [a b] wait backupT reverse a wait until turned reverse b } Zwar laufen die Motoren, aber ohne on würde avoids sie abschalten. Die Drehung erfolgt, indem die Motoren entgegengesetzt laufen. breaks kümmert sich um die Bremsnachrichten: 6/navigator.rcx2 // message 1/3 watcher: toggle left/right break pedal watcher breaks monitor leftBreak, rightBreak { priority 5 if leftBreak { select motors { when 0 { motors = 1 } when 1 { motors = 0 } when 2 { motors = 3 } when 3 { motors = 2 } } } else { select motors { when 0 { motors = 2 } when 2 { motors = 0 } when 1 { motors = 3 } when 3 { motors = 1 } } } try { power [leftM rightM] 8 } abort on fail clear message } Je nach Motorzustand und Nachricht wird ein neuer Zustand berechnet, in dem der betroffene Motor gestoppt oder gestartet ist. Dann wird mit hoher Priorität auf die Motoren zugegriffen. Später steuert die triviale Task die Motoren mit dem neuen Zustand an. 81 squeals erhält ebenfalls die Bremsnachrichten und macht Musik dazu: 6/navigator.rcx2 // message 1/3 watcher: sound it watcher squeals monitor leftBreak, rightBreak { priority 5 try { if leftBreak { sound 3 } else { sound 4 } } abort on fail } fixes nimmt die Kurskorrektur vor: 6/navigator.rcx2 // message 2 watcher: back to original direction watcher fixes monitor fix { priority 6 try { a = -1 b = 1 on [leftM rightM] if compass > 1 { reverse rightM wait until turned reverse rightM } else { if compass < -1 { reverse leftM wait until turned reverse leftM } } } abort on fail clear message } Dieser watcher läßt sich nicht durch den gleichen Event unterbrechen. Er operiert aber mit vergleichsweise geringer Priorität und könnte zum Beispiel an einem Hindernis abbrechen — der Rotations-Sensor zählt dabei korrekt weiter. beats demonstriert, wie man einen timer verarbeitet und eine Melodie spielt. 6/navigator.rcx2 // timer watcher: background sound watcher beats monitor beat { priority 7 clear beater try { tone 220 for 5 wait 10 tone 110 for 5 } stop on fail } restart on event 82 idles realisiert schließlich den Motorzustand, der dann eingenommen wird, wenn sich keine Task mit höherer Priorität einmischt: 6/navigator.rcx2 // idle task: run forward depending on motors task idles { priority 8 // lowest priority try { forever { select motors { when 0 { off [leftM rightM] } when 1 { on leftM off rightM } when 2 { off leftM on rightM } when 3 { on [leftM rightM] } } wait 1000 } } retry on fail } } Anders als ein watcher muß diese Task explizit am Leben erhalten werden, denn durch Verlassen ihres Codes würde sie beendet. 6.4 Fazit P-brick Script macht ein gewisses Umdenken erforderlich: Ein Programm reagiert mit watcher und monitor auf Events, die man (leider?) am Anfang des Programms vereinbaren muß. Offensichtlich wird aus den Events eine Task generiert, in der vermutlich die EventBedingungen der Reihe nach ausgewertet werden. Die Bedingungen können dynamisch beeinflußt werden. Subsumption entsteht durch die Prioritäten: Eine Task kann jederzeit durch priority ihre Priorität beeinflussen. Greift sie innerhalb von try auf Ausgabe-Ports zu, verdrängt sie eine Task mit geringerer Priorität. Synchronisation zur vollständigen Verteilung von Events scheint nicht nötig zu sein. Man kann vermutlich ähnliche Taktiken einsetzen wie bei nqc. 83 7 Java im RCX Bei der Analyse des RCX wurde entdeckt, daß man die Firmware, also den BytecodeInterpreter, ersetzen kann. Jose H. Solorzano hat kürzlich einen Interpreter für eine Untermenge der Bytecodes der Java Virtual Machine für den RCX implementiert. Man kann mit dieser tinyVM den RCX in einer Untermenge von Java mit spezialisierten Klassen programmieren. Von Donald W. Doherty gibt es eine Cross Platform Edition dieser Software, das heißt, er und seine Kollegen haben Solorzanos Hilfs- und Download-Programme in Java für das Java Communications API implementiert. 84 85 8 pbForth Forth ist eine sehr maschinennahe Sprache, die von Charles Moore vor mehr als 30 Jahren entwickelt wurde, ursprünglich zur Steuerung von Radioteleskopen. Seit 1983 gibt es einen Quasi-Standard (Forth-83), seit 1993 arbeitet man an einem ANSI-Standard. Ein Forth-System ist ein Compiler und Interpreter und enthält normalerweise einen Assembler. Forth ist vor allem auf sehr viele, sehr kleine Systeme portiert worden. Unter Linux kann man zum Beispiel gForth oder pForth verwenden. Ralph Hempel hat pbForth als Firmware für den RCX implementiert und dazu ein Tutorial sowie eine rudimentäre Dokumentation geschrieben. Mehr Details finden sich in Knuden’s Buch. 8.1 Erste Schritte Um pbForth zu benützen, muß man zuerst das System als Firmware laden — dazu kann man zum Beispiel nqc verwenden: c> nqc -SCOM1 -firmware Anschließend zeigt der RCX-Display 4th und man kann zum Beispiel unter Windows mit Hyperterm (Verbindung direkt zu COM1, 2400 baud, 8 Bit, 1 Stop-Bit, keine Parität; außerdem sollte man abgeschickte Zeilen mit einem newline versehen lassen) mit Forth im RCX kommunizieren: 1 2 3 ( drei Zahlen auf den Stack bringen ) ok . . . ( drei Zahlen vom Stack holen und zeigen ) 3 2 1 ok RCX_INIT ( RCX Vokabeln aktivieren ) ok 7 2 2 MOTOR_SET ( Motor C einschalten ) ok 7 3 2 MOTOR_SET ( Motor C ausschalten ) ok : hello ." Hello, World" ; ( neues Wort vereinbaren ) ok hello CR ( neues Wort ausführen ) Hello, World ok POWER_INIT ( Zugriff auf Power-Subsystem ) ok LCD_CLEAR LCD_REFRESH RCX_SHUTDOWN POWER_OFF ( RCX abschalten ) Danach ist der RCX stillgelegt. Mit dem ON-Knopf meldet er sich wieder, wobei die ersten übertragenen Zeichen auch defekt sein können: RCX_INIT LCD_4TH LCD_REFRESH ok ( RCX wieder initialisieren ) Alternativ zu nqc als Firmware-Lader und einem Terminal-Emulator wie Hyperterm kann man auch Hempels TCL/TK-basierte Oberfläche verwenden, die er allerdings nicht mehr unterstützt. Dazu muß man TCL/TK und die Oberfläche installieren, in der Datei rcxoption.tcl die Terminal-Schnittstelle wählen und dann die Datei rcxtk.tcl starten. 86 8.2 Einführung in Forth Forth-Implementierungen arbeiten als Interpreter, die Eingaben sofort bearbeiten. Es gibt dabei zwei Zustände: Zunächst wird jedes Wort sofort ausgeführt. Mit : leitet man aber eine Definition ein, die mit ; abgeschlossen wird. Erst wenn das neue Wort aufgerufen wird, wird der Inhalt der Definition ausgeführt. Manche Wörter darf man nur in Definitionen verwenden. Forth ist frei formatiert. Zwischenraum ist unabdingbar zur Trennung von Wörtern. Kommentare reichen von einem Wort mit einer linken Klammer bis zum Zeilenende oder einem Wort mit einer rechten Klammer oder von einem Wort mit \ bis zum Zeilenende. Großund Kleinschreibung ist bei pbForth signifikant. Ein Forth-Interpreter enthält ein Dictionary und einen Daten-Stack. Ein Wort wird zuerst im Dictionary gesucht; falls es definiert ist, wird seine Definition ausgeführt. Erst danach wird untersucht, ob das Wort eine Zahl ist; falls ja, wird ihr Wert auf den Stack gebracht. Das eröffnet böse Möglichkeiten: $ gforth marker funny ok : 1 17 ; ok 1 . 17 ok : 1 1 ; redefined 1 ok 1 . 17 ok : 1 3 2 - ; redefined 1 1 . 1 ok funny ok \ \ \ \ ok bis hierher kann man später abräumen 1 wird als 17 definiert und liefert 17 neue Definition bezieht sich auf alte \ so geht’s zur Not \ alles vergessen Wörter ändern in der Regel den Stack. Dies drückt man normalerweise mit einem formalisierten Kommentar am Beginn der Definition aus. . DUP OVER DEPTH SWAP DROP ROT PICK + - ( ( ( ( ( ( ( ( ( ( n -- , displays and pops top value ) n -- n n, duplicates top value ) a b -- a b a, duplicates value below top ) -- depth, pushes number of stack elements ) a b -- b a, exchanges top two values ) a -- , pop top value ) a b c -- b c a, rotate third item to top ) ... v0 N -- v0 vN, duplicates Nth value ) a b -- sum, add two values ) a b -- difference, subtract top from previous value ) Man kann Einträge in das Dictionary machen und entfernen: $ gforth marker funny ok variable x ok 0 constant null ok null x ! ok x @ . 0 ok : ? ( addr -- ) @ . ; redefined ? x ? 0 ok funny \ \ \ \ \ \ Anfang der Definitionen markieren Variable erzeugen Konstante erzeugen Wert zuweisen Wert auf den Stack bringen und zeigen Wort definieren ok \ Wort verwenden \ Definitionen löschen Eine vordefinierte Variable ist BASE. Man kann sie mit HEX und DECIMAL oder als Variable setzen. Eingaben und Ausgaben erfolgen relativ zu BASE: BASE @ 19 2 BASE ! . BASE ! 10011 ok 87 Kontrollstrukturen Innerhalb von Definitionen kann man die ‘‘üblichen’’ Kontrollstrukturen verwenden: : min ( a b -- minimum) 2DUP > IF SWAP THEN DROP ; redefined min 10 20 min . 10 ok 20 10 min . 10 ok ok Entscheidungen trifft man mit IF. Als Bedingung ist 0 falsch und alles andere wahr: ... IF ( flag -- , executed if true ) THEN ... ... IF ( flag -- , executed if true ) ELSE ( executed if false ) THEN ... Es gibt eine Reihe von Schleifen: : ten ( -- ) 10 0 BEGIN 2DUP < IF 2DROP EXIT THEN DUP . 1+ AGAIN ; ok ten 0 1 2 3 4 5 6 7 8 9 10 ok BEGIN ... AGAIN ist eine endlose Schleife. EXIT verläßt ein Wort — nicht eine Schleife. : ten ( -- ) 10 0 BEGIN 2DUP > WHILE DUP . 1+ REPEAT 2DROP ; redefined ten ten 0 1 2 3 4 5 6 7 8 9 ok ok BEGIN ... WHILE ( flag -- ) ... REPEAT ist eine while-Schleife, die bei WHILE verlassen wird, wenn die Bedingung nicht zutrifft. : ten ( -- ) 10 0 BEGIN DUP . 1+ 2DUP < UNTIL 2DROP ; redefined ten ok ten 0 1 2 3 4 5 6 7 8 9 10 ok BEGIN ... UNTIL ( flag -- ) ist eine do-while-Schleife, die verlassen wird, wenn die Bedingung vor UNTIL zutrifft. Eine Bedingung kann man mit NOT oder 0= umkehren. 88 : ten ( -- ) 10 0 DO I . LOOP ; redefined ten ten 0 1 2 3 4 5 6 7 8 9 ok ok DO ( limit start -- ) ... LOOP ist eine for-Schleife, deren Inkrement 1 ist, und die verlassen wird, wenn der Index ausgehend von start die Grenze limit erreicht oder überschreitet. I liefert den aktuellen Wert, J liefert den Wert einer umgebenden Schleife. Mit +LOOP ( inkrement -- ) kann man das Inkrement explizit angeben. : ten 100 I I ( -- ) 0 DO . 8 > IF LEAVE THEN LOOP ; redefined ten ten 0 1 2 3 4 5 6 7 8 9 ok ok Das folgende Wort gibt den Stack aus, wenn er nicht leer ist: : .S ( -- ) DEPTH ?DUP IF DUP 0 DO DUP I - PICK . LOOP DROP THEN ; redefined .S .S ok 1 2 3 ok .S 1 2 3 ok \ ?DUP ( x -- x x | 0, duplicates if not zero ) ok Das folgende Wort löscht den Stack, wenn er nicht leer ist: : SP0 ( ... -- ) DEPTH ?DUP IF 0 DO DROP LOOP THEN ; redefined sp0 with SP0 .S 1 2 3 ok SP0 ok .S ok SP0 ok ok Vektoren CREATE erzeugt einen Eintrag im Dictionary. ALLOT und , legen Speicher an. CELLS spielt die Rolle von sizeof. Damit kann man Vektoren herstellen: CREATE v 10 CELLS ALLOT ok \ erzeugt v mit 10 Elementen CREATE w 1 , 2 , 3 , ok \ erzeugt w[] = { 1 2 3 } : []@ ( addr idx -- addr[nidx], produces value of addr[idx] ) CELLS + @ ; ok : []! ( n addr idx -- , assigns n to addr[idx] ) CELLS + ! ; ok w 0 []@ . 1 ok \ zeigt w[0] w 1 []@ v 1 []! ok \ setzt v[1] auf den Wert w[1] v 1 []@ . 2 ok \ zeigt v[1] 89 8.3 Sprachumfang pbForth ist nicht sehr ausführlich dokumentiert. Man kann jedoch die Quellen und den Standard konsultieren. Die folgenden Tabellen skizieren den vermutlich nützlichen Sprachumfang in alphabetischer Reihenfolge. ! ( ( x a-addr -- ) ( "ccc<) * ( n1|u1 n2|u2 -- n3|u3 ) */ ( n1 n2 n3 -- n4 ) */MOD ( n1 n2 n3 -- n4 n5 ) + ( n1|u1 n2|u2 -- n3|u3 ) +! ( n|u a-addr -- ) +LOOP Compilation: ( C: do-sys -- ) Run-time: ( n -- ) ( R: loop-sys1 -- | loop-sys2 ) , ( x -- ) - ( n1|u1 n2|u2 -- n3|u3 ) -1 . ( -- -1 ) ( n -- ) ." ( "ccc<">" -- ) Run-time ( -- ) / ( n1 n2 -- n3 ) /MOD ( n1 n2 -- n3 n4 ) /STRING ( c-addr1 u1 n -- c-addr2 u2 ) 0 ( -- 0 ) Store x at a aligned address. Ignore following string up to next ) . A comment. Multiply n1|u1 by n2|u2 giving a single product. Multiply n1 by n2 producing double-cell intermediate, then divide it by n3. Return singlecell quotient. Multiply n1 by n2 producing double-cell intermediate, then divide it by n3. Return singlecell remainder and single-cell quotient. Add top two items and gives the sum. Add n|u to the contents at aaddr. Terminate a DO-+LOOP structure. Resolve the destination of all unresolved occurences of LEAVE. On execution add n to the loop index. If loop index did not cross the boundary between loop_limit-1 and loop_limit, continue execution at the beginning of the loop. Otherwise, finish the loop. Reserve one cell in data space and store x in it. Subtract n2|u2 from n1|u1, giving the difference n3|u3. Return -1. Display a signed number followed by a space. Compile an inline string literal to be typed out at run time. Divide n1 by n2, giving singlecell quotient n3. Divide n1 by n2, giving singlecell remainder n3 and singlecell quotient n4. Adjust the char string at c-addr1 by n chars. Return zero. 90 0< 0= 1 1+ ( ( ( ( n -- flag ) x -- flag ) -- 1 ) n1|u1 -- n2|u2 ) 1- ( n1|u1 -- n2|u2 ) : ( "<spaces>name" -- colon-sys ) ; < ( colon-sys -- ) ( n1 n2 -- flag ) = > ( x1 x2 -- flag ) ( n1 n2 -- flag ) ?DUP ( x -- x x | 0 ) @ ( a-addr -- x ) ABS ACCEPT ( n -- u ) ( c-addr +n1 -- +n2 ) AGAIN ( C: dest -- ) ALLOT ( n -- ) AND BASE ( x1 x2 -- x3 ) ( -- a-addr ) BEGIN ( C: -- dest ) BL ( -- char ) C! C, ( char c-addr -- ) ( char -- ) C@ ( c-addr -- char ) CELL+ ( a-addr1 -- a-addr2 ) CELLS ( n1 -- n2 ) Return true if n is negative. Return true if x is zero. Return one. Increase top of the stack item by 1. Decrease top of the stack item by 1. Start a new colon definition using next word as its name. Terminate a colon definition. Returns true if n1 is less than n2. Return true if top two are equal. Returns true if n1 is greater than n2. Duplicate top of the stack if it is not zero. Push the contents at a-addr to the data stack. Return the absolute value of n. Accept a string of up to +n1 chars. Return with actual count. Implementation-defined editing. Stops at EOL# . Supports backspace and delete editing. Resolve backward reference dest. Typically used as BEGIN ... AGAIN . Move control to the location specified by dest on execution. Allocate n address units in data space. Bitwise AND. Return the address of the radix base for numeric I/O. Start an infinite or indefinite loop structure. Put the next location for a transfer of control, dest, onto the data control stack. Return the value of the blank character. Store char at c-addr. Compile a character into data space. Fetch the character stored at caddr. Return next aligned cell address. Calculate number of address units for n1 cells. 91 CHAR CHAR+ CHARS CONSTANT COUNT CR CREATE DECIMAL DEPTH DO DROP 2DROP DUP 2DUP EKEY EKEY? ELSE EMIT EMIT? EXIT Parse next word and return the value of first character. ( c-addr1 -- c-addr2 ) Returns next character-aligned address. ( n1 -- n2 ) Calculate number of address units for n1 characters. ( x "<spaces>name" -- ) Create a definition for name name Execution: ( -- x ) which pushes x on the stack on execution. ( c-addr1 -- c-addr2 u ) Convert counted string to string specification. c-addr2 is the next char-aligned address after caddr1 and u is the contents at caddr1. ( -- ) Carriage return and linefeed. ( "<spaces>name" -- ) Create a data object in data name Execution: ( -- a-addr ) space, which return data object address on execution ( -- ) Set the numeric conversion radix to decimal 10. ( -- +n ) Return the depth of the data stack. Compilation: ( C: -- do-sys ) Start a DO-LOOP structure in a Run-time: ( n1|u1 n2|u2 -- ) colon definition. Place do-sys ( R: -- loop-sys ) on control-flow stack, which will be resolved by LOOP or +LOOP. ( x -- ) Discard top stack item or pair. ( x -- x x ) Duplicate the top stack item or pair. ( -- u ) Receive one keyboard event u. ( -- flag ) If a keyboard event is available, return true. Compilation: ( C: orig1 -- orig2 ) Start the false clause in an IFRun-time: ( -- ) ELSE-THEN structure. Put the location of new unresolved forward reference orig2 onto control-flow stack. ( x -- ) Send a character to the output device. ( -- flag ) flag is true if the user output device is ready to accept data and the execution of EMIT in place of EMIT? would not have suffered an indefinite delay. If device state is indeterminate, flag is true. ( -- ) Return control to the calling definition. ( "<spaces>ccc" -- char ) 92 FILL ( c-addr u char -- ) FORTHWORDLIST GET-CURRENT ( -- wid ) HERE HEX ( -- addr ) ( -- ) I IF ( -- n|u ) Compilation: ( C: -- orig ) Run-time: ( x -- ) INVERT J ( x1 -- x2 ) ( -- n|u ) KEY ( -- char ) LEAVE ( -- ) LITERAL Compilation: ( x -- ) Run-time: ( -- x ) LOOP Compilation: ( C: do-sys -- ) Run-time: ( -- ) ( R: loop-sys1 -- loop-sys2 ) LSHIFT ( x1 u -- x2 ) MAX ( n1 n2 -- n3 ) MIN ( n1 n2 -- n3 ) MOD ( n1 n2 -- n3 ) MOVE ( addr1 addr2 u -- ) ( -- wid ) NEGATE ( n1 -- n2 ) NIP ( n1 n2 -- n2 ) NONSTANDARD- ( -- wid ) WORDLIST Store char in each of u consecutive characters of memory beginning at c-addr. Return wid of Forth wordlist. Return the indentifier of the compilation wordlist. Return data space pointer. Set the numeric conversion radix to hex 10. Push the innermost loop index. Put the location of a new unresolved forward reference orig onto the control flow stack. On execution jump to location specified by the resolution of orig if x is zero. Return one's complement of x1. Push the index of next outer loop. Receive a character. Do not display char. Terminate definite loop, DO|?DO ... LOOP|+LOOP, immediately. Append following run-time semantics. Put x on the stack on execution Terminate a DO|?DO ... LOOP structure. Resolve the destination of all unresolved occurences of LEAVE. Perform a logical left shift of u bit-places on x1, giving x2. Put 0 into the least significant bits vacated by the shift. Return the greater of two top stack items. Return the smaller of top two stack items. Divide n1 by n2, giving the single cell remainder n3. Returns modulo of floored division in this implementation. Copy u address units from addr1 to addr2 if u is greater than zero. Return two's complement of n1. Discard the second stack item. Return wid of non-standard wordlist. 93 OR ( x1 x2 -- x3 ) OVER 2OVER ( x1 x2 -- x1 x2 x1 ) PAUSE ( -- ) PICK ( x_u ... x1 x0 u -x_u ... x1 x0 x_u ) REPEAT ( C: orig dest -- ) ROT ( x1 x2 x3 -- x2 x3 x1 ) RSHIFT ( x1 u -- x2 ) RX? RX@ S" ( -- flag ) ( -- u ) Compilation: ( "ccc<">" -- ) Run-time: ( -- c-addr u ) SPACE ( -- ) SPACES ( n -- ) SWAP 2SWAP ( x1 x2 -- x2 x1 ) SystemTask THEN ( -- a-addr ) Compilation: ( C: orig -- ) Run-time: ( -- ) TX! TX? ( u -- ) ( -- flag ) TYPE ( c-addr u -- ) U. ( u -- ) U< ( u1 u2 -- flag ) UNTIL ( C: dest -- ) Return bitwise inclusive-or of x1 with x2. Copy second stack item or pair to top of the stack. Stop current task and transfer control to the task of which ``"status' USER variable is stored in 'follower"'' USER variable of current task. Remove u and copy the uth stack item to top of the stack. An ambiguous condition exists if there are less than u+2 items on the stack before PICK is executed. Terminate a BEGIN-WHILEREPEAT indefinite loop. Resolve backward reference dest and forward reference orig. Rotate the top three data stack items. Perform a logical right shift of u bit-places on x1, giving x2. Put 0 into the most significant bits vacated by the shift. Return true if key is pressed. Receive one keyboard event u. Parse ccc delimetered by " . Return the string specification c-addr u on execution. Send the blank character to the output device. Send n spaces to the output device if n is greater than zero. Exchange top two stack items or pairs. Return system task's tid. Resolve the forward reference orig. Send char to the output device. Return true if output device is ready or device state is indeterminate. Display the character string if u is greater than zero. Display u in free field format followed by space. Unsigned compare of top two items. True if u1 < u2. Terminate a BEGIN-UNTIL indefinite loop structure. 94 VALUE ( x "<spaces>name" -- ) name Execution: ( -- x ) VARIABLE ( "<spaces>name" -- ) name Execution: ( -- a-addr ) WHILE ( C: dest -- orig dest ) WITHIN ( n1|u1 n2|n2 n3|u3 -- flag ) XOR \ ( x1 x2 -- x3 ) ( "ccc<eol>" -- ) memTop ( -- a-addr ) Create a value object with initial value x. Parse a name and create a variable with the name. Resolve one cell of data space at an aligned address. Return the address on execution. Put the location of a new unresolved forward reference orig onto the control flow stack under the existing dest. Typically used in BEGIN ... WHILE ... REPEAT structure. Return true if (n2|u2<=n1|u1 and n1|u1<n3|u3) or (n2|u2>n3|u3 and (n2|u2<=n1|u1 or n1|u1<n3|u3)). Bitwise exclusive OR. Parse and discard the remainder of the parse area. Top of free RAM area. Für den RCX hat Hempel folgende speziellen Wörter hinzugefügt, die sich im Wesentlichen auf Routinen im ROM stützen: retrieve button mask: 1 Run, 2 View, 4 Prgm -- ) initialize button system. -- ) segments for 4th. -- ) clears display. segment -- ) hides a segment. comma value number -- ) display a numerical value. -- ) sends output to display. segment -- ) shows a segment. power mode idx -- ) controls motor: power 1..7; mode 1 forward, 2 reverse, 3 braked, 4 floating; idx 0..2. addr-a code -- ) retrieve On (code hex 4000, 0 pressed) or battery level (4001). -- ) initialize power management. -- ) turns RCX mostly off. -- a-addr ) use for BUTTON_GET. -- a-addr ) RCX Data Buffer - Nominally 0x65 hex bytes, but we'll allocate 0x80 -- a-addr ) echo state variable, initially off. x -- ) sends a character to the output device if RCX_ECHO is non-zero. -- ) initializes outputs, timers, inputs. -- a-addr ) use for POWER_GET. -- ) shuts outputs, timers, inputs down. BUTTON_GET ( a-addr -- ) BUTTON_INIT LCD_4TH LCD_CLEAR LCD_HIDE LCD_NUMBER LCD_REFRESH LCD_SHOW MOTOR_SET ( ( ( ( ( ( ( ( POWER_GET ( POWER_INIT POWER_OFF RCX_BUTTON RCX_DATA ( ( ( ( RCX_ECHO RCX_EMIT ( ( RCX_INIT RCX_POWER RCX_SHUTDOWN ( ( ( 95 RCX_SOUND SENSOR_ACTIVE SENSOR_BOOL SENSOR_CLEAR SENSOR_INIT SENSOR_MODE ( ( ( ( ( ( -- a-addr ) idx -- ) idx -- value ) idx -- ) -- ) mode idx -- ) SENSOR_PASSIVE SENSOR_RAW SENSOR_READ SENSOR_TYPE ( ( ( ( idx -- ) idx -- value ) idx -- code ) type idx -- ) SENSOR_VALUE SOUND_GET SOUND_PLAY ( idx -- value ) ( a-addr -- ) ( sound code -- ) TIMER_GET TIMER_SET ( idx -- value ) ( value idx -- ) timer_GET timer_SET ( idx -- value ) ( value idx -- ) use for SOUND_GET. set for light sensor on 0..2. get sensor’s Boolean value. clear sensor’s value. initializes input system. set sensor mode: hex 0 raw, 20 Boolean, 40 edge, 60 pulse, 80 percent, a0 centigrade, c0 fahrenheit, e0 angle. set for other sensors on 0..2. get sensor’s raw value. reads sensor, 0 on success. set sensor type: 0 raw, 1 touch, 2 temperature, 3 light, 4 rotation. get sensor’s scaled value. gets state of sound system: 0 idle. plays sound 0..6, code hex 4003 queued, 4004 unqueued. get 1/10 second timer 0..3. set 1/10 second timer 0..3. These count up and wrap. get 1/100 second timer 0..9. set 1/100 second timer 0..9. These count down and stop. Es gibt dann noch verschiedene Erweiterungen, die man einzeln oder insgesamt als Firmware pbmax.srec laden kann. Nützlich dürften hier die Vokabeln zum kooperativen Multi-Tasking sein: Display tasks list in status-follower chain. Activate the task identified by tid. ACTIVATE must be used only in definition. The code following ACTIVATE must not EXIT. In other words it must be infinite loop like QUIT. AWAKE ( tid -- ) Awake another task. BUILD ( tid -- ) Initialize and link new task into PAUSE chain. ALLOT_TASK ( user_size ds_size rs_size Creates a new task - the name returns a "<spaces>name" -- ) task id at run time. (called HAT in multi.txt) .TASKS ACTIVATE ( -- ) ( tid -- ) PAUSE Run-time: ( -- tid ) ( -- ) SLEEP STOP ( tid -- ) ( -- ) Stop current task and transfer control to the task of which 'status' USER variable is stored in 'follower' USER variable of current task. Sleep another task. Sleep current task. 96 8.4 Trusty Als Beispiel für eine Steuerung mit pbForth wird der Navigator aus Abschnitt 5.5, Seite 68, so programmiert, daß er wie Trusty Hindernisse vermeidet. Zunächst definiert man eine Reihe von Konstanten — dieser Vorgang ist kumulativ: 8/trusty.fth \ trusty: two motors, two bumpers, single task loadPoint MARKER loadPoint \ remove previous load, if any \ mark load point HEX \ **compile** using hex notation : ready 5 4003 ( -- 5 4003, parameters for ready sound ) ; 4000 CONSTANT switch 20 CONSTANT boolean 1 CONSTANT run \ parameter to POWER_GET ON \ parameter to touchSensor \ parameter to GET_BUTTON DECIMAL 1 2 3 4 CONSTANT CONSTANT CONSTANT CONSTANT forward reverse breaking floating \ direction of a motor 7 CONSTANT speed \ motor speed 0 CONSTANT rightM 2 CONSTANT leftM \ right motor on A \ left motor on C 0 CONSTANT rightT 1 CONSTANT leftT 9 CONSTANT bounceT \ names for timers 150 CONSTANT backupTime \ total backup time 80 CONSTANT bothTime \ time to go straight during backup 1 CONSTANT rightB 2 CONSTANT leftB \ right bumper on 2 \ left bumper on 3 Durch Aufruf von loadPoint kann man später alle Definitionen eliminieren. ready illustriert eine Falle: HEX wirkt sich auf Ein- und Ausgabe der Zahlen aus. Man muß also HEX ausführen, bevor man ready definiert; macht man HEX zum Teil der Definition von ready, wirkt sich HEX nicht auf Werte wie 4000 in der Definition aus. 97 8/trusty.fth \ initialization : touchSensor ( idx -- , initialize as touch sensor ) DUP SENSOR_PASSIVE boolean OVER SENSOR_MODE 1 SWAP SENSOR_TYPE \ touch ; : init ( -- , initialize subsystems ) RCX_INIT LCD_4TH LCD_REFRESH BUTTON_INIT POWER_INIT SENSOR_INIT rightB touchSensor leftB touchSensor ready SOUND_PLAY ; : quit ( -- , turn rcx off ) LCD_CLEAR LCD_REFRESH RCX_SHUTDOWN POWER_OFF ; init initialisiert die RCX-Peripherie, schaltet den Display ein, konfiguriert die Sensoren und zeigt durch einen Klang an, daß das Programm arbeitsbereit ist. quit macht die Aktionen von init rückgängig und legt den RCX still. Zur Entwicklung tippt man entweder in einem Programm wie Hyperterm und übernimmt korrekte Definitionen per Cut&Paste in eine Textdatei, oder man bearbeitet eine Textdatei mit einem Editor und überträgt die Definitionen mit rcxtk.tcl aus der Datei in den RCX. Worte wie init und quit kann man sofort testen: init ok Jetzt steht 4th im Display und der RCX hat einen Ton von sich gegeben. quit Jetzt ist der Display dunkel. ok erhält man erst, wenn ON gedrückt wird. Reagiert der RCX nicht mehr, muß man die Batterien entfernen und dann die pbForthFirmware neu laden. 98 8/trusty.fth \ motor control : start ( -- , start motors ) speed forward rightM MOTOR_SET speed forward leftM MOTOR_SET ; : stop ( -- , stop motors ) speed floating rightM MOTOR_SET speed floating leftM MOTOR_SET ; start und stop sorgen dafür, daß der Roboter vorwärts fährt und wieder anhält. Auch diese Worte kann man sofort testen: init start ok Jetzt laufen die Motoren. stop ok Jetzt stehen die Motoren wieder. 8/trusty.fth \ backing up : checkTimer ( motor timer -- timer-value, motor forward if timer run out ) timer_GET DUP 0= IF \ -- motor timer-value SWAP speed forward ROT MOTOR_SET \ -- timer-value ELSE NIP ( motor timer-value -- timer-value ) THEN ; : backup ( tLeft tRight -- , run motors in reverse for indicated time ) speed reverse rightM MOTOR_SET \ reverse both motors speed reverse leftM MOTOR_SET rightT timer_SET \ set both timers leftT timer_SET BEGIN rightM rightT checkTimer \ wait for both timers to run out leftM leftT checkTimer + 0= UNTIL ; Um ein Hindernis zu vermeiden, soll der Roboter zuerst rückwärts fahren und sich dann drehen. Bei backup gibt man für jeden Motor die Zeit in 1/100 Sekunden an, die er rückwärts laufen soll. Aus der Differenz ergibt sich die Drehung. backup verwendet checkTimer, um einen Motor wieder umzukehren, und endet, wenn beide Timer abgelaufen sind. Auch dieses Wort testet man direkt: init start ok backupTime bothTime backup stop ok Die Räder drehen sich vorwärts, rückwärts, und nacheinander wieder vorwärts. 99 8/trusty.fth \ avoidance VARIABLE bumped \ counts bumps : bumper ( tLeft tRight idx -- , check bumper and backup ) DUP SENSOR_READ IF \ update sensor value DROP 2DROP EXIT \ ...failed THEN SENSOR_BOOL 0= IF \ sensor open? backup 1 bumped +! ELSE 2DROP THEN ; bumper reagiert auf ein Hindernis je nach Sensor-Wert mit backup und zählt die Treffer in bumped. Wieder testet man sofort: init start ok 0 bumped ! ok Jetzt drückt man die linke Stoßstange. backupTime bothTime leftB bumper ok stop bumped @ . 1 ok Die Räder drehen sich rückwärts und nacheinander wieder vorwärts. 100 8/trusty.fth \ checking buttons : wait ( n -- , use bounceT to wait n/100 sec ) bounceT timer_SET BEGIN bounceT timer_GET 0= UNTIL ; : ifOn ( -- flag, true if ON is pressed ) RCX_POWER DUP switch POWER_GET @ \ first reading BEGIN 10 wait \ debounce RCX_POWER DUP switch POWER_GET @ \ next reading DUP ROT ( first next -- next next first ) = UNTIL 0= \ 0-pressed -- true-pressed ; : ifButton ( mask -- flag, true if any of mask is pressed ) DUP RCX_BUTTON DUP BUTTON_GET @ AND \ first reading BEGIN 10 wait \ debounce OVER RCX_BUTTON DUP BUTTON_GET @ AND \ next reading DUP ROT ( first next -- next next first ) = UNTIL NIP ; Zur globalen Steuerung sollen ON und RUN dienen. Diese Knöpfe muß man in kurzen Zeitabständen so lange lesen, bis sie stabile Werte liefern. ifOn testet ON und ifButton prüft mit einer Maske die anderen drei Knöpfe. Beide Wörter testet man wieder direkt: init ifOn . 0 ok run ifButton . 1 ok Hier wurde RUN aber nicht ON gedrückt. 101 Das Hauptprogramm main prüft immer wieder ON und RUN: 8/trusty.fth \ main program for single-tasked Trusty : main ( nBumps -- , main loop: RUN robot until nBumps, ON toggles power ) init BEGIN run ifButton IF start 0 bumped ! BEGIN backupTime bothTime rightB bumper bothTime backupTime leftB bumper DUP bumped @ > 0= UNTIL stop THEN ifOn IF quit init EXIT THEN AGAIN ; Bei RUN setzt sich der Roboter in Bewegung und bricht nach einer als Argument angegebenen Anzahl von Treffern ab. ON legt den RCX still; nach erneutem Einschalten bricht main ab, damit man weiter mit pbForth arbeiten kann: 3 main Man kann RUN drücken und jedesmal gegen drei Hindernisse fahren. ok erhält man, wenn man ON zweimal drückt. 102 8.5 Multi-Tasking pbForth enthält Worte zum kooperativen Multi-Tasking. Man kann das Programm so weiterentwickeln, daß eine an Subsumption erinnernde Struktur entsteht: 8/subsumption.fth \ subsumption: two motors, two bumpers, multi-tasked; load after trusty.fth loadPoint2 MARKER loadPoint2 \ mark load point 0 64 CELLS 64 CELLS ALLOT_TASK buttons \ reacts to buttons buttons BUILD 0 64 CELLS 64 CELLS ALLOT_TASK motors motors BUILD \ controls motors based on timers+states 0 64 CELLS 64 CELLS ALLOT_TASK bumpers \ reacts to bumpers bumpers BUILD ALLOT_TASK hinterlegt drei Flächen unter einem Namen, der eine Task identifiziert; die Größe der Stacks ist Gefühlssache... BUILD reiht die Task in den Ring aller Tasks ein und markiert sie wie SLEEP. Man kann also eine Task konstruieren und sich auf ihren Namen beziehen, bevor der Code existiert, den die Task ausführt. 8/subsumption.fth \ ------------------------------------------------------------------ motors task VARIABLE left VARIABLE right \ current direction of each motor : runMotor ( l|r lM|rM lT|rT -- , reverse if timer, else state from variable ) timer_GET IF \ left|right leftM|rightM NIP \ leftM|rightM speed reverse \ leftM|rightM speed reverse ELSE SWAP @ \ leftM|rightM motor-state speed SWAP \ leftM|rightM speed motor-state THEN ROT MOTOR_SET ; : runMotors ( -- , check on each motor ) left leftM leftT runMotor right rightM rightT runMotor ; :NONAME motors ACTIVATE BEGIN runMotors PAUSE AGAIN ; EXECUTE motors SLEEP ACTIVATE verknüpft einen Task-Namen mit einer endlosen Schleife. In der Schleife muß PAUSE vorkommen, damit die anderen Tasks auch ausgeführt werden. :NONAME startet eine Definition — nur dort darf man ACTIVATE und die Schleife angeben. Die Definition wird sofort mit EXECUTE ausgeführt. Die Task ist dann AWAKE, das heißt, sie wird jetzt in Pausen der anderen Tasks ausgeführt. 103 motors steuert die Motoren: Wenn ein Timer für einen Motor gesetzt ist, läuft der Motor rückwärts. Andernfalls entscheidet eine Variable, wie der Motor laufen soll. Damit können andere Tasks entscheiden, wie die Motoren laufen sollen. Dieses Schema könnte man natürlich für Subsumption weiter ausbauen. [Die Idee wurde von Hempels Beispiel tortask.txt übernommen.] Man kann das wieder testen: init ok motors AWAKE ok forward left ! ok Jetzt dreht sich der linke Motor. floating left ! ok motors SLEEP .TASKS SystemTask awake motors sleeping ok Damit stehen die Motoren wieder still. 8/subsumption.fth \ ----------------------------------------------------------------- bumpers task : runBumper ( tLeft tRight idx -- , check bumper and backup ) DUP SENSOR_READ IF \ update sensor value DROP 2DROP EXIT \ ...failed THEN SENSOR_BOOL 0= IF \ tLeft tRight -- sensor open? rightT timer_SET leftT timer_SET ELSE 2DROP THEN ; : runBumpers ( -- , check on bumpers ) backupTime bothTime rightB runBumper bothTime backupTime leftB runBumper ; :NONAME bumpers ACTIVATE BEGIN runBumpers PAUSE AGAIN ; EXECUTE bumpers SLEEP bumpers beobachtet die Stoßstangen und weicht gegebenenfalls zurück: init ok bumpers AWAKE motors AWAKE ok Berührt man eine Stoßstange, drehen sich die Motoren verschieden lange rückwärts. bumpers SLEEP motors SLEEP ok Damit stehen die Motoren wieder still. 104 Als Hauptprogramm soll buttons parallel zur SystemTask laufen und ON und RUN beobachten. RUN agiert in Abhängigkeit von einer Variablen running: 8/subsumption.fth \ ----------------------------------------------------------------- buttons task VARIABLE running \ state of RUN button : init2 ( -- , initialize subsystems ) 0 running ! \ not running init ; : start2 ( -- , set motors running and start tasks ) 1 running ! forward right ! 0 rightT timer_SET forward left ! 0 leftT timer_SET motors AWAKE bumpers AWAKE ; : stop2 ( -- , set motors floating and stop tasks ) bumpers SLEEP floating right ! 0 rightT timer_SET floating left ! 0 leftT timer_SET PAUSE motors SLEEP 0 running ! ; init löscht running und initialisiert das System. start2 und stop2 kontrollieren motors und bumpers und lassen die Motoren laufen oder anhalten; außerdem notieren sie in running den aktuellen Zustand. init2 start2 ok stop2 ok Die Motoren drehen vorwärts und man kann jetzt die Stoßstangen betätigen. 105 8/subsumption.fth \ main program for multi-tasked Trusty : runButtons ( -- , check on each button ) run ifButton IF running @ IF stop2 ELSE start2 THEN THEN ifOn IF stop2 buttons SLEEP quit buttons AWAKE init2 THEN ; init2 :NONAME buttons ACTIVATE BEGIN runButtons PAUSE AGAIN ; EXECUTE buttons sorgt dafür, daß RUN mit start2 und stop2 die Motoren und Tasks startet oder anhält, und daß man mit ON das System ab- und wieder einschalten kann. Wenn buttons läuft, muß man Kommandos relativ langsam tippen. 8.6 Fazit pbForth enthält überwältigend viele Worte und reichlich unorthodoxe Strukturen. Das System eignet sich aber glänzend zur schrittweisen Entwicklung auch komplexer Programme. Man muß bottom-up vorgehen und kann kleinste Fragmente sofort testen. Außerdem steht immer eine Textkonsole zur Ausgabe von Werten und Meldungen zur Verfügung. pbForth erlaubt beliebig viele Funktionen, Variablen und sogar Vektoren. Man kann vermutlich auch ein Message-Protokoll implementieren, wird dann aber die Konsole verlieren. Subsumption ist auch mit kooperativem Multi-Tasking erreichbar, bedarf dann aber sorgfältiger Planung. Multi-Tasking sollte man während der Entwicklung eigentlich als Regelfall verwenden, denn dann kann man in der SystemTask noch die Kontrolle über das System behalten und muß bei groben Fehlern nicht gleich die Firmware nachladen... pbForth ist eine Stack-Maschine. Es sollte nicht schwierig sein, auf einem Host eine Sprache (a la nqc, aber flexibler) interaktiv zu übersetzen und über pbForth abzuwickeln, wobei dann allerdings ein Teil der Fähigkeiten von pbForth zugunsten einer konventionelleren Sprachstruktur brachliegt. 106 107 9 legOS Der GNU C/C++ Compiler kann auch Code für den Hitachi H8/300 Chip generieren. Markus L. Noga hat mit den GNU Tools eine Firmware für den RCX in C und C++ implementiert. Es handelt sich dabei um ein kleines Betriebssystem, das dynamisch Programme zuladen kann. legOS liegt in der Version 0.2.3 vor — im Netz entdeckt man Hinweise auf 0.2.4 oder auch 0.2.5. 9.1 Erste Schritte Prinzipiell kann man Linux (zum Beispiel SuSe 6.3) oder Windows 95/98 mit cygwin als Entwicklungsplattform verwenden. Dem Netz entnimmt man, daß es mit Linux wohl reibungsloser geht. Zuerst packt man das Cross-Compiler-System aus und kopiert es an die richtige Stelle: # cd /tmp; tar xfz rcx-tools-glibc.tar.gz # cp -R usr / Anschließend packt man die Systemquellen an einer beliebigen Stelle aus und übersetzt das System: $ tar xfz legOS-0.2.3.tar.gz $ cd legOS; vi Makefile.common $ make ## LEGOS_ROOT korrigieren In Makefile.common muß man bei LEGOS_ROOT den absoluten Pfad zur Wurzel der Quellen samt einem abschließenden / eintragen. legOS ist wie pbForth eine eigene Firmware, und man muß zuerst das System selbst mit einem Firmware-Lader übertragen: $ util/firmdl3 usage: util/firmdl3 [options] filename --debug show debug output, mostly raw bytes -f, --fast use fast 4x downloading (default) -s, --slow use slow 1x downloading --tty=TTY assume tower connected to TTY -h, --help display this help and exit $ util/firmdl3 boot/legOS.srec Als serielle Schnittstelle zum IR-Tower ist /dev/ttyS0 voreingestellt. Klappt’s nicht, könnte man die langsame Übertragung mit -s anfordern. Mit dem dynamischen Lader dll überträgt man dann Programme: $ util/dll usage: util/dll file.lx [-rrcxaddress -pprogramnumber -ssrcport -v] $ util/dll demo/helloworld.lx $ util/dll demo/robots.lx -p1 108 Bedienung Die Programme wählt man wie gewohnt mit Prgm und startet und terminiert sie mit Run. Mit On-Off schaltet man den RCX aus und ein; die Programme bleiben dabei erhalten. Um die Firmware zu eliminieren, hält man Prgm gedrückt und schaltet den RCX mit On-Off ein und aus; danach kann und muß man eine neue Firmware laden. Dokumentation Prinzipiell existiert Dokumentation. Louis Villa bastelt an einem HOWTO — aber der Link von der legOS-Homepage ist zur Zeit inaktiv. Wenn man doxygen unter Linux installiert, erzeugt make html eine mehr oder weniger rudimentäre Dokumentation aus den Quellen; dies kann man in Doxyfile konfigurieren. Leider ist wohl längere Zeit nicht mehr kontrolliert worden, in wieweit der resultierende Text brauchbar ist. Knudsen beschreibt zwar, wie man mit legOS programmiert, aber er bezieht sich auf Version 0.1.7. 109 9.2 Programmierung Man schreibt normale C-Programme für 16-Bit-Hardware: 9/zb/range.c /* number ranges */ #include <conio.h> static double d = 1.234; static int i = 1234; static void puti (int i) { lcd_int(i); lcd_refresh(); delay(1000); } int main (void) { puti(i++); { long i, j = 1L; int n = 0; do { i = j, j = i << 1L | 1L; ++ n; } while (j > i); puti(n); puti((int)i); do { i = j, j = i << 1; } while (j < i); puti((int)i); } lcd_number((int)(d*1000.), sign, e_3); lcd_refresh(); delay(1000); cls(); return 0; } Vernünftigerweise legt man einen eigenen Katalog parallel zu demo/ an, der einem Link zu boot/config.h enthält, und kopiert die wesentlichen Angaben aus demo/Makefile: KERNEL = ../boot/legOS PROGRAMS = range.lx all: $(PROGRAMS) include ../Makefile.common include ../Makefile.user Die Übersetzung kontrolliert man mit make, denn nach der C-Übersetzung erfolgen zwei Relocation-Schritte, aus denen dann erst das mit dll übertragbare Image range.lx erstellt wird. Dieses Programm gibt nacheinander auf dem RCX-Display die Werte 1234, 31, -1, 0 und 1,234 aus. 110 9.3 Funktionen Die Programmierung stützt sich natürlich auf ‘‘Systemaufrufe’’, die als C-Funktionen und Makros implementiert und in Dateien im Bereich include/ deklariert sind. conio.h void void void void void delay (unsigned d) cputw (unsigned word) cputc (char c, int pos) cputs (char *s) cls () dbutton.h BUTTON_ONOFF BUTTON_RUN BUTTON_VIEW BUTTON_PROGRAM RELEASED (state,button) PRESSED (state,button) int dbutton(void) dkey.h KEY_ONOFF KEY_RUN KEY_VIEW KEY_PRGM KEY_ANY wakeup_t dkey_pressed (wakeup_t data) wakeup_t dkey_released (wakeup_t data) int getchar() dlcd.h dlcd_show (a) dlcd_hide (a) rom/lcd.h lcd_int (i) lcd_unsigned (u) lcd_clock (t) lcd_digit (d) void lcd_show (lcd_segment segment) void lcd_hide (lcd_segment segment) void lcd_number (int i, lcd_number_style n, lcd_comma_style c) void lcd_clear (void) void lcd_refresh (void) dmotor.h off fwd rev brake MIN_SPEED MAX_SPEED void motor_a_dir (MotorDirection dir) void motor_b_dir (MotorDirection dir) void motor_c_dir (MotorDirection dir) void motor_a_speed (unsigned char speed) void motor_b_speed (unsigned char speed) void motor_c_speed (unsigned char speed) dsensor.h SENSOR_1 SENSOR_2 SENSOR_3 console input/output delay by msec display word in hex char at position 0..4 from right string, up to 5 chars clear screen query raw button states on/off button run button view button program button true if any is released true if all are pressed get button states debounced key driver on/off key is pressed run key is pressed view key is pressed program key is pressed any of the keys wakeup if any key is pressed wakeup if all keys are released wait for key press, return code control the LCD display directly set segment directly clear segment directly ROM LCD control display integer display unsigned display clock (4 digits) display digit right of man show segment hide segment show number clear display flush output into display direct motor access motor directions motor speeds set motor directions set motor speeds direct sensor access sensors 111 BATTERY LIGHT(a) LIGHT_1 LIGHT_2 LIGHT_3 ROTATION_1 ROTATION_2 ROTATION_3 void ds_active (unsigned *sensor) void ds_passive (unsigned *sensor) void ds_rotation_set (unsigned *sensor, int pos) void ds_rotation_on (unsigned *sensor) void ds_rotation_off (unsigned *sensor) dsound.h typedef struct { unsigned char pitch; unsigned char length; } note_t PITCH_A0 DSOUND_BEEP void dsound_play (const note_t *notes) void dsound_system (unsigned nr) void dsound_set_duration (unsigned duration) void dsound_set_internote (unsigned duration) int dsound_playing (void) wakeup_t dsound_finished (wakeup_t data) void dsound_stop (void) semaphore.h int sem_init (sem_t * sem, int dummy, unsigned int value) int sem_wait (sem_t * sem) int sem_trywait (sem_t * sem) int sem_post (sem_t * sem) int sem_getvalue (sem_t * sem, int *sval) int sem_destroy (sem_t * sem) stdlib.h void void void long void *calloc (size_t nmemb, size_t size) *malloc (size_t size) free (void *ptr) random (void) srandom (unsigned int seed) string.h void *memcpy (void *dest, const void *src, size_t size) void *memset (void *s, int c, size_t n) char *strcpy (char *dest, const char *src) int strlen (const char *s) int strcmp (const char *s1, const char *s2) time.h typedef unsigned long time_t TICK_IN_MS TICKS_PER_SEC SECS_TO_TICKS(a) MSECS_TO_TICKS(a) extern time_t sys_time battery sensor map light sensor to 0..LIGHT_MAX processed light sensors processed rotation sensors set light/rotation sensor set light/touch sensor passive set rotation sensor position start tracking stop tracking direct sound access note pitch, 0 is A_0 (~55 Hz) note length in 1/16ths (many) pitch values system sound play a sequence of notes play a system sound set 1/16th duration in msec set space, 0 is legato true while sound is playing sound finished stop playing POSIX 1003.1b semaphores for process synchronization initializes waits for nonzero, decrements like sem_wait(), returns EAGAIN if not possible increases sets value into *sval recycles; nobody must be waiting reduced standard C library dynamic memory random values string functions unsafe if overlapping time-related types timer tick in mses how many per second? conversion functions current system time 112 tm.h typedef unsigned long wakeup_t PRIO_LOWEST PRIO_NORMAL PRIO_HIGHEST typedef unsigned long wakeup_t P_DEAD P_ZOMBIE P_WAITING P_SLEEPING P_RUNNING DEFAULT_STACK_SIZE unistd.h pid_t execi (int (*code_start)(int, char **), int argc, char **argv, priority_t priority, size_t stack_size) void exit (int code) void yield (void) wakeup_t wait_event (wakeup_t(*wakeup)(wakeup_t), wakeup_t data) unsigned int sleep (unsigned int sec) unsigned int msleep (unsigned int msec) void kill(pid_t pid) void killall(priority_t p) task management wakeup data area type priorities wakeup data area type dead and gone, stack freed terminated, cleanup pending waiting for an event sleeping, wants to run running should be sufficient reduced UNIX standard library execute an image exit task yield rest of timeslice suspend until wakeup() != 0, return value delay seconds, return rest delay msecs, return rest kill process kill all processes with less priority Am legOS Networking Protocol (LNP) wird offenbar heftig gearbeitet, aber die deklarierten Funktionen reichen noch nicht für eine Übertragung. 113 9.4 Beispiele Touch-Sensor Als Beispiel zum Umgang mit Touch-Sensoren wird der Navigator aus Abschnitt 5.5, Seite 68, so programmiert, daß er wie Trusty Hindernisse vermeidet. An die Ausgänge A und C sind Motoren angeschlossen, an den Eingängen 2 und 3 befinden sich Touch-Sensoren, die an einem Hindernis gedrückt werden. 9/zb/trusty.c /* Trusty with active touch sensors, based on demo/rover.c */ #include <conio.h> #include <unistd.h> #include <dsensor.h> #include <dmotor.h> #define leftB SENSOR_3 #define rightB SENSOR_2 #define pressed > 0xf000 // configure bumpers // activated if released #define leftM motor_a #define rightM motor_c // configure motors #define cat(a, b) a ## b #define speed(motor, value) #define dir(motor, value) // yuck: avoid prescan cat(motor, _speed)(value) cat(motor, _dir)(value) static wakeup_t sensor_pressed (wakeup_t data) { return leftB pressed ? 1 : (rightB pressed ? -1 : 0); } int main (void) { int n; while (1) { speed(leftM, 2*MAX_SPEED/3); speed(rightM, 2*MAX_SPEED/3); dir(leftM, fwd); dir(rightM, fwd); cputs("fwd "); n = wait_event(sensor_pressed, 0); dir(leftM, rev); dir(rightM, rev); cputs("rev "); msleep(500); // backup 1/2 sec speed(leftM, MAX_SPEED); speed(rightM, MAX_SPEED); if (n > 0) dir(rightM, fwd), cputs("left "); else dir(leftM, fwd), cputs("right"); msleep(500); // turn } } sensor_pressed() liefert -1 oder 1, je nachdem, welcher Sensor gedrückt wird, das heißt, wenn ein Wert abfällt. 114 Licht-Sensor demo/linetrack.c zeigt,wie man einen aktiven Licht-Sensor programmiert, um einer Linie zu folgen. Die Unterschiede zu trusty.c bestehen im Wesentlichen darin, daß man Meßbereiche definieren muß — die man eigentlich zuerst per Programm kalibrieren sollte — und daß man den Sensor durch ds_active() mit Strom für seine Lichtquelle versorgen muß. Sound demo/robots.c zeigt, wie man das Sound-System programmiert. Mit wait_event() wartet man darauf, daß eine Funktion einen von Null verschiedenen Wert liefert; dsound_finished() ist genau für diesen Zweck vordefiniert. Multi-Tasking legOS hat preemptive multi-tasking mit Prioritäten. multi.c ist eine Variante von trusty.c auf der Basis von Subsumption. 9/zb/multi.c /* Trusty with active touch sensors and multi-tasking */ #include <conio.h> #include <unistd.h> #include <string.h> #include <dsensor.h> #include <dmotor.h> #define leftM motor_c #define rightM motor_a #define cat(a, b) a ## b #define speed(motor, value) #define dir(motor, value) static struct struct { int } left = { { right = { { // configure motors cat(motor, _speed)(value) cat(motor, _dir)(value) { // motor states for idle speed, how; } now, next; -1, -1 }, { 2*MAX_SPEED/3, fwd } }, -1, -1 }, { 2*MAX_SPEED/3, fwd } }; #define leftB SENSOR_3 #define rightB SENSOR_2 #define pressed > 0xf000 // configure bumpers // activated if released static wakeup_t left_pressed (wakeup_t data) { return leftB pressed; } static wakeup_t right_pressed (wakeup_t data) { return rightB pressed; } Das Beispiel zeigt, daß einige Zugriffe unglücklich definiert sind. wait_event() übergibt als User-Daten nur einen 16-Bit-Wert, Zeiger sind aber 32-Bit. Der Zugriff auf Sensoren erfolgt durch externe Variablen, die man folglich nicht als User-Daten übergeben kann. Daß man auf Motoren nur über Funktionsnamen zugreifen kann, ist ebenfalls unglücklich. 115 Mit niedrigster Priorität unterhält idle() den gewünschten Motorzustand: 9/zb/multi.c /* maintain desired motor state */ static int idle (int argc, char **argv) { while (1) { if (left.now.speed != left.next.speed) speed(leftM, left.now.speed = left.next.speed); if (right.now.speed != right.next.speed) speed(rightM, right.now.speed = right.next.speed); if (left.now.how != left.next.how) dir(leftM, left.now.how = left.next.how); if (right.now.how != right.next.how) dir(rightM, right.now.how = right.next.how); yield(); } } int main (void) { // starts tasks and exits execi(leftBumper, 0, NULL, PRIO_NORMAL+1, DEFAULT_STACK_SIZE); execi(rightBumper, 0, NULL, PRIO_NORMAL+1, DEFAULT_STACK_SIZE); execi(idle, 0, NULL, PRIO_NORMAL, DEFAULT_STACK_SIZE); return 0; } In kernel/kmain.c stellt man fest, daß der Task-Manager sofort gestartet wird. execi() führt eine Funktion als Auftrag für eine neue Task ein, wobei man die Priorität und den Stack festlegt. 116 Bei Druck auf die Stoßstangen wird der Motorzustand geändert und die Motor-Task muß entsprechend reagieren. Subsumption ergibt sich aus dem höheren Vorrang dieser Task. Bei msleep() kommt idle() zum Zug. 9/zb/multi.c /* bumpers control motors */ static void turn (const char *how) { int s = left.now.speed; left.next.how = right.next.how = rev; cputs("rev "); msleep(500); // backup and evade left.next.speed = right.next.speed = MAX_SPEED; if (strcmp(how, "right") == 0) left.next.how = fwd; else right.next.how = fwd; cputs((char*)how); msleep(500); left.next.speed = right.next.speed = s; left.next.how = right.next.how = fwd; cputs("fwd "); } static int leftBumper (int argc, char **argv) { while(1) { wait_event(left_pressed, 0); turn("right"); } } // avoid at left static int rightBumper (int argc, char **argv) { while(1) { wait_event(right_pressed, 0); turn("left "); } } // avoid at right 117 10 Handy Board LEGO kooperiert mit dem MIT. Dort hat Fred Martin 1989 einen Design-Kurs für Ingenieure entwickelt, dessen Herzstück das Handy Board ist — praktisch der Urahn des RCX. Das Handy Board enthält einen M68HC11 Microprozessor mit 32K RAM, der zum Beispiel mit dem frei verfügbaren Interactive C und in Assembler programmiert werden kann. Auf dem Board befinden sich 4 Ausgänge für Motoren, zwei Taster, 9 digitale und 7 analoge Eingänge, ein Anschluß für einen IR-Sender, ein IR-Empfänger, ein Drehknopf sowie ein LCD-Display. Man kann ein Expansion Board einstecken, auf dem sich Ausgänge für 6 Servos, 9 digitale Ausgänge (5 Volt, 5 mA), 4 Eingänge für LEGO Sensoren und 12 analoge Eingänge befinden. Dafür entfallen jedoch die analogen Eingänge 0 und 1 auf dem Handy Board selbst. Das Handy Board wird mit einem Telefonkabel mit einer kleinen Platine verbunden, an die man ein Netzteil (12V, 500mA, Zentrum negativ) und eine serielle Verbindung anschließen kann — das Netzteil kann man auch direkt mit dem Handy Board verbinden. Man kann das Handy Board fertig beziehen oder auch selbst bauen. 118 Truck Im Gehäuse unter dem Handy Board befinden sich Akkumulatoren, mit denen man einfache 9V-Motoren antreiben kann. Die Motoren muß man mit Schneckengetrieben und einem Riemenantrieb sehr stark untersetzen. Das Ganze ist etwas sperrig, aber man kann durchaus einen Lastwagen aus LEGO bauen, der das Handy Board trägt. An die Analog-Eingänge kann man zum Beispiel infrarote Reflex-Sensoren anschließen: Eine Leuchtdiode wird über einen Widerstand mit Strom versorgt und strahlt infrarotes Licht aus. Ein Phototransistor, passend zur Frequenz der Diode, mißt das reflektierte Licht und dient als Spannungsteiler zum 47K Widerstand am analogen Signal-Eingang. Sensor und Widerstand passen in einen kleinen LEGO-Stein. Vom Funktionieren überzeugt man sich zum Beispiel mit einer digitalen Kamera — deren Sucher ist Infrarot-empfindlich. Je nach Anwendung kann man die Steine näher an die Fahrbahn bringen. 119 Firmware Das Handy Board wird in Assembler programmiert. Von Motorola ist ein Assembler frei verfügbar, den Randy Sargent für das Handy Board modifiziert hat. Fred Martin, Randy Sargent und Anne Wright haben als Firmware einen Interpreter für PCode implementiert, der preemptive multi-tasking unterstützt. Den Interpreter lädt man mit einem Programm wie dl; am Handy Board hält man Start und Stop gedrückt, während man einschaltet: c> dl 6811 .s19 file downloader. Version 7.31 Aug 3 1994 Copyright Randy Sargent 1994 Arguments to dl are: filename.s19 Download this filename -port com1 (or com2-4) Set which serial port to use -libdir directory Set which directory has the .s19 files Possibly useful for boards other than the 6.270 board or rug warrior: -bootstrap filename.s19 Use filename.s19 as the bootstrap -config_reg value Set config register to value (in hex). -bs_ignore Ignore serial responses in bootstrap sequence -loopback Expect hardware serial echoes from board -eeprom Program EEPROM, not RAM -1.5 Set -bs_ignore, -loopback, -eeprom, config_reg 0xff -ignore_input Completely ignore returning serial c> dl pcode_hb.s19 6811 .s19 file downloader. Version 7.31 Copyright Randy Sargent 1994 Using port com1 Aug 3 1994 Please place board in download mode and press RETURN To quit, press Q For help, press H Downloading 256 byte bootstrap (229 data) Download successful Config is 0x0C Loading pcode_hb.s19 Sending pcode_hb.s19 Done. Der Bootstrap-Code lädt anschließend S-Records. Um den Interpreter zu starten, muß man das Handy Board aus- und einschalten; der Interpreter meldet sich im LCD-Display. 120 Interactive C Mit dem Interpreter kommuniziert man über den interaktiven C-Compiler ic: c> ic Interactive C for 6811. Version 2.851 (Jul 27 1994) IC written by Randy Sargent and Anne Wright. Copyright 1994. (uses board pcode by R. Sargent, F. Martin, and A. Wright) This program is freeware and unsupported. It is provided as a service to hobbyists and educators. Type 'about' for information about support and obtaining newer versions of IC. Attempting to link to board on port com1 Synchronizing with board Pcode version 2.81 present on board Loading /ic/libs/lib_hb.lis. Loading /ic/libs/lib_hb.c. Loading /ic/libs/lib_hb.icb. Loading /ic/libs/hbsensor.c. Loading /ic/libs/sony-ir.icb. Loading /ic/libs/libexpbd.icb. Loading /ic/libs/expservo.icb. Initializing interrupts Downloading 1727 bytes (addresses 8000-86BE): Globals initialized. Type 'help' for help 1727 loaded C> help Help: You may type a C expression at the prompt. with ; and others may end with } Most C expressions end You may also type one of the following commands: about load filename unload filename ps kill_all quit help list files list functions list globals mem Display useful info about IC Load filename.c into the board Unload filename.c from the board Print the status of currently executing processes Kill all processes Quit Display this message List all files List all functions List all global variables Show free memory on host C> 1+3; Downloading 7 bytes (addresses C200-C206): Returned <int> 4 7 loaded ic schickt arithmetische Ausdrücke zum Handy Board und läßt sie dort auswerten — damit kann man zum Beispiel Funktionen aufrufen. Am Anfang werden die Dateien geladen, die in lib_hb.lis aufgeführt sind. Dort sollte man nur die Dateien konfigurieren, die wirklich benötigt werden. 121 Sprachumfang Es gibt keinen Präprozessor, aber man kann gcc -E verwenden und #-Zeilen mit grep entfernen. Es gibt lokale und globale int, long und float Variablen und Vektoren sowie char Vektoren. Globale Variablen werden nur bei Reset und Laden eines Programms initialisiert. Es gibt Zeiger. Vektor-Argumente müssen als Vektoren deklariert werden. Es gibt dezimale, hexadezimale (0x) und binäre (0b) int und long (L) Konstanten sowie float und char Konstanten. int hat 16 Bits, float und long haben 32 Bits. Es gibt die üblichen Operatoren und mathematischen Funktionen. Es gibt kombinierte Zuweisungen und den Komma-Operator, aber keine bedingte Bewertung (?:). Es gibt Blöcke und if-, while- und for-Anweisungen sowie break und return. Prozeduren liefern void. printf() erlaubt die Formatelemente %b (binär), %c, %d, %f, %s und %x. Motoren kontrolliert man mit fd(motor), bk(motor), off(motor) und alloff() oder ao() sowie motor(motor, speed), wobei speed von -100 bis 100 per Software durch PulsbreitenModulation erreicht wird. Sensor-Werte erhält man mit digital(port) im Bereich 0/1 und analog(port) im Bereich 0/255. Knöpfe kann man mit start_button() und stop_button() im Bereich 0/1 und mit knob() im Bereich 0/255 untersuchen. Für IR-Eingaben muß man mit sony_init(1) einen Treiber für Sony-Geräte aktivieren und dann Werte mit ir_data(dummy) einlesen. sony_init(0) inaktiviert den Treiber. beep() und tone(float frequency, float length) machen Musik. start_process(aufruf(...)) erzeugt und startet einen Prozeß und liefert seine Nummer. Mit zusätzlichen Argumenten kann man die Zeitscheibe und die Stack-Länge vorgeben. kill_process(pid) zerstört den Prozeß. hog_processor() verlängert die Zeitscheibe, defer() verkürzt sie. Es gibt dann noch Funktionen wie peek() und poke() zum direkten Speicherzugriff. Je nach Konfiguration kann es weitere Funktionen geben, denn ic lädt sowohl C- als auch vorübersetzte Assembler-Programme (.icb). Beispiele etc.c enthält einfache Funktionen zum Testen vom Truck. irTest.c enthält eine einfache Funktion, um Codes einer Fernsteuerung zu finden. remote.c muß mit dem Präprozessor bearbeitet werden; dann kann man den Truck fernsteuern. multi.c muß mit dem Präprozessor bearbeitet werden und ist ein auf Subsumption beruhender Line-Tracker, aber die Reflektions-Sensoren sind offenbar zu langsam, um den Truck zuverlässig zu steuern. 122 123 11 IPC@CHIP Die Firma Back ist auf möglichst kleine PCs zum industriellen Einsatz spezialisiert. Der IPC@CHIP enthält einen 80186-kompatiblen Prozessor mit zwei seriellen und einer EthernetSchnittstelle sowie einer rudimentären DOS und TCP/IP-Implementierung mit den Protokollen FTP, HTTP und TELNET, alles in einem einzigen Chip. Der Chip kann zum Beispiel mit einer alten Version von Turbo C programmiert werden, die man kostenlos aus dem Archiv von Borland beziehen kann. 124