Über den Beitrag
Ihr alle habt schon Präprozessor-Direktiven wie #define
oder #include
verwendet. Die weniger Erfahrenen kennen aber vielleicht nicht wirklich den Unterschied zwischen Präprozessor-Direktiven und dem eigentlichen Programmcode. Diesen Unterschied, welche Präprozessor-Direktiven zur Verfügung stehen, wie ihr sie anwendet und welche Alternativen es gibt, soll dieser Beitrag vermitteln. Und vielleicht gibt es ja den einen oder anderen Punkt, der auch für die „älteren Hasen“ interessant ist. Folgendes behandele ich:
- Was ist der Präprozessor, was sind Präprozessor-Direktiven?
- Dateien einbinden – #include
- Makros – #define
- Bedingtes Einbinden von Code – #ifdef, #ifndef & Co
- Warnungen und Fehler – #warning, #error
- Zeilenzähler und Dateiname manipulieren – #line
- Das Multifunktions-Tool – #pragma
Was ist der Präprozessor, was sind Präprozessor-Direktiven?
Der Präprozessor ist ein Programm oder eine Komponente, die vor der eigentlichen Übersetzung oder Ausführung eines Codes eingesetzt wird. Er wird häufig in Programmiersprachen wie C und C++ (also auch Arduino-Code) verwendet, um Quellcode vor der Kompilierung zu bearbeiten. Im Wesentlichen erledigt der Präprozessor folgende Aufgaben:
- Ersetzen von Zeichenfolgen durch Makros.
- Einbinden von Dateien.
- Bedingtes In- bzw. Exkludieren von Code.
- Steuerung des Compilerverhaltens (Meldungen, Abbruch, Warnungen).
Präprozessor-Direktiven sind die Funktionen des Präprozessors. Sie sind leicht identifizierbar, da sie mit dem Hashzeichen # beginnen. Im Gegensatz zu Anweisungen des eigentlichen Programmcodes werden sie nicht mit einem Semikolon, sondern dem Zeilenende abgeschlossen.
Dateien einbinden – #include
Wie ihr wisst, bindet ihr Dateien wie etwa Bibliotheksdateien über #include
ein. Dabei gibt es die Schreibweise #include <Dateiname/Pfad>
und #include "Dateiname/Pfad"
:
#include <...>
: Der Compiler sucht die Datei in den Standard-Include-Pfaden. Diese Pfade führen meist zu Ordnern, die „libraries“ oder „include“ heißen.#include "..."
: Der Compiler sucht die Datei zunächst im aktuellen Verzeichnis. Wenn er sie dort nicht findet, setzt er die Suche in den Standard-Include-Pfaden fort.
Wenn ihr beispielsweise für euer Projekt eine Datei namens config.h geschrieben habt, dann ist die Wahrscheinlichkeit ziemlich hoch, dass es eine Datei mit demselben Namen irgendwo schon gibt. Dann könntet ihr das Einbinden einer falschen Datei vermeiden, indem ihr eure config.h in dem Ordner eurer „.ino“-Datei speichert und über #include "..."
einbindet.
Makros – #define
Einfache Makros ohne Variablenübergabe
Um ein Makro zu erstellen, nutzt ihr #define. In der einfachsten Form sieht das so aus:
#define identifier replacement
z. B.:
#define LED_PIN 10
Mit einfachen Makros meine ich Makros ohne weitere Variablenübergabe. In diesem Fall macht #define
im Prinzip dasselbe wie die Suchen-Ersetzen-Funktion eines Textverarbeitungsprogramms. Der Identifier (offiziell: der Makroname) LED_PIN
wird überall in eurem Programm durch 10 ersetzt. Dem kompilierten Programm ist nicht mehr anzusehen, ob im Quellcode die 10 oder der Identifier an den entsprechenden Stellen stand.
Zur „Rechtschreibung“: es ist allgemein üblich, die Makronamen in Großbuchstaben zu schreiben. Es funktioniert natürlich auch mit kleinen Buchstaben, nur tut ihr euch selbst und anderen einen Gefallen, wenn ihr euch an solche Konventionen haltet.
In den Tiefen der zum Arduinopaket gehörenden Bibliotheken werdet ihr viele Makronamen sehen, die mit einem oder zwei Unterstrichen beginnen und manchmal auch enden, wie „__AVR_ATmega328P__“, „_VECTORS_SIZE“ oder „__PACKED_STRUCT“. Solche Makros haben meistens eine besondere Bedeutung. Die Unterstriche stellen einen gewissen Schutz dar – solange ihr selbst darauf verzichtet.
Stolperfallen bei einfachen Makros
Das „replacement“, also der „Makro-Wert“ erhält seinen Datentyp erst durch den Kontext im Programmcode. Welchen Datentyp ihr meint und welchen Datentyp der Compiler daraus macht, sind u. U. zwei unterschiedliche Paar Schuhe. Dazu der folgende kleine Sketch:
#define A_DEF 30000 #define B_DEF 30000U /* alternative 1: */ // static const int A_DEF = 30000; // static const unsigned int B_DEF = 30000; /* alternative 2: */ // static constexpr int A_DEF = 30000; // static constexpr unsigned int B_DEF = 30000; void setup() { Serial.begin(115200); // delay(2000); Serial.print("A_DEF + 10000: "); Serial.println(A_DEF + 10000); Serial.print("B_DEF + 10000: "); Serial.println(B_DEF + 10000); } void loop() {}
Und hier die Ausgabe auf einem ATmega328P-basierten Board:

A_DEF
wird offensichtlich als Signed Integer interpretiert. Da ich ein Board gewählt habe, auf dem ein Integer eine Größe von 2 Byte hat, gibt es einen Überlauf und ein entsprechend negatives Ergebnis. Zwar warnt der Compiler, aber ist leicht zu übersehen. Abhilfe schafft der numerische Literaloperator „U“ bei der Definition des Makros B_DEF
, da er dem Compiler mitteilt, dass der Wert „unsigned“ sein soll. Alternativ könnt ihr „L“ für long, „UL“ für unsigned long, F für Float oder D für double anhängen. Kleinschreibung geht auch.
Eine andere, offensichtliche Stolperfalle ist eine Namenskollision. Zwar warnt auch hier der Compiler vor einer „Redefinition“, aber die Kollision führt nicht zum Abbruch.
Alternativen zu #define (const/constexpr, enum)
const und constexpr
Gerade bei größeren Programmen ist die Verwendung von Konstanten (const
) bzw. und konstanten Ausdrücken (constexpr
) den einfachen #define
-Direktiven vorzuziehen. Ihr drückt viel eindeutiger aus, was die Werte bedeuten.
Zum Unterschied const
vs. constexpr
: Einer Konstanten const
darf ihr Wert während der Laufzeit zugewiesen werden, bei einem konstanten Ausdruck constexpr
passiert das zwingendermaßen während des Kompilierens.
Der Zusatz static
sorgt zum einen dafür, dass eine Konstante nur einmal initialisiert wird, zum anderen kann static
die Sichtbarkeit der Konstante in anderen Dateien verhindern. Die genaue Wirkung von static
hängt davon ab, wo genau die Konstante definiert worden ist (z. B. innerhalb oder außerhalb von Funktionen und Klassen). Das führt hier aber zu weit.
enum
Eine Enumeration enum
verwendet ihr, wenn ihr Konstanten habt, die eine Gruppe bilden, wie etwa hier die Grundfarben:
enum COLOR { RED = 0xFF000, // RGB-value GREEN = 0x00FF00, BLUE = 0x0000FF }; void setup() { Serial.begin(115200); COLOR myFavoriteColor = GREEN; if (myFavoriteColor == GREEN){ Serial.println("My favorite color is green"); } } void loop () {}
Oft geht es dabei gar nicht darum, den enum
-Elementen bestimmte Werte zuzuordnen, sondern einfach nur einen Bezeichner zu haben, z. B.:
enum DAY_OF_THE_WEEK { MON, TUE, WED, THU, FRI, SAT, SUN };
Makros mit Variablenübergabe
Die #define
-Makros können auch Variablen entgegennehmen. Das macht sie immer noch zu einem Suchen-Ersetzen-Tool, aber in einer komplexeren Ausführung. Hier ein paar Beispiele, die zugleich ein paar Stolperfallen aufzeigen:
#define A_PLUS_B(a,b) a+b #define A_PLUS_B_BETTER(a,b) (a+b) #define SQUARE(a) a*a #define SQUARE_BETTER(a) (a)*(a) #define MAP_RANGE(a) map(a, 0, 100, 0, -500) #define MAKE_STRING(a) #a #define MERGE(a,b) a##b void setup() { Serial.begin(115200); // delay(2000); Serial.print("3 * A_PLUS_B(1,2): "); Serial.println(3 * A_PLUS_B(1,2)); Serial.print("3 * A_PLUS_B_BETTER(1,2): "); Serial.println(3 * A_PLUS_B_BETTER(1,2)); Serial.print("SQUARE(1+2): "); Serial.println(SQUARE(1+2)); Serial.print("SQUARE_BETTER(1+2): "); Serial.println(SQUARE_BETTER(1+2)); Serial.print("MAP_RANGE(50): "); Serial.println(MAP_RANGE(50)); Serial.print("MAKE_STRING(ABC123): "); Serial.println(MAKE_STRING(ABC123)); String mergedString(MERGE(1,2)); // = String mergedString(12); Serial.print("MERGE(1,2) as String: "); Serial.println(mergedString); int mergedInt = MERGE(1,2); // = int mergedInt = 12; Serial.print("MERGE(1,2) as Integer: "); Serial.println(mergedInt); } void loop() {}
Hier die Ausgabe:

Und hier ein paar Erläuterungen:
A_PLUS_B(1,2)
wird durch1+2
ersetzt.- Stolperfalle:
3*1+2
ist 5!
- Stolperfalle:
A_PLUS_B_BETTER(1,2)
wird durch(1+2)
ersetzt.- Also:
3*(1+2) = 9
.
- Also:
SQUARE(1+2)
wird durch1+2*1+2
ersetzt → Stolperfalle!SQUARE_BETTER(1+2)
wird durch(1+2)*(1+2)
ersetzt und liefert das wahrscheinlich erwartete Ergebnis.MAP_RANGE(50)
wird durchmap(50, 0, 100, 0, -500);
ersetzt.MAKE_STRING(ABC123)
wird durch die ZeichenketteABC123
ersetzt.
- Das Hashzeichen # vor dem a ist hier ein Operator, der Präprozessor anweist, a durch eine Zeichenkette zu ersetzen.
MERGE(1,2)
wird durch12
ersetzt.- Der Operator ## verkettet a und b.
- Die Bedeutung von 12 ergibt sich erst aus dem Kontext (hier: Integer oder String)
Makros vs. Funktionen
Im letzten Beispiel haben wir dem Makro A_PLUS_B
Integerwerte als Parameter übergeben. Das Coole ist, dass das Makro auch mit Float-Variablen oder Strings funktioniert. Und das Ganze in einer einzeiligen Definition! Als Funktion wäre das etwas aufwendiger. Entweder ihr löst das durch Überladung:
void setup() { Serial.begin(115200); int aInt = 1; int bInt = 2; int cInt = aPlusB(aInt, bInt); Serial.print("aPlusB(aInt,bInt) = "); Serial.println(cInt); String aStr = "1"; String bStr = "2"; String cStr = aPlusB(aStr, bStr); Serial.print("aPlusB(aStr,bStr) = "); Serial.println(cStr); } void loop() {} int aPlusB(int a, int b) { return a+b; } String aPlusB(String a, String b) { return a+b; }
… oder ihr arbeitet mit Templates:
void setup() { Serial.begin(115200); int aInt = 1; int bInt = 2; int cInt = aPlusB<int>(aInt, bInt); Serial.print("aPlusB(aInt,bInt) = "); Serial.println(cInt); String aStr = "1"; String bStr = "2"; String cStr = aPlusB<String>(aStr, bStr); Serial.print("aPlusB(aStr, bStr) = "); Serial.println(cStr); } void loop() {} template <typename T> T aPlusB(T a, T b) { return a+b; }
Beide Varianten liefern die folgende Ausgabe:

Überladung und Templates bedeuten mehr Schreibarbeit, aber diese Varianten sind einfach sicherer. Folgendes wird beispielsweise vom Makro A_PLUS_B
(bzw. vom Compiler) klaglos akzeptiert:
String a = "1"; int b = 2; Serial.println(A_PLUS_B(a, b));
Vielleicht wolltet ihr das ja – vielleicht war es aber auch ein Versehen.
„Arduino Makros“
Das Arduino-Programm-Paket verwendet Makros in enormem Umfang. Zum Teil dienen sie dazu, den Programm-Code verständlicher zu gestalten. So bedeuten beispielsweise LOW und INPUT schlicht 0, OUTPUT und HIGH sind 1.
Dann gibt es eine Reihe funktionalen Makros. Ein einfaches Beispiel ist das Makro „min“, das euch den kleineren zweier übergebener Werte liefert:
#define min(a,b) ((a)<(b)?(a):(b))
Zum Teil sind die Makros aber auch recht komplex und greifen auf weitere Makros zurück. Ein Beispiel dafür ist das „F“-Makro, das ihr aus Serial.print(F("......"));
kennt:
#define F(string_literal) (reinterpret_cast<const __FlashStringHelper *>(PSTR(string_literal)))
Es verwendet das PSTR-Makro:
#define PSTR(s) ((const PROGMEM char *)(s))
Vordefinierte Makros
Vordefinierte Makros sind Bestandteil des Präprozessors und nicht über ein #define
definiert. Die gängigsten vordefinierten Makros findet ihr im folgenden Sketch:
void setup() { Serial.begin(115200); Serial.print("Date: "); Serial.println(__DATE__); // Compiling date Serial.print("Time: "); Serial.println(__TIME__); // Compiling time Serial.print("File: "); Serial.println(__FILE__); // Compiled file Serial.print("Line: "); Serial.println(__LINE__); // Current line Serial.print("C++ : "); Serial.println(__cplusplus); // C++ Version (201103 = ISO C++ 2011) } void loop() {}
Hier die Ausgabe:

Bedingtes Einbinden von Code – #ifdef, #ifndef & Co
Ihr könnt #define
nutzen, um Teile des Quellcodes einzubinden oder durch den Compiler ignorieren zu lassen.
#ifdef MACRO_NAME
prüft, ob MACRO_NAME
definiert wurde oder nicht. Ist MACRO_NAME
definiert worden, so werden die darauffolgenden Codezeilen eingebunden. Mit #ifndef
verhält es sich genau umgekehrt.
Im einfachsten Fall steht im weiteren Verlauf des Quellcodes ein #endif
, das die bedingte Einbindung beendet. Mit #else
oder #elif
ist aber auch eine Verzweigung möglich. Also im Prinzip genau, wie ihr es von if
, else
und else if
her kennt. Ebenso sind Verschachtelungen möglich. Das kann aber auch schnell sehr unübersichtlich werden.
Gleichbedeutend mit #ifdef MACRO_NAME
ist #if defined(MACRO_NAME)
. Diese Schreibweise kommt bei logischen Verknüpfungen zum Tragen, wie beispielsweise #if defined(NAME_1) && defined(NAME_2)
. Mit #if
könnt ihr aber auch andere logische Bedingungen prüfen, wie #if A>B
oder #if !(A==B)
.
Am Ende muss jedes #if
, #ifdef
oder #ifndef
durch ein #endif
abgeschlossen sein.
Ein #undef MACRO_NAME
tut das, was man vermuten würde: Es beendet die Definition von MACRO_NAME
.
Hier ein Beispielsketch:
#define YIN #define YANG void setup() { Serial.begin(115200); // delay(2000); #ifdef YIN // or: #if defined(YIN) Serial.println("YIN is defined"); #elif defined(YANG) Serial.println("YIN is not defined, but YANG"); #endif #ifdef YANG Serial.println("YANG is defined"); #else Serial.println("YANG is not defined, YIN might be defined"); #endif #ifndef YANG Serial.println("YANG is not defined"); #endif #if defined(YIN) || defined(YANG) Serial.println("YIN and / or YANG are defined"); #endif #if defined(YIN) && defined(YANG) Serial.println("YIN and YANG are defined"); #elif defined(YIN) Serial.println("Only YIN is defined"); #endif #undef YANG #ifndef YANG Serial.println("YANG is not defined (anymore?)"); #endif } void loop() {}
Der Sketch erzeugt die folgende Ausgabe:

Wenn ihr wollt, könnt ihr nun #define YIN
und / oder #define YANG
auskommentieren und sehen, wie sich die Ausgabe verändert.
Anwendungsbeispiel 1: Vermeidung von doppeltem Einlesen
Als konkretes Beispiel schauen wir uns die SPI-Bibliotheksdatei SPI.h des Arduino Renesas Paketes an (hier findet ihr sie). Viele Bibliotheken von SPI-basierten Bauteilen binden SPI.h automatisch ein. Verwendet ihr mehrere Bauteile, dann könnte SPI.h mehrfach eingelesen werden. Das wird verhindert, indem der gesamte Code in SPI.h mit der folgenden Konstruktion umfasst wird:
#ifndef _SPI_H_INCLUDED #define _SPI_H_INCLUDED ...... Code ........ #endif
_SPI_H_INCLUDED
kann so nur einmal definiert und der Code nur einmal gelesen werden.
Anwendungsbeispiel 2: Debugging
In der Entwicklungsphase eines Projektes kann es sinnvoll sein, Zwischenwerte, Sensoreinstellungen, Statusdaten oder Ähnliches auf dem seriellen Monitor ausgeben zu lassen. Andererseits sind gerade Serial.print()
-Anweisungen sowohl langsam als auch Speicherfresser. In solchen Fällen empfiehlt es sich, diesen Code mittels bedingter Einbindung „ein“- oder „auszuschalten“. Es gibt viele Bibliotheken, die eine solche Debug-Option anbieten.
Allerdings gibt es auch hier wieder eine Stolperfalle. Schaut euch das folgende schlichte Beispiel an. Der Hauptsketch verwendet die Funktion sumUp()
, die in sum.h und sum.cpp definiert ist. sumUp()
addiert einfach nur drei Zahlen. Wenn DEBUG
definiert ist, sollen die Zwischenschritte ausgegeben werden, ansonsten nur das Endergebnis.
#define DEBUG #include "sum.h" #define SUMMAND_1 17 #define SUMMAND_2 4 #define SUMMAND_3 42 void setup() { Serial.begin(115200); int value = sumUp(SUMMAND_1, SUMMAND_2, SUMMAND_3); Serial.print("Sum: "); Serial.println(value); } void loop() {}
// #define DEBUG // #include "sum_config.h" #include "Arduino.h" #ifndef _SUM_UP #define _SUM_UP int sumUp(int, int, int); #endif
#include "sum.h" int sumUp(int s1, int s2, int s3){ int result = 0; result += s1; #ifdef DEBUG Serial.print("Interim sum 1: "); Serial.println(result); #endif result += s2; #ifdef DEBUG Serial.print("Interim sum 2: "); Serial.println(result); #endif result += s3; return result; }
#ifndef _SUM_CONF #define _SUM_CONF // uncomment the following line to activate DEBUG // #define DEBUG #endif
Wenn ihr den Sketch so wie er ist ausführt, erwartet ihr vielleicht die ausführliche Ausgabe (unten links), da DEBUG
im Hauptsketch definiert ist. Ihr bekommt aber die rechte Ausgabe.

Das Problem ist, dass der Präprozessor sich nicht an die Reihenfolge hält, sondern erst die #include
Direktiven ausführt. Ihr könnt das Problem lösen, indem ihr #define DEBUG
in sumUp.h entkommentiert. Um es dem Nutzer zu erleichtern und das Risiko zu minimieren, dass er aus Versehen etwas Falsches auskommentiert, kommen in solchen Fällen gerne Konfigurationsdateien zum Einsatz. Entkommentiert deshalb alternativ #include "sum_config.h"
in sum.h und #define DEBUG
in sum_config.h.
Anwendungsbeispiel 3: MCU-spezifischer Code
Das Arduino-Ökosystem versucht, Code möglichst universell, d. h. unabhängig vom verwendeten Board bzw. Mikrocontroller (MCU) zu gestalten. Dennoch gibt es doch Dinge, die nicht überall (gleich) funktionieren, wie beispielsweise SoftwareSerial. Als konkretes Beispiel schauen wir uns einen Beispielsketch der DFRobotDFPlayerMini-Bibliothek an (hier geht’s zum vollständigen Sketch).
#if (defined(ARDUINO_AVR_UNO) || defined(ESP8266)) // Using a soft serial port #include <SoftwareSerial.h> SoftwareSerial softSerial(/*rx =*/4, /*tx =*/5); #define FPSerial softSerial #else #define FPSerial Serial1 #endif
Nur wenn das Board ein AVR-basierter Arduino UNO ( z. B. R3) oder ein ESP8266-basiertes Board ist, wird SoftwareSerial eingebunden und das Objekt softSerial
erzeugt, ansonsten wird Serial1
(HardwareSerial) verwendet. softSerial
bzw. Serial1
werden in FPSerial
umbenannt, sodass man im weiteren Verlauf nicht zwischen Hard- und SoftwareSerial unterschieden werden muss.
Das Beispiel zeigt aber auch, dass es Stolpersteine gibt. So funktioniert der Sketch beispielsweise nicht mit einem Arduino Nano (Classic) oder einem Arduino Pro Mini. Sie sind kein Arduino UNO, sie sind nicht ESP8266 basiert, besitzen kein Serial1
und fallen so durch das Raster. Hier hätte ich eher __AVR_ATmega328P__
oder ARDUINO_ARCH_AVR
als ARDUINO_AVR_UNO
gewählt.
Wo finde ich die Board- bzw. MCU-#defines?
Leider gibt es nicht die eine große Tabelle, in der man alle Definitionen für die verschiedenen Architekturen, Boards und Mikrocontroller findet (zumindest habe ich keine gefunden). Man kann sich aber anders helfen:
Stellt das Board eurer Wahl in der Arduino IDE ein und kompiliert einen beliebigen Sketch. Dann klickt ihr auf das Fenster mit den Compilermeldungen und klickt STRG/f. In das Suchfenster tragt ihr „-D“ ein. Damit werden die Stellen markiert, an denen Definitionen stehen (deren Ursprung ist: board.txt und platform.txt). Man muss ein wenig hin- und herscrollen, um die markierten Stellen zu finden.
Für ein WEMOS D1 Mini Board habe ich die Definitionen in der folgenden Zeile gefunden (verkürzte Ausgabe):
C:\Users\Ewald\AppData\Local\Arduino15\packages\esp8266\……..-DESP8266…..-DARDUINO_ESP8266_WEMOS_D1MINI -DARDUINO_ARCH_ESP8266 …..
So sah das auf dem Bildschirm aus:

Was man auf diese Weise nicht findet, sind beispielsweise die Definitionen für die verschiedenen AVR-Mikrocontroller. Da hilft die Datei io.h aus den Compilerdateien weiter. Wo genau die bei euch liegt, hängt von eurer Installation ab. Schaut dort, wo eure „packages“ liegen. Bei mir sieht der Pfad so aus:
C:\Users\Ewald\AppData\Local\Arduino15\packages\arduino\tools\avr-gcc\7.3.0-atmel3.6.1-arduino5\avr\include\avr\io.h
Hier noch eine Zusammenstellung von Definitionen für verschiedene Boards:

Warnungen und Fehler – #warning, #error
Mit der Präprozessor-Direktive #warning
gebt ihr – Überraschung! – eine Warnung aus. Das werdet ihr im Normalfall an eine Bedingung knüpfen. Die Warnung hat keine weiteren Folgen.
Die Präprozessor-Direktive #error
macht dasselbe wie #warning
, führt aber zum Compilerabbruch. Hier ein schlichtes Beispiel:
#define ARRAY_SIZE 10 #define NUMBER_OF_ARRAYS 42 #if ARRAY_SIZE * NUMBER_OF_ARRAYS > 400 #warning You might run out of memory! // #error You will run out of memory! #endif void setup() {} void loop() {}
Das sind die resultierenden Ausgaben:


Zeilenzähler und Dateiname manipulieren – #line
Der Präprozessor „weiß“, in welcher Datei und in welcher Programmzeile er sich gerade befindet. Mit #line line_number "file_name"
könnt ihr beides ändern. Ich weiß allerdings nicht, warum man das tun sollte (außer um andere zu ärgern!).
Wie auch immer, hier ein Beispiel:
void setup() { //#line 33 "blabla.h" int i = 42!; } void loop() {}
Das ist die normale Ausgabe:

Und hier die manipulierte Version:

Das Multifunktions-Tool – #pragma
Die #pragma
-Direktiven sind ausgesprochen vielseitig, aber nicht standardisiert. D. h. jede Compiler-Implementierung kann im Prinzip eigene #pragma
-Direktiven definieren. Das bedeutet zum einen, dass ich keine vollständige Übersicht geben kann, zum anderen bedeutet es, dass ich nicht garantieren kann, dass alle hier vorgestellten Direktiven mit wirklich jedem Compiler kompatibel sind. Mit den gängigen Compilern der Arduino IDE für die gängigen Boards (z.B. AVR, Renesas, ESP32, ESP8266, ARM) sollte es funktionieren.
#pragma once
Ein einfaches #pragma once
am Anfang einer Datei stellt sicher, dass diese nur einmal eingelesen wird. Das ist schon wesentlich einfacher als die herkömmliche Methode, den gesamten Dateiinhalt mit einer #ifndef
– #def
– endif
Konstruktion zu umschließen.
#pragma poison
Mit #pragma poison
könnt ihr Bezeichner, Funktionen oder Zeichenketten als „Gift“ kennzeichnen. Der Compiler bricht ab, wenn er auf diese trifft. Hier ein Beispiel:
#pragma GCC poison blablabla void setup() { Serial.begin(115200); String blablabla = "Nice to see you"; } void loop() {}

Das funktioniert auch mit begin oder „Nice to see you“, nicht aber einfach nur mit Nice.
#pragma message / warning / error
Die Direktive #pragma message "xxxxxxx"
gibt beim Compilieren die Nachricht „xxxxxxx“ aus. Der Compilierungsvorgang wird nicht abgebrochen. Mit #pragma GCC warning
verhält es sich genauso. #pragma GCC error
hingegen gibt einen Fehler aus, und der Compiler bricht die Arbeit ab.
void setup() { Serial.begin(115200); String blabla = "The weather is nice"; Serial.println(blabla); #pragma message "This is blabla" Serial.println(blabla); #pragma GCC warning "Again, this was blabla" Serial.println(blabla); // #pragma GCC error "Refusing to compile this blabla any longer!" } void loop(){}
Die Ausgabe ist:

Wenn ihr nun noch Zeile 9 entkommentiert, bekommt ihr nur die Fehlermeldung:

#pragma diagnostic
Warnungen unterdrücken
Die #pragma GCC diagnostic
-Direktive dient der Steuerung von Warnungen und Fehlermeldungen. Die verschiedenen Optionen werden über weitere Parameter gesteuert. #pragma GCC diagnostic ignored <warning type>
unterdrückt die Warnung des Typs „warning type“.
Als Beispiel komme ich auf das Overflow-Problem zurück, das mit dem folgenden Code auftrat (bei Boards mit einer Integergröße von 2 Bytes):
//#pragma GCC diagnostic ignored "-Woverflow" #define A 30000 #define B 10000 void setup() { Serial.begin(115200); Serial.println(A + B); } void loop(){}
Die folgende Warnung könnt ihr unterdrücken, indem ihr die erste Zeile des Sketches entkommentiert:

Warnungen für einen Bereich unterdrücken / Fehler aus Warnungen machen
Mit #pragma GCC diagnostic error <warning-type>
macht ihr aus einer Compiler-Warnung einen Compiler-Fehler, d.h. der Compiler bricht seine Arbeit ab.
Das schauen wir uns am Beispiel ungenutzter Variablen an. Für diese bekommt ihr eine Warnung, sofern ihr in den Einstellungen der Arduino IDE unter „Compiler-Meldungen“ entweder „Mehr“ oder „Alle“ ausgewählt habt. Mit #pragma GCC diagnostic error "-Wunused-variable"
machen wir im nächsten Sketch daraus einen Fehler. Mit #pragma GCC diagnostic ignore "Wunused-variable"
können wir, wie wir zuvor gesehen haben, die Meldung wiederum unterdrücken.
Die Unterdrückung der Meldung können wir aber auf einen ausgewählten Bereich begrenzen. Dazu dienen #pragma GCC diagnostic push
und #pragma GCC diagnostic pop
. Mit push
merkt sich der Compiler die aktuelle Warnungseinstellung, mit pop
stellt er diesen Zustand wieder her. Klingt vielleicht kompliziert, ist aber einfach:
void setup() { Serial.begin(115200); #pragma GCC diagnostic error "-Wunused-variable" // makes an error out of warning int usedVar = 42; #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-variable" int unusedVar = 43; // error is inored here #pragma GCC diagnostic pop int anotherUnusedVar = 44; // error is not ignored here Serial.println(usedVar); } void loop(){}
Hier die Ausgabe:

Wenn ihr die Zeilen 5 und 8 auskommentiert, bekommt ihr keine Fehlermeldung. Kommentiert ihr die Zeilen 5, 6 und 8 aus, so erhaltet ihr zwei Fehlermeldungen.
#pragma pack()
Der folgende Sketch definiert eine Struktur exampleStruct
, die aus zwei uint8_t- und zwei uint32_t Elementen besteht. Damit sollte die Struktur 10 Byte Speicher beanspruchen. Der Sketch gibt den tatsächlichen Speicherbedarf aus.
//#pragma pack(1) struct exampleStruct{ uint32_t intVal_1; // 4 Byte uint8_t byteVal_1; // + 1 Byte uint32_t intVal_2; // + 4 Byte uint8_t byteVal_2; // + 1 Byte = 10 Byte (in theory!) }; void setup() { Serial.begin(115200); Serial.print("Size of exampleStruct: "); Serial.println(sizeof(exampleStruct)); } void loop(){}
Wenn ihr den Sketch auf einem AVR-basierten Board wie dem Arduino UNO R3 laufen lasst, bekommt ihr die erwarteten 10 Byte. Wechselt ihr nun beispielsweise auf ein ESP32 Board oder einen Arduino UNO R4 (Minima oder WIFI), seid ihr vielleicht erstaunt über die Ausgabe:

Was hier passiert, nennt man Padding (~ auffüllen, polstern). Die größeren 32-Bit-Mikrocontroller arbeiten schneller, wenn die Elemente der Struktur so in Vier-Byte-Blocks gepackt werden, dass nach Möglichkeit kein Element auf zwei oder mehr Blocks verteilt wird. Die Blocks sind sozusagen Schachteln mit vier Fächern, wobei in jedes Fach ein Byte passt. Das Element intVal_1
füllt die erste Schachtel. Für byteVal_1
wird eine neue Schachtel verwendet. Für intVal_2
reichen die freien drei Fächer nicht aus, weshalb eine neue Schachtel verwendet wird. byteVal_2
braucht wieder eine neue Schachtel, gleiches gilt für intVal_2
→ macht vier Schachteln = 16 Fächer = 16 Byte.
Wenn ihr die Elemente anders anordnet, könnt ihr Platz sparen, z.B.: intVal_1
→ byteVal_1
→ byteVal_2
→ intVal_2
. Dann passt byteVal_2
noch in die zweite Schachtel und ihr verbraucht nur drei Schachteln = 12 Byte.
Mit #pragma pack(x)
könnt ihr die Fächerzahl x pro Schachtel festlegen. Dabei muss x = 2n sein, d. h. x = 0, 2, 4, etc. Mit #pragma pack(1)
erreicht ihr die maximale Packungsdichte. Entkommentiert die Zeile 1 im obigen Beispielsketch, um die Größe von exampleStruct
auf 10 Byte zu begrenzen. Wählt ihr #pragma pack(2)
, so ist das Ergebnis 12 Byte. Die Voreinstellung entspricht also #pragma pack(4)
.
Hallo Wolfgang,
herzlichen Dank für Deinen Beitrag. Deine Seite ist inzwischen ein fester Bestandteil bei meinen Spielereien.
Wenn ich so frech sein darf, ich hätte mal einen Wunsch für neue Themen?
1. Könntest Du mal das Thema „Zwei Wire verwenden“ drannehmen? Du verwendest es in Deinen Bibliotheken z.B. ADS115.
Liebe Grüße
Siggi
Hi Siggi,
das ist keinesfalls frech. Ich freue mich über Anregungen! Du meinst mehrere Geräte an einer I2C-Schnittstelle? Viele Arduinos habe ja nur eine I2C Schnittstelle. Oder meinst du mehrere I2C Schnittstellen? Letzteres habe ich mal für den ESP32 besprochen:
https://wolles-elektronikkiste.de/i2c-schnittstellen-des-esp32-nutzen
Mehrere I2C-Geräte an einer Schnittstelle habe ich mal hier besprochen:
https://wolles-elektronikkiste.de/tca9548a-i2c-multiplexer
Aber eben im Zusammenhang mit dem TCA9548A Multiplexer.
Ich denke, ich werde mal was über I2C, SPI, Serial und OneWire machen und dann den Punkt „mehrere Geräte“ mit abdecken.
Also, vielen Dank nochmal!
VG, Wolfgang
„https://wolles-elektronikkiste.de/i2c-schnittstellen-des-esp32-nutzen
Mehrere I2C-Geräte an einer Schnittstelle habe ich mal hier besprochen:
Ich glaube, dass ist genau das was ich gesucht habe.
Sobald ich Zeit habe werde ich es testen und berichte dann darüber.
Liebe Grüße Siggi
Hallo Wolfgang,
wie versprochen kurzer Bericht: 😉
<>
Hier war der Schlüssel versteckt. Herzlichen Dank für Deine Hilfe.
Ich bastel’ gerade mit einem Heltec LoRa V3.
„Wire“ ist hier duch Heltec bereits mit Pin 18,17 für das OLED in der Bibliothek.
Ich konnte nach Deinem Beispiel, ein 2. Wire Objekt erzeugen, ohne Fehlermeldung von Arduino.
#define SDA_2 42
#define SCL_2 41
#define I2C_FREQ 400000
TwoWire I2C_2 = TwoWire(1);
Liebe Grüße
Siggi
Vielleicht verschiebst Du, dass hier, zu Deinem oben genannten Beitrag.
Gut, dass es klappt und danke für die Rückmeldung! Ich schaue mal, wie ich das aufnehme.
Wieso hast Du das gewust?
Ich war gerade dabei, mich in das Thema einzuarbeiten!
Wie immer – Danke für die Mühe, super Artikel!
Liebe Grüße,
Armin
Gedankenübertragung – vielen lieben Dank!
Danke, dein Artikel kommt goldrichtig.
Ich hatte gestern gerade Probleme wegen „static“: wann, wo und warum muss man static einsetzten – et voilà! Deine fundierten und ausgezeichneten Artikel helfen mir, mich mit diesem Thema zu beschäftigen. Vielen Dank.
Andreas
Vielen lieben Dank!