← Kursens material Tid utan delay · område 3 Område 3

Område 3 · lektion 13

Tid utan delay

I guiden om digital ingång stod att delay(20) inte var den slutgiltiga lösningen och att den riktiga hör hemma här. Det här är den. Den handlar om ett enda uttryck – millis() - forra – som är rätt av ett skäl de flesta handledningar på nätet inte förklarar, och några av dem får fel.

Mål

Genomgång

Vad delay() faktiskt gör

delay(500) ser ut som en paus. Det är det inte. Det är en tom slinga: kortet står och kollar sin egen klocka om och om igen tills 500 millisekunder har gått, och gör ingenting annat under tiden. Ingen digitalRead() körs. Ingen utgång ändras. Ingen rad i din loop() nås.

Hur mycket är det som inte händer? Kortet går på 16 MHz, alltså 16 miljoner klockcykler i sekunden. Ett varv i en enkel loop() som läser en pinne och sätter en utgång tar några mikrosekunder – räkna med 5 µs. Under en enda delay(500) hade loopen alltså hunnit varvas

500 ms / 5 µs = 0,5 s / 0,000005 s = 100 000 varv

Hundratusen tillfällen att läsa en knapp, kastade bort. Och det är just knappen som drabbas: trycker du i ungefär en tiondels sekund, och koden bara tittar en gång per halvsekund, är chansen att den ser trycket

100 ms / 500 ms = 0,2 = 20 %

Fyra tryck av fem försvinner. Det är inte ett kontaktfel, inte en trasig knapp och inte kontaktstuds. Det är delay().

Två tidsaxlar över varandra. Den övre, märkt delay, har täta streck som avbryts av ett brett skuggat fält över mitten av axeln. En pil märkt tryck pekar ned mitt i det skuggade fältet. Den nedre axeln, märkt millis, har lika täta streck hela vägen utan avbrott, och samma pil märkt tryck pekar ned på ett av strecken. delay() millis() tryck tryck

Kortet är inte helt dött under delay() Avbrotten fortsätter gå i bakgrunden. Det är därför millisekundräknaren tickar vidare även inuti en delay(), och det är därför tecken som kommer in på serieporten oftast inte tappas bort – de samlas i en mottagningsbuffert som rymmer 64 byte, och en tillräckligt lång delay() hinner fylla den. Men din kod – allt du skrivit i loop() – står stilla. Det är den skillnaden som spelar roll i ett bygge.

millis(): en räknare som aldrig stannar

millis() lämnar antalet millisekunder sedan kortet startade. Den räknar av sig själv, i bakgrunden, oavsett vad din kod håller på med. Anropet tar mikrosekunder och stoppar ingenting.

unsigned long nu = millis();
Serial.println(nu);      // t.ex. 12480, alltså 12,48 sekunder sedan start

Typen är unsigned long och ingenting annat. Det är fyra byte, alltså 32 bitar, och den kan inte vara negativ. Både variabeln du lägger värdet i och variabeln du sparar en tidpunkt i måste ha den typen – hela resten av den här guiden vilar på det.

Mönstret: en tidsstämpel och ett intervall

Idén är att inte vänta utan att komma ihåg när det senast hände och fråga varje varv om det gått tillräckligt länge sedan dess.

const int LAMPA_PIN = 8;
const unsigned long INTERVALL = 500;   // ms mellan varje omslag

unsigned long forra = 0;               // när det senast hände
bool lampanLyser = false;

void loop() {
  unsigned long nu = millis();

  if (nu - forra >= INTERVALL) {
    forra = nu;                        // stämpla om, direkt
    lampanLyser = !lampanLyser;
    digitalWrite(LAMPA_PIN, lampanLyser);
  }

  // Här nedanför får allt annat plats, och det körs varje varv.
}

Tre rader att lägga på minnet. forra står utanför funktionerna, annars nollställs den varje varv och intervallet löper aldrig ut. forra = nu; sker först i blocket, så att nästa intervall räknas från rätt punkt. Och villkoret är en subtraktion, inte en addition – vilket är nästa avsnitt.

Räknaren viker runt efter 49,7 dygn

En unsigned long är 32 bitar. Med samma räkning som i den första guiden i området rymmer den

2³² = 4 294 967 296 olika värden, alltså 0 … 4 294 967 295

Värdena är millisekunder. Räkna om till dygn:

4 294 967 296 ms / 1000 = 4 294 967,296 s

4 294 967,296 s / 60 = 71 582,79 min

71 582,79 min / 60 = 1 193,05 h

1 193,05 h / 24 = 49,71 dygn

Efter ungefär 49 dygn och 17 timmar är räknaren full. Nästa millisekund blir den inte 4 294 967 296 – det talet ryms inte – utan 0, och så börjar den om. Det kallas att den viker runt.

Ett bygge som står påslaget i en och en halv månad är ovanligt i en skolsal och fullständigt normalt i en maskin. Och felet är inte att räknaren viker runt. Felet är att den ena av de två varianterna nedan tål det och den andra inte gör det.

Varför subtraktionen är säker och additionen inte är det

De två varianterna ser lika rimliga ut:

if (nu - forra >= INTERVALL)      // säker
if (nu >= forra + INTERVALL)      // trasig

Grunden till skillnaden är en regel i C++ som är lätt att missa: räkning med heltal utan tecken viker runt med flit. Det är inte ett fel, inte något odefinierat – språket lovar att varje resultat räknas modulo 2³², alltså att det som blir för stort räknas om från början. Subtraktionen som skulle bli negativ blir i stället ett stort positivt tal. Och det talet är precis rätt.

Det syns bäst i en mindre räknare. Låt oss låtsas att räknaren bara är 8 bitar, alltså 2⁸ = 256 värden, 0 till 255. Sätt INTERVALL till 20 och antag att sist det hände var vid 250 – alltså strax innan varvet tar slut.

Räknaren nuVerklig tid sedan 250 nu − 250 modulo 256 Säker: ≥ 20 Trasig: nu ≥ 250 + 20
25222nej, rätt252 ≥ 14 → ja, 18 steg för tidigt
25444nej, rätt254 ≥ 14 → ja, för tidigt igen
066nej, rätt0 ≥ 14 → nej
142020ja, exakt rätt14 ≥ 14 → ja

Två rader i tabellen behöver läsas långsamt.

Kolumnen med subtraktionen. När räknaren står på 14 har den passerat 255 och börjat om. Räknat rakt av är 14 − 250 lika med −236, ett negativt tal som inte finns i en unsigned-typ. Modulo 256 blir det i stället

14 − 250 + 256 = 20

vilket är exakt den verkliga tiden: från 250 upp till 255 är fem steg, över kanten till 0 är ett steg, och från 0 till 14 är fjorton. 5 + 1 + 14 = 20. Rundningen i räknaren och rundningen i subtraktionen tar ut varandra. Det är hela hemligheten, och det gäller lika mycket för 2³² som för 256.

Kolumnen med additionen. 250 + 20 = 270, som inte heller ryms. Modulo 256 blir det

270 − 256 = 14

Målvärdet har alltså vikt runt och blivit ett litet tal – men räknaren har inte vikt runt än, den står fortfarande på 252. Och jämförelsen ≥ viker inte runt: den ställer två tal mot varandra rakt av. 252 är större än 14, så villkoret slår till direkt, 18 steg för tidigt. Sedan stämplas forra om till 252, nytt mål blir 272 modulo 256 = 16, och 253 är större än det också. Villkoret är sant varje varv ända tills räknaren vikt runt.

Översatt till kortet: den trasiga varianten fungerar felfritt i 49 dygn och 17 timmar, spårar sedan ur i ungefär en halv sekund – ett blink som går i loopens hastighet i stället för i halvsekunder – och fungerar därefter felfritt i 49 dygn till. Det är därför felet överlever i så många färdiga projekt: nästan ingen har sitt bygge påslaget så länge, och den som har det tror att det var ett strömglapp.

Regeln, i en mening Flytta aldrig över intervallet till andra sidan jämförelsen. Skriv nu - forra >= INTERVALL och låt både nu och forra vara unsigned long. Då är differensen alltid den verkliga tidsskillnaden, oavsett var i sitt varv räknaren råkar befinna sig.

Blinka och läsa knapp samtidigt

Här är hela poängen med området, i en sketch. Lampan blinkar i sin egen takt. Knappen läses varje varv, räknas med flankdetektering, och avstudsas med tid i stället för med delay().

const int KNAPP_PIN = 2;
const int LAMPA_PIN = 8;

const unsigned long BLINKINTERVALL = 500;   // ms mellan omslagen
const unsigned long STUDSTID       = 20;    // ms som studsen får hålla på

unsigned long forraBlinket = 0;
unsigned long forraTrycket = 0;

bool lampanLyser = false;
int  forraLaget  = HIGH;
int  antalTryck  = 0;

void setup() {
  pinMode(KNAPP_PIN, INPUT_PULLUP);
  pinMode(LAMPA_PIN, OUTPUT);
  Serial.begin(9600);
}

void loop() {
  unsigned long nu = millis();

  // 1. Blinket sköter sig självt och stoppar ingenting.
  if (nu - forraBlinket >= BLINKINTERVALL) {
    forraBlinket = nu;
    lampanLyser = !lampanLyser;
    digitalWrite(LAMPA_PIN, lampanLyser);
  }

  // 2. Knappen läses varje varv, alltså tusentals gånger i sekunden.
  int laget = digitalRead(KNAPP_PIN);      // LOW betyder nedtryckt
  if (forraLaget == HIGH && laget == LOW
      && nu - forraTrycket >= STUDSTID) {
    forraTrycket = nu;
    antalTryck = antalTryck + 1;
    Serial.println(antalTryck);
  }
  forraLaget = laget;
}

Jämför med avstudsningen i förra området. Där stod delay(20) mitt i flankdetekteringen och stoppade allt i tjugo millisekunder. Här är samma tjugo millisekunder ett villkor: ett nytt tryck godtas inte förrän det gått minst så länge sedan det förra. Studsarna sållas bort på samma sätt, men ingenting står stilla under tiden, och blinket märker inte att en knapp trycktes.

När delay() ändå duger

Det här är inte ett förbud. delay() är rätt verktyg när ingenting annat behöver hända:

Regeln är enkel: så fort loop() har mer än en sak att göra hör delay() inte hemma där. I bygge 2 har loop() minst tre saker att göra, och kravlistan säger därför rakt ut att den ska vara fri från delay().

I industrin och i vardagen

I industrin: en PLC arbetar i en fast rundgång som heter cykel eller scan. Den läser alla ingångar till ett minne, kör igenom hela programmet en gång, skriver alla utgångar, och börjar om. En typisk cykeltid ligger runt 5 ms, alltså

1 s / 5 ms = 1 / 0,005 = 200 cykler i sekunden

I det arbetssättet finns inget delay() att skriva. Vill man att en ventil ska stå öppen i tre sekunder startar man en timer och låter cykeln fortsätta – exakt samma idé som tidsstämpeln ovan, med samma skäl. En blockerande väntan hade fryst alla maskinens andra utgångar samtidigt, inklusive dem som håller något säkert. Styrenheten har dessutom en watchdog som övervakar cykeltiden: tar en cykel längre än den inställda gränsen antar den att programmet hängt sig och går till ett säkert läge. En blockerande väntan skulle alltså inte bara vara olämplig – den skulle fälla maskinen.

I vardagen: blinkersen i en bil ska enligt reglerna blinka omkring 90 gånger i minuten. Det ger en blinkcykel på

60 s / 90 = 0,67 s per cykel, alltså cirka 333 ms tänd och 333 ms släckt

Under de 333 millisekunderna fortsätter bilen att göra allt annat: läsa bromspedalen, tända bromsljuset, läsa den andra blinkersspaken, hålla igång instrumenten. Styrenheten som sköter karossfunktionerna använder samma sorts tidsstämpel som du just skrivit – den kommer ihåg när lampan senast slog om och jämför mot nu. Hade den i stället väntat ut tiden hade bromsljuset släpat en tredjedels sekund efter foten, varje gång blinkersen var på. Det är samma fel som ditt bygge gör när knappen missas, med den skillnaden att ingen skriver om ditt bygge i en olycksrapport.

Prova själv

  1. Skriv ett program som skriver ut millis() en gång i sekunden med tidsstämpelmönstret. Kontrollera i seriemonitorn att talen ökar med ungefär 1 000 varje rad.
  2. Bygg det blockerande felet med flit: en lampa som blinkar med delay(500) och en knapp som ska räknas. Tryck tio korta tryck och se hur många som räknas. Anteckna siffran.
  3. Skriv om samma bygge med millis() enligt exemplet ovan. Tryck tio korta tryck igen. Jämför med din anteckning.
  4. Lägg till en andra lampa som blinkar med ett annat intervall, till exempel 130 ms. Den behöver en egen unsigned long för sin tidsstämpel. Två blink och en knapp, i samma loop, utan att något stör något annat.
  5. Byt typen på en av tidsstämplarna från unsigned long till int och låt bygget gå i en dryg halvminut. Vid 32 767 ms tar en int slut – notera vad som händer, och byt sedan tillbaka.
  6. Skriv om villkoret till den trasiga formen nu >= forra + INTERVALL. Bygget fungerar. Skriv en mening om varför du ändå inte kan upptäcka felet genom att prova, och hur länge du hade behövt låta kortet stå på.

Kontrollfrågor

Efter hur lång tid viker millis() runt, och vad händer då?

Efter 2³² millisekunder, alltså 4 294 967 296 ms. Det blir 4 294 967,296 s, som är 71 582,79 min, som är 1 193,05 h, som är 49,71 dygn – ungefär 49 dygn och 17 timmar. Då börjar räknaren om på 0. Skriver du nu - forra >= INTERVALL märks det inte alls, eftersom subtraktionen ger rätt svar även över kanten.

Varför fungerar nu - forra när nu är mindre än forra?

Därför att räkning med heltal utan tecken är definierad modulo 2³² i C++. Ett resultat som skulle blivit negativt räknas om från toppen och blir ett stort positivt tal – och just det talet är den verkliga tidsskillnaden, eftersom räknaren själv har gjort samma hopp åt andra hållet. De två rundningarna tar ut varandra.

Vad är egentligen fel med if (nu >= forra + INTERVALL)?

Summan forra + INTERVALL viker runt och blir ett litet tal, medan nu ännu inte vikt runt och alltså är ett mycket stort tal. Jämförelsen >= jobbar inte modulo någonting – den ställer talen mot varandra rakt av – så villkoret blir sant direkt och sedan sant varje varv, ända tills räknaren också vikt runt. Följden är ungefär en halv sekunds galenskap vart femtionde dygn.

Du har deklarerat int forra = 0; i stället för unsigned long. När märks det?

Efter drygt en halv minut. En int på ett Uno-kort tar slut vid 32 767, och 32 767 ms är 32,8 sekunder. Efter det innehåller forra ett stympat och ofta negativt värde, och differensen blir ett meningslöst tal. Symtomet är ett bygge som fungerar under provkörningen på lektionen och inte gör det hemma, där det får stå på längre.

Räknar millis() vidare inuti en delay()?

Ja. Räknaren drivs av ett avbrott som går oavsett vad din kod gör, så tiden är korrekt när du kommer ut ur pausen. Det som står stilla är din loop() – ingen pinne läses, ingen utgång ändras. Det är därför problemet inte är tidmätningen utan allt annat som inte blir gjort.

Varför ska forra = nu; stå först i blocket och inte sist?

Det spelar sällan roll i praktiken, men det är lättare att läsa och omöjligt att glömma. Står omstämplingen efter en lång rad kod är risken att en tidig return eller ett nytt villkor hoppar över den, och då blir intervallet aldrig klart – villkoret är i stället sant varje varv, för alltid.

Vanliga fel

Vidare

Kapitlet Tid och millis() i språkreferensen är den här guidens uppslagsdel, med mönstret, typerna och rundningen samlade på ett ställe. Typer och variabler förklarar varför en int tar slut vid 32 767 och en unsigned long vid drygt fyra miljarder.

Nästa guide är Tillstånd. Nu när bygget kan mäta tid utan att stanna behöver det också minnas var i ett förlopp det befinner sig – annars vet det inte vad tiden som gått betyder.