DIY Bike Computer - Calculating average speed using hall sensors

I have built a bike computer, I'm implementing a couple of hall sensors to calculate wheel and crank rpm and subsequently calculate the speed, distance and cadence on a tiny Nokia 5110 display.

Problem 1:
I need some help to calculate average speed achieved over a trip.

So I know if I divide the total distance traveled by the total time taken to cover the distance, I get the average speed.

What I don't know is how I can calculate the time taken to cover the distance.

Problem 2:
The display indicates a certain speed and cadence when the bicycle is moving and the pedals are cranked. But, when the pedals are not being cranked, the cadence indicator does not zero and shows the last calculated value.

Same goes for the speed indicator, when the bike comes to a stand still, the display does not show zero, but shows the last calculated value.

I do have a clue as to what's causing this, but I'm unsure about how to fix it.

I believe, since the hall sensor pulses are being read by the microcontroller on an interrupt based algorithm that measures the time intervals between interrupts, the code infinitely measures the time interval until the next interrupt and the indicator is stuck in the last computed value.

/*Display*/
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_PCD8544.h>
/* Software SPI:
  pin 7 - Serial clock out (SCLK)
  pin 6 - Serial data out (DIN)
  pin 5 - Data/Command select (D/C)
  pin 4 - LCD chip select (CS)
  pin 8 - LCD reset (RST) */
Adafruit_PCD8544 display = Adafruit_PCD8544(7, 6, 5, 4, 8);

/*Wheel*/
float wheelDiameter = 660.00; // In millimeters
float wheelSpeed;
float distance;
float rpmWheel;
volatile byte revWheel;
unsigned long timeWheel;

/*Cadence*/
int rpmCrank;
volatile byte revCrank;
unsigned long timeCrank;

/*Button*/
byte button;
byte state;
byte oldButton = 0;
byte buttonPin = 12;

void setup() {
  Serial.begin (9600);

  /*Display*/
  display.begin();
  display.clearDisplay();

  /*Wheel*/
  attachInterrupt(0, wheelCounter, RISING);
  revWheel = 0;
  rpmWheel = 0;
  timeWheel = 0;
  distance = 0;

  /*Cadence*/
  attachInterrupt(1, crankCounter, RISING);
  revCrank = 0;
  rpmCrank = 0;
  timeCrank = 0;
}

void loop() {
  /*Sensor*/
  hallSense ();

  /*Display*/
  displayContrast ();
  displayPage1 ();

  /*Print to serial monitor*/
  //printVal ();
}

void hallSense () {
  /*Wheel*/
  if (revWheel >= 1) {
    detachInterrupt(0);
    rpmWheel = 60000.0 / (millis() - timeWheel) * revWheel;
    wheelSpeed = (rpmWheel * wheelDiameter * 3.141 * 60) / 1000000;
    timeWheel = millis();
    revWheel = 0;
    attachInterrupt(0, wheelCounter, RISING);
  }

  /*Cadence*/
  if (revCrank >= 1) {
    detachInterrupt(1);
    rpmCrank = 60000.0 / (millis() - timeCrank) * revCrank;
    timeCrank = millis();
    revCrank = 0;
    attachInterrupt(1, crankCounter, RISING);
  }
}

/*Wheel*/
void wheelCounter()
{
  revWheel++;
  distance = distance + (wheelDiameter * 3.141 / 1000000); // In kilometers
}

/*Cadence*/
void crankCounter()
{
  revCrank++;
}

/*Button [unused]*/
void displayPage () {
  button = digitalRead(buttonPin);
  if (button && !oldButton)
  {
    if (state == 0)
    {
      displayPage1 ();
      state = 1;
    }
    else
    {
      displayPage2 ();
      state = 0;
    }
    oldButton = 1;
  }
  else if (!button && oldButton)
  {
    oldButton = 0;
  }
}

/*Display*/
void displayContrast () {
  short potVal = map(analogRead(A7), 0, 1023, 0, 127);
  display.setContrast(potVal);
}

void displayPage1 () {
  display.clearDisplay();
  display.setTextColor(BLACK);
  display.setCursor(0, 0);
  display.print("Speed:");
  display.print(wheelSpeed);
  display.setCursor(0, 10);
  display.print("Trip :");
  display.print(distance);
  display.setCursor(0, 20);
  display.print("Average:");
  display.print(rpmCrank);
  display.setCursor(0, 30);
  display.print("Cadence:");
  display.print(rpmCrank);
  display.display();
}

void displayPage2 () { // [UNUSED]
}

/*Serial monitor
void printVal () {
  Serial.print("RPM= ");
  Serial.print(rpmWheel);
  Serial.print ("\t");
  Serial.print ("Km= ");
  Serial.print(distance);
  Serial.print ("\t");
  Serial.print("Cadence= ");
  Serial.println(rpmCrank);
}*/

Problem 1: A clock. You said you use the time to calculate the distance, that means you already have it.

Problem 2: You have already identified the problem in a way that identifies the solution. Your algorithm needs to set a minimum speed since a speed of zero can not be measured.

Nice looking prototype!

If you expect it to detect the start and stop times automatically, you have to deal with problem 2 first.

So you would want to stop the clock when you are below a minimum velocity, like when your stopped or walking the bike.

I am not too familiar with the arduino at this point but you can set an interrupt every second while you are above that min. velocity. The interrupt just increments a counter or a variable. You can use a 16 bit counter to give you 18 hours of riding time.

Your distance is just your revolutions x circumference of a wheel.

aarg:
Problem 1: A clock. You said you use the time to calculate the distance, that means you already have it.

Problem 2: You have already identified the problem in a way that identifies the solution. Your algorithm needs to set a minimum speed since a speed of zero can not be measured.

Nice looking prototype!

If you expect it to detect the start and stop times automatically, you have to deal with problem 2 first.

Thank you, Aarg!

noweare:
So you would want to stop the clock when you are below a minimum velocity, like when your stopped or walking the bike.

I am not too familiar with the arduino at this point but you can set an interrupt every second while you are above that min. velocity. The interrupt just increments a counter or a variable. You can use a 16 bit counter to give you 18 hours of riding time.

Your distance is just your revolutions x circumference of a wheel.

Thanks, Noweare!

So I gave some thought on what you guys suggested and of course, I need to figure out problem 2 first.

To calculate average speed, I wrote this bit using the millis() function, now I know the millis() keeps time from the moment the code starts to iterate which could make it very inaccurate if it times through the stop duration. so I add an 'if' condition making sure it runs only when the speed is greater than or equal to 3 Km/h.

void Average () {
  if (wheelSpeed >= 3) {
    unsigned long tripTime = millis ();
    avg = distance / (tripTime * 3600000);
  }
}

Also, I'm not sure if this is the correct way to do it, I added another 'if' and 'else' condition at the print function, so it prints values to the display only when the speed is greater than or equal to 3 Km/h and shows "0.00" when under the prescribed speed.

void displayPage1 () {
  if (wheelSpeed >= 3) {
    display.clearDisplay();
    display.setTextColor(BLACK);
    display.setCursor(0, 0);
    display.print("Speed:");
    display.print(wheelSpeed);
    display.setCursor(0, 10);
    display.print("Trip :");
    display.print(distance);
    display.setCursor(0, 20);
    display.print("Average:");
    display.print(avg);
    display.setCursor(0, 30);
    display.print("Cadence:");
    display.print(rpmCrank);
    display.display();
  }
  else {
    display.clearDisplay();
    display.setTextColor(BLACK);
    display.setCursor(0, 0);
    display.print("Speed:");
    display.print("0.00");
    display.setCursor(0, 10);
    display.print("Trip :");
    display.print(distance);
    display.setCursor(0, 20);
    display.print("Average:");
    display.print(avg);
    display.setCursor(0, 30);
    display.print("Cadence:");
    display.print("0.00");
    display.display();
  }
}

Please do point out where I might have gone wrong.

So my attempt to null the speedometer and cadence values on standstill using an 'if' condition checking if the speed is under a prescribed limit failed to work reliably. It did sort of work, but with a lot of glitches. Going to try the while() loop to see if that helps.

On the other hand, I managed to figure out the average speed bit of the code and it seems to work like it's supposed to.

Updated code with average speed indicator:

/* Lethal Lab | Anjan Babu
   [Facebook]   https://www.facebook.com/lethallab
   [Blog]       http://anjanbabu.wordpress.com

   BYK 1.0 | Basic Bike Computer
     Basic Bike Computer
     Speedometer
     Tachometer
     Odometer
     Cadence Meter
     LED RPM run-out Indicator

   Tested with:
     16x4 LCD Display
     Arduino Pro Mini 5V 16MHz
     AH44e hall-effect sensor
*/

/*Display*/
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_PCD8544.h>
/* Software SPI:
  pin 7 - Serial clock out (SCLK)
  pin 6 - Serial data out (DIN)
  pin 5 - Data/Command select (D/C)
  pin 4 - LCD chip select (CS)
  pin 8 - LCD reset (RST) */
Adafruit_PCD8544 display = Adafruit_PCD8544(7, 6, 5, 4, 8);

/*Wheel*/
float wheelDiameter = 660.00; // In millimeters
float wheelSpeed;
float distance;
float rpmWheel;
volatile byte revWheel;
unsigned long timeWheel;

/*Cadence*/
int rpmCrank;
volatile byte revCrank;
unsigned long timeCrank;

/*Average*/
float avg;

/*Button*/
byte button;
byte state;
byte oldButton = 0;
byte buttonPin = 12;

void setup() {
  Serial.begin (9600);

  /*Display*/
  display.begin();
  display.clearDisplay();
  display.display();

  /*Wheel*/
  attachInterrupt(0, wheelCounter, RISING);
  revWheel = 0;
  rpmWheel = 0;
  timeWheel = 0;
  distance = 0;

  /*Cadence*/
  attachInterrupt(1, crankCounter, RISING);
  revCrank = 0;
  rpmCrank = 0;
  timeCrank = 0;
}

void loop() {
  /*Sensor*/
  hallSense ();

  /*Average*/
  Average ();

  /*Display*/
  displayContrast ();
  displayPage1 ();

  /*Print to serial monitor*/
  //printVal ();
}

void hallSense () {
  /*Wheel*/
  if (revWheel >= 1) {
    detachInterrupt(0);
    rpmWheel = 60000.0 / (millis() - timeWheel) * revWheel;
    wheelSpeed = (rpmWheel * wheelDiameter * 3.141 * 60) / 1000000;
    timeWheel = millis();
    revWheel = 0;
    attachInterrupt(0, wheelCounter, RISING);
  }

  /*Cadence*/
  if (revCrank >= 1) {
    detachInterrupt(1);
    rpmCrank = 60000.0 / (millis() - timeCrank) * revCrank;
    timeCrank = millis();
    revCrank = 0;
    attachInterrupt(1, crankCounter, RISING);
  }
}

/*Wheel*/
void wheelCounter()
{
  revWheel++;
  distance = distance + (wheelDiameter * 3.141 / 1000000); // In kilometers
}

/*Cadence*/
void crankCounter()
{
  revCrank++;
}

/*Average*/                                 //AVERAGE SPEED//
vvoid Average () {
  avg = (distance * 3600000) / timeWheel ;  // timeWheel runs a millis() in loop with interrup(0)
}                                           // which counts-up when the hall sensor is excited, so that's 
                                            // an accurate time of the wheel's run-time to calc average.    
/*Button [unused]*/
void displayPage () {
  button = digitalRead(buttonPin);
  if (button && !oldButton)
  {
    if (state == 0)
    {
      displayPage1 ();
      state = 1;
    }
    else
    {
      displayPage2 ();
      state = 0;
    }
    oldButton = 1;
  }
  else if (!button && oldButton)
  {
    oldButton = 0;
  }
}

/*Display*/
void displayContrast () {
  short potVal = map(analogRead(A7), 0, 1023, 0, 127);
  display.setContrast(potVal);
}

void displayPage1 () {
  // if (wheelSpeed >= 3) {
  display.clearDisplay();
  display.setTextColor(BLACK);
  display.setCursor(0, 0);
  display.print("Speed:");
  display.print(wheelSpeed);
  display.setCursor(0, 10);
  display.print("Trip :");
  display.print(distance);
  display.setCursor(0, 20);
  display.print("Average:");
  display.print(avg);
  display.setCursor(0, 30);
  display.print("Cadence:");
  display.print(rpmCrank);
  display.display();
  // }
  /* else {
     display.clearDisplay();
     display.setTextColor(BLACK);
     display.setCursor(0, 0);
     display.print("Speed:");
     display.print("0.00");
     display.setCursor(0, 10);
     display.print("Trip :");
     display.print(distance);
     display.setCursor(0, 20);
     display.print("Average:");
     display.print(avg);
     display.setCursor(0, 30);
     display.print("Cadence:");
     display.print("0.00");
     display.display();
    } */
}

void displayPage2 () { // [UNUSED]
}

/*Serial monitor
  void printVal () {
  Serial.print("RPM= ");
  Serial.print(rpmWheel);
  Serial.print ("\t");
  Serial.print ("Km= ");
  Serial.print(distance);
  Serial.print ("\t");
  Serial.print("Cadence= ");
  Serial.println(rpmCrank);
  }*/

I would have thought that the input capture included in the 16bit Timer1 is designed for this type of application. See chapter 16 of the 328p manual for the Timer1 hardware capabilities. Alternatively you could use a pin change interrupt.

Also, I thought that bike computers usually used a magnetic reed switch, but maybe that's considered old tech now.

Anjanbabu:
So my attempt to null the speedometer and cadence values on standstill using an 'if' condition checking if the speed is under a prescribed limit failed to work reliably. It did sort of work, but with a lot of glitches. Going to try the while() loop to see if that helps.

I'd suggest a cut off so that if the time exceeds a certain interval without the interrupt having been triggered then the speed is reset to zero.

It seems to me that you could look at the distance variable each time through loop(). If distance is not increasing you are not moving.

BTW, the distance variable should be declared volatile.

Thanks a ton, everyone who took the time to help me out with my jerry-rigged code! I finally finished the last bits and it works like a charm.

Beedoo:
I'd suggest a cut-off so that if the time exceeds a certain interval without the interrupt having been triggered then the speed is reset to zero.

Thanks, Beedoo! I considered your suggestion and prescribed a 2s delay check before the speed and cadence indicators are zeroed out on the display.

/*Delay check*/
void delays () {
  delayTimeCrank = (millis () - timeCrank);
  delayTimeWheel = (millis () - timeWheel);
}

Thanks, Blue Eyes! I managed to fix it with the time delay checks. Let me know if the other datatypes are appropriate, I'm quite the noob!

Video:
YouTube