Translate

lunes, 3 de junio de 2013

Formateo de datos (para LCD o puerto serie)

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.

Código asociado a esta entrada: int2ascii.c 




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

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

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++;
  }


1 comentario:

  1. hola antonio a donde te puedo localizar para un proyecto jose-al682@hotmail.com

    ResponderEliminar