16- Funciones en Arduino


Como son las funciones en Arduino.

Una función es parte de un programa que tiene su propio nombre y realiza una tarea determinada en Arduino. Un programa grande se puede construir a partir de varias funciones, cada una de las cuales realiza su propia tarea. El uso de funciones simplifica enormemente la escritura y lectura de código y, en la mayoría de los casos, lo hace óptimo en términos de la cantidad de memoria ocupada.

La función debe describirse (declararse) y, a continuación, puede llamarse. ¡La función debe describirse fuera de otras funciones! En general, la función tiene la siguiente estructura:

tipo de datos nombre_función (conjunto de parámetros ) { 
  < cuerpo_función >
}

Donde el tipo de datos es el tipo de datos que devuelve la función, el nombre de la función es el nombre con el que se llama a la función, el conjunto de parámetros es un conjunto opcional de variables que admite para trabajar y el cuerpo de la función es el conjunto real de operaciones. La función se llama por el nombre de la función y pasando el conjunto de parámetros, si los hay:

Si no admite ningún parámetro, ¡debe especificar paréntesis vacíos de todos modos!

La función puede necesitar parámetros, puede no aceptarlos, puede devolver algún valor, puede no devolver nada. Echemos un vistazo a estas opciones.

Una función que no acepta nada y no devuelve nada

La opción más fácil de entender, empecemos por ella. Además de los tipos de datos que enumeré en la lección sobre tipos de datos, hay uno más: void, que se traduce del inglés como «vacío». Al crear una función de tipo void, le decimos al compilador que no se devolverán valores (más precisamente, la función devolverá “nada”). Escribamos una función que encuentre la suma de dos números y la asigne al tercer número. Dado que la función no tiene parámetros y no devuelve nada, las variables deberán declararse previamente y hacerse globales, de lo contrario la función no tendrá acceso a ellas y recibiremos un error:

byte a, b;
int c;
void setup() {
  a = 10;
  b = 20;
  sumFunction();
  // después de llamar a la función
  // c es 30
}
void loop() {
}
void sumFunction() {
  c = a + b;
}

Este es un muy mal ejemplo desde el punto de vista de la optimización del código, pero mejoraremos más este ejemplo. Por qué es malo en esta etapa: usamos variables globales, y ellas también participan dentro de la función, y uno de los principios fundamentales de la programación en C ++ es la separación de datos y acciones, además de minimizar el número de variables globales. Como principiante, no debes pensar demasiado en ello, más tarde tú mismo llegarás a esto.

Al separar datos y acciones, puede crear herramientas universales, la función discutida anteriormente no es universal: agrega el global a con global b y escribe el resultado en el global C. Demos el siguiente paso hacia la optimización: dejemos que la función devuelva un valor.

Una función que no acepta nada y devuelve un resultado

Para que una función devuelva un valor numérico, debe declararse con el tipo de datos a devolver. Debe pensar de antemano qué tipo se devolverá para evitar errores. Por ejemplo, sé que mi función de suma funciona con el tipo de datos byte, suma dos de esos números. Esto significa que el resultado puede exceder el límite del tipo de datos byte (sumo 100 y 200 y ahora, ya es 300), entonces la función debería devolver, por ejemplo, el tipo de datos int. Por eso la variable C tipo de datos también es int.

Para devolver un valor, necesitamos el operador de regreso que devolverá el número. Aquí tienes que recordar que return no solo devuelve un valor, sino que también finaliza la ejecución de la función, es decir, las acciones especificadas después return, no se ejecutará! De hecho, esto es muy conveniente, porque con la ayuda de estructuras lógicas y operadores de selección, puede proporcionar varias funciones diferentes en una función. return devolverá valores diferentes.

Un punto: una función de tipo void no parece devolver nada, pero usar return le permitirá terminar la ejecución de la función por condición u otra cosa. ¡Es muy conveniente!

Reescribamos nuestro código para que los números a y b se sumaron y el resultado fue devuelto por la función, y ya usaremos los «identificadores» para ajustar este resultado a C.

byte a, b;
int c;
void setup() {
  a = 10;
  b = 20;
  c = sumFunction();
  // c es 30
}
void loop() {
}
int sumFunction() {
  return (a + b);
}

Bueno, la función se ha vuelto un poco más versátil. Ahora el resultado de la suma de a y b como función se puede utilizar en otros lugares e igualar a otras variables. Para hacer que el código sea aún más versátil, pasemos los valores para sumarlos como parámetros.

Una función que toma parámetros y devuelve un resultado

En cuanto a los parámetros, se enumeran entre paréntesis, separados por comas, indicando el tipo de datos. Cuando se llama a una función, los parámetros especificados se convierten en variables locales que se pueden manipular dentro de la función. Cuando se llama a la función, estas variables reciben los valores que especificamos al llamar. Veamos:

byte a, b;
int c;
void setup() {
  a = 10;
  b = 20;
  c = sumFunction(a, b);
  // с vale 30
}
void loop() {
}
int sumFunction(byte paramA, byte paramB) {
  return (paramA + paramB);
}

Y así es como obtuvimos una función universal sumFunction, que toma dos valores del tipo byte, los suma y devuelve. Esta es la implementación del concepto de «separar código de datos», la función vive por sí misma y no depende de otras variables.

Parecería que uno podría usar una función como sumFunction( 100, 200 ) y devolverá el valor 300… Pero no es tan simple, porque el número entero predeterminado tiene un tipo de datos int, y al intentar transmitir tal int en nuestra función que lleva byte, recibiremos un error que indica que no se puede pasar int en lugar byte. En este caso, puede apuntar el tipo de número a byte manualmente, así es como se verá:

int c;
void setup() {
  c = sumFunction((byte)100, (byte)200);
  // с = 300
}
void loop() {
}
int sumFunction(byte paramA, byte paramB) {
  return (paramA + paramB);
}

Pero es mejor hacer que la función sea más universal, déjelo tomar int paramA y int paramB, porque estas variables son locales, es decir, se crearán cuando se llame a la función y se eliminarán de la memoria cuando finalice la función, y su «tamaño» no importa en principio.

Pero, ¿qué pasa si queremos sumar otros tipos de datos que ya están disponibles en la función? Por ejemplo, float. Puede convertir tipos de datos al pasar parámetros, pero la función seguirá devolviendo un número entero. Una función sobrecargada de C ++ puede hacer que nuestra función sea aún más universal.

Funciones sobrecargadas

Una función sobrecargada es aquella que se define varias veces con el mismo nombre, pero diferentes tipos de datos de retorno y diferentes conjuntos de parámetros

int c;
float d;
void setup() {
  float af = 5.5;
  float bf = 0.25;
  Serial.begin(9600);
  c = sumFunction(10, 20);      // resultado 30
  c = sumFunction(10, 20, 30);  // resultado 60
  d = sumFunction(af, bf);      // resultado 5.75
  Serial.println(c);
  Serial.println(d);
}
void loop() {
}
int sumFunction(int paramA, int paramB) {
  return (paramA + paramB);
}
int sumFunction(int paramA, int paramB, int paramC) {
  return (paramA + paramB + paramC);
}
float sumFunction(float paramA, float paramB) {
  return (paramA + paramB);
}

Entonces, ahora tenemos hasta tres funciones con el mismo nombre, pero diferentes conjuntos de parámetros y tipos de retorno. El programa determinará qué función utilizar en función de los parámetros pasados. Pasó dos float – la tercera función funciona, devolverá float. Pasó tres int – obtuvo su suma usando la segunda función. Pasó dos int – obtuvo su suma usando la primera función. ¡Aquí hay algo tan útil!

Otra variante de la función sobrecargada es una función de plantilla, te permite trabajar con datos de cualquier tipo, siendo declarada una vez. Lee abajo.

Descripción e implementación

Es una buena práctica declarar funciones por separado de la implementación. Qué significa: al principio del documento, o en un archivo separado, describimos la función (esto se llamará prototipo de función ), y en otro lugar escribimos la implementación. Esto se hace en proyectos de software serios, las bibliotecas de Arduino no son una excepción. Además, dicha escritura le permite acelerar ligeramente la compilación del código, porque el compilador ya sabe lo que encontrará en él. El algoritmo es como sigue:

Descripción de la función (prototipo)

tipo de datos nombre_función (conjunto de parámetros ) ;

Implementación de la función

tipo de datos nombre_función (conjunto de parámetros ) { 
  función cuerpo
}

Ejemplo:

// función descriptiva
void funcion2 ( byte dato ) ; 
int getMemes () ; 
void setup  () {  
}
void loop () {  
}
// implementación de la función
int getMemes () {  
  // acciones
}
void funcion2 ( byte dato ) {  
  // acciones
}

Nota: aunque el compilador le permite llamar a una función antes de que sea declarada (en el orden en que se escribe el código), a veces puede que no encuentre la función y mostrará un error «función no declarada». En este caso, basta con hacer un prototipo de la función y colocarlo más cerca del inicio del documento, y en general, todas las funciones utilizadas se pueden realizar en forma de prototipos y ubicarlas al inicio del código, junto con variables globales!

Pasar una matriz a una función.

A veces es necesario pasar una matriz a una función (ya hemos hablado de ellas ), pasar toda la matriz y no un elemento individual. En este caso, no puede prescindir de los punteros (lea la lección sobre los punteros). En el siguiente ejemplo, nuestra función sumFunction sumará los elementos de la matriz que se le pasa. La función sabe de antemano cuántos elementos hay en la matriz, porque indiqué explícitamente el número en el loop ().

int c;
int myArray[] = {100, 30, 890, 645, 251};
void setup() {
  c = sumFunction(myArray);   // resultado 1916
}
void loop() {
}
int sumFunction(int *intArray) {
  int sum = 0;  
  for (byte i = 0; i < 5; i++) {
    sum += intArray[i];
  }
  return sum;
}

Lo que debe recordar de esto: al describir una función, un parámetro de tipo matriz se indica con un asterisco, es decir, tipo_datos * nombre_matriz. Cuando se llama, la matriz se pasa como array_name. 

Le mostraremos cómo hacer una función genérica que sume una matriz de cualquier tamaño. Para ello, el operador sizeof () nos ayudará, que devuelve el tamaño en bytes. Necesitaremos pasar este tamaño como argumento a la función:

int c;
int myArray[] = {100, 30, 890, 645, 251, 645, 821, 325};
void setup() {
  // transfiere la matriz en sí y su tamaño en BYTES
  c = sumFunction(myArray, sizeof(myArray));
}
void loop() {
}
int sumFunction(int *intArray, int arrSize) {
  //variable a sumar
  int sum = 0;  
  // encuentra el tamaño de la matriz dividiendo su peso
  // por el peso de un elemento (aquí tenemos un int)
  arrSize = arrSize / sizeof(int);  
  for (byte i = 0; i < arrSize; i++) {
    sum += intArray[i];
  }
  return sum;
}

Entonces tenemos una función que suma una matriz de tipos de datos int de cualquier longitud y devuelve el resultado.

¡Importante! ¡La matriz pasada a la función no duplica la matriz original! ¡Cualquier acción realizada con la matriz pasada afecta la matriz «original»!


Tipos de funciones.

Funciones de plantilla

Template es otra poderosa herramienta de C ++ que le permite crear algoritmos sin vincularse a tipos de datos. El tema es muy extenso, lo consideraremos solo en relación con las funciones «universales».

Funciones de plantilla. En el ejemplo anterior, usamos funciones sobrecargadas para crear funciones con el mismo nombre, pero diferentes tipos de parámetros pasados. Con las plantillas, puede crear una función que funcione para todos los tipos de datos. El compilador elegirá qué tipos de datos sustituir en la función en la etapa de compilación. La sintaxis se ve así:

template <typename identifier> function_declaration;

Hagamos una función que devuelva el cuadrado de cualquier tipo.

// función de plantilla
// acepta datos de cualquier tipo
// retorna el mismo tipo
template<typename T>
T squareVal(T val) {
  return val * val;
}
void setup() {
  byte a = 10;
  int b = 125;
  float c = 3.14;
  
  a = squareVal(a);
  b = squareVal(b);
  c = squareVal(c);
  // a == 100
  // b == 15625
  // c == 9.85
}

¡Se pueden pasar más datos a la plantilla! Aquí hay un ejemplo de una función que multiplica dos números de cualquier tipo y devuelve el resultado:

template<typename T1, typename T2>
uint32_t mult(T1 val1, T2 val2) {
  return val1 * val2;
}
void setup() {
  byte a = 10;
  int b = 125;
  float c = 3.14;
  long val = mult(a, b);
  val = mult(a, c);
}

En algunas situaciones, las plantillas pueden ser muy útiles. Además, al llamar a una función de plantilla, puede pasar manualmente el tipo de datos:

mult < byte, float > ( a, c ) ;

Cambiar el tipo de datos devuelto funcionará exactamente de la misma manera (vea el primer ejemplo, la función devuelve un tipo de plantilla).

Puede reescribir el ejemplo anterior de función que suma una matriz. La ventaja de la versión de plantilla es que no es necesario pasar el tamaño de la matriz e incluso el tipo de variable; se puede calcular dentro de la función calculando el tamaño a través de la plantilla: longitud_matriz ( número ) = sizeof ( T ) / sizeof ( someArray [ 0 ]), es decir, el tamaño de toda la matriz en bytes se divide por el tamaño de cualquier elemento.

int c;
int myArray[] = {100, 30, 890, 645, 251, 645, 821, 325};
void setup() {
  // pasar la matriz
  c = sumFunction(myArray);
}
template <typename T>
int sumFunction(T &someArray) {
  // variable a sumar
  long sum = 0;
  for (int i = 0; i < sizeof(T) / sizeof(someArray[0]); i++) {
    sum += someArray[i];
  }
  return sum;
}
void loop() {}

Funciones Macro

Probablemente recuerde una directiva de preprocesador como #define. De la lección sobre variables y constantes, aprendimos que con la ayuda de una definición, puede establecer constantes. Característica clave del trabajo de #define, que reemplaza una secuencia de caracteres con cualquier cosa que escribamos allí, y esto hace posible crear las llamadas funciones macro (macro), que no se crean como funciones, sino que se insertan en el código durante la compilación. Por ejemplo, así es como se verá una macro que suma dos números:

#define suma (x, y) ((x) + (y)) 

En la etapa de compilación de código, todos los suma ( valor1, valor2 ) serán reemplazado por la suma de valor1 + valor2.

#define sum(x, y) ((x)+(y))
void setup() {
  byte a = 10;
  byte b = 20;
  byte c = sum(a, b);
  // s tiene el valor 30
  // en tiempo de compilación la expresión sum (a, b)
  // convertido en (a + b)
  int d = sum(500, 900);
  // ahora 1400
}
void loop() {
}

¿Por qué es necesario? Una función ordinaria con un nombre, como se discutió anteriormente, tiene su propia «dirección» en la memoria, y cada vez que se llama a la función, el kernel se refiere a ella en esta dirección, lo que lleva algún tiempo. La función macro está «incrustada» en el código del programa y se ejecuta inmediatamente. Al mismo tiempo, si se llama a una función macro en varios lugares del programa, ocupará más espacio que una función regular separada, ya que todas las llamadas serán reemplazadas con código. Tiene sentido crear macros para funciones simples (como en el ejemplo siguiente), para funciones que rara vez se llaman y en aquellos lugares donde el rendimiento máximo es importante, es decir, cada ciclo de la CPU cuenta. Por ejemplo en funciones computacionales.

Los “argumentos” de una función macro deben estar entre paréntesis como (x) + (y) de arriba, de lo contrario, la operación de la macro puede ser impredecible.

Además, es recomendable poner todo el cuerpo de la función macro entre paréntesis, ((x) + (y)) como arriba, de lo contrario puede encontrarse con una situación desagradable en el orden de cálculo. Por ejemplo, en el código usamos sum (x, y) * 5. La macro se expande a (x + y) * 5. Si el cuerpo de la macro no tiene paréntesis, obtendrá x + y * 5, que tiene un significado completamente diferente!

El «lenguaje» de Arduino tiene varias herramientas listas para usar que parecen ser funciones, pero en realidad son macros. Echemos un vistazo a Arduino.h y veamos lo siguiente:

#define min(a,b) ((a)<(b)?(a):(b))
#define max(a,b) ((a)>(b)?(a):(b))
#define abs(x) ((x)>0?(x):-(x))
#define constrain(amt,low,high) ((amt)<(low)?(low):((amt)>(high)?(high):(amt)))
#define round(x)     ((x)>=0?(long)((x)+0.5):(long)((x)-0.5))
#define radians(deg) ((deg)*DEG_TO_RAD)
#define degrees(rad) ((rad)*RAD_TO_DEG)
#define sq(x) ((x)*(x))

Entonces, ¡todas las funciones familiares resultaron ser macros!

También puede «ajustar una línea» para crear funciones de macro con mucho texto, el ajuste se realiza con una barra invertida – \

#define printWords()        \
  Serial.print("Hello, ");  \
  Serial.print("World");    \
  Serial.println("!");
void setup() {
  Serial.begin(9600);
  printWords();
}
void loop() {}

Tenga en cuenta que no es necesario ajustar en la última línea. Por lo tanto, mientras el preprocesador se está ejecutando, todas las llamadas a la «función» printWords() será reemplazadas por las líneas de código especificadas. Puede pasar argumentos de la misma manera:

#define printWords(digit)     \
  Serial.print("Digit is ");  \
  Serial.print(digit);
void setup() {
  Serial.begin(9600);
  printWords(10); // imprimirá "El dígito es 10"
}
void loop() {}

Como puede ver, declarar una función de macro grande es un inconveniente, y aquí es donde las funciones inline vienen al rescate.

Funciones inline

Una función inline tiene el mismo significado que una función macro: todas las llamadas a la función en el código se reemplazan con el código dentro de la función, lo que mejora el rendimiento, pero ocupa mas memoria Flash adicional con cada nueva llamada. A diferencia de una macro, que es reemplaza por el preprocesador, una función inline se reemplaza por el compilador, es decir, una vez que el preprocesador ha funcionado, esto debe recordarse.

La función inline se declara de manera muy simple: solo agregue la palabra clave inline antes de la declaración de función. El compilador puede negarse a incorporar una función (dependiendo de su configuración), por lo que puede pedirle que fuerce una función en línea utilizando el atributo __attribute__ (( always_inline )). Por lo tanto, para declarar una función inline, debe escribir desde el principio inline __attribute__ (( always_inline ))

Considere una función que simplemente incrementa una variable:

inline __attribute__((always_inline))
int incr(int value) {
  return ++value;
}

¡Ya esta! Ahora cada llamada a incr() será reemplazada con su código.

Si necesita separar la descripción y la implementación, puede escribirlo así:

// prototipo
inline __attribute__((always_inline)) int incr(int value);
//implementacion
int incr(int value) {
  return ++value;
}

Funciones estáticas. static

En la lección sobre variables y tipos de datos, discutimos el especificador static, que le permite ocultar la variable global de otros archivos de programa. Con el especificador de función static hace lo mismo: la función static está oculta para que no se la llame desde otros archivos, es decir su alcance es el archivo en el que reside. static void printHello ();

Puntero de función.

Los punteros son un tema muy complejo que difícilmente será útil para un principiante, pero necesitas conocer algunos algoritmos. Por ejemplo, pasar un puntero a una función es útil al crear sus propias funciones al estilo de ArduinoattachInterrupt (), en el que especificamos nuestra función, que creamos nosotros mismos, y se llama en otro lugar. Puedes hacerlo así:

void (*p_function)();   // puntero a p_function 
void setup() {
  Serial.begin(9600);
 // adjuntar myFunction a * p_function
  attachFunction(myFunction);
  // llama a p_function, que llama a myFunction
  (*p_function)();
}
void loop() {
}
void attachFunction(void (*function)()) { // pasando un puntero a una función
  p_function = *function;
}
void myFunction() {
  Serial.println("lolkek");
}

Deja un comentario