2022-02-05
Problemas con la función sprintf en Arduino.
El problema
Realizando un nuevo autómata, el cual incorpora un display para mostrar información he utilizado la función sprintf para "formatear" la salida con un resultado inesperado.
El código utilizado era este:
char s[18]; // Cadena que tendrá el formato. uint32_t t; // Variable de tiempo. char ch; // Caracter. const char *cadenas[8]= { "NOATAC", "NINGUNO", "BUSCANDO", "ACS", "ACB", "SUBIENDO", "BAJANDO", "ESPERANDO" }; void setup() { // Código de inicialización. } void loop() { // Otro código. // Obtengo el tiempo que le falta al temporizador para que haya // transcurrido el tiempo. Lo obtengo en segundos. t = ( total - ( millis()-temporizador ) ) / 1000; // Si el tiempo es mayor de 60 segundos, lo convierto en minutos // y el carácter será 'm' (minutos). Si no el tiempo se queda // como está y el carácter será 's' (segundos). if ( t > 60 ) { t = t/60; ch = 'm'; } else { t = s; } // Formateo la cadena y lo muestro en el display. sprintf(s, "1:%9s(%02d%c", cadenas[estado], t, ch); lcd.print(s); }
Si faltan 34 segundos para que el temporizador termine y esta en el estado "ESPERANDO" la salida en el display debería ser:
1:ESPERANDO(34s)
Sin embargo la salida obtenida es:
1:ESPERANDO(34
La cadena se trunca y no se muestra correctamente. Es como si después de escribir el número añadiera un "0" a la cadena dándola por terminada.
Pruebas y solución.
Lo primero que pensé era que la opción de carácter "%c" de la función sprintf no funcionaba así que hice un pequeño programa de pruebas y fui añadiendo líneas de código mostrando poco a poco los valores.
// Esta es la cadena donde vamos a almacenar los datos. char s[32]; // Esta es la variable de tiempo. uint32_t t = 34; // Este es el caracter. char ch = 's'; // Cadena de texto. char *p = "ESPERANDO"; void setup() { Serial.begin(9600); // Comprobamos la salida del caracter. sprintf(s, "1: %c", ch); Serial.println(s); // Comprobamos la salida de la cadena. sprintf(s, "2: %s", p); Serial.println(s); // Comprobamos la salida del entero largo. sprintf(s, "3: %d", t); Serial.println(s); // Comprobamos la salida de la cadena con el entero largo. sprintf(s, "4: %s-%d-", p, t); Serial.println(s); // Comprobamos la salida con la cadena y el carácter. sprintf(s, "5: %s-%c", p, ch); Serial.println(s); // Comprobamos la salida con el entero largo y el carácter. sprintf(s, "6: %d-%c", t, ch); Serial.println(s); sprintf(s, "7: %s-%d-%c", p, t, ch); Serial.println(s); } void loop() { }
La salida es este programa es la siguiente:
1: s 2: ESPERANDO 3: 34 4: ESPERANDO-34- 5: ESPERANDO-s 6: 34- 7: ESPERANDO-34-
Como observamos todo va correcto hasta la línea 6 donde mostramos el tiempo y el carácter. La línea 7 es casi la cadena formateada que deseamos e igualmente falla.
Al principio pensé que era un problema con el tamaño de la cadena "s" y por eso aumente su tamaño. Aunque estaba calculado para caber en una línea de LCD y por lo tanto no debía superar los 16 caracteres.
También probé con una versión más reciente del IDE de Arduino por si era un fallo de la versión con la que trabajo. Pero no, el resultado era el mismo. Aparentemente el "%c" funciona correctamente en cualquier versión.
Al final observe que el tiempo está expresado en enteros largos (32bits) y que en el formateo de la cadena estaba usando enteros (16 bits). Aquí ocurre algo interesante, lo lógico es pensar que el compilador coge ese entero y lo trunca y sigue con el siguiente parámetro.
Nada mas lejos de la realidad. La función sprintf al tener una lista de parámetro variables espera una especie de puntero apuntado a un array con los parámetros. Según el tipo que le indicamos en la cadena de formateo "lee" los bytes que se necesitan. Si el tipo es un entero solo leerá 16 bits, si nosotros le pasamos un entero de 32bits el siguiente parámetro será leído en los 16 bits que nos restan del entero largo. En este caso "34" es muy pequeño y la parte alta de esté serán ceros. De ahí que se trunque el valor.
La solución pasa por usar una conversión o en la cadena de formateo o en el parámetro, si este parámetro lo permite (que se usen menos de 16 bits).
// Opción 1: sprintf(s, "%s %ld %c", cadena, enterolargo, caracter); // Opcion 2: sprintf(s, "%s %d %c", cadena, (int)enterolargo, caracter);
Moraleja.
Cuando utilicemos la función sprintf debemos tener cuidado con los tipos de los parámetros, además de olvidarnos de usar floats.