PIC C Tutorial – Werkzeug

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.

Merke  Immer wenn eine Multiplikation oder Division mit einer Zweierpotenz (2, 4, 8 …) gerechnet werden soll, kann stattdessen auf eine Bitschiebe-Operation zurück gegriffen werden (>> für eine Division bzw. << für eine Multiplikation). Die Anzahl der Stellen die geschoben ergibt sich dann einfach aus dem Zehnerlogarithmus des Faktors (ld(x) = Anzahl an Schiebungen). Diese Art der Berechnung ist für den Prozessor deutlich schneller durchzuführen.

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

Leave a Comment