Ich möchte Euch hier in diesem kleinen PIC-C Tutorial etwas die Hochsprache C näher bringen und vor allem wie man damit PIC-Mikrocontroller programmieren kann. Ich persönlich habe damals mit Assembler angefangen, was mir beim Einstieg in C ganz gut geholfen hat. Während ich Assembler nur 1 Jahr lang in der Berufsschule gelernt habe, musste ich mir C komplett selber beibringen. Ihr werdet aber schnell feststellen, dass es nicht unmöglich ist – es macht sogar Spaß.

Ich werde euch im Verlauf dieses Tutorials immer wieder einzelne Codeschnipsel oder Programmausschnitte zeigen. Es handelt sich dabei wirklich immer nur um Ausschnitte und nicht um fertige Programme, die Ihr via Copy & Paste so 1:1 übernehmen könnt und ohne jegliche Anpassung kompilieren könnt. Jeder PIC-Typ braucht zum Beispiel einige spezielle Einstellungen (Konfigurationswort). Dies könnte ich hier nicht verallgemeinert für alle PIC-Typen abbilden. In diesem Tutorial wird es darum gehen wie Ihr mit der Sprache C umgehen müsst um Projekte zu erstellen und individuelle Probleme zu lösen. Ich zeige Euch dabei natürlich auch wie Ihr Schritt für Schritt ein Projekt von Null an beginnt. Also gut, legen wir los.

Vorbereitungen

Bevor wir loslegen können bedarf es ein paar Vorbereitungen. Zunächst einmal braucht Ihr die entsprechende Entwicklungsumgebung um das Programm für den PIC zu schreiben. Ich empfehle euch die kostenlose Software MPLABX IDE  direkt von Microchip. Sie ist zum einen kostenlos und lässt sich mit den neuen Compilern (zum Beispiel dem XC8) verbinden, welchen wir ebenfalls für das Schreiben unserer C-Programme benötigen.  Nachdem Ihr die IDE und den Compiler (wir benötigen den XC8) installiert habt, kann es auch schon losgehen.

Erstellen eines neuen Projekts

Damit Ihr euch für die ersten Schritte in der neuen Benutzeroberfläche der Entwicklungsumgebung zurecht findet, habe ich einen kurzen übersichtlichen Artikel geschrieben, der sich ausschließlich mit der Entwicklungsumgebung MPLABX beschäftigt. Hier findet Ihr auch einen Abschnitt darüber, wie Ihr ein neues Projekt erstellt. Hierfür bitte einmal mal diesen Abschnitt Artikel durchlesen.

Erstes Beispielprogramm

Wenn Ihr gleich das erste Beispielprogramm seht, werdet Ihr euch wahrscheinlich erstmal wundern. Zu sehen sind bloß irgendwelche Buchstaben und Zeichen. Verstehen werdet Ihr vermutlich erstmal wenig. Aber lasst Euch davon nicht abschrecken. Es verhält sich ganz ähnlich wie bei Euren ersten Englisch Unterrichtsstunden, da habt Ihr auch erstmal wenig verstanden. Genauso ist es auch beim Programmieren, denn es ist auch eine Fremdsprache.

Also gut, legen wir los: Das erste Programm (noch einmal der Hinweis: Dies ist nur ein Beispielcode und kein vollständig kompilierbares Projekt):

// Unser erstes Programm

/*Einbinden einer Headerdatei*/
#include <xc.h>

/*Beginn der main-Routine*/
void main (void)
{
   /*Aufruf eines Unterprogramms*/
   LED_Blinken();
}  /*Ende der Funktion*/

Wie ich oben bereits erwähnt habe wird Euch das eventuell erstmal abschrecken. Aber keine Sorge, es sieht komplizierter aus als es ist. Die Grundlagen der Programmiersprache C sind relativ einfach zu erlernen. Es ist allgemein üblich, dass Programmcodes in Englisch kommentiert werden. Ich werde aber grundsätzlich auf Deutsch zurück greifen, da man als Anfänger schon genug damit zu tun hat die Sprache C an sich zu begreifen, da braucht man nicht noch eine Erklärung in einer weiteren Fremdsprache! Wenn Ihr Euch dann irgendwann etwas geübter in der Sprache C bewegt, kann ruhig mal ein englischer Kommentar Platz finden. Aber Schritt für Schritt. Nun zurück zum Beispielprogramm. Nehmen wir das Programm einmal auseinander und schauen uns die einzelnen Befehle/Zeilen an:

// Unser erstes Programm

Das ist ein Kommentar und wird somit nicht vom Compiler(1) berücksichtigt. Durch die einleitenden zwei // wird ein Kommentar angezeigt. Alles was sich nach diesen beiden Slashs in der Zeile befindet hat keinen Einfluss auf das Programm. Sobald aber die Zeile zu Ende ist und Sie in der nächsten Zeile schreiben, ist der Kommentar zu Ende! Wenn Sie Kommentare verfassen, welche über die Länge von einer Zeile hinaus gehen, dann solltet Ihr euch einer anderen Schreibweise bedienen:

(1) Übersetzt das Programm in die Maschinensprache

/*So könnt Ihr
ruhig in mehreren Zeilen schreiben*/

Durch einen Schrägstrich mit einem folgenden Sternchen leitet Ihr ein Kommentar ein. Alles was nun bis zu dem beendenden des Kommentars mit */ geschrieben wird ist in dem Kommentar und wird nicht den Programmcode beeinflussen. Nun schauen wir uns den nächsten Teil des Programms an:

#include <xc.h>

Durch den #include Befehl können wir Funktions Bibliotheken in unser Programm einbinden und somit benutzen. Die Bibliothek des verwendenden PIC ist natürlich gezwungenermaßen erforderlich! Durch den include Befehl werden Dateien in das Programm eingebunden, welche die Dateiendung *.h haben. Also sind es Header-Dateien. Zusammengefasst: Ihr müsst immer die Bibliothek des PIC, den Sie verwenden einbinden. Anders als noch unter dem alten C18-Compiler wird unter Verwendung der XC-Compiler einfach nur noch die XC-Headerdatei eingebunden.

Die Headerdatei <xc.h> muss immer eingebunden werden!
Diese Headerdatei wird mit dem Compiler automatisch in eurem System abgelegt.

Programmaufbau

Damit ein Programm kompilierbar / ausführbar ist, muss ein für einen PIC-Controller geschriebenes C-Programm muss immer die folgenden Komponenten enthalten:

  • Das individuelle Konfigurationswort
  • Die eingebundene <xc.h> Datei (beim XC-Compiler)
  • Eine Endlosschleifewhile(1)

Damit sollte auch nochmal deutlich klar werden, warum das in diesem Tutorial gezeigte erste Beispielprogramm nicht vollständig ist, denn es fehlt (ganz abgesehen davon, dass die Funktion LED_Blinken() nicht definiert ist) das Konfigurationswort für den jeweiligen PIC-Typen. Ein paar Beispiele für vollständige Programme gibt es hier (diese Beispiele beziehen sich dann auch auf konkrete Hardware / einen bestimmten PIC).

Projektstruktur

Man ist (oder besser sollte man) als Programmierer immer bemüht sein eine gewisse Struktur in seinem Projekt zu bekommen, damit es übersichtlich bleibt. Nichts ist schlimmer als eine Codewüste wo nach zwei Wochen kein Mensch mehr durchsteigt. Muss man nach einer gewissen Zeit wieder an seinen Quellcode heran treten um ihn zu Warten (elementarer Bestandteil der Softwareentwicklung) und der Code ist derart schlecht strukturiert, dass man erstmal Stunden braucht um ihn wieder zu verstehen, dann ist damit keinem geholfen. Es hätte den selben Effekt ein ganz neues Programm zu schreiben. Daher sollte man sich an gewisse Konventionen halten:

  • Den Programmcode immer gut kommentieren
  • Moularisieren (Aufteilung in Source und Header Dateien)
  • Funktionen hinreichend verwenden
  • Dokumentation verfassen (oft vernachlässigt)

Mit diesen drei Punkten kann man sich im Nachhinein viel Arbeit, Ärger und Kummer ersparen. Ihr findet in meinem Artikel – Projektstruktur – eine detailliertere Einführung in das Strukturieren eines Softwareprojektes. Ich möchte besonders für die Einsteiger auf den zweiten und dritten Punkt noch einmal eingehen:

Stellt Euch folgendes Szenario vor: Ihr schreibt ein Programm indem der Analogwert einer Knopfzelle eingelesen/verarbeitet und formatiert auf einem Display dargestellt wird. An sich kein großer Programmieraufwand. Als Einsteiger (aber auch als Fortgeschrittener) könnte man hierfür die Bibliothek des Compilers verwenden. Man bindet diese in mit einem Präprozessor Befehl #include ein und es stehen einem ihre Funktionen zur Verfügung. Schon hat man eine Struktur erstellt. Es würde wenig Sinn machen den gesamten Code direkt in die C-Datei zu kopieren, denn es wird furchtbar unübersichtlich.

Funktionen verwenden um das Programm übersichtlich zu gestalten!

Oftmals bietet es sich an in der main.c auch nur die main-Funktion zu schreiben alles andere lagern wir in weitere C-Dateien aus. Somit erhalten wir maximale Übersicht und es ergeben sich mehrere Source Dateien, welche sich jeweils mit Problemen des Projekts befassen und sollten daher auch einen geeigneten Dateinamen bekommen. Zum Beispiel könnte man sich vorstellen, dass sich die Datei Uhr.c in irgend einer Weise mit der Uhrzeit befasst. In der main.c haben wir dann den vollen Überblick über den Ablauf des Programmes.

Grundsätzlich bekommt jede C-Datei eine sogenannte Header Datei zugewiesen. In dieser Headerdatei stehen wichtige Dinge, welche unter anderem auch für andere Dateien relevant sein können. Ein Beispiel: Wir haben in unserem Projekt zwei C-Dateien: DCF.c und Uhr.c. In der DCF Datei wird das DCF77 Zeitsignal eingelesen und dekodiert. Die einzelnen Zeitinformationen speichern wir in einem Vektor ab. Diese Informationen ist nun aber auch in der Uhr.c Datei notwendig. Wir können also  in der Headerdatei der Uhr.c Datei, also in der Uhr.h,  bekannt machen (Klasse: extern), dass dieser Vektor existiert und wir können ihn somit auch in der Uhr.c Datei verwenden.

Bitte achtet auf diese Strukturhinweise, damit helft Ihr nicht nur euch selber sondern auch anderen, wenn Sie euch bei Problemen unterstützen sollen.

Etwas detaillierter ist der strukturierte Aufbau eines C-Projektes hier beschrieben. Ich kann Euch nur dazu raten diese Vorschläge zu beherzigen, da es ansonsten in größeren Projekten sehr schnell sehr unübersichtlich wird (was auch ggf. Hilfe von außen erschwert).

Präprozessor

Der Präprozessor ist ein nützlicher Helfer, der uns das Schreiben von Programmen erleichtert. Wir haben den Präprozessor weiter oben schon angewendet:

Alle Codezeilen, die mit dem Zeichen “#” beginnen sind Anweisungen für den Präprozessor. Beim Kompilieren eines Programms wird zunächst der Präprozessor ausgeführt. Erst danach beginnt das eigentöiche Übersetzen des Quellcodes. Wir erinnern uns an folgende Zeile aus unsrem ersten Programm:

#include <xc.h>

Diese Zeile beginnt mit der Raute und ist folglich eine Anweisung für den Präprozessor. Hier soll er den Inhalt der Headerdatei xc.h an dieser Stelle in die Datei einbinden. Man könnte genauso gut den gesamten Inhalt der Headerdatei durch Copy&Paste an dieser Stelle einfügen. Damit wäre die Übersichtlichkeit jedoch völlig vernichtet. Der Präprozessor kann aber noch mehr…

Makros

Gerne benutzen wir den Präprozessor um Makros in unser Programm einzubauen. So lassen sich Dinge realisieren wie:

#define LED_EIN LATAbits.LATA0=1;
#define LED_AUS LATAbits.LATA0=0;
...
LED_EIN /*Anwendung des Makros*/
...
LED_AUS
...

Dies ist an sich kein C-Code doch durch den Präprozessor können wir uns nützliche Makros einfallen lassen, damit wir den Code leserlicher gestalten. Der Präprozessor sucht im Programmcode nun alle Stellen wo LED_EIN bzw. LED_AUS steht und ersetzt sie mit dem was wir oben im #define definiert haben.

Programmierstil: Makros werden üblicherweise groß geschrieben, damit man sie schnell identifizieren kann. Kleingeschrieben geht es zwar genauso man pflegt aber keinen guten Stil (herzu sei gesagt, dass habe ich erst erfahren als ich Makros schon oft verwendet habe, daher sind sie bei mir oftmals noch klein geschrieben).

Bedingtes Compilieren

Wenn wir ein Programm erstellen verwenden wir häufig kleine Codesnipsel um die Funktionalität des Programms zu prüfen. Zum Beispiel möchten wir eine LED einschalten, wenn eine if-Bedingung erfüllt wurde. Dies soll allerdings im fertigen Programm (Release) nicht mehr der Fall sein. Für solche (und noch mehr) Zwecke eignet sich die bedingte Compilierung des Präprozessors.

Hier ein Beispiel:

#define DEBUG 1
#define LEDEIN LATBbits.LATB3=1;
...

if(TEMPOVER)
{
   #if DEBUG
      LEDEIN
   #endif
   ...
}

Hier würde die LED nur dann eingeschaltet werden, wenn oben im #define eine 1 angegeben ist. Sobald die 1 durch eine 0 ersetzt wird, wird der Programmcode im Bereich zwischen #if DEBUG und #endif weder ausgeführt noch compiliert. Das bedeutet, dass dieser Code noch nicht einmal compiliert wird, wenn DEBUG nicht mit 1 definiert wurde. Somit kriegt zum Beispiel ein Kunde nichts von diesem Code mit.

Hinweis: Der Präprozessor kennt keine geschweiften Klammern, daher müssen wir die Anweisungen in dieses Muster schreiben #if … #endif

Liste aller Präprozessoranweisungen

Nachfolgend eine Aufzählung mit Anweisungen, sowie der Bedeutung, die Ihr mit dem Präprozessor verwenden könnt.

Anweisungen Bedeutung
#include Fügt einen Dateiinhalt ein z.B. eine Headerdatei
#define Definiert ein Makro oder eine symbolische Konstante
#if Verzweige in Abhängigkeit von einem Ausdruck
#undef Aufheben von Makros, d.h. ab jetzt wird makro nicht mehr ersetzt
#elif Weitere Alternative wenn #if nicht zutrifft
#else Alternative wenn #if , #ifdef oder #ifndef nicht zutreffen.
#ifdef Verzeige, wenn eine Präprozessorkonstante definiert wurde.
#ifndef Verzeige, wenn eine Präprozessorkonstante nicht definiert wurde.
#endif Abschluß für #if #ifdef oder #ifndef
#error Fehlernachricht ausgeben.
#warning Ausgabe von Warntexten ohne Abbruch des Vorgangs
#pragma Herstellerspezifische Aktionen

Für weitere Codebeispiele siehe WikiBooks 

Die Main Funktion

void main(void)

Durch diese Codezeile wird die main Funktion angeführt. Alles was nun in den beiden folgenden geschweiften Klammern { } steht, gehört zu der (main) Funktion. Dies ist ganz ähnlich wie in Assembler wo das main Programm durch die Marke main begonnen wird und durch ein Goto main abgeschlossen wird. Das void vor dem main ist der so genannte Rückgabeparameter. Man kann damit einer aufrufenden Funktion einen Wert zurück geben (dies wird aber später unter Funktionen erklärt). Das void nach dem main in Klammern ist der Übergabeparameter. Da wir in diesem Beispiel-Programm auf Über- bzw. Rückgabeparameter verzichten, wird an die stellen void geschrieben was soviel bedeutet wie, wird nicht verwendet, leer.

Die main-Funktion darf nie vergessen werden! Ist die main-Funktion nicht vorhanden, so kann der Compiler das Programm nicht übersetzen, da jedes Programm als erstes in diese Funktion springt.

Da es immer wieder vorkommt, dass Leute in Ihrem Programm eine fehlerhafte Struktur haben, hier nochmal ein allgemeiner Hinweis wie ihr euer Programm strukturieren solltet:

void main(void)
{
   /* Hier können sämtliche Initialisierungen 
      durchgeführt werden (PIC, Display, usw.) */

   while(1)
   {
      /* Hier kommt der Algorithmus/ die Algorithmen hinein,
         jedoch keine Initialisierungen! */
   }
}

Leave a Comment