Highly Inaccurate Speedometer

Hi Guys

I am battling to get realistic speed and millis/revolution readings from the following code!

/*
  Example 37.3 – Basic speedometer using millis();
  http://tronixstuff.wordpress.com/tutorials > chapter 37
  John Boxall | CC by-sa-nc
*/

#include "Wire.h"
#include "LiquidCrystal_I2C.h"
LiquidCrystal_I2C lcd(0x27,16,2);  // set the LCD address to 0x27 for a 16 chars and 2 line display

float start, finished;
float elapsed, time;
float circ=0.88; // wheel circumference relative to sensor position (in meters)
float speedkph;  // holds calculated speed values in kilometres per hour
const int debounce = 50;  // debounce in milliseconds

void setup() 
{
  attachInterrupt(0, speedCalc, HIGH); // interrupt called when sensors sends digital 2 high (every wheel rotation)
  start=millis();
  // setup LCD
  lcd.init();      // initialize the lcd
  lcd.backlight(); // turn on LCD backlight
  lcd.clear();
  lcd.println(" Initialising... "); 
  delay(3000);
  lcd.clear();
  Serial.begin(9600);
}

void speedCalc()
{
  elapsed=millis()-start;
  if (millis()-start > debounce) {
    start = millis(); 
  }
  speedkph=(3600*circ)/elapsed; // kph
}

void loop() 
{
  Serial.println(elapsed);
  Serial.println(speedkph);
  Serial.println();
  lcd.setCursor(0,0);
  lcd.print(speedkph);
  lcd.print(" km/h ");  
  lcd.setCursor(0,1);
  lcd.print(int(elapsed));
  lcd.print(" ms/rev      ");  
  delay(1000); // adjust for personal preference to minimise flicker
}

This is my physical setup on an Arduino Uno R3 (I2C LCD setup not shown in this image, but I am using the newly included SCL and SDA connections next to the AREF connector on the new UNO R3)

Instead of the push button, I am using a normally open MMPSA 240/100 reed switch and matching magnet

, here is it's datasheet: http://www.farnell.com/datasheets/1673516.pdf

My setup is a frame with a spinning disk 28cm in diameter, so circumference = pi*28 = 87.96 which I have rounded off to a neat 88cm or 0.88 metres as the code requires circumference to be in metres! I have the reed switch attached to the frame and the magnet is attached to the disks edge (because I don't have a bicycle wheel on hand at this point).

If I spin the disk at roughly 1 revolution per second I should be getting a reading of approx: 3.16kph, eg: 0.88 * 60 seconds * 60 minutes = 3.16kph

But with my setup and the code above I am getting speeds of over 100kph for roughly 1 revolution/second being recorded on the LCD display. So my question is, is the code above appropriate or is the reed switch shown above just hopeless?

Regards
Grant

Use 'unsigned long' instead of 'float' for time values.

Just calculate 'elapsed' in the interrupt routine. Save the heavy speed calculation for your loop() where it isn't time critical.

I'm guessing the contact bounce is causing some of your problems. You calculate 'elapsed' and a new speed value for every bounce. You should probably change the code to only calculate a new 'elapsed' when the interrupt happens more than 'debounce' milliseconds after 'start':

void speedCalc()
{
  unsigned long now = millis();
  if (now - start > debounce) {
    elapsed = now - start;
    start = now; 
  }
}
attachInterrupt(0, speedCalc, HIGH);

Should be either RAISING or FALLING to detect the leading edge of the signal. Using HIGH means many more interrupts will happen then you think should happen as the magnet passed the sensor.

Lefty

There is no HIGH interrupt.

#define HIGH 0x1

...

#define CHANGE 1
#define FALLING 2
#define RISING 3

Therefore using HIGH is exactly the same (but confusing) as using CHANGE.

"HIGH" isn't a valid parameter to interrupt: the chip can generate interrupts on low, any change, or falling / rising edges.

Your isr is also incorrect: you should only calculate the speed if the triggering is valid (aka not a bounce).

Hi Guys

Thanks for all the input, I have taken the advice and examples given by members who commented on this post and have come up with the following speedometer code below:

#include "Wire.h"
#include "LiquidCrystal_I2C.h"

#define CHANGE 1
#define FALLING 2
#define RISING 3

LiquidCrystal_I2C lcd(0x27,16,2);  // set the LCD address to 0x27 for a 16 chars and 2 line display

unsigned long start, finished;
unsigned long elapsed, time;
float circ=0.88; // wheel circumference relative to sensor position (in meters)
float speedkph;  // holds calculated speed values in kilometres per hour
const int debounce = 20;  // debounce in milliseconds

void setup() 
{
  attachInterrupt(0, speedCalc, RISING); // interrupt called when rising edge is detected on digital pin 2
  start=millis();
  // setup LCD
  lcd.init();      // initialize the lcd
  lcd.backlight(); // turn on LCD backlight
  lcd.clear();
  lcd.println(" Initialising... "); 
  delay(3000);
  lcd.clear();
  Serial.begin(9600);
}

void speedCalc()
{
  unsigned long now = millis();
  if(digitalRead(0) == HIGH) // check if the pin is really high
  {
    if (now - start > debounce)
    {
      elapsed = now - start;
      start = now;
      speedkph=(3600*circ)/elapsed; // kph
    }
  }
}

void loop() 
{
 
  Serial.println(elapsed);
  Serial.println(speedkph);
  Serial.println();
  lcd.setCursor(0,0);
  lcd.print(speedkph);
  lcd.print(" km/h ");  
  lcd.setCursor(0,1);
  lcd.print(int(elapsed));
  lcd.print(" ms/rev      ");  
  delay(100); // adjust for personal preference to minimise flicker
}

And I can happily say that the speedometer is now outputting very realistic readings for kph and milliseconds per revolution! And as can be seen in the code above, I followed Johns advice of using unsigned longs for time values as opposed to floats. I also experimented by using RISING and FALLING as parameters within attachInterrupt as opposed to HIGH which as Nick pointed out is the same as CHANGE.

As it turned out, interrupting on the RISING edge gave the best results, I also had no idea that HIGH was not a valid parameter and would act in the same manner as CHANGE, so I am pretty sure that that was the main reason for the highly unrealistic readings I was getting.

As can be seen I have also added my own code for reading the pin state [digitalRead(0)] for making absolutely sure that digital pin 2 has gone high, although I am not 100% sure that doing a digitalRead within an ISR is good practice or not, but it does seem to ensure that time values only get affected when digital pin 2 is HIGH and time values are met.

I am not even sure if my debounce code is appropriate or not but as I said, I am getting pretty good readings!

Regards
Grant

I am not 100% sure that doing a digitalRead within an ISR is good practice or not,...

That's OK. Inside a CHANGE interrupt in particular, you may well need to see which way it changed.

Got it!

Thanks Nick, You guys have all been a great help...