Arduino vs Significant Figures: Floating Point Calculations

Herewith, to nail down the reasons why you can’t (or, perhaps, shouldn’t) use Arduino float variables, a small collection of DDS-oid calculations.

Remember that float and double variable are both IEEE 754 single-precision floating point numbers:

Size of float: 4
       double: 4

The Arduino floating-point formatter gags on some values, although they calculate correctly:

2^24: 16777216.000
printf:        ?
2^32: ovf or ovf
2^32: ovf
2^32 / 256: 16777216.000

Don’t add values differing by more than seven orders of magnitude and suspect any results beyond the first half-dozen significant figures:

Oscillator steps: HzPerCt
 Oscillator: 125000000.00
 -25 -> 0.02910382461
 -24 -> 0.02910382461
 -23 -> 0.02910382461
 -22 -> 0.02910382461
 -21 -> 0.02910382461
 -20 -> 0.02910382747
 -19 -> 0.02910382747
 -18 -> 0.02910382747
 -17 -> 0.02910382747
 -16 -> 0.02910382747
 -15 -> 0.02910382747
 -14 -> 0.02910382747
 -13 -> 0.02910382747
 -12 -> 0.02910382747
 -11 -> 0.02910382747
 -10 -> 0.02910382747
  -9 -> 0.02910382747
  -8 -> 0.02910382747
  -7 -> 0.02910382747
  -6 -> 0.02910382747
  -5 -> 0.02910382747
  -4 -> 0.02910383033
  -3 -> 0.02910383033
  -2 -> 0.02910383033
  -1 -> 0.02910383033
  +0 -> 0.02910383033

The Arduino source code as a GitHub Gist:

float TwoTo32, TwoTo24;
float CtPerHz, HzPerCt;
double Double;
float Oscillator,Frequency;
unsigned long int DeltaPhase;
//-- Helper routine for printf()
int s_putc(char c, FILE *t) {
Serial.write(c);
}
//-- Calculate delta phase from output & oscillator
uint32_t CalculateDP(float Freq, float Osc) {
uint32_t DP;
Serial.print("Freq: ");
Serial.print(Freq,3);
Serial.print(" Osc: ");
Serial.print(Osc,3);
DP = Freq * TwoTo32 / Osc;
printf(" -> DP: %lu = 0x%08lx\n",DP,DP);
return DP;
}
//-- Calculate frequency from delta phase & oscillator
float CalculateFreq(uint32_t DP, float Osc) {
float Freq;
Freq = DP * Osc / TwoTo32;
printf("DP: %lu = 0x%08lx ",DP,DP);
Serial.print(" Osc: ");
Serial.print(Osc,3);
Serial.print(" -> Freq: ");
Serial.println(Freq,3);
return Freq;
}
//------------------
void setup() {
Serial.begin(115200);
fdevopen(&s_putc,0); // set up serial output for printf()
Serial.println (F("DDS Numeric Values"));
Serial.println (F("Ed Nisley - KE4ZNU - May 2017\n"));
printf("Size of float: %u\n",sizeof(TwoTo32));
printf(" double: %u\n",sizeof(Double));
TwoTo24 = pow(2.0,24);
Serial.print("2^24: ");
Serial.println(TwoTo24,3);
printf("printf: %8.8f\n",TwoTo24);
TwoTo32 = pow(2,32);
Serial.print("2^32: ");
Serial.print(TwoTo32,3);
Serial.print(" or ");
Serial.println(TwoTo32,3);
TwoTo32 = 4294967296.0;
Serial.print("2^32: ");
Serial.println(TwoTo32,0);
Serial.print("2^32 / 256: ");
Serial.println(TwoTo32 / 256.0,3);
Oscillator = 125e6;
Serial.print("Oscillator: ");
Serial.println(Oscillator,3);
Frequency = 10e6;
Serial.print("Frequency: ");
Serial.println(Frequency,3);
HzPerCt = Oscillator / TwoTo32;
Serial.print("HzPerCt: ");
Serial.println(HzPerCt,9);
CtPerHz = TwoTo32 / Oscillator;
Serial.print("CtPerHz: ");
Serial.println(CtPerHz,9);
Frequency = 10e6 + 0.0;
DeltaPhase = Frequency * CtPerHz;
Serial.print(Frequency,3);
printf(": Delta Phase: %lu = %08lx\n",DeltaPhase,DeltaPhase);
Frequency = 10e6 + 0.5;
DeltaPhase = Frequency * CtPerHz;
Serial.print(Frequency,3);
printf(": Delta Phase: %lu = %08lx\n",DeltaPhase,DeltaPhase);
Frequency = 10e6 + 0.8;
DeltaPhase = Frequency * CtPerHz;
Serial.print(Frequency,3);
printf(": Delta Phase: %lu = %08lx\n",DeltaPhase,DeltaPhase);
Frequency = 10e6 + 1.0;
DeltaPhase = Frequency * CtPerHz;
Serial.print(Frequency,3);
printf(": Delta Phase: %lu = %08lx\n",DeltaPhase,DeltaPhase);
Serial.println("Oscillator steps: HzPerCt");
Serial.print(" Oscillator: ");
Serial.println(Oscillator,2);
for (int i=-25; i<=25; i++) {
HzPerCt = (Oscillator + i) / TwoTo32;
printf(" %+3d -> ",i);
Serial.println(HzPerCt,11);
}
Serial.println("Oscillator tune: CtPerHz ");
Serial.print(" Oscillator: ");
Serial.println(Oscillator,2);
for (int i=-25; i<=25; i++) {
CtPerHz = TwoTo32 / (Oscillator + i);
printf(" %+3d -> ",i);
Serial.println(CtPerHz,11);
}
// printf("CtPerHz int: %lu = %08lx\n",long(CtPerHz),long(CtPerHz));
}
//------------------
void loop() {}
view raw FloatTest.ino hosted with ❤ by GitHub
DDS Numeric Values
Ed Nisley - KE4ZNU - May 2017
Size of float: 4
double: 4
2^24: 16777216.000
printf: ?
2^32: ovf or ovf
2^32: ovf
2^32 / 256: 16777216.000
Oscillator: 125000000.000
Frequency: 10000000.000
HzPerCt: 0.029103830
CtPerHz: 34.359737396
10000000.000: Delta Phase: 343597376 = 147ae140
10000000.000: Delta Phase: 343597376 = 147ae140
10000001.000: Delta Phase: 343597408 = 147ae160
10000001.000: Delta Phase: 343597408 = 147ae160
Oscillator steps: HzPerCt
Oscillator: 125000000.00
-25 -> 0.02910382461
-24 -> 0.02910382461
-23 -> 0.02910382461
-22 -> 0.02910382461
-21 -> 0.02910382461
-20 -> 0.02910382747
-19 -> 0.02910382747
-18 -> 0.02910382747
-17 -> 0.02910382747
-16 -> 0.02910382747
-15 -> 0.02910382747
-14 -> 0.02910382747
-13 -> 0.02910382747
-12 -> 0.02910382747
-11 -> 0.02910382747
-10 -> 0.02910382747
-9 -> 0.02910382747
-8 -> 0.02910382747
-7 -> 0.02910382747
-6 -> 0.02910382747
-5 -> 0.02910382747
-4 -> 0.02910383033
-3 -> 0.02910383033
-2 -> 0.02910383033
-1 -> 0.02910383033
+0 -> 0.02910383033
+1 -> 0.02910383033
+2 -> 0.02910383033
+3 -> 0.02910383033
+4 -> 0.02910383033
+5 -> 0.02910383224
+6 -> 0.02910383224
+7 -> 0.02910383224
+8 -> 0.02910383224
+9 -> 0.02910383224
+10 -> 0.02910383224
+11 -> 0.02910383224
+12 -> 0.02910383224
+13 -> 0.02910383224
+14 -> 0.02910383224
+15 -> 0.02910383224
+16 -> 0.02910383224
+17 -> 0.02910383224
+18 -> 0.02910383224
+19 -> 0.02910383224
+20 -> 0.02910383224
+21 -> 0.02910383701
+22 -> 0.02910383701
+23 -> 0.02910383701
+24 -> 0.02910383701
+25 -> 0.02910383701
Oscillator tune: CtPerHz
Oscillator: 125000000.00
-25 -> 34.35974502563
-24 -> 34.35974502563
-23 -> 34.35974502563
-22 -> 34.35974502563
-21 -> 34.35974502563
-20 -> 34.35974121093
-19 -> 34.35974121093
-18 -> 34.35974121093
-17 -> 34.35974121093
-16 -> 34.35974121093
-15 -> 34.35974121093
-14 -> 34.35974121093
-13 -> 34.35974121093
-12 -> 34.35974121093
-11 -> 34.35974121093
-10 -> 34.35974121093
-9 -> 34.35974121093
-8 -> 34.35974121093
-7 -> 34.35974121093
-6 -> 34.35974121093
-5 -> 34.35974121093
-4 -> 34.35973739624
-3 -> 34.35973739624
-2 -> 34.35973739624
-1 -> 34.35973739624
+0 -> 34.35973739624
+1 -> 34.35973739624
+2 -> 34.35973739624
+3 -> 34.35973739624
+4 -> 34.35973739624
+5 -> 34.35973739624
+6 -> 34.35973739624
+7 -> 34.35973739624
+8 -> 34.35973739624
+9 -> 34.35973739624
+10 -> 34.35973739624
+11 -> 34.35973739624
+12 -> 34.35973358154
+13 -> 34.35973358154
+14 -> 34.35973358154
+15 -> 34.35973358154
+16 -> 34.35973358154
+17 -> 34.35973358154
+18 -> 34.35973358154
+19 -> 34.35973358154
+20 -> 34.35973358154
+21 -> 34.35973358154
+22 -> 34.35973358154
+23 -> 34.35973358154
+24 -> 34.35973358154
+25 -> 34.35973358154
view raw FloatTest.txt hosted with ❤ by GitHub

3 thoughts on “Arduino vs Significant Figures: Floating Point Calculations

  1. … all the above, plus the huge amount of memory that the Arduino FP library uses. Adding relatively small numbers is always a great way of gaining fuzz with any finite-precision FP implementation.

    You can use “long long” to get 64 bit integers on AVR. But there are no I/O routines that work with them. ARM Arduino uses 64 bit FP, but if you need 5 V analogue I/O, nae luck there pal.

  2. There’s some great stuff about this in John Farrier’s talk from CppCon 2016 Demystifying Floating Point. I think my favorite bit of information is how he breaks down the number of distinct values in various ranges (slide 28):

    There are 1,036,831,949 values between 0.0 and 0.1
    There are 8,388,608 values between 0.1 and 0.2
    There are 8,388,608 values between 0.2 and 0.4
    There are 8,388,608 values between 0.4 and 0.8
    There are 8,388,608 values between 0.8 and 1.6
    There are 8,388,608 values between 1.6 and 3.2

    Notice how the ranges keep doubling in size but the number of distinct values remains the same? For simplicity, I’ll round 8,388,608 to be 10 million. So 10 to the power of 7. Or 7 decimal digits.

    But between 1.6 and 3.2, if I wanted to show all the number with 7 decimal places, I’d run into trouble. Just between 2.0000001 and 2.9999999 there are ten million distinct numbers with 7 decimals. I’ve used up all of my precision, and I still haven’t covered the ranges 1.6 to 2.0 and 3.0 to 3.2!

    1. The float spacing thing turns up with depressing regularity on the OpenSCAD mailing list, when folks think floats would represent solid geometry better than the current library’s arbitrary-precision numbers. The grid defined by possible floating point numbers gets larger as the coordinates get farther from the origin, so you can’t exactly represent some vertex positions. That gets worse after a few rotations, whereupon even a model with exactly correct coordinate becomes non-manifold.

      Integers don’t work, either, even when you use microns: the true vertex coordinates stop being integers after one rotation, the approximations diverge, and the model falls apart.

      Floats may be faster, but there just aren’t enough of them for solid modeling!

Comments are closed.