Arduino Tidsforhold

Fra Holstebro HTX Wiki
Skift til: navigering, søgning

Hvis en Arduino skal reagere på et taste-input og vise noget i et display, så er der ikke den store grund til at bekymre sig om hvordan tidsforholdene er omkring koden, da det typisk er en opgave hvor processoren i Arduinoen har pænt overskud til at nå at afvikle koden uden problemer.

Laver man andre mere tidskritiske ting, hvor kode skal afvikles med præcise tidsforhold eller Arduinoen skal nå at afvikle mange ting, så er det godt at man kan skrive kode der er effektiv og hvor man har kendskab til hvor lang tid det tager at afvikle de forskellige typer kode, dels for at kunne skrive koden optimalt og dels for at kende den timing der kommer ud af det.

En simpel måde at måle tidsforhold på

Specielt hvis det er ting der tager længere tid, så kan en god måde at måle tiden på være ved at lade softwaren selv registrere tiden. Det kan gøres med en kode der ligner følgende:

  unsigned long start, slut;

  start = micros();
  // Her placeres den kode man ønsker at måle tiden for
  slut = micros();
  Serial.print(slut - start);  // Udskriv den aktuelle måling

Der er to grundlæggende problemer ved denne metode:

  • Metoden micros() har en opløsning på 4μs, så ved korte tidsrum kan det give målefejl, og i nogle tilfælde betragtelige målefejl, da en maskininstruktion på en Arduino UNO tager 0,0625μs.
  • Selve kaldet til micros() tager ca. 3,0μs, så selvom der kaldes to gange, så må den tid der bliver lagt til være de 3,0μs, da det er samme tid inde i kaldet at selve tidsregistreringen foregår.

Et eksempel på anvendelse er placeret i koden Tidsmaal_Simpelt i følgende ZIP-fil.

Større præcision ved at måle mange gange

For at eliminere ulempen ved opløsningen på micros() og til dels også kaldetiden til micros() kan man lægge det man tester ind i en for-løkke der fx. kører 10000 gange, så man på den måde får opløsningen på micros() reduceret til at det er 0,0004μs for hver enkelt af instruktionerne der testes. Dette kan gøres med følgende kode:

#define antal 10000

  unsigned long start, slut;

  start = micros();
  for (int n = 0; n < antal; n++) {
    // Her placeres den kode man ønsker at måle tiden for
  }
  slut = micros();
  Serial.print((float) (slut - start) / antal);  // Udskriv den aktuelle måling

Dette har så den ulempe, at for-løkken også tager tid at afvikle, så man må tage højde for dette ved at trække den tid som for-løkken tager fra, man skal bare være sikker på at man har den præcise tid for løkkens afvikling.

Et eksempel på anvendelse er placeret i koden Tidsmaal_Loop i følgende ZIP-fil.

Hvis man ønsker at gå efter endnu større præcision, så kan man summere tiderne for loopets afvikling sammen og tælle hvor mange gange man har gjort det, og på den måde beregne gennemsnittet.

Et eksempel på anvendelse er placeret i koden Tidsmaal_Loop_Korrigeret i følgende ZIP-fil.

Endnu større præcision ved at måle forskel mellem mange målinger

En måde at være mere sikker på sine målinger, er ved at man laver en måling af lidt tilfældig kode med ovenstående metode, og derefter tilføjer den kode man faktisk vil måle på, og så registrerer hvor stor forskellen er ved at indføre den ekstra kode.

Fejlmuligheder ved kodemåling

Når man ønsker at foretage grundlæggende målinger af kode-tider med at bruge processorens egne måletider, så er det normalt relativt simple koder man måler på. Her skal man sikre sig at compileren ikke snyder en, da den faktisk er ret god til at optimere kode. Hvis man fx. vil teste hvor lang tid det tager at sætte en variabel til 0 og man lægger det ind i et for-loop uden anden kode, så kan compileren resonere at det ikke er nødvendigt at gøre det 10000 gange, men at det er nok at gøre det én gang, så hvis man regner med at kodelinjen er gennemført 10000 gange og at der ligger et for-loop udenom, så bliver man alvorligt snydt ved at for-loopet er væk og variablen bare sættes til 0 en eneste gang.

En måde man kan komme uden om dette er ved at erklære variablen for volatile som vist i følgende kode:

#define antal 10000

  unsigned long start, slut;
  volatile byte test;

  start = micros();
  for (int n = 0; n < antal; n++) {
    test = 0;
  }
  slut = micros();
  Serial.print((float) (slut - start) / antal);  // Udskriv den aktuelle måling

En anden ting der kan drille er beskrevet ved Fejlkilder ved måling

Pas på med bare hovedløst at erklære alle variabler volatile. Dette kan give helt anderledes målinger. Ved nogle konstruktioner som fx. for-loop, der vil tiden nærmest fordobles ved at sætte tællevariablen volatile. Dette er ikke analyseret nærmere, men det formodes at det skyldes, at koden forhindrer interrupt at gå ind i de enkelte dele af loop-funktionaliteten ved at slå interruptet fra.

Måling af tid med oscilloscop

En anden indgangsvinkel til at måle tiden på kode-afvikling er ved at bruge et oscilloscop til at bestemme hvor lang tid noget kode tager.

Man kunne umiddelbart tænke at en kode som følgende ville kunne give et output med 10 meget hurtige pulser:

for (int n = 0; n < 10; n++) {
  digitalWrite(7, HIGH);
  digitalWrite(7, LOW);
}
delay(1);

En måling der viser at der kommer 10 pulser:
Måling af den ovenstående kode der viser de 10 pulser
Måling af den ovenstående kode der viser de 10 pulser

Det vil dog overraske en, at digitalWrite faktisk tager ca. 3,38μs for hvert kald, svarende til 54 maskininstruktioner.

Koden til dette ligger som Port_DW i Denne ZIP-fil

En måling der efterviser dette er vist her:
Måling af den ovenstående kode på arduinoens ben 7
Måling af den ovenstående kode på arduinoens ben 7

Til præcise målinger af tid er det for besværligt at arbejde med.

Måling med hurtigere output-funktion

Man kan heldigvis manipulere direkte med port-registrene[1], så det går væsentligt hurtigere at tænde og slukke for portben.

Hvis man ønsker manipulere med kun et ben, så er man nødt til at lave bitwise AND og OR funktioner[2] [3].

Dette kunne realiseres med følgende kode:

for (int n = 0; n < 10; n++) {
  PORTD |= B10000000;  // Sætter portben 7 høj uden at berøre resten af porten
  PORTD &= B01111111;  // Sætter portben 7 lav uden at berøre resten af porten
}
delay(1);

Hvis man måler efter, så vil man konstatere at den enkelte bit-manipulation tager 0,125μs, altså kun 2 maskininstruktioner - dette er væsentligt mere effektivt.

Koden til dette ligger som Port_andor i Denne ZIP-fil

En måling der efterviser dette er vist her:
Måling af den ovenstående kode på arduinoens ben 7, ved bitmanipulation af PORTD
Måling af den ovenstående kode på arduinoens ben 7, ved bitmanipulation af PORTD

optimering af hurtigere output-funktion

Hvis man kan leve med at hele porten bliver sat (pin 0 - 7) på en gang, så kan det faktisk gøres hurtigere, nemlig med kun 1 maskininstruktion.

Dette kunne realiseres med følgende kode:

for (int n = 0; n < 10; n++) {
  PORTD = B10000000;  // Sætter portben 7 høj, resten af porten lav
  PORTD = B00000000;  // Sætter portben 7 lav og resten af porten lav
}
delay(1);

Man skal lige være opmærksom på at man ødelægger muligheden for Seriel kommunikation, da man også skriver på de ben der anvendes til seriel kommunikation.

Hvis man måler efter, så vil man konstatere at den enkelte bit-manipulation tager 0,0625μs, altså kun 1 maskininstruktion - mere effektivt kan det ikke blive.

Koden til dette ligger som Port_direct i Denne ZIP-fil

En måling der efterviser dette er vist her:
Måling af den ovenstående kode på arduinoens ben 7, ved skrivning til PORTD
Måling af den ovenstående kode på arduinoens ben 7, ved skrivning til PORTD

Fejlkilder ved måling

Selvom man syntes man har styr på alle tidsforhold når man måler, og det er med oscilloscop eller med arduinoens egne tidssytemer, så kan der stadig være ting der kan drille og give afvigelser i målingerne.

Ved måling med oscilloscop kan det være afvigelse i krystal-frekvensen, men den burde ligge inden for 1%, og det kan være afvigelse i oscilloscopets måling[4], men det vurderes til også at være inden for 1-2%, mens den værste fejlkilde nok er fejlaflæsninger på tiden.

En anden ting der kan drille er at Arduinoen afvikler anden kode man ikke har styr på. En ting der afvikles er et interrupt der skal vedligeholde micros() og millis() - det er ikke hvert 4. mikrosekund det kommer, umiddelbart kunne man tænke at det kun skulle være ca. 4 gange i sekundet, da der ligger hardware-timere der tæller op inde i processoren, og interrupter når den løber over, dette er hvis det er en 16 bit timer der anvendes. Ud fra test ser det dog ud til at det er en 8 bit timer der anvendes, så interruptet sker hvert millisekund. Dette vil give en målefejl på ca. 0,625%.

Det tyder på at det er ved den software-mæssig måling af koden at det kan drille, specielt ved mange målinger når man laver en gennemsnits-beregning.

Med lidt held kan man fange det tidspunkt (det tager en del forsøg), og så kan man konstatere at dette interrupt afbryder den normale kodeafvikling i ca. 6,24μs.

Tidsmåling af hurtigt loop, der rammes af et interrupt
Tidsmåling af hurtigt loop, der rammes af et interrupt

For at se om det er dette der kan drille, så kan man slå interruptet fra mens der måles. Dette er dog ikke helt ukompliceret, da det lige netop er interruptet der holder styr på den tid man måler med. Det betyder altså at man ikke må have interruptet slået fra i længere end maksimalt et millisekund. Dette kunne realiseres med følgende kode:

#define loops 1000
  unsigned long start, slut;

  noInterrupts();
  start = micros();
  for (int n = 0; n < loops; n++) {
    tal = 17;  // Det er denne programlinje der måles på
  }
  slut = micros();
  interrupts();
  Serial.print(slut - start);  // Udskriv den aktuelle måling

Her er det en simpel tilskrivning at en byte-variabel der måles på. Ved at måle på denne måde, så kommer man meget tættere på 0,125μs end hvis man ikke har slået interruptet fra under målingen.

Et eksempel på anvendelse er placeret i koden Tidsmaal_Uden_Interrupt i følgende ZIP-fil.

Referencer

  1. Arduinos reference omkring direkte port-manipulation
  2. Arduino reference omkring bitwise AND
  3. Arduino reference omkring bitwise OR
  4. Produktoversigt over PCSU1000 oscilloscop