Tutorial · Gevorderd · 35 min
Gesloten-luswielsnelheidsregeling met encoders
PWM is een spanning, geen snelheid. Bouw een snelheidslus per wiel met voorafsturing plus PI, bepaal de bemonsteringsfrequentie en voorkom koersafdrijven.
PWM is een spanning, geen snelheid
analogWrite(motorPin, 180) vraagt niet om een snelheid. Het vraagt om ongeveer 70% van de batterijspanning, en welke snelheid dat oplevert hangt af van de belasting op het wiel, de wrijving in die overbrenging, de ladingstoestand en de temperatuur van de borstels.
Daarom rijdt een robot met twee identieke motoren en identieke PWM in een boog. Ze waren nooit identiek — de ene overbrenging zit strakker, de ene borstelset is nieuwer, en bij dezelfde spanning lopen ze 3–8% uit elkaar. Over drie meter is dat een zichtbare kromme.
Op een robot met differentiële aandrijving kun je dat verhelpen met een trimconstante. Op een mecanumbasis kun je dat niet, want de inverse kinematica geeft de vier wielen drastisch verschillende doelen — in het rekenvoorbeeld daar krijgt het ene wiel 0.020 m/s en het andere 0.580 m/s uit hetzelfde commando — en die verhoudingen bepalen in welke richting de robot rijdt. Een wiel dat 5% te langzaam is maakt de robot niet een beetje langzaam; hij maakt hem ergens anders.
De lus sluiten rond elke gemeten wielsnelheid lost dat op, en het is dezelfde oplossing voor elke robot met meer dan twee wielen of met enige behoefte om ergens aan te komen.
Meet de snelheid die je werkelijk hebt
Begin bij de kwadratuurencodertellingen. De omrekening is vaste geometrie:
counts per wheel revolution = CPR_motor × gear ratio × 4 (×4 for quadrature edges)
metres per count = 2πr / counts per revolution
Voor een motorencoder van 12 CPR achter een reductie van 30:1 op wielen van 97 mm: 12 × 30 × 4 = 1440 tellingen/omw, en 2π × 0.0485 / 1440 = 212 µm per telling.
En dan het deel dat iedereen fout doet:
// DON'T: counts since last loop, divided by loop time.
float speed = (count - lastCount) * METRES_PER_COUNT / dt;
Bij 100 Hz en 0.05 m/s is dat één telling per monster. Je snelheidssignaal is 0, 212, 0, 212 µm — een blokgolf met ±100% rimpel, en een D-term versterkt dat tot een zoemende motor. Drie manieren eruit, en doorgaans wil je de eerste:
- Bemonster langzamer dan je regelt. Bereken de snelheid over 20 ms (50 Hz) en voer de lus op 50 Hz uit. Meer tellingen per venster, minder kwantisatie en geen filtervertraging.
- Meet de tijd tussen flanken in plaats van de flanken in een tijd. Uitstekend bij lage snelheid, onhandig bij hoge snelheid, en het vereist onderbrekingstijdstempels.
- Laagdoorlaat de snelheid. Goedkoopst, en het kost je fasemarge — het ding waarvan je er het minste hebt.
// A 50 Hz velocity estimate, with the counter read atomically because an
// encoder ISR is writing it.
noInterrupts();
long count = encoderCount;
interrupts();
float dt = (now - lastTime) * 1e-6f; // micros(), so no ms rounding
float v = (count - lastCount) * METRES_PER_COUNT / dt;
lastCount = count; lastTime = now;
volatile long encoderCount en de noInterrupts()-haakjes zijn niet optioneel: een 32-bits lezing is op een AVR meerdere instructies, en een onderbreking die er middenin terechtkomt levert je een getal dat nooit waar was.
Eerst voorafsturing, dan PID
Het instinct is naar PID te grijpen. Doe eerst de makkelijke 90%.
De stationaire snelheid van een DC-motor is bij benadering lineair in de aangelegde spanning, dus als je de gewenste snelheid kent kun je het meeste van de PWM berekenen:
pwm ≈ kS·sign(v) + kV·v
- kV — de helling. Hoeveel PWM één m/s oplevert.
- kS — het snijpunt met de verticale as. De PWM die nodig is om alleen maar de statische wrijving te doorbreken, voordat het wiel ook maar beweegt.
Meet beide in tien minuten:
- Zet de robot op blokjes zodat de wielen vrij kunnen draaien.
- Verhoog de PWM langzaam tot het wiel juist gaat draaien. Dat is kS, typisch 25–45 van de 255.
- Stap de PWM naar 100, 150, 200, 250 en noteer bij elke stand de stationaire snelheid.
- Fit er een rechte lijn door. De helling is kV, en die zou terug naar ongeveer je kS moeten extrapoleren.
De lus hoeft nu alleen nog de fout tussen die schatting en de werkelijkheid te corrigeren — en die is klein, dus de versterkingen kunnen klein zijn, dus het geheel is stabiel en stil:
float feedforward = kS * sign(target) + kV * target;
float output = feedforward + kP * error + kI * integral;
Zonder voorafsturing moet de integrator het hele commando elke keer dat het doel verandert vanaf nul opbouwen. Dat is traag, het schiet door, en het is de reden dat zo veel met de hand afgestelde motorlussen eindigen met een enorme kI en een robot die schokt.
De regelaar
P en I. Geen D.
struct WheelLoop {
float kS, kV, kP, kI;
float integral = 0, target = 0;
int update(float measured, float dt) {
float error = target - measured;
float ff = kS * (target > 0 ? 1 : target < 0 ? -1 : 0) + kV * target;
float unclamped = ff + kP * error + kI * (integral + error * dt);
int output = (int)constrain(unclamped, -255, 255);
// Conditional integration: only accumulate if the output is NOT saturated,
// or if the error would wind the integrator back toward the middle.
// Otherwise a stalled wheel builds a command it cannot use and then keeps
// driving for a second after you let go of the stick.
if (fabsf(unclamped) < 255.0f || (error * unclamped) < 0) integral += error * dt;
return output;
}
};
Waarom geen differentiaal: de D-term differentieert het snelheidssignaal, en dat is al een verschil van encodertellingen. Je differentieert twee keer, wat tweemaal kwantisatieruis versterkt, en op een wiel is er sowieso vrijwel niets om te dempen. Een snelheidslus met PI en goede voorafsturing is het standaardantwoord, en D toevoegen is een van de manieren waarop een werkende lus wordt verergerd. (Een positielus is iets anders — zie cascaded control.)
Anti-windup is het deel dat je niet kunt overslaan. Houd de robot tegen een muur terwijl je een snelheidscommando geeft, en een naïeve integrator loopt in een paar seconden op naar volle schaal. Laat los en de robot schiet weg. Voorwaardelijke integratie, één if, lost het op.
Afstellen, in de juiste volgorde
- Alleen voorafsturing. kP = kI = 0. Geef 0.3 m/s op en meet wat je krijgt. Binnen ongeveer 10% betekent dat kV klopt. Repareer dat voordat je iets anders aanraakt.
- kP omhoog tot het zoemt, dan een derde terug. Je luistert naar een gehuil dat met de versterking verschijnt en zonder verdwijnt.
- kI tot de blijvende fout verdwenen is. Een wiel met juiste voorafsturing heeft er heel weinig nodig. Begin op kP/10 per seconde en kruip omhoog.
- Controleer het onder belasting, niet op blokjes. Het hele punt is het wiel waarom wordt gevraagd.
- Test alle vier samen, door diagonaal te rijden. Dat is het commando met de grootste spreiding in wielerdoelen, en daar laat een te laag afgesteld wiel zich als eerste zien.
De PID-simulator is een snellere manier om het inzicht voor stappen 2 en 3 op te bouwen dan een robot, en de afstelgids bevat de algemene procedure.
Bemonsteringsfrequentie
De frequentie van de lus moet ruim boven het ding liggen dat hij regelt. De snelheid van een kleine DC-reductiemotor bezinkt met een tijdconstante van ongeveer 50–150 ms; tien monsters binnen die tijdconstante is de gebruikelijke vuistregel, dus 50–200 Hz.
| Frequentie | Oordeel |
|---|---|
| 10 Hz | Te langzaam. De motor is klaar met reageren voordat de lus het merkt. |
| 50 Hz | De praktische vloer voor een hobbyaandrijflijn, en geeft 20 ms encodervenster. |
| 100 Hz | Comfortabel. Vier lussen plus de kinematica passen ruim binnen 10 ms op een Uno. |
| 1 kHz | Encoderkwantisatie overheerst. Je regelt nu ruis. |
Voer het uit op een vast schema, niet “zo snel loop() gaat”, want dt komt rechtstreeks voor in zowel de snelheidschatting als de integrator:
const unsigned long PERIOD_US = 10000; // 100 Hz
static unsigned long next = 0;
void loop() {
unsigned long now = micros();
if ((long)(now - next) < 0) return; // signed compare survives rollover
next += PERIOD_US;
// ... read encoders, run four loops, write four PWMs
}
Die vergelijking met teken is dezelfde valkuil van de millis()-overflow in zijn micros()-vorm, waar hij na 71 minuten toeslaat in plaats van 49 dagen.
Wat het je oplevert, gemeten
Dezelfde robot, hetzelfde commando, dezelfde vloer:
| Open lus (PWM) | Gesloten lus (snelheid) | |
|---|---|---|
| Drift over 3 m rechtaan | 80–200 mm, varieert met de batterij | Onder 20 mm, reproduceerbaar |
| Snelheid terwijl de batterij van 8.4 naar 7.0 V zakt | Daalt ~17% | Blijft gelijk tot de motoren verzadigen |
| Snelheid op een helling van 5° | Daalt sterk | Wordt gehouden, tot het koppel op is |
| Nauwkeurigheid van mecanumdiagonaal | Slecht — de wielverhoudingen kloppen niet | Klopt, omdat het de verhoudingen zijn die worden gehouden |
| Storingsvorm als het op is | Stilzwijgend langzamer | Verzadigt, en je kunt het detecteren |
Die laatste rij wordt onderschat. Een gesloten lus weet wanneer hij het heeft laten afweten: een wiel waarvan de uitvoer op 255 vastzit en nog steeds onder het doel zit meldt iets bruikbaars, en je kunt dat op een telemetrielijn zetten, het commando weigeren, of de hele robot vertragen om de verhoudingen in orde te houden.
Storingzoeklijst
| Symptoom | Oorzaak | Oplossing |
|---|---|---|
| Wiel zoemt of giert in rust | kP te hoog, of D-term op een ruisvolle snelheid | Verlaag kP; verwijder D |
| Robot schokt nadat hij vastgehouden is | Integrale windup | Voorwaardelijke integratie |
| Snelheid oscilleert rond het doel | kI te hoog voor de bemonsteringsfrequentie | Halveer kI; controleer of de lus echt op de snelheid draait die je denkt |
| Snelheid leest 0, dan dubbel, afwisselend | Encodervenster te kort bij die snelheid | Bemonster over een langer venster, of tel flanken tussen tijdstempels |
| Eén wiel zit bij diagonaal altijd verzadigd | Om dat wiel wordt het meest gevraagd; de robot krijgt te veel commando | Schaal het hele commando, knip het niet af — zie de kinematica |
| Tellingen springen of lopen terug | Gemiste flanken, of een niet-atomaire lezing van een volatile long |
Onderbreek op beide kanalen; omlijst de lezing met noInterrupts() |
| Werkt op blokjes, faalt op de vloer | Afgesteld zonder belasting | Stel opnieuw af tijdens het rijden, niet tijdens vrij draaien |
Volgende
De wielen leveren nu de snelheden die hun wordt gevraagd. Richt het geheel op waar de bestuurder mee voorloopt met veldgeoriënteerde aandrijving, en voer het soepele doelen in plaats van stappen met bewegingsprofielen. Kijk daarna in de mecanum-aandrijfsimulator waar een snelheidslus je wel en niet van kan redden — een perfect gehouden wielsnelheid betekent nog steeds niets als de vloer het laat afweten.
Verken de graaf
Onderdeel van deze bouwprojecten
Projecten en leerroutes waarin deze tutorial voorkomt.
Verder lezen