And note, I believe the code will not work on Teensy 4.0/4.1 if you optimize for space, since the sprintf when optimizing for space omits the floating point formatting stuff. Also of course, it might not work on other processors using Arduino framework.
To that end, I wrote this fragment of code for my power meter that uses an INA219 sensor to read the amount of power used:
Code:
static const int BUFFER_SIZE = 40;
static const unsigned MAX_COUNTER = (500 / DEBOUNCE);
static unsigned int display_counter = MAX_COUNTER;
// prevent compiler from optimizing sprintf test
volatile float ten = 10.0f;
// Convert a floating point number to a string with a %6.3f format. Not all
// versions of sprintf support floating point, so on the first call test to see
// if it works, and if we don't get the expected result, do the conversion in
// an alternate fashion. This function is optimized for the range of values we
// expect to see with the power meter.
//
// On the Teensy, if you optimize for space, a smaller library is used that
// does not do have %f, %g, etc. formats.
char *
float_to_string (float number, char *buffer, int fract)
{
static bool first_time = true;
static bool sprintf_works;
if (first_time) {
first_time = false;
strcpy (buffer, "no results");
sprintf (buffer, "%6.3f", ten);
sprintf_works = (strcmp (buffer, "10.000") == 0);
if (!sprintf_works)
Serial.printf ("sprintf does not work for floating point, '%s'\n", buffer);
}
if (sprintf_works)
sprintf (buffer, "%*.*f", fract + 3, fract, number);
else {
bool negative = (number < 0.0f);
float number2 = fabs (number);
if (fract == 0) {
number2 += 0.5f;
sprintf (buffer, "%2d.", (int) (negative ? -number2 : number2));
} else if (fract == 1) {
int number_x10 = (int) ((number2 * 10.0f) + 0.5f);
int number_main = number_x10 / 10;
int number_fract = number_x10 % 10;
sprintf (buffer,
"%2d.%c",
negative ? -number_main : number_main,
number_fract + '0');
} else if (fract == 2) {
int number_x100 = (int) ((fabsf (number) * 100.0f) + 0.5f);
int number_main = number_x100 / 100L;
int number_fract = number_x100 % 100L;
sprintf (buffer,
"%2d.%c%c",
negative ? -number_main : number_main,
((number_fract / 10) % 10) + '0',
((number_fract / 1) % 10) + '0');
} else {
int number_x1000 = (int) ((fabsf (number) * 1000.0f) + 0.5f);
int number_main = number_x1000 / 1000;
int number_fract = number_x1000 % 1000;
sprintf (buffer,
"%2d.%c%c%c",
negative ? -number_main : number_main,
((number_fract / 100) % 10) + '0',
((number_fract / 10) % 10) + '0',
((number_fract / 1) % 10) + '0');
}
}
return buffer;
}
And while using a suffix of 'L' will work on the Teensy 3.x/LC, it technically is not a double constant. The 'L' suffix says the constant is
long double instead of
double. So if you are in a context where a
double is wanted, the C++ compiler will typically auto cast the
long double type back to
double.
It 'works' because the current ARM compilers do not have a larger
long double type, and under the covers, both
double and
long double have the same representation.
However, not all processors have this property. The Intel/AMD x86 processors typically use the internal 80-bit type for
long double.
The PowerPC compiler that I work on typically has
long double being represented as a pair of
double values. This is done via library routines -- there isn't hardware support directly for this paired floating point. Modern PowerPC servers (starting with power9) has native support for the IEEE 128-bit floating point that is defined in the IEEE 754R standard. One of the many tasks I have been doing over the last few years is adding the support in the GNU compiler suite to support changing the default
long double format to IEEE 128-bit, and working with the GLIBC and LIBSTDC++ teams to get their support in. Most of the pieces are committed, and I have a few loose ends to get committed in January.
Just to be clear, the following is my own opinion, and not that of my employer, my cats, or anybody else. I am speculating on why Paul made the choices he did. I tend to believe it is the right choice for the Teensy LC/3.x, but there are always side effects.
That being said, Paul was faced with an impossible choice when the Teensy 3.0 came out. The AVR Arduino systems had a non-standard floating point implementation where
float and
double use the same format (IEEE 32-bit single precision). So code imported from these sources expected this behavior. But the ISO C/C++ standards demand that
double has more precision than
float typically has.
The C/C++ standards specifies that if a
float and
double were combined, that the
float would be converted to
double. In addition, a floating point constant without a '
f', '
F', '
l' or '
L' sffix is automatically a
double type. If you use a normal floating point constant it will mean the expression will be done in double precision.
The Teensy 3.5/3.6 have hardware support for single precision (
float) but not double precision (
double). If the
-fsingle-precision-constant option was not specified, it would mean you would lose a lot of the power of the machine, because it would have to do the emulation using tens, hundreds or even thousands of instructions to do basic operations.
The Teensy 4.0/4.1 has hardware support for both single and double precision. So this option is not needed on those systems.