Funciones 1
A programar se aprende programando, aprendiendo la sintaxis de lenguaje, pero también aprediendo a resolver problemas lógicos y transformándolos en instrucciones. Hacer cursos de programación está bien, pero al final hay que ponerse a programar, tener problemas y resolverlos, por eso, vamos a centrarnos en algunos ejemplos clásicos de programación, para ir entrenado la capacidad de búsqueda de algoritmos prácticos para resolver problemas más o menos abstractos e ir conociendo algúnos conceptos adicionales
Lo primero es que no hay una única forma de resolver un problema, sino varias y no tiene porque ser una mejor que otra, pero si se suele aplicar criterios de eficiencia y elegancia para seleccionar la solución.
Vamos a hacer un programa que nos devuelva "true" o "false" cuando le pasemos un número para saber si es primo o no. Además que lo podamos llamar todas las veces que queramos, sin tener que volver a escribir el código con cada vez. A este tipo de programas se les llama función.
Haremos una función a la que llamaremos primo(), toda función va seguida de los parentesis, y lo que queremos es que la función devuelva true si el número es primo y false si no lo es, por tanto, queremos una función tipo bool.
En realidad, ya hemos utilizado varias funciones que Arduino trae predefinidas como el Serial.print() o abs() , o Serial.available() y se las reconoce por esa apertura y cierre de paréntesis.
C++ nos ofrece todas las herramientas para crear nuestras propias funciones y es algo muy útil porque nos ayuda a organizar un problema general en trozos o funciones más pequeñas y más fáciles de manejar.
Para definir una función así, tenemos que declararla primero y describirle a C++ que hacer:
bool Primo( int x)
{
Aquí va lo que tiene que hacer
…………
return( bool);
}
Declaramos la función Primo () como bool, o sea va a
devolver true o false y por eso en algún punto tendremos que usar la
instrucción return( true) o return( false) para devolver un resultado a
quien la llame. Dentro del paréntesis pondremos los parámetros que necesita la instrucción para realizar su trabajo, en este caso un entero a través de una variable, la x. Así que sería Primo(int x). Si la función no necesita parámetros, se abre y se cierra los paréntesis sin poner nada dentro. Si devolviera un entero habría que definirla como int
Primo( int x). x es el número que vamos pasarle a la función.
Si una función no va a devolver ningún valor, sino que simplemente realiza su trabajo y finaliza sin mas entonces hay que declararla como void (vacía). Ya cononocemos dos funciones así : setup() y loop()
Para saber si un número es primo o no, basta con dividirlo por todos los números positivos menores que él y mayores que 1. Para ello dividimos el número n empezando en 2 y finalizando en n-1.
Si encontramos un valor de i que devuelve resto 0, entonces es divisible, es decir, no es primo, y por tanto, devolvemos false con return y seguimos el programa por la intruccion que habia llamado a la función. Si al finalizar el for, no hallamos ningún divisor, entronces devolvemos true y listo. Este es el método de fuerza bruta y sin duda es mejorable pero de momento nos sirve.
Para usar Primo hay que pasarle un entero. Recordad que al definir la función dijimos bool Primo (int n) donde n representa el valor que queremos probar. Así pues
En el setup() añadimos la librería Serial.begin para poder ver el resultado en el monitor serie.
En el void loop vamos a declarar la variable entera x y le asignamos el valor del número primo que queremos comprobar.
Luego creamos una variable tipo bool, a la que llamamos p y le asignamos como valor la función Primo() o mejor dicho, lo que devuelva la función primo y como parámetro de dicha función, le asignamos el valor de la variable x, que es el número a comprobar. Luego tenemos un if, en el que si p es igual a true imprimirá que el número es primo, y si no, imprimira que el número no es primo. Arduino cuando llegue a la variable p, saltará a la función y volverá con el resultado de la función a la variable p y ejecutará el if o el else.
Si la función devuelve true, el resultado será "Es primo."
Si la función devuelve false, el resultado será "427 es divisible por: 7" "427 no es primo"
Con el siguiente código vamos a ver cuantos primos hay hasta el 1024. Para ello vamos a declarar dos variables, una de tipo bool, llamada "control" y otra de tipo entero, llamada "maximo", que será la encargada de guardar el valor máximo de numeros a comprobar.
En el void setup() solo vamos a poner el Serial.begin(9600); para poder ver el resultado en el monitor serie.
En el void loop(), que es el programa principal, empezamos con un if que se ejecutará solo si la variable "control" es true. En tal caso, imprimira el texto que dice: "los números primos hasta el 1024",por que 1024 es el valor que le asignamos a la variable "maximo". Luego tenemos un for con una variable x que empiza en el 2, que irá incrementando de uno en uno con cada iteración y repetirá el código de su interior, mientras x sea menor que el valor de la variable máximo, es decir, 1024, es a la variable tipo bool llamada "p" la que guardará el resultado de la función primo para el valor de la x en cada repetición. Con el segundo if cada vez que p sea true, imprimirá dicho número.
Si en el primer if no se cumple la condición, guardará el valor de false en la variable "control" y los números no se imprimirán, igualmente cuando el if finalice, se guardará el valor de false en la variable "control" y dejará de imprimir.
Las funciones se escribirán siempre después de la función void loop(), si son varias una a continuación de otra y en este caso, se escribe la función Primo() que escribimos anteriormente.
Si cargas el programa en arduino y abres el monitor serie, veras que salen todos los números primos uno debajo de otros y todos en una sola columna, esto no es muy presentable, vamos a darle formato usando el caracter tabulador, que se representa por "\t" y pondremos una coma después. También vamos a modificar la variable bool p, por una variable tipo entero llamada contador, también modificaremos el interior del if del for . El código es el siguente:
Hemos declarado al principio una nueva variable tipo entero llamada "contador" y la hemos inicializado asignándole el valor 1, en ella se va a guardar el orden del número que se vaya imprimiendo, cada vez que llegue a 8 o múltiplo de 8, imprimirá la coma y hará un salto de línea, en el resto solo habrá una coma y espacio tabulador después de cada número.
En el void setup() no hay cambios, sigue el Seria.begin para ver el resultado en el monitor serie.
En el void loop() Tampoco hay cambios, excepto el interior del bucle for, donde la variable p la hemos quitado junto con el if y en su lugar hemos puesto if el valor guardado en la nueva variable contador, a la que le sumamos 1 y la dividimos por 8 y nos da de módulo 0, entonces imprime el valor del x, seguido de una coma y un salto de línea, sino da 0, imprime el valor de x seguido de una coma y un espacio tabulador.
Es igual que el códgo anterior, y ahora como resultado nos dá:
Solo comentar que en el caso de la condición del segundo if del for, hemos puesto contador++, eso significa que leerá el valor guardado en el contador y le sumará 1 antes de hacer la división. Si hubiesemos puesto ++contador entonces primero le suma 1, luego lee la variable y le hace la división, con lo cual no aparecería el 1 en la lista. Es algo, que hay que tener claro con los incrementos y las variable.
Funciones 3
El Tipo Entero
Hemos estado hablando del tipo entero, que ya sabemos que se trata de números positivos o negativos sin parte decimal. Pero hay que saber más sobre este tipo de dato.
Los enteros, int en Arduino C++ utilizan 16 bits por lo que el máximo sería 216 = 65.536, esta es la cántidad máxima de enteros, pero como la mitad tienen que ser negativos y la mitad positivos, su valor está comprendido entre -32.768 y +32.767.
Bueno, se puede usar un entero sin tener en cuenta su signo, este tipo de valor se conoce como unsigned int, es decir, entero sin signo y su valor va desde 0 a 65.536.
Para números enteros más grandes podemos usar los de tipo long, que son de 32 bits, lo cual es 232 – 1 lo cual son 4.294.967.295, que si dividimos entre 2, positivos y negativos nos salen que los tipo long van desde -2.147.483,648 hasta 2.147.483.647 y por su puesto si no queremos usar el signo, tenemos lo del tipo unsigned long que van desde 0 a 4.294.967.295. Para finalizar tenemos un entero de tipo byte, de 8bits = 1 byte, de ahí su nombre que solo son positivos, y un byte tiene 28 ,por tanto va de 0 hasta 255, este tipo de datos es muy utilizado para variable que contenga valores analógicos, por ejemplo. En resumen:
De hecho en Arduino C++ hay varios tipos de distintos tamaños para manejar enteros:
Tipo | Descripción | Valor |
---|---|---|
int | Entero con signo, 16 bits | entre -32,768 y 32,767 |
unsigned int | Entero sin signo, 16 bits | 216 – 1 ; de 0 hasta 65.535 |
long | Entero con signo, 32 bits | 232 – 1 ,Desde -2.147.483,648 hasta 2.147.483.647 |
unsigned long | Entero sin signo, 32 bits | Desde 232 – 1 ; 0 a 4.294.967.295 |
byte | Entero sin signo, 8 bits | 28 de 0 hasta 255 |
Todos
estos tipos tienen su límite, ya que un ordenador y mucho menos arduino no tienen capacidad infinita. C++ esperar que nosotros llevemos el
cuidado de no pasarnos metiendo un valor que no cabe en una variable.
Cuando esto ocurre se le llama desbordamiento (overflow) y C++ ignora el asunto, dando lugar a problemas difíciles de detectar si uno no se da cuenta.
Prueba lo que ocurre al calcular esto en un programa:
int i = 32767 ;
Serial.println ( i+1);
Enseguida veras que si i=32767 y le incrementamos en 1, para C++ el resultado es negativo. Eso es porque sencillamente no controla el desbordamiento. También es ilustrativo probar el resultado de
int i = 32767 ;
Serial.println (2* i + 1);
Que según Arduino es -1.
Algunas cosas más que debemos saber sobre las funciones en C++
Cuando se declara una función se debe especificar que tipo de dato va a devolver. Así:
Instrucción | Significa |
int Funcion1() | Indica que va a devolver un entero |
String Funcion2() | Indica que va a devolver un String. |
unsigned long Funcion3() | Indica que va a devolver un long sin signo |
void Funcion4() | No va a devolver valores en absoluto |
Una función puede devolver cualquier tipo posible en C++, pero sólo puede
devolver un único valor mediante la instrucción return(). Expresamente
se impide devolver más de un valor. Si se requiere esto, existen
otras soluciones que veremos en su momento como es el uso de variables globales o pasar valores por referencía.
En cambio, sí se puede pasar varios argumentos a una función:
int Funcion5 ( int x , String s , long y)
Aquí declaramos que vamos a pasar a Funcion5, tres argumentos en el orden definido, un entero un String y por ultimo un long.
Planteando un programa un poco más complicado.
Hemos visto ya como definir funciones. Ahora vamos a plantear un programa que acepte un número desde la consola y compruebe si es primo o no, y si no lo es, nos calcule cuales son los divisores primos de este.
Para resolver un problema complejo lo mejor es partirlo en problemas más pequeños que se resuelven más fácilmente. En este caso vamos a plantear al menos 3 funciones:
La idea es, que primero compruebe si un numero es primo. Si lo es, bien y si no Io es, llamaremos a una función que calcule cuales son sus divisores. Y por ultimo necesitamos algo que nos permita pasar a arduino un número desde la consola para probarlo. A esto se le llama estrategia de resolución, y suele llevar algo de tiempo y esfuerzo, prueba y error, hasta conseguir el resultado.
Funciones 4
Operando con Arrays (matrices).
Con la función Primo() que hemos visto, a medida que el tamaño del número a probar, crece, el tiempo que tarda en determinar si es primo, también crece, ya que dividimos por todos los números que hay entre el 1 y el número en cuestión.
Una manera más eficaz de calcular si un número es primo, es dividirlo solo, por los números primos menores que el. Pero necesitamos un modo de archivar estos primos.
Podríamos ejecutar primero el programa que hemos visto para hallar los N (1024) primeros números primos, y si dispusiéramos de algún medio para guardarlos, tendríamos un sistema más eficaz para decidir si un número es o no primo.
Una manera de archivar estos números es definir un array.
Un
array es simplemente una colección de elementos organizados como una
matriz, y pueden definirse con varias dimensiones.
Empecemos con un array de una sola dimensión. Se puede definir de dos maneras:
Definimos un array de enteros, de una sola dimensión con 5 elementos, sin asignar valores de momento.
int serie1 [ 5] ; //Creamos una colección de 5 enteros cuyas posiciónes están vacía de momento.
Definimos el array por enumeración, le pasamos los valores entre llaves
int serie2 [ ] = { 3,5,6,12, 23} ; //Aquí ya tenemos 5 enteros ocupando su posiciones dentro del array
Para asignar o leer los valores de un array se utiliza un índice entre corchetes.
En la primera línea hemos declarado un array, llamado "serie2" que contiene tipos enteros. Se le ha asígnado una coleccíón de valores, por enumeración, donde el 3 ocupa la posición 0, el 5 ocupa la posición 1, el 6 ocupa la posición 2, el 12 ocupa posición o índice 3 y el 23 ocupa la posición 4.
Vemos que un array se declara parecido a las variables, pero después de nombre lleva abrir y cerrar corchete, y tras el signo de asignación se le pasa los valores ordenado como queremos, separados por comas y entre llaves y recueda el punto y coma.
En el void setup, ponemos el Serial.begin(9600) para ver el resultado en el monitor serie.
En el void loop, tenemos el for que va a recorrer el array, empezando por la posición 0, mientras que la variable sea menor que 5, y se va incrementando la posición en 1 con cada repetición. En cada repetición va a imprimir en el monitor serie "posición (la posición dentro del array): (el valor almacenado en dicha posición)
El delay de 10 segundos es para que nos de tiempo a ver los valores en cada posición. Cada 10 segundo se vuelve a imprimir.
¡ojo! que dentro de un array la primera posición es la posición 0 y la última posición es el número de elementos menos uno. Si hay cinco elementos, las posiciones van del 0 al 4.
Otra cosa con la que hay que tener cuidado es poner dentro del for, en la condición posiciones de más, por ejemplo si tiene 5 elementos y pones mientras que i<15, en lugar de i<5, C++ no dará lugar a error, sino que lo va a compilar sin problemas, arduino, leerá bien los 5 elementos y se inventará el resto, hasta la posición 14, con lo cual dará problemas en el funcionamiento del código, y esto es algo difícil de detectar.
Por último, mencionar que podemos manejar arrays de varias dimensiones:
Int Tablero[ 8, 8 ] ;
Imaginad que Tablero representa las posiciones de una partida de ajedrez y cada valor que contiene esa posición corresponde a una pieza que se encuentra en esa casilla.
Funciones 5
Mejorando la función Primo().
Creamos un array con los primos entre 1 y 1024, ejecutando la programa que hicimos en el segundo vídeo de funciones, y hacemo un copy y paste del resultado:
int P[ ] =
{ 2, 3, 5, 7, 11, 13, 17, 19,
23, 29, 31, 37, 41, 43, 47, 53,
59, 61, 67, 71, 73, 79, 83, 89,
97, 101, 103, 107, 109, 113, 127, 131,
137, 139, 149, 151, 157, 163, 167, 173,
179, 181, 191, 193, 197, 199, 211, 223,
227, 229, 233, 239, 241, 251, 257, 263,
269, 271, 277, 281, 283, 293, 307, 311,
313, 317, 331, 337, 347, 349, 353, 359,
367, 373, 379, 383, 389, 397, 401, 409,
419, 421, 431, 433, 439, 443, 449, 457,
461, 463, 467, 479, 487, 491, 499, 503,
509, 521, 523, 541, 547, 557, 563, 569,
571, 577, 587, 593, 599, 601, 607, 613,
617, 619, 631, 641, 643, 647, 653, 659,
661, 673, 677, 683, 691, 701, 709, 719,
727, 733, 739, 743, 751, 757, 761, 769,
773, 787, 797, 809, 811, 821, 823, 827,
829, 839, 853, 857, 859, 863, 877, 881,
883, 887, 907, 911, 919, 929, 937, 941,
947, 953, 967, 971, 977, 983, 991, 997,
1009, 1013, 1019, 1021
} ;
Al hacer esto, hemos definido un array enumerando sus elementos, entre llaves y separados por comas. Importante que no haya coma después del 1021. También es importante el punto y coma despues de la llave de cierre, porque es una instrucción. Al definir el array por enumeración, si el número de elementos es alto podemos perder la cuenta de los elementos que contiene. Para calcular el número de elementos, se usa la instrucción sizeof(); y podemos utilizarla asignándola a una variable por ejemplo, la variable int size
int size = sizeof(P) / sizeof(int);
Ahora bastaría dividir el número a probar por aquellos elementos del array P, menores que él:
Tenemos la función Primo a la que le pasaremos como parámetro el número entero x, a comprobar. Dentro de la función tenemos una variable local llamada index inicializada con cero.
Luego tenemos el bucle while que dice que mientras el valor del array que se encuentra en la posición indicada por la variable index, sea menor que el número x que se está comprobando, se divide el número x a comprobar entre el número guardado en la posición index del array más uno, y si da de resto 0 devuelve un false, porque es un número compuesto y por tanto no es primo.
Si al terminar el bucle, ninguna de las divisiones ha dado de resto cero, el bucle devuelve un true, porque entonces si se trata de un número primo.
La función Divisores()
Esta función va a recorrer los elementos del array en tanto en cuanto sean menores (posibles divisores) que el número que probamos. Si encontramos divisores primos los guardamos en un array que hemos llamado Div[ ] al que le asignamos un máximo de 32 divisores:
Declaramos la matriz o array con 32 posiciones de tipo entero. Creamos la función divisores a que se le pasará un parámetro tipo entero, que es el número a evaluar.
Dentro de la función divisores() tenemos dos variables, index y pos. ambas inicializadas con un cero. Luego tenemos el bucle while, que nos viene a decir que: mientras que el número guardado en la posición index del array p sea menor que x, que es el numero a comprobar, ejecutaremos el siguiente bloque de código:
Dicho bloque, con tiene una variable k a la que le hemos asignado el número contenido en la posición index más uno. Si dividimos el número a comprobar x entre el número contenido en la variable k, nos da de resto cero, guardaremos el número x en el array Div[ ] en la posición determinada por el valor de la variable pos +1. y volvemos a empezar, hasta que se salga del bucle while. Para entonces tedremos una serie de valores guardados en el array Div[ ] que serán los valores que devuelva la función.
Es importante entender las fuciones Primo() y Divisores(), ambas recorren los valores hasta que sobrepasan el valor del número a probar, pero si el valor del número a probar es mayor que el valor máximo que contiene el array P[ ] entonces dará resultados extraños.
Este método de buscar divisores es válido solo para números inferiores a 1024( o en su caso, al máximo número hasta el que hayamos calculado primos), Si ponemos un número mayor que 1024, entonces un número no será primo si Primo() lo afirma, ya que encontrará divisores. Por tanto, puede afirmar que un numero es primo erróneamente si sus divisores son superiores al máximo primo en P[ ].
La función GetLine()
Aunque ya vimos como usar una función parseInt () incluida en Arduino para recoger un valor del puerto serie, tiene el inconveniente de que si no recibe una entrada salta al cabo de un tiempo ( muy escasito) y devuelve 0, por lo que tendríamos que controlar el valor devuelto para que no se esté repitiendo continuamente.
Por eso vamos a escribir una función de uso general que permite recoger una cadena de texto de la puerta serie sin que salga hasta que reciba un String y pulsemos intro.
Definimos la funciónGetline() de tipo String, porque queremos que
nos devuelva un texto.
Creamos la variable S que inicialmente estará vacía.
La condición del if es comprobar si en la puerta
serie, hay algo disponible para leer, en caso es afirmativo, se ejecutará el código de if, en caso negativo seguira comprobando, por si llega algo por el puerto serie.
Cuando se cumple la condición, la variable tipo char c, que tiene asignado la intrucción de leer el puerto serie, guarda dentro de sí, el primer carácter que llega por el puerto serie a arduino.
Con el while, mientras que no se guarde en la variable char c el caracter de intro o salto de línea '\n' se ejecutará en bucle el código que hay dentro del while. Lo primero es leer el contenido de la variable S, que puede ser vacío o nada o puede contener una palabra o frase o número, etc, luego se concatena a esto el nuevo caracter leido y guardado en la variable c, y luego se guarda todo junto de nuevo en la variable S, espera 25 milisegundos para evitar errores de lectura y escritura y vuelve de nuevo a asignar a c la instrucción de leer de nuevo el puerto serie, borrando el caracter que se ha concatenado y comenzando de nuevo. Así, caracter a caracter se forma una cadena dentro de la variable S, que dejará de crecer cuando se lea el caracter intro o salto de línea '\n' en dicho caso, se sale del bucle while y la función devuelve la cadena incluida en la variable S. En este caso dicha cadena será el número a comprobar.
El programa principal
Ya tenemos las tres funciones necesarias, con lo que ya podemos escribir nuestro programa principal loop(), que llame a las funciones que hemos definido a lo largo de esta sesión, para determinar si un numero que le pasamos por la puerta serie es primo o no y en caso negativo que nos muestre los divisores primos encontrados.
Empezamos declarando un array P [ ] definido por enumeración, que contiene todos los números primo menores de 1024 ordenados. Luego hemos declarado otro array con 32 posiciones vacías, para se utilizadas cuando llegue el momento al que hemos llamado Div[ ].
En la función void setup() solo hemos colocado el Serial.begin para poder ver los resultados en el monitor serie a 9600 baudios.
En la función void loop() empieza con un if cuya condición es la de comprobar si hay algo para leer en la puerta serie y si es asi llamamos a función GetLine() para que lea todo lo que entra en el puerto serie, y cuando finalice, no envíe la cadena completa que se guardará en la variable tipo String S, que tiene asignada la llamada a dicha función.
También hemos declarado una variable de tipo entero llamada "i" a la que le hemos asignado la instrucción s.toInt(); que se va a encargar de convertir el texto String que acabamos de guardar en la variable S a un número entero, ya, que para comparar dicho número y este tiene que ser un valor numérico.
A continuación tenemos un if cuya condición es que el valor guardado en la variable "i" que hemos convertido a numérico a ser llamado por la función Primo(), dicha función devuelva un true, es decir que sea primo. Por eso se llama a la función Primo(i) desde el interior de la condición. Si de vuelve el true, se imprimirá en el monitor serie un texto con el valor que esté guardado en la variable "i" seguido de la frase "Es primo". En el caso de que la función Primo() devuelva un false, en el monitor serie, se imprimira con salto de línea, el valor que esté guardado en la variable "i" seguido de la frase "No es primo", luego se imprimirá con salto de línea la frase "Sus divisores son" se leerá el valor que esté guardadon en la variable j a la que se le ha asignado el valor que devuelva la función Divisores () en la posición correspondiente al número guardado en la variable i.
Luego se ejecutará el bucle for, que empezará con la variable local n en cero y mientras que n sea menor que el valor de la variable j imprimirá los valores del array Div[ ] en las posicionesque marque n separados por comas. El bucle irá imprimiendo en el monitor serie el valor todos los valores guardado en el array Div[ ], mostrando así todo los divisores del número que ha encontrado.
Después de void loop() encontramos las tres funciones que ya hemos visto.