Go Down

Topic: use PString to avoid: crashes due to String, sprintf() or dtostrf() float issues (Read 2458 times) previous topic - next topic

Florin_Andrei

I've done my share of hardware design and software programming in the past, but I'm fairly new to Arduino and I'm still learning the do's and don'ts of this platform. Here's a few don'ts and a do I learned recently.

For a project, I had to continuously (well, once per second) print two float values on an LCD. The float values had to be formatted to fit the limited space on the LCD. Basically, I have 4 characters for each float, including the decimal point. Example of how the LCD may look at some moment:

mirror: 32.4 RPM
eccent: 1.73 RPM

Simple task, right? Well, not so fast...

My first thought was to use sprintf() to format the float values, because that's what any programmer would think of first. It turns out, sprintf() provided by the Arduino libs won't accept float values. That was both surprising and unpleasant. Okay, let's look for other solutions.

Some older posts on this forum suggested using the dtostrf() function as a sprintf() replacement for floats. That seemed to work well, except once in a while it would trigger a subtle bug that allowed "10.00" to be printed exactly as-is, instead of truncating it to "10.0". Moreover, if I tried to feed "10.00" intentionally into the function, the bug would not trigger. It would only appear in some random combination of factors that I was unable to reproduce. That's not really confidence-inducing, so I abandoned this idea too.

I thought to write my own formatting functions. But most of the sane ideas I had all used the String library, or some sort of string objects, and that caused all manner of mysterious crashes and corruption. Plus, writing your own basic routines is not always fun. So that didn't work either. Looking for other solutions...

...enter PString:

http://arduiniana.org/libraries/pstring/

It doesn't crash, no matter how hard I try. That, right there, was a huge relief and a big change from everything else I tried before. It works well for simple formatting tasks, such as feeding it a float and asking "now return this float within N characters total". It's small. In fact, the code using PString generates a smaller binary than all the other options I've explored.

I highly recommend it for most simple usage scenarios when you would otherwise use the regular String lib. It's perhaps not a complete replacement, but it works well for the usual stuff.

Part of my trials and tribulations with String / sprintf / dtostrf are described here:

https://forums.adafruit.com/viewtopic.php?f=25&t=52883

Here's a sample of my test code (only the display part, the actual values are generated randomly, instead of being read from a sensor) that appears to work well with PString:

Code: [Select]
#include <Adafruit_CharacterOLED.h>
Adafruit_CharacterOLED lcd(OLED_V2, 6, 7, 8, 9, 10, 11, 12);

// http://arduiniana.org/libraries/pstring/
// This should cure the variety of problems formatting and printing
// floats on Arduino, without using the bug-ridden String library.
#include <PString.h>

// global variables for the simulated speeds that we display
float mir;
float ecc;

void setup()
{
  // initialize display
  lcd.begin(16, 2);
  lcd.setCursor(0, 0);
  lcd.print("mirror:      RPM");
  lcd.setCursor(0, 1);
  lcd.print("eccent:      RPM");

  // initialize simulated values
  mir = random(0, 100);
  ecc = random(0, 100);
}

void printspeed(float sp, int x, int y)
{
  // properly format the displayed speed based on value
  // then print it

  // output print buffer
  char buff[5];

  // position the cursor
  lcd.setCursor(x, y);

  // format the number depending on its value
  if (sp < 0) {
    char buff[5] = " neg";
  } else if (sp >= 0 && sp < 1000) {
    PString(buff, sizeof(buff), sp);
  } else {
    char buff[5] = "high";
  }

    lcd.print(buff);
}

float gensp(float sp)
{
  // generate new random value for speed

  // max change at each step
  float jit = 1.5;
  // returned value
  float ret;

  int jitint = (int) 1000 * jit;
  ret = sp + (float)random(-jitint, jitint)/1000;
  if (ret < 0)
    ret += jit;
  if (ret >= 1000)
    ret -= 100;

  return ret;
}

void loop()
{
  // refresh random value
  mir = gensp(mir);
  ecc = gensp(ecc);
  // display
  printspeed(mir, 8, 0);
  printspeed(ecc, 8, 1);
  delay(1000);
}

PaulS

Quote
That's not really confidence-inducing, so I abandoned this idea too.

Were you printing a space, or some other visible character, after the float, when printing it on the LCD, so you know that the value from dtostrf() was 5 characters, instead of 4? Or was the value not fully overwriting what was already displayed?
The art of getting good answers lies in asking good questions.

Florin_Andrei

If you look at the example formatting above, there's an initial label terminated with space, then 4 characters for the float, then a space character, then the string "RPM". The label, spaces and "RPM" are only written once, during initialization. The float is refreshed once per second.

Sometimes, but not always, when the input was 10.00, dtostrf() would return "10.00" instead of "10.0", overwriting the space. The extra "0" would stay there forever after that. The only way to reproduce this was to let the code do its thing - in 30 minutes to 1 hour or so it would stumble upon the corner case and trigger the bug. Feeding "10.00" intentionally into dtostrf() would not work.

Anyway, this project will become part of a digital controller for a machine operating in the real world, creating large quadratic surfaces in solid glass with a precision of 0.1 microns or less. Random mysterious errors, even when rare, even when seemingly affecting just the display, are unacceptable. After I ditched the initial code and started using PString, I was unable to trigger any bug, no matter what.

Go Up