El Preprocesador en Arduino.

El proceso de compilaci贸n del firmware en arduino es muy complicado y tiene varias etapas, una de las primeras es el trabajo del preprocesador. El preprocesador puede recibir comandos que ejecutar谩 antes de compilar el c贸digo de firmware: puede conectar archivos, reemplazar texto, condicionales y algunas otras cosas. El preprocesador tambi茅n tiene macros que le permiten agregar algunas cosas interesantes a su c贸digo.


#include – incluir archivo.

Ya estamos familiarizados con la conexi贸n de archivos: la directiva #include conecta un nuevo documento al actual, como una biblioteca. Despu茅s de #include debe especificar el nombre del archivo que est谩 conectado. Puede especificar en禄doble comillas禄, pero puedes usar < corchetes angulares >… 驴Cu谩l es la diferencia? El compilador en arduino buscar谩 un archivo cuyo nombre est茅 especificado entre comillas dobles en la carpeta con el documento principal, si no lo encuentra, buscar谩 en la carpeta con bibliotecas. Si lo especifica entre corchetes, buscar谩 inmediatamente en la carpeta con las bibliotecas, cuya ruta generalmente se puede configurar.

#include "mylib.h" // incluya mylib.h, primero busque en la carpeta con el boceto
#include <mylib.h> // incluye mylib.h de la carpeta de la biblioteca

Tambi茅n puede especificar la ruta al archivo que se conectar谩. Por ejemplo, en nuestra carpeta de bocetos hay una carpeta libs y en ella hay un archivo mylib.h. Para conectar un archivo de este tipo, escriba:

#include "libs / mylib.h"

El compilador lo buscar谩 en la carpeta de bocetos, en la subcarpeta libs.


#define / undef.

Ya nos hemos encontrado #define en las lecciones anteriores, ahora quiero hablar de algunos casos especiales. D茅jame recordarte #define es un comando para el preprocesador de arduino para reemplazar una orden de caracteres con otra, por ejemplo #define MOTOR_SPEED 50 reemplazar谩 todo lo que se encuentre en el c贸digo como MOTOR_SPEED con el d铆gito 50 al compilar. Si no escribe nada despu茅s de especificar el primer conjunto de caracteres, el preprocesador los reemplazar谩 con 芦nada禄. Es decir #define MOTOR_SPEED simplemente eliminar谩 todas las combinaciones del c贸digo MOTOR_SPEED.

tambi茅n #define le permite crear funciones macro, de esto hablamos en la lecci贸n sobre funciones. Por ejemplo, con la ayuda de define, puede crear construcciones al estilo de un bucle eterno.

#define FOREVER for(;;)
......
FOREVER {
  // el c贸digo gira y gira
}

O para deshabilitar r谩pida y eficazmente la depuraci贸n en el c贸digo:

#ifdef DEBUG
#define DEBUG_PRINT(x) Serial.println(x)
#else
#define DEBUG_PRINT(x)
#endif

O incluso defina un fragmento de c贸digo completo con guiones y barras diagonales inversas.

#define printWords(digit)     \
Serial.print("Digit is ");  \
Serial.print(digit);     
//en la 煤ltima l铆nea \ no es necesario

Si DEBUG esta definido, entonces DEBUG_PRINT Es una funci贸n macro que env铆a un valor a un puerto. Y si no se incumple, DEBUG no esta definido, DEBUG_PRINT simplemente se eliminan del c贸digo y ahorran memoria.

La depuraci贸n es importante a la hora de desarrollar un proyecto en arduino, lo hacemos por medios serial.println (). Para no eliminar todas las llamadas Seriales del c贸digo despu茅s del final del desarrollo y no cargar el c贸digo con construcciones condicionales #ifdef DEBUG鈥. #endif, puedes hacer esto:

#ifdef DEBUG_ENABLE
#define DEBUG(x) Serial.println(x)
#else
#define DEBUG(x)
#endif

Si DEBUG_ENABLE esta definido- todas las llamadas DEBUG() en el c贸digo se reemplazar谩 con la salida al puerto. Si no est谩 definido, ser谩n reemplazadas por NADA, es decir, simplemente ser谩n 芦cortados禄 del c贸digo. Tambi茅n con DEBUG_ENABLE puede obtener un control total sobre la depuraci贸n: si no la necesita, se elimina DEBUG_ENABLE y la salida al puerto serie y todas las inclusiones se eliminar谩n del c贸digo, lo que reduce dr谩sticamente la cantidad de memoria ocupada:

// definir o no-definir para su uso
// # definir 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("kek");
  delay(100);
}

#undef

Tambi茅n hay una directiva #undef que cancela #define, puede ser 煤til en algunos casos.

Problemas

Cual es el peligro de #define? Se aplica a todos los documentos que se incluyen en el c贸digo posterior, y tambi茅n funcionan en el c贸digo, siendo descrito en otro documento. Miremos m谩s de cerca:

  • Si ANTES de enlazar el archivo declara #define, entonces esta definici贸n se aplicar谩 a este archivo y reemplazar谩 el texto especificado.
  • Si algo en el archivo incluido (nombres de funciones y variables) coincide con su definici贸n, habr谩 un error de compilaci贸n. Por ejemplo, la biblioteca FastLED tiene un colorDarkMagenta, dentro de la biblioteca los colores se declaran como enumeraci贸n. Si defino un nombre como este, aparece un error:
  • Pero, si el archivo incluido tiene su propio #define con el mismo nombre, funcionar谩 #define en el 隆archivo!
  • Un punto importante: nuestro boceto en el IDE de Arduino es esencialmente un archivo .cpp , y un #define en 茅l se puede propagar a archivos de encabezado .h ! Es decir, en el archivo .h de la biblioteca incluida, la definici贸n ser谩 芦visible禄 y activa, pero en .cpp no.

驴C贸mo resolver este problema? Por ejemplo, queremos controlar la compilaci贸n de una biblioteca usando #define que no se encuentren en el archivo de encabezado de la biblioteca (porque es posible desde el archivo de encabezado, esto es comprensible). Hay dos opciones en arduino relativamente simples:

  • Coloque el c贸digo ejecutable de la biblioteca en el archivo de encabezado .h (no cree un .cpp en absoluto), entonces ser谩 posible influir en la compilaci贸n del c贸digo ejecutable defini茅ndolo desde el boceto
  • Cree un archivo de encabezado separado en la carpeta de la biblioteca , por ejemplo config.h, para recopilar las definiciones de 芦configuraci贸n禄 necesarias, e incluya este archivo en todos los archivos de la biblioteca, en este caso, el archivo de la biblioteca .cpp podr谩 recoger la definici贸n requerida. Esto se hace, por ejemplo, en la biblioteca FastLED.

Las dificultades no terminan ah铆:  Un #define de una biblioteca puede arrastrarse a otra biblioteca, que est谩 conectada despu茅s de la primera. Volvamos al mismo ejemplo con DarkMagenta– si defino esta palabra en mi biblioteca y conecto la biblioteca antes de que FastLED est茅 conectado, obtendr茅 un error de compilaci贸n. Si cambia la conexi贸n, no habr谩 ning煤n error. Pero, si quiero usar DarkMagenta en mi boceto, me sorprender茅 desagradablemente =)

Lo que quiero decir al final: #define Es una herramienta de arduino mucho m谩s poderosa de lo que parece a primera vista. El uso de define con nombres desatentos puede llevar a un error que puede ser dif铆cil de detectar. Esta es una espada de doble filo: por un lado, desea usar define en su biblioteca para que nadie m谩s rastree accidentalmente sus definiciones. Al mismo tiempo, su propia biblioteca puede comenzar a entrar en conflicto con otras bibliotecas. 驴Cual es la soluci贸n? 隆Muy simple! Haga que los nombres de las definiciones sean lo m谩s 煤nicos posible: si se trata de una biblioteca, deje el prefijo de biblioteca; si es un boceto, anteponga el nombre del boceto. Tambi茅n puede abandonar define en favor de constantes o enum , por cierto, enum es m谩s conveniente que definir en t茅rminos de crear un conjunto de constantes, 隆y ocupa muy poco espacio!


#if – compilaci贸n condicional.

  • #if – Si.
  • #ifdef – si est谩 definido.
  • #ifndef – si no se especifica.
  • #else- de lo contrario.
  • #elif – de lo contrario si.
  • #endif – fin de condici贸n.
  • defined- comprobando si.

Con la ayuda de la compilaci贸n condicional, literalmente puede activar y desactivar partes enteras del c贸digo de la compilaci贸n, es decir, de la versi贸n final del programa que se cargar谩 en el microcontrolador arduino. Consideremos varias construcciones, por ejemplo:

Compilaci贸n condicional, ejemplo 1

#define USE_DISPLAY 1 //configuraci贸n para el usuario
#if (USE_DISPLAY == 1)
#include <biblio_display.h>
#endif
void setup() {
#if (USE_DISPLAY == 1)
  // display.initialization
#endif
}
void loop() {
}

Compilaci贸n condicional, ejemplo 2

#define SENSOR_TYPE 3   // configuraci贸n para el usuario
// conecta la biblioteca seleccionada
#if (SENSOR_TYPE == 1 || SENSOR_TYPE == 2)
#include <biblioteca del sensor 1 y 2.h>
#elif (SENSOR_TYPE == 3)
#include <biblioteca de sensores 3.h>
#else <biblioteca de sensores 4.h>
#endif

Compilaci贸n condicional, ejemplo 3

#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)
// c贸digo para ATmega1280 y ATmega2560
#elif defined(__AVR_ATmega32U4__)
// c贸digo para ATmega32U4
#elif defined(__AVR_ATmega1284__)
// c贸digo para ATmega1284
#else
//c贸digo para el resto de Mc
#endif

Mensajes del compilador.

Para mostrar un mensaje, puede utilizar la directiva #pragma – mensaje. Tambi茅n hay una directiva #error, tambi茅n genera texto, pero genera un error de compilaci贸n. Tanto el mensaje pragma  como el error se pueden llamar mediante la compilaci贸n condicional, que se analiz贸 en el cap铆tulo anterior.

#pragma

#pragma es toda una clase de directivas con diferentes capacidades. Ya lo hemos considerado arriba #pragma mensaje, aqu铆 hay algunos m谩s.

#pragma once

Es una directiva de preprocesador no est谩ndar pero ampliamente admitida dise帽ada para hacer que el archivo actual se incluya solo una vez en esta compilaci贸n.

#pragma once

struct  foo  
{ 
    int  miembro ; 
};

Esta directiva se debe incluir en el encabezado del archivo el cual queremos que se incluya solo una vez en el proyecto.

Puede encontrar una construcci贸n de este tipo en el 99% de las bibliotecas, archivos del n煤cleo y, en general, encabezados con c贸digo.

#pragma pack/pop

Diferentes sistemas tienen diferentes modos de alinear los datos en la memoria. Sistemas de 8 bits alinean su memoria de byte en byte, los sistemas de 16 bits la alinean en palabras asea 2 bytes. Por ultimo en los sistemas de 32 bits los accesos a memoria se realizan en grupos de 4 bytes. En estos sistemas al definir estructuras y otros tipos de datos, cada dato en memoria ocupa 4 bytes, da lo mismo que sea de tipo byte, int o char. Esto hace que se desperdicie memoria innecesariamente.

La directiva #pragma pack(n) cambia la alineaci贸n de la memoria a lo especificado en (n).

#pragma pack(1) // cambia alineacion
struct MyStruct
{
  char b; 
  int a; 
  int array[2];
};
#pragma pack(pop) //retorna a la alineacion por defecto 

Despues de cambiar la alineaci贸n con #pragma pack se debe restablecer la alineaci贸n original con #pragma pack(pop), o el sistema puede colapsar.

Construcci贸n con #pragma pack y #pragma pop permite una asignaci贸n m谩s racional de estructuras en la memoria de arduino. El tema es complejo y animo a profundizar en 茅l por ejemplo aqu铆.


Macros.

El preprocesador tiene algunas macros interesantes que puede usar en su c贸digo. Consideremos algunas 煤tiles que funcionan en Arduino (m谩s precisamente, en el compilador avr-gcc). Estas macros se usan mucho durante la depuraci贸n del c贸digo. Tambi茅n se suelen incluir en el interior de funciones.

__func__ y __FUNCTION__

Las macros __func__ y __FUNCI脫N__  son an谩logas entre s铆. 芦Devuelven禄 como una matriz de caracteres (cadena) el nombre de la funci贸n dentro de la cual son llamadas.Por ejemplo:

void myFuncion() {
  Serial.println(__func__); //// imprime "myFuncion"
}

__DATE__ y __TIME__

__DATE__ devuelve la fecha de compilaci贸n en la hora del sistema como una matriz de caracteres (char) en el formato <primeras tres letras del mes> <n煤mero> <a帽o>

__TIME__ devuelve la hora de compilaci贸n en la hora del sistema como una matriz de caracteres (char) en el formato HH: MM: SS

Serial.println(__DATE__); // Feb 27 2020
Serial.println(__TIME__); // 14:32:18

Es muy 煤til trabajar directamente con esta macro.

__FILE__ y __BASE_FILE__

__FILE__ y __BASE_FILE__ Son an谩logos entre s铆, devuelve la ruta completa al archivo actual, nuevamente como una cadena.

聽Serial.println(__FILE__);
// salida C:\Users\raul\Desktop\sketch_feb27a\sketch_feb27a.ino

__LINE__

__LINE__ devuelve el n煤mero de l铆nea del documento en el que se llama a esta macro.

__COUNTER__

__COUNTER__ devuelve un valor que comienza en 0. El valor __COUNTER__ se incrementa en uno con cada llamada de la macro en el c贸digo.

int val = __COUNTER__;
void setup() {
  Serial.begin(9600);  
  Serial.println(__COUNTER__);  // 1
  Serial.println(val);          // 0
  Serial.println(__COUNTER__);  // 2
}
void loop() {}

Deja un comentario