Programafvikling i Arduino
For at få Arduinoen til at gøre noget, skal man have lavet et program til den. Programmet er ligesom en bageopskrift; Den beskriver helt konkret, hvad der skal ske, og hvornår det skal ske. Arduinoen udfører 100% det der står i programmet, modsat bageren, der godt kan slække lidt på nøjagtigheden når bageopskriften skal fortolkes…
Grunddele i Programmet
Et Arduino program er som minimum opdelt i to dele:
Del 1 bruges til at lave diverse opsætninger for programmet, og denne del bliver kun afviklet idet Arduinoboardet startes. Del 1 kaldes for setup i selve programkoden.
Del 2 er selve programmet der skal afvikles. Del 2 starter så snart del 1 er afviklet, og afvikles igen og igen, indtil man slukker for Arduinoboardet. Del 2 kaldes for loop i selve programkoden.
På figur 1 ses Arduino IDE når man starter på et nyt program; der er allerede skrevet nogle linjer programkode. Det er “skelettet” til de to dele som programmet som minimum skal deles op i, og man kan se, at der bruges Tuborg-tegn ved hver del: { }
. Disse Tuborg-tegn beskriver, at der henholdsvis starter {
og slutter }
en del eller et “afsnit”.
Figur 1. Startvindue i Arduino IDE.
Man kan på figur 1 også se, at der er to linjer med almindelig læsbar engelsk tekst. Disse to linjer starter begge med // og disse to slash-tegn betyder, at alt hvad der står til højre på den linje, ikke bliver tolket som programkode, men i stedet for kan anvendes som kommentarer og beskrivelser af selve programkoden.
Gennemgang af programeksemplet Blink
Det følgende er en gennemgang af programeksemplet Blink, der hentes i Arduino IDE under:
Fil - Eksempler - 01. Basics - Blink[1]. Programmet får en lysdiode, koblet til portben 13, til at blinke.
Hele programmet er gengivet her, og de forskellige afsnit i koden er markeret med farver:
/*
Blink
Turns on an LED on for one second, then off for one second, repeatedly.
Most Arduinos have an on-board LED you can control. On the UNO, MEGA and ZERO it is attached to digital pin 13, on MKR1000 on pin 6. LED_BUILTIN is set to the correct LED pin independent of which board is used.
If you want to know what pin the on-board LED is connected to on your Arduino model, check the Technical Specs of your board at https://www.arduino.cc/en/Main/Products
This example code is in the public domain.
modified 8 May 2014
by Scott Fitzgerald
modified 2 Sep 2016
by Arturo Guadalupi
modified 8 Sep 2016
by Colby Newman
*/
// the setup function runs once when you press reset or power the board
void setup() {
// initialize digital pin LED_BUILTIN as an output.
pinMode(LED_BUILTIN, OUTPUT);
}
// the loop function runs over and over again forever
void loop() {
digitalWrite(LED_BUILTIN, HIGH); // turn the LED on (HIGH is the voltage level)
delay(1000); // wait for a second
digitalWrite(LED_BUILTIN, LOW); // turn the LED off by making the voltage LOW
delay(1000); // wait for a second
}
Dokumentationsdel
Den første del markeret med rødt ovenfor
er en beskrivende tekst om selve programkoden. Den starter med /*
og afsluttes med */
. Alt det der står mellem disse to “koder”, bliver ikke tolket som programkode, og man kan dermed skrive lige hvad man ønsker.
Det er altid en god idé at gøre sig den ulejlighed at skrive sådan en dokumenterende tekst i toppen af sit program, da man så har et godt overblik over hvad det egentlig er, programmet udfører. Især, hvis andre skal se ens kode.
Setup() del
Den næste del markeret med blåt ovenfor
er setup()
delen. Det er den del, der kun bliver afviklet én gang - lige når man starter (eller genstarter) sit Arduinoboard. Der er kun én linje med programkode:
pinMode(LED_BUILTIN, OUTPUT);
Denne linje fortæller, at man ønsker at bruge portbenet LED_BUILTIN
som et Output. Det er umiddelbart lidt svært at se, hvilket portben der er tale om, men det er portben 13. Det vil sige, at man i stedet for kunne have skrevet:
pinMode(13, OUTPUT);
Man skal for hvert portben man ønsker at anvende, skrive en linje kode hvor man med funktionen pinMode()
definerer om portbenet skal være OUTPUT
eller INPUT
.
Loop() del
Den tredje del markeret med grønt ovenfor
er loop()
delen. Denne del gentages igen og igen, så længe der er strøm til Arduinoboardet. Der er fire linjer programkode, og kigger man på dem, ses det, at linje 1 og 3 ligner hinanden meget, mens linje 2 og 4 er identiske.
Hvis vi starter med linje 1 af loop()
delen, så står der:
digitalWrite(LED_BUILTIN, HIGH);
Funktionen digitalWrite()
bruges til at få et givent portben til at tænde eller slukke. Her tændes der så for lysdioden der er koblet til portben 13. Husk på, at LED_BUILTIN
betyder portben 13.
Efter portben 13 er blevet tændt, skal der holdes en pause, inden portbenet igen slukkes. Til det formål bruges funktionen delay()
, der får Arduino’en til at holde pause i det antal millisekunder, som man har specificeret. Det vil sige, at programkoden:
delay(1000);
får Arduino’en til at holde 1000 millisekunders pause (dvs. pause i 1 sekund).
Derefter slukkes for portben 13 i linje 3 af loop()
delen:
digitalWrite(LED_BUILTIN, LOW);
Eneste forskel i forhold til linje 1, hvor portben 13 blev tændt, er at der her står LOW
i stedet for HIGH
. Så LOW
slukker og HIGH
tænder.
Fjerde linje er magen til anden linje, og er igen et delay, der får Arduino’en til at holde 1000 milllisekunders pause.
Når fjerde programkodelinje i loop()
er blevet udført, startes forfra ved første programkodelinje i loop()
, og dermed har vi forløbet:
Tænd lysdiode → Hold 1 sekund pause → Sluk lysdiode → Hold 1 sekund pause →
Tænd lysdiode → Hold 1 sekund pause → Sluk lysdiode → Hold 1 sekund pause →
Tænd lysdiode → Hold 1 sekund pause → Sluk lysdiode → Hold 1 sekund pause →
Tænd lysdiode → Hold 1 sekund pause → Sluk lysdiode → Hold 1 sekund pause →
Tænd lysdiode → Hold 1 sekund pause → Sluk lysdiode → Hold 1 sekund pause →
Tænd lysdiode → Hold 1 sekund pause → Sluk lysdiode → Hold 1 sekund pause →
Tænd lysdiode → Hold 1 sekund pause → Sluk lysdiode → Hold 1 sekund pause →
… og så videre, lige indtil man tager strømmen fra Arduinoboardet.
Programafviklingshastighed
Når man nu laver et program der kan blinke med en lysdiode, så kan man måske få tanken, at lysdioden kommer til at blinke uden man bruger delay()
funktionen. Hvis man nu forkortede loop()
delen fra Blink programmet til at se sådan ud:
void loop() {
digitalWrite(LED_BUILTIN, HIGH);
digitalWrite(LED_BUILTIN, LOW);
}
… så vil det se ud til, at lysdioden lyser hele tiden. Det skyldes, at tiden det tager at udføre en linje kode, er meget meget kort. Helt præcist afhænger den af hvilken funktion eller kommando man skal have udført, men et digitalWrite()
funktionskald tager omkring 4 µsek. at udføre [2]. Det går derfor så hurtigt at tænde og slukke for lysdioden, at det menneskelige øje slet ikke når at detektere det. I stedet for vil man se lysstyrken let dæmpet, da lysdioden halvdelen af tiden er slukket. Mere om dette i af afsnittet Pulse Width Modulation (PWM) output i dokumentet Tips til anvendelse af ben på Arduino UNO.
Opdeling af programmet
Når man skriver et lidt mere kompliceret program end det gennemgåede, så vil man stadig have de to grundlæggende dele setup()
, der som beskrevet bliver kaldt én gang efter reset og loop()
, der kaldes igen og igen indtil Arduinoen slukkes.
For at holde styr på de forskellige dele af koden kan det være en god ide at dele koden op i funktioner, som beskrevet i Dokumentation med Kode og Flowchart, hvor opdelingen laves ud fra de forskellige ting som programmet skal løse.
Dette skal man ikke forstå som at programmet laver forskellige ting på samme tid - det kan et arduino-program IKKE. Man kan godt programmere det sådan at det ser ud som om tingene sker samtidigt. Det er nærmere beskrevet i Tid og Samtidighed i Software, men som det også er dokumenteret der, så er det ud fra at Arduinoen kan lave ting rigtigt hurtigt, og at alle ting tjekkes meget hurtigt efter hinanden, at det ser ud som om tingene sker samtidigt.
Sekventiel afvikling af programkode
Det at programkode afvikles en linje ad gangen, og at en programlinje afsluttes før den næste afvikles betegnes som sekventiel[3] program-afvikling.
At noget er sekventielt siger netop at det afvikles i en sekvens, altså et trin ad gangen. Vender vi tilbage til Blink-eksemplet, så er det jo netop at det første der sker efter reset er, at setup()
kaldes hvor et ben sættes til output. Herefter kaldes loop()
igen og igen, hvor loop()
består af sekvensen tænd lysdiode - 1 sekunds pause - sluk lysdiode - 1 sekund pause. Der kan altså ikke ske andet i det program, så længe det er kodeindholdet.
Hvis vi tænker os at vi sætter følgende funktion ind før loop()
, så vil dette ikke have nogen effekt på programmets opførsel.
// Funktion der tænder en lysdiode ud fra en indgang
void kontaktSense() {
if (digitalRead(kontaktInput) == HIGH) {
digitalWrite(LED_2, HIGH);
} else {
digitalWrite(LED_2, LOW);
}
}
Forklaringen på at det ikke har nogen effekt på programmets opførsel er, at funktionen kontaktSense()
aldrig bliver aktiveret, så koden kommer altså ikke ind i program-sekvensen.
Hvis vi så sætter et kald til kontaktSense()
ind i loop()
et sted, fx. før digitalWrite(LED_BUILTIN, HIGH);
, så får vi en oplevelse af af programmet måske nok virker, men ikke lige som vi gerne ville have det til, nemlig at LED_2 tænder når der trykkes på kontakten forbundet til kontaktInput-benet.
Forklaringen på dette er at afviklingen af funktionen kontaktSense()
sker meget hurtigt set i forhold til de to gange 1 sekunds pause der ellers afvikles i loop()
. Det vi vil se er at vi skal holde kontakten nede indtil funktionen bliver kaldt, og der kan i værste tilfælde gå 2 sekunder. Når så først LED_2 er tændt, så vil den være tændt i ca. 2 sekunder inden den slukkes igen, selvom vi slipper kontakten hurtigt.
Hvordan dette løses er beskrevet i Tid og Samtidighed i Software, det kræver mere forståelse.
Interrupt
Nogle gange har man brug for at en del af programkoden skal kunne reagere rigtigt hurtigt, hvilket man kan gøre ved hjælp af interrupt [4] der er det engelske ord for at afbryde, altså noget der kan afbryde den sekvens der kører.
I Arduinoen er det specielt elektriske signaler der kan programmeres til at give interrupt [5] ved at man skriver en speciel interrupt-rutine, der ikke kaldes direkte fra koden, men som hægtes på et interrupt med attachInterrupt()
, som så sørger for at kalde funktionen umiddelbart som det angivne ben går højt eller hvad man nu har angivet.
Denne form for programmering hører til det mere avancerede, men er med til at forklare hvordan sekvens fungerer og hvordan det kan omgås.
Til forklaringen omkring interrupt hører også, at tidsfunktionen millis()
bliver vedligeholdt ved hjælp af interrupt, samt opsamlingen af karakterer fra den serielle port sker ved hjælp af interrupt.
Der er mere information omkring anvendelse af interrupt i dokumentet Tid og Samtidighed i Software.
Multitasking
For at få det til at se ud som om en computer afvikler flere programmer samtidigt, så laver man et styresystem, der kan håndtere flere programmer på en gang og fordele tiden mellem dem [6]. Dette kan etableres på mange forskellige måder, men det kræver både meget processorkraft og en del hukommelse.
Arduinoen kan ikke håndtere egentlig multitasking, men man kan få den til at gøre noget der minder om det [7] som det er beskrevet i kilden og de 6 efterfølgende sider.
Opgaver
Udvid programmet Blink, så det kan blinke med 4 lysdioder, i stedet for kun 1 lysdiode. Brug portbenene: 10, 11, 12 og 13 til formålet, og lav følgende versioner af blinkemønstre:
- Alle 4 lysdioder blinker sammen (alle 4 tændt → Hold 1 sekund pause → alle 4 slukket → Hold 1 sekund pause).
- Blinker parvis.
- Løbelys, hvor kun én lysdiode er tændt ad gangen. F.eks. som lysstriben på KITT fra Knight Rider TV serierne fra 80’erne.