Wir haben im Laufe dieses Tutorials bereits Variablen kennen gelernt. Im Umgang mit Mikrocontrollern dreht sich im Endeffekt alles um Zahlen. Aus diesem Grund bietet uns die Sprache C eine Reihe an Rechenoperationen an, die wir benutzen können, um einen Ausdruck zu bilden. Ein Ausdruck besteht aus Operanden (also den Zahlen) und den Operatoren (Rechenzeichen: Plus Minus, …). Schauen wir uns zunächst einmal an, welche Operatoren uns zur Verfügung stehen…
Operationen
Arithmetische Operationen
Symbol | Beschreibung |
+ |
Addition – Beispiel (a=7 , b=2 ): c=a+b Ergebnis c=9 |
- |
Subtraktion – Beispiel (a=7 , b=2 ): c=a-b Ergebnis c=5 |
* |
Multiplikation – Beispiel (a=7 , b=2 ): c=a*b Ergebnis c=14 |
/ |
Division – Beispiel (a=7 , b=2 ): c=a/b Ergebnis c=3 |
% |
Modulo (Restwert einer ganzahligen Division) – Beispiel (a=7 , b=2 ): c=a%b Ergebnis c=1 |
Bis auf die Rechenart Modulo sollten eigentlich alle anderen Arten klar sein. Die Rechenoperation Modulo teilt eine Zahl durch eine andere Zahl und gibt den Rest aus.
Logische Operationen
Symbol | Funktion |
&& |
UND |
|| |
ODER |
! |
NICHT |
Diese Operationen beziehen sich auf die ganze Variable, nicht Bitweise, siehe;
// Beispiele für logische Operationen a = 10; b = 20; c = 0; if( ((a == 10) && (b < 20)) || (c < 5) ) { // wird ausgeführt wenn a den Wert 10 hat // UND b kleiner als 20 ist // ODER wenn c kleiner als 5 ist }
Relationale Operationen
Ich denke zu den folgenden Operationen brauche ich nicht viel zu sagen. Wobei eine Sache vielleicht erwähnt werden sollte: Vergleiche zwischen Operanden sollten immer nur dann durchgeführt werden, wenn die Operanden vom selben Typ sind. Zumindest sollten nie signed
und unsigned
Variablen vergleichen. Das kann ähnlich wie das Überlaufen von Variablen zu Problemen führen!
Symbol | Beschreibung |
> |
Größer als – Beispiel (a=7 , b=2 ): a>b Ergebnis: true , denn a ist größer als b |
>= |
Größer/gleich als – Beispiel (a=7 , b=2 ): a>=b Ergebnis: true , denn a ist größer als b |
< |
Kleiner als – Beispiel (a=7 , b=2 ): a<b Ergebnis: false , denn a ist nicht kleiner als b |
<= |
Kleiner/gleich als – Beispiel (a=7 , b=2 ): a<=b Ergebnis: false , denn a ist nicht kleiner/gleich als/wie b |
== |
Gleich wie – Beispiel (a=7 , b=2 ): a==b Ergebnis: false , denn a ist nicht gleich als b |
!= |
Ungleich wie – Beispiel (a=7 , b=2 ): a!=b Ergebnis: true , denn a ist ungleich b |
Bitweise Logische Funktionen
Symbol | Operation |
& |
Bitweise UND |
| |
Bitweise ODER |
^ |
Bitweise Exklusiv ODER (entweder oder) |
~ |
Einerkomplement (das Gegenteil) |
>> |
Bitweises verschieben nach rechts |
<< |
Bitweises verschieben nach links |
Hinweis: Die Verschiebeoperationen >>
und <<
sind das selbe wie eine Division bzw. eine Multiplikation mit einer Zweierpotenz: Soll eine Zahl durch 2
geteilt werden, kann der Inhalt der Variablen stattdessen einfach um eine Stelle nach rechts verschoben werden: Angenommen die Variable x hat den Wert 128
, dessen binäre Darstellung 1000000
entspricht. Diese Zahl soll nun durch 2
geteilt werden (das Ergebnis also 64
). Verschiebt man stattdessen einfach alle Bits der Variable um eine Stelle nach rechts, wird aus 10000000
die Zahl 01000000
. Das Ergebnis ist das selbe, der Aufwand für den Prozessor deutlich geringer.
Diese in der Tabelle dargestellten Operationen beziehen sich auf jedes einzelne Bit einer Variable. Hier zwei Beispiele:
A='10101010' , B='01010101' --> A&B='00000000' A='10101010' , B='10101010' --> A&B='10101010'
Inkrement / Dekrement
Selbstverständlich sind auch die gängigen Operationen wie das Inkrementieren (+1
) oder Dekrementieren (-1
) einer Variablen möglich. Man unterscheidet verschiedene Schreibweisen:
A = A+1; A++; ++A; // A wird bei allen Anweisungen um 1 erhöht B = B-1; B--; --B; // B wird bei allen um 1 vermindert
Verarbeitungsreihenfolge
Eine ganz wichtige Regel bei der Verwendung von Operationen ist zu wissen in welcher Reihenfolge diese ausgeführt werden, wenn denn mehrere in einer Zeile/Anweisung stehen. Hier eine Übersicht:
Rang | Operation | Reihenfolge |
---|---|---|
1 | () , [] , -> , . |
von links nach rechts |
2 | ! , ~ , ++ , -- , + , – , * , & , (type) , sizeof |
von rechts nach links |
3 | * , / , % |
von links nach rechts |
4 | + , – , |
von links nach rechts |
5 | << , >> |
von links nach rechts |
6 | < , <= , > , >= |
von links nach rechts |
7 | == , != |
von links nach rechts |
8 | & |
von links nach rechts |
9 | ^ |
von links nach rechts |
10 | | |
von links nach rechts |
11 | && |
von links nach rechts |
12 | || |
von links nach rechts |
13 | ?: |
von rechts nach links |
14 | = , += , -= , *= , /= , %= , &= , ^= , |= , <<= , >>= |
von rechts nach links |
15 | , |
von links nach rechts |
Bei den Operatoren +
, -
, *
und &
im Rang 2 handelt es sich um die unären Varianten (positives und negatives Vorzeichen, Verweis- und Adressoperator).
Bitmanipulation
Oftmals ist es notwendig in einem Byte nur ein einzelnes Bit zu verändern. Stellt euch zum Beispiel vor, dass ihr in eurem Programm viele Flags habt, die das Auftreten eines wichtigen Ereignisses wieder spiegeln. Dann ist es auf dem PC vielleicht “in Ordnung” dafür 8 einzelne Variablen zu definieren doch auf dem Mikrocontroller müssen bzw. sollten wir idealerweise speicherptimiert arbeiten. Also versuchen wir das effizienter zu lösen. Wir benötigen für 8 Flags nicht 8 Variablen, sondern lediglich eine Variable des Typs uint8_t
. Dies ist möglich indem wir jedes Bit des Bytes (eine uint8_t
Variable = 8 Bit
= 1 Byte
) als einzelnes Flag betrachten. Zum Beispiel ist das Bit 0
für das Auftreten eines Alarms und Bit 1
für eine Schwellwerterkennung …
Wir schaffen uns also eine Variable vom Typ char und initialisieren sie mit null:
uint8_t flags = 0;
Das auftreten eines Alarms (zum Beispiel wenn Bit 0 == 1
), können wir nun wie folgt prüfen:
if(flags & 0x01) // wenn Bit 0 eine 1 ist, dann.. { // ... }
Diese if
Anweisung wird nur durchlaufen, wenn das Bit 0
der Variable flags
eine 1
ist (siehe Logische Operationen). Die 1
an dieser Stelle muss natürlich auch irgendwie entstehen und zwar ohne die anderen Bits dabei zu verändern und das macht man so:
// Bit 0 setzen, alle anderen nicht ändern flags = flags | 0x01; // verkürzte Schreibweise flags |= 0x01;
In Analogie dazu können wir folgendermaßen ein bestimmtes Bit löschen (hier Bit 0
):
// Bit 0 löschen, alle anderen nicht ändern flags = flags & 0xFE; // verkürzte Schreibweise(n) flags &= 0xFE; flags &= ~0x01;
Vektoren / Arrays und Strings
Vektoren / Arrays
Wir erinnern uns, dass wir Variablen deklarieren können und in diesen genau einen Wert speichern können. Doch was macht man, wenn man mal mehrere Werte automatisiert speichern will? Dafür benutzen wir sogenannte Vektoren oder mehrdimensional Arrays. Ein Vektor besteht aus einer bestimmten Anzahl von Variablen mit dem selben Typ. Sehen wir uns einmal an wie ein Vektor deklariert werden kann. Das Muster sieht dabei so aus: Typ Name [Anzahl] = {Initialisierung};
Dabei ist die direkte Zuweisung von Werten (also die Initialisierung) nur optional.
uint8_t foo[5] = {0, 3, 4, 9, 10};
Somit hätten wir unseren ersten Vektor erstellt. Dieses besteht nun aus fünf uint8_t
Variablen (mit den Indexen 0
bis 4
) mit dem Namen foo
. Beschreiben können wir diesen wie folgt:
foo[0] = 50; //Somit steht eine "50" in der ersten Variable foo[1] = 50+50; //Somit steht eine "100" in der zweiten Variable
Ihr seht schon man kann mit dieser Schreibweise Platz sparen, doch bis hier hin würde das auch nicht viel bringen, doch wenn wir uns den nächsten Schritt anschauen, werden wir schnell den Vorteil eines Vektors erkennen:
uint8_t i; uint8_t foo[5]; for (i=0;i<5;i++) { foo[i]=5*i; }
In diesem Beispielprogramm wird in der Zeile 1 erst einmal eine Variable mit dem Namen i
deklariert. Gefolgt von einer Deklaration für einen Vektor Namens foo
mit fünf Speicherplätzen. Dann kommt in der Zeile 4 etwas, das wir bisher noch nicht kennen. Das soll uns aber erst einmal nicht weiter stören (wird später erklärt). Man spricht von einer for
Schleife, diese wird solange wiederholt, bis der Ausdruck, hier i<5
, nicht mehr wahr ist. Durch die Initialisierung i=0
wird die Variable i
zu Beginn auf 0
gesetzt. Die Schleifenanweisung i++
wird nach jeder durchlaufenden Schleife ausgeführt. Wir erinnern uns, dass i++
einer Erhöhung von i
um 1
entspricht. Somit wird also die Variable i
von 0
bis 5
hoch gezählt, wobei die Schleife, wenn i
den Wert 5
erreicht hat nicht mehr ausgeführt wird, da die Bedingung i<5
dann nicht mehr wahr ist. Die for
Schleife wird wie erwähnt noch einmal näher erklärt. Wir gehen also davon aus, dass i
seinen Wert nach jeder Schleife um 1
erhöht. So würde sich folgender Inhalt in dem Vektor ergeben:
Durchgang 0 --> foo[0] Inhalt: 0 Durchgang 1 --> foo[1] Inhalt: 5 Durchgang 2 --> foo[2] Inhalt: 10 Durchgang 3 --> foo[3] Inhalt: 15 Durchgang 4 --> foo[4] Inhalt: 20
Und genau dadurch lassen sich später Datenbanken leichter und vor allem mit weniger Schreibarbeit erstellen, als wenn wir jeder Variablen einen Wert einzeln zuordnen müssen und somit auch für jede Variable eine eigene Rechnung haben müssten. Ein Vektor wird zwar ähnlich wie eine Variable deklariert muss aber immer mit einem Index angesprochen werden. Wir haben diesen Index bereits verwendet. In unserem Beispiel war es die Variable i
. Folglich gilt:
// diese Operation kann nicht funktionieren foo_a = foo_b; for (i=0; i<5; i++) { // so würde es funktionieren! foo_a[i] = foo_b[i]; }
Mehrdimensionale Vektoren und Arrays
Bisher haben wir nur über die eindimensionalen Vektoren gesprochen es gibt aber auch noch mehrdimensionale Vektoren. Diese bezeichnet man dann als Arrays. Sie können sich mehrdimensionale Arrays wie eine Tabelle vorstellen.
//Eindimensional mit 5 Zeilen uint8_t foo_a[5]; //Zweidimensional mit 5 Zeilen und 5 Spalten uint8_t foo_b[5][5];
Man kann auch Arrays direkt bei der Deklaration Werte zu weisen. Dies würde wie folgt aussehen:
uint8_t foo_ED [5] = {0,1,2,3,4}; uint8_t foo_MD [4][4] = { {0,1,2,3}, {4,5,6,7}, ... };
Strings
Ein String ist eine Kette aus aneinandergereihten char
Zeichen. Im Grunde genommen hat die Sprache C keine Unterstützung für richtige Strings. Jedoch ist ein String im Grunde genommen nichts anderes als ein Vektor aus char
. Die nachfolgende Zeile zeigt die Deklaration eines solchen Strings bzw. Char-Arrays.
char foo[12];
Das hier angelegte char
Array bzw. der String hat eine Kapazität von zehn char
. Jedoch können lediglich maximal neun char
in diesem String gespeichert werden. Das liegt daran, dass ein jeder String mit einer sogenannten Terminierung abgeschlossen werden muss. Diese Terminierung ist notwendig, damit ein Controller weiß, wann ein String zu ende ist. Als Terminierung wird eine 0
hinter das letzte Zeichen des Strings geschrieben.
char foo[] = "Hello Word";
Eine alternative Zuordnung wäre die folgende (siehe auch die Zuweisung des Terminators '\0'
bzw. 0
:
foo[ 0] = 'H'; foo[ 1] = 'e'; foo[ 2] = 'l'; foo[ 3] = 'l'; foo[ 4] = 'o'; foo[ 5] = ' '; foo[ 6] = 'W'; foo[ 7] = 'o'; foo[ 8] = 'r'; foo[ 9] = 'l'; foo[10] = 'd'; foo[11] = '\0'; // identisch zu foo[11] = 0;
Bedingungen
Bedingungen sind sehr wichtig für die Programmsteuerung. Mit ihnen entsteht eigentlich erst ein logischer Zusammenhang. Wir können mit den if/else Bedingungen sehr genaue Steuerung erarbeiten. Wir können mit den Bedingungen Werte überprüfen und in Abhängigkeit Operationen durchführen, Texte ausgeben oder es ggf. nicht machen. Schauen wir uns einmal ein Beispiel an:
uint16_t ubat; ubat = 500; if (ubat < 1000) { lcd_write("Warnung Akku"); }
Bei dieser if
Bedingung würde in Zeile 4 geprüft werden ob der Wert in der Variable ubat
kleiner ist als 1000
. Wenn diese Aussage wahr
ist, also ubat
kleiner ist als 1000
, würde der if
Anweisungsblock {}
ausgeführt werden. Nun ist dieses Beispiel sinnlos, da wir die Variable ubat
ja oben immer mit 500
beschreiben. Es würde also immer die if
Anweisung ausgeführt werden. Angenommen die Variable nimmt nun einen Wert an, der größer ist als 1000
, dann würde die Warnung nicht gelöscht werden und die if
Anweisung würde nicht mehr ausgeführt. Also müssen wir das Programm bearbeiten:
if (ubat < 1000) { lcd_write("Warnung Batt"); else { lcd_write("Batt OK"); }
Nun würde, wenn sich der Wert von ubat
ändert und größer 1000
wird, die else
Anweisung abgearbeitet werden. Dadurch hätten wir eine zweiseitige Bedingung. Nur mit der if
Abfrage allein, wäre es eine einseitige Abfrage.
Eine if
Bedingung hat also immer mindestens folgende Ausführung (einseitig):
if(Ausdruck) { // Anweisungen }
Eine if
Bedingung kann folgende Ausführung haben (zweiseitig):
if(x) { // Anweisungen y } else { // Anweisungen z }
Vereinfacht gesagt: Wenn x
, dann y
ansonsten z
. Folgende Ausdrücke können für die Bedingung genutzt werden:
if((A < B) && (A > 80) || (A == 11)) { // Anweisungen }
Diese if
Funktion würde nur ausgeführt werden, wenn A
kleiner ist als B
und A
größer ist als 80
oder A
gleich 11
ist. So lassen sich mehrere Bedingungen in einer zusammenfassen.
Switch Case
Es wird euch öfter mal passieren, dass Variablen auf viele verschiedene Zahlenwerte überprüft werden müssen. Nun könnt ihr das natürlich mit diversen if
Funktionen erledigen, wesentlich eleganter geht dies allerdings mit der switch
/ case
Funktion. Sehen wir uns auch dazu ein Beispiel an:
switch(state) { case 0: { // do something break; } case 1: { // do something break; } default: { // do something } }
Das Beispiel zeigt eine einfache switch-case
Anweisung. In Zeile 1 prüft der Controller den Wert der Variable state
und verzweigt dementsprechend in die passende case
Sektion: Hat state den Wert 0
, wird case 0
abgearbeitet, hat state den Wert 1
wird case 1
abgearbeitet. Wenn state einen Wert hat, der nicht durch einen case
abgebildet wird, wird die default
Anweisung abgearbeitet.
Schleifen
Jetzt haben wir ja schon eine ganze Menge verschiedener C-Werkzeuge kennengelernt. Damit ist der Sprachumfang der Hochsprache C jedoch noch nicht ausgeschöpft. Weiter geht es mit den sogenannten Schleifen. Sie werden immer dann benötigt, wenn ein Anweisungsblock {}
mehrfach wiederholt werden soll/muss. Hier mal direkt ein Beispiel:
while
while(1) { // Anweisungen }
Dieses Beispiel zeigt die Endlosschleife. Eine Endlosschleife wird genutzt um (wie der Name bereits sagt Anweisungen immer wieder und wieder durchzuarbeiten. Eine while
Schleife wird solange durchlaufen, wie ihr Ausdruck wahr
(bzw. nicht 0
ist) ist. Da bei diesem Beispiel eine 1
in den runden Klammern steht, ist der Ausdruck immer wahr
, sodass die Schleife wird nie verlassen. Es sei denn, der Controller bekommt einen Reset oder der Controller trifft auf eine break
Anweisung. Die Schreibweise für eine while
Schleife ist also:
while(Ausdruck) { // Anweisungen }
Man kann die Anweisung natürlich auch anders gestalten, sodass sich bedingte Schleifen ergeben, zum Beispiel:
while (i <= 10) { // Anweisungen }
Diese while
Schleife würde solange wiederholt werden, wie der Wert von i
kleiner/gleich 10
ist. Anders ausgedrückt, die while
Schleife würde beendet werden, wenn i
den Wert 10
überschreitet. Man kann die Bedingung im while
Kopf, genau wie bei den if
Anweisungen, beliebig erweitern.
do while
Die do while
Schleife ist der normalen while
Schleife nahezu identisch. Der einzige Unterschied ist, dass diese mindestens einmal durchlaufen wird, da ihre Bedingung erst am Ende der Schleife steht.
do { // Anweisungen } while(i <= 10);
Also selbst, wenn beim Eintritt in die do while
Schleife die Variable i
bereits einen Wert größer 10
hat, wird die Schleife trotzdem einmal durchlaufen obwohl die Bedingung der Schleife nicht erfüllt ist. Jedoch steht die Schleifenbedinung am Ende der Schleife, weshalb die do while
Schleife immer mindestens einmal durchlaufen wird.
for
Die for
Schleife haben wir oben in Beispielen schon angesprochen. Ich möchte sie hier aber dennoch noch einmal genau erklären. Eine for
Schleife ist ähnlich wie eine while
Schleife nur dass man im Schleifen-Kopf mehr einbringen kann. Dazu schauen wir uns wieder ein Beispielcode an:
uint8_t i; uint8_t array_01[5]; for(i=0; i<5; i++) { array_01[i]=5*i; }
Bei einer for
Schleife steht eine Initialisierung, eine Bedingung sowie eine Schleifenanweisung im Funktionskopf. Im gezeigten Beispiel ist die Initialisierung das i=0
, die Bedingung i<5
und die Schleifenanweisung das i++
. Die for Schleife wird solange durchlaufen, wie die Bedingung wahr ist. Im Anschluss an jeden Schleifendurchlauf wird entsprechend die Schleifenanweisung ausgeführt. Mit anderen Worten im oben gezeigten Beispiel wird die Variable i
von 0
bis 5
gezählt wobei die for
Schleife beim Erreichen des Wertes 5
nicht mehr durchlaufen wird, da die Bedingung i<5
nicht mehr wahr
ist.
Weitere Anweisungen
break
Die break
-Anweisung wird innerhalb von Schleifen verwendet, um die Schleife sofort zu beenden. Der Quelltext wird dann ganz normal nach der Schleife fortgesetzt. break
kann in jeder der drei Schleifen (while
, do while
oder for
) verwendet werden.
continue
Das zweite wichtige Schlüsselwort für Schleifen ist continue
. Es wird genau so verwendet wie break
, bricht die Schleife allerdings nicht völlig ab, sondern setzt die Codeausführung am Ende des Schleifenrumpfes fort. Für while
und do-while
bedeutet das beim Bedingungstest, für die for
-Schleife beim Anweisungsteil. Mit continue
können Sie also den Rest des aktuellen Schleifendurchlaufs überspringen.
return
Die return
Anweisung beendet die Abarbeitung der aktuellen Funktion. Wenn eine return
Anweisung mit einem Ausdruck angegeben wird, dann wird der Wert des Ausdrucks an die aufrufende Funktion als Rückgabewert geliefert. In einer Funktion können beliebig viele return
Anweisungen angegeben werden. Jedoch muss dabei darauf geachtet werden, dass nachfolgender Programmcode durch den Programmfluss noch erreichbar bleibt.
Ausblick
Im nächsten Kapitel des PIC-C-Tutorials beschäftigen wir uns mit Pointern, Strukturen, dem Schlüsselwort typedef
und Enums. Wenn du dich jetzt fragst, was es mit diesen Begriffen auf sich hat, solltest du keine Zeit verlieren und direkt im nächsten Artikel weiterlesen: PIC C Tutorial – Pointer und mehr