Translate

martes, 4 de junio de 2013

Formateo de números en coma flotante

Como comentamos en la entrada anterior, la función sprintf (y sus variantes) del compilador C18 no tienen soporte para volcar datos de tipo float en coma flotante (modificador %f en ANSI C). La posible razón es que interpretar y volcar números en coma flotante no es trivial, por lo que el código necesario es relativamente grande. La función sprintf ya ocupa bastante código (alrededor de 2K word) y si incluyese soporte para números reales su tamaño posiblemente se doblaría. Al ser una función monolítica, dicho coste adicional lo sufrirían incluso aquellos usuarios que nunca trabajasen con números en coma flotante.


En esta entrada vamos a describir la representación de números en coma flotante dentro del PIC para entender como se guarda la información. A partir de ahí crearemos un par de rutinas que nos permitan volcar este tipo de datos a una cadena.

El objetivo es poder sacar datos por el LCD o puerto serie con los formatos mostrados en la figura adjunta, que corresponden a %e (izquierda) y %f (derecha) del printf definido en ANSI C.



Código asociado a esta entrada: float2ascii.c 


-------------------------------------------------------------------------------------------

Números reales en coma fija:

Tendemos a asociar los números reales a una representación de coma flotante porque así es como se manejan en la mayoría de los ordenadores actuales. Sin embargo una alternativa a usar coma flotante (y que para ciertas aplicaciones puede ser preferible) es usar una representación en coma fija.

Una representación en coma fija no es más que el uso de una representación entera junto con  el acuerdo de que la unidad de dichos enteros no vale 1, sino un cierto valor acordado de antemano (valor del dígito menos significativo). Por ejemplo podemos manejar enteros en base diez y decidir que las unidades valen 0.01. El ordenador guardaría y operaría con enteros pero al final, si el dato almacenado en memoria fuese el 12345, lo presentaríamos como 123.45, ya que la unida del  dígito menos significativo valer 0.01.

Esencialmente se está usando un tipo entero de datos  (con las ventajas que eso conlleva en un micro que carece de un hardware específico de coma flotante). A la hora de enseñar los resultados, simplemente colocaríamos el punto decimal en el lugar adecuado. Como el valor del digito menos significativo está fijado, el puesto del punto (o coma) decimal será siempre el mismo (2ª posición por la derecha en el caso anterior). De ahí el nombre de coma fija que recibe esta representación. La característica principal de una representación en coma fija es que el salto entre sus números es constante (0.01 en el nuestro ejemplo).

Si estamos usando base 10 es trivial ir volcando los dígitos de un número entero y al llegar a la posición de la coma pintar el carácter "." y terminar de volcar los restantes dígitos.

Al estar usando base 2 es un poco más complicado, pero no mucho más. En la entrada anterior vimos como convertir un número entero de base 2 a base 10. Ahora nos planteamos convertir una fracción decimal (en base 2) a base 10. Consideremos un byte compuesto de 8 bits (p.e. 10010101). Obviamente codifica un número entero desde 0 a 255 (en este caso el 149). Pero si decidimos que la coma que separa la parte entera de las fracciones la posicionamos (por ejemplo) separando los 5 primeros bits de los 3 últimos el número binario quedaría:  10010.101

La interpretación de dicho número sería:

Valor
16
8
4
2
1
1/2
1/4
1/8
Posición
b(4)
b(3)
b(2)
b(1)
b0
B(-1)
b(-2)
b(-3)

 1
0
0
1
0
1
0
1

La parte entera es 10010 (18 en decimal). En cuanto a la fracción,  cada bit a la derecha de la coma "decimal" valdrá sucesivamente 1/2, 1/4, etc. de la misma forma que en base 10 los decimales valen 0.1, 0.01, etc. Por lo tanto, el número codificado por el byte 10010101 será:

  Parte entera:  1 x 16 + 0 x 8 + 0 x 4 + 1 x 2 + 0 x 1       =  18
  Fracción      :  1 x 1/2 + 0 x 1/4  + 1 x 1/8  =0.5+0.125  =  0.625            

El resultado es 18.625. Está representación tiene una resolución (separación entre números sucesivos) de 1/8 = 0.125 (el valor del dígito menos significativo). Cada bit vale ahora 0.125, en lugar de 1 unidad (de hecho si hacemos 149 x 0.125 obtenemos 18.625). Obviamente  en un byte sigue habiendo espacio para sólo 256 valores, por lo que lo que ganamos en precisión lo perdemos en rango: la representación sólo puede manejar números desde 0.00 (0/8) hasta 31.875 (255/8).

Se trata ahora de volcar estos números en base 10. La parte entera ya sabemos hacerlo. En el ejemplo anterior bastaría quedarnos con la parte entera (b >> 3) y usar una de las rutinas vistas en la entrada anterior (o sus equivalentes tipo itoa de C18) para crear una cadena ASCII conteniendo un 18. Nos falta un poco de código para volcar la parte fraccionaria (.625).

Como ejemplo del algoritmo a usar, el siguiente código int2frac recibe un entero sin signo de 16 bits (uint16) e interpreta sus 12 bits menos significativos como si fuesen una fracción decimal, volcándola con ndec decimales (base 10)  de la forma '.XXX'. Como un dígito decimal equivale a algo más de 3 dígitos binarios solo tiene sentido presentar un máximo de 3 o 4 (12 bits / 3.algo) decimales.

El código ignora cualquier posible contenido de los 4 bits más significativos. Los pone a 0 al principio y usa ese espacio para ir sacando los sucesivos decimales:

uint8 int2frac(uint16 frac, unsigned char ndec, char* dest)
// The function decodes the 12 least significant bits of a uint16 as a
// decimal fraction and writes it as .xxx in dest[], using ndec decimal figures
// Since one decimal figure equals log2(10)=3.x binary digits, you'd be getting
// garbage past the 3th or 4th decimal figure.
// If ROUNDING is defined, considers rounding and returns 1 if there is a carryover
// into the integer part. Otherwise (or if rounding is not considered) it returns 0.
{
 unsigned char dig,i,p=12;
 uint16 mask;
 uint8 k=0;

 dest[k++]=46; // "."

 mask = 0x0FFF;
 for(i=0;i<ndec;i++)
   {
    frac&=mask;    
    frac*=10;
    dig = (frac>>p);
    dest[k++]=dig+48;
   }
 dest[k]=0;

#ifdef ROUNDING
 frac&=mask;
 dig=0; if(frac>=2048) dig=1;

 i=k-1;
 while(dig && (i>0))
   {
    dest[i]++; if (dest[i]==58) dest[i]=48; else dig=0;
    i--;
   }
 return dig;
#endif

 return 0;
}

La primera parte del código (hasta el #ifdef ROUNDING) va sacando los sucesivos decimales. Es más sencillo que el usado para volcar la parte entera. En primer lugar los decimales se obtienen empezando por el más significativo, por lo que pueden ser volcados según van saliendo. Además, en vez de tener que ir dividiendo por 10, lo que se hace a cada paso es multiplicar por 10, una operación más rápida. 

En la 2ª parte del código, si ROUNDING está definido la función redondea correctamente para mostrar la fracción más cercana con los decimales dados. Por ejemplo, con 2 decimales la fracción 0.2785 se vería como 0.28. Si por el contrario ROUNDING no está definido la fracción se mostraría truncada como 0.27.

El problema de usar redondeo es a veces no sólo afecta al último dígito como sucede en el ejemplo anterior. Si tenemos la fracción 0.2985, tras volcarla como ".29" tendríamos que modificarla a ".30" si se usa redondeo. Mayor problema tendríamos con una fracción como 0.999. Si la truncamos con 2 decimales tendríamos ".99". Si queremos redondearla sería ".00" y deberíamos incrementar por 1 la parte entera (que ya estaba "escrita" en la cadena). Por eso la función anterior devuelve 1 si tras redondear hay un acarreo hacia la parte entera.

En general, salvo que sea imprescindible, se recomienda truncar y no redondear. Los resultados serán un poco menos precisos, pero nos evitaremos mucho trabajo. En el caso anterior (acarreo a la parte entera) tendríamos que sumar uno al número entero. Y eso podría producir sucesivos acarreos (pensar en 99.99). Truncar nunca nos obligará a modificar el trabajo ya hecho, por lo que será mucho más rápido.

Limitaciones de las representación en como fija:

Cualquier intento de representar infinitos números (los reales) con un número finito de casillas va a tener limitaciones. Las limitaciones son de dos tipos:

  • Rango: en la representación anterior (un byte) teníamos un rango de 0 a 32 aprox. Números mayores no podrían manejarse. La solución si necesitamos más rango es aumentar el número de casillas (a int16 o int32).

  • El otro problema es la resolución o salto entre números. Como hemos comentado la  característica principal de una representación en coma fija es que el salto entre sus números es constante (0.01 en el primer ejemplo o 0.125 en el caso de la representación binaria anterior).

Cuando intentamos meter un número real dentro de una cierta representación cometeremos un error. Sea la representación binaria (usando 1 byte) anterior y el número real 12.342. Dicho número cae entre:
         12.250 (01100.010)
         12.375 (01100.011)

Los números anteriores son representados de forma exacta en la representación dada (es lo que se llama números máquina). Pero 12.342 no "está" en la representación. Podremos optar por cambiarlo por alguno de los anteriores (12.250 si truncamos, 12.375 si redondeamos). El caso es que cometeremos un error. El error máximo (cota) posible corresponde a la mitad (si hay redondeo previo) o todo (si truncamos) el intervalo entre números.

Como dicho intervalo es constante, también lo será nuestra cota del error absoluto. Por lo tanto, una representación en coma fija mantiene constante el error absoluto.

El problema es que suele ser más conveniente mantener constante el error relativo que el absoluto. No es lo mismo error de 0.1 si manejamos un número como 1232.76 que si el número es el 0.34. En el primer caso el error relativo cometido al convertirlo a 1234.8 puede ser despreciable (0.003%), pero el en otro caso (0.34 à 0.3) es del orden de un 15%.

Las representaciones en coma fija son adecuadas cuando los números que vayamos a manejar no cubran un rango excesivo. Si los números a usar son más o menos similares, usar errores absolutos o relativos es equivalente. Si el rango de posibles números a manejar es muy grande, es mejor abandonar la representación en  coma fija y pasarnos a coma flotante.
La ventaja de una representación en coma flotante es que mantiene constante el error relativo y puede manejar un rango mucho mayor de números, manteniendo mejor controlado el error.


Números reales en coma flotante:

La representación de un número en una representación en coma flotante basada en la base B consiste en:
·         Signo (+/-)
·         Mantisa positiva m, en el intervalo [1,B
·         Exponente e, positivo o negativo.

Con estos datos, el número representado es (+/-) m x B^e

Una notación en coma flotante es similar a la conocida representación científica donde números como 123 o 0.057 se expresan como 1.23 10^2  o 5.7 10^-2.

En el caso de la notación científica (destinada a nuestro uso) estamos usando B=10. Dentro de un ordenador es mucho más común usar B=2.

El PIC no tiene un soporte nativo para coma flotante, por lo que cada compilador es libre de escoger los parámetros que desee al implementar (por software) su representación. En el caso del compilador C18 se ha escogido la misma representación que se usa en la mayoría de los ordenadores personales, la basada en el standard IEEE 754 cuyos parámetros son:

  • Base B=2
  • 1 bit dedicado al signo.
  • 8 bits dedicados al exponente. El signo se maneja con un sesgo (127). Eso quiere decir que al número (sin signo, de 0 a 255) que se guarde en esos bits debemos restarle 127 para obtener el exponente. De esta forma podemos manejar exponente positivos (para números mayores que 1) y negativos (números menores que la unidad).
  • 24 bits dedicados a codificar la mantisa (obviamente en binario)

Un número en coma flotante ocupa por lo tanto (1+8+23 = 32 bits = 4 bytes)

La razón por la que solo cuento 23 bits para la mantisa es que uno de sus 24 bits no es necesario guardarlo. La mantisa (para garantizar unicidad) debe estar en el intervalo [1,2). Luego debe ser 1.algo. Por lo tanto su primer bit siempre será 1 (no puede ser 0 porque 0.xxx es menor que 1). Y si siempre es 1 no hay que gastar espacio para guardarlo. Es lo que se llama un bit implícito o bit fantasma. Si los bits almacenados son b1,b2,b3,…,b23, la mantisa se interpretará como:

               m =  1.b1 b2 b3 . . . b23  = 1 + b1/2  + b2/4 + . . . +  b23 2^-23

Esto quedará más claro con un ejemplo. El siguiente código crea una variable float x y le asigna el valor x=5.75. A continuación apunto con un puntero tipo uint32 (32 bits) a la dirección donde la variable se almacena (&x) y obtengo el valor almacenado (operador *) en una variable de tipo uint32. Usando la rutina Long2Bin vuelco los 32 bits que codifican al número 5.75 en una cadena de texto

 x=5.75; 
 l = *(uint32*)&x;
 Long2Bin(txt,l);

Si vuelco dicha cadena por el puerto serie o el LCD obtengo el siguiente resultado:

    01000000101110000000000000000000

que podemos separar en:  0  10000001  01110000000000000000000

  • 1 bit de signo: 0 = +
  • 8 bits de exponente:  10000001 = 129  à e = 129-127 = 2
  • 23 bits mantisa: 01110000000000000000000, que interpretaremos (con bit fantasma)
 
            1.01110000000000000000000   =  1 + 0/2 + 1/4 + 1/8 + 1/16 = 1.4375

¿Qué tienen que ver ese exponente y mantisa con el número original? Podemos ver que:

   1.4375 x  2^2  = 1.4375 x 4 = 5.75 = x

La representación usada por C18 es esencialmente la misma que la del standard IEEE 754. (aunque tiene algunas carencias, por ejemplo, la ausencia de números denormalizados). Eso es conveniente porque podemos mandar datos float en binario a través del puerto serie o grabarlos en una tarjeta de memoria y serían directamente legibles desde nuestro PC.

Nota: otros compiladores usan pequeñas variantes de este esquema. Por ejemplo el compilador de MikroCPro usa también este reparto de bits (1 signo, 8 exponente, 23 mantisa) pero por conveniencia altera su orden (8 exp , 1 signo, 23 mantisa). De esta forma el exponente (8 bits) es justo el primer byte de los 4 que forman un float y podemos acceder y operar con él más eficazmente que antes (donde está repartido entre dos bytes). En este caso los floats usados por MikroC Pro no serían directamente exportables a nuestro PC (aunque tienen la misma precisión que el standard, porque el número de bits y su reparto no ha cambiado).

¿Errata en manual de Microchip?: el manual de las librerías del C18 (funciones matemáticas, capítulo 5) parece indicar que su compilador usa la misma organización interna que MikroC Pro en sus variables float: 1er byte dedicado al exponente y 3 bytes dedicados a signo y mantisa (1+23). Incluso suministra un par de rutinas para cambiar de uno a otro formato. Sin embargo, con los datos del ejemplo anterior a mi me parece claro que C18 está usando el orden "correcto" definido por IEEE754.

Todas las rutinas siguientes asumen que C18 usa el orden definido por el standard IEEE754 y a mi me están funcionando adecuadamente.


Volcado de números reales a ASCII:

A las complicaciones de manejar coma flotante se le añade el problema del cambio de base, aunque para ello contamos con la experiencia de las rutinas que vimos en la entrada anterior.

La primera solución que se nos ocurre si queremos volcar datos de tipo float en el PIC es separar parte entera y parte fraccionaria. Por ejemplo, sea el número 1234.5678, que queremos volcar con 2 decimales. El siguiente código:

float x = 1234.56789

i = (int16)x;    // Integer part  = 1234
x-= i;           // Fraction  = 0.5678
x*=100;          //  Fraction x 100 = 56.78
f = (uint16)x;   //  Force to int = 56

extrae la parte entera (i=1234) y fraccionaria (f=56). Luego podemos usar itoa o Word2Dec (vista en la entrada anterior)  para volcar ambos números intercalando un punto decimal entre medias  obteniendo así "1234.56".

¿Qué posibles problemas tiene este enfoque?

  • No hemos redondeado correctamente. 1234.5678 con dos decimales debería aparecer como 1234.57. Esta es un problema menor, que nos perseguirá aunque usemos otros enfoques. Como hemos visto antes, redondear trae algunas pegas.

  • La resta de la parte entera (x-=i) y la multiplicación por 100 (x*=100) son operaciones con floats y por lo tanto lentas.

  • Una pega importante es que si el número hubiese sido 1234.0567 habríamos obtenido i=1234 y f=5 y escribiríamos el resultado como 1234.5 en vez de 1234.05. Se podría arreglar recordando el número de decimales (2) y rellenando con 0's pero es un rollo.

  • Otro problema aparece cuando tenemos números muy grandes o muy pequeños. En general no deseo ver cosas como:

            12345678901230000000.000  (mejor 1.234e20)
            0.0000000000000000001234  (mejor 1.234e-20)

  En otras palabras, desearía tener la posibilidad de usar notación científica al volcar los resultados.


Rutina float2str: similar a usar el descriptor %f en printf (ANSI C).

Nuestro primer objetivo será escribir una rutina que vuelque un número real con un cierto número de decimales (el equivalente del modificador %f en la cadena de formato de printf):

uint8 float2str(char* dest,float x, unsigned short dec)
// Writes float to dest[] with a xxx.yyy format with dec decimal digits
// Works if |x| less than 65535. At most 4 digits after the point.
// For numbers greater than 65536 or less than 0.0001 use float2exp
// that writes real numbers using scientif notation.

Lo primero que tenemos que hacer es acceder al contenido de x interpretándolo como un entero de 32 bits. Luego (sabiendo donde está cada cosa) con operaciones de desplazamiento y AND lógico extraemos los bits correspondientes a signo, exponente y mantisa:

 F = *(uint32*)&x;
 s=0; if (F & 0x80000000) s=1; // Sign bit = MS bit
 E = ((F>>23)&0xFF);           // Exponent bytes (8 bits following sign bit)
 F = F & 0x007FFFFF;  // Mantissa

 if (E>0) F = F | 0x00800000;  // Add phantom bit to mantissa 1.xxxxxxx
 e = (E-0x7F);  // non biased exponent

Las dos últimas líneas añaden el bit fantasma a la mantisa y corrigen el sesgo (127) del exponente. 

Como hemos dicho antes con este formato no me gusta ver números muy grandes ni muy pequeños. Lo primero que hago es ver el número está en el rango adecuado (observando su exponente):

// Number too large: X
 if (e>=16) { dest[0]='X'; dest[1]=0; return 1;}

// Number too small: 0
 if (e<=-16) { dest[0]=48; dest[1]=0; return k;}


Si el exponente (valor absoluto) es mayor que 16 el número se considera fuera de rango y no se sigue procesando. Se muestra un 0 (si |x|<1e-5) o X (|x|>65536) para indicarlo.
Si queremos ampliar el rango de números a mostrar tendríamos que modificar este código.

Una vez decidido que el número está en el rango deseado, pasamos a separar su parte entera (I) y los primeros 12 bits de su mantisa (por lo que estaremos limitados a mostrar unos 3 o 4 decimales):

// Extracts integer part(I)
 p = 23-(int8)e;  //position of "decimal" point when exponent is taken into account
 if (p>24) I=0; else  I=F>>p; // integer part

 // First 12 bits of fraction (F)
 p -=12; if (p>=24) F=0; else if (p>=0) F>>=p; else F<<=(-p);
 F&=0x0FFFL;

Una vez que disponemos de signo (s), parte entera (I) y fracción (F) es sólo cuestión de volcar dichos contenidos en la cadena de destino, usando las rutinas Word2Dec (para parte entera) e int2frac (para la fracción):

 // Write float number: sign + integer part + fraction
 dest[0]=43+(s<<1);  // Sign 43 = +, 45 = -
 k=1; k+=Word2Dec(dest+k,(uint16)I,1);   // Integer
 int2frac(F,dec,dest+k);        // sends 12 MS bit to INt2frac
 k+=(dec+1);

 return(k);

La función devuelve el número de caracteres escritos.

En el código suministrado la función se completa con unos #ifdef que permiten seleccionar si se desea o no redondeo (#define ROUNDING) y si se está usando el formato de floats del compilador MIKROC Pro (#define MIKROE). Por defecto se usa el formato de C18 (IEEE 754).


Descriptor %e en ANSIC sprintf: función float2exp()

Vamos a ver una rutina (float2exp) que vuelca un número real en notación científica, de forma similar al descriptor %e en la función sprintf de ANSI C. Por ejemplo, el número 1234.5678 se presentaría como:

       1.234e+03  1.23e+03, etc.  dependiendo del número de decimales de mantisa elegidos.

Aunque hemos visto que la notación científica y la representación en coma flotante son esencialmente la misma cosa (mantisa, base, exponente), la pega principal es que en el PIC la base usada es 2 mientras que en nuestros volcados querremos usar base 10.

Los parámetros de entrada de la rutina serán el puntero a la cadena de texto donde volcaremos el número, el número a volcar y el número de decimales a usar para la mantisa:

uint8 float2exp(char* dest, float x, uint8 dec)
// Writes the real number x in string starting at *dest,
// Uses using scientific notation: 123456.7890 is written as 1.234e+05.
// The third argument dec are the number of decimal digits used in mantissa.

Empezaremos con la versión más simplificada del código y luego iremos comentaremos algunas complicaciones que podemos considerar  (opcionales a través de #defines):

Al igual que antes, accedemos a los contenidos de x como un entero de 32 bits (uint32) y de ellos extraemos el signo (bit más significativo), bits de exponente (8 siguientes bits) y mantisa (23 bits menos significativos). Los bits de exponente se corrigen por el sesgo para obtener el verdadero valor del exponente. Con la opción #define MIKROE podemos acceder a dichos datos en el formato IEEE754 modificado:

r=0;
#ifdef MIKROE
 if (m & 0x00800000) r=1; // Sign detection (+ -> r=0, - ->  r=1)
 p = (m>>24);            // Extract exponent bits 
#else // C18 format
 if (m & 0x80000000) r=1; // Sign detection (+ -> r=0, - ->  r=1)
 p = ((m>>23)&0xFF);      // Extract exponent bits  
#endif

 m &= 0x007FFFFF;  // Mantissa
 e=p; e-=0x7F;     // corrects exponent bias (127 = 0x7F)

Una vez que contamos con la mantisa (m) y exponente (e) el siguiente código hace el trabajo principal de la rutina, pasando de un número representado como:

      m  2^e    à   x.yyyy 10^e10  con x un digito entre 1 y 9

Es decir, pasamos de mantisa (m) y exponente (e) en base 2 a mantisa y exponente (e10) en base 10. Esencialmente el código va multiplicando/dividiendo por 10 si el número es pequeño/grande hasta conseguir que la mantisa (en base 10) quede en el intervalo [1, 10).

El exponente en base 10 se guarda en la variable e10. Empezamos con e10=0 y se incrementa/decrementa cada vez que se divide o multiplica por 10 el número original.

El código no se ejecuta si los bits del exponente son 0, ya que ese caso singular representa una mantisa = 0 (y por lo tanto el número representado es el 0.0):

// Reduce m 2^e to to x.yyy 10^e10 with leading digit x = 1, 2, .., 9 
 e10=0;
 if (p>0)  // Non 0 number
  {
   m |= 0x00800000;  // Add phantom bit to mantissa 1.xxxxxxx

   cont=0;
   while(e<0)
    {
     m += (m>>2); e+=3; e10--;
     cont++; if (cont==3) {cont=0; m>>=1; e++;}
    }

   cont=0;
   while(e>=3)
    {
     m = m<<2; m=m/5; e-=3; e10++;
     cont++; if (cont==3) {cont=0; m<<=1; e--; }
    }

   if (e>0) m<<=e; else if (e<0) {m>>=(-e);}

   p = m>>23;  // check if integer part is within [1,10). Correct if needed.
   if (p>=10)  {m>>=1; m/=5; e10++; }  // divide by 10, increment e10
   else if (p==0) {m= (m<<1) + (m<<3); e10--; } // multiply by 10, decrement e10
   
   p = m>>23; // Leading digit (1-9)
   m=m&0x007FFFFFL;  // Fraction
  }

Al final de este código el primer dígito de la mantisa (1-9) se guarda en p y la fracción de la mantisa en m. En e10 tenemos el exponente en base 10 (número entero entre -38 y 38) y en r habíamos previamente extraído el signo (0 para +, 1 para -).

Ahora es sólo cuestión de volcar dicha información secuencialmente: signo, primer digito de mantisa, coma decimal, fracción de mantisa, el carácter "e" para  indicar exponente y el valor de e10 (entero con signo volcado con 2 digitos):

 // Writes number to dest[]
 k=0;   // Start at the beginning of char string
 dest[k++]=43+(r<<1); // writes sign ('+'=43 '-'=45)
 dest[k++]=48+p;      // Leading digit of mantissa
 r=int2frac(m>>11,dec,dest+k); // write first 12 bits of mantissa as a decimal fraction

#ifdef ROUNDING   //Correct if using rounding
 if (r) { if (p==9) { dest[1]=49; e10++;} else dest[1]++;}
#endif

 k+=(dec+1);  // End of string so far

 // Write exponent
 dest[k++]='e'
 if (e10<0) { dest[k++]='-'; e10=-e10;} else dest[k++]='+'// exponent sign

 // absolute value of exponent (2 digits with leading 0 if needed)
 p=e10/10; dest[k++]=48+p;
 p*=10; p=e10-p; dest[k++]=48+p;

 dest[k]=0; // Null-terminate the string

 return k;  // return numbers of chars written (length of string)

El código puede complicarse añadiendo una opción de redondeo (con un #define ROUNDING):

#ifdef ROUNDING
 if (r) { if (p==9) { dest[1]=49; e10++;} else dest[1]++;}
#endif

Este código se ejecutaría tras la llamada a int2frac (que devuelve r). Si r es 1 ha habido un acarreo que ha llegado a la parte entera. Si dicha parte entera era menor que 9 simplemente se incrementa. Si era 9, pasaría a 10, por lo que la mantisa pasa a ser 1.0 y el exponente e10 se incrementa.


Excepciones: NaN y Inf:

La representación IEEE754 tiene algunos casos que codifican excepciones, como Inf (infinito) para operaciones del tipo 1/0 o NaN (Not a Number) para operaciones no definidas (como 0/0). Dichos "números" se codifican poniendo a 1 todos los bits del exponente = 0xFF. En ese caso, si la mantisa es 0, tenemos un Inf y si no lo es, un NaN.

El código anterior interpretaría incorrectamente dichos números. Si pensamos que podemos obtener este tipo de números (NaN o Inf) y queremos que aparezcan correctamente, tendríamos que hacer un #define NaN_Inf y el siguiente código se incorporaría a la función:

#ifdef NaN_Inf
 if (p==0xFF) // NaN or Inf
  {
   k=0;
   if (m==0) // show '+Inf' or '-Inf'
    {
     dest[k++]=43+(r<<1); dest[k++]=73; dest[k++]=110; dest[k++]=102;}
   else     // show 'NaN'
    {
     dest[k++]=78; dest[k++]=97; dest[k++]=78; 
    }
   dest[k++]=0;
   return k;
 }
#endif

Como ejemplo del uso de esta opción, el siguiente código (si NaN_Inf está definido):

 x=0.0/0.0;  float2exp(txt,x,3); send_txt(txt); send_CR; send_LF;
 x=1.0/0.0;  float2exp(txt,x,3); send_txt(txt); send_CR; send_LF;
 x=-1.0/0.0; float2exp(txt,x,3); send_txt(txt); send_CR; send_LF;

produce por el puerto serie la siguiente salida:   NaN
                                        +Inf
                                        -Inf


Tiempos de ejecución y tamaño del código:

En cuanto a tiempos y tamaño de código la siguiente tabla recoge el tamaño y tiempo de ejecución de las dos funciones consideradas:
  
Rutina
Código (words)
Tiempo (usec)
float2str
470
150-300
float2exp
820
500





Ambas funciones usan por debajo la función int2frac, lo que añade 150 palabras de código en ambos casos. La función float2str usa además la función Word2Dec (250 palabras más), por lo que el código total necesario para usar float2str es de unas 870 palabras frente a unas 970 para float2exp.

El tiempo de ejecución de float2exp es de medio milisegundo (@ 4x8 = 32 MHz), lo que puede parecer elevado, hasta que consideramos que el objeto de generar estas cadenas es que un usuario humano las vea en el LCD o puerto serie. Por ejemplo, la cadena típica generada por float2exp es de unos 12 caracteres. Incluso a velocidades como 57600 bauds, tardaríamos unos 2 msec en mandarla por el puerto serie, unas 4 veces el tiempo empleado en crearla.


Como chequeo final de las rutinas escritas, ejecutamos el siguiente código:

 x=3e-38;
 while(x<1e38)
  {
   k=Long2Bin(txt,*(uint32*)&x);
   txt[k++]=32;
   k+=float2exp(txt+k,x,3);
   txt[k++]=' ';  txt[k++]='='
   txt[k++]=' ';
   k+=float2str(txt+k,x,3);
   send_txt(txt); send_CR; send_LF;
   x*=200;
  }

El código barre el intervalo de los números reales (1e-38 a 1e+38). En cada paso multiplicamos  el número por un factor 200 y mostramos los 32 bits que se guardan en la variable. El número es interpretado con el formato %e y con %f. El resultado es el mostrado en la figura adjunta.



Notad el rango mucho más pequeño (de 0.001 a 65535) donde la función float2str es "operativa", frente al código alternativo de float2exp.


No hay comentarios:

Publicar un comentario