Tutorial · Gevorderd · 14 min lezen
Lijnmaze-kruisingen detecteren met een IR-array
Hoe een lijnvolger een T-kruising van een kruis onderscheidt, een doodlopende weg van de finish — en waarom één sensorwaarde nooit genoeg is om te beslissen.
Een lijnvolger heeft één taak: de lijn gecentreerd houden. Een lijnmaze-robot heeft er twee, en bij de tweede lopen beginners vast. Hij moet de lijn nog steeds volgen, maar hij moet ook de momenten opmerken waarop de lijn een keuze aanbiedt — en bepalen wat voor soort keuze dat is.
Die beslislaag ligt bovenop de PID-regellus, niet erin. De regelaar mag nooit weten wat een doolhof is. Hij stuurt naar een lijn; de kruisingslaag let dezelfde sensorwaarden op patronen die betekenen dat “er iets is veranderd” en neemt het over wanneer er zo’n patroon opduikt.
Zes dingen die een kruising kan zijn
Elke kruising in een lijnmaze met plakband valt terug op een van de zes gevallen. Met een 8-kanaals reflectie-array die 1 leest voor zwarte tape en 0 voor het witte bord levert elk geval een signatuur op:
- Alleen links — de buitenste sensoren links gaan zwart terwijl de rechterkant wit blijft.
- Alleen rechts — het spiegelbeeld daarvan.
- T-kruising — de tape loopt de volle breedte door, met niets ervoor.
- Kruis — de tape loopt de volle breedte door, en de lijn gaat rechtdoor.
- Doodlopende weg — elke sensor leest wit; de lijn houdt gewoon op.
- Finish — een volledig gevuld blok, breed en lang, dat zwart blijft terwijl je erin rijdt.
De waarde alleen is dubbelzinnig
Kijk naar de T-kruising, het kruis en de finishmarker. Alle drie melden 11111111. Een zijtak waar de lijn ook rechtdoor gaat meldt hetzelfde als een gewone linkerbocht.
Dit is het stuk dat je moet onthouden: het momentane sensorpatroon kan een kruising niet classificeren. Een robot die beslist op het moment dat zijn buitenste sensoren zwart worden, draait vol overtuiging links op een vierwegsplitsing waar hij rechtdoor had moeten rijden.
De oplossing is beweging, niet meer sensoren. Als de array voor het eerst een breed zwart patroon meldt:
- Leg vast welke kant (of kanten) zwart is geworden — dat is de verzameling beschikbare takken.
- Rij ongeveer één robotlengte door.
- Lees opnieuw. Zien de middelste sensoren nog tape, dan was ook rechtdoor een optie. Is alles wit, dan was het een T. Is alles nog steeds zwart, dan ben je in het finishblok gereden.
Pas nu weet je welke soort kruising je achterliet, en pas nu kan de maze-laag kiezen.
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();
}
Merk op dat de classifier rijdt. Dat is ongebruikelijk voor een meetfunctie, en dat is precies het punt — het type kruising is een eigenschap van een korte baan, niet van één meting.
Een tak kiezen
Zodra een kruising is geclassificeerd, kiest de maze-laag een tak volgens een vaste regel. De linkerhandregel — neem altijd de meest linkse beschikbare tak en keer om bij een doodlopende weg — is de standaardkeuze, omdat hij geen geheugen gebruikt, volledig is in een doolhof zonder lussen, en met de hand te controleren valt.
Hier is consistentie belangrijker dan slimheid. Een robot die altijd links prefereert, bezoekt uiteindelijk elke bereikbare tak, en de registratie van zijn bochten is wat de routevereenvoudiging later ineenvouwt tot het optimale pad. Bekijk hetzelfde verkennen-en-optimaliseren in de Maze Solver Simulator — die neemt muren waar waar de lijn ligt, maar de beslislaag boven de waarneming is identiek.
De array moet breder zijn dan de lijn
Kruisingsdetectie stelt de sensor één specifieke vraag: zijn de buitenste kanalen zwart geworden terwijl het midden nog op de lijn ligt? Een array die nauwelijks breder is dan de tape kan dat fysiek niet beantwoorden.
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
│
| Tapebreedte | Minimale bruikbare arraybreedte | Waarom |
|---|---|---|
| 19 mm (standaard) | ~60 mm | Buitenste sensoren moeten met een volledige sensorafstand voorbij de tape komen |
| 25 mm | ~75 mm | |
| 12 mm | ~40 mm |
De vuistregel is drie keer de tapebreedte. Smaller en de robot kan niet onderscheiden of “de lijn maakte een scherpe bocht” of “de lijn vertakte” — en dat is precies de beslissing waar het hele project op rust. Een 8-kanaals array met 8 mm sensorafstand is ongeveer 60 mm breed, en daarom is dat de standaardkeuze voor 19 mm tape.
Een kruising ontdenderen
De array rijdt in meerdere lusiteraties over een kruising, en elke iteratie is een verse meting. Een classifier die bij het eerste brede patroon aanslaat, slaat voor één kruising vaak twee of drie keer aan en vult het vastgelegde pad met tekens die nooit echte bochten 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;
}
De dode tijd dimensioneren is een echte afweging, en dat doe je beter met getallen dan op gevoel:
deadtime < (minimum spacing between junctions) / speed
Bij 0,2 m/s met kruisingen die niet dichter bij elkaar liggen dan 15 cm heb je 750 ms speelruimte — dus 300 ms is veilig. Rij je 0,5 m/s in een doolhof met 10 cm tussenruimte, dan zakt het budget naar 200 ms, en daarom verkent langzaam. Deze randvoorwaarde, niet het algoritme, bepaalt de verkenningsnelheid.
De bocht zelf
Een kruising classificeren is de helft van het werk; de bocht uitvoeren zonder de lijn kwijt te raken is de andere helft, en een bocht met een vaste hoek is de verkeerde manier om dat te doen.
| Methode | Hoe hij eindigt | Betrouwbaarheid |
|---|---|---|
| Draai een vaste tijd | Na N milliseconden | Slecht — verandert met accuspanning en ondergrond |
| Draai een vaste hoek op encoders | Na N tellingen | Beter, maar speling en slip stapelen zich op |
| Draai tot de lijn opnieuw gevonden is | Zodra de middelste sensoren tape zien | Beste — zelfcorrigerend, eindigt waar de lijn ligt |
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();
}
De delay(120) is belangrijk. Direct na het starten van de bocht liggen de middelste sensoren nog over de eigen tape van de kruising, dus een naïef “draai tot je de lijn ziet” stopt meteen en de robot draait helemaal niet. Verlaat eerst de kruising, kijk dan pas.
De timeout is net zo belangrijk: zonder timeout draait een robot die voorbij de tak stuurt — omdat hij te snel reed, of omdat de tak verkeerd werd gedetecteerd — eindeloos in het rond.
De volledige classificatie, met rechtdoor opgelost
Debounce, doorrijden en bochten samen:
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;
}
De classifier rijdt, wat ongebruikelijk is voor een meetfunctie en precies het punt is: het type kruising is een eigenschap van een korte baan, niet van één meting. Drie van de zes gevallen — T, kruis en finish — zijn bij aankomst niet van elkaar te onderscheiden.
Testen zonder doolhof
Je hebt geen doolhof nodig om deze laag te verifiëren, en er een bouwen voordat de detectie werkt is een klassieke verspilling van een avond. Leg één kruising van elk type op een bord en laat de robot er achtereenvolgens overheen rijden, met de classificatie geprint:
| Teststuk | Verwacht | Gangbare fout |
|---|---|---|
| Rechte lijn | Helemaal geen kruising | Slaat aan op een lichte bocht — buitendrempel te gevoelig |
| Lichte bocht, straal 15 cm | Geen kruising | Idem |
| Alleen linkertak | LEFT |
Gedetecteerd als T — dodetijd te kort, twee keer aangeslagen |
| Alleen rechtertak | RIGHT |
Spiegelbeeld van het vorige |
| T-kruising | T |
Gedetecteerd als LEFT — niet doorgereden om rechtdoor op te lossen |
| Kruis | CROSS |
Gedetecteerd als T — de doorrijafstand was te kort |
| Doodlopende weg | DEAD_END |
Gedetecteerd als lijnverlies, en de robot reed door |
| Volledig finishblok | FINISH |
Gedetecteerd als CROSS — het blok is langer dan een kruising |
De test met de lichte bocht vangt de meeste bugs, en het is de test waar niemand aan denkt. Een scherpe bocht zet meerdere buitenste sensoren tijdelijk op de tape, wat er precies uitziet als een tak. Als je robot kruisingen verzint op bochten, is de oplossing dat de buitenste sensoren minstens een bepaalde afstand zwart moeten blijven, en niet minstens een bepaald aantal metingen — een bocht laat ze snel weer los, een echte tak niet.
Storingzoeken
| Symptoom | Waarschijnlijke oorzaak | Oplossing |
|---|---|---|
| Kruisingen gedetecteerd op bochten | Buitendrempel te gevoelig | Verlang dat het patroon over een minimale afstand blijft bestaan |
| Eén kruising tweemaal vastgelegd | Geen dode tijd | 200–300 ms nadat je voor een kruising hebt gekozen |
| T gemeld als linkerbocht | Niet doorgereden om rechtdoor op te lossen | De classifier moet bewegen voordat hij beslist |
| Kruis gemeld als T | Doorrijafstand te kort | Eén volle robotlengte voorbij de splitsing |
| Finish gemeld als kruis | Het blok is langer dan een kruising | Blijf rijden; het blijft zwart, een kruis niet |
| Kruisingen volledig gemist | Te snel, of de array is te smal | Verken langzamer; array ≥ 3× de tapebreedte |
| Bochten schieten voorbij de nieuwe lijn | Bocht met vaste tijd of vaste hoek | Draai tot de middelste sensoren de lijn opnieuw vinden |
| Robot draait eindeloos door na een kruising | Geen timeout voor de bocht | Voeg er een toe en stop netjes met een foutmelding |
| Draait helemaal nooit | De eigen tape van de kruising gelezen als de nieuwe lijn | Verlaat de kruising voordat je kijkt |
| Werkt op het ene bord, niet op het andere | Kalibratie, of omgevingslicht | Kalibreer bij elke spanningsaanleg; aftrekken met uitgeschakelde zender |
Betrouwbaar maken op echte tape
Drie dingen breken kruisingsdetectie op hardware veel vaker dan het algoritme:
- Kalibratiedrift. Voer de kalibratiesweep van de array bij elke spanningsaanleg uit over zowel zwarte tape als wit bord, en herhaal hem als je naar een andere ruimte gaat. Omgevingsinfrarood van zonlicht en tl-lampen verandert de meetwaarden.
- Sensorhoogte. 5–8 mm boven de vloer. Te hoog en het contrast verdwijnt; te laag en een licht gebogen bord schuurt over de array.
- Tapekwaliteit. Gebruik 18–20 mm tape en maak de kruisingen fysiek schoon. Een gerafelde hoek leest als een wisselende tak, en geen hoeveelheid code repareert tape die tegen je liegt.
Maak eerst gewone lijnvolging stabiel op een simpele ovaal — stel hem af in de Line Follower Simulator en daarna op echte tape. Een doolhof verbergt mechanische en sensorproblemen achter wat op een logische bug lijkt.
Verken de graaf
Onderdeel van deze bouwprojecten
Projecten en leerroutes waarin deze tutorial voorkomt.
Verder lezen