Projektpraktikum Künstliche Intelligenz und Roboterbau

Carl von Ossietzky Universität Oldenburg
Sommersemster 2003
Dozent Dr. Achim Kittel

Home

Erste Vorstellung
Theorie
Hardware
   - Baugruppen
Software
   - Entwicklung
   - Quellcode
Ergebnis
Das Team
Quellen und Links

Die Entwicklung unserer Software

Grob lassen sich die Entwicklungsschritte unseres Roboters in folgende Abschnitte unterteilen:

1. Der Prozessor gibt eine Rechteckspannung an die Steppercontroller aus und ermöglicht so einfache Fahrt
2. Der Roboter fährt eine bestimmte Anzahl von Schritten
3. Wir führen den Navigator ein
4. Der Roboter reagiert auf Kontakt
5. Beschleunigen und Abbremsen
6. Der Roboter reagiert mit Entfernungssensoren auf ein Hindernis
7. Suche eines IR-Feuers

1. Einfache Fahrt

Im ersten Abschnitt ging es darum, auf die Steppercontroller eine Rechteckspannung (als Clock-Signal) und die notwendigen Signale für die Motorfunktionen (Motor an, Drehrichtung, Step-Modus) auszugeben. Um ein möglichst gutes Fahrverhalten in Bezug auf Beschleunigen und Abbremsen zu erreichen, war es unser Ziel, eine in einem Frequenzbereich von 500-1000 Hz in möglichst kleinen Schritten veränderbare Schrittfrequenz zu erhalten.
Da unsere Planung vorsah, eine seitliche Abweichung des Roboters von der Fahrtstrecke durch die Sensoren von zwei optischen Maussensoren zu erkennen, und durch Abbremsen eines Motors zu korrigieren, war es notwendig, die beiden Motoren mit unabhängig variierbaren Frequenzen anzusteuern.

Eine der Möglichkeiten dies zu realisieren, bestand daraus, zwei der unabhängigen Counter des Microcontrollers zu nutzen. In diesem Fall würden die Counter mit der Frequenz des durch den Prescaler geteilten Prozessortaktes hochgezählt werden, und bei Erreichen eines definierten Wertes einen Interrupt auslösen. Auf den Interrupt je eines Counters würden dann jeweils der Zustand auf einem zugehörigen Pin geändert werden, und auf diesem Pin so eine Rechteckspannung erzeugt werden. Mit den beiden Countern hätten wir die Möglichkeit, beide Motoren mit unabhängigen Frequenzen anzusteuern. Das Problem dieses Ansatzes besteht darin, das der Prescaler den Prozessortakt nur durch einige feste Werte teilen kann (8, 64, 256, 1024), und es daher fraglich ist, ob wir den gewünschten Bereich der Ausgabefrequenz zusammen mit einer guten Variierbarkeit der Frequenz erzielen können. Dazu wäre es sinnvoll, den Counter mit einer möglichst hohen Frequenz zu erhöhen, (daraus folgt, daß in einem Takt der Motorfrequenz viele Takte der Counterfrequenz liegen, eine Änderung der Counterfrequenz also feine Änderungen der Motorfrequenz ermöglicht) bei der die Größe des Counters (8 Bit) aber dennoch ausreicht, um eine niedrige Anfahrfrequenz zu erhalten.
Counterfrequenz / 256 = 500Hz
Unser Problem war, das wir bei der Berechnung der Frequenz einen Fehler gemacht hatten, indem wir nicht beachteten, das wir nur die Frequenz berechneten, mit welcher der Pin umgeschrieben wird. Der Motor hingegen macht auf die ansteigende Flanke des Rechtecksignals hin einen Schritt, die Motorfrequenz ist also nur die Hälfte der berechneten Frequenz. Die Motorfrequenz läßt sich mit folgender Formel beschreiben:
Prozessortakt / Prescaler / Wert des 8-Bit-Registers / 2 = Motorfrequenz

Mit den fehlerhaften Überlegungen schien es uns nicht möglich, die Fahrfrequenz mit der geschilderten Methode auszugeben. Daher mußten wir noch eine zweite Idee entwickeln, die Frequenz zu erzeugen: Diese zweite Möglichkeit war, mit einem Counter zwei Zählregister zu erhöhen, deren Größe zu prüfen und bei einem bestimmten Wert die Ausgabepins zu ändern.
Dabei wäre es möglich gewesen, die geschilderten Problem zu umgehen, da wir zusätzlich zu den 8 Bit des Counters noch 8 Bit des Registers, und damit mehr Variationsmöglichkeiten zwischen Prescaler und Counter-Größe gehabt hätten. Das Problem dabei wäre natürlich ein erheblich größerer Rechenaufwand gewesen, insbesondere da die Aufgabe des Interrupts durch eine ständige Kontrolle der Register hätte ersetzt werden müssen.

Nach Erkennen des Fehlers beschränkten wir uns auf die Nutzung der 8 Bit Counter 0 und 2. Das Toggeln der Ausgabepins vereinfachte sich ebenfalls, da die Counter einen Modus besitzen, in dem sie bei Erreichen der oberen Schranke automatisch einen Pin toggeln. Damit fährt der Roboter nun, je nach Einstellung der Motordrehrichtung geradeaus oder er dreht sich um seinen Mittelpunkt.

2. Definierte Strecke

Im zweiten Abschnitt erweiterten wir das Programm um einen Zähler der gefahrenen Schritte, so das es möglich wurde, den Roboter eine bestimmte Strecke fahren zu lassen.

Dies lies sich einfach verwirklichen, indem wir den bisher ungenutzten Interrupt eines der Counter dazu verwendeten, die Distance-Routine aufzurufen, die ein 16 Bit Register erniedrigte (Durch zwei 8-Bit Register realisiert, sobald ein Register auf Null gezählt ist, wird das zweite erniedrigt, dann das erste wieder heruntergezählt). Dieses Register enthält die noch zu fahrende Anzahl an Schritten (aufgrund des oben geschilderten das Doppelte).
Wenn das obere Register Null erreicht, springen wir im Programm in die Stop-Routine, die in diesem Entwicklungstand ausschließlich die Motoren ausschaltet. Es muss also berücksichtigt werden, daß die letzten 256 Schritte nicht ausgeführt werden. kompensieren lässt sich dieser Fehler durch eine Erhöhung des oberen Registers um eins.

3. Der Navigator

Der Roboter ist nun in der Lage eine definierte Strecke zu fahren. Unsere Planung sah vor, das später ein zweiter Prozessor (Navigator) einen Weg berechnen und dem ersten in einfacher Form, eben in der Form von aufeinander folgenden Strecken mit bestimmter Länge übermitteln würde. Drehungen würden genau wie gerade Fahrt aus einer definierten Strecke, aber unterschiedlichen Drehrichtungen der Motoren bestehen.

Da der zweite Prozessor nicht vorhanden war, führten wir als Simulation die Navi-Routine in unserem Programm ein. Ihre Aufgabe besteht darin, in bestimmten Registern die Fahrtstrecke und Art (Drehung oder Gerade) zu setzen.
Sobald diese Werte vorhanden sind (was später der zweite Prozessor erledigen würde, daher die Input- Routine, die später die Kommunikation der Chips aufnehmen soll), führt das Programm die eigentliche Fahrroutine unabhängig vom Navigator (Routine/Prozessor) aus. Nach Durchführen des Fahrbefehls, das heißt erreichen der Stop-Routine, springt das Programm wieder in die Navi-Routine.
Um verschiedene Fahrbefehle in einer Reihenfolge durchzuführen, nutzen wir ein Register als Statusregister NA. Die Navi-Routine fragt die Bits dieses Register nacheinander ab. Ein gesetztes Bit in diesem Register führt dazu, daß die Navi-Routine in einen Teil springt, der einen bestimmten Fahrbefehl lädt. Wenn in diesem Teil das NA-Register wieder geändert wird, läßt sich erreichen, das nach einem bestimmten Fahrbefehl zwangsläufig ein definierter anderer durchgeführt wird. Als einfaches Beispiel fährt der Roboter nun eine Strecke, dreht um, fährt zurück, dreht um,.............

4. Erste Interaktion mit der Umwelt

In diesem Abschnitt geht es zum ersten Mal darum, den Roboter auf seine Umwelt reagieren zu lassen. Mechanisch erreichten wir dies durch zwei mit "Fühlern" versehenen Schaltern an der Frontseite. Diese Schalter schließen bei Kontakt der Fühler mit einem Hindernis.

Um eine möglichst schnelle und zugleich während der Fahrt rechenarme Verarbeitung zu erreichen, legten wir die Schalter auf die beiden externen Interrupts des Prozessors, welche die Routinen Left und Right aufrufen.
Dadurch ist es auch einfach möglich die beiden Schalter unabhängig zu betrachten, und so grob die Position des Hindernisses zu erkennen. Die Interrupts führen dazu, das die Motoren ausgeschaltet werden. Außerdem schreiben die Interrupt-Routinen in das NA-Register die Bits für Rückwärtsfahren, und je nachdem welcher Schalter ausgelöst hat, eine Drehung vom Hindernis weg. Am Ende der Routinen springt das Programm wieder in die Navi-Routine. Durch die Reihenfolge der NA-Registerabfragen im Hauptprogramm erreichen wir das der Roboter zuerst von dem Hindernis zurückfährt, dabei gleichzeitig das Bit im NA-Register für Rückwärts wieder löscht, und daraufhin von dem Hindernis wegdreht. Danach führt der Roboter wieder sein "typisches" Fahrverhalten (fahren, drehen,.....)aus.

5. Beschleunigung und Abbremsen

Bisher war der Roboter mit konstanter Geschwindigkeit gefahren. Darin lagen insofern Probleme, als das bei geringen Drehzahlen der Roboter zwar unter allen Bedingungen sicher angefahren ist, dabei aber einen hohen Stromverbrauch hatte, was sich auch durch eine deutliche Erwärmung der Steppercontroller zeigte. Bei höheren Geschwindigkeiten hingegen sank der Stromverbrauch, dafür blieb der Roboter insbesondere mit schwächeren Akkus bei Drehungen manchmal stehen.
Um sowohl das höhere Drehmoment der Motoren bei kleinen Geschwindigkeiten zum Anfahren als auch den geringeren Stromverbrauch mit höherer Geschwindigkeit zu nutzen, änderten wir unser Programm so, das der Roboter mit 500 Hz anfuhr und auf 1000 Hz beschleunigt. Wir erreichten dies, indem wir die Distance-Routine um folgende Operationen erweiterten:
a.) Wir zählen ein weiteres Register hoch, das so die Zahl der gefahrenen Schritte enthält. Solange dieses Register unter einer bestimmten Größe bleibt, verkleinern wir die obere Schranke der Counter.
b.) Wir ändern in unserem aktuellen Programm die Geschwindigkeit nur alle 128 Schritte, das heißt in dem Programmteil, in dem die Distance-Routine den oberen Teil des 16 Bit Registers ändert. Eine Änderung bei jedem Schritt verursacht zwar eine sehr eindrucksvolle Beschleunigung, der Roboter zeigt aber trotzdem das Verhalten in bestimmten Fahrsituationen stehen zu bleiben.
c.) Die Beschleunigung ist zusätzlich durch eine Abfrage abgesichert, welche die noch verbleibende Strecke überprüft und unter einem Betrag keine Beschleunigung mehr zulässt.

d.) Ebenfalls in der Distance-Routine erhält der Roboter eine Funktion, die den Roboter abbremst (Obere Schranke des Counters erhöht), sobald das Streckenregister unter einen Wert gefallen ist. Dabei wird gesichert, das die Frequenz nicht unterhalb von 500 Hz verringert wird, was Stehen bleiben und hohe Halteströme verursachen würde.

6. Abstandssensorik

Bisher konnte der Roboter Hindernisse erst bei Berührung erkennen. Als Erweiterung haben wir nun zwei Entfernungssensoren mit einem Messbereich von bis zu 80 cm auf der Vorderseite des Roboters angebracht. Die Sensoren geben eine der Entfernung umgekehrt proportionale Spannung aus, die wir über den AD-Wandler des Prozessors einlesen.
Ziel war es, Hindernisse auf diese Reichweite zu erkennen, abzubremsen (im Gegensatz zum Motorausschalten der Schalter) und ihnen auszuweichen.

Versuchsweise implementierten wir zuerst einen der Sensoren. Dazu wurde die Distance-Routine um den Start einer AD-Wandlung des anliegenden Signals erweitert. Damit erreichten wir, das bei jedem Schritt die Entfernung gemessen wird, also in einer idealen Auflösung, aber ohne unnötige Berechnungen. Bei Ende der Wandlung löst der AD-Wandler einen Interrupt aus. Die aufgerufene Scan-Routine verglich den Wert mit einem Maximalwert, und stoppte den Roboter noch einfach, wenn ein Hindernis in unter 30 cm Entfernung von dem Sensor erfasst wurde.
Anfangs hatten wir das Problem, das jeweils die erste Messung nach dem Anschalten (über Stromversorgung, nicht bei Reset) ein falsches Ergebnis (Hindernis vorhanden) lieferte. Wir konnten dies nur durch eine "Blind-Abfrage" ohne Verwertung des Ergebnisses am Anfang des Programms umgehen.

Als dieses Programm lief, versuchten wir auf zum Auslesen beider Sensoren überzugehen. Dazu wechselten wir beim Auslesen zwischen den Sensoren und prüften so abwechselnd, mit der selben Routine, die Abstände auf beiden Seiten des Roboters.
Um nun Informationen über die Lage des Hindernisses zu erhalten, hatten wir vorgesehen, nach einem Stop aufgrund der Entfernungssensoren, in einer weiteren Routine nacheinander beide Sensoren auszulesen und die Werte voneinander abzuziehen. Aufgrund des Vorzeichens des Ergebnisses konnte man dann erkennen, welcher Sensor die größere Spannung und damit die kleinere Entfernung zum Hindernis hatte. Damit wären wir in der Lage gewesen, im NA-Register die Bits für eine Drehung vom Hindernis weg zu setzen.
Die Implementierung sah vor, eine AD-Wandlung zu starten, in einer Schleife auf das Bit zu warten, welches das Ende der Wandlung anzeigt, den anderen Sensor ebenso auszulesen und die Werte zu vergleichen.
Anscheinend ist dies so aber nicht möglich. Der Roboter drehte bei Fahrversuchen unabhängig vom Hindernis immer in eine Richtung. Unsere einzige Erklärung für dieses Verhalten ist, das die AD-Wandlungen nicht in derartig kurzen Abständen voneinander möglich sind (was laut Datenblatt von Atmel möglich sein müsste ). Eventuell wird das Beendet-Bit vor Abschluss der Berechnung gesetzt, so das es zu Überschneidungen kommt (unsere übrigen Aufrufe des AD-Wandlers liegen einige tausend Prozessortakte voneinander getrennt).
Wir haben das Problem gelöst, indem wir in der Scan-Routine, welche die Entfernung überprüft, die Messwerte der Sensoren jeweils in ein eigenes Register schreiben. Falls der Roboter nun wegen eines Hindernisses stoppt, vergleicht die Stop-Routine diese Werte und entscheidet so, ob in dem NA-Register das Bit für eine Rechts- oder Linksdrehung gesetzt wird.
Außerdem haben wir in der Navi-Routine die Motorbefehle für eine Rechts- und Linksdrehung so geändert, das sich die Drehwinkel unterscheiden. Das ermöglicht dem Roboter auch aus Ecken wieder herauszufinden.

7. IR-Empfänger

Im letzten Abschnitt haben wir dem Roboter einen IR-Empfänger angebaut, mit dem Ziel einen entsprechenden Sender zu finden. Sender und Empfänger arbeiten mit einer modulierten IR-Strahlung, sind daher relativ unempfindlich gegenüber Störungen. Im Betrieb gibt der Sender eine Spannung von 4.8 Volt aus. Wenn er ein Signal empfängt, sinkt die Spannung auf 2,3 Volt.

Unsere Planung sah vor, den Empfänger mit einer Blende zu versehen und so eine Richtcharakteristik zu erhalten. Der Roboter sollte nun Suchmanöver fahren, bis er den Sender gefunden hatte, und auf ihn zufahren. Unsere Hoffnung Bezugs der Implementierung, das der Prozessor die 2,3 Volt als Low-Bit interpretieren würde, würde erfüllt.

Dadurch waren wir in der Lage, den Sender direkt digital an einem Pin einzulesen. Solange an diesem Pin ein High-Bit anlag, fuhr der Roboter ein Suchmuster in Form eines Rechteckes mit sich vergrößernden Seitenlängen. An den Ecken vollführte der Roboter eine Drehung um 450 Grad um so das gesamte Umfeld zu erfassen.
Wenn der Empfänger ein Signal erhielt, änderte sich die Spannung an dem Pin auf Low. Das Low-Bit verursachte einen Stop der aktuellen Fahrbefehle. Außerdem wurden im NA-Register das Bit für einen neuen Fahrmodus gesetzt, indem der Roboter nun geradeaus fuhr, und auf Meldungen der Sensoren (mechanisch und optisch) nicht mehr mit Ausweichen, sondern mit einem Stop reagierte. Dadurch näherte sich der Roboter an den Sender an und blieb dann stehen.

Hauptproblem in diesem Fall war, das der Sender nur über eine sehr geringe Leistung verfügt, das Signal daher erst bei Entfernungen unter ca. einem Meter erfasst wird.