Wir haben ja im Laufe dieses Tutorials bereits Variablen kennen gelernt. Und im Umgang mit Mikrocontrollern dreht sich im Endeffekt ja 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…

Folgende Operationen sind im Sprachumfang von C vorhanden:

Arithmetische Operationen

SymbolBeschreibung 
+               Addition 
-               Subtraktion 
*               Multiplikation 
/               Division 
%               Modulo

Bis auf die Rechenart “Modulo” sollten eigentlich alle anderen Arten klar sein. Die Rechenoperation Modulo teilt eine Zahl durch eine andere Zahl und gibt des Rest aus. Hier ein Beispiel:

A = 11%5  11/5=2 (Rest=1) 
A würde den Wert "1" annehmen

B = 5%3  5/3=1 (Rest=2) 
B würde den Wert "2" annehmen

Logische Operationen

SymbolFunktion 
&&             UND 
||             ODER 
!              NICHT

Diese Operationen beziehen sich auf die ganze Variable! Nicht Bitweise!

// 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. Vor allem aber nicht signed und unsigned vergleichen. Das kann ähnlich wie das Überlaufen von Variablen zu Problemen führen!

SymbolBeschreibung 
>              größer 
>=             größer oder gleich 
<              kleiner 
<=             kleiner oder gleich 
==             gleich 
!=             ungleich

 Bitweise Logische Funktionen

SymbolOperation 
&              bitweises UND 
|              bitweises ODER 
^              bitweises Exklusiv-ODER 
~              Einerkomplement 
>>             bitweises Verschieben nach rechts 
<<             bitweises Verschieben nach link

Hinweis: Die Verschiebeoperationen >> und << sind das selbe wie eine Division bzw. eine Multiplikation mit 2.

Diese Operationen beziehen sich auf jedes einzelne Bit einer Variable! Und so arbeiten sie:

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 euremProgramm 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 wir Speicher bewusst arbeiten. Also versuchen wir das effizienter zu lösen. Wir benötigen für 8 Flags nicht 8 Variablen sondern lediglich 1 Variable des Typs char (auch wenn wir keine Buchstaben hier hinein speichern). Dies ist möglich indem wir jedes Bit des Bytes (eine char 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:

unsigned char 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;
// Verkuerzte 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 so aus:

Typ vektorname [Anzahl] = {1, 10, 4, ...};
Int vektor[5] = {0, 3, 4, 9, 10};

Das direkte Zuweisen von Werten ist nur optional!

Somit hätten wir unseren ersten Vektor erstellt. Dieses besteht nun aus fünf Integer Variablen (0-4) mit dem Namen Vektor. Beschreiben können wir den Vektor wie folgt:

vektor[0] = 50;
//Somit steht eine "50" in der ersten Variable
vektor[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:

unsigned char i;
int vektor[5];

for (i=0;i<5;i++)
{
  vektor[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 vektor mit 5 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 i<5 (hier) nicht mehr wahr ist! Durch die Initialisierungsanweisung 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++; gleich der Erhöhung von i um 1 ist. Somit wird also die Variable von i hoch gezählt. Also würde diese Schleife genau 5 mal durchlaufen werden. 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:

Wir zählen die “0” immer mit, daher hat ein Vektor mit [5] auch 5 Speicherplätze ( 0-4 ).

Durchgang 0 --> vektor[0] Inhalt: 0
Durchgang 1 --> vektor[1] Inhalt: 5
Durchgang 2 --> vektor[2] Inhalt: 10
Durchgang 3 --> vektor[3] Inhalt: 15
Durchgang 4 --> vektor[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:

vektor_A = vektor_B;
//Diese Operation kann nicht funktionieren!
for (i=0; i<5; i++) {vektor_A[i] = vektor_B[i];}  //So würde es funktionieren!

Mehrdimensionale Vektoren bzw. 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.

unsigned char Array_A[5];
//Eindimensional = 5 Zeilen
unsigned char Array_A[5][5];
//Zweidimensional = 5 Zeilen und 5 Spalte

Man kann Arrays auch direkt bei der Deklaration Werte zu weisen. Dies würde wie folgt aussehen:

unsigned char Array_ED [5] = {0,1,2,3,4};
unsigned char Array_MD [4][4] = {0,1,...,14,15};

Strings

Strings sind im Grunde genommen nichts neues. Es sind Vektoren, welche aus dem Typ char gebildet werden und werden dafür benutzt Text zu speichern. Wenn wir einen String deklarieren…

char text[10];

…können in diesem String (nur) 9 Zeichen stehen, nicht 10! Denn es muss immer Platz für das Abschluss-Zeichen die 0 sein. Der Text könnte ja auch bloß 5 Zeichen lang sein und wie sollte der PIC dann wissen wo Schluss ist? Somit wird immer eine abschließende 0 angehängt um das Ende des Textes zu markieren.

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:

int Batterie_Volt;
Batterie_Volt = 500;

if (Batterie_Volt < 1000)
{
  LCD_Write_String("Warnung Akku");
}

Bei dieser if Bedingung würde in Zeile 5 geprüft werden ob der Wert in der Variable Batterie_Volt kleiner ist als “1000”. Wenn diese Aussage wahr ist, also Batterie_Volt kleiner ist als 1000, würde der if Anweisungsblock {} ausgeführt werden. Nun ist dieses Beispiel sinnlos, da wir die Variable Batterie_Volt ja oben immer mit “500” beschreiben. Es würde also immer die if-Funktion ausgeführt werden. (Es ist ja nur ein Beispiel!) 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-Funktion würde nicht mehr ausgeführt. Also müssen wir das Programm bearbeiten:

int Batterie_Volt;
Batterie_Volt = 500;

if (Batterie_Volt < 1000)
{
  LCD_Write_String("Warnung Batt");
}
else
{
  LCD_Write_String("Batt OK");
}

Nun würde, wenn sich der Wert von Batterie_Volt ändert und größer 1000 wird, die else-Funktion einschalten und durcharbeitet werden. Dadurch hätten wir eine zweiseitige Bedingung. Nur mit der if Abfrage allein, wäre es eine einseitige Abfrage. Wir wissen, dass die Variable Batterie_Volt in diesem Programm immer den Wert “500” behalten wird, es dient ja nur um die Funktionen if, else zu verstehen. Später in richtigen Programmen könnte die Variable zum Beispiel mit dem gemessenen Analogwert (umgewandelt in digital durch den Analog-Digital-Umsetzter des PIC) gefüllt werden und somit wäre das Programm eine Art Batterie-Wächter.

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 benutzt werden:

x == y   logischer Vergleich auf Gleichheit 
x != y   ... Vergleich auf Ungleichheit 
x <  y   ... Vergleich auf "kleiner" 
x <= y   ... Vergleich auf "kleiner oder gleich" 
x >  y   ... Vergleich auf "größer"  
x >= y   ... Vergleich auf "größer oder gleich"

Es können natürlich auch mehrere Bedingungen verknüpfen werden:

x && y   wahr, wenn x wahr und y wahr sind 
x || y   wahr, wenn x wahr und/oder y wahr 
!x       wahr, wenn x nicht wahr ist

Ich denke die einzelnen Vergleichsoperatoren dürften klar sein. Für die Bedingungs-Verknüpfung möchte ich hier nochmal ein Beispiel zeigen, damit das Prinzip klar wird:

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:

void Read_ADC(unsigned char channel)
{
  switch(channel)
    {
    case 0:
      ADCON0bits.CHS0=0;
      ADCON0bits.CHS1=0;
      ADCON0bits.CHS2=0;
      ADCON0bits.CHS3=0;
      break;
    case 1:
      ADCON0bits.CHS0=1;
      ADCON0bits.CHS1=0;
      ADCON0bits.CHS2=0;
      ADCON0bits.CHS3=0;
      break;
    case 2:
      ADCON0bits.CHS0=0;
      ADCON0bits.CHS1=1;
      ADCON0bits.CHS2=0;
      ADCON0bits.CHS3=0;
      break;
    default:
      break;
    }
}

Stellt Euch vor, der PIC bewegt sich in main Funktion und bekommt nun die Anweisung in die Funktion Read_ADC zu springen. Bei dieser Anweisung würde der PIC einen Übergabeparameter mitgeben. Wir gehen davon aus, dass der Übergabewert eine “1” ist. Beim erreichen der Funktion wird diese “1” zunächst einmal in die Variable channel geschrieben. Jetzt startet der Anweisungsblock wo direkt die Switch-Case Anweisung wartet. Hier steht nun switch(channel), dass heißt, der PIC würde nun (1 in channel) zu dem Case “1” springen und da arbeiten. Wichtig ist an dieser Stelle auch das break; Denn wenn diese Anweisung am Ende einer case Anweisung nicht stehen würde, dann würden alle anderen Case Anweisungen da drunter auch noch abgearbeitet werden (es sei denn das ist gewollt). Das break kann bei der letzten Case-Anweisung natürlich entfallen, denn da drunter kommt ja ohnehin nichts mehr.

Schleifen

Jetzt haben wir ja schon eine ganze Menge angeschaut aber es gibt selbstverständlich noch mehr. Schleifen! Sie werden immer dann benötigt, wenn ein Anweisungsblock {} mehrfach wiederholt werden soll/muss. Hier mal direkt ein Beispiel:

 while-Schleife

while(1)
{
   // Anweisungen
}

Dieses Beispiel zeigt die Endlos-Schleife, sie muss in jedem C Programm (für µC) vorhanden sein, denn ein PIC arbeitet seinen Programmcode immer durch, wieder und wieder. Eine while Schleife wird solange durchlaufen, wie ihr Ausdruck “wahr” (unterschiedlich zu 0) ist. Da in der Klammer eine 1 steht, ist der Ausdruck immer 1 (=wahr) und die Schleife wird nie verlassen. Es sei denn, die Betriebsspannung wird entfernt. Nachfolgend die Grundschreibweise für eine While-Schleife:

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. 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-Funktionen, beliebig erweitern.

for-Schleife

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:

unsigned char i;
int array_01[5];

for (i = 0; i < 5; i++)
{
  array_01[i]=5*i;
}

In einer for-Schleife gibt es immer eine Variablen Initialisierung eine Abbruchbedingung und eine Schleifenanweisung. In dem obigen Beispiel ist die Variablen Initialisierung das: i=0; Somit ist festgelegt, dass die Variable “i” zu Beginn den Inhalt “0” hat. Die Abbruchbedingung ist: i<5; Also sobald diese Bedingung nicht wahr ist, wird die For-Schleife beendet. Die Abbruchbedingung wird immer beim Start überprüft. Zum Schluss noch die Variablen Veränderung, diese ist in dem Beispiel: i++; Also wird nach jeder durchlaufenden Schleife die Variable “i” um 1 erhöht (inkrementiert). Somit können wir sagen, diese Schleife würde 5 mal durchlaufen werden.

do While-Schleife

Die do while Schleife ist wie eine ganz normale while Schleife, bloß dass diese mindestens einmal durchlaufen wird. Am Ende der Schleife wird dann die Bedingung überprüft, wenn dieser Ausdruck von 0 verschieden ist (wahr), dann wird die Schleife wiederholt, ist der Wert 0 (falsch), dann wird die Schleife beendet!

do
{
} while(i <= 10);

Weitere Steuerungen (break, continue, …)

break Anweisung

Die break Anweisung beendet einen gerade laufenden Programmblock. Hinweis: Es wird die Schleife komplett beendet! Siehe hierzu auch das Beispiel bei Switch Case.

continue Anweisung

Ist ähnlich wie die Break Anweisung. Wenn in einer Schleife die continue Anweisung erscheint, wird der Rest der Schleife nicht mehr durchlaufen sondern der PIC springt sofort zu Schleifenabfrage vor. Also wird nicht wie bei der Break Anweisung die Schleife ganz beendet sondern nur der aktuelle Durchlauf!

return Anweisung

Dir return Anweisung wird hauptsächlich dafür verwendet, wenn Werte aus einer Funktion zurück gegeben werden sollen.

Leave a Comment