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.

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.
-------------------------------------------------------------------------------------------
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:
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