← Kursens material Språkreferens Dokumentationsmall

Mekatronik 1 · alltid till hands

Språkreferens

C++ i ordnad följd, för dig som kan Python från tidigare kurser. Kapitlen står i språkets ordning, inte i den ordning kursen möter dem – sidan byggs ut i takt med områdena och är tänkt att slås upp i, inte läsas från början till slut.

Från Python till C++

Samma idéer, annan syntax. De fyra skillnader som stör flest i början, den första du möter innan någon av de andra ens hinner spela roll:

PythonC++Vad som skiljer
python skript.py Knappen Ladda upp Python kör raden du skrev, direkt. Det finns ingen REPL i Arduino IDE – ingenting kör förrän hela sketchen kompilerat utan fel, och då kör den på kortet, inte på datorn. Första felmeddelandet du ser är ofta ett kompileringsfel på en rad du var säker på var klar.
print("hej") Serial.println("hej"); Utskrift går till seriemonitorn, inte till en terminal, och varje rad avslutas med semikolon.
def area(bas, hojd): float area(float bas, float hojd) { C++ är statiskt typat. Både returtypen och varje parameters typ skrivs ut, i stället för att gissas vid anropet.
varden = [] int varden[10]; En array i C++ har fast längd, satt när den skapas. Den växer inte som en Python-lista – fler platser än 10 ryms helt enkelt inte.

Typer och variabler

I Python skriver du antal = 5 och tolken listar ut resten. I C++ säger du vilken sorts låda värdet ska ligga i, och lådan byter aldrig sort efteråt. Deklarationen är typ, namn, likhetstecken, värde, semikolon.

int antal = 5;
float spanning = 4.92;
bool tand = false;
char bokstav = 'A';

Typerna, med de storlekar de har på ett Uno-kort

TypStorlekRymmerAnvänds till
bool1 bytetrue eller falseav eller på, tryckt eller inte
byte1 byte0 … 255små positiva tal, till exempel ett pinnummer
int2 byte−32768 … 32767vanliga heltal: vinklar, räknare, mätvärden
unsigned int2 byte0 … 65535heltal som aldrig kan bli negativa
long4 byte−2 147 483 648 … 2 147 483 647stora tal
unsigned long4 byte0 … 4 294 967 295tid i millisekunder
float4 byteungefär 6–7 siffrors noggrannhetspänningar och andra decimaltal
char1 byteett tecken, 'A'enstaka tecken. Enkla citattecken

int är 16 bitar här, inte 32

Det här är den enda typfakta som verkligen biter, och den överraskar även den som läst C++ förut. På ATmega328P är en int två byte, alltså 16 bitar. 216 = 65 536 värden, hälften negativa:

−2¹⁵ … 2¹⁵ − 1 = −32768 … 32767

Går talet över taket viker det runt till andra änden, tyst och utan felmeddelande. 32767 + 1 blir −32768. Ett verkligt fall:

int sekunder = 60;
int millisekunder = sekunder * 1000;   // 60000 ryms inte
Serial.println(millisekunder);         // skriver -5536

60 000 − 65 536 = −5 536

Lösningen är en större typ – men typen måste vara större redan när räkningen görs, inte först när svaret läggs undan:

long millisekunder = (long)sekunder * 1000;   // 60000, som det ska

(long) framför kallas en typomvandling och betyder "räkna det här i den större typen". Utan den hinner multiplikationen spilla över innan resultatet flyttas.

const – värden som inte ska ändra sig

Pinnummer, gränsvärden och fasta vinklar hör överst i filen, med namn och med const framför. Försöker du skriva över en const blir det ett kompileringsfel, alltså ett fel du ser innan bygget rör sig.

const int KNAPP_PIN = 2;
const int SERVO_PIN = 9;
const int VILOVINKEL = 20;

Versaler i namnet är ingen regel i språket utan samma vana som i Python: den gör att du ser på en rad om ett namn är en inställning eller något som förändras.

Var variabeln står avgör vad den minns

int raknare = 0;        // utanför funktionerna: minns mellan varven

void setup() {
}

void loop() {
  int tillfallig = 0;   // inuti loop(): börjar om från 0 varje varv
  raknare = raknare + 1;
}

En variabel som deklareras inuti loop() skapas på nytt varje varv och glömmer allt. Ska den minnas – ett tillstånd, en räknare, förra varvets knappläge – måste den stå utanför funktionerna. Det är den vanligaste orsaken till att en räknare envist står kvar på 1.

Villkor

Samma idé som i Python, tre skillnader i skrivsättet: villkoret står inom parentes, blocket omges av måsvingar, och indraget betyder ingenting för kompilatorn.

if (vinkel > 90) {
  Serial.println("hogt");        // ASCII i utskrifter – se Bibliotek nedan
} else if (vinkel > 45) {
  Serial.println("mitten");
} else {
  Serial.println("lagt");
}

Skriv alltid måsvingarna, även när blocket är en enda rad. Utan dem hör bara första raden till villkoret, hur du än drar in de följande – och det är ett fel som överlever många genomläsningar.

Jämförelser

PythonC++Betyder
====lika med
!=!=skilt från
< >< >mindre än, större än
<= >=<= >=mindre eller lika, större eller lika
and&&båda måste vara sanna
or||minst en måste vara sann
not!vänder på sanningsvärdet
True Falsetrue falsesmå bokstäver i C++

Ett likhetstecken är inte två if (lage = LOW) tilldelar i stället för att jämföra, och villkoret blir något helt annat än du menade. Python stoppar dig med ett syntaxfel; C++ gör det inte alltid. Läs villkoret en extra gång och räkna tecknen.

Villkor som läser en pinne

Det vanligaste villkoret i den här kursen jämför ett pinneläge. Med INPUT_PULLUP är en nedtryckt knapp LOW, inte HIGH – skriv en kommentar om det, varje gång.

if (digitalRead(KNAPP_PIN) == LOW) {   // LOW betyder nedtryckt
  digitalWrite(LAMPA_PIN, HIGH);
} else {
  digitalWrite(LAMPA_PIN, LOW);
}

Ett villkor som ska reagera på att något ändras – ett knapptryck, inte att knappen ligger nere – behöver jämföra med förra varvets värde:

if (forra == HIGH && nu == LOW) {   // knappen gick just ned
  tand = !tand;
}

Att kombinera flera villkor med && och || mot en sanningstabell hör till område 3, som bygger vidare på det som står här.

Loopar

En loop upprepar ett block ett bestämt eller obestämt antal gånger. I den här kursen dyker behovet upp i område 4, när en givare ska läsas flera gånger i rad och avläsningarna vägas samman till ett medelvärde.

for: när du vet hur många varv

Samma tanke som Pythons for i in range(16):, men de tre delarna skrivs ut i stället för att gömmas i range.

for (int i = 0; i < 16; i++) {
  Serial.println(i);        // skriver 0, 1, 2 … 15
}

Parentesen innehåller tre delar, åtskilda av semikolon:

DelI exempletNär den körs
Startvärdeint i = 0en gång, innan första varvet
Villkor att fortsättai < 16före varje varv. Är det falskt är loopen slut
Stegi++efter varje varv. Betyder i = i + 1

Räkna från 0 och använd <, inte <=. Med i < 16 körs loopen exakt sexton varv, med i mellan 0 och 15 – vilket är precis de namn platserna i en array med sexton element har. Skriver du i <= 16 får du sjutton varv och ett steg utanför arrayen.

i är deklarerad inuti parentesen och finns bara inuti loopen. Behöver du värdet efteråt måste variabeln deklareras utanför.

Räknat exempel: medelvärde av sexton avläsningar

const int GIVARE_PIN = A0;
const int ANTAL_PROV = 16;

int medelvarde() {
  long summa = 0;
  for (int i = 0; i < ANTAL_PROV; i++) {
    summa = summa + analogRead(GIVARE_PIN);
  }
  return summa / ANTAL_PROV;
}

summa är en long och inte en int, och det är inte pedanteri. Sexton avläsningar på högst 1023 ger

16 · 1023 = 16 368

som ryms i en int. Men sextiofyra ger

64 · 1023 = 65 472

och en int tar slut vid 32 767. Summan viker då runt till ett negativt tal, tyst, och medelvärdet blir meningslöst. Skriv long från början.

while: när du inte vet hur många varv

const int KNAPP_PIN = 2;

int vantaPaTryck() {
  int forsok = 0;
  while (digitalRead(KNAPP_PIN) == HIGH && forsok < 1000) {
    forsok = forsok + 1;
  }
  return forsok;
}

while upprepar så länge villkoret är sant. Det är den farliga loopen: blir villkoret aldrig falskt står bygget still för alltid, utan felmeddelande. Ge alltid en while-loop en andra väg ut, som räknaren ovan eller en tidsgräns med millis().

loop() är ingen loop du skrivit void loop() är en funktion som Arduino-miljön anropar om och om igen åt dig. Den är alltså en oändlig upprepning en våning ovanför din kod. Det är därför en variabel som deklareras inuti loop() börjar om varje varv, och det är därför en while-loop inuti loop() som aldrig tar slut stoppar allt annat i bygget.

Vanliga fel

Funktioner

Du har använt funktioner sedan område 2 – digitalRead, millis, analogRead – och fyllt två av dem själv, setup() och loop(). I område 4 skriver du dina egna, med namn du valt, och skälet är konkret: omvandlingen från råvärde till en storhet behövs på mer än ett ställe, och två kopior av samma formel är två ställen att rätta.

Formen

I Python skrev du def area(bas, hojd): och lät tolken lista ut resten. C++ vill veta två saker till: vad funktionen lämnar tillbaka och vad varje parameter är för sorts värde.

float area(float bas, float hojd) {
  return bas * hojd / 2.0;
}
DelI exempletVad den betyder
Returtypfloatvad anropet lämnar tillbaka. void om det inte lämnar något
Namnareadöp efter vad funktionen gör
Parametrarfloat bas, float hojdvarje parameter har sin egen typ utskriven
Kroppmellan måsvingarnakoden som körs vid varje anrop
returnreturn bas * hojd / 2.0;skickar tillbaka värdet och avslutar funktionen på stället

Anropet ser ut som vilket annat anrop som helst, och resultatet tas emot av en variabel av rätt typ:

float triangeln = area(4.0, 3.0);   // 6.0

void: en funktion som inte lämnar något

const int LAMPA_PIN = 8;

void blinkaEnGang(int tid) {
  digitalWrite(LAMPA_PIN, HIGH);
  delay(tid);
  digitalWrite(LAMPA_PIN, LOW);
}

void betyder "inget värde tillbaka". Funktionen gör något i stället för att räkna ut något. setup() och loop() är av samma sort – nu vet du varför det står void framför dem.

Parametern är en kopia

void oka(int tal) {
  tal = tal + 1;          // andrar bara kopian inuti funktionen
}

void provaOka() {
  int mitt = 5;
  oka(mitt);
  Serial.println(mitt);   // skriver 5, inte 6
}

Det värde du skickar in kopieras till parametern. Ändringar inuti funktionen syns inte utanför den. Vill du få ut ett värde använder du return.

Var funktionen ska stå

Skriv dina egna funktioner ovanför setup(). Arduino-miljön är snäll och klarar oftast att de står längst ned, men i vanlig C++ måste en funktion vara känd innan den används. Att alltid lägga dem överst är en vana som fungerar i båda världarna och kostar ingenting.

Räknat exempel: kalibrering

Områdets riktiga användning. Två uppmätta punkter blir en rät linje, och linjen blir en funktion.

const int   RA_VID_20 = 569;    // ravardet da referensen visade 20,0 grader
const int   RA_VID_30 = 456;    // ravardet da referensen visade 30,0 grader
const float TEMP_20   = 20.0;
const float TEMP_30   = 30.0;

float ravardeTillGrader(int ravarde) {
  float k = (TEMP_30 - TEMP_20) / (RA_VID_30 - RA_VID_20);
  return TEMP_20 + k * (ravarde - RA_VID_20);
}

Kontrollera typerna, för det är där felet sitter. TEMP_30 - TEMP_20 är 10.0, ett flyttal; RA_VID_30 - RA_VID_20 är -113, ett heltal. Ett flyttal delat med ett heltal räknas i flyttal, så k blir

10,0 / (−113) = −0,0885

Hade båda varit heltal hade heltalsdivisionen gett 0, och funktionen hade lämnat 20,0 vad du än matade in.

Vanliga fel

Arrayer

En array är ett antal platser för värden av samma typ, i rad, under ett gemensamt namn. Den kommer in i område 4, när sexton mätvärden ska sparas så att det går att räkna på dem i efterhand.

Deklaration och användning

const int ANTAL_PROV = 16;

int prov[ANTAL_PROV];      // sexton platser for int, numrerade 0 till 15

void fyllOchSkriv() {
  for (int i = 0; i < ANTAL_PROV; i++) {
    prov[i] = analogRead(A0);
  }
  Serial.println(prov[0]);       // forsta platsen
  Serial.println(prov[15]);      // sista platsen
}

En array kan också fyllas när den skapas, och då räknar kompilatorn längden åt dig:

int vinklar[] = {0, 45, 90, 135, 180};   // fem platser, 0 till 4

Tre skillnader mot Pythons listor

PythonC++Följden i praktiken
varden = [] och append int varden[16]; Längden bestäms när arrayen skapas och kan aldrig ändras. Det finns inget append, och antalet måste vara känt redan vid kompileringen – därför const int.
får blanda tal, text och listor alla platser har samma typ En int-array rymmer bara int.
varden[16] ger IndexError prov[16] ger ingenting alls C++ kontrollerar inte gränsen, varken vid kompilering eller vid körning. Du läser eller skriver i minnet bredvid arrayen.

Steget utanför arrayen är det otäckaste felet i kapitlet Sexton platser heter prov[0] till prov[15]. Det finns inget prov[16], men C++ säger inte ifrån om du skriver det – det är arbetsminnet bredvid arrayen som läses eller skrivs över. Symtomet är att en helt annan variabel ändrar sig av sig själv, ofta långt från raden som orsakade det. Låt loopens villkor vara i < ANTAL_PROV, med samma konstant som i deklarationen, så kan felet inte uppstå.

Räknat exempel: största och minsta

Det medelvärdet inte kan berätta är hur mycket avläsningarna darrar. Det kräver att proven finns kvar, och det är därför arrayen behövs.

const int ANTAL = 16;
int matvarden[ANTAL];

int spridning() {
  int minsta  = matvarden[0];
  int storsta = matvarden[0];
  for (int i = 1; i < ANTAL; i++) {
    if (matvarden[i] < minsta)  { minsta  = matvarden[i]; }
    if (matvarden[i] > storsta) { storsta = matvarden[i]; }
  }
  return storsta - minsta;
}

Loopen börjar på 1, eftersom plats 0 redan använts som utgångsvärde. Att i stället starta minsta på 0 vore ett fel: 0 är mindre än eller lika med varje möjligt råvärde, och funktionen hade i praktiken alltid svarat med det största provet.

Vanliga fel

Tid och millis()

Att mäta tid i stället för att vänta ut den. Det är grunden för allt som ska pågå samtidigt, och det är skillnaden mellan ett bygge som gör en sak och ett som gör tre.

delay() stoppar din kod, inte kortet

delay(500) är ingen paus utan en tom slinga: kortet kontrollerar sin egen klocka om och om igen tills tiden gått, och kör inte en enda rad av din loop() under tiden. Ingen pinne läses, ingen utgång ändras.

Avbrotten i bakgrunden fortsätter däremot. Millisekundräknaren tickar vidare inuti en delay(), och tecken som kommer in på serieporten tappas oftast inte bort – de samlas i en mottagningsbuffert på 64 byte, och först när den är full går tecken förlorade. Det som står stilla är enbart det du skrivit.

delay() är fortfarande rätt i setup(), i ett litet testprogram, och när pausen är kortare än en millisekund. Så fort loop() har mer än en sak att göra hör den inte hemma där.

millis() och micros()

AnropGerTypViker runt efter
millis()millisekunder sedan start unsigned longungefär 49,7 dygn
micros()mikrosekunder sedan start unsigned longungefär 71,6 minuter

Båda lämnar unsigned long, alltså fyra byte utan tecken. micros() har på ett Uno-kort en upplösning på 4 µs – talet hoppar alltså i steg om fyra – och behövs sällan i den här kursen. Använd millis() om du inte har ett skäl att göra något annat.

Mönstret: en tidsstämpel och ett intervall

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

unsigned long forra = 0;               // utanför funktionerna: minns mellan varven
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);
  }

  // Allt annat får plats här, och körs varje varv.
}

Tre regler följer med mönstret. forra står utanför funktionerna, annars nollställs den varje varv. Omstämplingen sker först i blocket. Och villkoret är en subtraktion, aldrig en addition.

Flera saker samtidigt

Varje sak som har sin egen takt får sin egen tidsstämpel. Att låta två saker dela på en variabel är ett av de vanligaste felen.

unsigned long forraBlinket = 0;
unsigned long forraUtskriften = 0;

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

  if (nu - forraBlinket >= 500) {
    forraBlinket = nu;
    lampanLyser = !lampanLyser;
    digitalWrite(LAMPA_PIN, lampanLyser);
  }

  if (nu - forraUtskriften >= 2000) {
    forraUtskriften = nu;
    Serial.println(nu);
  }
}

Räknaren viker runt, och varför det inte gör något

En unsigned long är 32 bitar och rymmer

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

Räknat som millisekunder blir det

4 294 967 296 / 1000 / 60 / 60 / 24 = 49,71 dygn

ungefär 49 dygn och 17 timmar. Sedan börjar räknaren om på 0.

Skriver du nu - forra märks det inte alls. Räkning med heltal utan tecken är i C++ definierad modulo 2³²: ett resultat som skulle blivit negativt räknas om från toppen. Har räknaren hoppat över kanten har den gjort samma hopp åt andra hållet, och de två tar ut varandra. Differensen är därför alltid den verkliga tidsskillnaden.

Skriver du i stället nu >= forra + INTERVALL går det sönder, och av ett skäl som är värt att förstå: summan forra + INTERVALL viker också runt och blir ett litet tal, medan nu ännu inte vikt runt och alltså är enormt. Men jämförelsen >= räknar inte modulo någonting – den ställer talen mot varandra rakt av. Villkoret blir sant direkt, och sedan sant varje varv, ända tills räknaren också vikt runt.

Regeln 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.

Vanliga fel

Bibliotek

Ett bibliotek är färdig kod någon annan skrivit, som du får använda utan att öppna. Servo.h räknar ut servopulser på mikrosekunden – något du varken vill eller behöver göra själv. En del bibliotek följer med Arduino-miljön, andra hämtas med Verktyg → Hantera bibliotek. Servo följer med.

Receptet, i fyra steg

Nästan alla bibliotek används likadant. Servo.h är exemplet, men stegen ser likadana ut för de sensor- och displaybibliotek du möter längre fram.

#include <Servo.h>      // 1. hämta in biblioteket

Servo spakservo;         // 2. skapa det du ska styra, och döp det

void setup() {
  spakservo.attach(9);   // 3. koppla det till en pinne
}

void loop() {
  spakservo.write(90);   // 4. använd det
}
  1. #include <Servo.h> hör överst i filen, utanför både setup() och loop(). Vinkelparenteser runt namnet, och inget semikolon på slutet – raden är ingen sats utan ett meddelande till kompilatorn.
  2. Servo spakservo; läses precis som int antal;: en sak av sorten Servo, med namnet spakservo. Namnet väljer du själv. Två servon kräver två rader och två namn.
  3. attach() hör i setup() och körs en gång. Glöms den händer ingenting alls, och inget felmeddelande kommer.
  4. Punkten mellan namnet och kommandot betyder "på just den här". spakservo.write(90) rör spakservo och inget annat servo du råkar ha inkopplat.

Vilka kommandon ett bibliotek erbjuder står i dess dokumentation, inte i koden du skriver. För Servo är det i praktiken attach(), write() och detach().

Ett bibliotek kan ta hårdvara med sig

Det här är lärdomen som gäller alla bibliotek och som ingen berättar i förväg. Ett bibliotek använder kortets inbyggda resurser, och de tar slut.

Servo på ett Uno-kort tar kortets Timer1 i anspråk för att räkna ut pulserna. Samma timer sköter analogWrite() på pinnarna 9 och 10, så de två pinnarna slutar gå att dimra så länge biblioteket är i bruk – även när servot sitter på en helt annan pinne. Vanliga digitalWrite() påverkas inte.

Symtomet är typiskt för den här sortens fel: koden ser rätt ut, kompilatorn är nöjd, och något som fungerade i går slutade fungera när du la till ett bibliotek. Börjar något bete sig så – fråga vad det senast tillagda biblioteket tog med sig.

Å, ä och ö i utskrifter

Kodexemplen på den här sidan skriver "hogt" och inte "högt". Det är inget stavfel. Seriemonitorn tolkar inte alltid svenska tecken rätt, och en trasig bokstav mitt i en felsökning hjälper ingen. Håll dig till a–z i det som skickas ut genom Serial. I kommentarer och i din dokumentation skriver du förstås svenska som vanligt.