ArduTips

Bienvenidos todos.
Hacía ya tiempo se me pasó por la cabeza realizar una especie de cajón de sastre, eso sí, con un índice, para poner una serie de tips o consejos de programación para arduino. No intenta ser ningún tutorial de aprendizaje, sino ideas y conceptos "desordenados", pero que consideremos que son útiles o buenas prácticas o ideas para quien ya se ha introducido un poco más a fondo en la programación. Por supuesto, todo abierto a la discusión y a la colaboración. Si nos armamos de paciencia, igual hasta somos capaces de construir "la guía definitiva de millis" u otros imposibles ;).
Espero que sea de utilidad.

Índice:

Cuando necesitamos que una determinada variable mantenga su valor entre llamadas, solemos agregar una variable global al principio de nuestro código.

Sin embargo, cuando sólo vamos a utilizar dicha variable dentro de una determinada función, tenemos una opción que hará nuestro código más legible: la podemos declarar dentro de esa función como static.

El código será más claro, pues veremos la declaración en el lugar donde se va a utilizar, y evitaremos que otra función por accidente pueda alterar nuestra variable. Podemos incluso declararla con el mismo nombre en distintas funciones, y cada una mantendrá su valor.

Ejemplo:

void setup()                   
{
  Serial.begin(9600);
}

void loop()
{
contador1();
contador2();
delay(500);
}

void contador1(){
  static byte contador=255;
  Serial.print("Contador 1: ");
  Serial.println(contador --);
}

void contador2(){
  static byte contador=0;
  Serial.print("Contador 2: ");
  Serial.println(contador ++);
}

Vemos dos variables locales contador. Sin embargo, no se interfieren entre ellas. De hecho, podríamos declarar una variable global con el mismo nombre y tampoco interferiría.

Me parece una buena idea, y me gustaria contribuir en la medida en que me sea posible.

Como no conozco muy bien el funcionamiento de los hilos ¿ cual seria el modo de hacer aportes para que los puedas añadir al indice ?

Edito:
ajustar el tamaño de las variables segun el uso

Iremos poniendo, discutiendo y editando en este hilo los tips que se nos vayan ocurriendo. Cuando consideremos que alguno está "maduro", editaré el post de índice para agregar un vínculo al post correspondiente. Aprovechando que tu post, Alfaville, está también en los "albores" del hilo, también puedes editarlo para ir incluyendo vínculos a tips. No sé si se os ocurre alguna forma mejor, pero estoy abierto a sugerencias.

Existe una tendencia generalizada, pero tal vez más acentuada en C, de intentar condensar las expresiones, o incluso varias líneas de código en una.
No me refiero a la "vagancia", que hace que nos olvidemos de comentar nuestro código, o demos nombres poco ilustrativos a las variables o funciones, que "sólo" redundará en que posteriormente ni nosotros mismos entendamos nuestro propio código (mea culpa); sino a cierto tipo de prácticas erróneamente consideradas por algunos como eficientes.

"Espesar" el código fuente no hace que el programa resultante sea más corto ni más rápido, sino en principio sólo más espeso, y en no pocas ocasiónes menos eficiente y desarrollable. Recuerda: ¡Divide y vencerás!
Un ejemplo típico lo selemos tener con los if. ¿Os suenan expresiones de este tipo?

if (boton1==HIGH && boton2==HIGH) {
    Serial.print("ambos botones pulsados"); // Para entrar aquí habremos pasado por dos comparaciones
}

¿Alguien piensa que este otro código equivalente va a resultar en un programa más largo o más lento que el anterior?

if (boton1==HIGH){
    if (boton2==HIGH) {
        Serial.print("ambos botones pulsados"); // Para entrar aquí habremos pasado por dos comparaciones
    }
}

Pues la respuesta es que el código máquina resultante en ambos casos es el mismo, y más semejante a este último. Hasta aquí todo se reduce a una cuestión de legibilidad.
Claro, que luego desarrollamos la primera opción manteniendo la filosofía "compacta" y vemos cosas como esta:

if (boton1==HIGH && boton2==HIGH) {
    Serial.print("ambos botones pulsados"); // para entraraquí se han realizado dos comparaciones
}
else if (boton1==HIGH && boton2==LOW){
    Serial.print("sólo botón 1 pulsado"); // para llegar aquí habríamos realizado cuatro comparaciones
}
// Desde el inicio del código hemos pasado un mínimo de dos comparaciones.

Mientras que desarrollando la segunda, buscando entre qué llaves meter cada comando, nos dará un código más compacto y tal vez un poco más efectivo (ver aclaración):

if (boton1==HIGH){
    if (boton2==HIGH) {
        Serial.print("ambos botones pulsados"); // para llegar aquí se han realizado dos comparaciones
    } else {
        Serial.print("sólo botón 1 pulsado"); // para llegar aquí se han realizado dos comparaciones
    }
}
// Desde el inicio del código hemos pasado un mínimo de una comparación.

No declarar una variable intermedia tampoco significa mayor velocidad ni menor uso de memoria de programa o RAM. Si compilamos este breve código:

void setup(){
 if (digitalRead(10) == HIGH) {
 digitalWrite(13, HIGH);
 }
 else {
 digitalWrite(13, LOW);
 }
}

void loop(){
}

Tras la compilación vemos que el sketch usa 1,494 bytes de programa y 9 bytes de RAM. Exactamente lo mismo que si lo hacemos de esta otra forma:

void setup(){
 pinMode(10, INPUT);
 pinMode(13, OUTPUT);
}

void loop(){
 byte aa = digitalRead(10);
 if (aa == HIGH) {
 digitalWrite(13, HIGH);
 }
 else {
 digitalWrite(13, LOW);
 }
}

Sencillamente, el hecho de no declarar la variable intermedia aa no evita el paso intermedio de almacenar localmente (aunque no le demos nombre a esa variable intermedia) el resultado del digitalRead, antes de compararlo. Caso distinto sería si hubiéramos declarado aa como global, pues entonces sí estaríamos "hipotecando" permanentemente ese espacio de memoria, en lugar de tomarlo temporalmente para el almacenamiento intermedio. Podremos decidir cuál de las dos versiones del código nos parece más legible, pero no pensando en el rendimiento.

Ajustar el tamaño de las variables segun el uso.

Tenemos tendencia a declarar las variables siguiendo ejemplos y hacemos codigos como este:

int pinLed = 5;  // esta declaracion
const int pinLed = 5;  // o esta otra

pero quizás no nos hayamos parado a pensar en la repercusion que ello tiene para la maquina.

Los procesadores que usamos en la mayoria de los casos son el ATmega328P (Arduino UNO), y el ATmega2560 (Arduino MEGA), y ambos tiene una arquitectura de 8 bits, es decir que si le pedimos crear una variable de tipo sea constante o nó, estamos reservando dos bytes de memoria.

No obstante todos sabemos que un pin posiblemente no tendrá un numero mayor de 255 por lo que un byte nos habria servido igual y habriamos ahorrado memoria.

Nuestas declaraciones deberian pues transformarse en

byte pinLed = 5;  // o tambien...
const byte pinLed = 5;  // esta otra que parece mas adecuada

Hay que considerar que tanto los registros como las instrucciones que hacen uso de ellos se apoyan mayoritariamente en el tamaño de 8 bits.

Declaraciones como esta

unsigned int numero1 = 17;
unsigned int numero2 = 25;
if(numero1 > numero2)
{
  // codigo
}

Obligan al procesador a extender la comparacion a dos bytes cuando quizas en nuestro caso nos habria bastado con

byte numero1 = 17;
byte numero2 = 25;
if(numero1 > numero2)
{
  // codigo
}

ahorrando tiempo de proceso (un byte frente a dos) y memoria (dos bytes frente a cuatro).
Puede parecer insignificante pero cuando hacemos programas con gran cantidad de variables, o utilizamos arrays, el ahorro en tiempo y espacio puede ser significativo.

Para tener una pequeña referencia, os detallo el tamaño de los tipos mas usados en nuestros programas:

  • --- tamaño: 8 bits ---
  • char rango: -128 a +127
  • unsigned char rango: 0 a +255
  • byte rango: 0 a +255
  • uint8_t rango: 0 a +255
  • boolean indica false si vale 0, o true para cualquier valor distinto de 0
  • --- tamaño: 16 bits ---
  • int rango: -32,768 a 32,767
  • word rango: -32,768 a 32,767
  • short rango: -32,768 a 32,767
  • unsigned int rango: 0 a 65,535
  • uint16_t rango: 0 a 65,535
  • --- tamaño: 32 bits ---
  • long rango: -2,147,483,648 to 2,147,483,647
  • dword rango: -2,147,483,648 to 2,147,483,647
  • unsigned long rango: 0 to 4,294,967,295
  • uint32_t rango: 0 to 4,294,967,295
  • float rango: -3.4028235E-38 a +3.4028235E+38
  • double rango: -3.4028235E-38 a +3.4028235E+38 (para Arduino DUE es de 64 bits)

Resumiendo:

  • declarad las variables teniendo en cuenta el mayor numero que van a contener, esto incluye los posibles calculos intermedios almacenados en ellas
  • cuando declareis datos que se van a usar solamente de modo informativo (ej. numero de pin), hacedlo siempre como , eso os permitirá guardarlos en la memoria Flash (nuevo ahorro de la preciosa Sram)

noter:
"Espesar" el código fuente no hace que el programa resultante sea más corto ni más rápido, sino en principio sólo más espeso, y en no pocas ocasiónes menos eficiente y desarrollable. Recuerda: ¡Divide y vencerás!
Un ejemplo típico lo selemos tener con los if. ¿Os suenan expresiones de este tipo?

if (boton1==HIGH && boton2==HIGH) {

Serial.print("ambos botones pulsados"); // Para entrar aquí habremos pasado por dos comparaciones
}




¿Alguien piensa que este otro código equivalente va a resultar en un programa más largo o más lento que el anterior?


if (boton1==HIGH){
   if (boton2==HIGH) {
       Serial.print("ambos botones pulsados"); // Para entrar aquí habremos pasado por dos comparaciones
   }
}



Pues la respuesta es que el código máquina resultante en ambos casos es el mismo, y más semejante a este último. Hasta aquí todo se reduce a una cuestión de legibilidad.
Claro, que luego desarrollamos la primera opción manteniendo la filosofía "compacta" y vemos cosas como esta:


if (boton1==HIGH && boton2==HIGH) {
   Serial.print("ambos botones pulsados"); // para entraraquí se han realizado dos comparaciones
}
else if (boton1==HIGH && boton2==LOW){
   Serial.print("sólo botón 1 pulsado"); // para llegar aquí habríamos realizado cuatro comparaciones
}
// Desde el inicio del código hemos pasado un mínimo de dos comparaciones.



Mientras que desarrollando la segunda, buscando entre qué llaves meter cada comando, nos dará un código más compacto y un poco más efectivo:


if (boton1==HIGH){
   if (boton2==HIGH) {
       Serial.print("ambos botones pulsados"); // para llegar aquí se han realizado dos comparaciones
   } else {
       Serial.print("sólo botón 1 pulsado"); // para llegar aquí se han realizado dos comparaciones
   }
}
// Desde el inicio del código hemos pasado un mínimo de una comparación.

Me alegro que alguien haya hecho esta relfexion en el foro.

Respondiendo a tu pregunta sobre si: ¿Alguien piensa que este otro código equivalente va a resultar en un programa más largo o más lento que el anterior?, te dire que lamentablemente si, muchos.

Piensan que una linea de codigo mas o menos hace que luego el programa compilado pueda ser diferente, despreciando lo que es la logica de la programacion que puede ser igual aunque se escriba diferente. En mi caso en particular me paso que algun "experto" me corrigio en el foro por ese mismo ejemplo, es decir, yo propuse un if dentro de otro if y al parecer para esta persona era mas "eficiente" el hacerlos ambos juntos en una linea con &&.

No solo esta el tema de como finalmente se compilaran ambas instrucciones en forma igual, cosa que ni me puse a discutir por no consideraba que la otra parte llegase a entenderlo. Si no fundamentalmente aquello que dices de "espeso". "Espeso" porque me dificultara en el futuro poder agregar nuevas instrucciones, pero ademas "espeso" por lo poco didactico que es, por lo dificil que se hace entender este metodo en comparacion al otro en donde, a fuerza de como bien dices dividir, obtienes instrucciones de una gran simpleza.

Ademas esta el tema de la funcionalidad. Por mas que desperdiciaramos algo de recursos, que generalmente no pasa, dificil sera que fuera eso importante. Cuando empece a programar, (y no soy informatico), usaba una commodore 64. Entonces hubiera pagado muchas veces por un Kb mas. Ahora en el 99% de las cosas que puedo programar ni siquiera me es importante perder mucho tiempo en saber como voy a dimensionar una variable puesto que me sobran recursos por todos lados.

Excelente ArduTip @Noter

Gracias, cas6678. Precisamente redacté ese recordatorio, porque casi todos hemos pasado en algún momento por ese pensamiento. Supongo que porque tratamos de "humanizar" el código, construyendo largas frases con muchos conceptos juntos.

Aprovecho para decir que si alguien quiere proponer algún tema para algún ArduTip puede proponerlo, y se intentará llevar a cabo.

Tras un interesante cambio de impresiones con Alfaville (gracias, Alfaville), y cotejo del ensamblador resultante en comparaciones complejas, concretamente realizando dos funciones con idéntica funcionalidad:

void fun1(){
  if (var1 && var2){
    salida=1;
  }
  else if (!var1 && var3){
    salida=2;
  }
  else if (!var2 && var3) {
    salida=3;
  }
}

void fun2(){
  if (var1){
    if (var2) {
      salida=1;
    }
    else if (var3) {
      salida=3;
    }
  }
  else if (var3) {
    salida=2;
  }
}

Efectivamente, el código resultante era favorable a la segunda opción (en menor grado del que cabía esperar, gracias a las bondades de optimización del compilador, resultando el tamaño del código 46-50 bytes).
La explicación de la ventaja inicial (pues el optimizador consigue aproximar bastante bien el rendimiento) del "método de llaves" se explica porque inmediatamente a evaluarse una condición, se salta a una sección de programa donde esa condición ya se da por evaluada, mientras que con "frases compuestas" es probable que a lo largo de las sucesivas comparaciones volvamos a evaluar la misma condición de nuevo.

Sin embargo, Alfaville reconstruyó la primera opción:

// Modificada por Alfaville
void fun1()
{
  if (var1 && var2)
  {
    salida=1;
  }
  else if (var1 && var3)
  {
    salida=3;
  }
  else if (!var1 && var3)
  {
    salida=2;
  }
}

consiguiendo el compilador optimizar y dar exactamente el mismo código ensamblador de 46 bytes. En resumen, podemos afirmar que la primera opción puede llegar a ser igual de rápida que la segunda, si sabemos establecer las comparaciones con el orden y sintaxis correctos; mientras que la segunda opción "guía" a establecerlas de forma correcta. Por lo tanto, para la mayor parte de programas, no nos volveremos locos y optaremos por la "lógica de frases compuestas" si nos resulta más comprensible que la "lógica de llaves", pero no por que creamos que va a resultar en mejor código.

Gracias por los Ardutips.

Bien saben que soy un fan de las pantallas TFT para arduino. Les comparto el siguiente tip que me pareció muy útil para el llamado de imágenes que tienen nombres de archivo con una secuencia numérica.

Les comparto el contexto. Hace unas semanas llegó a mis manos un teensy 3.6. Esta plaquita (literal: "plaquita" de penas 6.23 cm x 18 cm), se puede programar con el IDE de arduino, y trae consigo un lector SDIO de 4 bits, mucho mas rápido que los lectores SD SPI que conocemos. Ha multiplicado por varias decenas la velocidad de despliegue de imágenes en las pantallas FT81X.

Anteriormente realizaba la carga individual, pero en estas semanas el aprendizaje ha estado lleno de sorpresas, entre ellas el tip que les comparto.

ArduTip TFT: Llamado de múltiples imágenes en FT81X

Para enviar a pantalla una secuencia de imágenes almacenadas en una microSD, podemos llamar cada una de ellas por separado, o bien usando la estructura siguiente:

  snprintf(nombre, 30, "V%03d.jpg", 5);
  String str(nombre);
  archivo = SD.open(str);      
  GD.cmd_loadimage(0, 0);
  GD.loadSDIO(archivo);

Nombre del grupo de imagenes jpg: VXXX.jpg

Defiendo previamente como variables globales:

File archivo;
char nombre[32];

El uso de la secuencia de llamado puede extenderse a otras pantallas sin problema, realizando los ajustes de acuerdo a la librería para TFT en uso:

PD: gracias RndMnkIII

El modificador const, como cabe deducir, tiene como primera funcionalidad la declaración de una constante. Por lo tanto, cuando vayamos a declarar una constante, se lo indicaremos mediante este modificador, con lo que por un lado ahorraremos memoria, ya que el compilador en lugar de almacenarlo en memoria, puede sustituirlo por su literal de forma similar a como lo realizaría con un #define, ahorrando memoria y ciclos. Además, si inadvertidamente tratamos de modificar la constante, el compilador nos lo impedirá y avisará.
Cuando pasamos parámetros de gran tamaño a una función, en muchos casos, en lugar de enviarlos por valor, los podemos enviar por referencia. Es decir, enviar en lugar de una copia del valor de los datos, un puntero señalando dónde se encuentran esos datos. Esto, por un lado, si son datos de gran tamaño, nos va a ahorrar el gasto de memoria y ciclos de reloj que conlleva hacer la copia de los valores. La otra vertiente es que la función tendrá acceso y posibilidad de modificar los parámetros originales. Esto a veces es deseable, pero cuando no tenemos intención de hacerlo, es muy buena costumbre indicar nuestras intenciones, "prometiendo" no tocar los parámetros recibidos si no lo tenemos previsto. El compilador nos avisará si no cumplimos lo prometido; y cualquiera que vaya a utilizar nuestra función y vea el const en la declaración sabrá que si envia una variable ésta no va a ser "mancillada".
Ejemplo un poco "especial":

void setup()                   
{
  Serial.begin(9600);
  char *cad="Esta es mi cadena";
  cuenta(cad);
  Serial.println(cad);
}

void loop()
{
}
// este doble const "promete" que no vamos a modificar ni el puntero de la cadena, ni ningún elemento de la misma.
void cuenta(const char * const cadena){
  Serial.print(cadena);
  Serial.print(" tiene ");
  Serial.print(strlen(cadena));
  Serial.println(" caracteres.");
  //cadena[1]='e'; // prohibido por el primer const
  //cadena=""; // prohibido por el segundo const
}

INTRODUCCIÓN

Dedicaré algunos ArduTips para intentar ayudar a comprender un poco mejor uno de los temas que más engorroso suele resultar a los que se embarcan en el aprendizaje profundo de C/++, y a la vez una de las características que más definen al propio lenguaje, y tal vez de las que más "poder" le proporcionan. Sin embargo, como dijo la frase: "un gran poder conlleva una gran responsabilidad". Un puntero puede proporcionar acceso prácticamente ilimitado a la memoria, lo que puede ser muy buen bien manejado, y nefasto cuando se desboca. Por ello, aunque tampoco deben inspirarnos temor, hay que conocer bien el terreno antes de pisarlo alegremente.
Como digo al principio de este post, "intentar ayudar a comprender". No sé si conseguiré eso o todo lo contrario, o lo uno o lo otro dependiendo del sujeto. Se trata de una materia "avanzada" del lenguaje y no es sencillo (hasta que lo entiendes y piensas que tampoco era para tanto). Así que si alguien comienza a leer estos post y se siente frustrado, que trate de documentarse por otra vía, pues tal vez de con una forma que le llegue mejor. Yo sólo puedo intentarlo con la mejor intención. Si veis algún fleco, detalle, error o lo que sea que se pueda incluir en cualquiera de los post (de este o cualquier otro tema) no dudéis en comentarlo en el hilo.
Gracias.

I ALMACENANDO VARIABLES

Cuando hacemos una declaración en C, el compilador busca la primera posición libre de memoria (de la pila o del montón, dependiendo de si es local o global), y realiza una serie de “anotaciones” acerca de ese valor. Por ejemplo:

int varA = 0x27f;

El compilador “anota” internamente:

  • Que varA es un int. Por lo tanto, para acceder a la variable deberá recuperar dos bytes consecutivos. De paso deberá lanzar warning o error si intentamos utilizar “a” de forma distinta a int.
  • Que varA está almacenado en la posición #102 de memoria.
    Y genera un código ensamblador que reservará dicha posición, y ubicará en ella el el valor literal 0x27f, que quedará tal que así:

    Como curiosidad, si alguien se ha fijado, no me equivoqué al poner el número. Es que arduino coloca primero el byte de menor peso. Muchos procesadores trabajan así, y tiene su lógica aunque a nosotros en principio nos parezca “antinatural”.
    Cuando nuestro programa haga referencia a varA, el compilador mirará sus anotaciones, y generará un código de programa que recuperará el valor entero almacenado en la posición #102-103.
    Es decir, cuando nosotros trabajamos “alegremente” con variables, el compilador controla y traduce esas transacciones mediante esa agenda en la que tiene anotados el tipo de cada variable y la dirección en la que se encuentra almacenada.
1 Like

II ALMACENANDO UN PUNTERO

Pues bien; C/C++, permite la creación de un tipo especial de variable llamada puntero, en la que no guardamos directamente una variable de un determinado tipo, sino una “anotación” de una variable de un determinado tipo, similar a la referida anteriormente. Bueno; realmente sólo se almacena una dirección de memoria al que podremos acceder mediante dicho puntero, para lectura o escritura de un valor del tipo indicado. El compilador tendrá en cuenta igualmente el tipo de variable para la que se ha construido el puntero y nos advertirá si queremos leer o escribir mediante el puntero un valor de otro tipo.

Un puntero se declara mediante el tipo de variable al que va a apuntar, más el operador de indirección (*) más el nombre de la variable. Es buena idea leer la declaración al revés:

  • char *a; // a es un puntero o de carácter/es
  • int *b; // b es un puntero de entero/s
  • date *c; // c es un puntero a datos tipo date (habría que definir primero dicho tipo de dato).
  • long **d; // d es un puntero a un puntero de long/s (nunca he visto la necesidad insalvable de utilizarlos, pero doy fe de que a veces hay quien usa el puntero a puntero).

El operador de indirección, cuando no se está utilizando en una declaración de puntero (ni como operador de multiplicación), y precede a un puntero ya declarado, se utiliza para leer o escribir el valor al que apunta el mismo. Existe también el operador de dirección (&), que colocado antes de cualquier variable, nos devolvería la “anotación” que tiene el compilador de dicha variable en forma de puntero a la misma. Es decir, si tenemos una variable long var1, &var1 nos devuelve un puntero de tipo long a var1 (lo tendríamos que guardar en una variable declarada long * pvar).

A continuación de la declaración de la variable int varA que definimos antes, vamos a definir un puntero, que apuntará precisamente a esa variable:

int *pintA = &varA;

El compilador, al igual que con cualquier variable, buscará la siguiente posición de memoria disponible, y “anota”:

  • Que pintA es un puntero a entero. Esto tiene múltiples connotaciones:

  • Lanzará warning o error si intentamos asignar su valor a otro tipo de variable. Por ejemplo:

  • int c=b. Aunque en arduino un puntero usa dos bytes, al igual que un int, y podría tener traducción (c=102), el compilador advierte que no es lo mismo.

  • char *c = b. Aunque declaramos c como puntero, lo es a char, y no a int. Por tanto también nos puede protestar el compilador. Recordemos, b es un puntero de enteros (int *).

  • También lanzará warning o error si intentamos apuntarlo a algo que no sea un entero (pintA = &variablefloat), o leer/escribir con él a memoria algo que no sea un entero (*pintA = variablelong).

  • Tendrá en cuenta el tamaño del tipo int (2 en arduino) tanto si usamos operador de incremento o decremento sobre el puntero. Esto está relacionado con la aritmética de punteros y arrays, de la que intentaremos hablar en otra ocasión. Valga saber que si hacemos pintA++, pasaría a valer #104 o, lo que es lo mismo, apuntarse a sí mismo.

  • Que pintA está en la dirección de memoria #104.

Y a continuación mirará en los “apuntes” la dirección de varA (&varA == #102) y generará un código ensamblador que cargará dicho valor en la zona de memoria recién reservada a pintA (#104-105). La memoria quedaría así:

III PARA QUÉ SIRVE UN PUNTERO

¡Vale! Ya sé declarar un puntero y asignarle la dirección de una variable. ¿Y ahora, que?
Pues lo más inmediato que podemos decir sobre el puntero recién creado es que su nombre, precedido del operador de indirección (*pintA) desreferencia o "materializa" la variable apuntada; es decir, (*pintA)++ es lo mismo que decir varA++, puesto que es la variable a la que estamos apuntando.
Podemos modificar en cualquier momento el valor del puntero, para que “apunte” a cualquier otra variable entera (pintA=&variableEntera). A partir de ese momento si lo "materializamos" equivaldrá a esa otra variable. Un mismo comando sobre un puntero "materializado" puede estar leyendo/escribiendo distintas variables dependiendo de la que esté siendo "apuntada".
La aplicación inmediata de esto puede ser la de recepción de parámetros en una función mediante punteros. De esa forma, trabajar sobre la "materialización" del parámetro es trabajar directamente sobre la variable cuya dirección se envía, en lugar de sobre una copia de su valor. Esto permite por un lado, ahorrar la memoria y proceso de copiado del valor de los parámetros, y por otro permite modificar los propios parámetros originales. Si no es nuestro objetivo hacer esto último, es buena costumbre “prometerlo” mediante el modificador const.

Ejemplo:

struct fecha
{
  int year;
  byte month;
  byte day;
  byte hour;
  byte minute;
  byte second;
};

void setup()                   
{
  Serial.begin(9600);
  fecha miFecha;
  llenaFecha(&miFecha);
  imprimeFecha(&miFecha);
}

void loop()
{

}

// Esta función modificará la propia fecha cuya dirección se envía (miFecha)
void llenaFecha(struct fecha *f) {
  (*f).year=2015;
  (*f).month=5;
  (*f).day=31;
  (*f).hour=14;
  (*f).minute=0;
  (*f).second=0;
}

// Esta otra no tiene necesidad de tocar la fecha, así que lo "prometemos"
void imprimeFecha(const struct fecha *f){
  Serial.print((*f).day);
  Serial.print("/");
  Serial.print((*f).month);
  Serial.print("/");
  Serial.print((*f).year);
  Serial.print(" ");
  Serial.print((*f).hour);
  Serial.print(":");
  Serial.print((*f).minute);
  Serial.print(":");
  Serial.print((*f).second);
  //(*f).day=1;  // dará error si lo descomentamos, porque hemos prometido no tocarlo
}

Si incrementamos o decrementamos el propio puntero, éste lo hará aumentando o disminuyendo el valor señalado, multiplicado por el tamaño de variable para la que se definió el puntero. Es decir, para un puntero definido como puntero, puntero+x equivale a puntero + (xsizeof ). Esto tiene que ver con la íntima relación entre punteros y arrays. Esta cuestión, junto con otros operadores relacionados con punteros, la veremos con más detalle en un próximo ArduTip.

IV OPERACIONES Y EQUIVALENCIAS CON PUNTEROS

Como hemos dicho, el compilador realiza una “anotación” (su tipo y posición en la memoria) sobre cada variable que declaramos. Un puntero es una variable que puede contener una de esas anotaciones (digamos que es una “anotación variable”). Los operadores relacionados con los punteros, algunos de los cuales ya hemos visto, y tomando como ejemplo el que habíamos utilizado anteriormente, que era un puntero a entero llamado pintA, serían los siguientes:

  • Operadores de incremento/decremento sobre el propio puntero. Realizan lo propio sobre el valor de dirección que contiene el puntero, multiplicando el incremento/decremento por el tamaño del tipo de variable del puntero. Esto es así, porque equivaldría a recorrer los elementos de un array del tipo del puntero. Ejemplos:
    pintB=pintA+1; // pintB apuntará dos posiciones posteriores a pintA (int ocupa dos bytes)
    pintA+=10; // pintA ahora apuntará 20 posiciones más adelante
    pintA--; // pintA ahora apuntará dos posiciones más atrás

  • Operador de indirección * :
    Precedido del tipo de variable para el que se construye, se utiliza para la propia declaración de un puntero. Ejemplo:
    int * pintA;
    Precediendo a un puntero existente, indirecciona, desreferencia o "materializa" (escoger la palabra más entendible) una supuesta variable del tipo y ubicación indicada por el puntero. Ejemplo:
    (*pintA)++; // Si hubiéramos direccionado pintA a una variable entera a, equivaldría a a++;
    En este punto conviene hacer un inciso para recordar un detalle que nos puede dar más de un quebradero de cabeza. Hay que tener muy claras las precedencias o, mejor aún, no arriesgar y asegurar con paréntesis la operación que queremos realizar, o separar en pasos las operaciones; ya que la *indirección, *desreferencia o "*materialización" ES UN OPERADOR MÁS. Por ejemplo:
    int x = *pintA+1; // ¿A cuál de las dos versiones posteriores equivaldrá?
    int x = (*pintA)+1; // x igual al (entero apuntado por pintA) más uno
    int x = *(pintA+1); // x igual al entero apuntado por (pintA más uno), o siguiente entero en un supuesto array

  • Operador de dirección &:
    Precediendo a cualquier variable, el compilador nos devolverá un puntero a dicha variable. El tipo de puntero será el de la propia variable, así que si intentamos cargarlo en un puntero de otro tipo, probablemente el compilador se enfade. Ejemplo:
    int pintA = &variableInt;
    byte pintA = &variableInt; // Error en conversión int a byte

    Sin embargo, esto último a veces sería deseable, por ejemplo para poder descomponer una variable en bytes. Lo podemos hacer, pero indicándole al compilador mediante cast que sabemos lo que hacemos:
    byte pintA = (byte) &variableInt; // Ahora el compilador se fiará de nosotros
    Para un array, podemos obtener puntero al mismo sin necesidad del operador de dirección, sencillamente quitando los corchetes que contendrían el índice a un elemento determinado. Ejemplo:
    int arrayA[]={100, 299, 150, 250};
    pintA = arrayA; // pintA apuntará al inicio de arrayA
    Esta equivalencia entre array y puntero, nos lleva al siguiente operador.

  • Operador de índice []:
    Como acabamos de ver, hay una estrecha relación entre un array y un puntero. De tal forma que así como el compilador al indicarle el nombre de un array sin [índice] devuelve un puntero a dicho array, también un nombre de puntero seguido de un [índice] devuelve el elemento correspondiente de un supuesto array apuntado por el mismo. Igualmente podríamos referenciar un elemento de un array, en lugar de mediante un índice, mediante operador de dirección y aritmética de puntero. Por ejemplo, dadas las declaraciones anteriores de arrayA y pintA, todas estas sentencias serían equivalentes:
    Serial.print(array[2]); //imprime el elemento 2 de arrayA (150)
    Serial.print(pintA[2]);
    Serial.print( *(pintA+2) );
    Serial.print( *(array+2) );

  • Operador de selección para punteros a estructura ->:
    Dado un puntero de tipo struct, desreferencia o "materializa" el campo indicado de la estructura apuntada. Es decir, sustituye al operador punto de la estructura, cuando queremos acceder desde un puntero. Por ejemplo, si tenemos un puntero definido como date *pFecha, podríamos hacer referencia al mes, bien mediante el operador de indirección, "materializando" la estructura y utilizando el punto (__*_fecha)**.mes (como hicimos, de hecho, en el post anterior), o "materializando" el campo directamente desde el puntero con _fecha->**mes (esta sería la forma más correcta). Resumiendo, el punto para seleccionar un elemento de una estructura; la “flecha” -> para seleccionarlo desde un puntero a estructura.
    La función llenaFecha del ejemplo anterior podría quedar así:

// Esta función modificará la propia fecha cuya dirección se envía (miFecha)
void llenaFecha(struct fecha *f) {
  f->year=2015;
  f->month=5;
  f->day=31;
  f->hour=14;
  f->minute=0;
  f->second=0;
}

¡QUÉ JALEO CON LOS PUNTEROS! ¿CONOCES LAS REFERENCIAS?

Si te vuelve un poco loco la notación de punteros, tal vez te encuentres más cómodo con las referencias. Una referencia, que se declara mediante el operador de dirección &, y se asigna valor en la propia declaración. Por ejemplo:

          int varA;
          int &varB=varA; // varB es un alias de varA
          varB=10; // hemos modificado varA a través de su alias

Así, en principio, el uso de una referencia no parece aportar gran cosa, pues disponiendo de la variable "original" poca ventaja puede aportar utilizar un alias. La gran ventaja está en el traspaso de parámetros de forma equivalente, pero tal vez menos liosa y más intuitiva de ver que la que realizaríamos con punteros. Al igual que recomendamos con los parámetros de punteros, si nuestras intenciones son sanas y nuestra función no tiene porqué tocar el original recibido, hay que "prometerlo" en la declaración con el modificador const. Para recibir un parámetro por referencia, se utiliza el operador &.

Ejemplo:

struct fecha
{
  int year;
  byte month;
  byte day;
  byte hour;
  byte minute;
  byte second;
};

void setup()                   
{
  Serial.begin(9600);
  fecha miFecha;
  llenaFecha(miFecha);
}

void loop()
{

}

void llenaFecha(struct fecha &f) {
  f.year=2015;
  f.month=5;
  f.day=31;
  f.hour=14;
  f.minute=0;
  f.second=0;
}

void imprimerFecha(const struct fecha &f){
  Serial.print(f.day);
  Serial.print("/");
  Serial.print(f.month);
  Serial.print("/");
  Serial.print(f.year);
  Serial.print(" ");
  Serial.print(f.hour);
  Serial.print(":");
  Serial.print(f.minute);
  Serial.print(":");
  Serial.print(f.second);
}

Podemos ver rápidamente las diferencias con el programa equivalente con punteros en el post III Para qué sirve un puntero. Hay dos detalles distintos, además de la declaración del parámetro:

  • Para llamar la función con puntero, lo hacemos obteniendo el puntero a nuestra variable:
              llenaFecha(&miFecha); Mientras que para llamar la función con referencia lo hacemos directamente con la variable:
          llenaFecha(miFecha);
  • Para acceder a la propia variable dentro de la función con puntero, debemos utilizar el operador de indirección (para estructuras podemos utilizar puntero->dato en lugar de (*puntero).dato:
              f->year=2015; // o (*f).year=2015;Mientras que dentro de la función con referencia la utilizamos como una variable normal:           f.year=2015;

VISIÓN GENERAL DEL PUERTO SERIE

Uno de los problemas recurrentes que se observa muchas veces en el foro, viene dado por no conocer las limitaciones o no trabajar correctamente con el Serial de Arduino. Intentaremos abarcar los problemas más típicos. Como siempre, estoy abierto a sugerencias y rectificaciones.

Lo primero que debemos tener en cuenta es una serie de precisiones sobre el mismo.

  • La transferencia por puerto serie es “lenta y costosa”. A 9600bps reclamaría la atención del procesador casi diez veces por milisegundo (milis lo hace una vez por milisegundo). A 115200 bps reclamaría la atención cada 8 microsegundos, lo que consumiría gran parte, si no la totalidad, de la capacidad de procesamiento de un Arduino a 16 Mhz.

  • En el párrafo anterior decimos “reclamaría”, porque, si bien utilizando softwareserial el proceso requerido a la CPU va a ser similar al que hemos indicado, utilizando los puertos Serial, gracias al hardware dedicado, se interrumpirá la actividad normal sólo una de cada diez veces de las señaladas anteriormente; es decir, una interrupción por cada byte en lugar de una interrupción por cada bit. En consecuencia, si tienes posibilidad de usar hardwareserial, evita el usar softwareserial. No es infrecuente ver que alguien está pegándose con softwareserial en un Arduino Mega, mientras tres de sus cuatro puertos serie están bostezando. En ocasiones, incluso aunque durante la fase de desarrollo necesitemos el único puerto serie de un Arduino Uno para depurar nuestro código, y por tanto usar softwareserial, puede ser interesante para el proyecto final, si vamos a prescindir del monitor serie, utilizar Serial en lugar de softwareserial. Nuestro programa será más pequeño, y el Arduino va a funcionar más desahogado.

  • Aunque sería posible (y a veces más efectivo) gestionar nosotros las interrupciones que genera el puerto serie, para la gran mayoría de aplicaciones es más que suficiente comunicarse con el objeto de clase Serial que nos viene construido, dejándole gestionar a él las interrupciones. No viene mal, por lo tanto, conocer un poco su funcionamiento interno.

  • Los objetos Serial constan básicamente de un buffer de transmisión y uno de recepción. Ambos tienen un tamaño por defecto de 64 bytes. Si bien habitualmente no va a ser necesario, no está de más saber que se puede modificar desde nuestro programa dicho tamaño. Cuando finaliza la recepción de un byte en el puerto (unos diez bits entre dato, start, stop y paridad), se dispara una interrupción. Entonces el objeto Serial recoge el dato recibido y lo deposita en el buffer de recepción en espera de que nosotros nos acordemos de retirarlo. De igual modo, nosotros dejaremos los bytes a transmitir en el buffer de transmisión, y el objeto Serial mediante una interrupción irá echándolos uno a uno al transmisor según éste los va “tragando”. Por lo tanto nosotros ni transmitimos ni recibimos directamente nada, sino que le dejamos en una mesa los datos a transmitir, y recogemos en otra los datos recibidos.

  • Aparte de los dos buffers, que contienen la información que se va a enviar/recibir, hay tres “números” muy importantes.

  • El número de bytes pendientes de recoger en la mesa de recepción. Los obtenemos mediante el método Serial.available().
    ¡IMPORTANTE! Si hacemos una lectura simple sin verificar con Serial.available que tenemos datos que leer, y el buffer está vacío, la lectura devolverá un dato falso.

  • El número de bytes para el que hay espacio en la mesa de transmisión. Podemos conocer su número mediante la función Serial.availableForWrite.

  • El tiempo de timeout, que es el tiempo máximo que una función de lectura múltiple (readBytes, readBytesUntil, parseInt o parseFloat) esperará a que llegue el siguiente byte cuando no hay ninguno en el buffer de recepción. Por defecto un segundo. Este parámetro se puede establecer mediante la función Serial.setTimeOut.

LA TRANSMISIÓN
El procedimiento de transmisión no tiene mucho misterio. Una vez inicializado el puerto serie (Serial.begin) las distintas funciones de envío de datos (print, println, write…) dejarán en el buffer de transmisión los datos correspondientes. Este proceso debería ser casi instantáneo, continuando por un lado el flujo de nuestro programa, mientras por otro lado, el puerto serie “a su ritmo” irá traspasando bytes de dicho buffer al transmisor. Si deseamos enviar más datos y queda suficiente espacio en el buffer de transmisión, sencillamente se depositan a continuación. Esto es: en estos casos la ejecución de nuestro programa no se detiene, sino que continúa paralelamente al envío de los datos que hemos depositado.
El problema puede venir si se llena el buffer. Entonces nuestro programa se detendrá en la función de envío, e irá depositando uno a uno los bytes restantes a medida que los que se van transmitiendo dejan su hueco en el buffer. Esto significa que nuestro programa permanecerá detenido hasta que, a ritmo de Serial, termine de ir dejando en el buffer todos los bytes a transmitir.
Por tanto, en cuanto al envío eficiente por Serial, si queremos que la transmisión transcurra de forma paralela a nuestro programa sin interrumpirlo, “sólo” hay que tener en cuenta la cantidad y frecuencia de envío de datos que vamos a exigir, para no sobrecargar el buffer. Podemos prever si vamos a desbordar el buffer (y por tanto a detener el flujo del programa), si Serial.availableForWrite() es menor que el número de bytes que vamos a dejar en la mesa.

No obstante, en algún caso, en lugar de transferencia paralela, necesitaremos precisamente que nuestro programa permanezca detenido hasta que se haya terminado de enviar todo el buffer, para lo que sólo tenemos que agregar un Serial.flush().