Manejo de enteros, funciones y arrays.

Objetivos

  • Operar con enteros.
  • Aprender a definir funciones en Arduino.
  • Diferentes tipos de enteros en C++.
  • Aprender a operar con Arrays .
  • Operar con enteros y Strings.

Material necesario

arduino
  • Arduino Uno o similar.
  • Un PC con el entorno de Arduino correctamente instalado y configurado.

El tipo entero

¿Tiene un entero límite de tamaño? La respuesta es afirmativa. Los enteros int en Arduino utilizan 16 bits por lo que el máximo sería 2 elevado a 16 = 65.536,  Pero como el tipo int usa signo, su valor está comprendido entre -32.768 y +32.767.

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 de 0 hasta 65.535
long Entero con signo, 32 bits Desde  -2.147.483,648 hasta 2.147.483.647
unsigned long Entero sin signo, 32 bits 0 a 4.294.967.295
byte Entero sin signo, 8 bits de 0 hasta 255

Todos estos tipos representan enteros con y sin signo y se pueden utilizar para trabajar con números realmente grandes pero no sin límite.

Cuando nos salimos del tamaño máximo se le llama desbordamiento (overflow) y C++ ignora olímpicamente el asunto, dando lugar a problemas difíciles de detectar si uno no anda con cuidado.

Prueba a 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.

Esto no es un error, sino que se decidió así en su día y C++ no controla los desbordamientos, así que mucho cuidado, porque este tipo de errores pueden ser muy complicados de detectar.

Funciones

Vamos a ver como resolver problemas lógicos partiéndolos en bloques de instrucciones.

Un ejemplo clásico de programación, es el cálculo de números primos. Es importante destacar que no existe una forma única de resolver un problema concreto y que una no tiene porque ser mejor que otra, aunque con frecuencia se aplican criterios de eficiencia o elegancia para seleccionar una solución.

Supongamos que queremos crear un programa que nos devuelva true o false según que el número que le pasamos sea primo o no y a la que podamos llamar varias veces sin copiar el código una y otra vez. La llamaremos primo() y queremos utilizarla de la siguiente manera: Si el numero n que le pasamos es primo nos tiene que devolver true y en caso contrario que devuelva false, o sea, queremos que nos devuelva un valor bool.

Esto es lo que llamamos una función.

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) // int x representa el parámetro que pasaremos a esta función
         {
                Aquí va lo que tiene que hacer
                …………
                return( bool);
         }

Declaramos la función primo() como bool, es decir, que va a devolver un valor bool 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. Si devolviera un entero habría que definirla como:

int primo( int x)

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()

Veamos cómo podría ser el código de la función primo():

bool primo(int n){
     bool respuesta = true
     for (int i=2; i<n; i++){
         // Si el resto es 0, es divisible.
         if (n%i==0){ 
             Serial.println(String(n)+" es divisible por: "+String(i));
             respuesta = false;
         }
     }
     return(respuesta);
 }

Para saber si un número es o no primo basta con dividirlo por todos los números positivos  menores que él y mayores que 1. En el ejemplo 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 (no es primo), devolvemos false, asignando este valor a la variable de retorno y volvemos a la intruccion que llamo a la función.
  • Si no hallamos ningún divisor, al finalizar el for devolvemos true.

Este es el método de fuerza bruta y sin duda es mejorable.

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.

void loop(){ 
   int x = 427 ; // El número a probar
   bool p = primo(x);
   if(p)
       Serial.print(String(x)+" Es primo.");
      else
       Serial.print(String(x)+" No es primo.");
}

Veamos cuantos primos hay hasta el, digamos 1024:

bool control = true ; 
int maximo = 1024 ;
void loop(){ 
    bool p;
   // Solo es para que no repita una y otra vez 
   if(control){ 
      Serial.println("Los numeros primos hasta el "+String(maximo));
      for(int x=2; x<maximo; x++){ 
          p = primo(x);
          if(p) Serial.println(x); 
      }
   }
   control = false ;
}

Aunque el programa funciona correctamente la salida no es muy presentable. Vamos a formatearla. Para ello usaremos el carácter tabulador que se representa como ‘\t’ y una coma después.

        if (primo(x) )
               if ( contador++ % 8 == 0)

                                        Serial.println( String(x)+”,” ) ;
                                    else
                                         Serial.print( String(x) +”,”+ ‘\t’) ;

Ahora el programa formatea la salida de una forma un poco más presentable y cómoda de leer.

 

Para conseguirlo, hemos añadido una coma y un tabulador a cada número excepto a uno de cada 8 que añadimos un intro. También tenemos una línea que conviene comentar:

if ( contador++ % 8 == 0)

Cuando a una variable se le añaden dos símbolos más (+) al nombre, significa que primero se use su valor actual en la instrucción en curso, en este caso en el if, y después se incremente en 1 su valor.

Si hubiéramos escrito:

if ( ++contador % 8 == 0)

Querría decir que queremos incrementar su valor antes de utilizarlo. Esta notación es muy habitual en C++ y conviene reconocerla. También podemos usar contador– y –contador para decrementar.

El tipo entero

Este sería un buen momento para preguntarnos hasta donde podría crecer la variable máximo en el programa anterior. Le asignamos un valor de 1024, pero ¿Tiene un entero límite de tamaño?

La respuesta es afirmativa. Los enteros int en Arduino C++ utilizan 16 bits por lo que el máximo seria en principio 2 elevado a 16 = 65.536, Pero como el tipo int usa signo, su valor está comprendido entre -32.768 y +32.767.

De hecho en Arduino C++ hay varios tipos, de distintos tamaños para manejar enteros:

int : Entero con signo, 16 bits , entre -32,768 y 32,767

unsigned int : Entero sin signo, 16 bits, de 0 hasta 65.535

long : Entero con signo, 32 bits, Desde -2.147.483,648 hasta 2.147.483.647

unsigned long: Entero sin signo, 32 bits, de 0 a 4.294.967.295

byte : Entero sin signo, 8 bits, de 0 hasta 255

Todos estos tipos representan enteros con y sin signo y se pueden utilizar para trabajar con números realmente grandes pero no sin límite.

De hecho C++ tiene la fea costumbre de 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 olímpicamente el asunto, dando lugar a problemas difíciles de detectar si uno no anda con cuidado.

Más sobre funciones en C++

Cuando se declara una función se debe especificar que valor va a devolver. Así:

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 iremos viendo.

Lo que sí está permitido es 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.

Operando con arrays

Con la función Primo() que vimos, a medida que el tamaño del número a probar crece, el tiempo que tarda en determinar si es primo también, ya que dividimos por todos los números que le preceden.

Una manera más eficaz de calcular si un número es primo, es dividirlo solo por los números primos menores que él. Pero para esto necesitaríamos un modo de archivar estos primos.

Podríamos ejecutar primero el programa anterior y hallar los N 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. Para definirlo podemos optar por dos maneras:

int serie1 [ 5] ; //Creamos una colección de 5 enteros

int serie2[] = { 3,5,6,12, 23} ;

En el primer caso definimos un array de enteros, de una sola dimensión con 5 elementos, sin asignar valores de momento.

En el segundo caso asignamos un array de enteros a los valores que le pasamos entre llaves, sin especificar cuantos, porque le dejamos a C++ la tarea de contarlos. Decimos que definimos el array por enumeración.

Para asignar o leer los valores de un array se utiliza un índice entre corchetes. Veamos este programa:

int serie2[] = { 3,5,6,12, 23} ;

void setup(){

Serial.begin(9600) ;

}

void loop(){

for (int i=0 ; i<5 ; i++)

Serial.println(“Posicion ” + String(i)+ “: “+ String(serie2[i]));

}

 

El programa imprime el contenido del array recorriendo sus 5 posiciones.

Atención: la primera posición del un array es la 0 y la última el número de elementos – 1. Así serie2 [0] devuelve el primer elemento 3, y serie2[4] el último 23.

Un error muy peligroso, y difícil de detectar sería algo así:

int serie2[] = { 3,5,6,12, 23} ;

for (int i=0 ; i<99 ; i++)

Serial.println(“Posicion ” + String(i)+ “: “+ String(serie2[i])) ;

Uno esperaría que C++ generase un error, ya que definimos un array de 5 elementos y hacemos referencia a 100, pero no. Nuevamente C++ nos sorprende devolviendo correctamente los 5 primeros valores y luego sigue leyendo posiciones de memoria consecutivas tan tranquilo, como si tuvieran sentido.

C++ espera que seamos nosotros quienes controlemos esto, así que mucho cuidado

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.

La función getLine()

Aunque ya comentamos que podemos 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 y devuelve 0, por lo que tendríamos que controlar el valor devuelto para que no se repitiese continuamente.

Por eso vamos a escribir una función de uso general que nos permita recoger una cadena de texto de la puerta serie sin que salga hasta que reciba un String que vamos a hacer finalice en intro.

String getLine(){

String s = “” ;

if (Serial.available()){

char c = Serial.read();

while ( c != ‘\n’) {

s = s + c ;

delay(25) ;

c = Serial.read();

}

}

return(s) ;

}

Definimos getline() de tipo String, porque queremos que nos devuelva un texto. Comprobamos que hay algo disponible en la puerta serie, y en caso afirmativo construimos un String S añadiéndole cada uno de los caracteres que leemos del puerto serie, hasta que encontremos un intro.

Al encontrar el intro, se cumple la condición de salida del while y termina la función devolviendo la cadena construida (sin el intro).

Normalmente convendrá comprobar si hay algo disponible en la puerta serie antes de llamar a GetLine(), y si es así, la comprobación que hace getLine() de tener algo disponible en el Serial seria redundante.

Pero si llamáramos a getLine() sin comprobarlo y esta no lo controlase, quedaríamos atrapados en esta función hasta que alguien escribiera algo finalizado con intro para poder salir.

Nuevamente hemos incluido un delay de 25 ms en el while para asegurarnos de que Arduino no puede volver a leer mas caracteres antes de que a la velocidad de 9600 bps haya llegado el próximo carácter. Si la velocidad de comunicación es de 115200 bits por segundo o más, se puede suprimir este retraso.