Forth für MEGA32 (Gilt für Versionen Q4x) Beschreibung der Befehle: .S ist nützlich zur Kontrolle des Datenstacks. Ergebnis ist entweder die Meldung "leer" oder ähnliches wie: [2 ] 66 77 Dabei steht zwischen den [] die Anzahl der Byte auf dem Stack. Anschließend werden diese angezeigt. Der oberste Wert steht rechts und wird in Forth als TOS (Top-of-Stack) bezeichnet. . schickt den obersten Wert auf dem Datenstack zum PC-Bildschirm. Dieser Wert wird damit vom Stack entfernt. War das Stack vorher leer, gibt es eine Fehlermeldung + Neustart des MEGA32. Beim Neustart wird ein vorher zum ROBOprogy übertragenes Programm gelöscht. Der Befehl ist wirklich NUR ein einziger Punkt ohne Nachbarn! D. nimmt die beiden obersten Bytes vom Stack, bildet eine Zahl im Bereich -32767...+32767 und schickt diese zum PC. Wie vorher gilt: Es müssen mindestens zwei Byte aus dem Stack sein. CR erzeugt eine neue Zeile auf dem PC-Bildschirm. Dieser Befehl ist nur zusammen mit TEXT sinnvoll. TEXT sendet den folgenden Text (durch Leerzeichen begrenzt) zum PC. Dieser Text darf seinerseits weder Leerzeichen noch : besitzen, man kann Unterstriche _ verwenden. Die Einstreuung von Texten an Schlüsselpunkten des Programms kann die Fehlersuche vereinfachen. MRESET setzt den MEGA32 in den Anfangszustand (soft start) und sollte immer zuerst zum ROBOprogy übertragen werden. ******* Mathe 8-Bit-Werte ********* Sensoren rund um den Roboter liefern entweder - nur 1 Bit (Schalter ein/aus). Der ROBOprogy macht daraus 8 Bit, die alle das gleiche bedeuten und arbeitet mit Bytes weiter. - oder 8 Bit (ADC). Das ist die "Normalbreite" für den ROBOprogy. - oder 16 Bit (Frequenz oder Inkrementalzähler). Diese Werte werden in zwei Bytes gespeichert und werden meist mit Befehlen bearbeitet, die mit D... beginnen wie D2*. Die Beschränkung auf 8 Bit hat den Vorteil, dass die Prozessoren billig und kompakt sind und wenig Strom verbrauchen. Bei kleinen Robotern besteht das Programm zu 99% aus Vergleichen und logischen Entscheidungen, es wird sehr wenig gerechnet. 8 Bit sind ideal. Wenn viel gerechnet werden muss, macht die Beschränkung auf 8 Bit das Programmieren mühsam und kann schwer zu findende Fehler wie overflow bei ganz speziellen Sensorwerten erzeugen. Gegen manche Fehlermöglichkeiten wurden Spezialbefehle wie U+(MAX=255) erfunden, andere lassen sich einfach vermeiden, indem man Berechnungen mit 16-Bit-Zahlen ausführt. Für die Übergänge zwischen diesen beiden Bereichen gibt es Befehle. Im folgenden Abschnitt wird 8-Bit-Mathe gemacht. Beachten Sie, dass Forth nach Zahlen-Überläufen (runtime error) keine Fehler meldet und auch nicht ins Nirvana springt. Der Roboter wird aber falsch reagieren. Konstante Werte (wie 49 oder -27) werden normal eingegeben. Begrenzungszeichen ist "Space". Wertebereich: -127..255 Vorsicht: Die 8-Bit-Darstellung $D9 kann -39 oder 217 bedeuten, für die Interpretation ist der Programmierer zuständig. Der ROBOprogy rechnet immer nur mit $D9. + { a b -- c } nimmt die beiden obersten Werte a und b vom DatenStack, addiert sie und legt das Ergebnis c auf das Stack. Wertebereich ganzzahlig 00..255 ohne Überlaufmeldung! Rechnet auch vorzeichenrichtig im Bereich -127..+127. Beispiele: 45 9 + [Return] ergibt 54. -56 13 + [Return] ergibt -43. Der PC zeigt 213 an. 250 33 + [Return] ergibt 27 (=288 mod 255) Anmerkung: Zwischen { und } steht links von -- die Stackbelegung vor der Funktionsausführung; rechts von -- steht die Stackbelegung nach der Funktionsausführung. Noch tiefer liegende Bytes werden nicht geändert und haben mit dieser Funktion nichts zu tun. Es ist ratsam, im eigenen Programm die Änderung der Stackbelegung auf gleiche Art unmittelbar nach der Vereinbarung des Namens anzugeben. Der Text zwischen { und } wird aber vom Programm überlesen und dient nur zur Info für den Programmierer. - { a b -- c } subtrahiert a-b und legt das Ergebnis c auf das Stack. * { a b -- c } multipliziert die obersten beiden Werte a und b vorzeichenrichtig. Keiner der beiden Faktoren soll größer sein als 127, sonst entspricht das Ergebnis nicht den mathematischen Erwartungen. Auch darf das Produkt c nicht größer als 127 werden. Wenn dieser Bereich nicht genügt, muss man mit 16-Bit-Zahlen arbeiten. U* { a b -- c } multipliziert die obersten beiden Werte a und b und ignoriert dabei die Vorzeichen. Hier darf das Ergebnis c zwischen 0 und 255 liegen. U*/ { a b c -- d } (nur unsigned, ignoriert alle Vorzeichen) erwartet drei 8-Bit-Werte und liefert einen zurück. Die ersten beiden a und b werden zu einem 16-Bit breitem Zwischenergebnis multipliziert. Dieses wird dann durch den dritten Wert c dividiert. Das Ergebnis d ist ein gerundeter 8-Bit-Wert. Beispiel: 110 200 185 U*/ [Return] berechnet (110*200)/185 = 119 256*/ { a b -- c } multipliziert a und b und dividiert das Zwischenergebnis immer durch 256. Vorteil: erheblich schneller als */, weil die Division durch eine schnelle Bitverschiebung ersetzt wird. Dieser Befehl berechnet vorzeichenrichtig das Ergebnis c. Beide Werte sollten deshalb im Bereich -127...+127 liegen. Beispiel: -47 -55 256*/ [Return] liefert das Ergebnis 10 -47 55 256*/ [Return] liefert das Ergebnis -10. Der PC zeigt aber 245 (=255-10) an U256*/ { a b -- c } macht fast das Gleiche für positive Zahlen im Bereich 0...255. Beispiel: 129 177 256*/ [Return] liefert 89 128*/ und 64*/ arbeiten entsprechend mit Vorzeichen. Da in diesen Befehlen die Division durch eine Bitverschiebung vorgenommen wird, dauert die gesamte Berechnung nur etwa 2 Mikrosekunden. U128*/ und U64*/ arbeiten entsprechend mit positiven Zahlen Anmerkung: Die eben besprochenen Befehle werden oft zur Berechnung kleiner Korrekturfaktoren verwendet. Man wird zum Beispiel nie zwei Helligkeitssensoren finden, die bei gleicher Helligkeit gleiche Zahlen liefern. Wenn das Ergebnis vom 4. ADC um den Faktor 1,17 vergrößert werden muss, kann man schreiben: 5ADC 150 U128*/ und erhält das gewünschte Resultat. 2* { a -- b } schiebt den obersten Wert a eine Stelle nach links. Wenn dieser vorher kleiner als 128 war, entspricht das einer Verdopplung. Das vorher 7. Bit (Vorzeichenbit) geht verloren, in das 0. Bit wird eine 0 geschoben. Unverändert vorzeichen- richtig einsetzbar. In manchen Programmiersprachen heisst dieser Befehl SHL (=shift left). 2/ { a -- c } schiebt a vorzeichenrichtig nach rechts und legt Das Ergebnis c wieder auf das Stack. Das entspricht einer Halbierung mit Abrundung, falls a eine Zahl im Bereich -127..+127 war. Intern wird trotz des Schiebevorgangs das oberste (7.) Bit beibehalten. Das vorher 0. Bit geht verloren. In manchen Programmiersprachen heisst dieser Befehl ASR (=arithmetic shift right). U2/ { a -- c } halbiert positive Werte zwischen 00..255. Das oberste Bit ist anschließend immer 0. In manchen Programmiersprachen heisst dieser Befehl SHR (=shift right) 1+ { a -- c } addiert 1 zum obersten Wert a. Vorsicht: aus 255 wird 0! Es gibt keine Warnung oder Fehlermeldung. 1- { a -- c } subtrahiert 1. Aus 0 wird 255 oder -1, je nach Rechenmethode! NEG { a -- c } vertauscht das Vorzeichen des obersten Wertes a. Aus 34 wird -34. 2EXP { a -- c } ersetzt den obersten Wert a des Datenstacks durch 2^a und legt das Ergebnis c auf das Stack. Beispiel: 5 wird ersetzt durch 32 = $20 = %00100000 Im Byte c ist immer genau ein Bit gleich 1, die anderen sind 0. ABS { a -- c } bildet den Betrag des obersten Wertes a. Positive Werte wie 69 bleiben unverändert; ein negativer Wert wie -83 wird zu 83. U+(MAX=255) { a b -- c } kann bei speziellen Problemen helfen, weil es bei dieser Addition keinen Überlauf gibt, wenn man zwei beliebige positive Werte a und b im Bereich 0...255 addiert. Ein Überlauf entsteht, wenn die Summe zwischen 256 und 511 liegen würde. Für die Speicherung dieser Werte müsste man 9 Bit verwenden, ein Byte besitzt aber nur 8 Bit. Deshalb würde für a=66 und b=201 der + Befehl das falsche Ergebnis c=11 (=267-256) liefern. Der Befehl U+(MAX=255) begrenzt aber auf c=255. Das ist zwar auch nicht richtig, für manche Anwendungen aber sinnvoller als 11. U-(MIN=255) { a b -- c } berechnet a-b und kennt ebenfalls keine negativen Zahlen. Ein internes Programm begrenzt das Ergebnis c auf den niedrigsten Wert 0. Beispiel: Für a=97 und b=101 würde der - Befehl das sehr falsche Ergebnis 252 liefern. Der Befehl U-(MIN=0) begrenzt dagegen auf 0. Dieser Wert liegt deutlich näher beim korrekten Ergebnis -4. Den zuletzt genannten Befehl benötigt man zum Beispiel, wenn die Geschwindigkeit eines Motors per Programm geändert werden muss. Dann kann man vom Tempo immer konstante Werte wie 4 subtrahieren, bis die Geschwindigkeit des Roboter passt und muss nicht befürchten, dass sich wegen Zahlenüberlauf die Drehrichtung umkehrt. ************ Logik-Befehle ************* AND { a b -- c } nimmt zwei Werte a und b vom Stack und bildet bitweise die logische UND-Verknüpfung. Im Ergebnis c haben nur dann Stellen eine 1, wenn die entsprechenden Stellen in a UND b 1 waren. Verwendung sinnvollerweise mit HEX- oder BinärZahlen. Beispiel im Hex-System: $55 $43 AND [Return] liefert $41. Angezeigt wird die Dezimalzahl 65. Beispiel im Binärsystem: %10011010 %01110111 AND [Return] liefert %00010010 OR { a b -- c } nimmt zwei Werte a und b vom Stack und bildet bitweise die logische ODER-Verknüpfung. Im Ergebnis c haben alle Stellen eine 1, wenn die entsprechenden Stellen in a ODER b 1 waren. Verwendung sinnvollerweise mit HEX- oder BinärZahlen. Beispiel im Hex-System: $55 $43 OR [Return] liefert $57. Angezeigt wird die Dezimalzahl 87. Beispiel im Binärsystem: %10011010 %01110110 OR [Return] liefert %11111110 XOR { a b -- c } nimmt zwei Werte a und b vom Stack und bildet bitweise die logische EXCLUSIV-ODER-Verknüpfung. Im Ergebnis c haben nur dann Stellen eine 1, wenn die entsprechenden Stellen in a und b VERSCHIEDEN waren. Verwendung sinnvollerweise mit HEX- oder BinärZahlen. Beispiel im Hex-System: $55 $43 XOR [Return] liefert $16. Angezeigt wird die Dezimalzahl 22. Beispiel im Binärsystem: %10011010 %01110111 XOR [Return] liefert %11101101 *********** Mathe 16-Bit-Werte *********** Konstanten 16-Bit-Werten (wie 4039) muss D( vorangestellt werden. Begrenzungszeichen ist "Space". Wertebereich: -32767..32767. Beispiel: D(-2317 legt zwei Bytes auf das Datenstack: Zuerst das Low-, dann das High-Byte. Diese Zahl lautet in Hex: $F6F3. Prüft man das Datenstack, erhält man die Dezimalzahlen 243 und 246. Da ist kein einfacher Zusammenhang erkennbar. Zwischen D( und der folgenden Zahl KEINEN Zwischenraum einfügen! Es darf auch kein Funktionsname mit D( beginnen. SINGLE>DOUBLE { a -- x y } wandelt eine 8-Bit-Zahl vorzeichenrichtig in eine 16-Bit-Zahl um. Wenn a im Bereich 0..127 liegt, ist x=a und y=0. Wenn a im Bereich -127..-1 (oder 128..255) liegt, ist y=$ff. Wenn man die positive Zahl 210 in eine 16-Bit-Zahl umwandeln will, funktioniert S>D nicht, weil diese Funktion das 7.Bit prüft und dann eine ungewünschte Entscheidung trifft. In so einem Fall lässt man a unverändert auf dem Stack und legt eine 0 drüber. DOUBLE>SINGLE { a b -- c } wandelt eine 16-Bit in eine 8-Bit-Zahl um. Das funktioniert nur dann fehlerfrei, wenn die 16-Bit-Zahl im Bereich -127..+127 lag. Jenseits dieser Grenzen ist das Ergebnis falsch, weil das Ergebnis auf +127 bzw -127 begrenzt wird. Mehr passt nicht in ein Byte. Beispiel: Egal, ob die beiden obersten Bytes auf dem Stack 140 oder 2000 oder 19456 bedeuten, nach Ausführung des Befehls DOUBLE>SINGLE wird daraus auf dem Stack ein Byte mit dem Wert 127. Entsprechend wird aus zu großen negativen Zahlen immer -127. Bei allen folgenden Befehlen gilt: Wenn der erste Buchstabe ein D ist, wird ein 16-Bit-Ergebnis geliefert. D. { a b -- } schickt den obersten 16-Bit-Wert zum PC D+ { a b c d -- x y } addiert die beiden obersten 16-Bit-Zahlen D- { a b c d -- x y } subtrahiert die obersten 16-Bit-Zahlen D*DS { a b c -- x y } (Double*Single) multipliziert die zweitoberste 16-Bit-Zahl a b mit der obersten 8-Bit-Zahl c und erzeugt ein 16-Bit-Ergebnis x y D*SD { a b c -- x y } (Single*Double) Im Gegensatz zum obigen Befehl ist nun die Reihenfolge vertauscht: Die tiefer liegende 8-Bit-Zahl a wird mit der darüber liegenden 16-Bit-Zahl b c multipliziert. Das Ergebnis x y liegt dann auf dem Stack. D/DS { a b c -- x y } (Double/Single) dividiert den zweitobersten 16-Bit-Wert a b durch den darüber liegenden 8-Bit-Wert c . Beispiel: 22605 65 D/DS D. liefert .... Nebenbei: Der MEGA32 verfügt über keinen Divisionsbefehl, deshalb muss dieser per software nachgebildet werden. Die vielen erforderlichen Rechenschritte dauern etwa 100 mal länger als "normale" Befehle. D2* { a b -- c d } Verdoppelt die oberste 16-Bit-Zahl a b D2/ { a b -- c d } Halbiert die oberste 16-Bit-Zahl a b D1+ { a b -- c d } Addiert 1 zur obersten 16-Bit-Zahl a b D1- { a b -- c d } Verringert die oberste 16-Bit-Zahl a b um 1 DNEG { a b -- c d } Ändert das Vorzeichen der obersten 16-Bit-Zahl **** Folgende Funktionen mit D-Vorsatz machen für 16-Bit-Zahlen das Gleiche wie die entsprechenden Funktionen (ohne D) für 8-bit-Zahlen. Details können dort nachgelesen werden. DSWAP { a b c d -- c d a b } DOVER { a b c d -- a b c d a b } (Es gibt weder DDUP noch DDROP, stattdessen 2DUP und 2DROP verwenden!) D= { a b c d -- x } Wenn a=c und b=d, ist x=$FF, sonst x=0 DUNGLEICH { a b c d -- x } Wenn a=c und b=d, ist x=0, sonst x=$FF D> { a b c d -- x } Wenn die 16-Bit-Zahl a b größer ist als c d, ist x=$FF, sonst x=0 D< { a b c d -- x } Wenn die 16-Bit-Zahl a b kleiner ist als c d, ist x=$FF, sonst x=0 D0= { a b -- x } Wenn a=b=0, ist x=$FF, sonst x=0 DABS { a b -- x y } Bildet den Betrag von a b D0< { a b -- x } Wenn die 16-Bit-Zahl a b negativ ist, liegt b im Bereich $80..$FF. Dann ist x=$FF, sonst x=0. *********** Minimum/Maximum zweier Werte ****************** MIN { a b -- c } vergleicht vorzeichenrichtig die beiden obersten Werte a und b und lässt nur die kleinere der beiden auf dem Stack. Sinnvoll nur im Bereich -127..+127 UMIN { a b -- c } arbeitet arbeitet mit positiven Zahlen zwischen 00..255 MAX { a b -- c } (vorzeichenrichtig) lässt nur die größere der beiden obersten Zahlen a und b auf dem Stack. Sinnvoll nur im Bereich -127..+127 UMAX { a b -- c} lässt den größeren der beiden positiven Werte a und b (Bereich 00..255) auf dem DatenStack. DMIN { a b c d -- x y } vergleicht die beiden 16-Bit Zahlen a b und c d und lässt nur die kleinere auf dem DatenStack. DMAX { a b c d -- x y } vergleicht die beiden 16-Bit Zahlen a b und c d und lässt nur die größere auf dem DatenStack. ******** Datenstack umordnen *************** DUP { a -- a a } dupliziert den obersten Wert (ohne vorhergehende Prüfung) ?DUP { a -- a } oder { a -- a a } Der obersten Wert a wird geprüft: nur wenn a ungleich 0 ist, wird er dupliziert; er steht dann zweimal auf dem Stack. Wenn der oberste Wert 0 ist, passiert nichts. Dieser Befehl steht gelegentlich vor IF, weil bei dieser Fallunterscheidung 0 auch "nein" bedeutet. Man kann sich auf diese Weise die Kombination ELSE DROP ersparen. 2DUP { a b -- a b a b } dupliziert das oberste Werte-PAAR! Es könnte deshalb auch DDUP heissen. Beispiel: Aus 22 33 44 wird 22 33 44 33 44. 2DUP darf nicht mit DUP DUP verwechselt werden! DROP { a -- } entfernt den obersten Wert vom Stack 2DROP { a b -- c } entfernt die beiden obersten Werte (ist schneller als DROP DROP). Dieser Befehl könnte auch DDROP heissen. SWAP { a b -- b a } vertauscht die beiden obersten Werte. Beispiel: Aus 22 33 44 SWAP wird 22 44 33 OVER { a b -- a b a } setzt den vorletzten Wert nochmal ganz oben auf das Stack. Beispiel: aus 22 33 44 OVER wird 22 33 44 33 ROT { a b c -- b c a } (rotieren) holt den drittletzten Wert nach oben und schiebt die anderen beiden je einen Platz nach unten. Beipiel: Aus 22 33 44 55 ROT wird 22 44 55 33 ******** Auf dem Returnstack zwischenspeichern ************ Die folgenden Befehle sind vorsichtig einzusetzten, weil man damit das ReturnStack zweckentfremdet. Dieses ist primär für die Zwischenspeicherung von Rücksprungadressen gedacht. Dazu etwas Hintergrund: Jedes längere Programm wird in Funktionen zerlegt, die bei Forth mit : beginnen und mit ; enden. Wenn nun eine Funktion A eine andere Funktion B aufruft, muss die momentane Programmadresse in A auf dem ReturnStack gespeichert werden. Sobald die Funktion B zu Ende ist, wird die Programmadresse vom ReturnStack geholt und die Funktion A an der Stelle fortgesetzt, wo sie vorher unterbrochen wurde. Falls in diesem Augenblick ein falscher Wert auf dem ReturnStack liegt, springt das Programm irgendwo hin, nur nicht zur korrekten Stelle in der Funktion A. Also: Finger weg vom ReturnStack. Ein weiterer Punkt: Wenn in einer Funktion eine DO..LOOP Wiederholung (Schleife) vorkommt, werden die Zählvariable i sowie der Endwert, bei dem die Schleife endet, auch auf dem ReturnStack gespeichert. Der Befehl LOOP holt sich blind diese Werte vom Returnstack und entscheidet damit, ob eine weitere Wiederholung gemacht wird oder nicht. Wenn man also in der Umgebung einer DO..LOOP Schleife etwas am Returnstack ändert, kann das die Schleife ärgern. Also: Finger weg vom ReturnStack. Es hat sich aber gezeigt, dass es mitunter ganz praktisch ist, "schnell mal" dort ein paar Bytes zwischenzuspeichern und rechtzeitig wieder wegzuräumen. Das geht schneller als mit Variablen. Dazu gibt es folgende Befehle: >R { a -- } entfernt den obersten Wert vom Datenstack und legt ihn auf den ReturnStack. R> { -- a } holt ihn wieder zurück zum Datenstack. DUP>R { a -- a } kopiert den obersten Wert zum ReturnStack. Dieser Befehl ist eine Abkürzung für die beiden Befehle DUP >R und deshalb schneller. R>DROP { -- } entfernt den obersten Wert vom ReturnStack. Der DatenStack bleibt unverändert. Dieser Befehl ist eine Abkürzung für die Kombination R> DROP R@ { -- a } kopiert den obersten Wert vom ReturnStack zum DatenStack. Der ReturnStack bleibt unverändert. Es gibt auch Befehle, mit denen man immer gleich zwei Bytes hin und her transportieren kann. Diese werden in erster Linie beim Rechnen mit 16-Bit-Zahlen verwendet. D>R { a b -- } Zwei Bytes sind nun auf dem R-Stack. DR> { -- a b } so kommen beide Bytes wieder zurück. DDUP>R { a b -- a b } Ein Duplikat von a b kommt auf das R-Stack DR>DROP { -- } Auf dem R-Stack wurden die beiden obersten Bytes gelöscht. Das Datenstack ist unverändert. DR@ { -- a b } Die beiden obersten Bytes wurden vom R-Stack zum DatenStack kopiert. Das R-Stack ist unverändert. ******* Vergleichen von Bytes ************* = { a b -- x } nimmt die beiden obersten Werte a und b vom DatenStack und prüft, ob diese gleich sind. Wenn ja, wird x=$FF (=wahr) auf das DatenStack gelegt, andernfalls x=0 (=falsch). UNGLEICH { a b -- x } Wenn a und b ungleich, wird x=$FF auf das DatenStack gelegt, sonst x=0. > { a b -- x } prüft vorzeichenrichtig für Zahlen im Bereich -127..+127, ob der a größer ist als b. Wenn ja, wird x=$FF auf das DatenStack gelegt, sonst x=0. U> { a b -- x } (unsigned) arbeitet mit positiven Zahlen im Bereich 00..255. < { a b -- x } prüft vorzeichenrichtig, ob a kleiner ist als b. Wenn ja, wird x=$FF auf das DatenStack gelegt, sonst x=0. U< { a b -- x } (unsigned) arbeitet entsprechend nur mit positiven Zahlen. 0= { a -- x } prüft, ob a=0 ist und ersetzt ihn dann durch x=$FF. Jeder (!) andere Wert a wird durch x=0 ersetzt. 0< { a -- x } prüft das 7. Bit (das Vorzeichenbit) von a. Ist dieses 0, wird der Wert des Bytes a als positiv interpretiert und durch x=0 ersetzt. Ist das höchstwertige Bit gleich 1 (liegt also der Wert zwischen 128 und 256 bzw. zwischen -127 und -1 je nach Interpretation), so wird a durch x=$FF ersetzt. BETWEEN { a b c -- x } (englisch für dazwischen) prüft vorzeichenrichtig, ob sowohl b 125 Hz, 875 Hz, 1125 Hz, 1875 Hz, 2125 Hz, .... bit2/1/0 = 011 -> 62 Hz, 938 Hz, 1063 Hz, 1938 Hz, 2063 Hz, ... bit2/1/0 = 110 -> 21 Hz, 145 Hz, 187 Hz, 312 Hz, 354 Hz, ... bit2/1/0 = 010 -> 10 Hz, 156 Hz, 177 Hz, 323 Hz, 344 Hz, ... Die höheren Werte der jeweiligen Zeile sind systembedingt und leider automatisch dabei. Ursache ist das Abtasttheorem. Man kann sie mit Frequenzfiltern (nicht im ROBOprogy enthalten) dämpfen oder bevorzugt verstärken. Das ist nur dann erforderlich, wenn die unerwünschten Frequenzen tatsächlich stören. Meist sind keinerlei Filter notwendig, oft reichen auch simple Tiefpässe aus Kondensator und Widerstand, die alle Frequenzen oberhalb von 150 Hz abschneiden. Wozu benötigt man diese Sonderfunktion? Anlass war der Einsatz des ROBOprogy in Robotern, die leuchtende Bälle in ein Ziel schieben sollen. Der Ball hat eine LED mit Batterie eingebaut, das Ziel ebenfalls. Und dann ist da noch die Raumbeleuchtung oder die Sonne. Da hatte der Roboter Probleme, die Lichtquellen auseinander zu halten. Unsere Lösung war: Ball und Ziel blinken mit unterschiedlichen Frequenzen und lassen sich leicht unterscheiden. Weil die Raumbeleuchtung und die Sonne mit anderen Frequenzen oder gar nicht blinken, "sieht" sie der Roboter nicht. Zusätzlich kann man am Spielfeldrand eine Bake anbringen, die mit einer noch anderen Frequenz blinkt und die Orientierung erleichtert. Die Orientierung setzt voraus, dass der Roboter zwei "Augen" hat, damit er zwischen links und rechts unterscheiden und sich für eine Fahrtrichtung entscheiden kann. Aus diesem Grund wird die Messung zweikanalig mit identischen Eigenschaften an 5ADC und 6ADC ausgeführt. Die beiden Ergebnisse der Analyse können mit dem Befehl 56AMPL@@ { -- a b } auf das DatenStack geholt und verglichen werden. Die Ergebnisse liegen immer im Bereich 01..255. Anschliessend werden sie im Ergebnisspeicher automatisch gelöscht (auf dem Stack bleiben sie selbstverständlich erhalten!), damit nicht zweimal die gleichen Ergebnisse geholt werden. Erst wenn die folgende Analyse beendet ist, kann man mit 56AMPL@@ wieder Ergebnisse holen, die nicht Null sind. Zweck: Kontrolle, ob ein aktualisierter Messwert vorliegt. Vor jeder Analyse sind genügend Messwerte zu sammeln, deshalb muss einige Zeit gewartet werden. Eine Messung dauert: 100ms, wenn das Bit0 in CONFIG den Wert 1 hat 580ms, wenn das Bit0 in CONFIG den Wert 0 hat Es gibt noch eine weitere Möglichkeit, die Fourier-Analyse zu beeinflussen. Mit dem Bit2 in CONFIG kann man wählen, wie viele Messwerte (Samples) gesammelt werden, bevor die Auswertung beginnt. a) Wenn Bit2=0 ist, werden sechs Perioden lang je 16 Samples pro Periode gesammelt. Das ergibt - in der Sprache der Hochfrequenztechnik - geringe Trennschärfe. In diesem Fall muss die in der Tabelle angegebene Frequenz nicht exakt getroffen werden. Wie hoch diese Frequenz sein soll, hängt eng mit der Abtastfrequenz von 5ADC und 6ADC zusammen. a1) Wenn Bit0=1, liegt das Maximum bei 62,5 Hz a2) Wenn Bit0=0, liegt das Maximum bei 10,4 Hz b) Wenn Bit2=1 ist, werden zwölf Perioden lang je 8 Samples pro Periode gesammelt. Das ergibt - in der Sprache der Hochfrequenztechnik - höhere Trennschärfe. In diesem Fall muss die in der Tabelle angegebene Frequenz genauer getroffen werden. Wie hoch diese Frequenz sein soll, hängt eng mit der Abtastfrequenz von 5ADC und 6ADC zusammen. b1) Wenn Bit0=1, liegt das Maximum bei 125 Hz b2) Wenn Bit0=0, liegt das Maximum bei 20,8 Hz Was ist unter "Abtasttheorem" zu verstehen? Falls den Eingängen 5ADC und 6ADC kein (analoges) Frequenzfilter vorgeschaltet ist, ist auch bei deutlich höheren Frequenzen "Empfang" möglich. Das ist bei der Wahl 111 für Bit2/1/0 (um 125 Hz zu messen) die 7-fache bzw. 9-fache Frequenz. Oder auch die 15-fache bzw. die 17-fache Frequenz. Wieso genau diese Faktoren auftreten, will ich nun aber verschweigen. Hier noch einmal die Tabelle der optimalen Frequenzen: bit2/1/0 = 111 -> 125 Hz, 875 Hz, 1125 Hz, 1875 Hz, 2125 Hz, .... bit2/1/0 = 011 -> 62 Hz, 938 Hz, 1063 Hz, 1938 Hz, 2063 Hz, ... bit2/1/0 = 110 -> 21 Hz, 145 Hz, 187 Hz, 312 Hz, 354 Hz, ... bit2/1/0 = 010 -> 10 Hz, 156 Hz, 177 Hz, 323 Hz, 344 Hz, ... Je weiter man von den Sollfrequenzen abweicht, desto mehr verringern sich die "Empfangswerte" (das wird als Bandbreite bezeichnet). Die Zahlenwerte wurden so gewählt, dass es bis 200 Hz keine Überlappungen gibt und auch möglichst geringe Beeinflussung durch Helligkeitsschwankungen von Lampen, die mit 50 Hz betriebenen werden. Netzbetriebene Lampen stören auf Vielfachen von 100 Hz. Ferner ist zu beachten, dass bei der genannten Einstellung 125 Hz auch die kräftige 3. Oberwelle einer 42 Hz-Rechteckschwingung gemessen wird. Es sollte also keine LED mit 42 Hz ein- und ausgeschaltet werden. Die gewünschte Frequenz 125 Hz wird so eingestellt: : F125HZ { -- } $82 CONFIG! $81 CONFIG! $80 CONFIG! ; Die gewünschte Frequenz 62 Hz wird so eingestellt: : F62HZ { -- } 2 CONFIG! $81 CONFIG! $80 CONFIG! ; Ein letzter Hinweis: Dem ROBOprogy ist es gleichgültig, ob die Wechselspannung an 5ADC und 6ADC von einem Phototransistor oder von einem Mikrofon (mit Verstärker) kommt. Wenn man bei der genannten Einstellung 125 Hz mit beispielsweise 875 Hz ins Mikrofon pfeift, kann man eine Reaktion des ROBOprogy hervorrufen. Und mit zwei Mikrofonen und einer Blende dazwischen kann man die Richtung festellen, aus der der Pfiff kommt. *********** Entfernungen messen ****************** Man kann Entfernungen mit optischen Sensoren messen. Der bekannte Typ GP2D120 liefert abhängig von der Entfernung Ausgangsspannungen zwischen 0,4 V und 3 V. Leider ist die Beziehung sehr nichtlinear und das Programmieren erfordert einen häufigen Blick auf die Kennlinie. Mit dem ROBOprogy geht das einfacher, denn der gelieferte Messwert ist proportional zur Entfernung. Da man oft in zwei Richtungen messen muss, gibt es zwei gleichwertige Eingänge. D30a { -- c } linearisiert das Ergebnis des Einganges 1ADC für den kurzreichweitigen IR-Sensor GP2D120. Das Ergebnis wird in Vielfachen von 2 mm angegeben und ist sinnvoll im Bereich 4 cm..30 cm. Wenn zum Beispiel ein reflektierender Gegenstand 20 cm vor dem GP2D120 liegt, liefert der Befehl D30 den Wert c=100 auf das Datenstack. Das bedeutet 100*2 mm = 200 mm = 20 cm. Man kann aber auch ganz normal 1ADC abfragen und die Entfernung mit Hilfe des Datenblattes feststellen. D30b { -- c } macht das Gleiche für den Eingang 2ADC Entfernungen lassen sich auch mit einem Echolot messen. Dazu wird am Echolot-Ausgang 2OUT ein kurzer Impuls von etwa 0,6 Mikrosekunden Dauer erzeugt. Dieser bringt über einen MOSFET einen Ultraschall-Lautsprecher (miest 40 kHz) zum Schwingen. Eine bewährte Schaltung ist in Befehle.htm (Entfernungen messen mit Echolot) gezeigt und erläutert. Ein Ultraschall-Mikrofon empfängt nach einiger Zeit das Echo. Der nachfolgende Verstärker (nicht in ROBOprogy enthalten) erzeugt beim Überschreiten einer gewissen Lautstärke einen elektrischen Impuls, der zum Eingang 5IN des ROBOprogy führt. Sobald dieser Impuls ankommt, wird die Zeit seit der Echo-Erzeugung gemessen, mit der bekannten Schallgeschwindigkeit 340 m/s in Entfernung umgerechnet und auf das DatenStack gelegt. Nach einer anschließenden Pause von etwa 0,3 ms wird der Empfänger erneut scharf gestellt. Trifft ein weiteres Echo ein, wird auch dessen entsprechende Entfernung berechnet und auf das Stack gelegt. Nach 7 ms ist Meßende, dann wird die Anzahl der erfolgreichen Messungen und darüber $ff auf das Stack gelegt. Gab es keine Echos, liegt nur 0 auf dem Stack. ECHO { -- e1 e2 e3... } erzeugt den Impuls und liefert das Ergebnis der Entfernungsmessung. Es sind folgende Ergebnisse möglich: 0 kein Echo empfangen oder x1 1 $FF oder x2 x1 2 $FF oder x3 x2 x1 3 $FF oder x4 x3 x2 x1 4 $FF Wenn also mindestens ein Echo empfangen wurde, ist der oberste Wert auf dem Stack $FF (=TRUE), danach folgt die Anzahl der Echos und dann die Weglängen (gemessen in cm). Mehr als 10 Echos werden innerhalb der Messzeit 7 ms wohl kaum empfangen werden. Die Auswertung geht so: Zuerst wird geprüft, ob überhaupt ein Echo empfangen wurde. Wenn ja, die Anzahl gespeichert und dann die Einzelergebnisse ausgewertet. Manchmal werden Ergebnisse auch gelöscht, denn das erste Echo wird oft am Roboter selbst erzeugt und ist deshalb uninteressant. Der Befehl UBETWEEN ist gut geeignet, interessante Entfernungen herauszufischen. ************ Inkrementalzähler ******************** Ein sehr gutes Verfahren, Entfernungen zu messen, besteht darin, den gefahrenen Weg des Roboters zu protokollieren. Dazu bekommt jedes Rad eine Zahnscheibe mit zwei optischen Unterbrechern (Lichtschranken), die beim ROBOprogy an die Digitaleingänge 1IN/2IN oder 3IN/4IN angeschlossen werden. Mit jeweils zwei Lichtschranken kann der ROBOprogy die Drehrichtung jedes Rades erkennen und entsprechend bei jedem Zahn den Zählerstand entweder erhöhen oder verringern. Wenn die Räder nicht rutschen, ist dieses Verfahren das Genaueste und Problemloseste von allen. Wenn der Roboter symmetrisch gebaut ist, müssen beim Geradeausfahren beide Zähler bei jeder Prüfung den gleichen Wert haben. Etwaige Differenzen kann man zur Korrektur der Richtung heranziehen. Beim Kurvenfahren kann man aus Radumfang, Radabstand und den Zählerständen die Daten der gefahrenen Kurve bestimmen. Das ist ein Gebiet für mathematisch Begeisterte. INK00 { -- } setzt genauso wie Reset beide 16-Bit-Inkrementalzähler auf 00 zurück. Ein Zähler reagiert auf die Eingänge 1IN und 2IN, der andere auf die Eingänge 3IN und 4IN. Es wird jede Hell-Dunkel-Flanke gezählt. Eine 60-Scheibe erzeugt pro Umdrehung 240 Inkremente. WEG { -- c } liefert den zurückgelegten Roboweg (in 5mm-Schritten) als Ergebnis des Mittelwertes beider Inkrementalgeber. Intern sind das 16-Bit-Zähler, der nach Reset den Wert 0 erhält. Bei 82mm-Rad von LEGO und 60-Scheibe laufen die Zähler nach 70m über. Vorzeichenrichtige Umrechnung von -127cm..0..+255cm Umrechnung: Distanz in cm = RohInk*16/149 INKA { -- a b } kopiert die beiden Bytes a und b des Inkrementalzählers A zum Datenstack. Zuerst das "Low Byte", dann das "high Byte". Diese 16-Bit-Zahl sollte mit den D-Rechenbefehlen bearbeitet werden. Wertebereich: -32767..0..32767 INKB { -- a b } macht das Gleiche für den anderen Inkrementalzähler. INKA+! { a -- } nimmt einen 8-Bit-Wert vom Stack, erweitert ihn vorzeichenrichtig auf 16 Bit (wie der Befehl SINGLE>DOUBLE) und addiert dieses Zwischenergebnis zum Zähler A. Dann wird das Zwischenergebnis vom Zähler B subtrahiert. Mit diesem Befehl kann man die Richtung korrigieren, weil die Motorgeschwindigkeiten normalerweise so gesteuert werden, dass beide Inkrementalzähler Gleichlauf zeigen. ****** Peripherie-Geräte mit I2C bedienen *************** I2C! { c b a x 4 --- f } sendet insgesamt 4 Bytes (Adr x + 3 Daten-Bytes a, b, c in dieser Reihenfolge) an die Adresse x. I2C! { a x 2 --- f } sendet insgesamt 2 Bytes (Adr x + 1 Daten-Byte a in dieser Reihenfolge) an die Adresse x. Die jeweils benötigten Daten müssen vorher auf dem Stack bereitstehen. Alles schön in der Reihenfolge, die für Forth üblich ist: Die Bytes, die zuletzt auf das Stack gebracht wurden (weiter rechts), werden zuerst gesendet. Beispiel: Wenn nur ein Byte $89 an die Adresse $13 gesendet werden soll, lautet der Befehl $89 $13 2 I2C! Die Reihenfolge lautet: Daten, Adresse, Anzahl, Befehl. Alle diese Bytes werden vom DatenStack entfernt. Anschließend muss geprüft werden, ob die Sendung korrekt erfolgte. Es gibt folgende Möglichkeiten für f: f=0 Sendung ok, kein i2c-Fehler f=-1 nach 3 Schreib/Leseversuchen keine Antwort f=-2 I2C ist nicht eingeschaltet. Vorher $83 CONFIG! f=-3 mehr als 4 Bytes (einschließlich Adr) f=-4 mindestens 2 Bytes erforderlich (einschließlich Adr) Wenn es nicht klappen will, ist möglicherweise kein Baustein an den Anschlüssen SCL und SDA mit der angegebenen Adresse angeschlossen. Der ROBOprogy kann nur i2c-Master sein. I2C@ { x 2 -- b a f } holt 2 Bytes a und b vom Peripheriegerät mit der Adresse x und legt diese auf das DatenStack. Will man nur ein Byte von Adresse $25 holen, lautet der Befehl $25 1 I2C@ Oft benötigen die angesprochenen Bausteine Subadressen, die vorher mit I2C! übermittelt werden müssen. Nach dem Befehl I2C@ ist es ganz besonders wichtig, die Flagge f zu analysieren. Es gibt folgende Möglichkeiten: f=0 kein i2c-Fehler, Daten wie gewünscht f=-1 nach 3 Schreib/Leseversuchen keine Antwort f=-2 I2C nicht eingeschaltet. Vorher $83 CONFIG! f=-3 mehr als 3 Daten verlangt f=-4 mindestens 1 Datenbyte soll es schon sein! Dann gibt es noch eine Spezialität, wenn man mehrere Bytes von aufeinander folgenden (Sub-)Adressen lesen will. Seltsamerweise benötigen manche Geräte viel Zeit, um intern die Subadresse zu erhöhen (oder können nicht) und beenden die Sendung durch ein NACK-Signal. Dieses ist für den ROBOprogy aber das Zeichen, den Empfang zu beenden. Über den i2c-Kanal wurde dann schon etwas empfangen, aber nicht so viel wie erwartet. In so einem Fall ist f positiv und kleiner als die gewünschte Anzahl von Bytes. Ein Beispiel: Man will vom Gerät mit der Adresse $61 die Daten von drei aufeinander folgenden Subadressen $13, $14 und $15 lesen. Man übermittelt zuerst die Startadresse: $13 $61 2 I2C! und fordert dann vom gleichen Baustein drei DatenBytes an: $61 3 I2C@ Bei manchen Geräten kommt f=2 zurück, auf dem Stack liegen dann die Daten der Subadressen $13 und $14. Das dritte erwartete Byte wurde nicht gesendet, der entsprechende Baustein hat NACK gesendet. Man kann diese Besonderheiten vermeiden, wenn man immer nur ein Byte nach dem anderen transportiert. Vom Hersteller ATMEL sind für I2C fest verdrahtete Pins vorgegeben, die im ROBOprogy normalerweise als Digital-Eingänge 6in und 7in verwendet werden. Bevor man I2C-Betrieb machen kann, müssen die entsprechenden Anschlüsse des ROBOprogy intern umgeschaltet werden. Dann können die Eingänge 6in und 7in nicht mehr benutzt werden, weil die entsprechenden Pins am MEGA32 die Signale SDA und SCL erzeugen, die für I2C benötigt werden. Diese Umschaltung geschieht, wenn im Konfigurationsregister des ROBOprogy das Bit3 den Wert 1 hat (die anderen Bits haben andere Bedeutung). Hat dieses Bit 3 beispielsweise nach RESET den Wert 0, ist I2C abgeschaltet und 6in und 7in sind Eingänge. Man schaltet I2C mit folgenden Befehlen ein: $83 CONFIG! Man schaltet I2C mit folgenden Befehlen aus: 3 CONFIG! Nun sind 6in und 7in wieder normal verwendbar. Als vollständiges Beispiel folgt der Betrieb des elektronischen Kompasses CMPS03. Diese Funktion hat den Namen TEST: : TEST $83 conf! \ I2C einschalten begin 1 $60 2 i2c! drop cr \ Subadresse 1 schreiben $60 1 i2c@ \ Kompass-Richtung lesen if TEXT Fehler \ Klartext am Bildschirm des PC anzeigen else . \ Richtung am Bildschirm des PC anzeigen then 30 ms \ nicht zu oft 0 until ; \ endlose Wiederholung Wenn man den Kompass in die and nimmt und dreht, kann man am PC verfolgen, welche Richtungen gemeldet werden. In diese Endlosschleife kann man natürlich weitere Befehle einbinden, damit der Roboter auch andere sinnvolle Dinge erledigt. ********************** Motore steuern ************ 1MOT { v -- } steuert die Drehzahl von Motor 1. Für v sind nur Werte zwischen -127...+128 zulässig. Speziell gilt: 0 Leerlauf 2..127 gewünschte Drehzahl vorwärts -2..-127 gewünschte Drehzahl rückwärts 128 Bremsen Messgröße ist die Dynamospannung während kurzer Strompausen. 2MOT { v -- } entsprechend für Motor 2 MMGO { v -- } nimmt einen Wert vom Stack und steuert damit beide Motore. (Das Fahrzeug sollte geradeaus fahren) SPIN { v -- } nimmt einen Wert vom Stack und gibt ihn mit vertauschten Vorzeichen an die Motore. Beide Motore laufen entgegengesetzt. Der Roboter sollte sich drehen. Die Motordrehzahl wird über einen eingebauten PI-Regler konstant gehalten. Dazu wird 40-mal pro Sekunde die Drehzahl gemessen und der Motorstrom entsprechend geändert. Normal ist, dass der Strom zwei- oder dreimal nacheinander erhöht werden musste und dann wieder reduziert werden kann. Das geschieht alles automatisch. Wenn der Roboter über ein kleines Hindernis fährt, kommt es vor, dass bei 10 aufeinanderfolgenden Messungen jedesmal der Strom vergrößert werden musste. Wenn das noch häufiger der Fall ist, scheint der Motor blockiert zu sein. Darauf sollte das Programm irgendwie reagieren, sonst bleibt nur Not-Aus. MLOAD { -- a b } legt zwei Werte a und b (Bereich 00..15) auf das Stack, die jeweils angeben, wie oft nacheinander für welchen Motor die Pulsbreite verlängert werden musste, um die gewünschte Drehzahl konstant zu halten. Wenn der Roboter fest steckt, ist mindestens einer der beiden Werte zu hoch. Beispiel: MLOAD umax 12 > \ den größeren der beiden Werte prüfen IF BEEP \ Motor-Überlast, Piepsen oder pfeifen -22 MMGO \ beide Motore Rückwärtsgang 1 STOP c! \ Meldung an ein anderes Programm THEN \ Motore laufen, die nächsten Schritte ausführen ********************* Servos steuern **************** 1SERVO { a -- } erwartet einen Wert a im Bereich -127..0..127 auf dem Stack und erzeugt dann genormte Servo-Impulse am Ausgang 1 des 4017. Die Impulse sind in Neutralstellung 1,5 ms breit und folgen mit 24 ms Abstand. 2Servo .. 9Servo arbeiten entsprechend. Beispiel: Der Befehl 0 4Servo sollte das vierte Servo in Mittelstellung laufen lassen. Vorsicht: Es hat sich herausgestellt, dass die Servos mancher Hersteller bei einem Befehl wie 120 3Servo nicht einmal 90 Grad weit drehen, während andere voll anschlagen und brummend bei überhöhter Stromaufnahme stehen bleiben. Die maximal erlaubten Werte muss man bei jedem Fabrikat testen. Servos benötigen bis zu 500 mA und sollen deshalb nicht an die +5V-Versorgung des Prozessors angeschlossen werden. Sonst kann vorkommen, dass das Programm gelöscht wird, sobald ein Servo belastet wird. Optimal ist ein separater low-drop-Stabi für 5 V zwischen Batterie und Servos (LT1084, LT1085, LM2940). ********************** Zeiten messen ****************** Der ROBOprogy hat zwei Sorten eingebauter Uhren. Die erste Sorte bekommt vom Programm die Maximalzeit vorgegeben und zählt im Hintergrund jede Millisekunde (oder nach je 100 ms) einen Schritt runter. In einer Schleife sollte diese Uhrzeit immer wieder kontrolliert werden, ob Null erreicht ist. In dieser Schleife können auch andere Prozesse kontrolliert und gesteuert werden. Diese Uhren werden beispielweise gleichzeitig mit Roboter gestartet. Dann wird in einer Schleife geprüft, ob entweder die Zeit abgelaufen ist oder der Roboter ein Ziel erreicht hat. AT1! { a -- } nimmt den Wert a vom Stack und startet damit einen "Rückwärts"-Zähler, der pro Millisekunde um eine Einheit verringert wird. Bei 0 angekommen, passiert nichts mehr. Es ist Sache des Programms, beim Erreichen von 0 irgend etwas Sinnvolles zu machen. AT1@ { -- a } kopiert den aktuellen Stand dieses Zählers auf das Stack. BT2! und BT2@ machen Entsprechendes mit der zweiten Uhr. AT100! und BT100! starten Rückwärtszähler in 100 ms-Schritten. AT100@ und BT100@ lesen diese Zählerstände ab. Die zweite Sorte Uhr blockiert den ROBOprogy so lange, wie es der Wert auf dem Stack vorgibt. Dann erst geht es im Programm weiter. Wenn in der Zwischenzeit der Roboter irgendwo anstösst, kann das Programm nicht darauf reagieren. MS { a -- } nimmt den Wert a vom Stack und lässt den MEGA32 entsprechend viele Millisekunden lang nix tun. Während dieser Zeit reagiert das Programm nicht! Die maximale Zeit ist 255 ms. 0,1S { a -- } sorgt für eine 100 mal längere Pause. Beispiel: 40 0,1S lässt den ROBOprogy 4 Sekunden lang Siesta machen. Diese Pause kann maximal 25,2 s lang sein. ******************** Musik ****************** BEEP { -- } erzeugt einen 50 ms langen 500 Hz-Ton. Diese Frequenz wird im Hintergrund erzeugt, das Programm wird sofort fortgesetzt. BRUMM { -- } erzeugt einen 200 ms langen 250-Hz-Ton ********** Fuzzy-Logik ***************** Die Grundlagen dieser Art zu programmieren sind auf der homepage dargestellt. Hier folgen die Grundfunktionen der Klasseneinteilung: Die Funktion FUZZY-DOWN liefert volle Punktzahl (20) für kleine Messwerte und null Punkte für große Messwerte. Beim Wert a beginnt der Abfall, bei b endet er. In der Mitte zwischen a und b erhält p den Wert 10. Grafisch sieht das so aus: ------\ Knick bei a \ \ \------- Knick bei b ADC=0 ADC=mittel ADC=255 FUZZY-DOWN { ADC a b -- p } p liegt immer im Bereich 0..20 Die Funktion FUZZY-UP ist gewissermassen das Gegenteil; sie liefert keine Punkte für kleine Messwerte und volle Punktzahl (20) für große Messwerte. Beim Wert a beginnt der Anstieg, bei b endet er. Grafisch sieht das so aus: /------- Knick bei b / / -----/ Knick bei a ADC=0 ADC=mittel ADC=255 FUZZY-UP { ADC a b -- p } Die Funktion FUZZY-DREIECK vergibt maximale Punktzahl (20) rund um den Messwert b. Dort sieht man in der Grafik eine Spitze. Wie weit entfernt davon es überhaupt noch Punkte gibt, legen a und c fest. Links von a und rechts von c ist p immer gleich null. /\ Spitze bei b / \ Knicke bei a / \ Knick bei c -------/ \--------- ADC=0 ADC=mittel ADC=255 FUZZY-DREIECK { ADC a b c -- p } Ein vollständiges Beispiel für Fuzzy-Logik kann auf der homepage kopiert werden. ********************* Stackfehler finden ***************** Programmieren heisst immer, mit Fehlern zu kämpfen. Die Sprache Forth kennt eine ganz besondere Sorte Fehler - das sind Stackfehler. Der Programmierer hat ein Byte auf dem Stack vergessen oder er hat eines zu viel gelöscht. Um diese Fehler leichter zu finden, gibt es folgende Befehle, die im endgültigen Programm gelöscht werden sollten. ST! { -- } speichert den aktuellen Stand des DatenStack-Zeigers. ST? { -- } vergleicht den aktuellen Stand des DatenStack-Zeigers mit dem gespeicherten Wert. Bei gleichem Inhalt passiert nichts. Bei ungleichen Werten wird die Differenz an den PC gesendet und der MEGA32 macht Reset. Damit lässt sich verfolgen, in welchem Programm Daten auf dem Stack vergessen wurden. Bei diesem Verfahren müssen ROBOprogy und PC verbunden sein. ST1? { -- } macht fast das Gleiche, prüft aber, ob das Stack um ein Byte angewachsen ist. ST2?, ST3?, ST-1? ST-2? und ST-3? arbeiten entsprechend. ******* Programmablauf steuern ******************** Es gibt zwei grundlegende Arten, die Reihenfolge zu beeinflussen, in der das Programm abgearbeitet wird: Bei Vorwärtssprüngen werden ganze Befehlsgruppen übersprungen. Das kann im ROBOprogy mit IF..ELSE..THEN gesteuert werden. Bei Rückwärtssprüngen werden ganze Befehlsgruppen nochmal durchlaufen. Wie oft, kann entweder fest vorgegeben werden oder von Signalen der Sensoren abhängen. Bei "programmierten Sprüngen" wird genau eines von vielen gleichberechtigten Programmen durchlaufen. Es gibt keine Sprünge (GOTO) an eine beliebige Programmzeile. *** Vorwärtssprünge (zweiseitige Auswahl): (Abfrage) { -- a } IF { a -- } (Befehle_1) ELSE { -- } (Befehle_2) THEN { -- } Hier wird zuerst in (Abfrage) ein Wert a ermittelt (Schalter offen oder zu?) und auf das Stack gelegt. IF nimmt dieses Resultat a vom Stack und entscheidet: Ist der Wert a ungleich Null, werden die (Befehle_1) durchlaufen, dann geht es sofort zu den Befehlen, die nach THEN folgen. Ist der Wert a=0, werden die (Befehle_2) erledigt. Dann geht es nach THEN weiter. Es gibt keine dritte Möglichkeit, aber jeder der beiden Gruppen kann leer sein. Wenn die gesamte Gruppe (Befehle_2) leer ist, kann auch ELSE weggelassen werden. Beispiel: 1in \ legt einen Wert auf das DatenStack IF 22 mmgo \ Motore laufen vorwärts ELSE -11 mmgo \ Motore laufen langsam rückwärts THEN *** 1. Art der Wiederholung: BEGIN { -- } (Befehle) (Abfrage) { -- a } UNTIL { a -- } Der einleitende Begriff "BEGIN" ist kein Befehl, der irgend etwas macht, er kennzeichnet lediglich den Zielpunkt des Rücksprunges. Zuerst werden die zu wiederholenden (Befehle) durchlaufen. Dann - am Ende (vor UNTIL) - wird entschieden, ob die Befehlsgruppe wiederholt wird. Die (Abfrage) muss ein Ergebnis a auf dem DatenStack ablegen. UNTIL nimmt diesen obersten Wert a vom Stack und entscheidet: 0 bewirkt einen Sprung zurück zu BEGIN. Die (Befehle) werden erneut abgearbeitet und die (Abfrage) liefert wieder ein Ergebnis. Jeder andere Wert beendet die Schleife und das Programm geht zu dem Befehl, der nach UNTIL folgt. Wichtig bei der UNTIL-Schleife ist, dass die (Befehle) mindestens einmal durchlaufen werden. Wenn das stört, muss der nachfolgend beschriebene WHILE-Konstruktion verwendet werden. Beispiel: 0 mmgo BEGIN 1in UNTIL 20 1MOT BEEP Zuerst werden die Motore ausgeschaltet. Dann wartet das Programm, bis die Stop-Taste (zwischen Eingang 1IN und 0 Volt gedrückt wird. Daraufhin wird die Wiederholung verlassen, Motor 1 fährt mit mittlerer Geschwindigkeit los und es piepst kurz. *** 2. Art der Wiederholung: BEGIN { -- } (Abfrage) { -- a } WHILE { a -- } (zu wiederholende Befehle) REPEAT { -- } Der einleitende Begriff "BEGIN" ist auch hier kein Befehl, der irgend etwas macht. Er kennzeichnet lediglich den Zielpunkt des Rücksprunges nach REPEAT. Zuerst kommt die (Abfrage), die ein Ergebnis a auf dem Stack hinterlegen muss. Dann nimmt WHILE den obersten Wert a vom Stack und entscheidet: a=0 bewirkt einen Sprung vorwärts zu dem Befehl, der nach REPEAT folgt. Die (Befehle) werden übersprungen und nie ausgeführt. Jeder andere Wert sorgt dafür, dass die (zu wiederholenden Befehle) durchlaufen werden. REPEAT bewirkt einen Rücksprung zu BEGIN. *** 3. Art der Wiederholung: Wenn gleich zu Beginn bekannt ist, wie oft die (Befehle) wiederholt werden müssen, kann man die Zählschleife verwenden. Endwert Startwert DO { a b -- } (Befehle) LOOP { -- } Hier holt DO zwei Werte a (=Endwert) und b (=Startwert) vom Stack und versteckt diese in zwei Bytes auf dem Return-Stack. Der Speicherplatz für den Startwert b heisst auch Zählvariable. DO ist auch eine Markierung, wohin der Rücksprung nach LOOP gehen soll. Dann werden die (Befehle) durchlaufen. LOOP erhöht die Zählvariable um 1 und prüft: Ist der Endwert weder erreicht noch überschritten, geht es zurück zu DO und die (Befehle) werden erneut durchlaufen. Andernfalls wird das ReturnStack wieder gesäubert und es geht weiter mit den Befehlen nach LOOP. I { -- c } kopiert den aktuellen Wert der Zählvariablen auf das Stack. Es gibt einen Notausgang aus dieser Schleifenkonstruktion: LEAVE { -- } setzt Endwert=Startwert. Deshalb wird beim folgenden LOOP die Schleife verlassen. LEAVE ist *kein* Sprungbefehl! Eine besondere Konstruktion ist: +LOOP { a -- } addiert nicht 1 zur Zählvariablen, sondern den Wert a, den es vom Stack holt. Ausserdem wird die Schleife nur dann beendet, wenn die aktualisierte Zählvariable GLEICH dem Endwert ist. Das kann wegen der Modulo 256-Zählweise zu Überraschungen führen. ******** Programmierte Sprünge (Sprungverteiler) ************ Das Programm eines Roboters wird meist übersichtlicher, wenn es nach der Art eines "Zustands-gesteuerten-Automaten" entworfen wird. Die Zustände (=Module) können etwa sein: P0) Startposition, alle Sensoren kalibriert? dann zu P1, sonst zu P7 P1) Warten auf den Startbefehl, dann zu P2 P2) Ziel suchen; gefunden? dann zu P3, sonst zu P4 P3) Fahren zum Ziel; erreicht? dann zu P5, sonst zu P6 P4) Richtung ändern, dann zu P2 P5) Fahne schwenken, dann zu P4 P6) Zeit abgelaufen? dann zu P4, sonst zu P3 P7) Alles aus; Stoptaste? dann zu P0 Die Nummer des aktuellen Zustandes steht in der Variablen AMODE. Der Wert bleibt gleich, bis er beispielsweise nach die Berührung eines Sensors geändert programmgemäß geändert wird. Dieses Ereignis ändert den "Zustand" des Roboters und er macht nun etwas anderes. Der grosse Vorteil dieser Methode: Änderungen in einem Zustands-Modul können keine Seiteneffekte auf andere Module haben. Wenn der Roboter falsch reagiert, kann man sehr genau eingrenzen, in welchem Modul der Fehler stecken muss. Und wenn es notwendig erscheint, kann man weitere Zustände ergänzen. AM! { a -- } nimmt den Wert a vom Stack und speichert ihn in AMODE als Index für den nächsten "programmierten Sprung" AMODEJUMP. Nach Reset hat der Index den Wert 0. Für a gelten folgende Einschränkungen: 1) a muss im Bereich 0...15 liegen. 2) a darf nicht größer sein als die Anzahl der Unterprogramme zwischen AMODEJUMP und JUMPEND AM@ { -- c } kopiert den aktuellen Inhalt der Variablen AMODE zum Stack. Das kann wichtig sein, wenn man in anderen Programmteilen wissen muss, in welchem Zustand sich der Roboter befindet. BM! ... DM! und BM@..DM@ arbeiten entsprechend. AMODEJUMP { -- } setzt das Programm bei dem Unterprogramm fort, das durch den Wert von AMODE adressiert wird. Wenn dieses beendet wird, springt das Programm zum Befehl nach JUMPEND. JUMPEND { -- } ist eigentlich kein Befehl, sondern das Zeichen für den Compiler, dass nun die Kette der Module endet und andere Befehle folgen. BMODEJUMP...DMODEJUMP arbeiten entsprechend mit BM..DM. Der Sprungverteiler ist oft Teil einer Endlosschleife. Vorher und nachher können andere Befehle folgen. Bei der Liste der Unterprogramme zwischen AMODEJUMP und JUMPEND kommt es nicht auf deren Namen an, sondern auf die Reihenfolge (Platzziffer nach AMODEJUMP). Im Folgenden wird die obige Skizze teilweise in Forth übersetzt. \ beliebige Befehle, beispielsweise Einschalten der Sensoren.... : P0 { -- } \ Startposition, alle Sensoren kalibriert? dann zu P1, sonst zu P7 ..... \ hier müssen die entsprechenden Befehle stehen ..... \ einer davon lautet: 1 AM!, ein anderer lautet: 7 AM! ; \ Ende des Unterprogramms P0 : P1 { -- } 3IN \ Starttaste abfragen IF 2 AM! \ Startbefehl, jetzt zu P2 THEN ; \ Ende des Unterprogramms P1 \ Anmerkung: Das Programm springt nicht sofort, erst nach AMODEJUMP \ Dieser Befehl kommt unten in der Endlosschleife vor. : P2 { -- } \ Ziel suchen; gefunden? dann zu P3, sonst zu P4 ..... \ hier müssen die entsprechenden Befehle stehen ..... \ einer davon lautet: 3 AM!, ein anderer lautet: 4 AM! 200 AT100! \ Zeitgeber 20 sec starten für P6 ; \ Ende des Unterprogramms P2 : P3 { -- } \ Fahren zum Ziel; erreicht? dann zu P5, sonst zu P6 ..... \ hier müssen die Fahr-Befehle stehen 4IN \ Frontsensor abfragen IF 5 AM! \ Ziel erreicht ELSE 6 AM! \ Ziel nicht erreicht THEN ; \ Ende des Unterprogramms P3 : P4 { -- } \ Richtung ändern, dann zu P2 ..... \ hier müssen die entsprechenden Befehle stehen ..... \ einer davon lautet: 2 AM! ; \ Ende des Unterprogramms P4 : P5 { -- } \ Fahne schwenken, dann zu P4 ..... \ hier müssen die entsprechenden Befehle stehen ..... \ Wenn genug Fahne geschwenkt wurde kommt: 4 AM! ; \ Ende des Unterprogramms P5 : P6 { -- } AT100@ 0= \ Zeit abgelaufen? dann zu P4, sonst zu P3 IF 4 AM! \ hat zu lange gedauert ELSE 3 AM! \ Zeit läuft noch THEN ; \ Ende des Unterprogramms P6 : P7 { -- } 5IN \ Stoptaste? dann zu P0 IF 0 AM! \ ...wurde betätigt ELSE \ AMODE bleibt auf 7 THEN ; \ Ende des Unterprogramms P7 0 AM! \ mit P0 starten BEGIN \ Start der Endlosschleife = Hauptprogramm \ Genau *eines* dieser sieben Programme wird nun erledigt AMODEJUMP P0 P1 P2 P3 P4 P5 P6 P7 JUMPEND \ hier ist Platz für weitere Befehle, die immer wieder zu \ erledigen sind. 0 UNTIL \ Ende der Endlosschleife Anmerkungen: Der Wert von AMODE kann jederzeit an beliebigen Stellen geändert werden, massgeblich ist der Wert, sobald AMODEJUMP erreicht wird. Enthält dann AMODE einen Wert, der entweder größer ist als die Anzahl der Funktionen zwischen AMODEJUMP und JUMPEND oder der größer ist als 15, erfolgt eine Fehlermeldung zum PC und der ROBOprogy macht Restart. ********** Variable ************** Es gibt zwei unterschiedliche Speicherarten für Variable: 8-Bit-Var mit jeweils 256 möglichen Adressen für Werte im Bereich -127..255. Beispiel: VAR tempo 16-Bit-Var für Werte im Bereich -32767..32767. Jede dVariable belegt zwei Bytes. Beispiel: DVAR zaehl1 Insgesamt gibt es 256 reservierte Bytes im MEGA32. Jedesmal, wenn man dann tempo schreibt, wird im Programm eine vom Compiler zugewiesene Adresse eingesetzt und man kann dann den Speicherinhalt (=ein Byte) mit c@ holen oder mit c! überschreiben. Durch das Vereinbaren einer Adresse wird kein Speicherinhalt verändert, insbesondere nicht auf 0 gesetzt. Es gibt keine Speicher- und Rechenbefehle für noch größere Zahlen. Eine indizierte Adressierung ist einfach möglich. Beispiel: Man will der Reihe nach 20 Messwerte speichern und anschliessend auswerten. Die Nummer des Messwertes speichert man in VAR index und die Messwerte liegen (willkürlich) ab BasisAdresse 140. Das folgende Programm speichert den neuesten Wert m und vergrößert den Index jedes Mal um 1: : speich { m -- f } index c@ \ Index holen 140 + \ BasisAdresse addieren c! \ Messwert speichern 1 index c+! \ Index um 1 erhöhen index c@ 20 = \ Maximalwert erreicht? if 255 \ ja, dann TRUE zurückgeben else 0 \ nein, dann FALSE zurückgeben then ; Wenn alle 20 Messwerte gespeichert sind, wird das mit dem "Returnwert" 255 signalisiert. Das folgende Programm liest die gespeicherten Werte aus: : ZEIGM { -- } 20 0 \ Endwert, Startwert DO cr i \ Zählvariable läuft von 0 bis einschließlich 19 140 + \ Basisadresse addieren c@ . \ Byte holen und am PC zeigen LOOP ;