En muchos momentos (presentación de resultados, debug)
tendremos que visualizar valores de la variables de nuestro programa por el
LCD, puerto serie o equivalente. Ello conlleva convertir los datos numéricos en
cadenas de texto mostrando sus contenidos, bien sea en formato binario,
hexadecimal, decimal, etc.
El compilador C18 (y otros basados en ANSI C) ya cuentan
con una forma rápida y versatil de hacer dicha conversión: con las rutinas tipo
printf, sprintf o equivalentes.
Estas funciones son muy flexibles y convenientes pero como
veremos pueden ser lentas y ocupar bastante espacio. Son ideales por su
conveniencia en las primeras fases de un desarrollo. Sin embargo, si la aplicación se va complicando y nos acercamos
a los límites del micro usado podemos encontrarnos con que ocupan demasiada
memoria de programa y/o van demasiado lentas.
En esta entrada replicaremos la funcionalidad de sprintf
de forma modular. Escribiremos varias rutinas para volcar diferentes datos
enteros con varios formatos (binario, hexadecimal, decimal con o sin signo, etc).
Estas funciones, al preocuparse de un solo argumento y tipo de formato van a
ser mucho más pequeñas y rápidas que sprintf. Además, muchas veces, dentro de
una aplicación, solo trabajaremos con un cierto tipo de datos, por lo que este
tipo de rutinas pueden ahorrarnos bastante código.
En una entrada posterior trataremos el tema de la
presentación de datos tipo float, del que carecemos por defecto en C18.
---------------------------------------------------------------------------------------------
Origen de los inconvenientes de
sprintf:
Recordemos el uso de sprintf, cuyo resultado es una cadena
de texto formateada y lista para ser visualizada. Una típica llamada a sprintf
sería:
n=sprintf(dest,"format",arg1,arg2,…)
- dest[] puntero a char para almacenar cadena de
carácteres de salida. La cadena obtenida será null-terminated, esto es, el
valor 0 indica su final.
- "format": Una combinación de texto explicatorio +
descriptores (%d,%u,%b, etc) sobre como sacar los datos, en binario, hex,
decimal, con o sin signo, etc.
- arg1,
arg2,… datos que se convierten al formato indicado por los correspondiente
descriptores.
- El argumento de salida n indica la longitud de la cadena creada.
Los inconvenientes de sprintf y similares residen
precisamente en su flexibilidad:
1) Código muy grande: si compilamos
un programa añadiendo sprintf veremos que su tamaño aumenta en unacis 2000+ palabras, independientemente del tipo de formateo usado. La razón es que sprintf debe ser capaz de manejar cualquier tipo de formato que el usuario le presente en tiempo de ejecución, por lo que su código debe cubrir todas las posibilidades (volcar digitos binarios, hex, demales, con o sin signo, etc)
aunque el usuario solo desee una de ellas.
2) Lento en ejecución por la misma
razón de antes. La rutina debe parsear la cadena de formato y decidir que tipo
de formato desea el usuario antes de ejecutar el código correspondiente. La
forma de especificar el formato es muy abierta y tiene muchas posibilidades
(tipo de datos, número de dígitos, rellenar o no con 0's, etc) por lo que
tardará en parsearla.
3) En el caso de C18 estas funciones
no tienen soporte para volcar datos de tipo float en coma flotante (modificador
%f en ANSI C). La razón enlaza con la pega 1) de antes. Interpretar y volcar
números en coma flotante no es trivial, por lo que el código necesario es
relativamente grande. Al ser sprintf una función monolítica, su tamaño
aumentaría aún más, incluso para aquellos usuarios que nunca trabajan con
números en coma flotante.
Vamos a presentar algunas funciones alternativas a sprinft
que reproducen la mayoría de su funcionalidad con una fracción del coste en
tiempo y memoria. En esta entrada nos centraremos en como presentar enteros
(binario, hexadecimal y decimal con/sin signo) y en la siguiente exploraremos
los números en coma flotante.
Todas las funciones que veremos (igual que el sprintf que
reemplazan) reciben como primer argumento un puntero a carácter. Es
responsabilidad del usuario asegurar que hay espacio suficiente para almacenar
la cadena resultante (incluido el terminador 0). También devuelven la longitud
de la cadena creada.
Enteros sin signo a binario:
Llamadas equivalente usando sprintf:
Byte2Bin(dest,a)
|
sprintf(dest,"%b",a)
|
Word2Bin(dest,a)
|
sprintf(dest,"%b",a)
|
Long2Bin(dest,a)
|
sprintf(dest,"%lb",a)
|
La primera rutina Byte2Bin vuelca un byte (uint8) a
binario: se inicializan los 8 caractéres (bits) con 0's (ASCII 48) y se termina
la cadena con 0. A continuación se recorren los bits del 2º argumento: si es 1
se incrementa ('1'=49) la posición correspondiente en la cadena. Para volcar un
uint16 o uint32, apuntamos con un puntero de bytes al inicio de la variable. De
esta forma podemos acceder a los bytes individuales y llamar repetidamente a
Byte2Bin.
// uint8 (byte) to
binary. dest[] must be at least 8+1 chars long
uint8 Byte2Bin(char* dest,uint8 a)
{
uint8 k;
for(k=0;k<8;k++) dest[k]=48; dest[8]=0; // Fill with 0's
k=7; while(a) { if (a&0x01) dest[k]++; a>>=1; k--;} //
Add 1 if needed
return 8;
// always 8 chars written
}
// uint16 to binary.
dest[] must be at least 16+1 chars long
uint8 Word2Bin(char* dest,uint16 a)
{
uint8 k;
uint8* ptr=(uint8*)&a; //
Access to individual bytes in integer
for(k=0;k<2;k++) Byte2Bin(dest+(k<<3),ptr[1-k]);
// Call Byte2Bin twice
return 16; // always
16 chars written
}
// uint32 to binary.
dest[] must be at least 32+1 chars long
uint8 Long2Bin(char* dest,uint32 a)
{
uint8 k;
uint8* ptr=(uint8*)&a; // Access to individual bytes in long
for(k=0;k<4;k++) Byte2Bin(dest+(k<<3),ptr[3-k]);
return 32; // always 32 chars written
}
En la siguiente tabla comparamos el tamaño del código y
tiempos de ejecución de estas rutinas con las equivalentes usando sprintf.
Recordar que el código para sprintf es siempre de unos 2K words
independientemente del tipo de datos usados.
Rutina
|
Código (words)
|
Tiempo (usec)
|
Tiempo sprintf
|
Byte2Bin
|
75
|
50
|
1500
|
Word2Bin
|
+75
|
100
|
2500
|
Long2Bin
|
+75
|
150-250
|
6-7 msec
|
Enteros sin signo a hexadecimal:
Llamadas equivalente usando sprintf:
Byte2Hex(dest,a)
|
Sprintf(dest,"%02x",a)
|
Word2Hex(dest,a)
|
Sprintf(dest,"%04x",a)
|
Long2Hex(dest,a)
|
Sprintf(dest,"%08x",a)
|
El proceso es muy similar al caso binario, ya que cada
dígito en base 16 no es más que un bloque de 4 dígitos binarios. Repetimos el
proceso de antes pero examinando bloques de 4 bits (un AND con 0x0F en vez de
0x01 y un SHIFT de 4 posiciones en vez de 1 a cada paso).
Como antes la función básica corresponde al volcado de un
byte:
// Byte to hex. dest[] must be at least 2 chars long
uint8 Byte2Hex(char* dest,uint8 a)
{
unsigned char dig,k;
dest[0]=48; dest[1]=48; dest[2]=0; //
Fill with 0's and null-terminated
k=1;
while(a)
{
dig=(a&0x0F); if (dig>9) dig+=7;
dest[k--]+=dig;
a>>=4;
}
return 2; // Always two chars written
}
La cadena
se inicializa con 00 (ASCII 48) y se va examinando cada dígito hexadecimal (AND
0x0F). Valores entre 0 y 9, incrementan por dicha cantidad el correspondiente
código ASCII. Para valores entre 10 y 15 añadimos 7 para obtener los caracteres
'A','B', etc.
Al igual
que en el caso binario, las rutinas para uint16 y uint32 llaman repetidamente a
la anterior para volcar los 4 u 8 dígitos hexadecimales.
En la
siguiente tabla se compara el tamaño del código y tiempos de ejecución de estas
rutinas con los tiempos para sprintf:
Rutina
|
Código (words)
|
Tiempo (usec)
|
Tiempo sprintf
|
Byte2Hex
|
95
|
20
|
500
|
Word2Hex
|
120
|
35
|
500
|
Long2Hex
|
140
|
70
|
500
|
El código
para Byte2Hex son unas 100 palabras. Cada rutina adicional necesita unos 60
palabras (el + indica que también precisan la rutina Byte2Hex). El tiempo de ejecución de estas rutinas es
variable según el tamaño del dato en cuestión (menor cuantos menos dígitos
tiene). Como regla general se requieren unos 15 usec por cada dígito
adicional.
Enteros sin signo a decimal:
Ahora las
cosas se complican un poco. Volcar datos en base 2 o base 16 era relativamente
rápido porque base 2 es la base nativa de los datos en un PIC (y base 16 solo
es cuestión de examinar bloques de 4 bits).
Para
beneficio de los usuarios humanos, en la mayoría de los casos querremos volcar
los datos en base 10. Eso implica un cambio de base, por lo que estas rutinas
serán ligeramente más complicadas y lentas que las anteriores.
Dado un
número a, el algoritmo básico para ir obteniendo sus dígitos (base 10) desde el
menos al más significativo es el siguiente:
while(a)
{
b =
a/10; // Division entera
dig = a – b*10;
// remainder = least significant digit
a=b; // repeat for next digit
}
Se va
dividiendo a por 10 y el resto obtenido es el dígito buscado. El problema es
que se empieza por el menos significativo (unidades) y no se sabe a priori
cuantos tendremos. Por ejemplo un byte puede tener 1 (0-9), 2 (10-99) o 3
dígitos (100-255).
La
solución es reservar el número máximo de dígitos posibles (3 para un byte, 5
para un entero) e ir llenándolos de derecha a izquierda. Al acabar sabremos el
número de dígitos que han salido y podremos justificarlos a la izquierda o
dejarlos tal cual, rellenando con blancos o ceros a la izquierda:
// Byte (unsigned
int8) dumped as decimal (right justified and padded with 0's)
uint8 Byte2Dec(char* dest,uint8 a)
{
uint8 k,b,dig;
for(k=0;k<3;k++) dest[k]=48; dest[3]=0; // Fill with 0's and
null-terminate
k=2; // Starts with LS digit
while(a>=10)
{
b=a/10; dig = a-10*b;
dest[k--]+=dig;
a=b;
}
dest[k]=a+48;
// Leading digit
return 3; // Always
3 chars long (000, 001, , 255)
}
En este
caso se ha preferido mantener la justificación a la derecha, por lo que las
posibles salidas siempre tienen 3 digitos, desde 000 hasta 255.
En el
siguiente caso (enteros de 16 bits o uint16) se ilustra como justificar a la
izquierda. Un tercer argumento decide si correr el resultado a la izquierda (1)
o dejarlo tal cual (0).
uint8 Word2Dec(char* dest, uint16
a,uint8 left)
{
uint8 k,i,dig,n;
uint16 b;
k=4; // At most 5 digits, dest[0],dest[1],..., dest[4]
while(a>=10)
{
b=a/10; dig = a-10*b;
dest[k--]=dig+48;
a=b;
}
dest[k]=a+48;
n=5-k; // Number of digits used
if (left)
{
if (n<5) for(i=0;i<n;i++) dest[i]=dest[i+k]; //
Shift number left
}
else
{
for (i=0;i<k;i++) dest[i]=48; //
Right justified, padded with 0's
n=5;
}
dest[n]=0;
//Null terminate
return n;
}
En la segunda parte del código (si se ha elegido
justificación a la izquierda y el número resultante tiene menos de 5 cifras) se
desplazan los dígitos obtenidos hacia la izquierda. En caso contrario se
rellenan las posiciones vacías con 0's (ASCII 48). Cambiándolo por dest[i]=32,
rellenaríamos con espacios. Fijaros que el código de sprintf debe considerar
todas esas posibilidades a través de su cadena de formato, lo que complica su
código.
Enteros con signo a decimal:
Remarcar que en este caso, C18 si que tiene unas rutinas
para volcar enteros con signo a decimal sin usar sprintf (btoa, itoa, ltoa). Son
más compactas y tan rápidas como las aquí presentadas, por lo que en este caso
serían preferibles a las mías.
A pesar de esto, por completitud, presentare las
correspondiente rutinas, que no son más que las usadas en el apartado anterior,
añadiendo el manejo del signo.
La idea es detectar el signo y obtener el valor absoluto
del entero. Si el signo es negativo añadimos el carácter '-' a la cadena de
salida, para posteriormente volcar el valor absoluto.
En el PIC, los enteros con signo están codificados en
complemento a dos (ver tabla adjunta):
Numero binario
|
Interpretacion
|
0000 0000
|
0
|
0000 0001
|
1
|
. . .
|
. . .
|
0111 1111
|
127
|
1000 0000
|
-128
|
1000 0001
|
-127
|
…
|
…
|
1111 1110
|
-2
|
1111 1111
|
-1
|
El bit más significativo nos indica el signo (1 para números
negativos). Para hallar el valor absoluto de un número negativo basta negar el
byte (invertir sus bits) y sumar 1 al resultado. Así por ejemplo vemos que 1000
0001 (-127) sería negativo al ver primer bit = 1. Invirtiéndolo obtenemos 0111 1110 = 126. Sumando 1 tenemos
su valor absoluto (127).
Aprovechando podemos escribir una rutina (abs) que
devuelva el valor absoluto de un entero y que no encontramos en C18:
uint16 abs(int16 a)
{
uint8 s;
s = (a>>15); if (s) {a=~a; a++; }
return a;
}
Las rutinas siguiente convierten enteros con signo de 8 y 16
bits (el código para 32 bits es similar). Para valores positivos el signo '+'
se omite. Sería trivial modificar el código para que el carácter del signo (+ o
-) siempre acompañe al número.
// signed int8 to
decimal
uint8 SignedByte2Dec(char* dest,int8 a)
{
uint8 k,s;
k=0;
s = (a>>7); if (s) { a=~a; a++; dest[k++]='-'; } //
Get sign and absolute value
k+=Byte2Dec(dest+k,a);
return k;
}
// signed int16 to
decimal (left=1, left justified)
uint8 SignedWord2Dec(char* dest, int16
a,uint8 left)
{
uint8 k,s;
k=0;
s = (a>>15); if (s) { a=~a; a++; dest[k++]='-'; } //
Get sign and absolute value
k+=Word2Dec(dest+k,(uint16)a,left);
return k;
}
El código anterior ilustra también como concatenar varias
cadenas obtenidas sucesivamente con estas rutinas.
El equivalente a: k=sprintf(dest,"%u %x %d",a,b,c);
sería:
k=Word2Dec(dest,a);k+=Word2Hex(dest+k,b);k+=SignedWord2Dec(dest+k,c);
k=Word2Dec(dest,a);k+=Word2Hex(dest+k,b);k+=SignedWord2Dec(dest+k,c);
De nuevo los tiempos dependen del número de digitos (cuanto
antes se "acabe" el número, antes dejaremos de tener que hacer
costosas divisiones). Por ejemplo los tiempos citados para la conversión de
enteros de 16 bits (words) son para 4 o 5 cifras (max):
Rutina
|
Código
(words)
|
Tiempo
(usec)
|
Tiempo
sprintf
|
Byte2Dec
|
200
|
40-60
|
500-700
|
+ signo
|
+75
|
idem
|
Idem
|
Word2Dec
|
251
|
180-220
|
800-1200
|
+ signo
|
+95
|
idem
|
Idem
|
El siguiente código muestra la funcionalidad de las
rutinas presentadas. Un entero (uint16 sin signo) se va incrementando y a cada
paso se presenta en formato binario, hexadecimal, decimal sin signo y decimal
con signo:
i=0;
while(1)
{
k=Word2Bin(txt,i); txt[k++]=32;
k+=Word2Hex(txt+k,i); txt[k++]=32;
k+=Word2Dec(txt+k,i,0); txt[k++]=32;
k+=SignedWord2Dec(txt+k,i,0);
send_txt(txt);
send_CR; send_LF;
i++;
}
hola antonio a donde te puedo localizar para un proyecto jose-al682@hotmail.com
ResponderEliminar