Tutorial · Mittelstufe · 14 Min. Lesezeit
Linienlabyrinth-Kreuzungen mit IR-Array erkennen
Wie ein Linienfolger ein T von einem Kreuz unterscheidet, Sackgasse vom Ziel – und warum eine einzelne Sensormessung nie zum Entscheiden reicht.
Ein Linienfolger hat einen Job: die Linie mittig halten. Ein Linienlabyrinth-Roboter hat zwei – und beim zweiten bleiben Anfänger stecken. Er muss weiter der Linie folgen, aber auch die Momente bemerken, in denen die Linie eine Wahl anbietet – und entscheiden, welche Art Wahl es ist.
Diese Entscheidungsschicht sitzt auf der PID-Schleife, nicht darin. Der Regler darf nie wissen, was ein Labyrinth ist. Er lenkt auf eine Linie zu; die Kreuzungsschicht beobachtet dieselben Sensormesswerte auf die Muster, die „etwas hat sich geändert“ heißen, und übernimmt, wenn eines auftaucht.
Sechs Dinge, die eine Kreuzung sein kann
Jede Kreuzung in einem geklebten Linienlabyrinth reduziert sich auf einen von sechs Fällen. Mit einem 8-Kanal-Reflexionsarray, das 1 für schwarzes Klebeband und 0 für die weiße Platte liest, erzeugt jeder eine Signatur:
- Nur-links-Abbiegung – die äußeren linken Sensoren werden schwarz, während die rechte Seite weiß bleibt.
- Nur-rechts-Abbiegung – das Spiegelbild.
- T-Kreuzung – Klebeband spannt die volle Breite, mit nichts voraus.
- Kreuz – Klebeband spannt die volle Breite, und die Linie läuft voraus weiter.
- Sackgasse – jeder Sensor liest weiß; die Linie hörte schlicht auf.
- Ziel – ein massiver gefüllter Block, breit und lang, der beim Hineinfahren schwarz bleibt.
Die Messung allein ist mehrdeutig
Die T-Kreuzung, das Kreuz und die Zielmarkierung ansehen. Alle drei melden 11111111. Eine Linksabzweigung, wo die Linie auch geradeaus weiterläuft, meldet dasselbe wie eine schlichte Linksabbiegung.
Das lohnt Verinnerlichen: Das momentane Sensormuster kann eine Kreuzung nicht klassifizieren. Ein Roboter, der entscheidet, sobald seine äußeren Sensoren schwarz werden, biegt selbstbewusst links ab, wo er geradeaus hätte durchfahren sollen.
Die Lösung ist Bewegung, nicht mehr Sensoren. Wenn das Array erstmals ein breites schwarzes Muster meldet:
- Notieren, welche Seite (oder Seiten) schwarz wurde – das ist die Menge verfügbarer Abzweigungen.
- Etwa eine Roboterlänge geradeaus weiterfahren.
- Erneut lesen. Sehen die Mittelsensoren weiter Klebeband, war geradeaus auch eine Option. Ist alles weiß, war es ein T. Ist alles weiter schwarz, ist man in den Zielblock hineingefahren.
Erst jetzt weiß man, welche Art Kreuzung man hinter sich ließ – und erst jetzt kann die Labyrinthschicht wählen.
Junction classify(Sensors& s) {
Branches at = s.readWideBranches(); // left / right seen on arrival
driveForward(ONE_ROBOT_LENGTH); // commit past the intersection
Reading after = s.read();
if (after.allBlack()) return FINISH; // still on tape = solid block
at.straight = after.centreOnLine(); // line continued through
return at.toJunction();
}
Beachte: Der Klassierer fährt. Das ist ungewöhnlich für eine Wahrnehmungsfunktion und der Punkt: Kreuzungstyp ist eine Eigenschaft einer kurzen Trajektorie, nicht einer einzelnen Messung.
Einen Abzweig wählen
Sobald eine Kreuzung klassifiziert ist, wählt die Labyrinthschicht per fester Regel einen Abzweig. Die Linke-Hand-Regel – immer den am weitesten links verfügbaren Abzweig nehmen und an einer Sackgasse umkehren – ist die Standardwahl: zustandslos, erschöpfend auf einem schleifenfreien Labyrinth und trivial von Hand prüfbar.
Konsistenz zählt hier mehr als Klugheit. Ein Roboter, der immer links bevorzugt, besucht irgendwann jeden erreichbaren Abzweig – und die Aufzeichnung seiner Drehungen ist das, was der Routenvereinfachungsschritt später zum optimalen Pfad kollabiert. Dasselbe Erkunden-dann-Optimieren im Labyrinthlöser-Simulator laufen sehen – der spürt Wände statt Klebeband, aber die Entscheidungsschicht über der Wahrnehmung ist identisch.
Das Array muss breiter sein als die Linie
Kreuzungserkennung stellt eine bestimmte Frage an den Sensor: Sind die äußersten Kanäle schwarz geworden, während die Mitte noch auf der Linie ist? Ein Array, kaum breiter als das Klebeband, kann sie physisch nicht beantworten.
left branch 8-channel array, ~60 mm wide
────────────────┐ [ 1 1 1 0 0 0 0 0 ] outer left black -> a branch exists
│
════════════════╪═══════ [ 0 0 0 1 1 0 0 0 ] centre still on the line
│
| Klebebandbreite | Minimale sinnvolle Arraybreite | Warum |
|---|---|---|
| 19 mm (Standard) | ~60 mm | Äußere Sensoren müssen das Klebeband um vollen Sensorabstand freigeben |
| 25 mm | ~75 mm | |
| 12 mm | ~40 mm |
Die Faustregel lautet dreifache Klebebandbreite. Schmaler, und der Roboter kann „die Linie bog scharf“ nicht von „die Linie verzweigte“ unterscheiden – die zentrale Entscheidung, auf der das ganze Projekt ruht. Ein 8-Kanal-Array bei 8 mm Abstand ergibt etwa 60 mm – weshalb das die Standardwahl für 19-mm-Klebeband ist.
Eine Kreuzung entprellen
Das Array überquert eine Kreuzung über mehrere Schleifendurchläufe, und jeder ist eine frische Messung. Ein Klassierer, der beim ersten breiten Muster feuert, feuert oft zwei-, dreimal für eine Kreuzung – und füllt den aufgezeichneten Pfad mit Zeichen, die nie echte Drehungen waren.
const uint8_t CONFIRM_SAMPLES = 3; // consecutive readings that must agree
const unsigned long JUNCTION_DEADTIME_MS = 300;
uint8_t agreeCount = 0;
uint8_t lastPattern = 0;
unsigned long lastJunctionAt = 0;
bool junctionDetected(uint8_t pattern) {
// Ignore everything for a short while after committing to one.
if (millis() - lastJunctionAt < JUNCTION_DEADTIME_MS) return false;
if (pattern == lastPattern) agreeCount++;
else { lastPattern = pattern; agreeCount = 1; }
if (agreeCount >= CONFIRM_SAMPLES && isWide(pattern)) {
lastJunctionAt = millis();
agreeCount = 0;
return true;
}
return false;
}
Die Totzeit zu dimensionieren ist ein echter Zielkonflikt – und lohnt mit Zahlen statt nach Gefühl:
deadtime < (minimum spacing between junctions) / speed
Bei 0,2 m/s mit Kreuzungen nicht enger als 15 cm sind das 750 ms Spielraum – 300 ms sind also sicher. Bei 0,5 m/s fahren in einem Labyrinth mit 10-cm-Abstand schrumpft das Budget auf 200 ms – weshalb Erkundungsläufe langsam fahren. Diese Zwangsbedingung, nicht der Algorithmus, setzt das Erkundungstempo.
Die Drehung selbst
Die Kreuzung zu klassifizieren ist die halbe Arbeit; die Drehung auszuführen, ohne die Linie zu verlieren, die andere Hälfte – und eine Festwinkeldrehung ist der falsche Weg.
| Methode | Wie sie endet | Zuverlässigkeit |
|---|---|---|
| Für feste Zeit drehen | Nach N Millisekunden | Schlecht – ändert sich mit Akkuspannung und Untergrund |
| Festen Winkel auf Encodern drehen | Nach N Zählwerten | Besser, aber Spiel und Schlupf summieren sich |
| Drehen, bis die Linie wieder da ist | Wenn die Mittelsensoren Klebeband sehen | Am besten – selbstkorrigierend, endet wo die Linie ist |
void turnUntilLine(int direction) {
pivot(direction);
delay(120); // clear the junction's own tape first
while (!centreOnLine()) { // then rotate until the new line appears
if (millis() - startedAt > TURN_TIMEOUT_MS) { stopMotors(); fault(); return; }
}
stopMotors();
}
Das delay(120) zählt. Direkt nach Drehbeginn liegen die Mittelsensoren noch über dem Klebeband der eigenen Kreuzung – ein naives „drehen, bis du die Linie siehst“ stoppt also instant und der Roboter dreht nie überhaupt. Erst die Kreuzung freigeben, dann schauen.
Das Timeout zählt auch: Ohne dreht ein Roboter, der am Abzweig vorbeidreht – weil er zu schnell fuhr oder der Abzweig fehlerkannt wurde –, ewig.
Die volle Klassierung, mit aufgelöstem Geradeaus
Entprellen, Vorbeifahren und Drehen zusammengefügt:
Junction classifyAndTurn() {
const Branches at = readWideBranches(); // left and/or right, on arrival
driveForward(ONE_ROBOT_LENGTH); // commit past the intersection
const Reading after = read();
if (after.allBlack()) return FINISH; // still on tape = the solid block
Junction j;
j.left = at.left;
j.right = at.right;
j.straight = after.centreOnLine(); // resolved only by moving
if (!j.left && !j.right && !j.straight) return DEAD_END;
return j;
}
Der Klassierer fährt – ungewöhnlich für eine Wahrnehmungsfunktion und der ganze Punkt: Kreuzungstyp ist eine Eigenschaft einer kurzen Trajektorie, nicht einer einzelnen Messung. Drei der sechs Fälle – T, Kreuz und Ziel – sind im Ankunftsaugenblick ununterscheidbar.
Testen ohne Labyrinth
Man braucht kein Labyrinth, um diese Schicht zu verifizieren – und eines zu bauen, bevor die Erkennung arbeitet, ist eine gängige Abendverschwendung. Je eine Kreuzung jedes Typs auf einer Platte auslegen und den Roboter der Reihe nach darüberfahren lassen, mit ausgegebener Klassierung:
| Teststück | Erwartet | Gängiger Fehler |
|---|---|---|
| Schlichte Gerade | Gar keine Kreuzung | Feuert auf leichter Kurve – Außenschwelle zu empfindlich |
| Sanfte Kurve, 15 cm Radius | Keine Kreuzung | Ebenso |
| Nur-Links-Abzweig | LEFT |
Als T erkannt – Totzeit zu kurz, doppelt gefeuert |
| Nur-Rechts-Abzweig | RIGHT |
Spiegelbild des obigen |
| T-Kreuzung | T |
Als LEFT erkannt – nicht vorbeigefahren, um geradeaus aufzulösen |
| Kreuz | CROSS |
Als T erkannt – Vorbeifahrstrecke zu kurz |
| Sackgasse | DEAD_END |
Als Linienverlust erkannt und der Roboter fuhr weiter |
| Massiver Zielblock | FINISH |
Als CROSS erkannt – der Block ist länger als eine Kreuzung |
Der Sanfte-Kurve-Test fängt die meisten Fehler – und an ihn denkt niemand. Eine enge Kurve legt momentan mehrere äußere Sensoren aufs Klebeband, was exakt wie ein Abzweig aussieht. Erfindet dein Roboter Kreuzungen auf Kurven, als Lösung verlangen, dass die äußeren Sensoren über eine Mindeststrecke schwarz bleiben, nicht nur eine Mindestzahl Messwerte: Eine Kurve gibt sie schnell wieder frei, ein echter Abzweig nicht.
Fehlersuche
| Symptom | Wahrscheinliche Ursache | Lösung |
|---|---|---|
| Kreuzungen auf Kurven erkannt | Außenschwelle zu empfindlich | Verlangen, dass das Muster über eine Mindeststrecke besteht |
| Eine Kreuzung doppelt aufgezeichnet | Keine Totzeit | 200–300 ms nach Festlegen auf eine Kreuzung |
| T als Linksabbiegung gemeldet | Nicht vorbeigefahren, um geradeaus aufzulösen | Der Klassierer muss sich bewegen, bevor er entscheidet |
| Kreuz als T gemeldet | Vorbeifahrstrecke zu kurz | Eine volle Roboterlänge an der Schnittstelle vorbei |
| Ziel als Kreuz gemeldet | Der Block ist länger als eine Kreuzung | Weiterfahren; er bleibt schwarz, ein Kreuz nicht |
| Kreuzungen glatt verfehlt | Zu schnell oder Array zu schmal | Erkundungslauf verlangsamen; Array ≥ 3× Klebebandbreite |
| Drehungen schießen über die neue Linie hinaus | Festzeit- oder Festwinkeldrehung | Drehen, bis die Mittelsensoren wieder fassen |
| Roboter dreht nach Kreuzung ewig | Kein Dreh-Timeout | Eines hinzufügen und sauber fehlen |
| Dreht nie überhaupt | Das Klebeband der eigenen Kreuzung als neue Linie gelesen | Die Kreuzung freigeben, bevor man schaut |
| Auf einer Platte in Ordnung, auf anderer nicht | Kalibrierung oder Umgebungslicht | Bei jedem Einschalten neu kalibrieren; Emitter-aus-Subtraktion |
Auf echtem Klebeband verlässlich bekommen
Drei Dinge brechen Kreuzungserkennung auf Hardware weit öfter als der Algorithmus:
- Kalibrierdrift. Den Kalibriersweep des Arrays über schwarzes Klebeband wie weiße Platte bei jedem Einschalten fahren – und erneut, wenn man den Raum wechselt. Umgebungsinfrarot aus Sonnenlicht und Leuchtstofflampen ändert die Messwerte.
- Sensorhöhe. 5–8 mm über dem Boden. Zu hoch und der Kontrast kollabiert; zu niedrig und eine leicht verzogene Platte schabt am Array.
- Klebebandqualität. 18–20-mm-Klebeband verwenden und Kreuzungen physisch sauber legen. Eine ausgefranste Ecke liest sich als intermittierender Abzweig – und kein Code repariert Klebeband, das lügt.
Erst schlichtes Linienfolgen auf einem einfachen Oval stabil bekommen – im Linienfolger-Simulator abstimmen, dann auf echtem Klebeband. Ein Labyrinth versteckt Mechanik- und Sensorprobleme hinter dem, was wie ein Logikfehler aussieht.
Den Graphen erkunden
Teil dieser Bauprojekte
Projekte und Lernpfade, die dieses Tutorial enthalten.
Weiterführende Literatur