Tutorial · Beginner · 20 min lezen

Bouw een lijnvolger: van sensoren tot PID-besturing

Hoe een lijnvolger werkt: reflectiesensoren, een gewogen positiefout en een PID-lus die een differentiaalaandrijving vloeiend bestuurt.

Een lijnvolger is de klassieke eerste autonome robot: hij leest een lijn onder zijn sensoren en stuurt bij om erop te blijven. Op de bodem is het een klein regelprobleem — sensormetingen omzetten in een stuurcorrectie — waardoor het een perfecte manier is om teruggekoppelde regeling te leren. Volg het in de Line Follower Simulator.

De lijn waarnemen

Een reflectiesensor schiet infrarood licht naar beneden en meet hoeveel er terugkaatst. Een donkere lijn kaatst minder terug dan het heldere oppervlak eromheen, dus elke sensor meldt of hij boven de lijn staat. Een rijtje van meerdere sensoren over de voorkant van de robot vertelt je niet alleen of je op de lijn zit, maar waar de lijn zit ten opzichte van het midden.

Vijf sensoren zijn een goed startpunt. Drie werken bij lage snelheid; wedstrijdrobots gebruiken er vaak acht voor een fijnere resolutie.

Vijf lijnsensoren op een rij, elk gelabeld met een gewicht van min twee links via nul in het midden tot plus twee rechts, en elk met eronder een uitlezing van één of nul. De twee sensoren met gewicht nul en plus één lezen één en zijn uitgelicht; de rest leest nul. De formule voor het gewogen gemiddelde berekent de som van gewichten maal uitlezingen gedeeld door de som van de uitlezingen, en geeft nul plus één over twee gelijk aan plus nul komma vijf. Een waarschuwing stelt dat alle nullen betekent dat de lijn kwijt is, wat een ander geval is en geen fout van nul.
Vijf aan/uit-metingen klappen in tot één getal met teken dat de stuurlus kan gebruiken. Het teken is de richting, de grootte is hoe ver je van de lijn af zit. SVG downloaden

Metingen omzetten in één foutgetal

In plaats van op losse sensoren te reageren, combineer je ze tot één gewogen positie. Geef elke sensor een positiewaarde (negatief links, nul in het midden, positief rechts) en neem het gemiddelde gewogen naar hoe sterk elke sensor de lijn ziet:

// positions: {-2, -1, 0, 1, 2}; readings: reflectance per sensor
float weightedPosition = sum(position[i] * reading[i]) / sum(reading[i]);

Het resultaat is je fout: nul betekent gecentreerd, negatief betekent dat de lijn links ligt, positief dat ze rechts ligt. Een gladde, continue fout is precies wat een regelaar nodig heeft.

Sturen met PID

Voer die fout in een PID-regelaar. De correctie versnelt het ene wiel en vertraagt het andere, waardoor de robot terugdraait naar de lijn:

float correction = kp * error + ki * integral + kd * derivative;
setMotorSpeeds(baseSpeed - correction, baseSpeed + correction);

De proportionele versterking verzorgt het meeste van het stuurwerk; de differentiële versterking dempt de wiebeling in bochten; een kleine integrerende term haalt eventuele blijvende drift weg. Ben je nog niet gewend om versterkingen te kiezen, werk dan eerst hoe je een PID-regelaar afstelt door.

Verhelp de veelvoorkomende fouten

  • Oscilleert rond de lijn: de proportionele versterking is te hoog of de differentiële te laag. Verlaag eerst de snelheid en stel daarna opnieuw af. Op echte hardware voegt een lange delay() in de lus dode tijd toe die de differentiële term ondermijnt.
  • Verliest de lijn in scherpe bochten: de robot is sneller dan de sensors bij kunnen werken. Vertraag, verbreed de sensorrij, of hergebruik de laatst bekende lijnrichting zodat de robot terugdraait in plaats van rechtdoor van de lijn af te rijden.
  • Drijft naar één kant: kalibreer de sensoren. Beweeg ze over de lijn en de achtergrond, leg per sensor het minimum en het maximum vast, en normaliseer live uitlezingen tegen dat bereik.

De hele lus op één plek

De fragmenten hierboven worden pas een werkende robot als de timing, de kalibratie en het geval van lijnverlies allemaal samen worden afgehandeld:

const int N = 5;
const int PIN[N] = {A0, A1, A2, A3, A4};
const int POS[N] = {-2000, -1000, 0, 1000, 2000};   // weights, in the error's units

int minv[N], maxv[N];
long lastError = 0;
float integral = 0;
unsigned long lastMicros = 0;

const float KP = 0.10f, KI = 0.0f, KD = 1.6f;
const int   BASE = 140;                              // out of 255

void loop() {
  // 1. Real elapsed time — never assume the loop period.
  const unsigned long now = micros();
  const float dt = (now - lastMicros) * 1e-6f;
  if (dt < 0.002f) return;                           // cap the rate at 500 Hz
  lastMicros = now;

  // 2. Position from the weighted average.
  long weighted = 0, total = 0;
  for (int i = 0; i < N; i++) {
    const int v = normalized(analogRead(PIN[i]), i);
    if (v > 100) { weighted += (long)v * POS[i]; total += v; }
  }

  // 3. Line lost: hold the last error, do NOT zero it.
  const long error = (total == 0) ? lastError : (weighted / total);

  // 4. PID, with derivative on measurement and conditional integration.
  const float derivative = -(float)(error - lastError) / dt;
  const float candidate = integral + error * dt;
  const float raw = KP * error + KI * candidate + KD * derivative;
  const float correction = constrain(raw, -BASE, BASE);
  if (raw == correction) integral = candidate;

  lastError = error;

  // 5. Differential steering.
  setMotors(BASE - correction, BASE + correction);
}

Vijf dingen daarin scheiden een robot die een lijn volgt van een die het bijna doet.

Echte dt. Zowel I als D delen door de verstreken tijd. Een lus waarvan de duur verandert naarmate je functies toevoegt — en dat doet ze — rekent allebei met een leugen als je een constante periode aanneemt.

De drempel v > 100. Zonder hem trekken vijf kanalen die elk een kleine vloerwaarde uitlezen het gewogen gemiddelde, ongeacht waar de lijn ligt, naar het midden. Het symptoom is een robot die een dikke lijn goed volgt en een vage slecht.

De laatste fout vasthouden bij lijnverlies. Nul betekent “gecentreerd”, dus een robot die op nul zet rijdt recht vooruit op volle snelheid — van de lijn af, die vrijwel zeker aan het buigen was. De laatste fout vasthouden laat hem blijven draaien zoals hij al draaide.

Differentiatie op de meting. De fout differentiëren betekent dat een setpointverandering een enorme piek veroorzaakt. Hier is de setpoint altijd nul dus het maakt weinig verschil, maar de gewoonte is het waard om aan te leren.

Voorwaardelijke integratie. Zodra de correctie satureert, verder integreren alleen maar een overschot op te slaan dat je later moet terugbetalen.

Een afstelvolgorde die werkt

Stel niet alle drie tegelijk af. Zet KI = 0 en KD = 0 en dan:

Stap Wijziging Let op Stop wanneer
1 Verhoog KP vanaf een kleine waarde de robot gaat reageren op de lijn hij een flauwe bocht volgt en een beetje slingert
2 Blijf KP verhogen het slingeren groeit uit tot oscilleren hij gestaag oscilleert — noteer deze waarde als Ku
3 Zet KP op ongeveer de helft van Ku respons is snel, overschot aanwezig —
4 Verhoog KD het slingeren dempt uit rechte stukken zijn schoon; meer KD maakt hem schutterig
5 Alleen als hij consequent naast het midden volgt, voeg wat KI toe de verschuiving sluit ze sluit — KI is zelden nodig bij een lijnvolger
6 Verhoog de BASE-snelheid alles degradeert tegelijk stel KP en KD opnieuw af bij de nieuwe snelheid

Stap 6 is de stap die mensen vergeten: versterkingen zijn snelheidsafhankelijk. Een afstelling die perfect is bij duty 140 zal oscilleren bij 220, omdat dezelfde stuurcorrectie nu per tijdseenheid een grotere verandering in richting oplevert. Verwacht telkens opnieuw af te stellen als je de snelheid wezenlijk verandert.

Integreren is hier werkelijk zelden nuttig. De setpoint van een lijnvolger is nul en de verstoringen zijn tijdelijk, dus meestal is er geen blijvende afwijking die KI kan dichten. Zie je een aanhoudende verschuiving, verdenk dan ongelijke motortrim of een ongekalibreerde sensor voordat je naar integrerende versterking grijpt.

De lusfrequentie begrenst stilletjes alles

Lussnelheid Interval Afgelegde afstand bij 0,4 m/s
20 Hz 50 ms 20 mm tussen beslissingen
50 Hz 20 ms 8 mm
200 Hz 5 ms 2 mm
500 Hz 2 ms 0,8 mm

Een robot die elke 20 mm een beslissing neemt, kan een bocht niet strak volgen hoe de versterkingen ook zijn ingesteld, en geen differentiële term herwint informatie die nooit is bemonsterd.

De gebruikelijke boosdoeners zijn makkelijk te verwijderen: een delay() ergens in de lus, Serial.print van meer dan een paar waarden op 9600 baud, en sensoren uitlezen die je niet gebruikt. Ze eruit halen doet een eerste bouw doorgaans van 50 Hz naar enkele honderden stijgen, en het volgen verbetert zonder één versterking aan te raken. Meet de frequentie vóór het afstellen, niet erna.

Problemen oplossen

Symptoom Waarschijnlijke oorzaak Oplossing
Slingert op rechte stukken KP te hoog of KD te laag halveer KP en verhoog daarna KD
Oscilleert steeds sneller KP boven de uiterste versterking verlaag tot hij stabiel blijft
Snijdt hoeken te snel voor de voorsprong van de sensor vertraag, of verplaats de sensorrij naar voren
Rijdt weg als hij de lijn verliest fout op nul gezet bij lijnverlies houd de laatste fout vast
Volgt de ene kant goed, de andere slecht sensorrij niet waterpas, of motortrim zet de sensorrij waterpas; trim elke motor
Drijft naar één kant ongekalibreerde sensoren, of motorverschil kalibreer per kanaal en trim daarna
Prima op de werkbank, dwaalt op de vloer rijhoogte veranderd 3–8 mm, stevige montage, opnieuw kalibreren
Werkte gisteren, traag vandaag batterijspanning versterkingen zijn eigenschappen van het proces — stel opnieuw af of sluit een snelheidsregeling aan
Trilt alleen bij lage snelheid onder de dode zone van de motoren meet de startduty per motor en compenseer
Niets reageert op KD een delay() in de lus dode tijd ondermijnt de differentiële werking volledig

Probeer het zelf

In de Line Follower Simulator kun je een baan tekenen, het aantal sensoren en de snelheid wijzigen, en de versterkingen afstellen terwijl je de fout en de rondetijd observeert. Alles wat je leert over de gewogen fout en de stuurlus gaat direct mee naar een Arduino-robot.

Verken de graaf

Onderdeel van deze bouwprojecten

Projecten en leerroutes waarin deze tutorial voorkomt.

Verder lezen

Bronnen