Math refuses to return the expected value

I have recently discovered that I like tinkering with electronics.

I build entries, automatic gates, welded types of fencing, and railing. For some reason mechanical measuring wheels won't last. So I am building a digital measuring wheel as my first legit arduino project. I want it to measure in inches, feet, and mileage. I have the displayed inches resetting to 0 inches (the total inches keeps on incrementing in the background, verified by serial monitor) at every foot. I do not have any decimal places on the inches, nor do I want decimal places on the inches. I have my footage displaying to the hundredth of a foot, and the mileage displaying to the thousandth of a foot. Because why not?

Here is my issue. As far as I can tell, the inches increment correctly. The footage increments correctly. BUT the mileage refuses to increment correctly. One thousandth of a mile equals 5.28 feet. Right?? So I am expecting to see the mileage displayed as .001 when the footage rolls over to 5.28, or somewhere between 5.2 and 5.3 if only displaying to the tenth position. I have tried using footage to make the formula. I have tried using inches to make the formula. I have tried multiple different formulas. At least a dozen. I have tried different decimal places. No matter what, the mileage displays .001 when the footage hits about 2.8 feet. Given this, you would think the mileage would display .002 when the footage hits 5.6 feet, but that's not the case. The mileage incrementing seems to be exponential, not linear.

I have resorted to asking 3 different AI programs for help before asking you folks. But I get NOWHERE there.

I am using an elegoo arduino uno R3 and a Taiss encoder model #E38S6-600-240 off of amazon (which by the way has 2400 pulses per revolution, not 600. I think I understand why.) My lcd is a Hiletgo 2004 I2C from amazon.

Here is the last code I used. I know there is some coding for a button. I do not have it set up yet. I know there are also some variables for another encoder (a future menu selector) that is not programmed yet. I am just trying to figure the mileage math problem before I move on. I have verified that the display is the same on my LCD screen and in the serial monitor. The serial monitor returns the same results. I have also verified that the wheelEnc is giving 2400 pulses per revolution. I also get the proper inch and footage measurement per revolution, no matter what my variable wheel diameter is set to. So those seem to be working. It is just the mileage that is stubborn.

Any thoughts?

/* My Measuring Wheel Project. Calculations based off of
     wheel diameter and pulses per revolution.
     Adjust these two value based on actual products used.
*/

#include <Encoder.h>
#include <LiquidCrystal_I2C.h>

LiquidCrystal_I2C lcd(0x27, 20, 4);

// Change these pin numbers to the pins connected to your encoder.
// Best Performance: both pins have interrupt capability
// Good Performance: only the first pin has interrupt capability
// Low Performance: neither pin has interrupt capability
// On Arduino UNO, pins 2 and 3 are interrupt pins
Encoder wheelEnc(2, 4);
//Encoder menuEnc(3,5);

volatile long wheelPulseCount;

float totalInches = 0.0;
static float lastTotalInches = 0.0;
float inchesDisplayed = 0.0;
float totalFootage = 0.0;
float footageDisplayed = 0.0;
float totalMileage = 0.0;
float mileageDisplayed = 0.0;
const float inchesPerMile=63360.0;
const float inchesPerFoot=12.0;
const float feetPerMile=5280.0;
int reset = 10;

// Wheel size variables
float diameter = 15.0;//Change as needed for whichever wheel size
float inchesPerPulse;
float wheelInches;
float pi=M_PI;

int pulsesPerRev = 2400;//Change as needed depending on encoder used

void setup() {
  // Serial.begin(230400);
  // Serial.println("Measurement:");
  lcd.init();
  lcd.backlight();
  lcd.clear();
  lcd.setCursor(8,1);
  lcd.print("CFG");
  delay (3000);
  lcd.clear();
  lcd.setCursor (2,0);
  lcd.print("READY TO MEASURE");
  pinMode(reset, INPUT_PULLUP);
  
}

long wheelCountStart = 0;

void loop() {
  wheelPulseCount = wheelEnc.read();//Read the pulses from encoder
  
  wheelInches = diameter * pi;//Calculate the circumference of wheel. 
  inchesPerPulse = wheelInches / pulsesPerRev;//Used to calculate inches covered per pulse.
  
  if (wheelPulseCount != wheelCountStart) {//Starts the counting process
    wheelCountStart = wheelPulseCount;
  }

  totalInches = wheelPulseCount * inchesPerPulse;//Calculate total inches covered
  totalFootage = floor(totalInches/inchesPerFoot);//Calculate the footage
  totalMileage = totalFootage/feetPerMile;//Calculate the mileage. 

  // Calculate the remaining inches within a foot
  inchesDisplayed = abs(static_cast<int>(totalInches)) % 12;/*The % sign uses float, but total
                                                       inches is an int, so static_cast<int>
                                                       turns it back to an integer. ABS means use
                                                       absolute value. Prevents incehs from being 
                                                       displayed in negatives.*/
  footageDisplayed = totalFootage + (inchesDisplayed / 12.0);//Displays footage
  mileageDisplayed = totalMileage;//Displays mileage

  if (totalFootage<0){
     totalFootage=0;
  }

  if (totalMileage<0){
     totalMileage=0;
  }
 
  if (totalFootage>99999 || totalFootage<-99999){
     totalFootage=0;
  }

  if (lastTotalInches != totalInches) { /*This if statement prevents constant monitoring of the 
                                        serial monitor. It only displays new data when the data 
                                        changes*/
    lastTotalInches = totalInches; // Resets if statements

     lcd.setCursor (0,2);
     lcd.print (" IN      FT      MI");
     lcd.setCursor(0,3);
     lcd.print("    ");
     lcd.setCursor(0,3);
     lcd.print (inchesDisplayed,0);
     lcd.setCursor (7,3);
     lcd.print("      ");
     lcd.setCursor(7,3);
     lcd.print(footageDisplayed,2);
     lcd.setCursor(15,3);
     lcd.print("      ");
     lcd.setCursor(15,3);
     lcd.print(mileageDisplayed,3);

  }

  if (digitalRead(reset) == LOW) {//If RESET button is pressed
    delay(100);//debounce. Could change to millis()
    Serial.print("RESET");
    wheelEnc.write(0);//Resets encoder counts to zero and displays all zeros on serial monitor
  }
}

No surprises there. No actual "AI" involved.

Please state which Arduino you are using. The default size of int varies with MCU, and you may be suffering from integer overflow.

This sort of statement is not advised if you don't know those rules (and the "AI" bots do not), also the comment is not correct.

 inchesDisplayed = abs(static_cast<int>(totalInches)) % 12;/*The % sign uses float, but total

Since the basic unit of measurement is the integer counts produced by the encoder, my approach would be to use long integer data type to keep track of counts, and do all the math on those.

1 Like

I don't know the library encoder.h but it appears to use interrupts, unless you explicitly tell it not to with #define ENCODER_DO_NOT_USE_INTERRUPTS, which could imply you need a critical section in your loop() to read any value it sets.

Maybe around here:
wheelPulseCount = wheelEnc.read();//Read the pulses from encoder

Not required with the Arduino Encoder library. It is very cleverly written.

I am using arduino uno R3.

I don't remember why i coded this line the way i did, but why is it wrong?

inchesDisplayed = abs(static_cast(totalInches)) % 12;

Please use code tags when posting code, even a single line. As you see, the line that appears in the post above is not what is in the code.

On an Arduino Uno R3, an "int" has maximum value of 32767. In inches, that is around 0.5 miles.

To debug your code, put in Serial.print() statements to check the value of variables at each step in a series of calculations, and compare those with what you expect (or get with a pocket calculator).

Looking at your math, you track the total number of inches, then divide it into total feet, and then divide that result into miles all using float variables. Each step introduces inaccuracies due to the properties of float variables. A more accurate method would be to calculate the miles by dividing the total inches by the number of inches per mile.

Personally, I would keep track of the total number of wheel rotations in an unsigned long variable and the current portion of the next wheel rotation with an unsigned int. From there, I would derive the total inches, feet, and miles individually from the number of rotations at each display update. Of course, also send all 5 variables out the serial port as well.

For additional information, learning how microcontrollers store integer variables and float variables will help you understand what is happening within your Arduino hardware and Arduino sketch. Also of note, floating point variables may be stored as either float or double.

See:
https://docs.arduino.cc/language-reference/en/variables/data-types/float/

https://docs.arduino.cc/language-reference/en/variables/data-types/double/

1 Like

when i simulate your code on my laptop and print various values i see the following, and the mileage becoming 0.001 when the feet is > half of 5.28

       1400 pulse      27.49 tot in       2.00 tot ft      0.000 tot mi,     0.000 mi
       1500 pulse      29.45 tot in       2.00 tot ft      0.000 tot mi,     0.000 mi
       1600 pulse      31.42 tot in       2.00 tot ft      0.000 tot mi,     0.000 mi
       1700 pulse      33.38 tot in       2.00 tot ft      0.000 tot mi,     0.000 mi
       1800 pulse      35.34 tot in       2.00 tot ft      0.000 tot mi,     0.000 mi
       1900 pulse      37.31 tot in       3.00 tot ft      0.001 tot mi,     0.001 mi
       2000 pulse      39.27 tot in       3.00 tot ft      0.001 tot mi,     0.001 mi
       2100 pulse      41.23 tot in       3.00 tot ft      0.001 tot mi,     0.001 mi
       2200 pulse      43.20 tot in       3.00 tot ft      0.001 tot mi,     0.001 mi
       2300 pulse      45.16 tot in       3.00 tot ft      0.001 tot mi,     0.001 mi
       2400 pulse      47.12 tot in       3.00 tot ft      0.001 tot mi,     0.001 mi
       2500 pulse      49.09 tot in       4.00 tot ft      0.001 tot mi,     0.001 mi
       2600 pulse      51.05 tot in       4.00 tot ft      0.001 tot mi,     0.001 mi
       2700 pulse      53.01 tot in       4.00 tot ft      0.001 tot mi,     0.001 mi
       2800 pulse      54.98 tot in       4.00 tot ft      0.001 tot mi,     0.001 mi

        printf ("  %9d pulse", wheelPulseCount);

        printf ("  %9.2f tot in", totalInches);
        printf ("  %9.2f tot ft", totalFootage);
        printf ("  %9.3f tot mi", totalMileage);

        printf (", %9.3f mi", mileageDisplayed);
        printf ("\n");

I have used Serial.prints all over that code and still cannot figure out why the mileage will not calculate correctly. It just displays the math but does not reveal why. I will try it again. There has to be something I am missing.

I still don't know why

inchesDisplayed = abs(static_cast<int>(totalInches)) % 12;/*The % sign uses float, but total

is wrong. If I remember correctly this is what I found after hours of google and youtube to get the inch display to go back to zero instead of incrementing on up 12, 13, 14 etc... This is supposed to insure that totalInches continues to increment in the background while only displaying 0-11 inches. At 12 inches the display returns to 0 instead of 12.

I do remember for sure I used abs to prevent viewing negative inches if the wheel is rolled backwards below 0 totalInches.

Off to do more serial prints.

jeremy97469 and gcjr I just saw your comments.

jeremy97469 I have tried to get the mileage using inches to calculate it. I get the same results. I will try the suggestion about keeping track of the wheel rotations. And I will read the links you posted. Edited-I did read these links. It is my understanding that on my arduino uno, using double is pointless. But I did see something interesting to me. The round function. The example shows it rounding up. But does it round down as well?

gcjr Yes your laptop is simulating my results. But why?? I am not sure what printf and %9 mean in the code you posted. Remember I am new to this.

Yes, the dividing line is 0.5, but I can't think of a use for round() in your project.

not sure what printf and %9

Features that are not available on an Uno R3 and are not needed for your project.

If you use a long integer to keep track of encoder counts (internally declared int32_t in the Encoder library), and properly convert counts into feet, miles etc. using float variables and functions, you should not encounter significant math errors.

1 Like

Encoder has NPN, open collector outputs, is your hardware properly set up for that? What is the circumference or your measuring wheel?

Here is an easy way to convert encoder counts to miles + ft + in, with no significant error.
Totalizer wheel diameter = 15.0 inches, 2400 counts/rev encoder:

Note, if the wheel is driven backwards, counts will be subtracted. That is the correct behavior and should not be overridden by code.

void setup() {
  Serial.begin(115200);
  while (!Serial);
  Serial.println("Wheel encoder totalizer");
  float diameter = 15.0; //inches
  int counts_per_rev = 2400;
  float inches_per_count = M_PI * diameter / counts_per_rev;

  // make up some distances in encoder counts and convert to miles + ft + in
  
  for (long int i = 0; i < 10000001; i += 200000) {
    float total_counts = i;
    float total_inches = total_counts * inches_per_count;
    float miles = floor(total_inches / 63360.0);  //integral miles
    total_inches = total_inches - miles * 63360.0; //remainder in inches
    float feet = floor(total_inches / 12.0); //integral feet
    total_inches = total_inches - feet * 12.0; // remainder in inches
    Serial.print(i);
    Serial.print(" counts, ");
    Serial.print(miles, 0);
    Serial.print(" mi, ");
    Serial.print(feet, 0);
    Serial.print(" ft, ");
    Serial.print(total_inches, 2);
    Serial.println(" in.");
  }
}

void loop() {}

Output:

Wheel encoder totalizer
0 counts, 0 mi, 0 ft, 0.00 in.
200000 counts, 0 mi, 327 ft, 2.99 in.
400000 counts, 0 mi, 654 ft, 5.98 in.
600000 counts, 0 mi, 981 ft, 8.97 in.
800000 counts, 0 mi, 1308 ft, 11.96 in.
1000000 counts, 0 mi, 1636 ft, 2.96 in.
1200000 counts, 0 mi, 1963 ft, 5.95 in.
1400000 counts, 0 mi, 2290 ft, 8.94 in.
1600000 counts, 0 mi, 2617 ft, 11.93 in.
1800000 counts, 0 mi, 2945 ft, 2.92 in.
2000000 counts, 0 mi, 3272 ft, 5.91 in.
2200000 counts, 0 mi, 3599 ft, 8.90 in.
2400000 counts, 0 mi, 3926 ft, 11.89 in.
2600000 counts, 0 mi, 4254 ft, 2.88 in.
2800000 counts, 0 mi, 4581 ft, 5.87 in.
3000000 counts, 0 mi, 4908 ft, 8.86 in.
3200000 counts, 0 mi, 5235 ft, 11.86 in.
3400000 counts, 1 mi, 283 ft, 2.84 in.
3600000 counts, 1 mi, 610 ft, 5.84 in.
3800000 counts, 1 mi, 937 ft, 8.83 in.
4000000 counts, 1 mi, 1264 ft, 11.82 in.
4200000 counts, 1 mi, 1592 ft, 2.81 in.
4400000 counts, 1 mi, 1919 ft, 5.80 in.
4600000 counts, 1 mi, 2246 ft, 8.79 in.
4800000 counts, 1 mi, 2573 ft, 11.78 in.
5000000 counts, 1 mi, 2901 ft, 2.77 in.
5200000 counts, 1 mi, 3228 ft, 5.77 in.
5400000 counts, 1 mi, 3555 ft, 8.76 in.
5600000 counts, 1 mi, 3882 ft, 11.74 in.
5800000 counts, 1 mi, 4210 ft, 2.73 in.
6000000 counts, 1 mi, 4537 ft, 5.73 in.
6200000 counts, 1 mi, 4864 ft, 8.72 in.
6400000 counts, 1 mi, 5191 ft, 11.71 in.
6600000 counts, 2 mi, 239 ft, 2.70 in.
6800000 counts, 2 mi, 566 ft, 5.69 in.
7000000 counts, 2 mi, 893 ft, 8.69 in.
7200000 counts, 2 mi, 1220 ft, 11.67 in.
7400000 counts, 2 mi, 1548 ft, 2.67 in.
7600000 counts, 2 mi, 1875 ft, 5.66 in.
7800000 counts, 2 mi, 2202 ft, 8.64 in.
8000000 counts, 2 mi, 2529 ft, 11.64 in.
8200000 counts, 2 mi, 2857 ft, 2.63 in.
8400000 counts, 2 mi, 3184 ft, 5.63 in.
8600000 counts, 2 mi, 3511 ft, 8.61 in.
8800000 counts, 2 mi, 3838 ft, 11.59 in.
9000000 counts, 2 mi, 4166 ft, 2.59 in.
9200000 counts, 2 mi, 4493 ft, 5.58 in.
9400000 counts, 2 mi, 4820 ft, 8.58 in.
9600000 counts, 2 mi, 5147 ft, 11.56 in.
9800000 counts, 3 mi, 195 ft, 2.56 in.
10000000 counts, 3 mi, 522 ft, 5.55 in.
1 Like

Once the value hits 0.0005, forcing it to 3 decimal places will show it as 0.001. It will stay there until it hits 0.0015. Something similar for any given number of places. Going one pulse at a time

       1832 pulse      35.97 tot in       2.00 tot ft      0.000 tot mi,     0.000 mi
       1833 pulse      35.99 tot in       2.00 tot ft      0.000 tot mi,     0.000 mi
       1834 pulse      36.01 tot in       3.00 tot ft      0.001 tot mi,     0.001 mi

which is because of floor:

  • 0.0005 miles is 31.680 inches
  • At two feet, 24 inches is less
  • At three feet, 36 inches is more, and rounded up to 0.001 miles

OK. I think I can see it although the library code is not particularly easy to read because it is somewhat obscured by desperate looking optimisation in some places.

The library function int32_t read(), which the user calls from the loop(), actually handles any necessary suspension of interrupts. In this case, during the creation of the return value ret (encoder.position) which is then passed back to the caller.

From: Encoder/Encoder.h at master · PaulStoffregen/Encoder · GitHub

#ifdef ENCODER_USE_INTERRUPTS
	inline int32_t read() {
		if (interrupts_in_use < 2) {
			noInterrupts();
			update(&encoder);
		} else {
			noInterrupts();
		}
		int32_t ret = encoder.position;
		interrupts();
		return ret;
	}
. . .
. . .
. . .

So, indeed, it appears that the user does not have to consider a critical section here.

kenB4--I was thinking that my issue is due to cumulative rounding errors. Taking out floor does indeed raise the footage value a hair before mileage hits .001, but still nowhere close to the 5.28 it should show.

I did change my formulas to all derive from the inch, footage, and mileage distance around the wheel. So instead of deriving the mileage from inches and then feet, I am now deriving it directly from the pulse counts. Inches and footage work but mileage is still no dice. Is the only way to get rid of rounding errors to use ints? Or unsigned longs? If those are the only way, they will not produce decimal points correct?

I have to go to work now, but will be thinking about this and will alter my code later today and see what happens.

Float has less significant numbers (approx.7) than unsigned long (>9) So: perform all calcs in unsigned long...
Never use a float as a counter... instead of rolling over it will simply not increment anymore for high values...
1E10 + 1 = 1E10
Instead of
4,294,967,295 + 1 = 0
You might need to catch and properly handle the rollover of unsigned long.

You might use uint16_t for your inch counter and handle the rollover at 1 mile...(63360 inches) (depending on which type of mile...).

There are rounding errors, but the main problem is (again) just plain rounding

void setup() {
  Serial.begin(115200);
}

void printRounded(float f, unsigned places) {
  Serial.print(f, places);
}

void printTruncated(float f, unsigned places) {
  for (int i = 0; i < places; i++) {
    f *= 10;
  }
  f = trunc(f);
  for (int i = 0; i < places; i++) {
    f /= 10;
  }
  Serial.print(f, places);
}

float examples[] = {
  2.62, 2.63, 2.64, 2.65,
  5.26, 5.27, 5.28, 5.29,
};
unsigned xi;

void loop() {
  if (xi >= sizeof(examples) / sizeof(examples[0])) {
    return;
  }
  float footage = examples[xi++];
  float miles = footage / 5280;
  Serial.print(footage, 3);
  Serial.print('\t');
  Serial.print(footage, 7);
  Serial.print('\t');
  printRounded(miles, 3);
  Serial.print('\t');
  printTruncated(miles, 3);
  Serial.print('\t');
  Serial.print(miles, 7);
  Serial.println();
}

Don't have an Uno R3, but on the simulator, it prints

2.620	2.6199998	0.000	0.000	0.0004962
2.630	2.6300001	0.000	0.000	0.0004981
2.640	2.6400001	0.000	0.000	0.0005000
2.650	2.6500000	0.001	0.000	0.0005019
5.260	5.2600002	0.001	0.000	0.0009962
5.270	5.2699999	0.001	0.000	0.0009981
5.280	5.2800002	0.001	0.001	0.0010000
5.290	5.2899999	0.001	0.001	0.0010019

which has only one surprising value. For fun, the same thing on ESP32


2.620	2.6199999	0.000	0.000	0.0004962
2.630	2.6300001	0.000	0.000	0.0004981
2.640	2.6400001	0.001	0.000	0.0005000
2.650	2.6500001	0.001	0.000	0.0005019
5.260	5.2600002	0.001	0.000	0.0009962
5.270	5.2700000	0.001	0.000	0.0009981
5.280	5.2800002	0.001	0.001	0.0010000
5.290	5.2900000	0.001	0.001	0.0010019

Build_1971

If I perform all of my calculations in unsigned long I lose my decimal precision right? I need some precision because if a 15" diameter wheel has a 47.124" circumference, then I stand to either lose .876" per revolution or gain .124" per revolution, which will quickly turn into wrong measurements. Bear with me. I am just trying to understand.

jremington--I tried your totalizer. For some reason, the mileage would not register at all. It just stayed 0.000.

I'm going to keep googling, reading, and youtubing. Maybe something will jump out at me that makes sense to me.

Most likely a mistake in the code you forgot to post.

The code I posted just converts encoder counts to the distance units used only in the U.S.