Roboter programmieren mit Arduino (Teil 5: Geschwindigkeit, Spurgenauigkeit, Bögen, Drehungen)

 

Im letzten Tutorial haben wir Strategien entwickelt, um den RoverROM-B3000 möglichst exakt einen bestimmten Streckenabschnitt fahren zu lassen. Was allerdings noch zu wünschen übrig ließ, war seine Spurgenauigkeit, also die Fähigkeit, wirklich geradeaus zu fahren. Technisch gesehen bedeutet das, dass sich beide Räder exakt gleich schnell drehen, also die Anzahl der Encoder-Impulse jedes Rads pro Zeiteinheit gleich sind. 

 

Ein sehr einfacher regelungstechnischer Ansatz könnte wie folgt aussehen:

  1. Wir zählen die Impulse pro Rad für eine bestimmte Zeit
  2. Wir vergleichen die Werte (der schnellere Motor wird einen höheren Wert aufweisen)
  3. Wir reduzieren die Geschwindigkeit des schnelleren Motors um den Wert "1" und beschleunigen den langsamen um den gleichen Wert

Als Programmfragment  sähe das folgendermaßen aus (wir betrachten nur die loop-Schleife):

 

Geradeaus_1

 

Zunächst wird in einer 1. Abfrage geprüft, ob es links mehr Impulse gibt als rechts (1. Zeile) und wenn das so ist, wird die rechte Geschwindigkeit speedR um 1 erhöht und speedL um eins erniedrigt. Die anderen if-Abfragen begrenzen die Werte auf maximal 255 und minimal 60. In der 2. Abfrage finden wir die Anweisungen für den Fall, dass der rechte Encoder mehr Impulse gezählt hat. Am Schluss setzen wir mit den beiden analogWrite()-Befehlen die neuen Geschwindigkeiten. 

Wenn wir das Programm übersetzen und in den RoverROM-B3000 laden, ist das Ergebnis schon gar nicht mal so schlecht. Auf jeden Fall können wir das Nachregeln deutlich erkennen, denn der RoverROM-B3000 fährt praktisch in einer zitternden Schlangenlinie. Doch wie kann das kommen, wo wir doch nur um den Wert 1 erniedrigen oder erhöhen? Dieses Phänomen basiert auf der Tatsache, dass die Anzahl der loop-Schleifen im Verhältnis zum Auftreten der Impulse wesentlicher größer ist.

 

Beispiel: Nehmen wir an, wir hätten rechts 310 Impulse gezählt und links 309. Nun wird die loop()-Funktion durchlaufen und die 2. Abfrage erweist sich als wahr (iRCount  > iLCount). Wir setzen speedL um einen Wert hoch und speedR um eins runter. Und bis ein neuer Interrupt auftritt, führen wir die loop()-Funktion noch einige Male durch. Jetzt ist aber der Wert des rechten Encoders mit Sicherheit kleiner als der des linken und das Spiel beginnt von vorn, nur dieses Mal in die andere Richtung. 

 

Dieses Fehlverhalten können wir dadurch umgehen, dass wir wieder einmal mit einem Flag arbeiten, dass uns das Auftreten eines neuen Interrupts signalisiert. Dabei müssen wir nicht zwischen links oder rechts unterscheiden. Wichtig ist lediglich, ob ein Interrupt aufgetreten ist und nur dann führen wir die Abfrage in der loop-Funktion durch und korrigieren die Geschwindigkeit. 

 

Die Interrupt-Service-Routinen sehen nun folgendermaßen aus:

 

Geradeaus_2

 

Und hier ist das Hauptprogramm:

 

Geradeaus_3

 

Wir ändern den Wert nicht mehr starr um 1 oder -1, sondern geben einen Differenzwert "speedDiff" an, den wir zu Beginn des Programm mit einem #define-Statement festlegen. Beginnen wir mit einem Wert von 1 beginnt der Roboter mit einem leichten Taumeln, das ständig stärker wird. Bei einem Wert von 10 fährt er Kreise, die langsam gerader werden. Einen sehr guten Effekt erhalte ich bei einem Wert von 5. Hier muss man einfach herum experimentieren und dazu ist ein Experimentier-Roboter ja nun mal da ;o)

Der Lerneffekt dabei ist ein wachsendes Verständnis für regelungstechnische Vorgänge. Theoretisch ist das alles ganz simpel: "Stimmt die gewünschte Größe nicht, reduziere oder erhöhe ich sie einfach!" Doch in der Praxis kommen sehr viele Faktoren hinzu, die eine Regelschleife schnell ins Schleudern bringen. Genau wie ein Auto, dass von der Fahrbahn abkommt, weil Sie immer wieder zu heftig gegen lenken.  

 

Wir messen die Zeit anstelle der Impulse

 

Bisher haben wir nur die einzelnen Impulse gezählt. Da allerdings pro Umdrehung nur 24 Impulse gezählt werden, ist die Auflösung recht grob. Ein Unterschied von einem Impuls mehr oder weniger hat schon einen recht großen Einfluss auf die Geradlinigkeit des RoverROM-B3000 und wir können erst dann korrigierend eingreifen, wenn es eine Differenz von mindestens einem Zähler gibt. Ganz anders wäre dies, wenn wir die Zeit zwischen 2 Zählimpulsen messen. Dieses Vorgehen hat noch einen weiteren Vorteil: Wir können die Geschwindigkeit wie bei einem Tempomat konstant halten. Beim Zählen der Impulse konnten wir lediglich sagen, ob beide Räder annähernd gleich schnell laufen.

Um die Geschwindigkeit messen zu können, brauchen wir eine Stoppuhr. Wenn wir den AVR-Controller ohne Arduino programmieren, würden wir diese Funktion direkt über die Timer-Programmierung erledigen, aber wir arbeiten ja mit Arduino-Unterstützung und da gibt es eine nützliche Funktion, die sich "micros()" nennt. Sie liefert uns als Rückgabewert die Zeit in Mikrosekunden seit dem Programmstart. Speichern wir die Zeit beim Auftreffen eines Interrupts und dann wieder beim nächsten Auftreffen und subtrahieren beide Werte, so bekommen wir die Zeit zwischen 2 Impulsen in Mikrosekunden.

Schreiben wir erst einmal ohne weitere Auswertung ein kleines Programm, dass diese Aufgabe erledigt, die Impulse zählt und uns die Differenz zum Vorgänger-Impuls ausgibt:

 

Geschwindigkeit_1

 

Wir brauchen zunächst 4 Variablen vom Typ unsigned long, in denen wir die Zeitwerte speichern. Dann die üblichen Zähler für die Impulse, die wir dieses Mal nur für die Zuordnung der Ausgaben einsetzen. Dann müssen wir noch die Zeitdifferenzen speichern, dafür deklarieren wir zwei Integer-Variablen und dann brauchen wir noch 2 Flags, die uns sagen, ob ein Interrupt ausgelöst wurde. Dieses Mal getrennt für links und rechts. 

 

Geschwindigkeit_2

 

Im setup()-Teil sind im Grunde nur die beiden micros()-Aufrufe neu, mit denen wir die anfänglichen Werte in den Vorgängervariablen speichern. Nun zum Hauptteil:

 

Geschwindigkeit_3

 

Die Interrupt-Service-Routinen erklären sich von selbst. Wird ein Interrupt erzeugt, wird das Flag gesetzt und beim Durchlauf der loop()-Funktion wird entsprechend in den jeweiligen if-Block (oder auch beide) verzweigt. Dort wird zunächst der aktuelle Zeitwert gespeichert und die Differenz berechnet. Danach wird der aktuelle Wert als Vorgänger gespeichert und das Ergebnis auf der seriellen Schnittstelle ausgegeben. Anschließend wird das Flag zurückgesetzt. Laden wir das Programm in den RoverROM-B3000 und lassen es laufen. Dazu können Sie den Roboter auf eine Unterlage stellen, bei der sich die Räder frei drehen können. Ein kleiner runder Behälter mit einem Durchmesser von ca. 6 cm oder weniger ist ideal. 

Die Ausgabe sieht folgendermaßen aus (alle Werte des rechten Encoders sind der besseren Übersichtlichkeit wegen gelb markiert):

 

Geschwindigkeit_4

 

Wir sehen, dass auf der rechten Seite  z. B. zwischen Impuls 3.124 und 3.125 genau 14.336 µs vergehen, zwischen Impuls 3.126 und 3.125 aber nur 10.824 µs. Es wechseln sich immer Werte von ca. 14.000 und ca. 11.000 µs ab. Beim linken Rad ist es ähnlich, da ist der Unterschied sogar noch etwas größer. Wenn wir uns den Aufbau der Rad-Encoder in Erinnerung rufen, dann kommen wir auch schnell zu einer Erklärung. Die Zapfen und die Lücken sind nicht identisch groß. Dieses Problem müsste sich allerdings dadurch lösen lassen, dass wir nicht bei jeder Flanke einen Interrupt auslösen, sondern nur bei der steigenden (oder der fallenden). Wir ändern also die beiden Befehle zum Einrichten der Interrupts wie folgt ab:

Alt:

Geschwindigkeit_5

Neu:

Geschwindigkeit_6

 

Nachdem Kompilieren und Hochladen in den RoverROM-B3000 erhalten wir nun das folgende Ergebnis:

 

Geschwindigkeit_7

 

Die Werte liegen doch jetzt sehr gut beieiander und variieren rechts zwischen ca. 24,2 und 26,4 ms und links sind es 25,6 und 27,0 ms. Die Zeiten sind rechts immer kürzer als links, das rechte Rad dreht sich also etwas schneller, woraus auch die höhere Impulsrate am rechten Rad resultiert.

Um beide Räder mit einer vorgebenen Geschwindigkeit gleich schnell drehen zu lassen, müssen die Zeiten zwischen 2 Impulsen möglichst gleich lang und dem Vorgabewert entsprechen. Dazu müssen wir aber zunächst den Geschwindigkeitswert (der ja zwischen 50 bis 60 und 255 liegt) in eine Zeit umrechnen. Das ändert sich aber sehr stark in Abhängigkeit von der Batteriespannung. Aus diesem Grund messen wir einfach nach 10 Impulsen die Zeit am rechten Encoder und nehmen diesen Wert als vorgebene Geschwindigkeit an. Würden wir früher messen, ergibt sich ein langsamerer Wert, weil sich der Roboter noch in der Anfahrphase befindet. 

Das Programm ist ein wenig länger, aber keine Angst, der größte Teil sind Ausgaben über die serielle Schnittstelle, die wir für Analysezwecke brauchen. Betrachten wir die loop()-Funktion lediglich für das rechte Rad (für links sieht es identisch aus):

 

Geschwindigkeit_8

 

Wenn tun nur etwas, wenn ein Interrupt ausgelöst wurde, deshalb arbeiten wir wieder mit einem Flag und zwar für rechts und links getrennt. Wenn rechts ein Impuls ausgelöst wurde, wird zunächst die Zeit gemessen und daraus mit der Vorgängerzeit die Zeitdauer (diffR) zwischen 2 Impulsen ermittelt. Fall der 10. Impuls erzeugt wurde, wird diese Zeit als Richtwert (setTime) festgelegt. Ab dem 11. Impuls kontrollieren bzw. regeln wir dann die Geschwindigkeit. Falls die aktuelle Zeitspanne größer ist als der Richtwert, erhöhen wir die Geschwindigkeit um 1 und geben entsprechend eine Meldung aus, damit wir alle Zeitwerte und die aktuelle Geschwindigkeit überprüfen können. Wir laden das Programm in den RoverROM-B3000 und schauen uns auf dem seriellen Monitor das Ergebnis an: 

 

Geschwindigkeit_9

 

Wir sehen bei jedem Impuls, ob das jeweilge Rad zu schnell oder zu langsam ist, wie lang die Zeit zwischen den beiden letzten Impulsen war und wie lang sie hätte sein sollen und wie der geändert Geschwindigkeitswert aussieht. 

Die 1. Zeile sagt uns z. B.: Das rechte Rad ist schneller als gewollt, die Zeit liegt mit 23,696 ms knapp unter dem Richtwert von 23,732 ms und wir erniedrigen die Geschwindigkeit auf 61. In Zeile 3 ist jetzt aber das rechte Rad wieder zu langsam und zwar mit 23,768 ms zum Richtwert von 23,732 ms, deshalb wird die Geschwindigkeit jetzt wieder um einen Wert erhöht. Wir können sehr schön sehen, dass eine Änderung der Gschwindigkeit um 1 Punkt schon recht gravierende Auswirkungen hat. Um die Routine zu verbessern könnten wir hier natürlich differenziert eingreifen. Es macht wenig Sinn, die Geschwindigkeit zu erhöhen, nur weil die Zeitspanne 1 µs kürzer oder länger ausfällt. Auch hier bringt es wieder sehr viel mit bestimmten Intervallen zu experimentieren. Im Verhältnis zum einfachen Zählen der Impulse haben wir jetzt gut auflösende Werte, um eine ausgeklügelte Regelungsstrategie anzuwenden. 

Wir sehen auch deutlich, das sich bei gleichen Geschwindigkeitswerten sehr unterschiedliche Zeitwerte (also tatsächliche Geschwindigkeiten ergeben). Der Geschwindigkeitswert entspricht der Spannung am Motor, der Zeitwert der realen Drehgeschwindigkeit. Die Werte liegen für den rechten und linken Motor bis zu 10 Punkte auseinander. 

Interessant ist aber auch, wie sich der Roboter verhält, wenn man ein Rad festhält. Das werde ich mal mit dem rechten Rad tun:

 

Geschwindigkeit_10

 

So lange ich das Rad festhalte, erscheinen nur Impulse (und folglich auch Ausgaben) für das linke Rad, das sich ja noch dreht. Ich lasse das Rad wieder los und der 1. Impuls rechts wird wieder erfasst (rosa Zeile). Der Zeitwert beträgt etwas über 3 Sekunden, genauso lang wie das Rad stand. Das ist natürlich zu langsam und der letzte Geschwindigkeitswert von 64 wird auf 65 erhöht. Interessant ist aber auch, wie schnell das Rad wieder auf Kurs ist. Ganze10 Impulse dauert es, bis das erste Mal wieder ein zu schneller Zeitwert ermittelt wird. Auch hier könnte man natürlich wieder dynamisch reagieren und bei einer so hohen Zeitdifferenz den Geschwindigkeitswert stärker erhöhen. Was wieder sehr deutlich wird: Wir müssen die Spannung stark überhöhen (bis 75) um Zeiten zu erreichen, die wir im Normabetrieb mit einem Wert von 61 bis 62 erreichen. 

Dieses Programm korrigiert keine Verluste durch zu geringe Geschwindigkeiten. Würde der Roboter z. B. an einem Hindernis kurz hängenbleiben, so würde er seine Richtung ändern, danach aber wieder mit gleich bleibender Geschwindigkeit geradeaus fahren. Aber das "Nachholen" von Strecken haben wir ja auch im letzten Tutorial durchgeführt, wenn beide Encoder die gleiche Pulszahl erreichten. 

 

Kreise und Kurven

 

Um Kreise, Kurven oder Bögen zu fahren, stellen wir lediglich für eine bestimmte Zeit (Kurve) oder auch dauerhaft (Kreise) unterschiedliche Geschwindigkeitswerte ein und korrigieren diese gegebenenfalls wie oben beschrieben. 

Der größte (gleichmäßige) Kreis ergibt sich bei einer Differenz von 1 Geschwindigkeitswert, der kleinste Radius wird bei einem stehenden Rad erreicht. Noch kleinere Werte werden als Drehung bezeichnet. 

 

Drehungen

Bei einer Drehung rotiert der RoverRom um einen Mittelpunkt, der innerhalb seiner Grundfläche liegt. Bei einem 2-rädrigen Roboter ist das eine recht einfache Sache, ein Rad dreht nach vorn und eins nach hinten. Drehen beide Motoren mit gleicher Geschwindigkeit erreichen wir eine Drehung um den Mittelpunkt. Wir wollen aber exakte Winkelrotationen ausführen und da kommt uns wieder eine Funktion in den Sinn, die wir schon programmiert haben, nämlich die Kriechfahrt. Bei der Kriechfahrt "missbrauchen" wir die Motoren praktisch als Schrittmotoren. So können wir auch sehr langsame Geschwindigkeiten realisieren, die mit der Pulsweitenmodulation nicht möglich sind. 

Wir wollen jetzt eine Linksdrehung auf der Stelle um genau 90° ausführen. Wir wissen, dass die Spurweite des RoverROM 87,5 mm beträgt und damit ein Vollkreis genau 275 mm lang ist. Ein Viertelkreis wäre also nach einer Strecke von 68,75 mm erreicht und das würde wiederum bei einer Encoderauflösung von 5,625 mm etwas mehr als 12 Impulse ergeben. 

Das Programm dazu ist sehr einfach, denn wir ändern einfach nur bei einem Motor die Drehrichtung und weil wir eine Linksdrehung erreichen wollen, lassen wir den linken Motor rückwärts laufen. Das Programm dazu:

 

Drehung_Teil1

 

 

Der Setup-Teil und die Variablendeklarationen bedürfen keiner Erläuterungen mehr. Und auch die Interrupt-Service-Routinen und die Loop-Funktion bringen keine Neuigkeiten mit sich:

 

Drehung_Teil2

 

Es handelt sich um die schon besprochene Kriechfahrt, nur diesem Mal mit einem Motor vorwärts und dem anderen rückwärts. Nach jedem erkannten Impuls wird gestoppt, kurz gewartet und danach der Zähler ausgewertet. Ist der Endstand noch nicht erreicht, fährt der Motor noch einmal langsam los. 

In der Praxis sieht das schon ganz ordentlich aus:

 

Titelbild

 

Damit wären die Tutorials für den Encoder zunächst einmal abgeschlossen. Wir werden diese Beispiele aber laufend überarbeiten, erweitern und anpassen. Wenn Sie keine neuen Tutorials verpassen wollen, registrieren Sie sich unter dem Link, der unten im Hinweis angegeben ist. 

Im nächsten Tutorial schauen wir uns den Lichtsensor und die Beleuchtung etwas genauer an. 

 

Für weitere Informationen und Bestellungen des

RoverROM-B3000

einfach auf nachfolgenden Link klicken: Böcker RoverROM-B3000 Version 2

oder als Bausatz: Böcker RoverROM-B3000 Version 2 Bausatz

 

Sie wollen Arduino-Profi werden? Dann ist unser Video Mikrocontroller-Lehrgang MC1 genau das Richtige für Sie!

 

Falls Sie Fragen zum Thema haben, wenden Sie sich einfach an uns.

Wir helfen Ihnen gern weiter!

 

Ihr Team von Böcker Systemelektronik

 

Hinweis: Unser Tutorialangebot wird in unregelmäßigen Abständen erweitert. Wenn Sie sofort über eine Neuerscheinung informiert werden möchten, tragen Sie sich bitte hier in unsere Benachrichtigungsliste "Tutorials" ein. Sie können diesen unverbindlichen Service jederzeit in Ihrem Kundenkonto oder per E-Mail wieder abbestellen.  

 

 

Copyright © Böcker Systemelektronik