Arduino Bike Computer

I like to keep an eye on my cadence while riding my stationary bike, and I figured it would be a fun Arduino project to make a bike computer / phone holder.

Bike Computer
Here’s the finished product nestled in my bike’s handlebars. It shows current and average RPM (at the crank) and keeps a timer that pauses automatically if I stop pedaling. It also has a handy phone tray.

Schematic
Very simple circuit, with the input from the hall connected to pin 2 (interrupt 0) and the LCD occupying most of the remaining Arduino pins. The potentiometer is 10K, and is for PCB contrast adjustments. The USB port was conceived as a phone charger connection. It’s unregulated though, so only do this if using a decent quality 5V switched supply. A phone charger may actually make a pretty good supply for this project, although I haven’t checked current draw.

The hall sensor will be connected to the bike frame, in a location where a magnet attached to the crank will pass right over it.

Parts
I was feeling pretty clever when I found this M-F 3.5mm audio cable in my parts bin, thinking that it supplied the perfect 3 conductor cable, and male/female connector pair for the hall sensor. Little did I know that the ground line was set up as an uninsulated shield, so I had to collect all the strands together and make a wire out of it.
The picture shows all the parts, pre-wired and ready to go.
Assembly
The weird enclsoure shape is to accommodate a very specific spot on my handlebars. It was a bit of a squeeze getting everything packaged in here, but it worked out alright in the end.



Here’s the Arduino code. Thanks Johnny and Kevin for helping me make the code look beautiful.
/*******************************************
 *
 * Name.......:  Bike Computer
 * Description:  RPM, total rotations, and running time, with input from hall sensor
 * Author.....:  Kyle Twynam
 * Version....:  1.0
 * Date.......:  2016-07-10
 * Project....:  /arduino-bike-computer/
 * License....:  This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.
 *               To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/3.0/ 
 ********************************************/


#include <LiquidCrystal.h>          //Library for interfacing with LCD display.
LiquidCrystal lcd(12, 11, 10, 9, 8, 7);

const int MAX_RPM = 4000;           //maximum anticipated measurement speed in RPM used for debounce. Frequencies higher than this threshold will be under-sampled.
const int ROLLING_PERIOD = 8;       //number of samples for rolling average calculation. Must be 3 or greater.


const int SENSOR_PIN = 2;
const int SENSOR_PIN_INTERRUPT = 0;

const int MS_PER_SEC = 1000;
const unsigned long MS_PER_MIN = 60000;
const unsigned long MS_PER_HR = 3600000;
const unsigned int DEBOUNCE = MS_PER_MIN/MAX_RPM;


// Global state used in interrupt service routines
volatile int index = 0;
volatile unsigned long timeArray[ROLLING_PERIOD];
volatile unsigned int rotationCounter = 0;
volatile unsigned long timer = 1;
volatile boolean fullArray = false;


/**
 * Arduino setup entry point.
 * Invoked once at program start.
 */
void setup() {
    Serial.begin(9600);
    lcd.begin(16, 2);
    delay(500);

    //For use with hall sensor
    pinMode(SENSOR_PIN, INPUT_PULLUP);

    //For use with function generator
    //pinMode(SENSOR_PIN,INPUT);

    //pin 2 is interrupt 0. Hall sensor pulls pin low, thus detect falling edge.
    attachInterrupt(SENSOR_PIN_INTERRUPT, rotation, FALLING);

    //avoids a divide by 0 calculation on the first iteration for rpm
    delay(5);
}

/**
 * Arduino loop entry point.
 * Invoked periodically while the board is running.
 */
void loop() {
    detachInterrupt(SENSOR_PIN_INTERRUPT); //pause interrupts and capture interrupt variables into a buffer to ensure they are stable during calculations

    unsigned long tempArray[ROLLING_PERIOD];
    memcpy(tempArray, (unsigned long *)timeArray, sizeof timeArray);

    int tempIndex = index;
    attachInterrupt(SENSOR_PIN_INTERRUPT, rotation, FALLING);

    //Calculate outputs
    int nextIndex = (tempIndex + 1) % ROLLING_PERIOD;                             //returns oldest index location
    unsigned long arrayTime = tempArray[tempIndex] - tempArray[nextIndex];        //calculates time between oldest and newest index
    unsigned int rpm = ((ROLLING_PERIOD - 1) * MS_PER_MIN) / arrayTime;           //divides total time by number of periods and converts to rpm
    unsigned long currentPeriod = millis() - tempArray[tempIndex];                //stores time since last interrupt
    unsigned int averageRPM;

    if (fullArray == true)
    {
        averageRPM = (rotationCounter * MS_PER_MIN) / timer;
    }
    else
    {
        averageRPM = 0;
    }

    static unsigned long timerLast = 0;

    //if time since last interrupt is greater than time between oldest and newest index, assumes that motion has stopped
    if (currentPeriod > arrayTime)
    {
        rpm = 0;
        timerLast = millis(); //pauses timer
    }
    else
    {
        timer = timer + tempArray[tempIndex] - timerLast;
        timerLast = tempArray[tempIndex];
    }

    //pause interrupts during serial transmission for output display
    detachInterrupt(SENSOR_PIN_INTERRUPT);
   // serialPrint(rpm, averageRPM, timer);
    lcdPrint(rpm,averageRPM,timer);
    attachInterrupt(SENSOR_PIN_INTERRUPT, rotation, FALLING);

    while (millis() % MS_PER_SEC > 50) { //ensures loop() only executes once per second.
        delay(40);
    }

    delay(55); //ensures the function won't re-enter delay-while loop during the same second.
}

/**
 * Interrupt service routine.
 * Stores current millis() value to rolling average array.
 */
void rotation() {
    unsigned long triggerTime = millis();

    //only accepts input if time is greater than required for debounce
    if (triggerTime >= (timeArray[index] + DEBOUNCE))
    {
        index = (index + 1) % ROLLING_PERIOD;
        timeArray[index] = triggerTime;
        rotationCounter++;
    }

    if (fullArray == false && index == (ROLLING_PERIOD - 1))
    {
        //flag prevents incorrect calculation of average rpm during startup.
        fullArray = true;
    }
}

void lcdPrint(unsigned int rpm, unsigned int averageRPM, unsigned long timer){
   
    unsigned int hours = timer / MS_PER_HR;
    unsigned int minutes = (timer % MS_PER_HR) / MS_PER_MIN;
    unsigned int seconds = ((timer % MS_PER_HR) % MS_PER_MIN) / MS_PER_SEC;

    lcd.clear();
    lcd.print(hours, DEC);
    LCDprintDigits(minutes);
    LCDprintDigits(seconds);
    lcd.setCursor(0,1);
    lcd.print("RPM:");
    lcd.print(rpm);
    lcd.setCursor(8,1);
    lcd.print("AVG:");
    lcd.print(averageRPM);
}


void LCDprintDigits(unsigned int digits) {
    lcd.print(":");
    if(digits < 10)
    {
        lcd.print('0');
    }
    lcd.print(digits, DEC);
}







/**
 * Writes current and average RPM value to LED display.
 */

/**
void serialPrint(unsigned int rpm, unsigned int averageRPM, unsigned long timer) {
    unsigned int hours = timer / MS_PER_HR;
    unsigned int minutes = (timer % MS_PER_HR) / MS_PER_MIN;
    unsigned int seconds = ((timer % MS_PER_HR) % MS_PER_MIN) / MS_PER_SEC;

    Serial.print("RPM: ");
    Serial.println(rpm);
    Serial.print("Average: ");
    Serial.println(averageRPM);
    Serial.print(hours, DEC);
   // printDigits(minutes);
    //printDigits(seconds);
    Serial.println();
}
*/

/**
 * Prints colon and leading 0 to digital clock display.
 */

/* 
void printDigits(unsigned int digits) {
    Serial.print(":");
    if(digits < 10)
    {
        Serial.print('0');
    }
    Serial.print(digits, DEC);
}
*/