– Af Rasmus Jensen (rasje17) og Patrick Christoffersen (pachr16).

Formålet med denne guide er at give en grundlæggende forståelse for opbygningen af en selvkørende robot som kan følge en sort streg, og undgå at køre ind i forhindringer. Først gives en gennemgang af hvordan de enkelte delkomponenter fungerer og implementeres. Derefter illustreres det hvordan det endelige system bliver opbygget og hvordan det skal kodes for at den ønskede funktionalitet opnås.

Den endelige robot vil være i stand til at registrere en sort streg på gulvet ved hjælp af lyssensorer, og registrere vægge med en ultrasonisk distance sensor. Derudover vil hjulene blive drevet af hver sin motor, og det hele vil blive styret ved hjælp af en arduino.

Opbygning af hardware

Herunder beskrives de hardware-komponenter der bruges til opbygningen af robotten, som i dette tilfælde har fået navnet SmartMobilen. Det vil blive gennemgået hvordan de enkelte komponenter fungerer, hvordan de opsættes i et kredsløb, og hvordan vi interagerer med dem ved hjælp af arduino platformen. De anvendte komponenter er light dependent resistors (lyssensorer), en afstandssensor, jævnstrømsmotorer, en servomotor, led pærer, og en h-bro til motorstyring, der benytter en L293D motor driver. 

Light Dependent Resistor

En Light Dependent Resistor (LDR) er en variabel modstand som afhænger af hvor meget lys selve modstanden påvirkes med, således at modstanden gennem komponenten falder, når intensiteten af lyset stiger. Dermed kan man måle lysintensiteten ved at måle spændingsfaldet over LDR’en. For at vi kan foretage denne måling med arduinoen, opsættes et kredsløb som det ses på billedet herunder. Det ene ben af LDR’en forbindes med 5 volt (i dette tilfælde fra arduinoen), og det andet ben forbindes til en af arduinoens analoge porte. En analog port benyttes da vi måler hvor meget spænding der er tilbage, hvilket ikke er et digitalt signal. Desuden forbindes en 100k ohm modstand til jord.

//Angiver at vi måler lys sensoren på port A5
int LDRIndgang = A5;

int lysverdi = 0;
void setup()
{
	//Opsætning til at vi kan udskrive 
	Serial.begin(9600);
}

void loop()
{
	//Definere at lysværdien er det som vi måler på LDR indgange 
	lysverdi = analogRead(LDRIndgang);
	// udskriver den lysværdi som vi måler
	Serial.println(lysverdi);
    //venter 100 ms før vi afslutter og kører det hele igen
    delay(100);
}

Som det ses i koden så aflæses lysværdien fra analog porten, og i dette tilfælde udskrives den simpelthen bare til seriel monitoren, men vil ellers danne grundlag for hvad det ønskes at benytte lysafmålingen til. Målingerne fra LDR’en kan variere fra 0 til 1023, hvor lavere målinger indikerer lavere lysintensitet.

Det er vigtigt at notere, afhængigt af hvilket miljø robotten skal køre i, at det kan være nødvendigt at sætte en lyskilde ved siden af LDR’en, således at denne lyskilde oplyser det underlag, som vi gerne vil måle på. Dermed måler LDR’en på det tilbagekastede lys, og kan derudfra bestemme farven på underlaget. Hvis en LDR placeres i skygge, for eksempel på undersiden af en bil, kan det være meget svært at måle disse forskelle. (I dette tilfælde, hvor robotten skal testes i en Unity-simulering, er det ikke et problem, men det ville det sandsynligvis være i den virkelige verden).

Afstandssensor

Den afstandssensor som der bliver taget udgangspunkt i er PING))) Ultrasonic Distance sensor (#28015) fra Parallax inc. (https://www.parallax.com/sites/default/files/downloads/28015-PING-Sensor-Product-Guide-v2.0.pdf). Sensoren virker ved at den udsender ultralyd, og måler hvor lang tid det tager før lyden reflekteres tilbage til sensoren. Den skal forbindes til 5 volt og jord, og den har en signal pind som håndterer både indgående og udgående signaler. Sensorens signal pind forbindes til en af arduinoens digitale porte, og kan opsættes som vist nedenfor.

For at benytte PING sensoren er det nødvendigt at aktivere sensoren ved at sætte strøm på signal pinden i mindst 2 mikrosekunder (i koden gøres det i 5). Dog sættes den til lav i 2 mikrosekunder først, for at sikre en ren måling.
Herefter ændres signalpinden til input mode, og tiden der går inden ekkoet returnerer til sensoren aflæses her.
For at få omregnet dette signal til en afstand kan følgende formel benyttes:

Afstand i cm = (mikrosekunder målt / 29) / 2

Tallene kommer af at lyden bevæger sig med en hastighed på 340 m/s hvilket svarer til 29 cm/mikrosekund. Idet den tid som er målt er tiden det tager lyden at nå fra sensoren til objektet og tilbage igen skal det hele deles med to for at få afstanden til objektet.
Kode for alt dette kan ses her:

// Pin som sonaren er forbundet til
const int pingPin = 10;

void setup() {
  // initialisering af serial kommunikation
  Serial.begin(9600);
}

void loop() {
  // variabler som bruges i koden
  long duration, cm;

  //sonaren skal aktiveres med en HIGH puls
  //først sikres en ren måling ved at sætte en LOW puls før aktiveringen
  pinMode(pingPin, OUTPUT);
  digitalWrite(pingPin, LOW);
  delayMicroseconds(2);
  digitalWrite(pingPin, HIGH);
  delayMicroseconds(5);
  digitalWrite(pingPin, LOW);

  
  // Da sonaren benytter samme pin til både input og output ændres denne til INPUT
  pinMode(pingPin, INPUT);
  //målingen fra sonaren er tiden (i mikrosekunder) 
  //fra det afsendte ping til at sonaren modtager ekkoet
  duration = pulseIn(pingPin, HIGH);

  // konvertering til cm
  cm = microsecondsToCentimeters(duration);

  Serial.print(cm);
  Serial.print(" cm");
  Serial.println();

  delay(100);
}

long microsecondsToCentimeters(long microseconds) {
  //Lydens hastighed er 340 m/s eller 29 mikrosekunder pr. cm
  //Da den målte tid er tiden det tager for lyden at nå objektet og tilbage 
  //skal resultatet deles med 2 for at få afstanden til objektet
  return microseconds / 29 / 2;
}

Servomotor

En servomotor er en motor hvis position kan kontrolleres med stor præcision. Der er meget forskel mellem forskellige servomotorer på hvor stor bevægelighedsgrad motoren har. De fleste modeller har en 120-180 graders bevægelighed men der findes også versioner som har 360 graders frihed.
Til dette projekt benyttes en 180 graders servomotor til at ændre orienteringen af afstandssensoren, således at det er muligt at vurdere afstanden til objekter både foran og til begge sider for robotten.

Servoen har tre forbindelsesporte. Den ene er til strøm, hvor servoen kræver mellem 4,8 og 6 volt, en anden er til jord og den sidste er en signalport. På billedet herunder ses en eksempelopstilling hvor servoen får 5 volt fra arduinoen og signalporten er forbundet til en af arduinoens digitale pinde.

For at benytte servomotoren med arduino er det nødvendigt at indlæse servo-biblioteket. Herefter sættes den op som en Servo variabel, ved først at initiere en Servo, og derefter benytte .attach(int x) metoden på Servo, hvor x refererer til den digitale pind, som servoen er forbundet til. Herefter benyttes servoen ved at man skriver den ønskede vinkel til Servo variablen med .write(int vinkel) metoden. Der kan desuden benyttes en .read() metode, for at finde den nuværende orientering af servoen. Koden viser et eksempel hvor servoen starter på 0 grader, og derefter bevæges en grad ad gangen indtil den når 180 grader, og derefter tilbage til 0 igen på samme måde. Koden ses her:

#include <Servo.h>

//variabel for positionen af servoen
int pos = 0;

//et servo objekt oprettes
Servo servo_9;

void setup()
{
  //servoen forbindes med servo opbjektet
  servo_9.attach(9);
}

void loop()
{
  // loop der drejer servoen fra 0 til 180 grader i 1 grads intervaller 
  for (pos = 0; pos <= 180; pos += 1) {
    //her forlælles servoen at den skal antage den vinkel som svare til pos
    servo_9.write(pos);
    // indsat forsinkelse for at sikre at servoen har tid til at dreje til den ønskede position
    delay(15);
  }
  delay(150);
  // loop der flytter servoen fra 180 til 0 grader
  for (pos = 180; pos >= 0; pos -= 1) {
    servo_9.write(pos);
    delay(15); 
  }
  delay(150);
}

Selvom det er muligt at dreje servoen fra en hvilket som helst retning til en anden inden for dens bevægelighed kan det være en fordel at implementere reorienteringen som en gradvis process, idet en servo som f.eks den afbildede SM-s2309S (https://servodatabase.com/servo/springrc/sm-s2309s) drejer 60 grader på 0.12 sekunder ved 4,6 volt, så er det ikke sikkert at det, som servoen driver, kan tåle at dreje med så høj hastighed.

H-bro

Den opbyggede h-bro anvender en motor driver af typen L293D (http://www.ti.com/lit/ds/symlink/l293.pdf). Den har otte pins på hver side, sat op som på følgende billede:

For at den skal fungere skal der løbe strøm til både pin 8 og 16 (Vs og Vss). Pin 8 bruges til at forsyne motoren med, og er dermed det der løber fra komponentens output-pins. Pin 16 er til at forsyne det interne system, og må maksimalt modtage 7 volt. Pin 4, 5, 12 og 13 går alle til jord.
De to enable-pins, 1 og 9, er controllere for hver sin side af komponenten. Det vil sige, at de kan kontrollere om henholdsvis den venstre og den højre side virker. For eksempel når pin 1 er high, vil input 1 og 2, og ligeledes output 1 og 2 være slået til – og når pin 1 er low, er de slået fra.

Resten af komponentens pins består af inputs og outputs. Input-pins bestemmer hvilke output-pins der løber en strøm fra, således at når input 1 er high, så løber der strøm fra output 1, og så fremdeles.

Motor driveren er blevet brugt til at lave en H-bro opstilling, der kan styre hvilken vej robotten skal køre. Den driver de to jævnstrømsmotorer, der drejer hjulene, og alt efter hvilke signaler der sendes fra arduinoen til de forskellige input-pins, vil de to motorer kunne skifte retning, så hvert hjul både kan køre forlæns og baglæns. H-bro opsætningen er lavet i TinkerCad og kan ses på nedenstående billede:

Her kan det ses hvordan de to motorer er forbundet (med grønne ledninger) til motor driverens output-pins på hver deres side. Arduinoen kan nu sende signal til begge enabler-pins (gennem de gule ledninger) for at slå begge sider til, og kan så ved at sende signal til de forskellige input-pins (via de blå ledninger) bestemme hvilke outputs der skal tændes, og dermed hvilken vej strømmen skal løbe igennem motorerne. Alt efter hvilket signal, der skrives til de to enabler-pins, kan arduinoen også kontrollere hastigheden på motoren, ved at regulere hvor meget strøm der løber fra de tændte output-pins.
Derudover er arduinoen også brugt som strømkilde – de røde ledninger leder strøm til de to nødvendige pins (9 og 16) på L293D-komponenten. De sorte ledninger fører tilbage til jord.

Sammenspil mellem hardware og software

Robottens opbygning

For at teste og demonstrere principperne bag den måde både robotten er opbygget på, er der blevet lavet en simulering i programmet Unity, hvortil robotten er overført. Den er blevet lavet af de udleverede prefabs:

  • Robot root, som er den centrale plade der fungerer som fæstningspunkt for alt det andet.
  • Line sensors, som sidder på undersiden af den centrale plade, og simulerer LDR-sensorerne fra TinkerCad – de måler, hvornår robotten kører over den sorte streg. Derfor sidder de også helt fremme, så det opdages hurtigt, når robotten begynder at vige fra ruten.
  • To store motordrevne hjul, der er placeret på hver sin side af den centrale plade, forholdsvist langt bagude, og et lille fritløbende hjul, der sørger for at pladen ikke skraber hen ad underlaget fortil. Det lille hjul sidder lige imellem de to line sensors.
  • En servomotor på toppen af den centrale plade, ligeledes langt fremme og på midten, da det er herpå distance sensoren skal placeres. Derfor er det vigtigt at den, ligesom line sensors, kan registrere vægge tidligt.
  • En ultralydssensor, der sidder på servo-armen, så den kan måle afstande i forskellige retninger – skal bruges til at registrere hvor og hvor langt der er til væggene.
  • Et breadboard, der ikke er visuelt til stede i simuleringen, men som står for arduino-koden og styringen af servo og hjul samt læsning af sensor-signaler, ligesom i TinkerCad-opbygningen.

Dette ender med en model der ser således ud:

Flowchart over robottens opførsel

Der er blevet udarbejdet et flowchart, der illustrerer de forskellige målinger, som robotten foretager, og hvilke beslutninger hver måling vil føre til.

Opbygning af software

Som vist i det ovenstående flowchart er der to forskellige states i robottens opførsel – en state, hvor robotten skal følge en linje, og en hvor robotten skal undgå væggene. Disse to states er meget definerende for, hvordan robottens logik forløber, og det er derfor det første der tjekkes på.

Hvis robotten er i LineFollower staten, tjekker den om der er noget foran den. Hvis der er det, skal den lave en undvigemanøvre ved at dreje til venstre, og derefter køre rundt om objektet ved at bruge det som referencepunkt. Hvis der ikke er noget foran den, tjekker den blot dens LDR-sensorer, for at se om en af dem er ved at ramme stregen – hvis det er tilfældet, så korrigerer den retningen, og ellers fortsætter den blot fremad.
Når robotten rammer et sted, hvor den laver venstresvinget for at undgå et objekt, men derefter opdager at der stadig er en væg foran den, så skifter den til WallAvoider-staten.

Efter robotten har skiftet til WallAvoider-staten, skal den køre tæt ind på den væg der er foran den, og vende siden til. Distance sensoren skal herefter drejes ind mod væggen, så robotten kontinuerligt kan måle afstanden til væggen, som den kører langs. Hvis den måler, at den er ved at komme for langt væk fra væggen, korrigerer den ved at dreje lidt mod højre (ind mod væggen) – og omvendt hvis den er ved at komme for tæt på, drejer den lidt til venstre (væk fra væggen). Hvis den har den ønskede afstand til væggen, skal den blot fortsætte fremad.
Hele kildekoden til arduinoen kan ses her:

#include <Servo.h>

int enabler = 11;
int backRight = 10;
int forwardRight = 9;
int backLeft = 5;
int forwardLeft = 6;
int ldrLeft = A4;
int ldrRight = A5;
int servoPin = 3;
int distanceSensor = 2;

Servo servo;
String state;

void setup()
{
    pinMode(enabler, OUTPUT);
    digitalWrite(enabler, HIGH);

    pinMode(backRight, OUTPUT);
    pinMode(forwardRight, OUTPUT);
    pinMode(backLeft, OUTPUT);
    pinMode(forwardLeft, OUTPUT);
    pinMode(ldrLeft, INPUT);
    pinMode(ldrRight, INPUT);
    servo.attach(servoPin);

    state = "LineFollower";
    servo.write(90);
    delay(2000);

    Serial.begin(9600);
}

void loop()
{
    if (state == "LineFollower")
    {
        Serial.print("Measured distance is: ");
        Serial.println(measureDistance());
        if (measureDistance() < 800 && measureDistance() != 0)
        {
            Serial.println("We measure a wall ahead!");
            stopAll();

            turn90Left();
            delay(1200); // enough delay to turn ~89-91 degrees, with some uncertainty due to Unity
            stopAll();

            servo.write(88);
            delay(2000);
            if (measureDistance() < 1500)
            {
                state = "WallAvoider";
                delay(1000);
            }

            if (state == "LineFollower")
            {
                servo.write(180);
                delay(2000);
                while (measureDistance() == 0)
                {
                    digitalWrite(forwardRight, HIGH);
                    digitalWrite(forwardLeft, HIGH);
                    delay(100);
                }
                stopAll();

                while (measureDistance() < 1000)
                {
                    digitalWrite(forwardLeft, HIGH);
                    digitalWrite(forwardRight, LOW);
                    delay(10);

                    if (analogRead(ldrRight) < 100 || analogRead(ldrLeft) < 100)
                    {
                        break;
                    }
                }
                stopAll();
            }
        }
        else
        {
            if (analogRead(ldrLeft) > 900 && analogRead(ldrRight) > 900)
            {
                Serial.println("We are on the line, drive forwards!");
                driveForwards();
            }
            else if (analogRead(ldrLeft) < 900 && analogRead(ldrRight) > 900)
            {
                turnLeft();
            }
            else if (analogRead(ldrLeft) > 900 && analogRead(ldrRight) < 900)
            {
                turnRight();
            }
            delay(10);
        }
    }
    else
    { // this is when we go to WallAvoider state
        Serial.println("Now in WallAvoider!");
        if (servo.read() != 179)
        {
            servo.write(90);
            delay(250);
            while (measureDistance() > 700)
            {
                driveForwards();
                delay(10);
            }
            turnLeft();
            delay(500);
            stopAll();
            servo.write(180);
            delay(2000);
        }

        if (measureDistance() > 600 || measureDistance() == 0)
        {
            turnRight();
        }
        else if (measureDistance() < 500)
        {
            turnLeft();
        }
        else
        {
            driveForwards();
        }
        delay(10);
    }
}

long measureDistance()
{
    pinMode(distanceSensor, OUTPUT); // Clear the trigger
    digitalWrite(distanceSensor, LOW);
    delayMicroseconds(2);
    // Sets the trigger pin to HIGH state for 10 microseconds
    digitalWrite(distanceSensor, HIGH);
    delayMicroseconds(10);
    digitalWrite(distanceSensor, LOW);
    pinMode(distanceSensor, INPUT);
    // Reads the echo pin, and returns the sound wave travel time in microseconds
    return pulseIn(distanceSensor, HIGH);
}

void turn90Left()
{
    digitalWrite(backLeft, HIGH);
    digitalWrite(forwardRight, HIGH);
}

void turn90Right()
{
    digitalWrite(backRight, HIGH);
    digitalWrite(forwardLeft, HIGH);
}

void turnLeft()
{
    if (state == "LineFollower")
    {
        servo.write(45);
    }
    digitalWrite(forwardLeft, LOW);
    digitalWrite(forwardRight, HIGH);
}

void turnRight()
{
    if (state == "LineFollower")
    {
        servo.write(135);
    }
    digitalWrite(forwardRight, LOW);
    digitalWrite(forwardLeft, HIGH);
}

void driveForwards()
{
    if (state == "LineFollower")
    {
        servo.write(90);
    }
    digitalWrite(forwardRight, HIGH);
    digitalWrite(forwardLeft, HIGH);
}

void stopAll()
{
    digitalWrite(forwardRight, LOW);
    digitalWrite(forwardLeft, LOW);
    digitalWrite(backRight, LOW);
    digitalWrite(backLeft, LOW);
}

Testing i Unity

Arduino-koden er blevet konverteret til C#-kode, så den kan køres i det udleverede Unity-projekt. Der er desuden blevet udleveret en testbane, hvor robotten skal klare nogle forskellige scenarier – det er denne, der benyttes i videoen nedenfor.

Både den konverterede kode og den opbyggede robot-model kan findes på vores github repository: https://github.com/pachr16/OB-HR-PF2-Uge-16-17-Lab-master

Her ses robottens gennemkørsel af banen:

Konklusion

Der er blevet udarbejdet en robot der bliver drevet af to hjul, og som ved hjælp af lyssensorer og en distancesensor kan køre igennem den udleverede testbane uden at ramme nogen vægge. Til at styre hjulene er der blevet lavet en h-bro, så hjulene kan køre i begge retninger. Denne h-bro styres af en arduino, der også læser fra de nævnte sensorer, og står for alt robottens logik.
Dermed kan robotten følge en sort linje rundt i en bane ved at måle på det lys, der kastes tilbage, og desuden skifte til en anden state, hvor den i stedet kan finde vej baseret på de vægge der er i nærheden.

Perspektivering

Da vi ikke har haft mulighed for at bygge robotten fysisk ligger de fleste af de forbedringer som der kunne laves hovedsageligt i programmerings regi. Den måde hvorpå robotten løser opgaven baserer sig på en vis mængde “hardcoding”, der ikke nødvendigvis ville fungere for alle andre typer af testbaner. 
En stor forbedring til robotten ville således være at gøre løsningerne mere generiske, især den metode der bruges når robotten er i WallAvoider-staten vil kunne komme i problemer i baner med venstresving, fordi implementationen følger den højre væg, og derfor ikke kan håndtere disse.
Derudover er obstacle avoidance metoden også baseret meget på hvordan den pågældende bane ser ud og metoden vil komme i betydelige problemer hvis det enten ikke er muligt at køre højre om det objekt som er i vejen, eller hvis objektet er aflangt i stedet for kubisk.

Således kunne disse metoder for robotten have gavn af en mere generisk implementering, for at gøre robotten bedre til at håndtere ukendte baner, ligesom det er tilfældet med LineFollower-staten, hvor robotten umiddelbart vil kunne fungere på ukendte baner, så længe den kun skal følge en streg.

Leave a Reply