PIC C Tutorial – Einleitung

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 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 (der Compiler übersetzt das Programm in die Maschinensprache) berücksichtigt. Durch die einleitenden zwei // wird ein Kommentar angezeigt. Alles was sich nach diesen beiden Slashs in dieser (!) Zeile befindet hat keinen Einfluss auf das Programm. Sobald aber die Zeile zu Ende ist und ihr in der nächsten Zeile weiterschreibt, ist der Kommentar zu Ende! Wenn ihr längere Kommentare über mehrere Zeilen verfassen möchtet, dann solltet ihr euch einer alternativen Schreibweise bedienen:

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

Alternativ ginge natürlich auch:

// Mehrzeilige Kommentare können auch
// so geschrieben werden

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 Teil des Kommentar und wird den Programmcode nicht 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 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 sogenannte Headerdateien. 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.

Merke  Die Headerdatei xc.h muss immer mit #include <xc.h> eingebunden werden, damit Register etc. des in den Projekteinstellungen ausgewählten PIC bekannt sind.

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 Headerdatei (nur beim Einsatz des XC-Compiler)
  • Eine Endlosschleife in der main Funktion

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:

  • Programmcode immer gut kommentieren
  • moularisieren (Aufteilung in Source und Header Dateien)
  • Funktionen hinreichend verwenden
  • Dokumentation verfassen (oft vernachlässigt)
  • einem gewissen Stil folgen (Funktionsnamen immer klein / Makros immer groß geschrieben …)

Mit diesen genannten 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.

Merke  Funktionen verwenden um ein Programm/Projekt übersichtlich(er) zu gestalten!

Grundsätzlich bekommt jede Sourcedatei C eine sogenannte Headerdatei H 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 clock.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 clock.c Datei notwendig. Wir können also  in der Headerdatei der clock.c (also in clock.h)  bekannt machen (siehe Schlüsselwort extern), dass dieser Vektor existiert und wir ihn somit auch in der clock.c Datei verwenden können. Oftmals bietet es sich an in der main.c auch nur die main-Funktion zu schreiben. Alles andere lagern wir in weitere C-Dateien (auch Module genannt) aus. Somit erhalten wir maximale Übersicht und es ergeben sich mehrere Source Dateien, welche sich jeweils mit Problemen des Projekts befassen. Diese sollten daher auch einen geeigneten Dateinamen bekommen. Zum Beispiel könnte man sich vorstellen, dass sich die Datei bzw. das Modul clock.c in irgend einer Weise mit der Uhrzeit befasst. In der main.c haben wir dann den vollen Überblick über den Ablauf des Programms.

Hinweis  Bitte nehmt euch diese Strukturhinweise zu Herzen. 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 eigentliche Ü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 des Moduls 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.

Es geht aber noch weiter: Makros bzw. Defines können auch mir Parametern ausgestattet werden:

#define SUM(a,b) (a + b)

Das gezeigte Beispiel SUM(a,b) würde in diesem Fall nichts anders tun als die Summe aus a und b zu berechnen.

Hinweis  Makros werden üblicherweise groß geschrieben, damit man sie schnell identifizieren kann. Kleingeschrieben geht es zwar genauso man pflegt aber keinen guten Stil. Außerdem kann man klein geschriebene Makros, die ggf. noch über Parameter verfügen nicht mehr von normalen Funktionsaufrufen unterscheiden. Tut euch und eurer Umwelt den Gefallen und schreibt Makros in Großbuchstaben!

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
#define LEDEIN LATBbits.LATB3=1;
...

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

Hier würde die LED nur dann eingeschaltet werden, wenn oben das #define DEBUG einkommentiert ist. Sobald die Zeile auskommentiert werden würde (z.B. durch vorangestellte //) wird der Programmcode im Bereich zwischen #if DEBUG und #endif weder ausgeführt noch compiliert. Das bedeutet tatsächlich, dass dieser Code noch nicht einmal compiliert wird. Somit kriegt zum Beispiel ein Kunde nichts von diesem Code mit. Da der Präprozessor keine geschweiften Klammern kennt, wie wir sie für Funktionen einsetzen {}, müssen wir Anweisungen zwischen #ifdef und #endif verpacken.

Liste aller Präprozessoranweisungen

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

Anweisung 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 Abschluss für #if, #ifdef oder #ifndef
#error Fehlernachricht ausgeben
#warning Ausgabe von Warntexten ohne Abbruch des Vorgangs
#pragma Herstellerspezifische Anweisung

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 zur 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 main in den runden Klammern ist der Übergabeparameter (können mehrere sein). 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.

Achtung  Die main Funktion darf nie vergessen werden! Wenn die main Funktion nicht vorhanden ist, kann der Compiler das Programm nicht übersetzen, da jedes Programm als erstes in diese Funktion springt und somit der Einsprungvektor fehlt.

Da es immer wieder vorkommt, dass Leute in ihrem Programm eine fehlerhafte Struktur haben, hier nochmal ein allgemeiner Hinweis wie ihr euer Programm bzw. in diesem Fall eure main Funktion 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! */
   }
}

Aufwendige Algorithmen haben im Allgemeinen nichts in der main Funktion verloren. Nach meinem Dafürhalten befinden sich in der main Funktion neben einigen notwendigen Funktionsaufrufen zu diversen Initialisierungen lediglich noch die notwendige Endlosschleife mit dem Aufruf bzw. dem Hauptzyklus des Programms.

Ausblick

Im nächsten Kapitel des PIC-C-Tutorials beschäftigen wir uns mit den Grundlagen der Programmiersprache C. Dabei wird es hauptsächlich um Variablen gehen und wie wir damit arbeiten können. Wenn ich dein Interesse geweckt habe, dann lies doch direkt im nächsten Kapitel weiter: PIC C Tutorial – Grundlagen

Leave a Comment