Motorcycle Control Panel with Arduino + Bluetooth

BoB is retiring for the moment. I cobbled up an LCD mount which I had hoped would stand up to modest water torture. Sadly, no.

So how has BoB come up after the unexpected showering ?? :frowning:

BoB is fine, the "head" is in disgrace on my coffee table. I bought (another!) bike speedo and I'm going to try using that as a readout - presumably it's waterproof.

It's also kind of a neat idea if you think of it as a bike speedo circuit bending exercise. You're basically breaking into the speedo's sensor wire and installing an arduino that changes the readout depending on the tach input.

Bill I like the idea it is a great ap to the micro. I would be interested in your code as I would like to build a data logger for the rpm on my dirt race car. any help would be apreciated. Hugh

Here is the latest version of the BoB code. I may have to split it in multiple postings. The only tricky part of this in my mind is the way I handle interrupts. I have them split into stubs which catch the interrupts and store them in a table, and routines which run periodically out of "Loop" which scan the table and handle the events. I do it this way so sloppy/slow coding doesn't make me miss interrupts without knowing. In principal I could take that stuff out once it's working but I never seem to.

//july 11 V8 cut out voltage display cuz it doesn't work!
//display speed in MPH for US trip
#include <LiquidCrystal.h>
LiquidCrystal lcd(10,13,9,12,15,11,16); //(rs,rw,enbl,d4,d5,d6,d7) 
unsigned long lasttachint,lastwheelint; //time in millis of most recent tach and wheel interrupts
float insttachrpm=0, avgtachrpm=0, wheelrpm=0;  //tach instantaneous and rolling average rpm,  wheel rpms 
float revratio; int currentgear=0; //updated by setgear routine
float roadspeed=0; //speed in kph
volatile int ilog=0; 
int maxlog=31;  
volatile boolean logoflo; //index to log table, max entry #, log overflow flag
volatile byte lflag[32]; 
volatile unsigned long lmillis[32]; //log of interrupts
int tachpin=2;  //pedal sensor booster, drives interrupt 0;
int wheelpin=3; //wheel sensor, drives interrupt 1;
int neutralpin=5;
void setup() {
  lcdbegin(); //initialize the lcd
  clearlog();  //clear the interrupt table
  attachInterrupt(0, tachmonitor, RISING);  //interrupt from tachometer
  digitalWrite(wheelpin,HIGH); //set pullup resistor for reed switch
  attachInterrupt(1, wheelmonitor, RISING);  //interrupt from reed switch wheel sensor
}

void loop(){
  checklog();  //make sure log is ok before processing
  processlog(); //process and dump the log file to the PC
  chkneutral(); //test for neutral
  chkstale(); //check to see if tach or speed readings are >3 seconds old (bike has stopped!)
  updatedisplay();
  delay(100);  //wait a while
}
void chkstale(){
  if ((millis()> (lastwheelint+3000)) && (wheelrpm>0)){
    wheelrpm=0; roadspeed=0;
  }
  if ((millis()> (lasttachint+3000)) && (insttachrpm>0)){
    insttachrpm=0; avgtachrpm=0;    
  }
}

void updatedisplay(){
  lcd.clear();
  pbc(currentgear); //display what gear we're in
  lcd.setCursor(4,0); lcd.print(int((avgtachrpm+50)/100)*100); //tach
  lcd.setCursor(4,1); lcd.print(int((roadspeed/1.6)+.5)); //speed in Mph
}
void chkneutral(){
  if (inneutral()){
    currentgear=10;
  } //10 is code for neutral
  else{ //need to see if we've come out of neutral with no other info   
    if (currentgear==10) {// if we used to be in neutral
      currentgear=0; //      0 is "don't know"
    }
  }
}

boolean inneutral(){ //see is neutral light is on
  int n=analogRead(neutralpin); //neutral light sensor is on analog pin 5 - usually <100 in neutral, >300 in gear.
  if (n<150){
    return(true);
  }
  else{
    return(false);
  }
}
void checklog(){ //make sure log hasn't overflowed
  if (logoflo==true){
    noInterrupts();
    clearlog();
    interrupts();
    Serial.print("log overflowed at ");
    Serial.println(millis());
  }
}

void processlog(){
  int dlog,i; //how many entries to copy and dump
  unsigned long dmillis[32];
  byte dflag[32];  //space for the log entries
  noInterrupts(); //disable interrupts while we copy the log
  dlog=ilog;  //copy the count
  if (dlog!=0) { //if there's anything to copy
    for(int i=0; i<dlog;i++){
      dmillis[i]=lmillis[i]; 
      dflag[i]=lflag[i];
    }
    ilog=0; //reset the count
  }
  interrupts(); //reenable interrupts
  if (dlog!=0){ //if there's anything to process
    for (i=0;i<dlog;i++){
      //Serial.print(i); Serial.print(dflag[i]); Serial.print(" "); Serial.print(dmillis[i]); Serial.print(" ");
      if (dflag[i]=='W') {
        processwheel(dmillis[i]);
      }
      else {
        if (dflag[i]=='T') {
          processtach(dmillis[i]);
        }
      }
      //Serial.println(" ");
    }
  }
}

void processtach(unsigned long lmillis){ // input is interrupt time in millis
  unsigned long tachtime; //elapsed time for 10 tach events
  if (lasttachint!=0) { //skip calculation on first time in
    tachtime=lmillis-lasttachint;  //time for last 10 events
    if (tachtime>0){ //just avoid divide by 0
      insttachrpm=60000/(float)tachtime*10;  //rpm based on time in ms for 10 tach pulses
      avgtachrpm=(avgtachrpm*2+insttachrpm)/3; //rolling average rpm
      //Serial.print(" tachtime="); Serial.print(tachtime); Serial.print(" inst tach rpm="); Serial.print(insttachrpm); Serial.print(" avg tach rpm="); Serial.print(avgtachrpm);
    }
  }
  lasttachint=lmillis;
}

void tachmonitor(){
  static int tachcount;
  if (tachcount <9) { //bulking up tach interrupts by a factor of 10
    tachcount++;
  }
  else {  //every 10th tach pulse we spawn a log entry
    if (ilog<=maxlog){
      lflag[ilog]='T'; //pedal pass flag
      lmillis[ilog]=millis();
      ilog++;
    } 
    else {
      logoflo=true; //we've overrun the log table
    }
    tachcount=0;
  }
}

void processwheel(unsigned long lmillis){ //input is time of interrupt from log
  unsigned long wheeltime; //time for previous revolution
  if (lastwheelint!=0){ //don't do calculations 1st time in
    wheeltime=lmillis-lastwheelint; //track the exact rotation time
    wheelrpm=60000/(float)wheeltime; 
    //Serial.print(" wheeltime="); Serial.print(wheeltime); Serial.print(" wheel rpm="); Serial.print(wheelrpm);
    roadspeed=.123*wheelrpm; //condensed version of 2.05*60*wheelrpm/1000; where 205 is wheel circ. in metres
    //Serial.print(" road speed kph="); Serial.print(roadspeed);
    setgear(); //figure out what gear we're in
  }
  lastwheelint=lmillis;
}

void wheelmonitor(){
    if (ilog<=maxlog){
      lflag[ilog]='W'; //wheel pass flag
      lmillis[ilog]=millis(); //record time
      ilog++;
    } 
    else {
      logoflo=true;
    }
}

remaining code:

int whichgear(float ratio){
  float validratios[]={
    11.7,8.37,6.65,5.54,4.61  }; 
  int maxratioindex=4; //ratios for motorcycle

  int gearsforratios[]={
    1,2,3,4,5    };
  for (int i=0;i<=maxratioindex;i++){
    if (closeto(ratio,validratios[i],5)) return gearsforratios[i];
  }
  return 0;
}

boolean closeto(float param, float reference, float pcttolerance){ //true if param is within tolerance% of reference
  return (param>=(reference*(100-pcttolerance)/100))&&(param<=(reference*(100+pcttolerance)/100));
}


void setgear(){ //determine which gear the bike is in
  int newgear; 
  static int prevgear, prev2gear;  
  if (wheelrpm!=0 && insttachrpm!=0){
    revratio=insttachrpm/wheelrpm;
    //Serial.print("<"); Serial.print(revratio); Serial.print(">");
    newgear=whichgear(revratio);
    if (newgear!=0){
      if (newgear==prevgear /*&& newgear==prev2gear*/){
        currentgear=newgear;
      //  Serial.print("Sel: "); Serial.print(newgear); Serial.print(" ");
      }
      //prev2gear=prevgear; 
      prevgear=newgear;
      //Serial.print("Candidate: "); Serial.print(newgear); Serial.print(" ");
    }
  }
}

void clearlog(){ //clear the interrupt log
  for(int i=0; i<=maxlog; i++){
    lflag[i]=' '; 
    lmillis[i]=12345678;
  }
  ilog=0; 
  logoflo=false;
}

void pbc(byte x){
  //routine to print digits 0-9 & letter N in double height characters on an LCD
  //strings to index into for displaying the big numbers
  char bignumchars1[]={
    32,32,32,0, 1,4,32,0, 3,3,4,0, 1,3,4,0, 4,2,4,0,   4,3,3,0, 4,3,3,0, 1,1,4,0,   4,3,4,0, 4,3,4,0, 4,6,4,0  }; //0 was 4,4,4
  char bignumchars2[]={
    32,32,32,0, 2,4,2,0,  4,2,2,0, 2,2,4,0, 32,32,4,0, 2,2,4,0, 4,2,4,0, 32,32,4,0, 4,2,4,0, 2,2,4,0, 4,7,4,0  }; //0 was 4,2,4
  lcd.setCursor(0,0);
  lcd.print(bignumchars1+x*4);
  lcd.setCursor(0,1);
  lcd.print(bignumchars2+x*4);
}
void setupbigchars(){
  //routine to load the character generator ram on an lcd controller with block characters
  //credit to dcb on arduino forums http://www.arduino.cc/cgi-bin/yabb2/YaBB.pl?num=1213319639
  lcd.command(B01001000);  // set cgram
  byte chars[]={
    B11111,B00000,B11111,B11111,B00000,B00000,B01110,
    B11111,B00000,B11111,B11111,B00000,B11000,B00111,
    B00000,B00000,B00000,B11111,B00000,B11000,B00111,
    B00000,B00000,B00000,B11111,B00000,B11000,B00111,
    B00000,B00000,B00000,B11111,B00000,B11100,B00011,
    B00000,B00000,B00000,B11111,B01110,B11100,B00011,
    B00000,B11111,B11111,B11111,B01110,B11100,B00011,
    B00000,B11111,B11111,B11111,B01110,B01110,B00000  };

  for(byte x=0;x<7;x++)
    for(byte y=0;y<8;y++)
      lcd.write(chars[y*7+x]); //write the character data to the character generator ram    
}

void lcdbegin(){
  delay(25); //make sure there's a clear delay for lcd power-on initialization
  lcd.command(0x28);  // function set: 4 bits, 1 line, 5x8 dots
  lcd.command(0x0C);  // display control: turn display on, cursor off, no blinking
  lcd.command(0x06);  // entry mode set: increment automatically, display shift, right shift
  lcd.clear();delay(5);
  lcd.print("V8 Bill!"); delay(250); lcd.clear(); delay(5);
  setupbigchars();delay(25); //write big fonts to lcd cgram
}

I have them split into stubs which catch the interrupts and store them in a table, and routines which run periodically out of "Loop" which scan the table and handle the events

Nice! The essence of a micro-kernel running on a micro-conroller.

I bought (another!) bike speedo and I'm going to try using that as a readout - presumably it's waterproof.

It's also kind of a neat idea if you think of it as a bike speedo circuit bending exercise. You're basically breaking into the speedo's sensor wire and installing an arduino that changes the readout depending on the tach input.

Ok, so meet FrankenBoB - it's BoB with a head transplant.

This is my attempt to use the original bike speedometer head as a waterproof serial LCD display - initially I was just using it to show what gear the bike is in. The good news is, it's serial, it's waterproof, it's simple to drive. The bad news is it's meant to be a bicycle speedometer! It doesn't want to register very low speeds so I couldn't just run 1mph for first gear etc. I tried a bunch of things- and what I settled on was driving it at gear*10+"a bit" kph - the gear number shows up in the 10's digit of the speed. In principle you could mask off or disable the other digits but then you have a more limited display.

I had it out on the road today for a couple of hours and it worked fine except for being sluggish and the extra digits being distracting. I don't really care for the look of it either so I probably won't pursue the idea further - although knowing me I don't like to let a bad idea go without a fight.

Ok, in the spirit of beating a dead horse, I just had a go at keeping the gear indicator in the ones digit with an offset of 10.5 i.e. 1st gear is 11.5, 2nd is 12.5. This was responsive enough that I might try finding a small speedo who's LCD traces I can hack.

BoB has had another head transplant. This one is named Charlie - it's a set of 5 leds charlieplexed from 3 pins on the arduino. I's a tad fugly but it's something I wanted to try.


What are the 5 LEDs showing exactly?

It looks ok, you just need a better place to mount it I think. With the motorcycle being chrome, maybe you will need to spend some money getting a mount chromed...

Mowcius

there is chrome paint, its not very nice to apply but you get the look on any surface you want :wink:

I hate to be a bother Bill, but I am currently attempting to build a digital tach for a car my buddies and I will be entering in the "24 hours of lemons" race this fall.

Can you please give me a run down on how you built the circuit to trap the tachometer signal from the bike?

I am a definite novice when it comes to electronics, I was trying to read the pulses earlier today and am afraid I've killed my first arduino board.

Please help me if you can, Thanks.

Say hello to my little friend. A while ago I decided that there would be two versions of the computer control panel. One, code-named Brawny, would be a full function arduino clone with heavy duty power and connections, and would have all kinds of output capabilities - LCD, bluetooth etc. Brawny became BoB who has been successful but won't fit on my bike without taking off the tachometer.

Pikachu, I figured, would be based on an attiny, have simpler connections, and be small enough to fit alongside my tachometer. Sadly, I haven't succeeded with the attiny and I couldn't put aside my connection lust. The results is sort of a Pikachu-heavy. I'm still using a 168/328 with a resonator but it MIGHT be able to fit.

I'm pretty pleased with how the parts layout turned out. I look at it now and I think, yeah, that's pretty obvious, but this took me hours and hours of staring and poking. On the bottom right are 4 voltage dividers, one each for reading battery voltage, signal lights, neutral light, and tachometer. The bottom left is a 10K resistor on the reset line and above that the debounce circuit for the reed switch wheel sensor. There's female headers for the tx&rx pins but no other outputs yet.

I've decided the most practical waterproof output is the charlieplexed led strip. That needs 3 pins, probably the 28-27-26 on the top left.

I really wish I could have used an attiny, I look at the bottom of the board and half of the atmega's pins aren't connected.

Anyway, I've tested it with a few sketches and it worked fine, I'm hoping to try it out with serial output tonight. Once I have that I'll start on a new LED strip hoping to get done in the next week before I leave on a 5,000 km motorcycle trip.

Pikachu hits the road.

Tomorrow I'm leaving on a three week motorcycle trip starting in Ottawa and ending in Seattle with lots of stops in between. I got pikachu buttoned down under the tank cowl and it's on board for the ride.

The only functions implemented are the basic gear indicator and a voltmeter during startup.

Wish me luck.

I'm doing something similar.

My Arduino uses IC1 to decode J1850 (could measure RPM directly), but also goes to a GPS, bluetooth, and a 3 axis accelerometer.

http://forum.sparkfun.com/viewtopic.php?t=17018

And my radar detector.

Using my n810 (wimax edition with Harley colors!) as a front-end so I can use the map, media player, etc. while logging the GPS and other data.

The J1850 stream on my harley includes speed, rpm, gear with clutch and neutral, fuel consumption, odometry, fuel level, and even turn signals. I wrote up a quick dash program in python linked to minigpsd (my project at maemo.org). It also does OBD-II via the J1962 connector if you have it in a car. (it probably includes cruise control, but I don't have that on my bike, I only see the indicator on the speedo so it might also be part of the data stream).

In Google Earth, after some post processing: (729kb, 360kb)
http://www.zdez.org/HarleyTelem.kmz (from last year)
http://www.zdez.org/Radar.kmz
both of these are pre-arduino - I used an bluetooth OBD2 with monitor mode for j1850 and a direct bluetooth UART to V1 radar detector (the pulse train decodes at 19200 baud), and a 10Hz bluetooth GPS into my tablet.

The Arduino merges all the devices (with better GPS!) into one bluetooth link and adds the 3 axis acceleration.

I plan on testing it this weekend.

Crossposted from Motorcycle Instrument Cluster - SparkFun Electronics Forum

URLs are to the 1-2 Meg 12MP version

The Nokia Tablet with the dashboard program:

http://www.zdez.org/NokTabletHDash.jpg

Bottom of the board.

http://www.zdez.org/BoardBot.jpg

Top of the board.

http://www.zdez.org/BoardTop.jpg

The Ram box on the DockNRock on the handlebars

http://www.zdez.org/Handlebar3q.jpg

The test stack in my bag for the test

http://www.zdez.org/TestStacked.jpg

Closeup of the Ram Aqua Box (medium/wide)
http://www.ram-mount.com/CatalogResults/PartDetails/tabid/63/partid/082065077045072079076045065081054085/Default.aspx

http://www.zdez.org/RamMountNear.jpg

The Ram Aqua Box. A blackberry bluetooth audio gateway is in the back, and I have a nokia recharger behind.

http://www.zdez.org/RamMountFar.jpg

@ tZ

My Arduino uses IC1 to decode J1850 (could measure RPM directly)

Can you point me in the right direction to learn how to measure RPM directly?

OP : I'm looking at your code for reading tach signals and I'm slowly understanding it. However, I realized I need to fully understand/implement interrupts before I use your method of reading a tach signal. Thanks.

Time to read up on how to use interrupts with an Arduino.

Can you point me in the right direction to learn how to measure RPM directly?
Basically you want a frequency counter, but you need to convert Hz - which will be some pulses per second to the actual revolutions per minute. Frequency = 1 / period in seconds. If you get 5 pulses per rotation, and that is coming in at 10 hz, the equation is 10 pulses / second * 60 seconds / 1 minute * 1 revolution / 5 pulses = 120 revolution / minute. (note if you write this out like an equation, you can cancel the measurement units in the numerators and denominators just like 2/3 * 3/2).

With 4 cycle engines, there is one pulse every 2 revolutions for each cylinder. There may be a different sensor that does some number of pulses per revolution. Also you can inductively pick-up the spark plug.

My code http://www.zdez.org/pulse1.c uses 1000000 baud but feel free to edit that. It prints out high and low values. To get period, just add the current width and previous, or remove the edge detect flip code at the top of the IC ISR. Once you have it printing out the correct period, just do the appropriate scaling to get RPM.

My j1850 code uses the J1850 bus to read messages. You can find out more about the protocols if you google for OBD-II or some variant. the ELM327 chip will read from automotive busses, and there are OBD-II commands for cars that will return the engine RPM (Wikipedia has a page with the codes). It is a simple protocol, but I wouldn't know what any specific motorcycle uses (except recent Harleys using J1850). Even if the bike doesn't support OBD2, if it has a compuer, there is usually a datastream available somewhere.

@ tZ
Thanks. I guess I could just use trial/error methods to find out how many pulses I'm getting per revolution. I originally thought you meant that you could read RPM with the IC1 or something. I assumed that might have something to do with the ICSP interface on-board...?

For the hardware side, I'm probably just going to use a voltage divider to scale to 5v but can I just use an LM805 to attenuate to 5v instead? I think it has a fairly low drop-out voltage.

My code http://www.zdez.org/pulse1.c uses 1000000 baud but feel free to edit that. It prints out high and low values. To get period, just add the current width and previous, or remove the edge detect flip code at the top of the IC ISR. Once you have it printing out the correct period, just do the appropriate scaling to get RPM.

holy cow, that seems complicated. I was envying the J1850 but maybe not...

the whole setup is very nice though.