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:
| Python | C++ | 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
| Typ | Storlek | Rymmer | Används till |
|---|---|---|---|
bool | 1 byte | true eller false | av eller på, tryckt eller inte |
byte | 1 byte | 0 … 255 | små positiva tal, till exempel ett pinnummer |
int | 2 byte | −32768 … 32767 | vanliga heltal: vinklar, räknare, mätvärden |
unsigned int | 2 byte | 0 … 65535 | heltal som aldrig kan bli negativa |
long | 4 byte | −2 147 483 648 … 2 147 483 647 | stora tal |
unsigned long | 4 byte | 0 … 4 294 967 295 | tid i millisekunder |
float | 4 byte | ungefär 6–7 siffrors noggrannhet | spänningar och andra decimaltal |
char | 1 byte | ett 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
| Python | C++ | 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 False | true false | små 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:
| Del | I exemplet | När den körs |
|---|---|---|
| Startvärde | int i = 0 | en gång, innan första varvet |
| Villkor att fortsätta | i < 16 | före varje varv. Är det falskt är loopen slut |
| Steg | i++ | 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
- Semikolon direkt efter parentesen.
for (int i = 0; i < 16; i++);är en komplett loop med ett tomt block. Raderna efter körs en enda gång. Kompilatorn säger ingenting. <=där det ska stå<. Ett varv för mycket, och ett steg utanför arrayen.- Räknaren ändras inuti loopen också. Står
i = i + 1både i steget och i blocket hoppar loopen över varannan plats. whileutan väg ut. Bygget hänger sig och ser ut att ha kraschat. Lägg till en räknare eller en tidsgräns.
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;
}
| Del | I exemplet | Vad den betyder |
|---|---|---|
| Returtyp | float | vad anropet lämnar tillbaka. void om det inte lämnar något |
| Namn | area | döp efter vad funktionen gör |
| Parametrar | float bas, float hojd | varje parameter har sin egen typ utskriven |
| Kropp | mellan måsvingarna | koden som körs vid varje anrop |
return | return 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
- Returtypen glöms. C++ kräver den, och felmeddelandet pekar ofta på raden efter.
returnsaknas i en funktion som inte ärvoid. Vad som lämnas tillbaka blir då odefinierat, och kompilatorn nöjer sig ibland med en varning.- Heltalsdivision inuti funktionen. Ett svar som alltid blir 0 eller alltid samma tal. Kontrollera att minst ett av talen i varje division har decimalpunkt.
- Funktionen ändrar en parameter och väntar sig att det
märks. Parametern är en kopia. Använd
return. - Funktionen anropas innan den är deklarerad, i vanlig C++. Arduino-miljön döljer det oftast, men vanan att lägga funktionerna överst kostar ingenting och fungerar överallt.
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
| Python | C++ | 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
- Längden står som ett tal på flera ställen.
int prov[16];på en rad ochi < 20i loopen på en annan. Använd sammaconst inti båda. - Ett steg utanför arrayen. Se notisen ovan. Inget felmeddelande, och symtomet dyker upp någon helt annanstans.
- Arrayen deklareras inuti
loop(). Den skapas då på nytt varje varv. Ska värdena minnas mellan varven hör deklarationen utanför funktionerna. - Arrayen antas vara nollställd. En array som deklareras inuti en funktion innehåller vad som råkade ligga i minnet. Fyll den innan du räknar på den.
- Summan läggs i en
int. Se loopkapitlet ovan: 64 · 1023 ryms inte.
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()
| Anrop | Ger | Typ | Viker runt efter |
|---|---|---|---|
millis() | millisekunder sedan start | unsigned long | ungefär 49,7 dygn |
micros() | mikrosekunder sedan start | unsigned long | ungefä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
- Tidsstämpeln deklarerad inuti
loop(). Den nollställs varje varv och villkoret slår till hela tiden. - Fel typ.
inttar slut vid 32 767, alltså efter 32,8 sekunder.longkan bli negativ. Det ska ståunsigned long. - Additionen i stället för subtraktionen. Fungerar i varje test du hinner göra och går sönder efter 49,7 dygn.
- Två takter delar på en tidsstämpel. Båda beter sig oförutsägbart. En variabel per sak.
- Omstämplingen glöms. Utan
forra = nu;är villkoret sant från första gången och sedan för alltid.
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
}
#include <Servo.h>hör överst i filen, utanför bådesetup()ochloop(). Vinkelparenteser runt namnet, och inget semikolon på slutet – raden är ingen sats utan ett meddelande till kompilatorn.Servo spakservo;läses precis somint antal;: en sak av sortenServo, med namnetspakservo. Namnet väljer du själv. Två servon kräver två rader och två namn.attach()hör isetup()och körs en gång. Glöms den händer ingenting alls, och inget felmeddelande kommer.- Punkten mellan namnet och kommandot betyder "på just den här".
spakservo.write(90)rörspakservooch 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.