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