dtostrf() overflow

Hello,

According to the reference:

The format is

dtostrf(floatvar, StringLengthIncDecimalPoint, numVarsAfterDecimal, charbuf);

where

  • floatvar float variable
  • StringLengthIncDecimalPoint This is the length of the string that will be created
  • numVarsAfterDecimal The number of digits after the deimal point to print
  • charbuf the array to store the results

// Energy is float which goes in

char s_energy[8];
dtostrf(energy, 4, 2, s_energy);

The largest value I would expect to get this way:
99.99

Where if we count the . and the terminating NUL for the array that is 6 characters so my 8 declaration should be more than safe.

However I get these short of values and another dtostrfs stopped working yielding no result so I guess there is some overflow going on.

13457.38

I will increase the s_energy array to [20] to be safe just want to understand why is this happening.

The largest value I would expect to get this way:99.99

this is a misunderstanding or you did not read the right reference.

the doc states:

char* dtostrf ( double __val,

signed char __width,
unsigned char __prec,
char * __s
)



The dtostrf() function converts the double value passed in val into an ASCII representationthat will be stored under s. The caller is responsible for providing sufficient storage in s.

Conversion is done in the format "[-]d.ddd". The **minimum** field width of the output string (including the possible '.' and the possible sign for negative values) is given in width, and prec determines the number of digits after the decimal sign. width is signed value, negative for left adjustment.

The dtostrf() function returns the pointer to the converted string s.

so the width value is to be seen as a minimum. There is no action taken on your float, It's up to you to ensure that there is enough space in the buffer by constraining your float to what will fit.

The idea of the minimum width is that if you right adjust then you can pad with space on the left and always get a cString that "looks good" and is properly aligned in a column. If you left adjust then padding is added AFTER the conversion in ASCII so that if you print this on a LCD for example, you always know how many characters you have (and erase them) or just have nice looking left aligned columns

try this code

const byte bufferSize = 20;
char tmpBuffer[bufferSize];

void dumpTmpBuffer()
{
  for (byte i = 0; i < bufferSize; i++) {
    if (tmpBuffer[i] == '\0') Serial.write('*'); // a '*' will denote an NULL character
    else if (tmpBuffer[i] == ' ') Serial.write('_'); // an underscore will show a space is there
    else Serial.print(tmpBuffer[i]); // otherwise we print the character
    Serial.write(' '); // and add a spacer for readability
  }
  Serial.println();
}

void setup() {
  Serial.begin(115200); // Serial console set at 115200 Bauds

  double d1 = -10.345;

  memset(tmpBuffer, '\0', bufferSize); // fill up the buffer with  '\0'
  dtostrf(d1, 10, 3, tmpBuffer);       // right align
  dumpTmpBuffer();

  memset(tmpBuffer, '\0', bufferSize);  // fill up the buffer with  '\0'
  dtostrf(d1, -10, 3, tmpBuffer);       // left align
  dumpTmpBuffer();

  memset(tmpBuffer, '\0', bufferSize);  // fill up the buffer with  '\0'
  dtostrf(d1, 4, 3, tmpBuffer);         // right align, not enough space
  dumpTmpBuffer();

  memset(tmpBuffer, '\0', bufferSize);  // fill up the buffer with  '\0'
  dtostrf(d1, -4, 3, tmpBuffer);         // right align, not enough space
  dumpTmpBuffer();
}

void loop() {}

Serial Monitor (@ 115200 bauds) will show

[color=purple]
_ _ _ - 1 0 . 3 4 5 * * * * * * * * * * 
- 1 0 . 3 4 5 _ _ _ * * * * * * * * * * 
- 1 0 . 3 4 5 * * * * * * * * * * * * * 
- 1 0 . 3 4 5 * * * * * * * * * * * * * 
[/color]

the stars are where null chars were kept and you can see with _ how alignment works.
in the first example you have spaces before the ASCII representation
in the second example you have spaces after the ASCII representation
with the two last examples though you can see that when the minimum width is not enough you still get the full value in the buffer, no padding as there was no room left, and would lead to overflow if you are not careful.

Hmm that is interesting.
Just one more question then because I see you are using memset(tmpBuffer, '\0', bufferSize); // fill up the buffer with '\0' to zero fill all character output buffers first.

I have the char buffers declared inside a function, not globally so I assume that every time I call this function the arrays are created then destroyed.

Do I really need to reset them before using?

For example with using strcypy(buffer, "what ever string") it is not necessary, even if there is something in it it will get reseted and overwritten.

with a local buffer, it won't be initialized to '\0' by the compiler but the call to dtosrf() will ensure you get a trailing '\0' after the conversion (if you don't overflow), so you don't need to do it. what you get is a correct cString you can print.

I'm doing it in this sample code because I wanted to use the same buffer multiple times and show exactly what gets modified with the call to dtosrf(). If I had not done so, some bytes from the previous calls would have stayed in the buffer.

So long story short, no need to empty the buffer if you initialize it with a strcpy() or dtostrf() or itoa() etc (anything that will guarantee adding a trailing NULL). if you use strcat() etc as a first call to fill in the buffer, then ensure the buffer is a proper cString by initializing the first byte to NULL

void myFunction()
{ 
 char cStrBuf[20];
 cStrBuf[0] = '\0'; // make it a proper empty cString
  ...
}

global variables will get initialized to 0 so you don't have to do it.

if you use strncat() be careful that the trailing NULL is not added if you reached the full buffer space. I often add an extra byte to my cStrings which I keep to NULL to be on the safe side.