Arduino – Zusammenfassung

MCT - Grundlagen

Variablen

Variablen dienen als Zwischenspeicher für Werte. Je nach dem welcher Datentyp einer Variable zugewiesen wird, kann diese

  • Zustände (boolean),
  • Bytes (byte),
  • Ganzzahlen (int),
  • Gleitkommazahlen (float, double),
  • Texte (String)
  • oder einzelne Zeichen (char)

speichern. Jedoch erfolgt die Speicherung flüchtig! Nach jedem Reset der Versorgungsspannung oder des Controllers werden diese Informationen gelöscht. Eine dauerhafte Speicherung ermöglicht nur das Ablegen der Daten im EEPROM.

Lokale und globale Variablen

Der Geltungsbereich einer Variable wird durch die Position im Quelltext definiert. Wird zum Beispiel eine Variable zwischen zwei { } definiert, so gilt diese nur innerhalb dieser beiden Klammern. Andere Programme, welche außerhalb dieser zwei Klammern definiert sind, haben keinen Zugriff auf diese Variable.


void loop()
{
//Lokale Variablen
//Diese Variablen sind nur in der void loop() gültig
int a = 5;
int b = 8;
boolean ergebnis = false;

ergebnis = vergleiche(a, b);

}

boolean vergleiche(int a, int b)
{
//Diese Variable ist komplett unabhängig zu der in der loop() Funktion,
//auch wenn diese gleich heißen!
boolean ergebnis = (a > b ? true : false);
return ergebnis;
}

Um nun in Arduino globale Variablen zu erstellen, so müssen diese einfach außerhalb der void setup() und der void loop() Routine erstellt werden.


//Globale Variable
int wert = 5;

void setup()
{
//Lokale Variable
int wert_lokal = 3;
}

 

Arrays

Es lässt sich aus jedem Datentyp auch ein Array erstellen. Ein Array ist in der Lage mehrere einzelne Elemente vom selben Datentyp zwischen zu speichern. Dabei gibt es mehrere Möglichkeiten ein Array zu deklarieren.

1-Dimensionale Arrays


//1) Größe wird durch Anzahl der Daten festgelegt
int ZahlenArray[] = {0,1,2,3,4};

//2) Größe wird durch eine Zahl festgelegt
int ZahlenArray[5] = {0,1,2,3,4};

//3) Größe wird durch eine konstante (!wichtig!) Variable festgelegt
const int SIZE = 5;
int ZahlenArray[SIZE] = {0,1,2,3,4};

 

2-Dimensionale Arrays

Es sind auch Arrays in zwei oder Dimensionen möglich, jedoch wird das verwenden von Arrays mit jeder dazukommenden Dimension komplizierter und unübersichtlicher. 1D & 2D sind jedoch häufig zu finden.


//1) 2D Array [2 in y-Richtung][5 in x-Richtung]
int ZahlenArray[2][5] = {
{0,1,2,3,4},
{5,6,7,8,9}
};

 

Zugriff auf ein Array

Um nun auf Daten eines Arrays zugreifen zu können, muss auf den gewünschten Index des Arrays zugegriffen werden.


void loop()
{
for (int i = 0; i < sizeof(ZahlenArray) / sizeof(int); i++)
{
Serial.print("Wert ");
Serial.print(i);
Serial.print(": ");
//Zugriff auf den Index i (0 - 4)
Serial.println(ZahlenArray[i]);
}
//Wartet auf Hardware-Reset
while(true);
}

Um nun Daten in ein Array zu schreiben:


//Schreibt 4 auf die Position 2(sprich den 3. Wert).
ZahlenArray[2] = 4;

0

0

Back To Top

I/O

pinMode

Mit dem pinMode Befehle lassen sich die I/O entweder als Ausgang oder als Eingang definieren.


void setup()
{
//OUTPUT oder INPUT
pinMode(13, OUTPUT);
pinMode(5, INPUT);
}

Der Pin 13 des Arduino’s ist die interne LED. Diese wird mit Hilfe des pinMode(PIN, MODE) Befehl als Ausgang gesetzt. Dieser Befehl funktioniert für analoge und digitale I/O’s.

Digitale Pins

Digitale Pins können nur zwei logische Zustände einnehmen, 0 (LOW) oder 1 (HIGH). Dabei sollte ein digitaler Pin nie offen betrieben werden, da sonst ein ständiges Wechseln zwischen LOW und HIGH auftritt (Der Eingang “floatet”)

digitalWrite

Um nun auf die digitalen Pins schreiben zu können, wird der digitialWrite(PIN, STATE) Befehl verwendet. Um nun die LED, welche sich an Pin 13 befindet und als Ausgang definiert ist, einzuschalten, kann folgender Code verwendet werden.


void setup()
{
pinMode(13, OUTPUT);
//HIGH oder LOW
digitalWrite(13, HIGH);
}

Um nun die LED wieder auszuschalten, muss der Parameter HIGH mit LOW ersetzt werden.

digitalRead

Es ist auch möglich den Zustand eines digitalen Pins auszulesen. Dazu wird der Befehl digitalRead(PIN) verwendet. Der Befehl digitalRead() liefert dabei entweder eine logische 0 (LOW) oder eine logische 1 (HIGH) zurück. Nun könnte man, aus der Kombination der beiden Befehle, die LED in einem Takt von 2 Sekunden blinken lassen.


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

void loop()
{
//2 Sekunden warten
delay(2000);
//Schaltet den Zustand um
digitalWrite(13, !digitalRead(13));
}

 

analogWrite

analogWrite hat nichts mit den analogen Pins zu tun! Dieser Befehl erzeugt ein PWM Signal auf PWM-fähigen Digitalpins (~)!

Um nun auf einen analogen Pin schreiben zu können, wird der analogWrite(PIN, VALUE) Befehl verwendet, wobei VALUE zwischen 0 – 255 liegen muss! Je nach übergebenen Wert wird ein PWM Signal mit dementsprechender Aus- und Einzeit generiert.

  • analogWrite(9, 0) = Immer aus
  • analogWrite(9, 128) = 50% Aus/Ein
  • analogWrite(9, 255) = Immer ein

void setup()
{

//nicht zwingend nötig
pinMode(9, OUTPUT);
//0 - 255
//In diesem Fall wird die Hälfte der Zeit 5V und die andere Hälfte 0V ausgegben (PWM)
analogWrite(9, 128);
}

 

Analoge Pins

Im Gegensatz zu den digitalen Pins kann ein analoger Pin jeden beliebigen Wert zwischen 0 – 1023  (da mit 1024 Stufen quantisiert wird) annehmen.

AnalogRead

Es ist auch möglich den Zustand eines analogen Pins auszulesen. Dazu wird der Befehl analogRead(PIN) verwendet. Der Befehl analogRead() liefert dabei einen Wert zwischen 0 – 1023 zurück.


byte wert = 0;
void setup()
{
//Angenommen am Pin A0 ist ein Poti angeschlossen
pinMode(A0, INPUT);
Serial.beginn(9600);
}

void loop()
{
if(wert != analogRead(A0))
{
//Da ein Byte nur max 255 speichern kann, wird der Wert durch 4 geteilt
wert = (analogRead(A0) / 4);
Serial.print("Wert = ");
Serial.println(wert);

}
//0.2 Sekunden warten
delay(200);
}

0

0

Back To Top

Funktionen & Routinen

Relationen

Sind Unterprogramme, welche zwar eine Funktion ausführen, jedoch keinen Rückgabewert haben.


void setup()
{
Serial.beginn(9600);
}

void printText(String text)
{
Serial.println(text);
}

 

Funktionen

Eine Funktion sind Unterprogramme, welche einen Wert zurückliefern. Werden ähnliche Funktionen öfters, jedoch mit anderen Werten benötigt, macht es Sinn, eine Funktion daraus zu erstellen.


boolean inRange(int value, int max, int min)
{
//Wenn der Wert außerhalb des übergebenen Bereiches ist
//so gibt die Funktion false zurück
if(value > max || value < min) return false;
//ansonsten true
else return true;
}

0

0

Back To Top

Operationen

Je nach Datentyp stehen unterschiedliche Operationen zur Verfügung.

Arithmetik


int a = 0; //Ergebniss bzw. Wertzuweisung
a = a + 2 //Addition
a++ //Inkrementieren um 1
a = a - 2 //Subtraktion
a-- //Dekrementieren um 1
a = a * 3 //Multiplikation
a = a / 3 //Division
a = a%3 //Modulo (Restwert)

Vergleich


int a = 0, b = 0;

a == b //Ist gleich
a != b //Ist ungleich
a >= b //a größer/gleich b
a > b //a größer b
a <= b //a kleiner/gleich b
a < b //a kleiner b

Bitweise Arithmetik


& //Bitweise UND
| //Bitweise ODER
~ //Bitweise NICHT

Boolsche Arithmetik


&& //UND
||  //ODER
!     //Nicht

0

0

Back To Top

Interrupts

Interrupts

Je nach Board können unterschiedlich viele Interrupts an fest definierten Pins erstellt werden. Ja nach Parametrierung regiert das Programm, wenn es ein Signal auf diesem Pin erhält und springt in die angegebene Routine. Dabei ist es völlig egal, wo das Programm gerade steht (auch während eines Delay wird ein Interrupt erkannt).

Eine Liste der Interrupt-fähigen Pins gibt’s hier.

Interrupts – Arduino Referenzen

Interrupt aktivieren

Um auf Interrupts “zu hören” muss der dementsprechende Pin erst aktiviert werden. Als Grundlage dient nun folgendes Programm.

//Interrupt-fähige Pins für den UNO: 2,3
//Für andere Boards:
//https://www.arduino.cc/en/Reference/AttachInterrupt

byte Taster = 3;
byte led = 13;

void setup()
{
pinMode(led, OUTPUT);
pinMode(Taster, INPUT);
digitalWrite(Taster, HIGH);
/*
* Da der Taster LOW-aktiv ist, wird der Interrupt dann ausgelöst,
* wenn der Zustand von HIGH auf LOW geht, sprich der Taster
* gedrückt wird.
*/
//Aktivieren des Interrupts auf Pin 3 (Taster)
//FALLING || RISING || CHANGE || LOW || (HIGH ,nur auf Arduino Due, Zero, MKR1000)
attachInterrupt((digitalPinToInterrupt(Taster)), Funktion, FALLING);

//Serielle Schnittstelle
Serial.begin(9600);
while(!Serial);

}

void loop()
{
noInterrupts();
//Hier wird kein Interrupt ausgeführt -> Für zeitkritsichen Code

interrupts();
delay(200);
digitalWrite(led, !digitalRead(led));
// Ab hier werden wieder Interrupts aufgeführt

}

void Funktion()
{
/*
* ACHTUNG!
* Hier funktioniert der Befehl delay(ms) nicht!
* Auch der Befehl millis() wird hier nicht erhöht!
*/
Serial.println("Interrupt erkannt!");
Serial.flush();
}

 

Erläuterung zum Code

In der Setup() Routine wird der Interrupt durch attachInterrupt(); aktiviert. Dabei kann entweder direkt die Interrupt-Nummer (Pin 2 = 0, Pin 3 = 1) verwendet werden oder, was verständlicher ist, direkt der Pin. Dazu muss jedoch die Unterfunktion digitalPinToInterrupt(PIN) verwendet werden.

attachInterrupt((digitalPinToInterrupt(INTERRUPT_PIN)), FUNKTIONSNAME, MODUS);

Moden sind

  • FALLING
  • RISING
  • LOW
  • CHANGE
  • HIGH (nur auf Arduino Due, Zero, MKR1000)

Enthält das Programm nun zeitkritische Funktionen, so muss noInterrupts(); vor diesem Code und Interrupts(); nach diesem Code aufgerufen werden.

Wird nun ein Interrupt erkannt, so springt das Programm sofort in die angegebene Routine und führt die Programmzeilen dort aus. Danach fährt das Programm dort fort, wo der Interrupt betätigt wurde.

0

0

Back To Top

Kontrollstrukturen

If / Else

Einfache if-Anweisung

Eine if-Anweisung wird nur betreten, wenn die angegebene Bedingung erfüllt, sprich TRUE ergibt.


boolean zustand = true;
//Betreten bei TRUE
if(zustand == true)
{
Serial.println("Bedingung erfüllt!");
}

Die obige if-Anweisung wird also nur betreten, wenn die Variable zustand den Wert TRUE hat. Ein weitere Möglichkeit das selbe zu erzielen, ist mit folgender Schreibweise.


boolean zustand = true;
//Betreten bei TRUE
if(zustand)
{
Serial.println("Bedingung erfüllt!");
}

Das gleiche Spiel ist auch mit dem “Nicht-Zustand” möglich.


boolean zustand = false;

if(zustand == false)
{
Serial.println("Bedingung erfüllt!");
}
//Oder
if(!zustand)
{
Serial.println("Bedingung erfüllt!");
}

 

If- / Else-Anweisung

Um nun eine “Entweder Oder”-Entscheidung zu realisieren, kann eine if-Anweisung mit de else ergänzt werden. Das Programm, welches sich in der else-Anweisung befindet, wird dann ausgeführt, wenn (if) die Bedingung nicht erfüllt ist.


boolean zustand = false;

if(zustand)
{
Serial.println("Bedingung erfüllt");
}
else
{
//Diese Anweisung(en) werden nur ausgeführt, wenn
//die Bedingung im if nicht erfüllt ist.
Serial.println("Bedingung nicht erfüllt");
}

Es ist auch möglich mehrere if-Bedingungen nacheinander anzureihen.



int nummer = 3;

if (nummer == 0) Serial.println("Option 0: Startet...");
else if(nummer == 1) Serial.println("Option 1: Startet...");
else if(nummer == 2) Serial.println("Option 2: Startet...");
else if(nummer == 3) Serial.println("Option 3: Startet...");
else
{
//Wird nur ausgeführt, wenn alle 4 obigen Bedingungen FALSE ergeben
Serial.println("Unbekannte Nummer");
}

Das obige Beispiel wird man aber so nicht verwenden, da für so ein Programm die Switch-Case Anweisung besser geeignet ist, jedoch ist beides möglich.

Short-if

Das short-if ist eine Kurzschreibweise für eine if-Anweisung.


int compareValues(int a, int b)
{
//Ist a größer b, so wird a zurückgegeben. Ist das nicht der Fall, wird b zurückgegeben.
return (a > b ? a : b);
}

Der Aufbau ist (Bedingung ? TRUE : FALSE). Ist die Bedingung erfüllt, so wird der Wert zurückgegeben, der an der TRUE Stelle steht. Ansonsten der, der an der FALSE Stelle steht. Die Klammern dienen nur der optischen Abgrenzung.

0

0

Back To Top

Switch Case

Eine weitere Möglichkeit um eine Entscheidungsfunktion in das Programm einzubauen, ist die Switch-Case-Anweisung.


int menu = 0;

switch(menu)
{
//Ist menu = 0, so wird der case 0 betreten.
case 0:
Serial.println("Fall 0 aktiviert!");
break;
//Ist menu = 1, so wird der case 0 betreten.
case 1:
Serial.println("Fall 1 aktiviert!");
break;
//Ist menu != 0 && menu != 1, so wird der default case betreten.
default:
Serial.println("Menü unbekannt!");
break;
}

Das ganze funktioniert auch mit Zeichen.


char menu = 'a';

switch(menu)
{
//Achtung! Hier wird auf Groß- & Kleinschreibung unterschieden!
//Ist menu = 'a', so wird der case 'a' betreten.
case 'a':
Serial.println("Fall a aktiviert!");
break;
//Ist menu = 'b', so wird der case 'b' betreten.
case 'b':
Serial.println("Fall b aktiviert!");
break;
//Ist menu != 'a' && menu != 'b', so wird der default case betreten.
default:
Serial.println("Menü unbekannt!");
break;
}

Was leider nicht funktioniert ist die Case-Abfrage auf einen String.

0

0

Back To Top

Schnittstellen

SPI

Serial Peripheral Interface (SPI)

Grundlegend dient die SPI Schnittstelle zur Kommunikation (vollduplexfähig) mit einem oder mehreren anderen Peripheriegeräten (z.B. ein Microcontroller). Weiteres ist auf der Arduino Seite nachzulesen.

SPI – Arduino Referenzen

Die SPI Schnittstelle nutzt für seine Verbindung folgende Leitungen.

  • SCLK (Serial Clock)
  • MOSI (Master Output, Slave Input)
  • MISO (Master Input, Slave Output)

Einstellmöglichkeiten

mit CPOL unD CPHA
  • Clock Polarität (CPOL) -> 0: Ruhezustand der Taktleitung ist LOW (GND) / 1: Ruhezustand der Taktleitung ist HIGH (VCC)
  • Clock Phase (CPHA) -> 0: Datenleitung wird an der ersten Flanke interpretiert und an der zweiten gesetzt / 1: Datenleitung wird an der ersten Flanke gesetzt und an der zweiten interpretiert

SPI verwenden

Um die Funktionen von SPI nutzen zu können, muss zuerst die dementsprechende Bibliothek inkludiert werden.

#include <SPI.h>

Danach wird ein SPISettings Objekt mit folgenden Parametern initialisiert.

SPISettings meineEinstellungen(8000000,LSBFIRST,SPI_MODE0);

void setup()
{
SPI.beginTransaction(meineEinstellungen); //endTransaction(); schließt Verbindung wieder
SPI.begin(); //SPI.end(); schließt die Verbindung wieder
}

 

Erläuterung zum Code

Das Objekt SPISettings enthält die Parameter für die SPI Verbindung. Dieses Objekt wird dann dem Befehl .beginTransaction() übergeben und mit den Einstellungen initialisiert. So lassen sich einfach verschiedene SPI Profile anlegen und je nach Verwendungszweck zu nutzen.

Die Übergabeparameter des SPISettings Objekt sind

OBJEKTNAME(FREQUENZ[Hz], LSBFIRST || MSBFIRST, SPI_MODE0 ||SPI_MODE1 || SPI_MODE2 || SPI_MODE3)

 

Weitere SPI Befehle

  • setBitOrder(LSBFIRST || MSBFIRST)
  • setClockDivider(SPI_CLOCK_DIV2 || SPI_CLOCK_DIV4 || … || SPI_CLOCK_DIV128)
  • setDataMode(SPI_MODE0 ||SPI_MODE1 || SPI_MODE2 || SPI_MODE3)
  • transfer(VAR[byte] || VAR16[2byte] || ARRAY[], ARRAY_LÄNGE)
  • usingInterrupt(INTERRUPT_NUMMER)

0

0

Back To Top

Seriell

Die serielle Schnittstelle kann zur Kommunikation mit externer Peripherie genutzt werden. Je nach verwendeten Arduino stehen keine, eine oder mehrere hardware-basierende, serielle Schnittstellen zur Verfügung.

Zur Verwundung der seriellen Schnittstelle ist keine extra Bibliothek nötig, außer man möchte eine zusätzliche, softwar-basierende Schnittstelle verwenden.

Hardware

Schnittstelle initialisieren

Die Schnittstelle kann mit verschiedenen BAUD-Raten initialisiert werden. Standardmäßig wird 9600 BAUD verwendet.

//Die Geschwindigkeit der Schnittstelle
//300, 600, 1200, 2400, 4800, 9600, 14400, 19200, 28800, 38400, 57600, oder 115200
const int BAUD = 9600;

void setup()
{
//Initialisiert die Schnittstelle mit der definierten Geschwindigkeit
//.end(); beended die Verbindung wieder.
Serial.begin(BAUD);
//Wartet bis Verbindung aufgebaut wurde
while(!Serial);

}

 

Durch Serial.begin(BAUD) wird die Schnittstelle mit der definierten Geschwindigkeit initialisiert und kann mit Serial.end() wieder geschlossen werden. Solange eine serielle Schnittstelle aktiv ist, können die TX und RX Pins (Arduino UNO Pin 0, 1) auf dem Arduino nicht für andere Zwecke verwendet werden. Es muss davor die serielle Schnittstelle geschlossen werden.

Schreiben und lesen

Es können Daten über die Schnittstelle jeweils gesendet und empfangen werden. Dazu folgendes Beispiel.

void loop()
{
while(!Serial.available())
{
//Wartet solange bis Daten kommen.
;
}
Serial.println("Daten werden empfangen...");
while(Serial.available())
{
//Schreibt die eingelesenen Daten direkt wieder raus.
Serial.write(Serial.read());
//Wartet bis die Serielle Übertragung abgeschlossen ist.
Serial.flush();

}

}

 

Erläuterung zum Code

Solange keine Daten vorhanden sind, wartet das Programm in der while(!Serial.available()) Schleife. Sind nun Daten verfügbar, so werden diese Byteweise eingelesen und direkt wieder ans Terminal zurückgeschrieben, bis der serielle Puffer leer, sprich Serial.available() FALSE ist bzw. 0.

arduino_serial_monitor

0

0

Back To Top

Peripherie

LCD

Liquid Crystal Display

Um nun einen LCD mit dem Arduino anzusteuern, wird die “LiquidCrystial” Bibliothek benötigt. Diese könnt Ihr über die Bibliotheken-Verwaltung oder direkt über include einbinden.

#include <LiquidCrystal.h>

 

LCD deklarieren

Als erstes muss ein neues Objekt der Klasse LiquidCrystal erstellt werden. Dem Konstruktor werden die Anschlüsse, an welches das LCD angeschlossen ist, übergeben.

//////////////////////////////////////////////////////////////////////
// Objekte //
//////////////////////////////////////////////////////////////////////
LiquidCrystal lcd(7,6,5,4,3,2);
/*
* RS_PIN: 13(D7) = 7
* E_PIN: 12(D6) = 6
* D5_PIN: 11(D5) = 5
* D4_PIN: 6 (D4) = 4
* D3_PIN: 5 (D3) = 3
* D2_PIN: 4 (D2) = 2
*/

 

LCD initialisieren

Bevor der LCD verwendet werden kann, muss dieser initialisiert werden. Dies wird in der Regel in der Setup() Routine gemacht. Dazu wird der OBJEKTNAME.begin(Breite, Höhe) verwerndet.

void setup()
{
//LCD initialisieren (16 Zeichen, 2 Zeilen)
lcd.begin(16, 2);
}

Nun kann der LCD in nachfolgendem Programm verwendet werden.

LCD verwenden

Um nun zum Beispiel eine Willkommens-Nachricht anzuzeigen, kann man folgenden Code in die loop() Routine einfügen.

//Willkommensnachricht
//Die Nachricht wird die ersten 2 Sekunden angezeigt.
while(millis() < 2000)
{
lcd.setCursor(0,0); //= lcd.home();
lcd.print("Herzlich");
lcd.setCursor(0,1);
lcd.print("Willkommen!");
}

 

Erläuterung zum Code

Der Befehl .setCursor() setzt den Cursor an die angegebene Position. “0,0” ist das erste Zeichen, und die erste Zeile. Der Befehl .home() bringt Sie automatisch zum ersten Zeichen und erster Zeile.

Eigene Symbole erstellen

Mit dem Befehl createChar(Position[byte], zeichen [byte[]]) lassen sich benutzerdefinierte Zeichen für den LCD erstellen. Im folgenden Beispiel wird ein Batterie-Symbol erstellt. Insgesamt stehen 40 Pixel zur Verfügung (5×8).

//////////////////////////////////////////////////////////////////////
// Bibliotheken //
//////////////////////////////////////////////////////////////////////
#include <LiquidCrystal.h>

//////////////////////////////////////////////////////////////////////
// Objekte //
//////////////////////////////////////////////////////////////////////
LiquidCrystal lcd(7,6,5,4,3,2);
/*
* RS_PIN: 13(D7) = 7
* E_PIN: 12(D6) = 6
* D5_PIN: 11(D5) = 5
* D4_PIN: 6 (D4) = 4
* D3_PIN: 5 (D3) = 3
* D2_PIN: 4 (D2) = 2
*/

//////////////////////////////////////////////////////////////////////
// Eigenes LCD Zeichen //
//////////////////////////////////////////////////////////////////////
void drawChar(int CharPos, byte CharArray[],
int zeichen = 0, int zeile = 0);
byte batterie[8] = {
B01110,
B11111,
B10001,
B10011,
B11111,
B11111,
B11111,
};
void setup()
{
lcd.begin(16,2);

}

void loop()
{

lcd.clear(); //Säubert den LCD
drawChar(0, batterie, 0,0); //Ruft die Routine auf
while(1); //Endlosschleife

}
void drawChar(int CharPos, byte CharArray[], int zeichen, int zeile)
{
//Mit createChar lasst sich ein beliebiges Zeichen erstellen.
//Dabei ist der erste Parameter die Speicherposition [byte]
//und der zweite Parameter Zeichen als byte[].
lcd.createChar(byte(CharPos), CharArray);
lcd.setCursor(zeichen, zeile);
lcd.write(byte(CharPos));
}

0

0

Back To Top

Register

I/O-Register

Jeder Atmel-Controller verfügt über ein GPIO-Register (General Purpose Input/Output), welche zum

  • Definieren der Anschlussart (Input / Output),
  • Festlegen der Zustände am Ausgang und
  • Erfassen der Zustände am Eingang

dienen. Die I/O’s werden in Ports gruppiert (z.B. PORTB, PORTC, PORTD usw.) Jeder Port besteht dabei aus 3 ansteuerbare Register.

DDR (Data Direction Register)

DDRx (x = der gerwünschte Port B,C,..)

Ist das jeweilige Bit, welches einen Pin repräsentiert, gesetzt (logisch 1), so ist dieser als Ausgang definiert. Ansonsten als Eingang (logisch 0).

Name: DDRD -> Einzelne Bits: DDD7 (MSB) – DDD0 (LSB)

PORT

Das Port-Register wird zum Ansteuern der Ausgänge verwendet (HIGH oder LOW). Bei Pins, welche im DDR als Eingang definiert wurden, kann hier der interne Pullup-Widerstand eingeschaltet werden (logisch 1).

Name: PORTD -> Einzelne Bits: PORTD7 (MSB) – PORTD0 (LSB)

PIN

Das Pin-Register wird zum Erfassen der Zustände an den Eingängen verwendet (HIGH oder LOW).

Name: PIND -> Einzelne Bits: PIND7 (MSB) – PIND0 (LSB)

0

0

Back To Top

Bitmanipulation

Grundlagen

  • >> : Nach rechts schieben
  • << : Nach links schieben
  • |   : Binäres Oder
  • &  : Binäres Und
  • =   : Zuweisung

Bits manipulieren

Bit setzen

Soll ein Bit auf 1 gesetzt werden, so wird dieses ODER-Verknüpft. Dabei bleiben alle Bits, welche mit einer logischen 0 “bit-maskiert” werden, erhalten und alle die mit einer logischen 1 “bit-maskiert” werden, werden sicher auf eine logische 1 gesetzt.

/*
* 1) DDD6 wird durch 6 ersetzt: (1<<6), somit wird die 1 6x nach link
* verschoben -> 0b01000000 (6) und folglich 0b10000000 (7)
* 2) Nun wird durch das | 0b01000000 mit 0b10000000 ODER-Verknüpft.
* -> 0b11000000
* 3) Diese Bitmaske wird dann mit |= mit dem DDRD ebenfalls ODER-Verknüpft.
* -> Somit werden Pin 6 & 7 auf Ausgänge gesetzt und der Rest bleibt
* unberührt.
*/
DDRD |= (1<<DDD6) | (1<<DDD7);

 

Bit löschen

Soll ein Bit auf 0 gesetzt werden, so wird dieses UND-Verknüpft. Dabei bleiben alle Bits, welche mit einer logischen 1 “bit-maskiert” werden, erhalten und alle die mit einer logischen 0 “bit-maskiert” werden, werden sicher auf eine logische 0 gesetzt (gelöscht).

 /*
* Bit löschen
* 1) DDD6 wird durch 6 ersetzt: (1<<6), somit wird die 1 6x nach link
* verschoben -> 0b01000000 (6) und folglich 0b10000000 (7)
* 2) Nun wird durch das | 0b01000000 mit 0b10000000 ODER-Verknüpft.
* -> 0b11000000
* 3) ~ invertiert nun alle Bits -> 0b00111111
* 4) Nun wird das DDRD Register mit 0n00111111 UND-Verknüpft
*/
DDRD &= ~((1<<DDD6) | (1<<DDD7));

 

Bit lesen

Soll ein Bit auf seinen Zustand ausgelesen werden, so wird eine UND-Verknüpfung für die Bitmaske verwendet.

/*
* Bit einlesen
* 1) (1<<PINB0) verschiebt die 1 um 0 Stellen
* -> 0b00000001
* 2) Dieses Bitmaske wird mit dem Port B UND-Verknüpft
* -> 0b00000001 & PORTB
* -> Ist Pin0 "1" 0b00000001 & 0bxxxxxxx1: Abfrageergebnis TRUE
* -> Ist Pin0 "0" 0b00000001 & 0bxxxxxxx0: Abfrageergebnis FALSE
*/
if(PINB & (1<<PINB0))
{
//Wird betreten, wenn am Pin 0 des Port B eine 1 anliegt.
}

 

Achtung! Nicht den PINx Befehl mit dem PORTx Befehl verwechseln. Ansonsten wird der Zustand der internen Pullups ausgelesen!

0

0

Back To Top

Interrupts

Interrupts können auch in der Register-Ebene gesetzt werden. Dazu unterscheidet man in erster Linie zwischen internen und externen Interrupts.

Interne Interrupts

Diese Art von Interrupts wird intern, vom Microcontroller, durch Events (Ereignissen) ausgelöst. Der interne Watchdog ist ein gutes Beispiel hierfür. Aber auch Timer können interne Interrupts auslösen.

Die ISR (Interrupt Service Routine)

Dies ist das Programm, welches bei einem Interrupt aufgerufen und abgearbeitet wird. Dabei wird der aktuelle Programmablauf unterbrochen und nach dem ausführen der ISR wieder aufgenommen. Dabei muss der Controller wissen, an welcher Adresse sich die jeweilige ISR befinden. Diese Adresse wird als “Interruptvektor” bezeichnet. Somit weiß der Controller, wo er die zu Abarbeitende ISR findet.

Interruptvektoren

  • Enthält die Adresse der ISR
  • Jeder Interrupt besitzt seinen eigenen Vektor
  • Die jeweiligen Vektoren können aus dem Datenblatt entnommen werden
Auszug aus der Liste
  • RESET_vect
  • INT0_vect
  • INT1_vect
  • WDT_vect
  • usw.
ISR(WDT_vect)
{
Serial.println("Watchdog ausgelöst!");
Serial.flush();
}

 

Externe Interrupts

Diese Art von Interrupts wird von außen ausgelöst, zum Beispiel durch einen Tastendruck auf einen Interrupt-fähigen Pin. Dazu stehen jedem Board eine unterschiedliche Anzahl an Interrupt-Pins zur Verfügung. Für den UNO gilt

  • INT0 = Pin 2
  • INT1 = Pin 3

Register für externe Interrupts

EICRA (External Interrupt Control Register)

  • Bit7-4: Nicht vergeben (nur Lesen)
  • Bit3: ISC11 (Lesen / Schreiben)
  • Bit2: ISC10 (Lesen / Schreiben)
  • Bit1: ISC01 (Lesen / Schreiben)
  • Bit0: ISC00 (Lesen / Schreiben)
ISC (Interrupt Sense Control)
  • ISC00 = INT0 Controlbit 0
  • ISC01 = INT0 Controlbit 1
  • ISC10 = INT1 Controlbit 0
  • ISC11 = INT1 Controlbit 1

Diese Controlbits legen den Modus fest, wie der jeweilige Interrupt reagieren soll (zu Vergleichen mit FALLING, RISING, HIGH, LOW & CHANGE)

Controlbit ISCx1 / ISCx0
  • 0/0 = LOW
  • 0/1 = CHANGE
  • 1/0 = FALLING
  • 1/1 = RISING

EIMSK (External Interrupt Mask Register)

Hier werden die Interrupts freigegeben (logisch 1). Zu vergleichen mit attachInterrupt().

  • Bit7-2: Nicht vergeben (nur Lesen)
  • Bit1: INT1 (Lesen / Schreiben)
  • Bit0: INT0 (Lesen / Schreiben)

Interrupt programmieren

  1. Modus im EICRA setzen
  2. Interrupt im EIMSK aktivieren
  3. Globale Interrupts durch interrupts(); aktivieren
  4. ISR(INT0_vect) bzw. ISR(INT1_vect) ausprogrammieren
void setup()
{
noInterrupts();
Serial.begin(9600);
while(!Serial);

//Externe Interrupts definieren.
//RISING
EICRA |= (1<<ISC11) | (1<<ISC10);
//Interrupt für INT1 aktivieren
EIMSK |= (1<<INT1);

// Interrupts global aktivieren
interrupts();
}

void loop()
{
// put your main code here, to run repeatedly:

}
//Externer Interrupt auf INT1
ISR(INT1_vect)
{
Serial.println("Interrupt (INT1) festgestellt!");
Serial.flush();
}

0

0

Back To Top

Timer

Ein Timer (oder auch ein Counter) ist ein sich selbst inkrementierender bzw. dekrementierender Register im Microcontroller. Timer werden nicht vom Programmablauf beeinflusst und laufen gesondert ab. Der Arduino UNO besitzt zwei Timer, einen 8-Bit und einen 16-Bit Timer. Hat ein Timer sein Maximum erreicht, wird ein Event ausgelöst (TIMERX_OVF_vect) und wieder von 0 begonnen.

Overflows

Mit der internen Taktgeschwindigkeit von 16 MHz würde der Timer pro Sekunde 16 Mio. zählen und innerhalb kürzester Zeit unzählige Interrupts (Overflows) auslösen. Aus diesem Grund wir dein Vorteiler (Prescaler) verwendet. Dadurch wird die Frequenz geteilt (8, 64, 256, 1024).

Bei einem Teiler von 1024 und dem 8 Bit Timer ergibt sich folgende Overflow Anzahl pro Sekunde.

16 Mio : (256 * 1024) = 61 OVF/s

Timer Register

Alle Control-Register der Timers müssen vor der Initialisierung auf “0” gesetzt werden!

OCRnX (Output Compare Register n X)

Der Wert in diesem Register wird ständig mit dem Wert im Datenregister TCNT1H/L verglichen. Bei Übereinstimmung wird das Event Output Compare Match ausgelöst.

TCNTx – Timer / Counter x

  • Datenregister für den jeweiligen Timer (x)
  • Bei einem Überlauf wird ein Overflow Event ausgelöst und wieder von 0 begonnen (Ausnahme PWM Modus)

TCCR1A – Timer / Counter1 Register A

  • Bit7: COM1A1
  • Bit6: COM1A0
  • Bit5: COM1B1
  • Bit4: COM1B0
  • Bit3-2: nicht vergeben
  • Bit1: WGM11
  • Bit0: WGM10

COMxxx (Compare Output Mode) (R/W)

Steuern das Schaltverhalten bei einem Compare Output Match.

WGM11/10 (Waveform Generation Mode)

Zusammen mit den WGM Bits aus dem TCCRB steuern diese Bits die Zählsequenzen des Counters. Dabei stehen folgende Operationsarten zur Verfügung.

  • Normal Mode: Zählrichtung immer aufsteigend, bei Überlauf wird wieder bei 0 begonnen.
  • CTC Modus: Erreicht der Counter den Wert im Output Compare Register, wird er auf 0 gesetzt.
  • PWM Modus: Es werden 3 Arten von PWM-Modi angeboten.

TCCR1B – Timer / Counter1 Register B

  • Bit7: ICNC1
  • Bit6: ICES1
  • Bit5: nicht vergeben
  • Bit4: WGM13
  • Bit3: WGM12
  • Bit2: CS12
  • Bit1: CS11
  • Bit0: CS10
CSxx – Clock Select

Hiermit wird der Prescaler eingestellt.

CS12 / CS11 / CS10

  • 0/0/0: Timer/Counter angehalten
  • 0/0/1: CLK/1 (Kein Prescaling)
  • 0/1/0: CLK/8
  • 0/1/1: CLK/64
  • 1/0/0: CLK/256
  • 1/0/1: CLK/1024
  • 1/1/0: Externer Clock am T1 Pin (Fallende Flanke)
  • 1/1/1: Externer Clock am T1 Pin (Steigende Flanke)

TIMSKx (Timer/Counter x Interrupt Mask Register)

  • Bit7-3: Nicht vergeben
  • Bit2: OCIE2B
  • Bit1: OCIE2A
  • Bit0: TOIE2

Dieses Register steuert die Interrupts.

OCIEyx (Timer/Counter y Output Compare Match x Interrupt Enable)

Bei 1 wird ein Interrupt beim Output Compare Match ausgelöst.

TOIEx (Timer/Counter x Interrupt Enable)

Bei 1 wird ein Interrupt bei Overflow ausgelöst.

0

0

Back To Top

Bibliotheken

EEPROM

EEPROM (Electrically Erasable Programmable Read Only Memory)

Der EEPROM erlaubt es Daten persistent zu speichern, sodass diese auch nach einem Spannungsverlust noch vorhanden sind. Je nach Board stehen verschieden große Speicher zur Verfügung. Der Aurdino UNO ist mit 1024 Bytes ausgestattet.

Achtung! Ein Schreibvorgang dauert 3,3 ms. Eine EEPROM-Zelle ist nach ca. 100.000 Schreibzyklen defekt. So kann man seinen EEPROM in 5,5 Minuten unbrauchbar machen.

Abhilfe schaft der .update() Befehl, da dieser nur auf dem EEPROM schreibt, wenn die bereits gespeicherten Daten unterschiedlich sind.

EEPROM verwenden

Dazu muss die dementsprechende Bibliothek inkludiert werden.

#include <EEPROM.h>

 

Dann stehen folgende Befehle zur Verfügung

  • .read(Adresse): Gibt das Byte an der angegebenen Adresse zurück. Sollte dieses noch nie beschrieben worden sein, lautet der Rückgabewert 255.
  • .write(Adresse, Wert): Schreibt an die Adresse einen Wert zwischen 0 und 255.
  • .update(Adresse, Wert): Schreibt Wert nur bei Unterscheidung.
  • .EEPROM[ ]: Direkte Referenz als Array auf den Speicher.
  • .get(Adresse, Variable): Sucht an der angegebenen Adresse nach dem Datentyp, den er in die Variable speichern kann.
  • .put(Adresse, Variable): Speichert die Variable komplett an der Adresse unter Benutzung der Funktion EEPROM.update().
  • .length(): Gibt die Anzahl der Speicherzellen zurück.

 

0

0

Back To Top

Beispiele

LED blinken ohne Delay()

Da der Prozessor bei einem Delay einfach nichts tut (auch nicht auf Inputs von außen regiert, außer Interrupts), sollten delays in aufwendigeren Programmen vermieden werden. Als kleines Beispiel lassen wir eine LED alle 2 Sekunden ihren Zustand ändern.

const int LED = 13; //Interne LED des Arduino Uno
float timestamp = 0; //Speichert Umschaltzeitpunkt
float gap = 2000; //Schaltdauer in ms

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

void loop()
{
if(millis() - timestamp > gap)
{
/*
Ist der Abstand zwischen der aktuellen Zeit und dem
letzen Zeitstempel größer als die angegebene Zeitlücke,
dann wird der Status der LED umgeschaltet.
*/
digitalWrite(LED, !digitalRead(LED));
//Darauffolgend wird der Zeitstempel akutalisiert
timestamp = millis();
}
}

0

0

Back To Top

Ausschaltverzögerung

Bei folgendem Beispiel handelt es sich um eine einfache Ausschaltverzögerung mit Tasterentprellung.

byte taster = 4; //Pin an dem der Taster angeschlossen ist
byte led = 13; //Interne LED des Arduino UNO
int verzoegerung = 2000; //Verzögerungszeit in ms
boolean ausschalten = false;

void setup()
{
pinMode(taster, INPUT); //Taster als Input definieren
digitalWrite(taster, HIGH); //Interne Pullups einschalten
pinMode(led, OUTPUT); //Definiert LED als Output
digitalWrite(led, HIGH);    //Schaltet die LED ein
}

void loop()
{
//Taster entprellen
//Der Taster muss gegen Masse geschaltet sein, da die
//internen Pullups eingeschaltet sind!
if(!digitalRead(taster))
{
//Wird der Taster gedrückt, wird erstmal kurz gewartet
delay(50);
if(!digitalRead(taster))
{
//Wird der Taster dann immer noch gedrückt, dann wird
//der Ausschaltvorgang begonnen
ausschalten = true;
do
{
//Falls der Taster noch länger gehalten wird, wird
//hier einfach gewartet.
}while(!digitalRead(taster));
}
}
//Ist der Ausschaltvorgang aktiv, dann wird jede ms der
//Wert um 1 verringert, solange der Wert > 0 ist.
if(ausschalten)
{
if(verzoegerung > 0)
{
verzoegerung--;
}
else
{
//Ist die Verzögerung abgelaufen, dann wird die LED
//geschalten
digitalWrite(led, LOW);
//und der Ausgangszustand wiederhergestellt.
ausschalten = false;
verzoegerung = 2000;
}

}
delay(1);

}

0

0

Back To Top

Was this article helpful?

Related Articles

Leave A Comment?