I'm having trouble with sprintf

Hi All

I'm having trouble with sprintf and I suspect my formatting is incorrect

I'm using a Mega 2560

So I have this function

String Create_Int32_to_String(int32_t Num) {

  char buffer[15];

  for (uint8_t i = 0; i < 15; i++) {

    buffer[i] = "";

  }

  int32_t Top_Part = abs(Num) / 10000;

  int32_t Lower_Part = abs(Num) % 10000;


  Serial.print("Top Part : ");

  Serial.print(Top_Part);

  Serial.print("  Lower Part : ");

  Serial.println(Lower_Part);


  sprintf(buffer, "%s%d.%04ld", Num < 0 ? "-" : "", Top_Part, Lower_Part);

  return String(buffer);

}

I can confirm that the Top Part & Lower Part report the correct values (via serial monitor)

However the sprintf returns the wrong values

for example - with the int32_t set as -344397 it should return -34.4397 but it returns -34.323551232

To give context, the result of the function is used as - LCD_4_Line.print(Create_Int32_to_String(GPS_Display_Data.Original_latitude));

As always any help given will be very much appreciated and if someone can show me a better way to do this even better

Kind Regards Grant Brown

  1. Always turn the compiler warning level to ALL. That alone catches mountains of "what the !#^%!^% was I thinking when I wrote that?"
  2. Don't assign strings to chars. In fact, in the this case, leave it alone.
  3. Use snprintf, not sprintf.
  4. If you're using int32_t, use %ld, not %d.
  5. Profit.
void setup() {
   Serial.begin(115200);
   Serial.println(Create_Int32_to_String(-344397));
}

void loop() {}

String Create_Int32_to_String(int32_t Num) {
   char buffer[15];
   int32_t Top_Part = abs(Num) / 10000;
   int32_t Lower_Part = abs(Num) % 10000;

   Serial.print("Top Part : ");
   Serial.print(Top_Part);
   Serial.print("  Lower Part : ");
   Serial.println(Lower_Part);

   snprintf(buffer, sizeof buffer, "%s%ld.%04ld", Num < 0 ? "-" : "", Top_Part, Lower_Part);
   return String(buffer);
}

Results:

Top Part : 34  Lower Part : 4397
-34.4397

PS String will eventually bite you.

+1 on that.

You've got some math with uninteresting numbers like 10000, with one case where it simply fails (on AVR at least). When printing the whole int32_t, it's already almost what you want, except for that decimal point; so just insert it.

Notably, the tricky part is still with the printf format specifier. In particular, leading zeroes apply to the width; if the number is negative, the sign "counts as one". With four digits to the right and at least one to the left of the decimal point, negative numbers need a width of 6 instead of 5.

bool scale4String(char *buffer, size_t len, long num) {
  int would = snprintf(buffer, len, num < 0 ? "%06ld" : "%05ld", num);
  auto width = static_cast<size_t>(would);
  if (would < 0 || width + 1 >= len) {
    return false;
  }

  char *p = buffer + width - 4;
  memmove(p + 1, p, 4 + 1);
  *p = '.';
  return true;
}

String Create_Int32_to_String(int32_t Num) {
   char buffer[15];
   int32_t Top_Part = abs(Num) / 10000;
   int32_t Lower_Part = abs(Num) % 10000;

   snprintf(buffer, sizeof buffer, "%s%ld.%04ld", Num < 0 ? "-" : "", Top_Part, Lower_Part);
   return String(buffer);
}

void tryBoth(int32_t n) {
  char buffer[15];
  Serial.print(n);
  Serial.print('\t');
  Serial.print(scale4String(buffer, sizeof(buffer), n) ? buffer : "scale4String failed");
  Serial.print('\t');
  Serial.println(Create_Int32_to_String(n));
}

void setup() {
  Serial.begin(115200);
}

int32_t tryThese[]{ 0, -1, 210, -321, 43210, -54321, -344397, INT32_MIN, INT32_MAX };

void loop() {
  static unsigned i;
  if (i < sizeof(tryThese) / sizeof(tryThese[0])) {
    tryBoth(tryThese[i++]);
  }
}

Instead of dynamic String, use a char buffer that you control. The output is

0	0.0000	0.0000
-1	-0.0001	-0.0001
210	0.0210	0.0210
-321	-0.0321	-0.0321
43210	4.3210	4.3210
-54321	-5.4321	-5.4321
-344397	-34.4397	-34.4397
-2147483648	-214748.3648	--214748.-3648
2147483647	214748.3647	214748.3647

Note the difference with INT32_MIN

At least the 2560 has RAM to waste. In time the OP may learn ways to cut that down. One of the things that small Arduino teaches once you get far is saving RAM!

Here are progressive links getting down to stdlib.h with sprintf and all the format notes.

Here is where to find AVR LibC, the Standard C Library Arduino uses.

This is the Online User's Manual.....

The main docs page, all those library docs.

And here is stdlib.h docs!

The Standard Library uses char array C strings, NOT C++ String Objects that can't be put or used from flash where C strings can be stored and used RAM-free.

On Arduinos it is better to never practice dynamic allocation which includes using any Container Class like String as well as new and delete. Using new is fine but deallocating and allocating new space will take utmost care and always suspect .... where working out a design that doesn't do that and maybe stores format strings, labels and prompts to flash can give you more room and certainty.

In Arduino Standard Functions, the ones that start with P_ are the flash-based functions, P is for PROGMEM, an Arduino C Standard Library in itself.

C++ String objects can't be stored in PROGMEM without breaking. Leave those for big memory PC environments, Arduino is a whole different world.

This whole business of buffering up long strings to print out wastes RAM. You already have a 64-char Serial output buffer that Serial.print() fills and transmits just fine. Multiple print lines can format output into the existing buffer and make allocating a buffer just for that string (2 buffers if 1 is for a format string) redundantly wasteful.

"if you want the rest of your sketch to keep running while you output results or debugging messages, you should not use any of the Serial print/write methods in your loop() code."

see Arduino Serial I/O for the Real World for why you need print buffering.

what about using dtostrf()

         1234      0.1234
        12345      1.2345
       123456     12.3456
      1234567    123.4567
     12345678   1234.5677
    123456789  12345.6800

const int32_t arr [] = { 1234, 12345, 123456, 1234567, 12345678, 123456789 };
const int     Narr   = sizeof(arr) /sizeof(int32_t);

char s [90];

// -----------------------------------------------------------------------------
void setup ()
{
    Serial.begin (9600);

    for (int n = 0; n < Narr; n++)  {
        char  t [20];
        float f = arr [n] /10000.0;

        dtostrf (f, 10, 4, t);

        sprintf (s, " %12ld  %s", arr [n], t);
        Serial.println (s);
    }
}

void loop () { }

dtostrf() certainly works, but does drag in a bit of code when you use it.

a7

Only about 7 significant digits with float, and AVR doesn't have double. If the magnitude of the top/left part is small enough, math and logic are more obvious.

don't know what that means.

i felt that the limitation would be fine if these were lat/long values

I would suggest that Serial.begin() be set to a high baud rate, at least 115200 if the channel permits just to empty the output buffer quicker.... and still planning needs to done to Prevent EVER Filling that buffer! It just allows more room to breathe.

If you change the label on your values, you won't need decimal points or floating point.

I can write 3.3V or I can write 3300mV.

OP has not said why they are using this fixed-point scheme, like storing money as cents instead of dollars. That's two decimal places; they're using four.

If it's four digits on each side -- left/right, or as they called it, top/lower -- then seven significant digits is not enough to be accurate. (Does that matter? I don't know.)

Fifteen digits with double would work, and all the logic to handle the sign and leading zeroes is "included"; instead of having to figure out printf specifiers. But no double on AVR like Mega 2560: it is implemented same as float.

static_assert(sizeof(float) != sizeof(double));

void setup() {}

void loop() {}

If we Work with tiny unit integers, we can print decimal points for the humans and still be faster than using floats (except in cases where only power of tens changes) and get 19+ places (types long and long long).

There is out there an incredibly fast div/5 for AVRs that I would use to do the decimal conversion, 1 right shift and /5 per digit out.

IIRC x386 has an FPU and before that, knowing how to work with integers was key to fast running lots of calculations and getting better precision as well unless you bought the Israeli-only-made FPU for your XP or AT. Gold was cheaper.

If you're doing something like an automotive GPS (or a model rocket tracker, as per another thread), the resolution of a float isn't quite fine enough to match the desired resolution in the real world, especially if you're using something like Haversine calculations to figure the distance between two "close" coordinates.
eg one corner of "Stanford Shopping center" is at 37.446074, -122.171062 and the other is at 37.439791, -122.171268. Those longitudes are identical to 6 digits, so if they were 32bit floats, you've thrown away most of the significant digits. The latitudes only lose about 3 digits...

x86 didn't get a built-in FPU until the 486dx (and at that time, you could still get the cheaper 486sx without an FPU.)

Huh? Intel sold 8087 (for 8088 and 8086), 80287, and 80387 float co-processor chips. As did a bunch of other people.
In mid-1984, an 8087 cost about $200 (in the same timeframe, an 8088 IBM PC with 256K RAM, 360KB floppy, Video, and 10MB Hard Disk was about $4k, so that wasn't TOO expensive, relatively speaking. An 8087 supported 64bit floats, but did internal calculations with 80bit values, IIRC. And the whole "co-processor interface" of the x86 was pretty neat.

Yeth. I've just learned that the 8087 was all tangled up with what eventually became IEEE 754, the standard for floating point.

Worth a google or wikipedia or AI dive accordian to one's personal preferences.

a7

I kinda' skimmed over this thread because I've been embroiled in this very subject for awhile. Global positions and GPS code. In the end, storing the lat/lon as int32_t(s) is what I choose. Is this agreeing with what I'm reading here? Because it seems to be so. I just wanted to check.

I'm using Teensy processors, being under the assumption that they would have no issues with int32_t variables.

Are you storing lat and long as separate int32_t at seven decimal places? (About one cm resolution at the equator)

INT32_MIN = -2147483648
                1234567 decimal places
            -180        degrees

Same basic idea, but OP is storing four decimal places for the "lower part" of whatever value they're doing.

Thanks for your answer!

I was wondering because NMEA2000 (Marine navigation stuff I'm working on) seems to like it as lat/lon both as signed 32 bit floats.

There is also a call for signed 64 bit floats. Why that is in the marine navigation pile? Is beyond me.