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