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.
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.
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.
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.
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);
}
*/
