3. Moment03 - Arduino PL

I detta moment kommer det vara fokus på programmeringsspråket Arduino PL (Programming Language). Detta är ingen grundkurs i programmering så i detta moment kommer jag fokusera på saker som fungerar på ett annorlunda sätt i Arduino PL än vad det gör i Python. Vissa av sakerna som jag kommer visa är väldigt lika medan annan funktionalitet skiljer sig en hel del. Fokus kommer heller inte vara på funktioner som hänger ihop med olika komponenter även om jag eventuellt kommer använda detta vid något tillfälle för att kunna ge bra exempel på programmeringen.

3.1 Basics

Arduino PL är i grunden ett programmeringsspråk som bygger på C++ vilket är ett väldigt strikt programmeringsspråk jämfört med tex Python. Arduino PL är fokuserat på att programmera just Arduino och Arduinokompatibla kort vilket gör att språket är nedbantat för att lösa de specifika uppgifterna som är intressanta när vi skall bygga våra Arduinoprojekt.

I moment02 så kodade du i Arduinos egen miljö men i detta kapitel kommer jag koda i TinkerCad för att ha en smidig miljö att laborera i. Det går att göra exempelkoderna i Arduinos miljö men då måste du också ladda upp dina koder till Ardunio för att kunna köra dem. TinkerCad når du här. Tänk på att ladda ner och spara dina koder, då detta inte görs automatiskt.

Detta moment är inte heltäckande utan det handlar om att jag visar skillnader/likheter med Python och sedan så laborerar/testar du en hel del på egen hand. Jag kommer också länka till bra guider/exempel som fungerar att utgå ifrån. En bra start är alltid att gå till The Arduino Reference.

3.1.1. Funktionerna setup() och loop()

När du startar ett nytt projekt i Arduino PL så kommer det finnas två funktioner i din kod (eventuellt finns det också lite exempelkod). Dessa två funktioner är setup() och loop().

Kodexempel: Funktionerna setup() och loop()

void setup()
{
}

void loop()
{
}

Dessa två funktioner är det som bygger upp din kod. Funktionen setup() är den funktionen som kommer anropas först. Det är här som vi bl.a. kommer beskriva vilka kopplingar vi gör och vilka pins som skall användas. Funktionen loop() innehåller den koden som skall köras hela tiden när programmet körs. Funktionen loop() anropas hela tiden direkt efter funktionen är färdig vilket innebär att det inte går att bestämma hur många gånger per sekund som denna funktion körs utan det beror helt på hur lång tid det tar att köra funktionen.

Nästa exempel visar hur dessa två funktioner används i praktiken.

3.2 Utskrifter till Serial Monitor

Serial Monitor är den möjlighet vi har att kunna styra utskrifter så att det blir enklare att testa/felsöka våra koder. Serial Monitor hör ihop med Arduino IDE och kommer inte finnas när våra applikationer sedan skall köras på vårt utvecklingskort. Vi skall nu testa en enkel utskrift till Serial Monitor.

Kodexempel: Utskrift till Serial Monitor

void setup()
{
  // Öppnar kommunikationen med Arduino, 9600 bps (bits/s)
  Serial.begin(9600);
  Serial.println("Print from setup()");
}

void loop()
{
  Serial.println("Print from loop()");
  delay(1000);			// pausar 1000ms
}

Programmet kommer nu köras genom att funktionen setup() anropas först och sedan kommer funktionen loop() att anropas gång på gång tills progammet avslutas.

Snabb genomgång av koden visar att först behöver vi öppna en kommunikation med Arduino för att kunna använda Serial Monitor, vilket görs på rad 4.

På rad 5, och rad 10, så skriver vi ut en rad med text till Serial Monitor, println() kommer skriva ut innehåll samt skapa en radbrytning, vill jag inte ha en radbrytning så anger jag print().

Testa nu koden för att se hur det fungerar. Testa också skillnaden mellan println() och print().

3.3 Variabler

Inom Arduino PL så måste vi deklarera en variabel innan vi kan använda den. Att deklarera en variabel innebär att vi ger den ett namn och vilken datatyp som variabeln skall lagra. Till skillnad från Python kan detta sedan inte ändras vilket innebär att en variabel som är tänkt att den skall lagra ett heltal alltid måste lagra ett heltal.

Kodexempel: Deklarera variabler

void setup()
{
  // Öppnar kommunikationen med Arduino, 9600 bps (bits/s)
  Serial.begin(9600);
  
  int tal1;			// Deklarerar tal1 som en int (heltal)
  tal1 = 4;			// Tilldelar tal1 ett värde
  int tal2 = 3;	// Initierar tal2 då det får både datatyp och värde vid skapandet

  // Utskrifter
  Serial.println(tal1);
  Serial.println(tal2);
}

void loop()
{

}

I exemplet ovan så visar jag både hur jag deklarerar en variabel, ger den ett namn och en datatyp, samt hur jag kan initiera en variabel då får den också ett värde i samband med att den skapas.

3.3.1 Synlighet

Synligheten för en variabel är viktig, en variabel som deklareras inne i en funktion finns endast tillgänglig där. Däremort kan vi deklarera en variabel som global vilket innebär att den är tillgänglig i samtliga funktioner i vårt program. Detta är ibland en smidig lösning, men det kan också skapa problem ifall du vill, eller inte vill, påverka denna variabels värde i olika funktioner.

Kodexempel: En variabels synlighet

// Initierar en global variabel som är synlig i alla funktioner
int tal1 = 9;

void setup()
{
  Serial.begin(9600);		  // Öppnar kommunikationen
  Serial.println(tal1);		// Utskrift av global variabel tal1
  int tal1 = 4;				    // Initierar en lokal variabel med samma namn
  Serial.println(tal1);		// Utskrift av lokal variabel
}

void loop()
{
  Serial.println(tal1);		// Skriver ut den globala variabeln
  delay(1000);				     // Paus i 1000ms
  exit(0);					       // Avbryter programmet
}

I denna koden vill jag visa hur man kan, men inte bör, skapa en global och en lokal variabel med samma namn. Risken är stor att du här påverkar en variabels värde på ett annat sätt än vad du har tänkt.

Att skapa globala variabler är inget fel, men gör detta endast om du tänker att variabeln behövs i flera funktioner.

3.3.2 Datatyper

Ardunio har ett antal datatyper som du behöver ha koll på, här kommer jag lista de vanligaste;

  • bool är datatypen som lagrar ett av två värden, true eller false. Boolean är endast ett alias för bool och ingen egen datatyp.
  • int är standarddatatypen för heltal. Det som är lite speciellt för int i Arduinovärden är att den lagrar olika stora tal på olika typer av kort. På en Arduino Uno så är tex intervallet mellan -32768 till 32767, medan det på ett mer kraftfullt kort lagrar värden på +/- drygt 2 miljarder. Vill du vara säker på att alltid kunna lagra så stora tal så skall du istället välja datatypen long och vet du att det lägre intervallet räcker så kan du använda short. Det som är lite intressant/lurigt med heltalsvariablerna är att när de slår i taket för vilket värde de kan ha så börjar de om utifrån lägsta värdet. Testa att deklarera short tal = 32767; öka sedan värdet med ett tal++; och skriv sedan ut värdet på Serial.println(tal);. tal++; är enklaste sättet i Arduino PL att öka värdet på en heltalsvariabel med ett. Du kan flytta intervallet på en heltalsvaribel så att den endast är positiv. Om storleken är rimlig på en short men du vill att den går från 0 till 65535 så kan du deklarera den som en unsigned short tal;.
  • double och float är de decimaltalsvariabler som används i Arduino PL. Double fungerar lite olika på olika kort, är det vikltigt med hög precision av decimaler så välj float.
  • char är datatypen för ett enskilt tecken. En char representerar både ett tecken, tex 'A', och ett tal, 65 för 'A', vilket lämpar sig väl för att modifiera enskilda tecken. Viktigt här är att tecken markeras med enkelfnutt.
  • String är datatypen som används för att lagra strängar, vilket egentligen är en array (mer om detta senare) av enskilda tecken. Denna datatyp är lite mer komplex än de andra men går att använda relativt enkelt om man vill. Initiera en variabel på följande sätt String kurskod = "tiatil00s";.
  • När det gäller variabelnamn så kommer inte TinkerCad att godkänna namn som innehåller å/ä/ö utöver de vanliga reglerna att det inte får förekomma ett mellanslag i variabelnamnet eller att namnet börjar med en siffra.

3.3.3 Konkatening och utskrifter

Att skriva ut text och variabler kan göras på olika sätt.

Kodexempel: Exempel på utskrifter

String code = "tiatil00s";
String name = "Tillämpad Programmering";
int points = 100;

void setup()
{
  // Öppnar kommunikationen med Arduino, 9600 bps (bits/s)
  Serial.begin(9600);
  
  // #1. Skriv ut en sak i taget
  Serial.print(code);
  Serial.print(" - ");
  Serial.print(name);
  Serial.print(", ");
  Serial.print(points);
  Serial.println("p.");

  // #2. Skriv ut allt på en gång
  Serial.println(code + " - " + name + ", " + points + "p.");

  // #3. Skapa variabel för att bygga utskriftssträng
  String output = code + " - " + name + ", " + points + "p.";
  Serial.println(output);

  // #4. Float (decimaltal) kräver lite mer omtanke för att fungera.
  float f = 1.256812;
  // Använd String()-konstruktorn för att skapa en formaterad sträng 
  String s = String(f, 3);		// 3 decimaler
  Serial.println(s);			    // Serial.println(String(f,3)); funkar också...

  String out = String("Orginaltal: ") + String(f);
  Serial.println(out);
}

void loop()
{

}

3.3.4 Typkonvertera

Att typkonvertera innebär att man gör om data från en datatyp till en annan. Oftast handlar det om att konvertera mellan int och float. I de programmeringsspråk där man matar in saker via en konsoll/GUI så behöver man också kunna konvertera från text till tal men det är ju inget som vi behöver tänka på för tillfället. Vårt data som behandlas är ju av typen int eller float.

Kodexempel: Exempel på utskrifter

void setup(){
  Serial.begin(9600);
  int i;
  String s = "3";
  float f = 3.6;
  Serial.println(f);	// Skrivs ut med 2 decimaler

  i = (int) f; 			// När typkonverteringen går från float till int så tas decimaler bort
  Serial.println(i);	// Innehåller inga decimaler
  
  float g = (float) i;
  Serial.println(g);	// Två decimaler igen, men nu har talet förändrats

  // Konvertering från string till int mha hjälpfunktionen toInt()
  int si = s.toInt();
  Serial.println(si);

  // Konvertering från string till float mha hjälpfunktionen toFloat()
  float sf = s.toFloat();
  Serial.println(sf);
}

void loop()
{
}

Utskrift

3.60
3
3.00
3
3.00

3.4 Array vs LinkedList

När vi programmerade i Python så var det enkelt att använda listor. I Arduino PL så är det inte riktigt lika enkelt utan då behöver vi i stället göra ett val. Valet består mellan att skapa en Array, vilket är en variabel med ett fast antal komponenter som skapas när variabeln skapas, eller en LinkedList vilket är en lista som kan variera i längd. Denna listan kräver lite mer för att kunna användas.

3.4.1 Array

En array är en variabel som består av ett förutbestämt antal komponenter. När du väl har skapat en array så kan du inte förändra dess storlek, däremot kan du förändra innehållet i varje komponent.

Du deklarerar en array genom att tala om vilken datatyp den skall innehålla, ger den ett namn och talar om vilken storlke den skall ha. Om du tilldelar den värden samtidigt som du skapar den så behöver du inte ge arrayen en storlek utan då kommer den bli så stor som den skall vara så att alla värden får plats.

Att arbeta med specifika komponenter i en array gör du på samma sätt som du gjorde i en lista när vi jobbade med Python, du anger indexet för komponenten som du vill anropa. Tänk på att indexeringen börjar på 0.

Kodexempel: Exempel på användningen av arrayer

int i[3];				        // Deklarera variabeln i
int j[] = {1, 2, 3};	   // Initiera variabeln j
int k[3] = {1, 2, 3};	  // Initiera variabeln k
String s[3] = {"Anna","Bo","Cilla"};

void setup(){
  Serial.begin(9600);
  
  i[0] = 1;		      		// Tilldela variabeln i ..
  i[1] = 2;		      		// .. värden. Siffran inne i ..
  i[2] = 3;		      		// .. klammern är indexet.
  
  Serial.println(i[2]);	// Skriver ut sista komponenten
  Serial.println(j[2]);	// Skriver ut sista komponenten
  Serial.println(k[2]);	// Skriver ut sista komponenten
  Serial.println(s[2]);	// Skriver ut sista komponenten
}

void loop()
{
}

Utskrift

3
3
3
Cilla

3.4.2 LinkedList

Finns det inte bra lista i Arduino PL? Jo, men ingen som inbyggd. För att kunna använda det som kallas LinkedList så behöver du först ladda ner och importera en inkluderingsfil. En inkluderingsfil innehåller Arduinokod som behöver importeras när du skall göra tillägg till de inbyggda funktionerna som redan finns i Arduino PL. En inkluderingsfil har oftast filändelsen .h. Inkluderingsfilen till LinkedList hittar du här. Du kan inte köra med inkluderingsfiler i TinkerCad utan då får du köra koden på något annat sätt.

Kodexempel: Exempel på användningen av LinkedList

#include <LinkedList.h>
LinkedList<int> lista = LinkedList<int>();

void setup(){
  Serial.begin(9600);
  Serial.println("Start");
  
  // Lägg till 
  lista.add(1);
  lista.add(2);
  lista.add(3);
}

void loop()
{  
  Serial.print("Storlek på listan: ");
  Serial.print(lista.size());
}

Utskrift

Start
Storlek på listan: 3

Inkluderingsfilen måste läggas i samma mapp som övriga filer i ditt projekt.

3.5 Iterationer

Iterationer, eller loopar som vi känner dem, finns naturligtvis också i Arduino PL. Skillnanden mot vad ni har sett tidigare är främst hur for-loopen skrivs. Vi saknar också en foreach-loop, alltså en enkel loop som itererar över en array/lista.

Kodexempel: Exempel på användningen av loopar

int len = 3;				// Initierar len som är längden på arrayen
int j[] = {1, 2, 3};		// Initiera variabeln j 

void setup(){
  Serial.begin(9600);

  Serial.println("Skriv ut med for-loopen");
  for(int i = 0; i < len; i++){		// i existerar bara inne i for-loopen
    Serial.println(j[i]);
  }
  
  Serial.println("Skriv ut med while-loopen");
  int i = 0;				// Initierar i
  while (i < len){
    Serial.println(j[i]);
    i++;
  }
}

void loop()
{
}

Utskrift

Skriv ut med for-loopen
1
2
3
Skriv ut med while-loopen
1
2
3

Uppgift: Arrayer och loopar

Skapa en variabel som lagrar antalet komponenter i en array. Fyll sedan arrayen med värden, låt värdet som lagras vara indexet, en räknare eller något värde som är en funktion av index eller räknare. Loopa sedan igenom arrayen och skriv ut dess värden.

Tips: Fundera på om det gör någon skillnad att skapa arrayen innan funktionen setup() eller inne i funktionen setup().

Uppgift: Lista och loopar

Om du vill så testa att göra samma uppgift som ovan fast med en LinkedList istället.

Tips: Detta måste du göra på din egna dator och via Arduino IDE, eftersom LinkedList.h inte fungerar på TinkerCad.

3.5 exit()

Det finns en funktion som avbryter programmet och det är exit(0);, siffran i parantesen kan vara olika för att kunna meddela vilken exit() som har körts.

Viktigt att tänka på är att programmet slutar köras men Arduinon stängs inte av bara för att programmet inte längre körs så detta är en funktion som bäst används när något är problematiskt och bör kombineras med någon form av felmeddelande. Att använda det i utvecklingssyfte är en annan sak då man kan vilja analysera olika delar av koden utan att programmet hela tiden fortsätter att köras.

Kodexempel: Exempel på användningen av exit()

int i = 1;				// Initiera variabeln i 

void setup(){
  Serial.begin(9600);	// Öppnar kommunikationen med konsollen
}

void loop()
{
  Serial.println(i++);	// Skriv ut i och öka sedan dess värde med 1
  delay(500);			// Pausar 0.5 s
  if (i > 9){
    exit(0);			// Avbryt programmet
  }
}

Utskrift

0
1
2
3
4
5
6
7
8
9

3.6 Funktioner

För att ditt program skall vara lättare att följa så kan du skapa egna funktioner. Vi har i kursen programmering01 pratat om funktioner och samma anledningar som tidigare gäller fortfarande att en funktion bör skapas när samma sak behöver göras flera gånger, då pratar jag inte om att funktionen loop() körs flera gånger utan om att något inne i loop() behöver göras flera gånger, eller att det är kod som av annan orsak blir tydligare av att flyttas ut och separeras. Utgå ifrån att en funktion gör en sak, antingen en beräkning eller en åtgärd. Var inte rädd för att skapa flera funktioner.

Precis som tidigare så byggs en funktion upp av tre delar;

  1. Vad som returneras från funktionen, här anges datatypen på det som returneras eller void om inget returneras.
  2. Funktionens namn, tex even()
  3. Inne i parantesen så anges de eventuella inparametrar som skall skickas till funktionen, even(int i)

Viktigt att tänka på är hur synligheten gäller för våra variabler. I exemplet nedan så använder jag synligheten på olika sätt, ibland skickar jag med en inparameter och ibland så använder jag den globala variabeln som syns i alla programmets funktioner. Att göra som jag visar här är inte att rekommendera då jag är inkonsekvent, men se det mer som ett sätt att se på olika alternativ.

Kika också på hur jag kommenterar mina funktioner.

Kodexempel: Exempel på användningen av exit()

int i = 0;				// Initiera variabeln i 

void setup(){
  Serial.begin(9600);	// Öppnar kommunikationen med konsollen
}

void loop()
{
  i++;					// Öka värdet med ett
  delay(500);			// Pausar 0.5 s

  if (i % 2 == 0){		// Om talet är jämnt delbart med två ..
    Serial.println(even(i));
  } else if (i == 9){	// .. annars om talet är 9
    end();
  } else {				// .. annars
    other();
  } 
}

// Funktion: end
// Stänger av programmet
// Inparametrar: inget/none
// Returvärde: inget/none
void end(){
  Serial.println("Programmet avslutas");
  delay(200);			// Pausar 0.2 s för att hinna skriva ut texten
  exit(0);
}

// Funktion: even()
// Returnerar en sträng ifall talet är jämnt
// Inparameter: ett heltal
// Returvärde: en sträng med text
String even(int i){
  String s = "Talet " + String(i) + " är jämnt"; 
  return s;
}

// Funktion: other()
// Skriver ut vad talet har för värde
// Inparameter: inget/none
// Returvärde: inget/none
void other(){
  Serial.println("Talet är " + String(i));
}

Utskrift

Talet är 1
Talet 2 är jämnt
Talet är 3
Talet 4 är jämnt
Talet är 5
Talet 6 är jämnt
Talet är 7
Talet 8 är jämnt
Programmet avslutas

3.7 Slump

Slump är en vanligt förekommande funktionalitet i olika program. I Arduino PL så är slumpen lätt att jobba med då allt finns inbyggt. Det är dock viktigt att anropa randomSeed(analogRead(0)); för att få en unikt slumpad sekvens vid varje körning. Testa att kommentera bort denna koden och ditt program kommer slumpa samma sekvens varje gång.

Kodexempel: Exempel på användningen av exit()

int tal;

void setup(){
  Serial.begin(9600);			   // Öppnar kommunikationen med konsollen
  randomSeed(analogRead(0));	// Anges för att få unik slumpsekvens
}

void loop()
{
  tal = random(6) + 1; 		 	// Slumpar ett värde mellan 1 - 6
  delay(500);					       // Pausar 0.5 s
  Serial.println("Slumpat tal: " + String(tal));
  if (tal == 1){
    Serial.println("Programmet avbryts!");
    delay(200);
    exit(0);
  }
}

Utskrift

Slumpat tal: 6
Slumpat tal: 5
Slumpat tal: 6
Slumpat tal: 3
Slumpat tal: 1
Programmet avbryts!

Uppgift: Slump

Nu gör vi en uppgift baserad på slump, array, loopar och funktioner. Jag gillar ju att använda mig av tärning, och yatzy, när vi har möjlighet.

Din uppgift är att slå fem tärningar och räkna hur många gånger du behöver slå en yatzyhand innan du får yatzy. Inga tärningar sparas utan hela handen kastas om. För att få yatzy så skall alla fem tärningar ha samma värde.

Uppgift: Vidareutveckling 1

Om du upprepar denna undersökning flera gånger, kan du då skapa en resultatlista med hur många slag det kräver i varje omgång. Redovisa sedan hela undersökningen samt ett medelvärde på dina resultat.

Uppgift: Vidareutveckling 2

Sannolikheten att få yatzy på ett slag är 1 på 1296. Du borde alltså få ca 15.4 yatzy på 20000 slag, vilket resultat kan du nå i en undersökning? Hur skiljer sig resultatet om du kör undersökningen 1, 3, 7, 20, 50 ggr?

3.8 Knapptryck

Tidsbrist

Tyvärr har jag inte hunnit att bygga mer content. Här kommer jag lägga ett avsnitt om hur man hanterar en knapp och hur denna knappen styr simulerningen av en tärning.