Tips mit PICs 10: Die Befehle #define und #ifdef

Auch dieser 'Tip' ist ein Hinweis, was man machen könnte, aber nicht unbedingt machen müßte.
Einschränkung: Wir beziehen uns hier auf das Programmsystem MPLAB IDE von microchip, das kostenlos heruntergeladen werden kann.

Wenn man von der Programmierung mit Hochsprachen kommt (man kann auch BASIC dazurechnen), dann nervt die 'Bit-Klopferei' der Assemblersprache doch gewaltig. Nur ein Beispiel: Es braucht immer etliche Phantasie, Befehle zur Null-Abfrage richtig zu interpretieren, zumal bei 'Null' das Zero-Bit im STATUS-Register auf '1' abgefragt werden muß. Da ist jedesmal eine Art Stolperstein integriert. Wenn Sie lesen
btfss STATUS,Z
dann erschließt sich der Sinn des Befehles nicht sofort. Dies ist aber für das Verstehen des Programms immens wichtig, da hier ja eine Verzweigung stattfindet. (Der Autor hat auch nach Jahren intensiver Programmier-Tätigkeit immer noch Probleme!)
Oder:
Sie programmieren eine Steuerung, die viele Ausgänge einzeln setzen soll. Später, beim Entwerfen der Platine, stellt sich aber heraus, daß sich viele Leiterbahnen am PIC überkreuzen würden und daß es daher sinnvoll wäre, diese zu entwirren. Kein Problem, nur müssen Sie in Ihrem Programm genau das Gleiche tun und dürfen dabei nicht einen einzigen Fehler machen. Ansonsten läuft Ihr Programm 'falsch'.

Derartige Probleme kann man auf einfachste Weise lösen, indem man den Befehl
#define
einsetzt. Dieser ist 'nur' die Anweisung an das Assemblier-Programm, einen Text durch einen anderen zu ersetzen. So steht es, reichlich dürftig, in der Beschreibung. Was man aber tatsächlich alles damit machen kann, läßt sich kaum erahnen.

Null-Abfrage:
Sie meinen, mit einem Befehl, der z.B.
ist_null?
heißen könnte, kämen Sie besser zurecht als mit dem (oben beschriebenen) btfss-Konstrukt. Dann definieren Sie sich doch diesen neuen Befehl, indem Sie schreiben:
#define ist_null? btfss STATUS,Z
Sogar das Fragezeichen funktioniert. Damit sagen Sie dem Assemblier-Programm, es soll jedesmal, wenn es die Zeichenfolge 'ist_null?' findet, stattdessen 'btfss STATUS,Z' einsetzen.
Das war's schon. Nur: Leerzeichen sind nicht erlaubt. Wenn Sie sich selber noch einen Gefallen tun wollen, schreiben Sie vor die Definitions-Zeile noch eine Kommentarzeile, etwa so:
; Ersatz der Null-Abfrage: skip, wenn Null
Das hilft Ihnen später beim Lesen enorm; und auch anderen, die Ihr Programm lesen und verstehen sollen. Nun können Sie an beliebiger Stelle im Programm den neuen Befehl einsetzen, und wenigstens dieser Stolperstein ist weg.
Der hier genannte Ersatz ist natürlich nur ein Vorschlag. Sie können jeden beliebigen Text (außer einem bereits definierten Schlüsselwort) verwenden, Beispiele:
skip_wenn_null
sze
Das Letztere ist aus dem Befehlssatz der alten AEG 60-10 entlehnt. Dort hieß die Nullabfrage 'tze' (test zero equal); hier würde es analog heißen: 'skip zero equal'.
Natürlich geht das auch mit Eigen-Konstruktionen für das ach so heißgeliebte Carry-Bit (bei dem man immer wieder Überraschungen erlebt), wie z.B. mit:
ist_ueberlauf?
ist_unterlauf?
Bevor wir hier jetzt etwas Falsches schreiben: Sie wissen ja jetzt, wie es geht ...

Setzen eines Bits an einem E/A-Port:
Hier sollten Sie jeder Ein- und Ausgabe-Leitung einen sinnvollen Namen geben. Beispiel:
#define Lampe1 PORTA,4
Es ist klar erkenntlich, daß Sie mit Leitung 4 von Port A die Lampe 1 schalten wollen. Diese Namensvergabe sollten Sie für jede Leitung vornehmen. Und im Programm können Sie jetzt ohne Kommentar schreiben:
bsf Lampe1
oder
bcf Lampe1
Nun brauchen Sie bei Änderungen in der Verdrahtung (Lampe 1 wird z.B. nunmehr von Port A, Leitung 5 geschaltet) nur noch ein einziges Mal dieses 'define' zu ändern,
#define Lampe1 PORTA,5
und alles läuft wie vorher.

Luxus-Version:
Wenn Sie schon einmal beim 'Um-Definieren' sind, können Sie auch noch einen Schritt weiter gehen und schreiben:
#define Lampe1_ein bsf Lampe1
#define Lampe1_aus bcf Lampe1
oder
#define Lampe1_ein bsf PORTA,4
#define Lampe1_aus bcf PORTA,4
Auch das doppelte Um-Definieren im ersten Vorschlag wird richtig verarbeitet. Die '#define'-Anweisung dürfen Sie beliebig oft verwenden. Nur muß die Neu-Definition aus einem einzigen Wort bestehen, das bisher nicht verwendet wurde und auch nicht als Kommentar auftritt. Ansonsten kommt der Assembler 'ins Schleudern'. Das zweite Vorschlags-Pärchen ist auch möglich, aber nicht zu empfehlen, da das Ändern bei Um-Verdrahtung (s. oben, PORTA,4 in PORTA,5 ändern) evtl. an mehreren Stellen geschehen muß. Und gerade das wollten wir ja vermeiden!

Falls Sie die Lampe gegen Masse (ein-)schalten wollen, muß der Ausgang natürlich auf 'Low' gesetzt werden. Elegant können Sie dann schreiben:
#define Lampe1_ein bcf Lampe1
#define Lampe1_aus bsf Lampe1
Jedesmal, wenn Sie die Lampe 1 einschalten wollen, schreiben Sie nur noch
Lampe1_ein
oder
Lampe1_aus
Wie und in welche Richtung das E/A-Bit gesetzt wird, ist hier völlig egal. Das haben Sie an anderer Stelle mit dem '#define' festgelegt. Sie entfernen Sich damit wohltuend vom Bit-Klopfen zur logisch verständlichen Programmierung.

Bei uns wird von diesen hier beschriebenen Um-Definierungen exzessiv Gebrauch gemacht:
Der Weichendecoder für das Faller-Car-System besteht aus einer ausgetesteten Platine, die nicht mehr verändert werden wird. In dem darauf sitzenden PIC existiert ein Rumpf-Programm, in dem alle Zuordnungen definiert worden sind. Diese Platine wird maximal 63-mal benötigt, für 63 verschiedene Weichen. Die individuellen Programme, eines für jede Weiche, sind noch nicht geschrieben (auch weil die Weichen und der Straßenverlauf noch in der Planung sind).
Wenn sich jemand daran macht, für eine neue Weiche ein Programm zu schreiben, braucht er nur noch anzugeben, welchen Ein- oder Ausgangspin der Platine er bearbeiten will. Die sind ihm, auch wegen der Verdrahtung in die Anlage, bestens bekannt. Um die Beinchen am PIC braucht er sich nicht mehr zu kümmern: eine erhebliche Vereinfachung und Fehlervermeidung.
Eine weitere Vereinfachung ist das Erstellen eines eigenen Programm-Schnipsels, das nur die Neu-Definierungen der Befehle beinhaltet. Dieses wird bei jedem Programm dazugebunden. Dadurch werden diese Befehle quasi zu einem Standard beim MEC.

noch eine wichtige Anwendung:
Eine Änderung der Verdrahtung, wie oben beschrieben, könnte auch den Wechsel des PICs beinhalten. Nehmen wir an, infolge gesteigerter Anforderungen (so etwas soll vorkommen!!) muß ein PIC mit mehr Beinchen eingesetzt werden. Auch hier genügt es, die neue Konfiguration (wir meinen hier die Zuordnung von Funktion zur Hardware) per #define einzugeben. Und wieder ist dieses Problem gelöst.

Ein Nachteil soll hier nicht verschwiegen werden:
Da das Assemblier-Programm die neu geschriebenen Texte nicht sofort als Befehle erkennt, werden sie auf dem Bildschirm nicht in der Befehls-Farbe (blau) dargestellt. Das ist gewöhnungsbedürftig!

Die Befehlsfolge #ifdef - #else - #endif
ist eine Anweisung an das Assemblier-Programm, die nachfolgenden Befehlszeilen nur dann einzubinden, wenn eine bestimmte Variable vorher definiert wurde (Stichwort: bedingte Assemblierung). Sinnvollerweise muß dieser Programmteil mit der Anweisung '#endif' abgeschlossen werden.
Beispiel:
Sie wollen beim Testen Ihres Programms an mehreren Stellen jeweils ein paar Befehle ausführen, die in der endgültigen Version nicht erwünscht sind, z.B irgendwelche Anzeigen setzen. Sie können sich das Schreiben und spätere Löschen vereinfachen und damit Fehler umgehen, wenn Sie das '#define' in Kombination mit '#ifdef' verwenden.
Als allererste Zeile in Ihrem Programm (muß dort nicht sein, steht aber an herausragender Stelle) schreiben Sie:
#define testfall 1
Hiermit erzeugen Sie eine Variable mit den Namen 'testfall' und geben ihr einen beliebigen Wert, hier '1'.
An beliebiger Stelle im Programm können Sie nun Befehle einfügen, die Sie nur zum Testen benötigen:
#ifdef testfall
...
...
#endif
Wenn Sie die Definition in der ersten Programmzeile aus-kommentieren, werden der beschriebene Block (und evtl. noch viele andere Blöcke) nicht mit assembliert, da 'testfall' nun nicht bekannt ist. So ist die einzige Umstellung zum 'Normalfall' das Einfügen eines Semikolons; mit dem Vorteil, daß auch wirklich alle Test-Stellen gelöscht werden.
Wollen Sie Befehlsfolgen beim Testen durch andere ersetzen, so ist auch dies möglich:
#ifdef testfall
... ; Ersatz-Befehle zum Testen
#else
... ; Original-Befehle
#endif

... und jetzt kommt die Kür ... :

Die folgenden Befehle hat der Autor in keiner Beschreibung gefunden, sondern selber ausprobiert:
#if testfall == 1
#if testfall > 1
Hier wird 'testfall' auf ganz bestimmte Werte abgefragt.

Und es geht noch schöner:

Es gibt eine Antwort auf die Ur-Frage 'wo bin ich?'. Dabei wird der Wert eines Labels abgefragt. Nehmen wir an, das Label 'xxx' ist die Ansprungadresse eines Unterprogramms und steht (rein zufällig) auf der Adresse 0x100; dann können Sie abfragen:
#if xxx == 0x100
und dann davon abhängig Befehle einfügen oder weglassen. (Merke: wir sind hier nicht mehr beim Testen!) Dies ist durchaus sinnvoll, wenn in einem Adreß-Bereich > 0x7f berechnete Sprünge ausgeführt werden sollen. Hier müssen, abhängig vom Programmzähler, von Hand Adreßerweiterungsbits gesetzt werden, ansonsten springt der Rechner 'in den Wald'. S. dazu auch die sehr gute Dokumentation von sprut.

Für weitere Fragen stehen gern zur Verfügung:
- der MEC; Besichtigung und Fachsimpelei z.B. an unseren "Club-Abenden"
- der Autor: Hans Peter Kastner

Version vom: 19.04.2025; erstellt am: 26.03.2010
Copyright © 2010 - 2025 by Modelleisenbahnclub Castrop-Rauxel 1987 e.V.

Valid HTML 4.01!