Interpreting RPM signal sent to a tach

I want to read RPM of a running motorcycle using Arduino. There are several threads here that show how to do this by watching the signal from an ignition coil. But this motorcycle HAS a tachometer. I'd like to see if I can use the signal sent to the tach for my purpose.

The tach connects to a three wire connector. They are ground, backlight and a third line that I call Signal.

I connected the signal line to a scope. I must admit I am not an expect in using a scope. As I rev the engine, the square waves get closer to each together. Here is a photo of the scope:

I don't know where to begin, I greatly appreciate some help! Should I use interrupts? I understand I should lower the voltage to 5v for Arduino with a voltage diver.

Thank you

Your peak voltage appears to be 13.8v, so, yes, a potential divider would be necessary to drive an arduino input.

But given the bike has a working tacho, what do you want to do with an arduino? display rpm in digits?

Allan

allanhurst:
Your peak voltage appears to be 13.8v, so, yes, a potential divider would be necessary to drive an arduino input.

But given the bike has a working tacho, what do you want to do with an arduino? display rpm in digits?

Allan

I am making a datalogger.

After posting this thread I came across a few others about this subject. I wrote a test code to read the RPM. I used another Arduino to generate a single HIGH/LOW signal every 20ms similar to what I was seeing on the scope on the bike to test my code. It showed some more or less stable readings of 1400rpm.

volatile unsigned int rev_count=0;
unsigned int copy_rev_count = 0;
unsigned int RPM=0;
unsigned long rev_count_timer = 0;
unsigned long rev_count_timer_previous = 0;

unsigned long duration_rev_count;

void setup()
{
  Serial.begin(9600);
  attachInterrupt(1, rev_counter_ISR, FALLING);

  rev_count_timer_previous = micros(); 
}

void loop()
{

  noInterrupts();
  //quickly copy and reset rev_count and get a time stamp. Then restart the interrupts.
  rev_count_timer = micros();  
  copy_rev_count = rev_count;
  rev_count=0;  
  interrupts();

  Serial.print(copy_rev_count);
  Serial.print(" counts in: ");
  
  duration_rev_count = rev_count_timer - rev_count_timer_previous;
  Serial.print(duration_rev_count);
  
  Serial.print(" RPM = ");
  //RPM = copy_rev_count * 60 *1000 *1000/duration_rev_count;
  RPM = copy_rev_count * 60000000UL/duration_rev_count;
  Serial.println(RPM);
  
  rev_count_timer_previous = rev_count_timer;
  
  //running other tasks
  delay(1000);
 
}

//I count number of engine revolutions.
void rev_counter_ISR(){
  rev_count++;
  //rev_count_timer = micros(); //not sure if its better to keep this here or outside of this function.
}

[EDIT: I this point I plugged in the board to the bike and let it idle at 1450 RPM]

I made a simple voltage divider using a 470ohm and a 220ohm to lower 12v square waves to under 5v and fed the signal from the bike to my Arduino through the voltage divider. I have two problem now:

1- Bike's tach dies when I connect it to my voltage divider. This is not a deal breaker. My datalogger won't live on the bike at all times. However eventually I'd like to figure out a way to have a tach when data logger is plugged in.

2 - RPM readings are all not stable and accurate. Also, I get a mix of very low and high readings when I rev the engine past 3000 or 4000rpm. My rev limit is 9400.

Here is a sample of that data Arduino produces when RPM is 1450 :

1140
1199
1201
839
1559
1319
1019
540
1079
1079
839
1440
1499
1500 
1379
1319
1140
1319
1260
1259

Is this noise? I read something about "debouncing" but not sure exactly how to implement it in my code of if at all that's what I need.

Thank you for your help

Your resistor values are far too low, and will load the sensor output too much. Hence the tach doesn't work.

Try 10k and 5k.

Noise - vehicles are famous for lots of interering noise. Try adding a small capacitor ( eg 1 - 10nF ) across the 5k for a start... If still a problem there are more complex solutions

Allan.

allanhurst:
Your resistor values are far too low, and will load the sensor output too much. Hence the tach doesn't work.

Try 10k and 5k.

Noise - vehicles are famous for lots of interering noise. Try adding a small capacitor ( eg 1 - 10nF ) across the 5k for a start... If still a problem there are more complex solutions

Allan.

All right , I put everything together. It turned out the signal is clean enough for Arduino. No need for capacitor. However, right around 4200 rpm it fails. I see random low numbers until rpm drops bellow 4200 where it produces correct readings.

Any why this is happening?

Code is the same as what I've posted above.

All those big multiplies and divides can easily give overflows... try using floats.

What does your serialPrint of rpm say?

To do this job I've used the pulseIn() function, which worked fine.

Allan.

allanhurst:
All those big multiplies and divides can easily give overflows... try using floats.

What does your serialPrint of rpm say?

To do this job I've used the pulseIn() function, which worked fine.

Allan.

Are you suggesting I use float for RPM?

How would pulseIn() rpm count would look like? I have a bunch of other as sensors to monitor at the same time. But none of them require interrupt.

1 Like

This is how I did it.

A lot of it is dealing with the quadrature meter and the calibration pot - which you don't need.

Worked fine.

Allan

/* aircored meter revcounter  afh jan 2016 version 1.0
 
 This code runs on an arduino pro mini.
   The engine alternator energise  pin is ac coupled to a simple amplifier
   which is thewn applied to an arduino digital input pin.
      The impulse is timed, obtained from the ripple on
   the alternator connection. The inverse of this , 
   scaled for alternator drive ratio and number of poles, gives 
   the rpm.  The scaling can be changed by the reading of
   a potentiomer, so is easily adjustable on site.
   
        The value given is scaled to the meter range, eg 0..270 degrees,
   and cos and sin lookup tables give the drive voltage to each coil.
   
   This sent as a pwm signal, of value 0..255 to each coil, with 
   appropriate phase reversals for the 4 possible quadrants. 
   The sw allows for up to 360 degrees,
   During normal operation the on-board LED flahes.
     
   out of range coditions : revs above 1.3 * maxrevs  are displayed as 1.3 *  maxrevs.
                          : no signal - the revcounter sweeps. 

   Debugging : a serial output displays at 57600 baud , per line:
   
rpm/10, scaling, meter angle, coil 0 current, coil 1 current                              
 0-?     0-1023     0-359         0-255            0-255

   
   afh January 2016.

*/
// Declarations...........



// constants won't change.

const int ledPin =  13;      //  the number of the LED pin
const int potinput = A2;     //  input pin from calibration pot
const int pulsepin = 4;      //  input pin from alternator

//  pins to which drive coils on the meter are connected
//sin coil
const  int coil_1_plus  = 5;
const  int coil_1_minus = 3;  
//cos coil
const  int coil_0_plus  = 6;
const  int coil_0_minus = 9; 


// now sin and cos lookup tables. 255 == 1 for driving pwm outputs
// in 1 degree intervals 0..89

const unsigned char sinA [] = 
{0, 4, 8, 13, 17, 22, 26, 31, 35, 39, 44, 48, 53, 57, 61, 65, 
70, 74, 78, 83, 87, 91, 95, 99, 103, 107, 111, 115, 119, 123, 
127, 131, 135, 138, 142, 146, 149, 153, 156, 160, 163, 167, 
170, 173, 177, 180, 183, 186, 189, 192, 195, 198, 200, 203, 
206, 208, 211, 213, 216, 218, 220, 223, 225, 227, 229, 231, 
232, 234, 236, 238, 239, 241, 242, 243, 245, 246, 247, 248, 
249, 250, 251, 251, 252, 253, 253, 254, 254, 254, 254, 254, 255};

const unsigned char cosA [] = 
{255, 254, 254, 254, 254, 254, 253, 253, 252, 251, 251, 250, 
249, 248, 247, 246, 245, 243, 242, 241, 239, 238, 236, 234, 
232, 231, 229, 227, 225, 223, 220, 218, 216, 213, 211, 208, 
206, 203, 200, 198, 195, 192, 189, 186, 183, 180, 177, 173, 
170, 167, 163, 160, 156, 153, 149, 146, 142, 138, 135, 131, 
127, 123, 119, 115, 111, 107, 103, 99, 95, 91, 87, 83, 78, 
74, 70, 65, 61, 57, 53, 48, 44, 39, 35, 31, 26, 22, 17, 13,
8, 4, 0};
// to date me ... cos and sin from excel, formatted with aedit!

// project specific values


const unsigned long interval =20;  // interval at which to  update reading       /mS

const unsigned int maxrevs = 4000;   // for this engine    /rpm
const unsigned int maxangle = 270;   // for typical meter  /degrees
const unsigned int offset = 35;      //  for meter zero
const unsigned int maxD = 20;        //   for averaging
unsigned long debounce[maxD];
unsigned long ave;
const unsigned long  zerorevs = 0;
unsigned char i,j;  //as counter

  
// Variables  :

unsigned long pulselow ;
unsigned long pulsehigh ; 
unsigned long pulselen ;
unsigned int potval ;
unsigned long freq;
unsigned long rpm;
int angle ; // 0..360  





//CODE STARTS HERE!

void setup() {  // setting pin directions - all unassigned are inputs

  pinMode(ledPin, OUTPUT);

 // and now the coils
  pinMode(coil_0_minus, OUTPUT);
  pinMode(coil_0_plus , OUTPUT);
  pinMode(coil_1_minus, OUTPUT);
  pinMode(coil_1_plus , OUTPUT);
  
  Serial.begin(115200);     // for debugging monitor.
  j=0;
  k=0;
  up = true;
}

// main do forever loop 

void loop() {


  //        :  read the pulse input, derive rpm, 
  //        :  debounce the value, and drive the meter.
  //        : finally send a load of debug data over the serial link.


  unsigned long currentMillis = millis();
  unsigned long previousMillis ;
  
  if (currentMillis - previousMillis >= interval) {
    previousMillis = currentMillis;




    potval = analogRead(potinput);       // get calibration value  0..1023

// now obtain pulse width - note I measure both high and low
// parts to deal with asymmetry of input pulsetrain 

    pulselow = pulseIn( pulsepin, LOW,20000);  // measure pulsewidth
    pulsehigh = pulseIn( pulsepin, HIGH,20000);  // measure pulsewidth    
    pulselen = pulselow + pulsehigh;    // total pulsewidth
    
    freq = 4000000 / pulselen ;          // gives freq in Hz * 4
             //and scale noting that typically 500hz = 2000rpm
    rpm = (potval * freq) / 400;         // and calibrate
             // such that pot halfway is about right 
                
    if (rpm > 10000) rpm= zerorevs;             // no input signal - offset value
                // - timeout from pulseIn gives a large value
                
    if (rpm >(1.3 * maxrevs )) rpm = 1.3*maxrevs ; // handle overscale

// now debounce routine - average of last maxD readings 

    if (++i>=maxD) i=0;
    debounce [i] = rpm;   // circular buffer
               
    ave = 0;      
    for(j=0;j<maxD;j++) {
      ave += debounce[j];
    }
    rpm = ave/maxD;

// now convert rpm to angle on meter
    
    angle = (rpm * maxangle) / maxrevs ;  // eg for full scale 4000 rpm 
                                          // = 270 degrees deflection on meter


    if (angle < offset){ 
      angle = angle  + 360 - offset;
      }
    else 
      angle = angle - offset ;  // deals with the displayed zero 
                                // position offset point 



                   
    angle = angle % 360;       // so we don't get silly values!

//now drive aircored meter via sin/cos lookup tables with pwm function
      
unsigned char coil_0_val;
unsigned char coil_1_val;
// for logging

//   0..90
  if ((angle >= 0) && (angle < 90)) {  //first quadrant 
      analogWrite( coil_0_minus , 0);
      analogWrite( coil_1_minus , 0);
      analogWrite( coil_0_plus , coil_0_val = sinA[angle]);
      analogWrite( coil_1_plus , coil_1_val = cosA[angle]);
  }
// 90 .. 180      
  if ((angle >= 90) && (angle <180)) {  //second quadrant 
      analogWrite( coil_0_minus , 0);
      analogWrite( coil_1_plus  , 0);
      analogWrite( coil_0_plus  , coil_0_val = cosA[angle - 90]);
      analogWrite( coil_1_minus , coil_1_val = sinA[angle - 90]);
   }

// 180..270
   if ((angle >= 180) && (angle < 270)) {  //third quadrant 
      analogWrite( coil_0_plus  , 0);
      analogWrite( coil_1_plus  , 0);
      analogWrite( coil_0_minus  ,coil_0_val =  sinA[angle - 180]);
      analogWrite( coil_1_minus , coil_1_val =  cosA[angle - 180]);
    }

// 270-360
   if ((angle >= 270) && (angle < 360)) {  //fourth quadrant 
      analogWrite( coil_0_plus   , 0);
      analogWrite( coil_1_minus  , 0);
      analogWrite( coil_0_minus  , coil_0_val = cosA[angle - 270]);
      analogWrite( coil_1_plus   , coil_1_val = sinA[angle - 270]);
    }
  
// and finally print out debug values.

    Serial.print(rpm/10);
    Serial.write("  ");  
    Serial.print(potval);
    Serial.write("  ");
    Serial.print(angle);
    Serial.write("  ");
    Serial.print(coil_0_val);
    Serial.write("  ");
    Serial.print(coil_1_val);
    Serial.write("\n");

  }

}
 unsigned int copy_rev_count = 0;
RPM = copy_rev_count * 60000000UL/duration_rev_count;

Operator precedence is left to right with the multiplication and the divide and there is overflow.

Try make copy_rev_count unsigned long and add parentheses to separate out the division and bring that factor down to about 60 with your one second duration = 1000000 microseconds.

RPM = copy_rev_count * (60000000UL/duration_rev_count)