Roboter programmieren mit Arduino (Teil 4: Streckenlängen)

 

 

Im letzten Tutorial haben wir uns die Grundlagen der Encodertechnik angeschaut, in diesem Tutorial werden wir dieses Wissen in die Praxis umsetzen. Eine der ersten -- scheinbar simplen Aufgaben -- ist das Fahren einer vorher festgelegten Streckenlänge, z. B.: "fahre eine Strecke von 20 cm". 

 

Wenn wir beide Motoren mit gleichem Spannungswert betreiben, muss das noch keine gerade Strecke ergeben, weil die reale Spannung veriieren kann und/oder die Drehzahl der Motoren selbst bei exakt gleicher Spannung nicht identisch sein muss. Aber wir haben ja unsere Encoder und wir wissen, dass wir damit bei einer Umdrehung exakt 24 Impulse zählen. Das Rad hat einen Durchmesser von 4 cm, was einem Umfang von 13,2 cm entspricht. Pro Impuls legen wir demnach 5,5 mm zurück. Für eine Strecke von 20 cm wären das ca. 36,36 Impulse. Da wir nur ganze Werte zählen können, nehmen wir 36 als Näherung. Auch wenn die Motoren nicht exakt gleich laufen, müsste sich das am Ende wieder ausgleichen, wenn wir die gleiche Pulszahl abfahren. 

 

Wir werden jetzt einfach ein kleines Programm schreiben, dass den Roboter mit gleichen Geschwindigkeitswerten vorwärts fahren lässt und die Encoder-Impulse zählt. Hat der rechte Encoder 36 Impulse erreicht, schaltet der rechte Motor ab, analog dazu verhält sich der linke. Das Programm dazu sieht folgendermaßen aus:

 

Aus Gründen der Übersichtlichkeit hier nur die loop()-Funktion. Alle restlichen Teile entsprechen dem Programm aus dem vorherigen Tutorial. Im Setup-Teil starten wir nachdem Initialisieren der Anschlüsse der seriellen Schnittstelle und der Interrupts den Motor mit einer Geschwindigkeit von 180 vorwärts. 

 

Encoder_Prog1_Setup

 

Die jeweiligen Interrupt-Service-Routinen zählen die Impulse der Encoder wie im vorigen Tutorial erläutert. In der loop()-Funktion erfolgt jetzt die eigentliche Auswertung der Zählimpulse:

 

Encoder_Prog1

 

Hat der Encoder den Zählerstand 36 überschritten, wird der jeweilige Motor abgeschaltet, das geschieht für links und rechts unabhängig. Außerdem wird beim ersten Abschalten noch eine Bremsleuchte eingeschaltet. Das die eine gewisse Berechtigung hat, werden wir später noch sehen. Außerdem geben wir noch den Zählerstand über die serielle Schnittstelle aus. Das bedeutet natürlich, dass das USB-Kabel angesteckt bleiben muss (es sei denn, Sie arbeiten mit einem XBee-Modul), aber bei 20 cm Fahrstrecke sollte das kein Problem sein.  

Hinweis: Es kommt immer mal vor, dass man sich mit der Zuordnung der Motoren und Encoder vertut. Dann tritt das folgende Phänomen auf: Der rechte Encoder erreicht z. B. zuerst seinen Zählerstand von 36 schaltet nun aber nicht den rechten, sondern den linken Motor ab. Da der linke Motor jetzt aber nicht mehr weiterläuft, erreicht er auch nie den Wert 36, um seinerseits den rechten Motor auszuschalten. Falls bei Ihnen also immer ein Motor weiterläuft, sollten Sie unbedingt die Zuordnung prüfen. 

Nun zum Test: wir bringen den RoverROM-B3000 in seine Startposition und drücken die Reset-Taste. 

 

IMG_3240

 

Er fährt ein Stück (nicht allzu gerade), hält dann an und dreht sich dabei noch mal kurz nach links:

 

IMG_3241

 

Die Forderung ist nicht wirklich erfüllt, er sollte ungefähr bei 20 cm stehen bleiben, erreicht aber (zumindest mit dem rechten Rad fast 28,5 cm. Einschließlich der Kurve, die er auch noch fährt müsste das einen Encoderwert von weit über 50 geben. Aber genau dazu haben wir unsere Ausgabe auf der seriellen Schnittstelle und tatsächlich, es sind links 46 und rechts 56:

 

Encoder_Stand_1

 

Der Wert in der Ausgabe verändert sich logischerweise nach dem Stillstand nicht mehr und wird deshalb immer wieder neu ausgegeben, aber der Endstand liegt deutlich zu hoch. Einen ersten Anhaltspunkt liefert uns das Bremslicht, die LED leuchtet ja nicht beim Stillstand, sondern genau ab einem Zählerstand von 36. Es lässt sich leider hier per Video nicht zeigen, aber wenn Sie es im Experiment nachvollziehen, können Sie deutlich sehen, dass die LED genau zu leuchten beginnt, wenn das Rad des RoverROM-B3000 die 20-cm-Marke erreicht. In diesem Moment werden auch die Motoren ausgeschaltet. Es scheint sich also tatsächlich um einen Bremsweg zu handeln, der durch das Gewicht des RoverROMs ausgelöst wird. Wir sind mit einem recht hohen Geschwindigkeitswert von 180 losgefahren, bei einem geringeren Wert müsste der Effekt des Nachlaufens entsprechend geringer ausfallen, falls unsere Vermutung mit dem Bremsweg richtig ist.

Wir ändern den Wert auf 70 und tatsächlich bleibt der RoverROM-B3000 jetzt bei einem Wert von knapp über 20 cm stehen. Die Ausgabe zeigt nun einen Endstand von 36 auf dem linken und 39 auf dem rechten Encoder. Der Roboter bleibt wieder ein wenig nach links ausgerichtet stehen. Die Encoderfunktion scheint auf jeden Fall richtig zu funktionieren. Aber unsere Forderung ist noch nicht erfüllt, denn wir wollen auch bei einer höheren Geschwindigkeit möglichst exakt an der vorgegebenen Position zum Stehen kommen. Um das zu erreichen, könnte uns die folgende Strategien einfallen:

Wir fahren zunächst mit einer recht hohen Geschwindigkeit vorwärts, zählen aufwärts und vergleichen den Sollwert mit dem Istwert. Ist der Istwert größer als der Sollwert (20-cm-Marke erreicht) ändern wir die Drehrichtung des entsprechenden Motors, fahren langsam zurück und zählen rückwärts. Ist der Istwert wieder kleiner, ändern wir wieder die Drehrichtung und fahren langsam vorwärts. Sind beide Werte gleich, stoppt der Motor. Dazu muss der Controller aber wissen, in welche Richtung der RoverROM gerade fährt. Wir könnten dazu eine Variable vom Typ "boolean" nutzen, die z. B. den Zutand 1 (vorwärts) und 0 (rückwärts) annimmt. Wir haben es aber genau genommen mit 3 unterschiedlichen Zuständen zu tun: 

0 = Vorwärts mit voller Geschwindigkeit
1 = rückwärts mit geringer Geschwindigkeit (um sich von hinten dem Endpunkt anzunähern)
2 = vorwärts mti geringer Geschwindigkeit (um sich von vorn dem Endpunkt anzunähern) 

Prüfen wir jetzt, ob es auch genauso funktioniert. Zunächst deklarieren wir 2 Variablen vom Typ "byte":

byte dirL, dirR;
dirL = 0;
dirR = 0; 

Dann müssen wir die Interrupt-Service-Routinen anpassen, denn dort wird unsere Zählvariable geändert:

 

ISR_2

 

In den Zuständen 0 und 2 zählen wir vorwärts, im Zustand 1 werden die Impulse vom Wert abgezogen. 


Hinweis: Sparen Sie bei logischen Vergleichen nicht mit Klammern, sonst kann es ja nach Compiler zu schwer nachvollziehbaren Fehlern kommen. In diesem Fall werden zunächst die beiden Vergleiche "dirR == 0" und "dirR == 2" durchgeführt und dann die Ergebnisse mit ODER verknüpft. Und noch etwas: einer der häufigsten und schwer zu findenden Fehler in C ist das Verwechseln des Zuweisungsoperators "=" und des Vergleichoperators "==". Würden Sie in einem Vergleich fälschlicherweise abfragen:

if (x = 1) ... dann befehl

würde der Befehl immer ausgeführt, egal welchen Wert x zunächst hatte. Es handelt sich nämlich nicht um einen Vergleich, sondern um eine Zuweistung. Der Variablen x wird die 1 zugewiesen und steht dann in der Klammer. Das Ergebnis wäre

if (1) ... dann befehl 

und damit eine Endlosschleife. Analog dazu würde "befehl" bei der  falschen Abfrage...

if (x =0)  ... dann befehl

nie ausgeführt, weil nach der Zuweisung 0 in der Klammer stünde und als Bedingung für die if-Abfrage immer als "falsch" gewertet würde.


 

Kommen wir zur eigentlichen Auswertung in unserer loop()-Funktion. Die fällt dieses Mal schon ein wenig umfangreicher aus:

 

loop_2

 

Wir haben insgesamt 3 Zustände, auf die wir reagieren und das jeweils für den rechten und den linken Encoder, also 6 if-Abfragen. Die obersten beiden Abfragen beziehen sich darauf, dass der Endstand des Zählers erreicht ist. In diesem Fall schalten wir den Motor und das Bremslicht aus. Danach folgt die Reaktion auf das Überschreiten des Zählers (iRCount > 36). Wir schalten zunächst das Bremslicht ein, weil wir uns ja im Bremszyklus befinden. Danach reduzieren wir die Geschwindigkeit auf einen Wert von 70 und schalten die Motorrichtung auf "rückwärts". 

Falls der Endstand unterschritten wird und der vorherige Zustand "1" ist, heißt das: wir befinden uns immer noch in der Bremsphase, also schalten wir das Bremslicht ein, die Geschwindigkeit müssten wir eigentlich nicht ändern, aber wir fügen den Befehl hier ein, um evtl. später mit anderen Werten zu experimentieren. Schließlich kehren wir abermals die Drehrichtung um.

Wir laden jetzt das Programm  in unseren RoverROM-B3000 und schauen uns das Ergebnis an. 

 

Warum es trotzdem nicht funktioniert

Es sieht zwar etwas besser aus als vorher aber immer noch nicht so wie wir es geplant hatten. Sie können übrigens noch einen "lustigen" Nebeneffekt erkennen. Wenn Sie nämlich nach dem Stillstand versuchen, den RoverROM zu bewegen, werden Sie eine heftige Gegenwehr registrieren.

Aber warum funktioniert es immer noch nicht? Dieses Mal liegt es weniger daran, dass sich die Theorie von der Praxis unterscheidet, vielmehr sind wir einem Denkfehler aufgesessen. Wenn Sie den bemerkt haben, dann können Sie sich schon mal ordentlich auf die Schulter klopfen. 

In dem Moment, wo wir den Wert 36 überschritten haben, drehen wir programmtechnisch zwar die Motorrichtung um und ziehen jetzt die Impulse von unserem Zählerstand ab. Allerdings läuft der Motor real noch gar nicht rückwärts. Auch jetzt drückt die Massenträgheit den RoverROM noch einen kurzen Moment nach vorn, trotzdem werden die Impulse heruntergezählt. Er kommt zwar durch die Bremswirkung schneller zum Stehen, aber er fährt nicht exakt die Position 36 an. Würden wir uns den Stand allerdings über die serielle Schnittstelle ausgeben lassen, würden wir die 36 als Ergebnis erhalten. So kann es also auch nicht funktionieren.

Damit das Programm nach dieser Strategie wirklich sauber funktoniert, müssten wir den tatsächlichen Stillstand feststellen können. Das scheint zunächst nicht sonderlich schwierig, denn der Stillstand ist erreicht, wenn der Encoder keine Impulse mehr abgibt. Aber wie erkennen wir diesen Zeitpunkt? Erstmal gar nicht. Wir erkennen zwar durch einen Interrupt, wenn ein Impuls erzeugt wird, aber im umgekehrten Fall ist das nicht so einfach. Wir müssten eine exakt definierte Zeit warten und wenn in dieser Zeit kein Impuls auftritt, dann definieren wir den Roboter als stehend. Aber diese Zeit sicher zu bestimmen ist nur schwer möglich, denn wir wissen nicht genau, wie lange der Roboter tatsächlich still steht bevor er in die andere Richtung wieder losfährt und diese Zeit kann in der Praxis auch sehr unterschiedlich ausfallen.

Aber im Grunde ist das auch nicht notwendig, denn in der Praxis geht es um 2 unterschiedliche Forderungen:

Notbremsung: Nach dem Eintreffen eines Ereignisse wollen wir möglichst schnell halten. Das haben wir mit dem obigen Programm schon recht gut erreicht. 

Punktlandung: Wir wollen möglichst exakt an einem vorgegebenen Punkt anhalten. 

 

Realisierung einer Punktlandung

Wir haben schon gesehen, dass der RoverROM-B3000 bei geringen Geschwindigkeiten den Endpunkt recht genau trifft. Wenn wir eine genaue Strecke vorgeben, dann wissen wir auch wann dieser Endpunkt erreicht ist und wir können die Geschwindigkeit rechtzeitig reduzieren. Aber wir haben auch gesehen, dass es selbst bei einem sehr geringen Geschwindigkeitswert von 70  noch zu einer Überschreitung kommen kann. Für eine optimale Annäherung müssen wir 4 Zustände definieren: 

  1. Normale Fahrt mit beliebiger Geschwindigkeit (grün)
  2. Abbremsphase kurz vor Erreichen der Zielmarke (orange)
  3. Schleichphae zum exakten Annähern (rot)
  4. Stillstand nach Erreichen der Endmark (violett)

 

Zustandsmodell_A

 

Die Phasen 1 und 4 brauchen wir programmtechnisch nicht mehr zu erläutern. An die Zustände 2 und 3 müssen wir uns experimentiell annähern. Dabei können wir unseren ersten Versuch als Anhaltspunkt nehmen. Bei einem Geschwindigkeitswert von 180 wurde der Zielpunkt schon um 20 Punkte überschritten. Damit wir den zeitraubenden Zustand 3 möglichst kurz halten können, sollten wir also die Länge von Phase 2 möglichst dynamisch in Abhängigkeit von der eingestellten Geschwindigkeit konfigurieren.

 

Möglicher Ansatz:

Nehmen wir an, dass bei einer maximalen Geschwindigkeit von 255 für die Abbremsphase eine Strecke von 20 Impulsen notwendig ist, bei der minimalen Geschwindigkeit von 60 bis 70 kann diese Phase entfallen. Nutzen wir die Formel:

d2R = (V0 - 70)/8 (V0 ist die Anfangsgeschwindigkeit)

dann erhalten wir für eine Geschwindigkeit von 255 einen Wert von 23 und für einen Anfangswert von 70 einen Wert von 0. Falls das nicht klappt, können wir einfach den Teilungsfaktor (8) erhöhen oder erniedrigen und spreizen oder stauchen damit die Länge der orangenen Strecke. 

Interessanter ist aber noch der rote Bereich. Bei d3R setzen wir die Geschwindigkeit auf 0, lassen den Motor dann kurz anlaufen und prüfen, ob die Pulszahl erreicht ist. Um tatsächlich eine Punktlandung zu erreichen, müsste es aber möglich sein, den Roboter konkret um 1 Impuls nach vorn zu fahren. Und ob das funktioniert, werden wir erstmal ein kleines Testprogramm schreiben und in den Roboter laden:

 

One_Step_Forward

 

Die setup()-Funktion und die beiden Interrupt-Service-Routinen sind mittlerweile selbsterklärend. 

 

One_Step_Forward_2

 

Auch der loop()-Teil ist sehr einfach gehalten. Nachdem die Motoren im setup-Teil mit einer Geschwindigkeit von 180 gestartet werden, werden sie beim Erreichen des Zählerstandes 1 sofort wieder ausgeschaltet. Wir wisssen (oder vermuten) mittlerweile, dass der Roboter einen "Sprung" nach vorn macht und anhält. Wichtig: Wie viele Impulse "springt" er? Und genau das wird uns über die Variablen iRCount und iLCount mitgeteilt. Bei einem Geschwindigkeitswert von 180 erreichen wir das folgende Ergebnis:

 

One_Step_Forw_Result180

 

Testen wir das Ganze noch bei Werten von 255:

 

One_Step_Forw_Result255

 

und 70:  

 

One_Step_Forw_Result70

 

um zu sehen, wie weit die Werte auseinanderliegen. Das sieht schon recht gut aus. Bei einem Wert von 70 können wir tatsächlich einzelne Impulse ansteuern. Vorausgesetzt allerdings, das auch die Pausen dazwischen groß genug sind.

Wir schreiben jetzt ein Programm für diese letzte Phase der Annäherung:

 

Kriechstrecke_1

 

Wir definieren noch 2 zusäztliche Variablen iEnd vom Typ Integer und eine boolesche Variable isrFlag, die uns signalisiert, ob ein Interrupt aufgetreten ist. Am Ende der setup()-Funktion initialisieren wir die beiden Variablen noch. 

 

Kriechstrecke_2

 

Die Interrrupt-Service-Routinen sehen auch ein wenig anders aus, weil wir dort noch das Flag setzen. In der loop-Funktion fragen wir nun diese Flag ab. Das heißt jedes Mal, wenn ein Interrupt (egal welcher) auftritt, halten wir zunächst beide Motoren an und warten 50 ms. Danach setzen wir das Flag wieder zurück. Nun fragen wir ab, ob die Zähler kleiner sind als der Endstand iEnd. Ist das der Fehler, schalten wir die Motoren wieder ein und geben über die serielle Schnittstelle die Werte wie gehabt aus. Wenn wir den RoverROM-B3000 mit diesem Programm betreiben, sieht es folgendermaßen aus:

 

Video_Bild

 

Das ruckelt noch ein wenig, aber experimentieren Sie doch mit den delay- und den Geschwindigkeitswerten. Ein Blick auf die Ausgabe der seriellen Schnittstelle zeigt: 

 

Kriechstrecke_Result

 

Diese Mal hat es geklappt und wir erreichen immer genau den Endwert. Die Kombination ist nun recht simpel. Der RoverROM-B3000 fährt bis zum Erreichen des d2R-Punktes und geht dann in die beschriebene Kriechfahrt über. Den Wert 20 passen Sie natürlich noch für Ihre Anwendung an.

 

Sie sehen, dass schon das genau Abfahren einer bestimmten Strecke keine ganz simple Angelegenheit ist. Im nächsten Tutorial werden wir dann eine Strategie entwickelt, um möglichst gerade oder auch gewollte Radien zu fahren.

 

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