Tid og Samtidighed i Software

Fra HTX-Arduino
Skift til: navigering, søgning
Video med forklaring til kapitlet

Når man programmerer en microcontroller, så bliver det i en del tilfælde vigtigt, at det tager tid at afvikle koden, og nogle gange kan det blive kritisk om man kan nå at afvikle alt den kode man skal. De største problemer man normalt støder på er dog det at man gerne vil have Arduinoen til at lave flere ting på en gang som for eksempel det at måle en værdi, håndtere indtastning fra et tastatur, vise noget i et display og måske endda holde rimeligt styr på tiden.

Problemet er at Arduinoen ikke har et styresystem, der kan håndtere multitasking [1] som en normal PC har det.

Kodeeksempler

Til dette kapitel er der nogle kodeeksempler man kan anvende. De er samlet i en ZIP-fil.

Håndtering af tid

Man kan godt få Arduinoen til at håndtere tid, men måden man gør det på kan være mere eller mindre simpel.

Hvis man ønsker at kunne håndtere det korrekte klokkeslæt uden at der er for store afvigelser, så skal man anvende et Real Time Clock [2] modul, som ikke ligger i Arduinoen.

delay()

Den simple måde at indføre tid i Arduino programkoden er ved at bruge delay() [3], der venter i et antal millisekunder (tusindedele af sekunder); Det kan gøre at afviklingen holder en pause i for eksempel et halvt sekund med delay(500);

Så længe der ikke skal udføres andet i koden, så er det ganske fint, men problemet er at koden ikke kan lave andet i det halve sekund der ventes, og i den tid kan brugeren faktisk have trykket på en knap, som man bare ikke ser i koden. Det bliver endnu mere problematisk, hvis man har programmer der skal vente i længere tider.

Hvis man ønsker korte delays (under et millisekund), så er der også delayMicroseconds() [4] som holder pause i et antal mikrosekunder (milliontedele sekunder).

Et problem med at bruge delay() er at selve kaldet tager den tid man angiver, men den resterende kode tager også tid. Det kan man selvfølgelig bare kompensere for ved at afkorte delayet tilsvarende, men det gør det svært at vedligeholde koden, og det kan være kode der kan tage forskellig tid, så man faktisk ikke kan regne det ud/eksperimentere sig frem.

millis()

Funktionen millis() [5] angiver hvor mange millisekunder der er gået siden Arduinoen vågnede fra reset. Tiden bliver vedligeholdt i baggrunden (interrupt), så den vil forblive præcis, så længe man ikke slår interruptet fra.

Det man kan bruge det til er at man har en variabel man sætter i forhold til værdien i millis(). På den måde bliver man uafhængig af hvor lang tid der resterende kode tager at afvikle. Man kan selvfølgelig ikke sikre at man udfører noget på præcise tidspunkter, men man kan lave det sådan at det sker på ca. det rigtige tidspunkt, men også sådan at der kompenseres for den ekstra tid.

Herunder ses et eksempel på en kode der vil kunne få noget til at ske hvert 5. sekund. Det forudsætter selvfølgelig, at den resterende kode kan afvikles på under 5 sekunder (det vil være en god ide at undgå delay). Hvis man ikke når forbi if-sætningen hvert millisekund, så kan der selvfølgelig forekomme små udsving fra det præcise tidspunkt, men fordi man lægger præcist 5 sekunder til sin tid-variabel hver gang, så vil det være kompenseret næste gang man kommer forbi if-sætningen.

unsigned long tid = 0;  // Udfør første gang lige efter reset
void loop() {
  if (tid < millis()) {
    tid += 5000;
    // Her placeres koden der skal udføres hvert 5. sekund
  }
  // Her placeres den resterende kode
}

Man skal være opmærksom på at millis() [5] starter forfra fra 0 efter ca. 50 dage. Grunden til dette er at millis() returnerer et tal af typen unsigned long, der er lagret i 32 bit, og derfor går fra 0 til 4.294.967.295. Det er derfor vigtigt at man erklærer sin tid-variabel af typen unsigned long. Hvis man fx bruger typen int til tid-variablen, så vil koden virke indtil variablen er 30.000, altså de første 30 sekunder. Herefter bliver tid-variablen negativ, da den ikke kan rumme mere end 32.767, så den vil mindre end tallet returneret fra millis(). Koden vil så tro at der er gået 5 sekunder hver gang den kommer rundt.

Den ovenstående kode vil fungere fint op til de 50 dage. Lige når millis() starter forfra på 0 vil der komme en lille unormalitet fordi de 5000 ikke går op i 4.294.967.296. Det vil betyde at der lige ved overløb kommer et større antal gennemløb hvor der ikke går 5000 ms mellem den kode der skal udføres hvert 5. sekund, fordi tid-variablen vil være større, når millis() returnerer 0. Det vil lægge 5000 til tid-variablen, der så løber over og får et tal under 5000, så de mange næste gennemløb vil blive kortere end 5 sekunder. Når millis() så løber over, så vil der komme en lang pause, da tid er talt langt frem, men nøjagtigheden vil blive reetableret. Hvis man kan leve med at det sker, så virker koden, den “låser” ikke.

En bedre måde at håndtere det på er ved følgende kode:

unsigned long startTid = 0;  // Udfør første gang 5 sekunder efter reset
const unsigned long interval = 5000;
void loop() {
  if ((millis() - startTid) > interval) {
    startTid += 5000;
    // Her placeres koden der skal udføres hvert 5. sekund
  }
  // Her placeres den resterende kode
}

Forskellen er at beregningen (millis() - startTid) vil være korrekt, uanset at startTid ikke er løbet over og millis() er løbet over, da resultatet er lagt i en unsigned long inden det vurderes, og dermed er overløbet fjernet, og man kan sammenligne med interval på 5000.

micros()

Har man brug for en finere opløsning på tidsmålingen, så kan man anvende micros() [6], der fungerer på samme måde som millis(), blot at de giver tiden i microsekunder (milliontedele sekunder) i stedet for millisekunder.

Overvejelserne med datatyper er de samme som ved millis, og man skal passe på med overflow på samme måde - det sker bare allerede efter ca. 70 minutter. En enkelt ting der er forskellig er at tiden springer med 4 mikrosekunder ad gangen, så det er den mindste måleenhed man kan forvente.

Samtidighed i software

Når man arbejder med software der skal kunne reagere på forskellige ydre hændelser, så kommer man ind i overvejelsen omkring samtidighed i softwaren.

Problemet er, at programkode afvikles sekventielt, altså at kodelinjerne afvikles efter hinanden i den rækkefølge koden angiver - der kan altså ikke reelt ske noget samtidigt i softwaren (med mindre man anvender flere processorer parallelt). Man kan dog anvende forskellige metoder til at få noget der for brugeren minder om samtidighed.

Polling

En måde at håndtere det på er ved at tjekke om de forskellige hændelser skal serviceres, og hvis de ikke skal, så løbe videre til at tjekke næste hændelse, og ellers ikke lave andet i loop(). Det er vigtigt at man ikke anvender delay() i serviceringen af hændelserne, da det vil sinke processen.

Strukturen i koden til polling ser ud som følger: void loop() {

 if (checkProces1()) {
   // Her placeres koden der servicerer hændelse 1
 }
 if (checkProces2()) {
   // Her placeres koden der servicerer hændelse 2
 }
 if (checkProces3()) {
   // Her placeres koden der servicerer hændelse 3
 }
 // Her kan placeres kode der skal udføres i hver loop,
 // men det er sjældent hensigtsmæssigt

}


På denne måde vil man komme hurtigst muligt forbi alle processer, og hvis man skriver sin kode til at håndtere hændelserne effektivt, så vil ingen hændelser komme til at vente unødvendigt lang tid.

Grov tidshåndtering i polling

En simpel måde at lave en form for tidshåndtering når man anvender polling er ved at indføre et kort delay() i loop() og så tælle sig frem til de tider man skal bruge.

Teknikken giver ikke en særlig præcis tidshåndtering, men til noget hvor man for eksempel vil opdateret sit display 3 gange i sekundet, der er tidsmålingen ganske fin, og den kan give en god forståelse af pollingprincippet.


Strukturen i koden til polling med delay() som tidsmåling ser ud som følger:

int tid = 0;
void loop() {
  if (checkProces1()) {
    // Her placeres koden der servicerer hændelse 1
  }
  if (checkProces2()) {
    // Her placeres koden der servicerer hændelse 2
  }
  if (checkProces3()) {
    // Her placeres koden der servicerer hændelse 3
  }
  delay(10);
  tid++;
  if (tid > 50) {
    tid = 0;
    // Her placeres kode der skal serviceres hvert halve sekund (50 loops af 10 ms)
  }
}

Teknikken er ikke den bedste, da de 10 ms kan være for lang tid at vente på check af nogle hændelser - for eksempel at modtage kommunikation kan være mere tidskritisk. Der er også den ulempe, at den ikke er særlig præcis fordi delay(10) tage 10 ms, men den resterende kode tager også tid, og det kan være svingende, alt efter hvilke hændelser der skal håndteres i loopet

Teknikken kan dog også være en fordel, hvis hændelserne helst ikke skal tjekke alt for tit. Det kan for eksempel være et tryk på en kontakt, der kan lave prel [7], der kan det være en fordel at der ikke testes alt for tit.

Finere tidshåndtering i polling

Hvis man har behov for en bedre håndtering af tiden, end det man kan opnå ved hjælp af delay(). Her bruger man millis() på samme måde som vist i afsnittet om millis(). Det har den fordel at tiden bliver opdateret i baggrunden ved hjælp af interrupt, så tiden bliver udmålt med den præcision som oscillatorfrekvensen på Arduinoen har (normal garanteret inden for 1%), men i praksis ligger den faktisk pænt under 0,1 promille.

Som omtalt ved millis() er det vigtigt af man gemmer sin tid i en variabel af typen unsigned long, da den ellers kommer i konflikt med at variablen løber over inden millis() området gør det.

Strukturen i koden til polling med millis() som tidsmåling ser ud som følger:

unsigned long tid1 = 0;
unsigned long tid2 = 0;
void loop() {
  if (checkProces1()) {
    // Her placeres koden der servicerer hændelse 1
  }
  if (checkProces2()) {
    // Her placeres koden der servicerer hændelse 2
  }
  if (checkProces3()) {
    // Her placeres koden der servicerer hændelse 3
  }
  if (millis() > tid1) {
    tid1 += 500;
    // Her placeres kode der skal serviceres hvert halve sekund
  }
  if (millis() > tid2) {
    tid1 += 10000;
    // Her placeres kode der skal serviceres hvert tiende sekund
  }
}

Som det vises i eksemplet her, så kan man fint håndtere forskellige tidsintervaller ved at have forskellige tidsvariabler, og de behøves ikke at gå op i hinanden - det vil faktisk være en fordel hvis de ikke gør det, fordi så servicerer den ikke i samme gennemløb, og dermed rammer serviceringen mere præcist.

Anvendelse af interrupt Svaert.png

Umiddelbart understøtter Arduinoens system kun en enkelt form for interrupt, nemlig det kanttriggede interrupt på portben 2 og 3. Det kan så indstilles til både at virke på stigende, faldende og alle kanter. Princippet er at man skriver en rutine der skal servicere det der skal ske når interruptet kommer, og så tilknytter interruptet til benet med attachInterrupt() [8].

Man kan anvende andre interrupts ved at skrive direkte i Arduinoens registre, men det er en lidt farefuld affære, da en del af de ting der foregår i Arduinoen er baseret på at bootloaderen har styr på nogle interrupts.

  • millis() og micros() er drevet af timerinterruptet, så hvis man ændrer på de, så vil det ikke virke korrekt længere.
  • Serial-modulet anvender interrupt både til at sende og modtage karakterer, så det er farligt at pille ved det, hvis man vil bruge Serial-modulet. Der er dog muligheden at anvende serialEvent() [9] til at hente modtagne karakterer, men den serviceres kun hver gang loop() er gennemført.
  • SoftwareSerial, Servo, Tone og andre indbyggede ting med tidsstyring anvender et andet tidsinterrupt til at styre tiden med

Arbejder man med tidskritisk kode, så skal man være opmærksom på at den serielle port og millis() afbryder den kørende programkode en gang imellem kortvarigt, så man vil kunne opleve tidsmæssige forstyrrelser i sin programkode. Hvis man ikke kan leve med det, så kan man slå interruptet fra [10], men man skal kun gøre det kortvarigt, for ellers ødelægger man alle de ting der bliver drevet af interrupt.

Referencer