Direttive del preprocessore.
Preprocessore.
Il processo di compilazione del firmware è molto difficile e si svolge in più fasi, una delle prime è il lavoro del preprocessore. Al preprocessore possono essere dati comandi che eseguirà prima di compilare il codice del firmware: questo può essere connessione di file, sostituzione di testo, costruzioni condizionali e altre cose. Il preprocessore dispone anche di macro che consentono di aggiungere alcune cose interessanti al codice.
#include - include un file.
Abbiamo già familiarità con i file di inclusione: la direttiva #include collega un nuovo documento a quello corrente, ad esempio una libreria. Dopo #include è necessario specificare il nome del file incluso. È possibile specificare tra "virgolette" o tra <parentesi angolari>. Qual è la differenza? Il compilatore cercherà un file il cui nome è tra virgolette nella cartella con il documento principale, se non lo trova, cercherà nella cartella con le librerie. Se specificato tra parentesi, cercherà immediatamente nella cartella con le librerie, il cui percorso è solitamente configurabile.
#include "mylib.h" // connetti mylib.h, prima guarda nella cartella con lo schizzo.
#include <mylib.h> // connetti mylib.h dalla cartella delle librerie.
Puoi anche specificare il percorso del file da includere. Ad esempio, abbiamo una cartella libs nella cartella sketch e il file mylib.h in essa. Per includere un tale file, scriviamo:
#include "libs/mylib.h"
Il compilatore lo cercherà nella cartella sketch, nella sottocartella libs.
#define/undef.
#define è un comando al preprocessore per sostituire un set di caratteri con un altro, ad esempio #define MY_CONST 32 sostituirà tutti i MY_CONST nel codice con il numero 32 durante la compilazione.
Se non scrivi nulla dopo aver specificato il primo set di caratteri, il preprocessore li sostituirà con "niente". Cioè, #define MY_CONST rimuoverà semplicemente tutte le combinazioni di MY_CONST dal codice.
#define permette anche di creare macro funzioni. Ad esempio, usando #define, puoi creare comode costruzioni nello stile di un ciclo eterno.
#define FOREVER for(;;)
......
FOREVER
{
// il codice gira
}
O un modo rapido e conveniente per disattivare il debug nel codice:
Quando si sviluppa un progetto il debug è importante, lo facciamo usando Serial.println(). Per non rimuovere tutte le chiamate a Seriale dal codice dopo la fine dello sviluppo e per non caricare il codice con costruzioni condizionali #ifdef DEBUG.... #endif, puoi farle cosi:
#ifdef DEBUG_ENABLE
#define DEBUG(x) Serial.println(x)
#else
#define DEBUG(x)
#endif
Se DEBUG_ENABLE è definito: tutte le chiamate DEBUG() nel codice verranno sostituite con Serial.println (x). Se non definiti verranno semplicemente "tagliati" dal codice. Inoltre, usando DEBUG_ENABLE, puoi avviare Serial e avere il pieno controllo sul debug, se non hai bisogno, rimuovere DEBUG_ENABLE: Serial.begin(speed);
e Serial.println (x) verranno
rimosse dal codice. Inoltre, questo riduce drasticamente la quantità di memoria occupata.
Esempio:
// definire o non definito per l'uso
//#define DEBUG_ENABLE
#ifdef DEBUG_ENABLE
#define DEBUG(x) Serial.println(x)
#else
#define DEBUG(x)
#endif
void setup()
{
#ifdef DEBUG_ENABLE
Serial.begin(9600);
#endif
}
void loop()
{
DEBUG("Electron32");
delay(100);
}
I problemi.
Qual è il pericolo di #define?
- Si applica a tutti i documenti inclusi nel codice dopo di esso. Diamo un'occhiata più da vicino: se dichiari #define prima di collegare un file, questo verrà esteso a questo file e sostituirà il testo specificato.
- Se qualcosa nel file incluso (nomi di funzioni e variabili) corrisponde a quello predefinito, si verificherà un errore di compilazione.
- Ma se il file incluso ha la sua #define con lo stesso nome, allora funzionerà la #define del file incluso.
Come risolvere questo problema? Ad esempio, vogliamo controllare la compilazione di una libreria utilizzando #define che non si trovano nel file di intestazione della libreria (poiché è possibile dal file di intestazione, questo è già chiaro). Ci sono due opzioni facili:
- Posizionare il codice esecutivo della libreria nel file di intestazione .h (non creare affatto .cpp), quindi la definizione dallo sketch può influenzare la compilazione del codice esecutivo.
- Creare un file di intestazione separato nella cartella con la libreria, ad esempio config.h, raccogliere le necessarie definizioni di "impostazioni" in esso e includere questo file in tutti i file di libreria. In questo caso, il file di libreria .cpp sarà in grado di raccogliere la definizione desiderata.
Un punto importante: il nostro schizzo nell'IDE di Arduino è essenzialmente un file .cpp e #define da esso può estendersi solo ai file di intestazione .h! Cioè, nel file .h della libreria dei plug-in, il define sarà "visibile", ma in .cpp non lo sarà più.
Quello che voglio dire alla fine: #define è uno strumento molto più potente di quanto potrebbe sembrare a prima vista. L'uso di define con una denominazione incauta può portare a un bug difficile da rilevare. Questa è un'arma a doppio taglio: da un lato, voglio usare #define nella mia libreria in modo che nessun altro possa accidentalmente "strisciare" con i loro #define. Allo stesso tempo, la tua libreria potrebbe iniziare a entrare in conflitto con altre librerie.
Qual è la via d'uscita? Molto semplice! Rendi i nomi definiti il più univoci possibile: se è una libreria, lascia il prefisso della libreria (ad esempio, la libreria ATCommand.h, definisci i prefissi ATC_MY_CONST), e se è uno sketch, anteponigli il nome dello sketch. Puoi anche abbandonare define a favore di costanti o enum, a proposito, enum è più conveniente da definire in termini di creazione di un insieme di costanti e occupa pochissimo spazio.
#if - compilazione condizionale.
La compilazione condizionale è uno strumento molto potente con il quale puoi interferire con la compilazione del codice e renderlo molto versatile. Considera le direttive di compilazione condizionale:
#if - analogo di if in un costrutto logico
#elif - analogo di else if in un costrutto logico
#elif - analogo di else if in un costrutto logico
#else - analogo di else in una costruzione logica
#endif - una direttiva che termina un costrutto condizionale
#ifdef - se "definito"
#ifndef - se "non definito"
defined
- questo operatore restituisce true se la parola specificata è "definita" tramite #define e false in caso contrario.
Usati per i costrutti di compilazione condizionale.
Esempio:
#define TEST 1 // definisce TEST come 1
#if (TEST == 1) // se TEST 1
#define VALUE 10 // definisce VALUE come 10
#elif (TEST
== 0) // TEST
0
#define VALUE 20 // definisce VALUE come 20
#else // in caso contrario
#define VALORE 30 // definisce VALORE come 30
#endif // fine della condizione
Pertanto, abbiamo ottenuto la costante VALUE definita, che dipende dal "impostazione" TEST.
Con la compilazione condizionale, puoi letteralmente attivare e disattivare intere parti del codice dalla compilazione, cioè dalla versione finale del programma che verrà caricato nel microcontrollore. Diamo un'occhiata ad alcuni esempi di costruzioni:
Esempio1:
#define USE_DISPLAY 1 // impostazione per l'utente
#if (USE_DISPLAY == 1)
#include <libreria di display.h>
#endif
void setup()
{
#if (USE_DISPLAY == 1)
// inizializzazione del display
#endif
}
void loop()
{
}
Esempio2:
#define SENSOR_TYPE 3 // impostazione per l'utente.
// collegamento della libreria selezionata
#if (SENSOR_TYPE == 1 || SENSOR_TYPE == 2)
#include <libreria di sensori 1.h>
#include <libreria di sensori 2.h>
#elif (SENSOR_TYPE == 3)
#include <libreria di sensori 3.h>
#else
#include <libreria di sensori 4.h>
#endif
Esempio3:
#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)
// codice per ATmega1280 e ATmega2560
#elif defined(__AVR_ATmega32U4__)
// codice per ATmega32U4
#elif defined(__AVR_ATmega1284__)
// codice per ATmega1284
#else
// codice per altri MCU
#endif
Messaggi dal compilatore.
Per visualizzare un messaggio, puoi usare la direttiva #pragma message.

C'è anche la direttiva #error, genera anche testo, ma provoca un errore di compilazione:

pragma message ed error possono essere invocati utilizzando la compilazione condizionale discussa nel capitolo precedente.
#pragma
#pragma è un'intera classe di direttive con capacità diverse. Sopra abbiamo già considerato il #pragma message, qui ne considereremo altri.
#pragma once
Indica al compilatore di includere questo file solo una volta. È un sostituto più conveniente e moderno.
#ifndef _MY_LIB
#define _MY_LIB
// codice
#endif
Puoi trovare una tale costruzione nel 99% delle librerie, nei file core e in generale nelle intestazioni con codice.
#pragma pack/pop
La costruzione con #pragma pack e #pragma pop permette di allocare più razionalmente le strutture in memoria. L'argomento è complesso, leggi qui.
Macro.
Il preprocessore ha alcune macro interessanti che puoi usare nel tuo codice. Diamo un'occhiata ad alcuni utili che funzionano su Arduino (più precisamente, sul compilatore avr-gcc).
__func__ e __FUNCTION__
Le macro __func__ e __FUNCTION__ "restituiscono" sotto forma di array di caratteri (stringa) il nome della funzione all'interno della quale sono chiamate. Sono analoghi tra loro. Per esempio:
void myFunc()
{
Serial.println(__func__); // visualizza myFunc
}
__DATE__ e __TIME__
__DATE__ restituisce la data di compilazione in base all'ora di sistema come array di caratteri (stringa) nel formato <prime tre lettere del mese> <numero> <anno>.
__TIME__ restituisce il tempo di compilazione in base all'ora di sistema come array di caratteri (stringa) nel formato HH:MM:SS.
Serial.println(__DATE__); // Aug 19 2022
Serial.println(__TIME__); // 19:00:00
__FILE__ и __BASE_FILE__
__FILE__ e __BASE_FILE__ restituiscono il percorso completo al file corrente, sempre come stringa. Sono analoghi tra loro.
Serial.println(__FILE__);
// visualizza
percorso completo al file corrente.
__LINE__
__LINE__ restituisce il numero di riga nel documento su cui viene chiamata questa macro.
__COUNTER__
__COUNTER__ restituisce un valore che inizia da 0. Il valore di __COUNTER__ viene incrementato di uno ad ogni chiamata di macro nel codice.
int val = __COUNTER__;
void setup()
{
Serial.begin(9600);
Serial.println(__COUNTER__); // 1
Serial.println(val); // 0
Serial.println(__COUNTER__); // 2
}
void loop()
{
}
__COUNTER__ può essere utilizzato per generare nomi di variabili univoci.