Structuur en syntax

Structuur

Programmeren van de Arduino gebeurt in C/C++. Deze taal is zeer gestructureerd en is opgebouwd uit functies.

Een C-programma bevat altijd een functie main(). Bij de Arduino bestaat er geen main() functie, maar is deze vervangen door twee andere functies: setup() en loop().

de basis van elk Arduino programma

 

Wanneer een Arduino wordt opgestart zal hij altijd eerst de setup()-functie uitvoeren, om daarna de loop()-functie blijvend te herhalen.

Het is natuurlijk heel eenvoudig om te zeggen dat een programma uit functies bestaat, maar wat is nu eigenlijk een functie?

Een functie is een groep van instructies die met een eigen naam aangeduid worden. Een functie geeft altijd een resultaat van een bepaald type weer. Indien geen resultaat wordt teruggegeven door de functie is het resultaat van het type void (=leegte). De functies setup() en loop() zijn typisch twee functies die niks teruggeven. Ze zijn dan ook allebei void.

Het type van de teruggegeven waarde is de eerste aanduiding waarmee een functie beschreven wordt. Zo kan een functie bijvoorbeeld een som van twee gehele getallen opleveren. De definitie van deze functie begint dan met de type-aanduiding int.

Na de type-aanduiding krijgen we de naam van de functie. Deze naam kan naar believen gekozen worden, zolang hij maar begint met een letter. Merk op dat namen in C hoofdlettergevoelig zijn.

Na de naam komt een paar haakjes. Deze haakjes kunnen eventuele parameters bevatten die aan de functie worden doorgegeven. Ook als er geen parameters worden doorgegeven moeten de haakjes na de functienaam komen. Dat is terug het geval bij zowel setup() als loop().

We weten nu al wat het resultaat van de functie zal zijn, wat zijn naam is, en met welke parameters de functie zal rekenen, maar hoe bepalen we welke instructies er wel en niet tot de functie behoren?

Daarvoor worden accolades gebruikt. Alles wat binnen de accolades staat behoort tot de functie. Elke instructie wordt van de volgende gescheiden door een puntkomma.

Terug naar de basisstructuur

Zoals reeds eerder vermeld bestaat de basis van een arduino programma uit twee functies:
  • Setup()
  • Loop()

Beide functies zijn van het type void, omdat ze geen informatie teruggeven aan de aanroepende functie. Die is er immers niet.

De setup() functie wordt éénmaal doorlopen en dient dus hoofdzakelijk voor initialisatie. De loop()-functie wordt steeds opnieuw herhaald en vormt een oneindige lus. Hieruit kun je al concluderen dat een microcontroller nooit stopt met werken.

voorbeeldprogramma: Blink

 

Het eerste programma dat gebruikt wordt is zo goed als altijd het blink-programma. Met dit programma kijken we of de arduino omgeving werkt, en of we in staat zijn om een programma op het arduino-bord te laten uitvoeren.

We zien duidelijk de twee basisfuncties setup() en loop().

In de setup wordt pin 13 van het arduino bord geconfigureerd als een output. Op het bord zit een SMD-led die met deze pin 13 verbonden is. Door deze te laten knipperen kunnen we het  bord testen zonder dat we extra hardware nodig hebben.

Het laten knipperen gebeurt in de loop-functie. Met behulp van een digitalWrite instructie wordt pin 13 achtereenvolgens hoog en laag gezet. De controller werkt aan een zeer hoge snelheid, waardoor het nodig is om na elke digitalWrite instructie een delay-functie te gebruiken om het knipperen zodanig te vertragen dat het voor een mens nog te volgen is.

De waarde die als parameter bij de delay-functie wordt meegegeven is een tijd, uitgedrukt in milliseconden. De delay(1000) instructie zal de controller dus 1 seconde doen pauzeren.

syntax

Een programma zal slechts succesvol gecompileerd worden als de syntax van het programma klopt. Dit betekent dat alle code volgens strikte regels moet geschreven worden.

Het groeperen van een aantal instructies gebeurt met behulp van accolades. Dit kan zowel bij de definitie van een functie voorkomen, als bij een instructie zelf, zoals te zien in de for-loop.

gebruik van accolades om statements te groeperen.

Een instructie wordt altijd beëindigd met een puntkomma. Een einde-regel teken heeft geen enkele betekenis binnen een programma. Je kan dus perfect alle instructies op één lijn schrijven, zolang ze maar van elkaar gescheiden zijn door middel van een puntkomma.

In sommige gevallen wil men tekst aan het programma toevoegen om iets te verduidelijken voor iedereen die het programma leest. Het is niet de bedoeling dat deze tekst aan de syntax voldoet. Het gaat hier dus om commentaar, dat door de compiler moet worden overgeslagen.

Het gebruik van commentaar is dan ook zeer belangrijk om in je programma aanwijzingen op te nemen over het verloop of het doel van het programma. Deze wordt door de compiler genegeerd, en zal ook geen invloed hebben op de snelheid of grootte van je programma.

Een eerste manier is de blok commentaar. De combinatie forward slash en sterretje geeft aan dat alles wat hierna komt commentaar is. Je kunt hiermee dus grote stukken commentaar plaatsen tussen de code in. De commentaar wordt afgesloten met de combinatie sterretje-forward slash. Deze combinatie geeft het einde van de blokcommentaar aan. Alles erna wordt weer behandeld als programmacode.

Wanneer we korte commentaar willen toevoegen kunnen we ook gebruik maken van regel commentaar. Deze wordt aangegeven door een dubbele forward slash en wordt niet afgesloten. Alles wat na de dubbele slash staat op deze ene regel wordt als commentaar behandeld. Door naar een volgende regel te gaan wordt de commentaar beëindigd.

Arduino

De naam Arduino slaat op het geheel van controller, bord waarop de controller is bevestigd en software omgeving waarmee de controller geprogrammeerd wordt. Het geheel wordt aangeduid met de term Arduino IDE, waarbij de IDE staat voor “integrated development environment, of geïntegreerde ontwikkelomgeving.

 

De Arduino omgeving is oorspronkelijk opgebouwd door een team van Italianen rond een Atmel microcontroller. De eerste versie (UNO) bevatte een AtMega168, maar al gauw werd overgestapt op de AtMega328p.

 

De Leonardo die wij gebruiken in deze cursus is opgebouwd rond de AtMega 32U4. Daar waar de AtMega328p via een serieel protocol wordt geprogrammeerd, en de Arduino UNO dus ook een USB-naar-serieel omzetter bevat, kan deze bij de Leonardo worden weggelaten. De Leonardo kan rechtstreeks via USB worden aangesloten, en heeft daardoor het bijkomende voordeel dat hij kan geprogrammeerd worden om zich te gedragen als een toetsenbord of als een muis wanneer hij aan een PC wordt gehangen.

AtMega32U4
AtMega328p

 

De AtMega328p en AtMega32u4 zijn microcontrollers, maar geen Arduino's. Arduino is de verzamelnaam voor het geheel van microcontroller, bord waarop de controller geplaatst is, en ontwikkelomgeving. Deze ontwikkelomgeving staat bekend als de Arduino IDE.

Arduino Leonardo

Zoals te zien op bovenstaande afbeelding bevat de Arduino Leonardo naast de AtMega32u4 controller ook nog enkele chips voor o.a. spanningsregeling, een resetknop, een USB-aansluiting enz. Het is dus dit geheel dat wordt aangeduid met de term Arduino.

Wat is een microcontroller?

We kennen allemaal de computer, een toestel voor allerlei soorten gegevensverwerking. Je kunt er een tekst mee maken door hem als tekstverwerker te gebruiken, maar evengoed kun je hem als rekenmachine gebruiken wanneer je een rekenbladtoepassing start. Zelfs foto- en videobewerking zijn met dit ene toestel mogelijk. Het is dus een universeel toestel, ontwikkeld om gebruikt te worden met de software die er op geïnstalleerd staat.

Een microcontroller is vergelijkbaar met een computer. Het bevat veel dezelfde onderdelen zoals een CPU, geheugen of ALU, maar de opzet van een microcontroller is anders. Een microcontroller is een “dedicated” toestel. Het is ontworpen en wordt geprogrammeerd om één specifieke taak uit te voeren. Dit kan bijvoorbeeld de sturing van een wasmachine of een magnetron zijn, maar kan ook een temperatuurregeling in een 3D printer of het anti-blokkeer systeem van een auto zijn.

Doordat de microcontroller voor één toepassing bedoeld is, zal hij veel minder “resources” zoals rekenkracht, geheugen en opslag nodig hebben in vergelijking met een laptop of PC. Meestal zal er zelfs maar een heel beperkte HMI (human-machine interface) aanwezig zijn. De interactie met de gebruiker beperkt zich dikwijls tot enkele leds die de status aangeven. Scherm en toetsenbord zullen dus dikwijls niet in het systeem aanwezig zijn.

Er bestaan honderden verschillende families van microcontrollers, die elk uit tientallen leden bestaan. Eén van de uitdagingen bij het ontwerpen van een microcontrollersturing is bijvoorbeeld de juiste keuze van controller. Die keuze kan slaan op rekenkracht, beschikbaar geheugen, snelheid maar ook op verbruik en omgevingstemperatuur.

assembler
Zoals we later zullen zien kan een microcontroller op veel verschillende manieren geprogrammeerd worden. De controller zelf verstaat alleen maar machinetaal. Dit is het laagste programmeerniveau. Op een volgend niveau kan de controller geprogrammeerd worden in assembler. Dit is nog steeds een laag niveau waarop de programmeur alles moet programmeren aan de hand van de instructieset. In assembler bestaat bijvoorbeeld geen lus-instructie.

Gelukkig bestaan er ook hogere programmeertalen. Bij het programmeren van de Arduino maken we gebruik van C/C++ om onze programma’s te schrijven. Deze taal en bijhorende compiler bieden heel wat tools om het programmeren gemakkelijker en aangenamer te maken.

Oplossing: SOS opgave

Uit de opgave blijkt dat een punt een hoog signaal van 0,1s is, terwijl een streep 0,3s duurt. Beide worden gevolgd door een laag signaal van 0,1s. Tussen twee letters van een woord is een laag-tijd van 0,3s en tussen twee woorden een laag-tijd van 0,7s.

de initialisatie

De morsecode wordt weergegeven op een led die op pin 12 wordt aangesloten. In onze setup() functie moeten we pin 12 definiëren als een uitgang. Dit doen we door middel van de instructie

pinMode(12,OUTPUT);

Merk op dat de functienaam hoofdlettergevoelig is, dus PinMode() of pinmode() zullen door de compiler niet herkend worden.

De functie pinMode heeft twee parameters. De eerste parameter is de naam van de pin die als output moet gedefinieerd worden. De digitale pinnen die als digitale uitgang kunnen gebruikt worden, worden aangeduid met een nummer (0 t.e.m. 13). Aangezien de led op pin 12 verbonden is moet hier dus als eerste parameter een 12 komen, of gelijk welke uitdrukking die in de waarde 12 resulteert.

Dus ook

pinMode(24/2,OUTPUT);

resulteert in pin 12 die als uitgang geconfigureerd wordt.

Het heeft natuurlijk geen enkele zin om een ingewikkelde constructie te gebruiken die resulteert in de waarde 12 zonder dat dit een meerwaarde oplevert. Er zijn echter enkele nuttige toepassingen van deze regel, die de leesbaarheid van ons programma verhogen, of die het eenvoudiger maken op het programma op termijn te onderhouden.

Soms is het vooraf niet duidelijk welke pin er zal gebruikt worden voor welke sensor of drukknop of led. In het geval van deze opgave hangt de led aan pin 12, maar het kan zijn dat in een latere versie van het programma het beter uitkomt als deze led aan pin 3 hangt. Als ons programma tegen dan al enkele bladzijden lang is moeten we via zoeken en vervangen in het volledige programma de waarde 12 vervangen door de waarde 3. Dit kan tot fouten leiden. In zo'n geval is het handig als we de waarde 12 vervangen door een betekenisvolle omschrijving zoals ledPin. Wanneer we het over ledPin hebben maakt het niet uit aan welke pin de led hangt, zolang de waarde van ledPin maar resulteert in het juiste pinnummer. We gebruiken daarvoor bijvoorbeeld een constante.

const byte ledPin = 12;

void setup() {
  pinMode(ledPin,OUTPUT);
}

Door de constante op de eerste lijn te definiëren zal deze constante doorheen het volledig programma gekend zijn. We kunnen dus in elke functie verwijzen naar ledPin.

Mocht het nodig zijn om de led op pin3 te plaatsen, dan hoeven we alleen in het begin van ons programma de regel

const byte ledPin = 12;

te veranderen in

const byte ledPin = 3;

opdat de aanpassing overal in het verdere programma zou overgenomen worden.

Bijkomend voordeel is dat de naam ledPin ook veel meer zegt dan de waarde 12. Wanneer we in een programma de regel

pinMode(ledPin,OUTPUT);

tegenkomen is het direct duidelijk dat deze regel code iets met een led te maken heeft.

De tweede parameter van de functie pinMode geeft aan hoe de pin moet geconfigureerd worden. In ons geval moet deze pin een uitgang worden waar de led aan hangt. We gebruiken daarvoor de waarde OUTPUT. Om een pin als input te gebruiken bestaan de uitdrukkingen INPUT en INPUT_PULLUP. Bij de laatste wordt een interne pullup-weerstand ingeschakeld die er bij de gewone INPUT niet is.

Het programma

Nu we pin 12 gedefinieerd hebben als een output wordt het tijd om onze loop functie te schrijven die achtereenvolgens drie punten, drie strepen en drie punten op de led toont. We gaan achtereenvolgens een aantal oplossingen bekijken die van zeer ruw naar verfijnd gaan.

quick and dirty

In de eerste oplossing maken we gebruik van de instructie digitalWrite om de uitgang hoog of laag te maken. Daarna gebruiken we de delay() functie om een aantal milliseconden te wachten. We kunnen met deze twee functies het volledig programma schrijven:

const byte ledPin = 12;

void setup() {
  pinMode(ledPin,OUTPUT);
}

void loop() {
  digitalWrite(ledPin,HIGH);
  delay(100);
  digitalWrite(ledPin,LOW);
  delay(100);
  digitalWrite(ledPin,HIGH);
  delay(100);
  digitalWrite(ledPin,LOW);
  delay(100);
  digitalWrite(ledPin,HIGH);
  delay(100);
  digitalWrite(ledPin,LOW);
  delay(300);

  digitalWrite(ledPin,HIGH);
  delay(300);
  digitalWrite(ledPin,LOW);
  delay(100);
  digitalWrite(ledPin,HIGH);
  delay(300);
  digitalWrite(ledPin,LOW);
  delay(100);
  digitalWrite(ledPin,HIGH);
  delay(300);
  digitalWrite(ledPin,LOW);
  delay(300);

  digitalWrite(ledPin,HIGH);
  delay(100);
  digitalWrite(ledPin,LOW);
  delay(100);
  digitalWrite(ledPin,HIGH);
  delay(100);
  digitalWrite(ledPin,LOW);
  delay(100);
  digitalWrite(ledPin,HIGH);
  delay(100);
  digitalWrite(ledPin,LOW);
  delay(700);
}

Dit programma zal perfect werken, maar is absoluut geen elegant programma.

iets beter: lussen.

De opgave schreeuw het haast uit: drie punten, drie strepen, drie punten. Het spreekt vanzelf dat we voor het maken van drie strepen en drie punten gebruik gaan maken van een lus. We bekijken het voorbeeld van de drie punten, zoals het in het vorig programma staat:

digitalWrite(ledPin,HIGH); 
delay(100); 
digitalWrite(ledPin,LOW); 
delay(100); 
digitalWrite(ledPin,HIGH); 
delay(100); 
digitalWrite(ledPin,LOW); 
delay(100); 
digitalWrite(ledPin,HIGH); 
delay(100); 
digitalWrite(ledPin,LOW); 
delay(300);

We zien dat eenzelfde stuk code drie keer herhaald wordt. Als we deze code in een lus steken wordt dit stuk code aanzienlijk korter. Om een lus te maken gebruiken we een for-instructie.

  for(int i=0;i<3;i++) {
    digitalWrite(ledPin,HIGH);
    delay(100);
    digitalWrite(ledPin,LOW);
    delay(100);
  }
  delay(200);

De for instructie is een functie die drie parameters nodig heeft.

  • De eerste parameter is een variabeledefinitie. Deze variabele fungeert als teller om te weten hoeveel keer we de lus doorlopen. Deze variabele wordt bij de definitie ook geïnitialiseerd op een startwaarde.
  • De tweede parameter is een voorwaarde. Zolang als aan deze voorwaarde voldaan wordt zal de lus opnieuw worden uitgevoerd.
  • De derde parameter is een bewerking die op de tellervariabele wordt uitgevoerd, elke keer dat de lus doorlopen wordt.

In bovenstaand voorbeeld wordt teller i geïnitialiseerd op waarde 0. De voorwaarde wordt gecontroleerd en aangezien 0<3 is zal de lus uitgevoerd worden en wordt een eerste morse-punt op de led weergegeven.

Na deze eerste doorgang van de lus wordt i met 1 verhoogd door de i++ van de derde parameter en krijgt dus de waarde 1. De voorwaarde van de tweede parameter wordt opnieuw gecontroleerd en 1<3, dus wordt de lus opnieuw doorlopen en wordt de tweede morse-punt weergegeven.

Na deze doorgang wordt i terug verhoogd en wordt nu 2. Dit is nog altijd kleiner dan 3, dus wordt de lus een derde maal doorlopen. Het derde morse-punt wordt weergegeven.

De teller i wordt opnieuw verhoogd en wordt nu 3. De voorwaarde is niet langer voldaan, dus de lus wordt niet meer uitgevoerd en het programma gaat verder met de code na de lus. Dit is de delay(200) die er voor zorgt dat er een totale pauze van 0,3s ontstaat tussen deze S en de volgende letter.

Uiteraard kunnen we op dezelfde manier ook de drie strepen programmeren. We hoeven enkel de eerste delay(100) te veranderen naar delay(300) om netjes drie strepen te krijgen. Zo kunnen we een nieuw programma maken:

const byte ledPin = 12;

void setup() {
  pinMode(ledPin,OUTPUT);
}

void loop() {
  for(int i=0;i<3;i++) {
    digitalWrite(ledPin,HIGH);
    delay(100);
    digitalWrite(ledPin,LOW);
    delay(100);
  }
  delay(200);

  for(int i=0;i<3;i++) {
    digitalWrite(ledPin,HIGH);
    delay(300);
    digitalWrite(ledPin,LOW);
    delay(100);
  }
  delay(200);

  for(int i=0;i<3;i++) {
    digitalWrite(ledPin,HIGH);
    delay(100);
    digitalWrite(ledPin,LOW);
    delay(100);
  }
  delay(600);
}

Merk op dat dit programma al een stuk korter is dan het vorige.

nog eleganter: functies

In bovenstaand programma valt op dat de twee lussen voor de drie punten identiek zijn. Wanneer dezelfde code herhaald wordt in het programma gaat men meestal deze code éénmaal schrijven en er een functie van maken, die dan verschillende keren kan opgeroepen worden vanuit het hoofdprogramma. Soms maakt men van grotere stukken code ook een functie, hoewel die maar één keer in het programma voorkomt. Men doet dit om het hoofdprogramma zo kort mogelijk te houden, en de leesbaarheid te verhogen.

In ons voorbeeld kunnen we twee functies maken: we noemen ze driepunt() en driestreep(). De ene functie zal drie punten weergeven terwijl de andere functie drie strepen op de led zal weergeven.

Een eigen functie maken is eenvoudig. Eerst moeten we bepalen welke waarde de functie zal teruggeven aan het aanroepende programma. In ons geval is dit niks, dus begint de functie met void. Daarna plaatsen we een zelfgekozen naam, in dit geval driepunt. Een functie kan parameters bevatten die tussen haakjes aangegeven worden. Hier zijn er geen parameters, maar de haakjes moeten toch altijd geplaatst worden. Onze functiedefinitie wordt dus:

void driepunt() {
  for(int i=0;i<3;i++) {
    digitalWrite(ledPin,HIGH);
    delay(100);
    digitalWrite(ledPin,LOW);
    delay(100);
  }  
}

de code die moet worden uitgevoerd plaatsen we tussen accolades na de functiedefinitie.

Op dezelfde manier definiëren we een functie driestreep().

Het aanroepen vanuit het hoofdprogramma gebeurt door de functienaam en de haakjes te plaatsen. Het nieuwe programma is:

const byte ledPin = 12;

void setup() {
  pinMode(ledPin,OUTPUT);
}

void driepunt() {
  for(int i=0;i<3;i++) {
    digitalWrite(ledPin,HIGH);
    delay(100);
    digitalWrite(ledPin,LOW);
    delay(100);
  }  
}

void driestreep() {
  for(int i=0;i<3;i++) {
    digitalWrite(ledPin,HIGH);
    delay(300);
    digitalWrite(ledPin,LOW);
    delay(100);
  }
}

void loop() {
  driepunt();
  delay(200);
  driestreep();
  delay(200);
  driepunt();
  delay(600);
}

nog compacter

Bovenstaand programma kan nog compacter gemaakt worden wanneer we de beide functies samen nemen in één functie en het onderscheid maken d.m.v. een parameter.

We maken één functie drie() met een parameter lengte, die bepaald of het om drie punten of drie strepen gaat. Is de parameter lengte gelijk aan 0 dan maken we punten, bij een 1 maken we strepen. Om het leesbaar te houden gaan we voor de waarde 0 en 1 constanten maken met als naam kort en lang.

het totale programma wordt nu

const byte ledPin = 12;
const byte kort = 0;
const byte lang = 1;

void setup() {
  pinMode(ledPin,OUTPUT);
}

void drie(byte lengte) {
  for(int i=0;i<3;i++) {
    digitalWrite(ledPin,HIGH);
    if(lengte==kort) {delay(100);} else {delay(300);}
    digitalWrite(ledPin,LOW);
    delay(100);
  }  
}

void loop() {
  drie(kort);
  delay(200);
  drie(lang);
  delay(200);
  drie(kort);
  delay(600);
}

De if-instructie zorgt er voor dat afhankelijk van de parameter lengte een punt of een streep gemaakt wordt.

Op internet is een simulatie te vinden met deze oplossing via deze link.