El otro día intentando hacer una operación muy sencillita con Arduino; me encontré con la sorpresa de que no funcionaba de forma correcta.
Explico un poco el esquema práctico en el siguiente post. Tengo una rueda controlada con un motor RC y conectado a ésta hay un encoder ranurado para medir la posición y la velocidad de la misma. Hago una función con un parámetro de entrada para introducir un valor y girar exactamente ese valor.
El número de ranuras que tiene un giro completo es de 20, por lo que se han de multiplicar por dos para definir los estados del giro. Es decir son 20 ranuras vacías y otras 20 ranuras no vacías. Ya explicaré más adelante el control de esta rueda con un sensor óptico e interrupciones.
El caso es, que programando esta función de la siguiente manera, tiene que devolver las ranuras que ha de contar para aproximarse a ese valor de ángulo introducido en grados.
int nrunes=40; void setup(){ Serial.begin(9600); } void loop(){ Serial.print("Ranuras para un giro de 200 : "); Serial.println(math_fail(200)); Serial.print("Ranuras para un giro de 1000 : "); Serial.println(math_fail(1000)); delay(1000); } // Variable "angle" in degrees int math_fail(int angle){ int giro = angle*nrunes/360; return giro; }
Introduzco dos valores, uno corresponde a un giro de 200º y otro de 1000º y aparece lo siguiente.
Efectivamente si hacemos la cuenta a mano, el giro de 200º se corresponde con un giro de 22 ranuras de las 40 que hay. Como se puede apreciar aparece un número negativo para la segunda instrucción de 1000º, cosa que es del todo incorrecta.
Esto ocurre porque la operación matemática se realiza sobre enteros cuyo límite es de 32768.
int giro = angle*nrunes/360
Si vamos en orden multiplicamos primero el valor del ángulo que es 1000*40=40000 y este valor supera el límite del entero dando un número negativo erróneo y posteriormente lo divide por 360.
Ahora vamos a intentar lo mismo pero vamos a modificar el orden de las operaciones, es decir en lugar de multiplicar primero y luego dividir, vamos a dividir primero y luego multiplicar.
int nrunes=40; void setup(){ Serial.begin(9600); } void loop(){ Serial.print("Ranuras para un giro de 200 : "); Serial.println(math_fail(200)); Serial.print("Ranuras para un giro de 1000 : "); Serial.println(math_fail(1000)); delay(10000); } // Variable angle in degrees int math_fail(int angulo){ int giro = angulo/360*nrunes; return giro; }
En este caso los dos valores son incorrectos, el valor de 1000º se corresponde con un valor de 111 ranuras , mientras que devuelve un valor de 80, ya que la división redondea debido al manejo de enteros.
int giro = angle/360*nrunes
En el segundo caso al dividir un número entero pequeño por otro más grande, como ocurre con 200/360, el valor devuelto es otro entero igual a 0.
Por ello al volver a multiplicar este número por 0 el resultado sigue siendo 0.
Una de las soluciones es utilizar un tipo de dato entero más amplio, para evitar este tipo de problemas, por ejemplo el tipo long.
int nrunes=40; void setup(){ Serial.begin(9600); } void loop(){ Serial.print("Ranuras para un giro de 200 : "); Serial.println(math_fail(200)); Serial.print("Ranuras para un giro de 1000 : "); Serial.println(math_fail(1000)); delay(10000); } // Variable angle in degrees long math_fail(long angulo){ long giro = angulo*nrunes/360; return giro; }
*Cuidado, la cuenta es la del primer caso int giro = angle*nrunes/360, si pusiéramos la otra fórmula, seguiríamos teniendo el problema del redondeo.
Un ejemplo habitual es el control de un ultrasonido y la fórmula que se establece para medir la distancia es la siguiente.
Como vemos se hace una simplificación de las unidades para que no ocurra el sobrepasamiento de la variable al multiplicar por 340 y por 1000. Por ello se suele recurrir a la división de la inversa en lugar de multiplicar por un valor muy grande.
Si fuéramos a la programación habitual de este caso suele aparecer de la siguiente manera.
//Calculate the distance (in cm) based on the speed of sound. distance = duration/29.1/2; //29.1 es la inversa de 0.34 = 340/1000
Pero en este caso del ultrasonido, sabemos seguro que el valor de la variable duration; que está en microsegundos; suele ser suficientemente más alta para evitar el problema descrito anteriormente.
De todas maneras la forma correcta para evitar este problema independientemente del orden de las operaciones es utilizar variables del tipo float cuando se requiere.
int nrunes=40; void setup(){ Serial.begin(9600); } void loop(){ Serial.print("Ranuras para un giro de 200 : "); Serial.println(math_fail(200)); Serial.print("Ranuras para un giro de 1000 : "); Serial.println(math_fail(1000)); delay(10000); } // Variable angle in degrees long math_fail(float angulo){ long giro = angulo/360*nrunes; return giro; }
Observar que la salida de la función ha de ser un entero, ya que el número de las ranuras son valores discretos y para asegurar valores muy altos lo definimos como long. Lo único que hemos cambiado es que los tipos de dato dentro de la operación se manejen con decimales, como la variable angle que ahora es del tipo float. Después ya se hará la conversión de la manera adecuada, pero si lo hacemos así, nos evitaremos muchos quebraderos de cabeza.
One comment