Aufbau der Elektronik

Nachdem wir uns in einer der ersten Stunden den Aufbau eines Roboters überlegt haben, können wir uns jetzt den tatsächlichen Schaltplan anschauen und ihn mit unserem Überblicksschema vergleichen. Bislang fehlen noch die Sensoren!

Für die Bestückung auf unserem Steckboard eignet sich folgende Darstellung besser. Man kann hier zwar nicht mehr so gut sehen, wie alle Bauteile miteinander verbunden sind, aber dafür kann man sehen, wo sie auf dem Steckboard platziert werden:

Der Einfachheit halber sind die Anschlüsse der Powerbank und der Einschalter weggelassen.

Soundausgabe

Spätestens seit R2D2 ist klar, dass Roboter Geräusche machen können. Natürlich kann unserer das auch, wenn auch nur verschieden hohe Piep-Töne. Aber immerhin, damit kann man verschiedene Zustände anzeigen oder sogar ein kleines Liedchen spielen.

Für einen richtig kräftigen Lautsprecher hat unser Mikrocontroller nicht genügend Leistung, aber für einen kleinen Piezo-Pieper, wie er auch in Digitaluhren eingebaut ist, reicht es gut. Und es ist nicht so laut, dass man den anderen Mitbewohnern zu sehr auf die Nerven geht.

Der Pieper wird an D9 und D2 angeschlossen, dabei ist die Polung egal. Über einen kleinen Widerstand von circa 100 Ohm zwischen B2 und X2 erfolgt eine Verbindung zur Versorgungsspannung. Der Widerstand sorgt dafür, dass der Strom beim Signalwechsel etwas gedämpft wird, schränkt dabei die Lautstärke aber kaum ein.

Zur Ausgabe eines Tones gibt es die Funktion setzeTon(), der man in der Klammer die gewünschte Frequenz der Schallschwingungen angibt. Ein guter Startwert ist 1000.
Durch unterschiedliche Frequenzen kann man unterschiedlich hohe Töne erzeugen und damit sogar ein Lied spielen.
Aber welche Frequenzen ergeben welchen Ton? Das kann man leicht im Internet finden, oder sich selbst ausrechnen: nebeneinander liegende Halbtöne unterscheiden sich um den Faktor 12te Wurzel von 2 voneinander, also 2(1 / 12) = 1,059463094. Nun muss man nur noch wissen, dass der Ton „a“ der Frequenz 440 Hz entspricht und kann daraus die Frequenzen aller anderen (Halb-)Töne berechnen. Fragt mal Eure(n) Musiklehrer/-in! 🙂

Die Funktion setzeTon() startet den Ton einfach nur und er wird die ganze Zeit automatisch ausgegeben, auch wenn das Programm inzwischen etwas anderes macht. Erst durch einen weiteren Aufruf mit einer anderen Frequenz wird der Ton geändert oder bei Angabe von 0 gestoppt.

Möchte man also nur einen kurzen Piepton ausgeben, muss man etwas in der Art programmieren:

. . .
setzeTon( 1000 );
warteMillisec( 500 );
setzeTon( 0 );
. . .

Hinderniserkennung mit Ultraschallsensor

So einfach die im letzten Post beschriebenen Fühler mit Tastern auch sind, haben sie doch den großen Nachteil, dass sie das Hindernis erst dann erkennen, wenn es schon gerammt wurde. Außerdem kann man nur erkennen, dass das Hindernis da ist, aber nicht, wie weit entfernt es noch ist.

Flexibler ist daher die Hinderniserkennung mit Ultraschall-Sensoren, bei denen das Echo von nicht hörbaren Ultraschall-Impulsen ausgewertet wird, so wie es auch Fledermäuse zur Hinderniserkennung machen. Nach Aussenden eines kurzen Tones von ungefähr 44000 Hz wird gemessen, wie lange es dauert, bis das Echo zurückkommt. Je kürzer dieser Zeitraum ist, desto näher ist das Hindernis. Auch wenn der Schall schon recht schnell ist, kann man seine Laufzeit mit elektronischen Mitteln relativ einfach messen, so dass diese Sensoren heutzutage nur ein paar Euro kosten. Auf Ebay findet man recht günstige Angebote.

Der Sensor wird am besten mit Heißkleber an seinen Anschlüssen auf einen Legostein geklebt, so dass er frei platzierbar ist. Wenn man die Anschlüsse ein paar Millimeter länger lässt, kann man den Sensor beliebig ausrichten, also nicht unbedingt nach vorne, sondern vielleicht leicht schräg auf eine Seite, so dass man den Roboter einer Wand folgen lassen kann.

Die Anschlüsse haben folgende Bedeutung:

rot: Spannungsversorgung; irgendwo in X stecken
blau: Triggersignal zum Starten der Messung; einstecken in H11
gelb: Mess-Signal, dessen Dauer die Laufzeit des Schalls angibt; H12
schwarz: Batterie Minus; irgendwo in Y einstecken

Im Programm kann eine Messung mit der Funktion AbstandCm() durchgeführt werden. Wie der Name schon sagt, liefert sie den gemessenen Abstand als ganze Zahl in Zentimetern. Allerdings kann es manchmal vorkommen, dass auch nach langer Zeit gar kein Echo empfangen wird, zum Beispiel weil das Hindernis zu weit weg oder eine Störung passiert ist. In diesem Fall wird 0 zurückgegeben.

Da die Messung je nach Abstand zum Hindernis durchaus einige Millisekunden dauern kann (über 200 wenn kein Echo zurückkommt!), lohnt es sich auf jeden Fall, den Messwert zunächst in eine Variable zu speichern, bevor er in if-Bedingungen ausgewertet wird. Dabei darf man nie vergessen, den Sonderfall 0 auszuschließen, um nur gültige Messungen zu betrachten:

. . .

uint distanz;

. . .


distanz = AbstandCm();
if ( ( distanz != 0 ) && ( distanz < 10 ) )
{
    // Hindernis in weniger als 10 Zentimetern Entfernung erkannt
    . . .
}

. . .

Na, wie wäre es denn mal mit einem Programm, das den Roboter einer Wand in 10 cm Entfernung folgen lässt?
Oder eines, das den Roboter geradeaus fahren lässt bis er ein Hindernis erkennt, dann dreht bis keines mehr vorhanden ist und danach seinen Weg fortsetzt?
Umgekehrt könnte der Roboter natürlich auch nach einem Hindernis suchen und hinfahren, um es wegzuschieben. Also so, wie es beim Robotersumo benötigt wird.

Ihr seht, mit einem Ultraschallsensor hat man viele Möglichkeiten!

Hinderniserkennung mit Fühlern

Die einfachste Methode, ein Hindernis zu erkennen, ist sicher die Verwendung von Fühlern. Sie betätigen bei Berührung einen elektrischen Kontakt, der dann im Programm abgefragt werden kann und zu einer Änderung der Bewegungsrichtung führt. Wir verwenden in der AG eine kleine Platine, auf der zwei Taster aufgelötet sind, an denen jeweils ein langer Draht befestigt ist, der eben wie ein Fühler den Raum vor dem Roboter abtastet. Da ein Fühler nach links und der andere nach rechts zeigt, kann sogar unterschieden werden, auf welcher Seite das Hindernis ist und entsprechend reagiert werden. So ist es naheliegend, dass ein eher links befindliches Hindernis rechts umfahren wird und umgekehrt.
Die Platine mit den Tastschaltern wird mit Heißkleber auf einen Legostein geklebt, so dass der Sensor bequem am Roboter befestigt und auch wieder leicht entfernt werden kann. Am besten klebt man die Platine so auf, dass die angelöteten Kabel nach oben weggehen.

Die drei Kabel haben folgende Bedeutung:

blau: Signal für linken Fühler, einstecken in A13
gelb: Signal für rechten Fühler, A14
schwarz: Batterie Minus, irgendwo in Y einstecken

Im Programm können die beiden Taster über die Funktionen
HindernisLinks() und
HindernisRechts()
abgefragt werden. Die Funktionen liefern 1, wenn der Kontakt betätigt ist und 0 sonst.

Man wird im Programm also etwas schreiben wie:

. . .

if ( HindernisLinks() == 1 )
{
    // linkes Hindernis erkannt
    . . .
}

. . .

Aber was soll eigentlich genau passieren, wenn ein Hindernis erkannt wurde?

Am einfachsten ist es, wenn wir nur kurz anhalten. Das ist dann sinnvoll, wenn mehrere Roboter hintereinander auf einer Linie fahren. Da nie alle genau gleich schnell fahren, kann es zu Kollisionen kommen. Durch das kurze Anhalten bei Berührung der Fühler wird dies vermieden.
Das könnte man folgendermaßen programmieren, und zwar in der Schleife des Linienfolgers, bevor die beiden Liniensensoren eingelesen und ausgewertet werden:

. . .
// Hindernis?
if ( ( HindernisLinks() == 1 ) || ( HindernisRechts() == 1 ) )
{
    // kurz anhalten
    setzeMotorLinks( 0 );
    setzeMotorRechts( 0 );
    warteMillisec( 2000 );
}
. . .

Die Motoren brauchen danach nicht wieder extra eingeschaltet zu werden, denn das passiert ja sowieso später beim Auswerten der Liniensensoren.

Eine andere Möglichkeit wäre es, einem auf der Linie stehenden Hindernis auszuweichen und danach die Fahrt auf der Linie fortzusetzen, wie es bei manchen Roboter-Wettbewerben gefordert wird. Dabei gehen wir davon aus, dass es sich bei den Hindernissen um Flaschen der ähnliche kleine Gegenstände handelt, sie also klein und leicht zu umfahren sind. Der Roboter soll nun zunächst einer Linie folgen, wie wir das schon programmiert haben. Erkennt er jedoch ein Hindernis, soll er kurz zurück fahren, sich nach links oder rechts drehen, ein Stückchen weg von der Linie fahren, sich dann wieder parallel zur Linie drehen, am Hindernis vorbeifahren und dann wieder zurück zur Linie finden.
Und hier haben wir auch schon die eigentliche Herausforderung: wie finden wir zurück zur Linie? Einfach einen kleinen Ausweichkurs zu programmieren ist einfach, sowas haben wir beim Fahren des Quadrates ja schon gemacht. Aber der wird je nach Akkuzustand und Bodenbeschaffenheit jedesmal leicht variieren, so dass wir nie wieder ganz exakt auf die Linie treffen werden. Die Linie muss also „automatisch“ gefunden werden.

Nun, grundsätzlich ist das gar nicht so schwer: wir müssen beim Umfahren der Linie nur dafür sorgen, dass der Roboter ungefähr in Richtung der Linie zeigt und ihn dann einfach geradeaus fahren lassen, bis einer seiner Lichtsensoren dunkel wird (anders herum formuliert: so lange geradeaus fahren wie beide Sensoren hell sind). Ist das erreicht, kann wieder die normale Funktion der Linienverfolgung weitergehen.
Das Programm könnte also ungefähr so aussehen (die Details wurden weggelassen, da sie sich je nach Abmessungen der Roboter ändern können, und etwas sollt Ihr ja auch noch machen:

. . .
// Hindernis?
if ( HindernisLinks() == 1 )
{
    // rechts herum ausweichen
    . . .

    // nun wieder geradeaus fahren und die Linie suchen
    setzeMotorLinks( 1 );
    setzeMotorRechts( 1 );

    hellLinks = SensorLinks();
    hellRechts = SensorRechts();

    while ( ( hellLinks > 50 ) && ( hellRechts > 50 ) )
    {
        hellLinks = SensorLinks();
        hellRechts = SensorRechts();
    }
}
. . .

Man darf nicht vergessen, in der Schleife die Sensorwerte neu einzulesen, sonst würden wir ja immer dieselben Werte verwenden. Außerdem müssen die Sensorwerte auch vor der Schleife aktualisiert werden, damit der erste Schleifendurchlauf passt.

Da man solche Situationen häufig antrifft, bei denen die Schleifenbedingung von Werten abhängt, die eigentlich erst in der Schleife bekannt werden, kann man die Schleife auch so schreiben, dass zuerst der Schleifenrumpf ausgeführt wird und dann erst geprüft wird, ob die Schleife nochmal durchlaufen werden soll:

. . .
// Hindernis?
if ( HindernisLinks() == 1 )
{
    // rechts herum ausweichen
    . . .

    // nun wieder geradeaus fahren und die Linie suchen
    setzeMotorLinks( 1 );
    setzeMotorRechts( 1 );

    do
    {
        hellLinks = SensorLinks();
        hellRechts = SensorRechts();
    }
    while ( ( hellLinks > 50 ) && ( hellRechts > 50 ) )
}
. . .

Achtung: das Wörtchen do nicht vergessen!

Alternativ könnte man auf das Setzen der beiden Variablen verzichten und stattdessen direkt die Sensorwerte in der Bedingung schreiben, also:

. . .
// Hindernis?
if ( HindernisLinks() == 1 )
{
    // rechts herum ausweichen
    . . .

    // nun wieder geradeaus fahren und die Linie suchen
    setzeMotorLinks( 1 );
    setzeMotorRechts( 1 );

    while ( ( SensorLinks() > 50 ) && ( SensorRechts() > 50 ) )
    {
    }
}
. . .

Hier hat man sogar eine leere Schleife! Aber Achtung: die leeren geschweiften Klammern darf man keinesfalls weglassen.

Hmmm, und wieso benutzen wir überhaupt diese Variablen? Man könnte doch auch alle anderen Bedingungen im Programm direkt mit den Sensorwerten schreiben!
Nun ja, stimmt schon, und wie so oft kommt es auf die Umstände an. Das Speichern in Variablen ist dann sinnvoll, wenn die Abfrage der Sensoren relativ lange dauert, wie wir beim Ultraschall-Abstandssensor sehen werden. Oder man möchte einen Messzeitpunkt festhalten, also in Variablen speichern, und kann dann „in Ruhe“ diesen Zustand abfragen und darauf reagieren. Letzteres ist zum Beispiel dann wichtig, wenn man viele Sensorwerte speichern möchte, die sich auch noch schnell ändern. Dann ist es am besten, alle Sensoren möglichst gleichzeitig zu erfassen (man sagt auch, den Prozess-Zustand erfassen) und danach mit diesen gespeicherten Werten weiter zu arbeiten. So hat man immer einen konsistenten Zustand mit zusammenpassenden Sensorwerten.

Zurück zum Finden der Linie, es geht sogar noch einfacher: die verbesserte Version des Linienfolgers („Pro-Version“) arbeitet mit einer Korrektur-Variable, die angibt, auf welche Seite der Linie sich der Roboter von ihr entfernt hat. Anfangs wird diese Variable auf 0 gesetzt, so dass der Roboter zunächst auf eine weiße Fläche gesetzt werden kann und dann geradeaus fährt, um die Linie zu suchen. Genau dies können wir hier ausnutzen, indem wir die Korrektur-Variable nach Umfahren des Hindernisses auf 0 setzen und die normale Linienverfolgung weiterlaufen lassen. Der Roboter wird dann wie beim Starten von sich aus geradeaus vorwärts fahren bis er eine Linie erkennt und ihr dann wieder folgen:

. . .
// Hindernis?
if ( HindernisLinks() == 1 )
{
    // rechts herum ausweichen
    . . .

    // nun wieder geradeaus fahren und die Linie suchen. Dazu verwenden wir einfach die Korrektur-Variable
    korrektur = 0;
}
. . .

verbessertes Programm zum Linienfolger

In einem früheren Post ist eine PDF-Datei hinterlegt, welche das einfache und das verbesserte Linienfolger-Programme als Listing enthält und den Bestückungsplan dazu. Damit Ihr das verbesserte Programm direkt mit Kopieren/Einfügen auf Eurem PC verwenden könnt, ist es hier nochmal direkt gelistet:

void folgeLinieProVersion( void )
{
    // Variablen müssen ganz am Anfang einer Funktion erstellt werden
    uint korrektur = 0; // wir setzen den Korrekturwert gleich auf 0, damit nicht korrigiert wird. 
                        // Das ist ganz praktisch wenn wir außerhalb der Linie starten, denn dann fährt der
                        // Roboter erstmal geradeaus bis er die Linie findet.
    uint hellLinks;
    uint hellRechts;

    // Endlosschleife
    while ( 1 )
    {
        // beide Sensoren einlesen
        hellLinks = SensorLinks();
        hellRechts = SensorRechts();

        // sind wir mit beiden Sensoren auf der Linie, also der linke UND (&&) der rechte Sensor dunkel?
        if ( ( hellLinks <= 50 ) && ( hellRechts <= 50 ) )
        {
            // ja, also geradeaus
            setzeMotorLinks( 1 );
            setzeMotorRechts( 1 );
        }
        else
        {
            // sind wir links, halb auf der Linie?
            if ( ( hellLinks > 50 ) && ( hellRechts <= 50 ) )
            {
                // ja, also nach rechts korrigieren.
                setzeMotorLinks( 1 );
                setzeMotorRechts( 0 );

                // wir merken uns, dass wir zu weit links waren
                korrektur = 1;
            }
            else
            {
                // sind wir rechts, halb auf der Linie?
                if ( ( hellLinks <= 50 ) && ( hellRechts > 50 ) )
                {
                    // ja, also nach links korrigieren
                    setzeMotorLinks( 0 );
                    setzeMotorRechts( 1 );

                    // wir merken uns, dass wir zu weit rechts waren
                    korrektur = 2;
                }
                else
                {
                    // hier kann nur noch hell / hell vorliegen, also dass wir komplett weg von der Linie sind.
                    // Jetzt kommt es darauf an, auf welche Seite wir die Linie verlassen haben.
                    // Zum Glück haben wir uns das in der Variable "korrektur" gemerkt!
                    if ( korrektur == 1 )
                    {
                        // wir waren zu weit links, also nach rechts korrigieren
                        setzeMotorLinks( 1 );
                        setzeMotorRechts( 0 );
                    }
                    else
                    {
                        if (korrektur == 2 )
                        {
                            // wir waren zu weit rechts, also nach links fahren
                            setzeMotorLinks( 0 );
                            setzeMotorRechts( 1 );
                        }
                        else
                        {
                            // Sonderfall: sollten wir schon neben einer Linie starten, also korrektur noch 0 sein, fahren wir
                            // einfach geradeaus und hoffen, dass irgendwann eine Linie kommt.
                            setzeMotorLinks( 1 );
                            setzeMotorRechts( 1 );
                        }
                    }
                }
            }
        }
    }
}


int main( void )
{
    initHardware();

    //fahreQuadrat();
    //folgeLinie();
    folgeLinieProVersion();
    
    // Endlosschleife, damit die LED und die Motoren weiter arbeiten
    while (1)
        ;

    return 0;
}

Erweiterung: Lastausgang

Neulich ergab sich der Bedarf eines weiteren Ausgangs der Robotersteuerung, um damit zum Beispiel einen kleinen Motor oder vielleicht ein paar helle LEDs ansteuern zu können. Glücklicherweise haben wir noch einen Anschluss des Mikrocontrollers frei, und zwar Port PA5 (Pin8). Über diesen und einen daran angeschlossenen Transistor BC337 lässt sich eine etwas größere Last bis zu 800 Milliampere schalten. Dann muss allerdings darauf geachtet werden, ob der Transistor im eingeschalteten Zustand heiß wird und bei Bedarf ein Kühlkörper aufgesetzt werden. Beim Anschluss eines kleinen Getriebemotors oder den zur Beleuchtung verwendeten LEDs kann jedoch nichts passieren.
Der Bestückungsplan zeigt, dass von Pin8 (Kontakt G9) zum Transistor (G20) ein langer Widerstand gelegt werden muss, den man am besten zumindest auf der rechten Hälfte mit einem Isolierschlauch versehen sollte, da er dicht an der grünen LED und den Kondensatoren vorbei führt.
Die Beinchen des Transistors müssen etwas gebogen werden, damit er in die Kontakte passt. Die Last (also der Motor oder die LEDs) wird mit ihrem Minuspol in Kontakt F21 gesteckt und mit dem Pluspol irgendwo in X:

Beim Anschließen von LEDs bitte den Vorwiderstand nicht vergessen! Dabei sollte jede LED ihren eigenen Widerstand bekommen und nicht etwa nur einer für alle verwendet werden.

Zum Ansteuern des Ausgangs dient die Funktion setzeAusgang(), der man in der Klammer mitteilt, ob der Ausgang eingeschaltet (1) oder ausgeschaltet werden soll (0), also zum Beispiel:

// Zusatz-Motor einschalten:
setzeAusgang( 1 );

. . .

// Zusatz-Motor ausschalten:
setzeAusgang( 0 );

Genaugenommen wird nicht nur bei 1 der Ausgang eingeschaltet, sondern bei jedem beliebigen Wert außer 0.

Damit dieser neue Befehl verwendet werden kann, müssen die Dateien hardware.h und hardware.c, die sich in folgendem ZIP befinden, ins Projektverzeichnis gelegt werden und die dort schon befindlichen Dateien ersetzen:
Dateien_Lastausgang.zip

Filme

  • Roboter – Noch Maschine oder schon Mensch [Dokumentation deutsch]: Darin geht es um einen Fotografen, der um die ganze Welt reist, um Humanoide Roboter zu fotografieren. Dabei stellt er sich interessante Fragen, die zum Nachdenken anregen. Ein sehr schöner und interessanter Film, auch wenn oder gerade weil er nicht so technisch daherkommt.
  • Faszination Robotik: ein Rundumblick über den aktuellen Entwicklungsstand und Einsatzmöglichkeiten für Roboter

Software-Paket

Für die Anfänger im Kurs gibt es hier ein Software-Paket, das alle Dateien enthält, die man zum Programmieren unseres Roboters benötigt. Bitte herunterladen und in Euer Server-Laufwerk „H:“ entpacken.

Zum Starten die Datei „pn.bat“ öffnen und dann das Projekt „roboter“ in „Projekte\ServoBot“ laden.

Übersetzen mit „Tools->Make all“ und Programmieren mit „Tools->Program“.

« Older Entries