In diesem Teil des PIC-C-Tutorials beschäftigen wir uns mit den wesentlichen Grundlagen der Programmierung in C. Dabei hat dieses Tutorial keinesfalls Anspruch auf Vollständigkeit. Der Leser sollte jedoch nach dem Durcharbeiten des (gesamten) Tutorials in der Lage sein erste eigene Projekte mit PIC-Mikrocontrollern zu bewältigen und dabei die Hochsprache C als Werkzeug einzusetzen. C ist Case sensitiv! Das bedeutet es macht einen Unterschied ob Ihr Buchstaben groß oder klein schreibt. Heißt eine Funktion zum Beispiel getInteger und Ihr möchtet sie mit GetInteger aufrufen, so wird das nicht funktionieren! …

Des weiteren möchte ich Euch dringen dazu ermahnen; schreibt eure Programme übersichtlich und wenn möglich auch nur einen Befehl pro Zeile. Schaut Euch mal an wie es aussieht, wenn mehrere Befehle in eine Zeile geworfen werden:

#include <xc.h>

void main (void) {LED_Blinken(); }

Das ist schlicht und einfach unübersichtlich. Es mag bei diesem Beispiel noch nicht so dramatisch sein, doch wenn das Programm umfangreicher wird, dann verliert man sehr schnell den Überblick. Rein programmtechnisch würde die Schreibweise aber so auch arbeiten. Es ist aber einfach nicht Zweckdienlich. Und außerdem wird Euch jeder Mensch dieser Erde bei einer solchen Programmierung seine Hilfe verweigern, mit Sicherheit! Daher gewöhnen wir uns ein paar Schreibregeln an. Wir schreiben Grundsätzlich nur einen Befehl pro Zeile gefolgt von einem Kommentar. Und wenn eine Funktion eingeleitet wird, dann wird der Code zwischen den geschweiften Klammern eingerückt geschrieben, so erkennt man sofort wo eine Funktion beginnt, wo sie endet und welche Abschnitte zu ihr gehören. Was wir uns nicht aussuchen können ist, dass eine Anweisung immer mit einem Semikolon “;” beendet werden muss. Ein PIC kann nicht mehrere Operationen gleichzeitig ausführen, sondern arbeitet eine Operation nach der anderen sequenziell ab. Das ist nicht weiter schlimm, weil er dies einerseits sehr schnell macht und anderseits kann er durch Interrupts an eine andere Programmstelle springen, dort etwas abarbeiten und anschließend mit der ursprünglichen Arbeit fortfahren.

Variablen

Um überhaupt mit dem Arbeitsspeicher des PIC zu arbeiten brauchen wir Variablen in die wir Daten schreiben und bei Bedarf wieder auslesen können. Bei Variablen unterscheidet man in C zwischen verschiedenen Typen. Grundsätzlich gilt aber, dass man mit dem Speicherplatz sparsam umgehen soll. Braucht man z.B. nur eine Speicherzelle, die eine maximale Speicherkapazität von 256 haben muss, macht es keinen Sinn eine Variable mit dem Fassungsvermögen von 65536 zu verwenden! Bitte denkt daran, die Speicherkapazität in einem Mikrocontroller ist begrenzt. Anders als in einem gängigen PC verfügt ein Mikrocontroller nicht über GByte an Speicher. Wir befinden uns eher im Bereich von KByte.

Folgend die verschiedenen Typen aufgeführt:

Typ Anzahl der Bits Wertebereich
bit 1 0 bis 1
char 8 -128 bis 127
unsigned char 8 0 bis 255
signed char 8 -128 bis 127
int 16 -32768 bis 32767
unsigned int 16 0 bis 65535
short int 16 -32768 bis 32767
unsigned short int 16 0 bis 65535
short long 24 -8 388 608 bis 8 388 607
unsigned short long 24 0 bis 16 777 215
long int 32 -2 147 483 648 bis 2 147 483 647
unsigned long int 32 0 bis 4 294 967 295
float 32 1.17E-38 bis 6.80E+38
double 32 1.17E-38 bis 6.80E+38

Ich würde Euch empfehlen die Headerdatei stdint zu verwenden.

#include <stdint.h>

Nach der Einbindung könnt Ihr dann Standard-Integer verwenden. Das hat de Vorteil, das diese Typen immer gleich sind. Egal ob Ihr nun auf dem PIC oder auf einem x86 Prozessor programmiert. Es ist sofort ersichtlich um was für einen Typ es sich handelt. Oft verwendet werden folgende:

uint8_t a;    // 8 Bit, vorzeichenlos
uint16_t b;   // 16 Bit, vorzeichenlos

Variablen Deklaration

Jetzt haben wir schon die verschiedenen Variablen Typen kennen gelernt aber wie wendet man diese an? Bei der “Variablen Deklaration” wird der Variablen Typ, der Name und eventuell ein Anfangswert festgelegt. Schauen wir uns das einmal anhand eines Beispiels an:

Typ Variablenname = Anfangswert;

Folglich schreibt man:

unsigned char speicher = 0;

Es hat sich durchgesetzt, dass Variablen immer mit einem Kleinbuchstaben beginnen. Das ist natürlich kein Zwang, jedoch ein inoffizieller Standard. Ihr werdet merken, dass nicht nur Euer Code lesbarer wird, sondern auch Euer Verständnis für fremden Code schneller wächst. Das bedeutet natürlich auch im Umkehrschluss, dass es auch anderen leichter fällt euren Code zu lesen.

Somit hätten wir eine Variable des Typ unsigned char mit dem Namen speicher deklariert. In diese Variable können wir nun Daten speichern. Um eine Variable zu beschreiben schreiben wir folgendes:

speicher = 100;                    // oder..
speicher = 100 + 100;              // oder..
speicher = speicher2 + speicher3;

Es lassen sich Variablen natürlich auch über Operationen beschreiben wie in dem Beispiel oben, dabei spielt es keine Rolle ob die Operation mit Zahlen oder anderen Variablen durchgeführt wird. Man muss natürich darauf aufpassen, dass man immer die entsprechenden Variablen-Typen benutzt. Denn eine Addition von 200+200 ergäbe 400 und wäre somit nicht geeignet für eine unsigned char. Es gäbe keinen Fehler beim Kompilieren jedoch erzeugt die Addition in diesem Fall einen Überlauf was zu schwerwiegenden Problemen führen kann. Interessanter Beitrag hierzu im Focus 

Weitere Beispiele:

speicher1 = 100;
speicher2 = 50;
speicher3 = speicher1/speicher2;
speicher4 = 2*speicher3;
//Der Inhalt von speicher4 ist 4

Speicherklassen

Es sind in C noch diverse Speicherklassen vorhanden mit denen zum Beispiel Variablen versehen werden können. Wozu das nützlich sein kann möchte ich Euch in den nächsten Abschnitten zeigen.

extern

Mit dem Keyword extern könnt Ihr Variablen in anderen Modulen bekannt machen (unter Anwendung von Headerdateien). Ein Beispiel: In der Datei main.c gibt es eine globale* Variable:

* überall gültig, sofern bekannt

unsigned char versionUID;

Die kann nun innerhalb einer Headerdatei, die in einer weiteren C-Datei eingebunden wird bekannt gemacht werden:

extern unsigned char versionUID;

Somit ist die globale Variable nicht nur in der C-Datei verwendbar, in der sie deklariert wurde, sondern auch in der C-Datei die die Headerdatei mit der extern Anweisung einbindet.

volatile

Durch das voranstellen von volatile an eine Variable, kann dem Compiler mitgeteilt werden, dass ein Zugriff von außerhalb auf dieses Element möglich sein soll. Ein Zugriff kann zum Beispiel durch eine ISR (Interrupt service routine) erfolgen. Der Compiler wird an dieser Variable keine Optimierung vornehmen.

volatile int count;

const

Durch const wird eine Variable zu einer Konstanten. Der Wert der Variable ist danach nicht mehr veränderbar!

Übrigens speichert der Compiler diese Daten im Flash und nicht im RAM! Der RAM dient, wie auf dem normalen PC zum Arbeiten. Daher der Name Arbeitsspeicher. Da eine Konstante nicht verändert werden kann wird sie im Flash Speicher abgelegt. So wird der wenige RAM Speicher nicht belastet.

Arrays können nur zu einer begrenzten Größe im RAM des PIC gespeichert werden. Der RAM ist in diverse Bereiche unterteilt. Wenn das Array die Größe eines solchen Bereiches überschreitet, so gibt der Compiler eine Fehlermeldung.

So deklariert Ihr eine Konstante:

const int freq_Hz = 32768;

static

Wird eine Variable mit dem Keyword static ausgestattet, so wird diese Variable global. Jedoch nur in dem Modul indem es initialisiert wurde: Ist eine Variable in einer Funktion als static gekennzeichnet, so wird es diese Variable unverändert bei erneuten Aufruf dieser Funktion geben.

static int x;

Funktionen

Jetzt kommen wir zu den Funktionen. Ihr werdet sie beim Programmieren immer wieder benötigen. Sie sind uns eine große Hilfe beim Strukturieren des Programms bzw. dessen Ablaufs. Schauen wir uns zunächst einmal wieder ein Beispiel an:

void Funktion_A(void)
{
  LATAbits.LATA5 = 1;
}

void Funktion_B(void)
{
  LATAbits.LATA5 = 0;
}

int main(void)
{
  Funktion_A();
  Funktion_B();
  Funktion_A();
  Funktion_B();
}

Was wir ja bereits wissen ist, dass ein PIC immer bei der Funktion main beginnt. Nun schauen wir uns an was dort als erstes auf den PIC wartet. In der Zeile 13 wartet die erste Anweisung auf den PIC, welche ihm mitteilt, dass er die Funktion Funktion_A aufrufen soll. Also wird er in die Funktion springen und landet dann bei dieser Anweisung:

LATAbits.LATA5 = 1;

Dies ist ein neuer Befehl, welchen ich hier nur ganz kurz ansprechen möchte (wird später noch genauer erklärt). Durch diesen Befehl wird das Bit 5 des PORTA auf High gesetzt. Wir nehmen an, dass dort evtl. einer Treiberschaltung (Transistor als Schalter) mit einer LED sitzt. Also würde die LED in der Funktion_A eingeschaltet werden. Nachdem der PIC die Funktion_A komplett abgearbeitet hat (in diesem Fall lediglich eine Anweisung) kehrt er dorthin zurück von wo aus er die Funktion aufgerufen hat und geht einen Schritt vorwärts. Also würde er nun Zeile 14 aufrufen, was zur Folge hätte, dass die LED ausgeschaltet wird. Und so weiter… Und so fort… Fazit: Die LED würde also blinken? Jein, denn die LED würde so schnell blinken, dass wir es gar nicht wahrnehmen würden, denn der PIC arbeitet so schnell, dass unser Auge das Schalten nicht wahrnehmen kann. Aber ohnehin ist das Programm sinnlos, es dient ja nur dem Zweck, dass wir es als Lehrbeispiel verwenden können.

Ein weiteres Beispiel:

int main(void)
{
  Funktion_A();
  Funktion_B();
  Funktion_A();
  Funktion_B();
}

void Funktion_A(void)
{
  LATAbits.LATA5 = 1;
}
void Funktion_B(void)
{
  LATAbits.LATA5 = 0;
}

Nun schauen Sie sich das Programm einmal genau an, dann werden Sie feststellen, dass sich eigentlich zum vorherigen Beispiel nicht viel verändert hat. Es steht jetzt lediglich die main Funktion über den anderen Funktionen. Also das selbe oder? Falsch! Dieses Beispiel würde so nicht funktionieren und der Compiler würde eine Fehlermeldung ausgeben, welche ungefähr heißen würde: “Unbekannte Funktion: Funktion_A“. Das liegt daran, dass eine Funktion immer vor der main Funktion stehen muss oder die Funktion, wenn sie unter der main Funktion steht einmalig angemeldet (Prototypen schreiben) werden muss. Folgendes Beispiel würde wieder funktionieren:

void Funktion_A(void);
void Funktion_B(void);

int main(void)
{
  Funktion_A();
  Funktion_B();
  Funktion_A();
  Funktion_B();
}

void Funktion_A(void)
{
  LATAbits.LATA5 = 1;
}

void Funktion_B(void)
{
  LATAbits.LATA5 = 0;
}

Dieses Programm würde funktionieren, da wir die Funktionen vor main angemeldet haben. Dies geschieht einfach indem man die Funktionsköpfe einmal aufführt. Man nennt das auch Prototypen.

Übergabeparameter

Als nächstes geht es um die “void”, die wir bisher nicht angesprochen haben. Funktionen haben in C noch einen kleinen aber feinen Vorteil, denn man kann ihnen Parameter übergeben. Das bedeutet, dass wenn man eine Funktion aufruft ihr einen Wert mitgeben kann und wenn die Funktion zu Ende ist, kann somit ein Wert zurück gegeben werden. Sehen wir uns dazu mal ein Beispiel an:

void Funktion_A(void);
void Funktion_B(void);

int main(void)
{
  Funktion_A(5);
}

void Funktion_A(unsigned char A)
{
  if (A==5)
  {
    LATAbits.LATA5 = 1;
  }
  else
  {
    LATAbits.LATA5 = 0;
  }
}

Hier würde der PIC in der Zeile 6 die Funktion Funktion_A aufrufen und würde den Wert “5” mit übergeben. Da nun in der Funktion_A der Übergabeparameter als “unsigned char A” deklariert wurden ist, steht die “5” nun in “A” (“A” ist nun auch eine normale Variable und kann innerhalb der Funktion als solche genutzt werden). Jetzt wird die Funktion Funktion_A abgearbeitet und beginnt also in der Zeile 11, wo etwas steht, was wir noch nicht kennen (Erklärung folgt) aber uns evtl. erschließen können. Es steht “if(Wenn) (A==5)”, dass bedeutet, wenn die Speicherzelle oder Variable “A” den Wert “5” beinhaltet, dann führe aus, was in den folgenden geschweiften Klammern {} steht… else(Ansonsten), wenn nicht (oder Ansonsten) führe aus was in den Nachfolgenden geschweiften Klammern {} steht. Da “A” ja nun den Wert “5” tatsächlich beinhaltet würde die LED bei diesem Programm immer eingeschaltet sein. Würde die Variable “A” einen von “5” unterschiedlichen Wert annehmen (im Beispielprogramm nicht möglich), würde die else Routine durchlaufen werden und die LED wäre aus. Das (A==5) ist eine so genannte Bedingung, wenn diese Bedingung wahr ist, wird die if-Routine ausgeführt.

Merke: 5 == 5 ist eine wahre Bedingung, während 5 == 4 eine falsche Bedingung ist. Und ganz wichtig: Nicht “==” mit “=” verwechseln! 

Rückgabewerte

Nun kann man natürlich nicht nur einen Wert übergeben, sondern kann auch Werte zurückgeben. Das sind dann die so genannten Rückgabewerte und sind im Prinzip nichts groß anderes. Schauen wir uns hierzu doch wieder ein Beispiel an:

void Funktion_A(void);
void Funktion_B(void);

unsigned char c;

int main(void)
{
  c = Funktion_A(13,17);
}

unsigned char Funktion_A(unsigned char A, unsigned char B)
{
  return(A+B)
}

Dieses Programm würde der Variable c den Summand der Zahlen 13 und 17 zuweisen (30). Dies würde folgendermaßen ablaufen: Das Programm will der Variable c einen Wert zuweisen, findet dann jedoch einen Aufruf zu einer Funktion vor. In dieser Funktion werden nun die ihr übergebenden Werte (13 und 17) in die Variablen (A und B) geschrieben. Anschließend wird die Funktion durch den Befehl return wieder verlassen mit dem Rückgabewert (A+B). Also wird die “30” zurück gegeben (Die “30” steht nun in de Variablen c!

Vielleicht stellt Ihr euch gerade die Frage was man machen kann, wenn man mal mehr als einen Rückgabewert benötigt. Dafür müssen wir uns erst mit Zeigern beschäftigen. Also noch etwas Geduld.

Leave a Comment