Closed dsyleixa closed 5 years ago
Hi @dsyleixa ,
Thanks for an interesting bug report! We don't have an exact answer for you, but I do have an observation.
I modified the code to plain .cpp so I could build it in IDF without Arduino, and also so I could build it on my host PC. Code here: lunar.cpp.txt
On my ESP32 with ESP-IDF, I get the same final value reported here: 54196.625000.
Building it on my 64-bit Linux PC (g++ -o lunar lunar.cpp
), I get the same final result (54196.625000) as on the ESP32. Note that there is nothing ESP-specific about how I'm building it on my PC, that's treating it vanilla C++. You can see the same result online, here: http://www.cpp.sh/2xwry
I also tried with -ffast-math
on my local PC and get the same final result.
Here's the different output:
Output at each step is the same for PC vs ESP32. Maybe there is some small difference in the way I modified the Arduino code to make it C++ compatible, although I don't see what it would be.
I expect this is probably something subtle with with different floating point math implementations and how they handle errors, or trade off acceptable error vs performance. The errors are accumulating differently and leading to these very different results.
As long as g++ on Linux is producing the same results, I'm inclined to think it's not a bug in ESP-IDF or the ESP32 as such. But I'm curious if you find out exactly where the root cause is...
(NB: If you want to avoid any kind of floating point errors, the only surefire way is to go to fixed point math. Or integer math where you "think big" with all your units, which is basically fixed point math.)
Just noticed that all the other micros you mentioned don't have FPUs, so it may be something specific with the software FPU implementation. Do the other micros also give the smaller result if you change the floats to doubles?
(ESP32 has single precision FPU only, so it's also using a software library if you go to double precision. But maybe it's a different software implementation.)
thank you, but that is really strange - finally all different Arduino platforms compute the same way...
Just to summarize, the AVR Mega2560 also uses plain C++ for fp32 computation, same to M0 and M3, whilst M4 gets the same result by it's fpu and all the same even for fp64 double (here actually no fp32 rounding error may occur at all!)
Arduino+Adafruit crews: 4 landed at Tranquility base,
vs. ESP32+Linux crews: 2 lost without trace
;)
but honestly, something is going crazy here...
I'm curious about your further observations and gladly looking forward, thank you!
ESP32 results for double:
just for documentation: code for fp64 double
// Lunar Lander
// preprocessor defaults for time sync:
//#define SYNC_REALTIME // real time sync; outcomment for time lapse
#if defined (SAM)
#include "avr/dtostrf.h" // sprintf() and dtostrf() for doubles
#endif
//----------------------------------------------------
// flight control
//----------------------------------------------------
// public:
double mFuel=8200; // fuel mass in kg for landing
double hi=15300; // act height in m
double vHorz=1685; // Horizontal orbital speed m/s
double sTargm=470000; // horizontal way to target landing place
double burnPerc=0; // user input: burnrate %
double ftilt; // tilt horiz...vert -1...0...+1
double tiltDeg; // tilt degrees -90°...0...+90°
//----------------------------------------------------
// private:
double ti=0.0, dt=0.5; // act time, delta time in sec
const double g=1.62; // Moon gravity
const double MoonRad=3476000/2.0; // Moon radius
const double mLander=6500; // lander mass in kg with launch fuel
const double Isp=3050; // Rocket engine Specific Impulse
const double FBrake=45000; // Rocket engine max Propulsion Force
double burnMax=FBrake/Isp; // absolute max fuel burnrate
double mTotal=mLander+mFuel; // brutto weight with full tanks
double rBrake; // user Rocket brake force 100%, percentual
double dFuel=0; // delta fuel
double burnf=0; // burnrate factor 0.0 ... 1.0
double dh=0.0; // delta height in m
double scaleH=hi/100; // scaler for tft.hight=100%
double vVert=0.0; // Impact speed in m/s
double accVert=0; // vertical accel (sum)
double accBrake=0; // acc by break rockets
double fCentrifug=0; // centrifugal force by orbital speed
double accCentrif=0; // centrifugal accel by orbital speed
double sHorzm=0.0; // horizontal way flown in m
//----------------------------------------------------
// draw and clear bitmaps
//----------------------------------------------------
//----------------------------------------------------
// Serial LogBook
//----------------------------------------------------
void LogBook(){
char sbuf1[50], sbuf2[50] ;
char* headline1 = "t.sec hi.m vVert vHoriz ";
char* headline2 = "Burn tilt brake acc Fuel TBase.m";
Serial.print(headline1);
Serial.println(headline2);
sprintf(sbuf1, "%5.1f %5d %4d %4d ",
ti, (int)hi, (int)vVert, (int)vHorz);
sprintf(sbuf2, "%3d%% %4d %3.1f %4.1f %5d %f",
(int)burnPerc, (int)(ftilt*90), accBrake, accVert,
(int)mFuel, (double)(sTargm-sHorzm));
Serial.print(sbuf1); Serial.println(sbuf2);
Serial.println();
}
uint32_t dtime;
//----------------------------------------------------
// Lander Move
//----------------------------------------------------
void LanderMove() {
static double t0=ti;
dtime=millis();
if(hi>0) {
// Burn Ratio:
// 100 = 100% == full brake power
// 50 = 50% == half brake power
// 0 = 0% == zero brake power
// or anything in between
//
// 100% BURN RATIO (BRAKE POWER)
// => 45000kN propulsion force
// => 14,75kg Fuel burn per second
// => brake accelation = 3m/s²
// XEROX Board Computer program, debug:
if( sHorzm<15600.0) burnPerc=0; // sHorzm<15600
else
if( vVert>60||vHorz>30 ) burnPerc=100; //
else
if(vVert>50) burnPerc=65;
else
if(vVert>40&&hi<3000) burnPerc=45;
else
if(vVert>35&&hi<1800) burnPerc=40;
else
if(vVert>10&&hi<1100) burnPerc=35;
else
if(vVert>=1&&hi<120) burnPerc=33;
else
burnPerc=0;
// calculate control
fCentrifug=vHorz*vHorz*mTotal/(MoonRad+hi);
accCentrif=fCentrifug/mTotal;
if(mFuel==0) burnPerc=0; // no fuel, no burn ;)
burnf = burnPerc/100.0; // factor 0...1
// vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
dFuel= burnMax*burnf*dt; // try burnrate: enough fuel?
dFuel = min(dFuel, mFuel); // calc available rest fuel
burnf = (dFuel/burnMax)/dt; // re-calc burnrate by rest fuel
burnPerc = burnf*100;
rBrake= FBrake*burnf; // rel brake force
accBrake= rBrake/mTotal; // rocket brake acceleration
mFuel = max(mFuel-dFuel, (double)0); // rest fuel >=0
mTotal= mLander+mFuel; // new total mass
if(vVert>=5 ) {
if(vVert>=40 && vHorz>2 ) ftilt=0.65; // 58.5°
else
if(vVert>=30 && vHorz>2 ) ftilt=0.75; // 67.5°
else
if(vVert>=20 && vHorz>2 ) ftilt=0.80; // 72.0°
else
if(vHorz<=2 && vHorz>=-2) ftilt=0.0;
else
if(vHorz<0) ftilt=0.0;
else ftilt=0.85; // 76.5°
}
else
if(vVert<5&&vHorz>20 ) ftilt=1;
else ftilt=0;
accVert = g - (1-abs(ftilt))*accBrake -accCentrif;
vVert = vVert + accVert*dt; // fractional vertical brake
if(vHorz>0)
vHorz = vHorz - (ftilt)*accBrake*dt; // fractional horizonal brake
// vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
sHorzm = sHorzm + vHorz*dt; // horizontal way flown
dh = 0.5*accVert*dt*dt + vVert*dt ; // delta height by res. grav+centrifug.+brake acc.
hi = hi-dh; // new resulting height
//-----------------------------------------------
// pause
#ifdef SYNC_REALTIME
while( millis()-dtime < dt*1000 );
#endif
//-----------------------------------------------
ti+=dt;
LogBook();
t0=ti;
//-----------------------------------------------
// Landing specs/ratings
if ( (( hi<=0 && vVert>=5 ) && vVert<8) ) // Damage
{
Serial.println();
Serial.println(" !! Damage !!");
}
else
if ( hi<=0 && vVert<5 && abs(sTargm-sHorzm)>100) { // very good Landing but way off
burnPerc=0;
Serial.println();
Serial.println("Very good but way off!");
}
else
if ( hi<=0 && vVert<5) { // Perfect Landing
burnPerc=0;
Serial.println();
Serial.println("Perfect Landing!");
}
else
if ( hi<=0 ) // B L A S T
{
Serial.println();
Serial.println(" !!! B L A S T !!!");
}
}
}
//----------------------------------------------------
// setup
//----------------------------------------------------
void setup() {
#if defined (SAM)
asm(".global _printf_double"); // sprintf() and dtostrf() for doubles
#endif
Serial.begin(115200);
delay(2000);
Serial.println("Serial started!");
sHorzm=0; // way flown
Serial.println();
delay(dt*1000);
fCentrifug=vHorz*vHorz*mTotal/(MoonRad+hi);
accCentrif=fCentrifug/mTotal;
accVert=g-accBrake-accCentrif;
LogBook();
}
//----------------------------------------------------
// loop
//----------------------------------------------------
void loop(void) {
if (hi>0) {
LanderMove();
}
}
double computations by Arduino Due:
@dsyleixa Thank you for posting these logs. Please consider attaching long pieces of output as files, or wrapping them with <details>
tags, to keep the thread readable (I have modified the two posts above).
@dsyleixa If you have both types of boards available, perhaps you can try to log more intermediate values, or to increase the precision at which the current ones are logged. With the logs provided, the difference starts at t=52.0:
< 52.0 15223 4 1552 100% 76 3.2 -0.2 7572 385217.239246
---
> 52.0 15224 3 1552 100% 76 3.2 -3.0 7572 385217.239246
(note acc=-3.0 for Linux/ESP32).
It is possible that the difference between the two implementations actually starts earlier, but it is not visible due to rounding of the logged values.
float or double?
(PS,
the dt interval is initialized in line 29:
dt=0.5;
so using smaller values increases the resolution!)
a user of the Arduino probably had found the cruicial reason
Hi dsyleixa, I managed to track down the bug.
After comparing the results from my Arduino Uno and ESP32 on a spreadsheet, I noticed that there was a spurious reading for the vertical acceleration (accVert variable) on the ESP32 during the 105th iteration.
It turns out that absolute abs() function in C should only be used with integers. However, it appears that while the AVR and ARM abs() function also accepts float data types, the ESP32 doesn't.
Replacing the abs() function for the equivalent floating point fabsf() function instead solves the issue. The AVR, ARM and ESP32 are now in agreement. http://forum.arduino.cc/index.php?topic=613077.msg4158633#msg4158633
Well spotted!
Seems that the Arduino core replaces standard C abs() with their own simplified version, that can take any type: https://github.com/arduino/ArduinoCore-avr/blob/master/cores/arduino/Arduino.h#L94
When I changed the source code to vanilla C++, I didn't copy this macro over so my "Linux" version definitely got the libc abs() and triggered the bug. If I add the Arduino abs() macro to the plain C++ version, everything is good and the lander result is as expected: http://www.cpp.sh/9tc2n
The weird thing is, the Arduino core on ESP32 has an abs() macro as well: https://github.com/espressif/arduino-esp32/blob/master/cores/esp32/Arduino.h#L76
So there is probably a bug in the Arduino core for ESP32 here, where it isn't using the "Arduino-ified" abs()
macro for some reason.
Are you able to please open an issue over at https://github.com/espressif/arduino-esp32/ and refer back to this post?
Are you able to please open an issue over at https://github.com/espressif/arduino-esp32/ and refer back to this post?
Never mind, I think I see the root cause. See linked PR.
IMO it's better to use the
Agreed, provided the Arduino core can keep compatibility with what "normal" Arduino cores do. Linked PR should achieve both goals.
edit, PS: just found out that also std:: has proprietary function, i.e. abs, fabs, fabsf strangely abs() at C++11 + C++14 were not overloaded for different var types
abs() for fp is only from C++17
(since C++17) float abs( float arg ); (1)
double abs( double arg );
@dsyleixa Where did you find this info?
I think as long as cstdlib or csmith is included (not the C stdlib.h or math.h), there should be overloads for other types as well, at least as far back as C++11: https://stackoverflow.com/a/13460915/1006619
g++ seems to be happy with floats & doubles back as far as C++98: https://gcc.godbolt.org/z/Flkq60
(You can see the libstdc++ function names mentioned in the disassembly on the right.)
my reference was: https://en.cppreference.com/w/cpp/numeric/math/fabs
(since C++17) // <<~~~~~very small printed
float abs( float arg ); | (1) |
double abs( double arg ); | (2) |
butgood to know that for gpp it will work though even for C++11/14
Closing as the PR with the fix has merged to arduino-esp32 repo.
Arduino IDE 1.8.8 different ESP32 MCUs (Adafruit Feather ESP32, ESP32 Dev Board, WROOM32) vs. different ARM Cortex boards (Adafruit Feather M4, Arduino Zero (M0), Arduino Due (ATSAM3X8E) Arduino AVR Mega2560
In the following code all ARM Cortex Boards and the Arduino Mega evalutate to 32.843750, whilst all ESP32 evaluate to 54196.625
Ref.: "HaWe" (ESP32, M3, M4, Mega2560): https://www.roboternetz.de/community/threads/73360-ESP32-berechnet-floats-falsch-im-Vergleich-zu-M4-und-Due-M3
"Ceos" (ESP32): https://www.roboternetz.de/community/threads/73360-ESP32-berechnet-floats-falsch-im-Vergleich-zu-M4-und-Due-M3?p=651789&viewfull=1#post651789
"BDL" (M4): https://forums.adafruit.com/viewtopic.php?f=57&t=151070&p=746220#p746147
"MartinL" (ESP32, M0): http://forum.arduino.cc/index.php?topic=613077.msg4157601#msg4157601
(to be continued...)
application source code (issue coincidentally detected): the questionable value is the last one in either Serial output line (TBase.m)
changing float (fp32) to double (fp64) makes no difference basically.