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谩 鈥渘ada鈥). 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 鈥渁rgumentos鈥 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