0

I'm having trouble with these variables. As you can see they're uint32_t type, so they should have a maximum value of 4 million or so, but they're rolling over and going negative at 32,767 like a signed int. Here's the relevant code:

byte lPin = 7;
char buff[100];
uint32_t loAvg = 0;
uint32_t hiAvg = 0;

void setup() {
  pinMode(lPin, OUTPUT);
  digitalWrite(lPin, HIGH); // Active LOW, so this is off

  Serial.begin(9600);
  // Generate high/low averages
  for(int i = 0; i < 100; i++) {
    loAvg += analogRead(A0);
  }
  loAvg /= 100;

  digitalWrite(lPin, LOW);
  delay(500);

  for(int i = 0; i < 100; i++) {
    hiAvg += analogRead(A0);
  }
  hiAvg /= 100;
  
  digitalWrite(lPin, HIGH);
  delay(500);
  
  sprintf(buff, "loAvg=%d, hiAvg=%d", loAvg, hiAvg);
  Serial.println(buff);

  Serial.println(hiAvg);
  digitalWrite(lPin, LOW);
  hiAvg = 0;
}

void loop() {
  sprintf(buff, "test=%d", hiAvg);
  Serial.println(buff);
  hiAvg+=100;
}

I removed a bunch of stuff from the code to save posting the whole (much longer) thing. If it ends up being relevant I can though, I just think I narrowed the problem down to this. Observing the serial monitor I get this on startup:

loAvg=287, hiAvg=0
933
test=0
test=100
test=200
...

The test=# repeats until 32700, then continues at -32736. I'm confused by the hiAvg=0 followed by the 933, showing that it totally is getting a high average, because that's around what it should be. Even if it's the sprintf() messing with it, in the setup function the value should only be around the 900 range so it wouldn't overflow there. Any ideas anyone?

3
  • 1
    there are only three lines involved ... my bet is on sprintf() ... specifically the %d ... that formats the data for output
    – jsotola
    Mar 14, 2021 at 5:43
  • 1
    "%" PRIu32 There's almost no reason to use sprintf over snprintf. In this code as written, no reason to use either.
    – timemage
    Mar 14, 2021 at 11:29
  • This code above is only the parts that were relevant to the question so I can understand not seeing the point. The sprintf is only used during testing. When DEBUG is defined, the serial prints are between #ifdef / #endif blocks so they're not present during normal operation. I did end up figuring out that I needed %lu. Thank you for the replies and answers. Mar 15, 2021 at 14:13

2 Answers 2

4

%d expects a signed int. %u expects an unsigned int. Insert the l modifier to expect a long in each of those cases. So you need a format code of %lu to print a uint32_t.

The following code:

#include <Arduino.h>

void setup()
{
    uint32_t x=110000;
    char buf[50];

   // Open console
   Serial.begin(115200);
   
    sprintf(buf, "%%d: %d\n", x);       Serial.print(buf);
    sprintf(buf, "%%u: %u\n", x);       Serial.print(buf);
    sprintf(buf, "%%ld: %ld\n", x); Serial.print(buf);
    sprintf(buf, "%%lu: %lu\n", x); Serial.print(buf);
}


void loop() {
   ;
}

prints this:

%d: -21072
%u: 44464
%ld: 110000
%lu: 110000
2

*printf and bit-width specified types

#include <inttypes.h> or C++'s #include <cinttypes> counterpart (when you have it; not on AVR-based Arduinos) gives access to macros defined for the format specifiers for each of the fixed, fast, and least versions of these types. All of these macros expand to some string literal or another.

For uint32_t the macro defining to its format specifier is PRIu32. For an AVR, this is "%lu". It needn't be "%lu" in the general case. uint32_t is one of the types where you can often get away with using "%lu" and have it either work out being exactly correct for the platform or somewhat incorrect but working anyway because it's a platform where unsigned int and unsigned long happen to have the same representation.

In C and C++, and so Arduino, adjacent string literals are concatenated into one logical string in the early phases of compilation that follow the macro preprocessor. So, the intention is that you choose the correct macro and you put them following something like "%" or "%12" if you wanted a minimum-width of 12. As in "%" PRIu32 and "%12" PRIu32 respectively. So, in your case, having included (directly or indirectly) <stdint.h> for uint32_t and <inttypes.h> for PRIu32 or their C++ counterpart headers, and then your code looks something like:

snprintf(
  buf,
  sizeof buf,
  "loAvg=%" PRIu32 ", hiAvg=%" PRIu32,
  loAvg,
  hiAvg
);

There's little to no reason to not use the snprintf. The reason why the non-n version exists at all is mostly that snprintf showed up in 1999, instead of circa the mid 1970s. I'd say it's pretty safe to use snprintf now. There probably hasn't been an Arduino that doesn't support it. About the only reason to use sprintf would be if you were calling it with a null pointer for the buffer argument to get it to do a dry run and tell how much memory you need for the result, and you just didn't want to supply a limiting size.

There are PRI... macros for all of the combinations of standard bit width and choice of signed decimal, unsigned decimal, lowercase hex, uppercase hex, etc. E.g "%" "PRId16" is for int16_t in decimal, "%" PRIX64 for uint64_t in hexadecimal with uppercase alphabetics. The are ones for the fast and least bitwidth types, and whole show is repeated with macros starting with SCN for the scanf specifiers, not that you should use the scanf family functions for anything really.

snprintf and sprintf are variadic functions. After the format specifier there's no type information available to the compiler to know what conversions to apply. It will convert types smaller than int rank up to int on unsigned int, but that's about it. E.g. it will not reliably down-convert an unsigned long long into an unsigned int, the way it would if the types were known. Modern compilers will sometimes warn you if you grossly misapply format specifiers, but they're not required to. They're not expected to dissect the format string and attempt to recover type information. So it's up to you to make sure whatever you pass in for arguments matches with the format specifiers. As you've seen it's confusing if you get it wrong with printf-type functions, but it doesn't cause real problems unless you, you know, don't limit it to stay within the output buffer with the snprintf version. In the case of passing in an unsigned long long where an unsigned int was expected, in practice, for a little-endian system like AVR, it may appear to do that for one argument. But then it may fail on the next argument, because the size of the last argument was in correct, it is now reading from the wrong place.

Print/Stream/Serial concatenating and buffering

The way you're using sprintf in your code isn't taking advantage of any of its features that Serial/Print doesn't do.

Serial.print(F("loAvg="));
Serial.print(loAvg);
Serial.print(F(", hiAvg="));
Serial.println(hiAvg);

Despite taking four lines, this is actually smaller, faster, more reliable code than the sprintf one-liner.

Putting the output into a buffer first might make sense if you were going to put it into a packet, like for UDP or nRF24L01. But, Print/Stream/Serial already concatenates and buffers things for you.


Incidentally, if you're going to use printf functions and you're doing it on an AVR-based Arduino, look at the avr-libc documentation for the _P variants that take their format strings from PROGMEM and the PSTR macro.

It is also possible to complete the implementation of <stdio.h> for stdout and stderr through libc-specific means. So, if you really want to, you can have the regular printf function pushing data out through Serial (among other things) such that you don't need to buffer repeatedly with sprintf.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.