Need help on tachometer circuit for Motorcycle

I am attempting to create a digital tachometer for my old 80's V4 (Suzuki Madura) motorcycle. I want to read the engine speed off one of the spark plug wires. I just tested one circuit and though was getting some readings, the display was also getting funky... garbage characters, etc.. My hardware is a Arduino Nano, connected to a 16x2 I2C based LCD display. This is on a breadboard. I have a single strand wire wrapped around the spark plug wire about 10 times (shown as Inductor L on schematic). One end is connected to "ground A" (first circuit Grounds A and B connected together) the other to the circuit as shown in the schematic.
I am looking for suggestions on how to replace the noisy transistor circuit, with a hopefully less noisy opto-coupled circuit. This would have separate grounds, i.e. be completely isolated.
Questions:

  1. Would the coil produce enough signal to drive the TLP621 opto-isolator?
  2. Or would I some how need to supply a voltage and pull up resistor?

I only have access to the plug wires, the coil etc is buried under the gas tank and not accessible.
Perhaps I should put the optocoupler between the Q1 transistor and the Arduino. Drive it with a separate power source? The software is a simple interrupt routine. It is the hardware I'm having problems with.

Any suggestions? Experiences relating to this?

Could you take a look at what is feeding the HT coil ?
If electronic, some use a hall effect device to detect the passing of a magnet that is embedded in the flywheel.
Older ignition systems used points, so probably a negative pulse when spark fired (5-12volt) and this is fed to the HT coil to create the KiloVolts that jump at the spark plug.

Currently it looks like you are picking some spikes and amplifying them trying to use and tame a HT spark!

Don't forget that spark transmits noise, so noise on power supply and screening of ardoino might need some attention too.

I've made a tachometer on my old car and got barely the same problem.
Best solution is to take input from the coil, on the negative side.
You can easily put the signal throught an opto to get it on the arduino.

Taking some time to put down the gaz tank will be easier and quicker than trying to have a clean measure from the spark plug wire.

I do agree that trying to tap into the signal feeding the coils would be an easier solution, and I may yet look into that. But I know there are devices out there that clamp on to (or wrap around) a spark plug wire to get the need information (timing lights, e.g. this Inductive Tachometer - on ebay, Fluke's inductive sensor, Wireless Tachometers, etc.)
I am looking into making a similar more generic device that can be easily applied to other things, like car, mowers, etc. So conditioning a signal from the spark plug wire is the way to go. I know it can be done, and trying to figure a good circuit for doing so.
The big problem is of course all the noise (which makes bread-boarding the circuit problematic). The breadboard will be more susceptible to RF noise. But I believe the bread-board stage is necessary before committing to a circuit board, with possible RF cover (if needed).

Which is why I have posted here for help! I am sure others here have tried this, and look forward to any constructive help! ANYONE?
Thanks to those who have taken the time to reply so far...

One thing you could try is replacing the transistor with a 555 one shot. That might get rid of some of the noise

If that is your objective, found this vid:

...and the website:

Actual Foggiest I had seen the link to that site, and my sensor board is almost identical. I didn't put a 75LS14N schmidt trigger between the sensor circuit and the arduino. I will see if I have one and give it a try. I believe a 555 can also be setup as a schmidt trigger, and could try that as well.

FYI here is what the ignition looks like on this Madura cycle (image 1)... I'm also looking at the circuit from a unit bought some time ago, for insights (image 2)

I am also looking for inspiration from this unit I bought some time ago. I goes from an input antenna (capacitvly coupled wire around spark plug wire) through a 0.1 cap, then a 10K resistor, and another 0.1 cap. After that I am not sure have not had time to look it over.

I thought I better test my interrupt routine that calculated RPM. So made a square wave oscillator from a 555, and input it into D2 port on my Nano. My cycle tachometer goes up to around 12K, red-line at 9500. So want to test those frequencies. Also looked at this signal with a scope to get frequency and look at duty cycle (trying to maintain 50%). Here are the results I collected...

I've attached my code, anyone have any ideas why the RPM is not matching the input signal at the higher frequencies?

/* StopWatch/Tachometer Software Sketch
 16 character 2 line I2C Display
 Backpack Interface labelled "LCM1602 IIC  A0 A1 A2" or similar
*/
 
//  Variables/settings for the Stop-Watch function
unsigned long previousMillis = 0;    //last time updated
unsigned long elapsed;
long interval = 1000;    //1 second is interval at which to do update stopwatch (milliseconds)
unsigned long Ctr = 0;



//  Variables settings for the Tachometer...  
int decades;
int monades;
const int ignitionPin = 2;
const int ignitionInterrupt = 0;
const unsigned int pulsesPerRev = 1;

unsigned long lastPulseTime = 0;
unsigned long rpm = 0;
int rpm_int;
int rpm_to_disp;
int shift_light_rpm;



/*-----( Import needed libraries )-----*/
#include <Wire.h>  // Comes with Arduino IDE
// Get the LCD I2C Library here: 
// https://bitbucket.org/fmalpartida/new-liquidcrystal/downloads
// Move any other LCD libraries to another folder or delete them
// See Library "Docs" folder for possible commands etc.
#include <LiquidCrystal_I2C.h>

/*-----( Declare Constants )-----*/
//none

/*-----( Declare objects )-----*/
// set the LCD address to 0x20 for a 20 chars 4 line display
// Set the pins on the I2C chip used for LCD connections:
//                    addr, en,rw,rs,d4,d5,d6,d7,bl,blpol
LiquidCrystal_I2C lcd(0x27, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE);  // Set the LCD I2C address
//NOTE: Some of this type board come with the A0 A1 A2 connections (See photo) that are 
//NOT bridged with solder as in the photo. Those will have address 0x27 not address 0x20.

/*-----( Declare Variables )-----*/
//none

//============================================================
void setup()   /*----( SETUP: RUNS ONCE )----*/
{
  Serial.begin(9600);  // Used to type in characters

  lcd.begin(16,2);         // initialize the lcd for 16 chars 2 lines and turn on backlight
  
  pinMode(ignitionPin, INPUT);
  attachInterrupt(ignitionInterrupt, &ignitionIsr, RISING);
  
// ------- Quick 3 blinks of backlight  -------------
  for(int i = 0; i< 3; i++)
  {
    lcd.backlight();
    lcd.setCursor(2,1);      //x=0, row=1
    lcd.print("Version 5.0");
    delay(250);
    lcd.noBacklight();
    delay(250);
  }
  lcd.backlight(); // finish with backlight on  

//-------- Write characters on the display ----------------
// NOTE: Cursor Position: CHAR, LINE) start at 0  
  lcd.setCursor(0,0); //Start at character 4 on line 0
  lcd.print("Tachometer/Watch");
  delay(1000);
  lcd.setCursor(0,1);
  lcd.print("From Nano-duino");
  Serial.print("Starting Tachometer/Watch Program...@");
  Serial.print(millis());Serial.print(" mSeconds\n");
  delay(2000); 

  
// Wait and then tell user they can start the Serial Monitor and type in characters to
// Display. (Set Serial Monitor option to "No Line Ending")
  lcd.setCursor(0,0); //Start at character 0 on line 0
  lcd.print("Start Serial Mon");
  lcd.setCursor(0,1);
  lcd.print("Type characters");   
  delay(6000); 

}/*--(end setup )---*/






//============================================================
void   DisplayTachometer(int valu)
{
    //if (valu > 500) { 
      lcd.setCursor(6,0); //Start at character 6 on line 0
      lcd.print("        ");  
      lcd.setCursor(0,0); //Start at character 0 on line 0
      lcd.print("RPM=");
      lcd.setCursor(4,0); //Start at character 6 on line 0
      lcd.print(valu);   
      lcd.setCursor(10,0);  // for Hz
      lcd.print(valu/60.0); 
    delay(700);
   // }
}

//============================================================
void   DisplayShiftIndicator()  // By flashing backlight 3 times
{
  for(int i = 0; i< 3; i++)
  {
    lcd.backlight();
    delay(150);
    lcd.noBacklight();
    delay(150);
  }
  lcd.backlight(); // finish with backlight on 
}
 
 


//============================================================
void HandleTachometer()
{
noInterrupts();    //Disable interrupts temp to display results...
rpm_to_disp=int(rpm);
interrupts();      // Turn interrupts back on to count pulses...
  

   
  if ((rpm_to_disp>100) && (rpm_to_disp<15000)){

   rpm_to_disp=rpm_to_disp;
   
   DisplayTachometer(rpm_to_disp);
   
   //DisplayShiftIndicator();    //not used yet.
  
    /*  Somewhere in external loop should as the user for the value when to indicate time to shift
      this will be stored in variable "shift_light_rpm".  Could also look into making this an array
      and store a specific value for each gear. (then or course would need a gear sensor too).  For testing
      purposes with just use value of 65...
    */
    
  }
  else if(rpm_to_disp>15000)    //RedLine at 9500, tach goes to 11000
  {
      lcd.setCursor(0,0); //Start at character 6 on line 0
      lcd.print("RPM exceeds 15K");  
  }
  else if (rpm_to_disp<100)
  {
      lcd.setCursor(6,0); //Start at character 6 on line 0
      lcd.print("RPM below 100...");  
}


//============================================================ 
void loop()   /*----( LOOP: RUNS CONSTANTLY )----*/
{
  unsigned long currentMillis;
  float h, m, s;
  unsigned long over;
 currentMillis = millis();
 
  HandleTachometer();
  
  {
    // when characters arrive over the serial port...
    if (Serial.available()) {
      // wait a bit for the entire message to arrive
      delay(100);
      // clear the screen
      lcd.clear();
      // read all the available characters
      while (Serial.available() > 0) {
        // display each character to the LCD
        lcd.write(Serial.read());     
      }    //wend
    }  //endif Serial.available
    
    elapsed = currentMillis - previousMillis;
     
    if (elapsed > interval) {
      Serial.print(currentMillis);
      Serial.print("       ");
      Serial.print(Ctr);
      Serial.print("\n");
      lcd.clear();
      
      Ctr++;
      h = int(Ctr / 3600);
      over = Ctr % 3600;
      m = int(over / 60);
      over = over % 60;
      s = int(over);
       
       //  Print results to serial port
       Serial.print("Elapsed time: ");
       Serial.print(h, 0);
       Serial.print("h ");
       Serial.print(m, 0);
       Serial.print("m ");
       Serial.print(s, 0);
       Serial.print("s ");
       Serial.println(); 
      
      //   print results of 16x2 LCD display
 
      lcd.setCursor(0,1);
      lcd.print("Time: ");  
      lcd.setCursor(5,1);
      lcd.print(h);
      lcd.setCursor(7,1);
      lcd.print("h ");
      lcd.setCursor(9,1);
      lcd.print(m);
      lcd.setCursor(11,1);
      lcd.print("m ");
      lcd.setCursor(13,1);
      lcd.print(s);
      lcd.setCursor(15,1);
      lcd.print("s");
      
      previousMillis = currentMillis;
    }  //endif
  }

}/* --(end main loop )-- */


//============================================================
void ignitionIsr()
{
  unsigned long now = micros();
  unsigned long interval = now - lastPulseTime;
  if (interval > 5000)
  {
     rpm = 60000000UL/(interval * pulsesPerRev);
     lastPulseTime = now;
     //rpm_int=int(rpm);
  }  
  
}
/* ( THE END ) */

Errors.png

For the chart, the left column is from Oscillscope measurement of signal. Next to columns are readings from the Arduino/LCD. Last columns checks arduino math, and last one shows the error in the Arduino measurement.... not sure why the correlation is that bad.

I don't see any mistake on your code, but I'm not an expert.
But I've got a problem on my tachometer while using the nointerrupt function. Finaly I remove it and everything is fine.

This works nicely up to 5kHz (even more if you adjust the sampleCounts constant)

int sampleCounts = 8;

const unsigned long E6 = 1000000;
volatile byte iCount;
uint32_t Now;
uint32_t sTime;

unsigned long rpm;
 
void setup(){
  Serial.begin(115200);
  attachInterrupt(0, incCount, RISING);
  Now = micros();
}//setup()
 
void loop(){
  if(iCount >= sampleCounts){
    iCount -= sampleCounts;
    sTime = micros()-Now;
    Now += sTime;
    rpm = (60*E6*sampleCounts)/sTime;
    Serial.println(rpm);
  }//if(iCount)
}//loop()
 
void incCount(){
  iCount++;
}//incCount()

I will give that a try. I had earlier modified my code to do something similar (but with Millis) and was getting good correlation up to about 12000 RPM (217Hz). Which is very close to the upper range I wanted, but not quite there. I'll give yours a shot next...

My Results colleced...
Scope Ard-Hz Ard-RPM Calc
25.06 25.47 1478 1528.2 3.28%
30.4 30.2 1814 1812 -0.11%
33.5 33 1980 1980 0.00%
37.7 37.1 2226 2226 0.00%
43.2 42.7 2556 2562 0.23%
49.95 49.33 2964 2959.8 -0.14%
60.1 59.5 3584 3570 -0.39%
71.7 71.7 4276 4302 0.60%
105.5 104.6 6258 6276 0.29%
153.8 153.8 9172 9228 0.61%
176 175 10556 10500 -0.53%
195 193 11684 11580 -0.90%
217 214 12918 12840 -0.61%

Columns: #1. Scope Freq Calculation
#2 Arduino Hz based in interrupt
#3 Arduino RPM (60*#2)
#4 Calculation of the error, (caveat some of the digits are jumping around)

Hi Nilton61.

I put your code into my nano, connected up my generator and scope to the unit. And collected some results, and here they are.... not sure what is going on...

#include <Wire.h>  // Comes with Arduino IDE
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x27, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE);  // Set the LCD I2C address

int sampleCounts = 10;

const unsigned long E6 = 1000000;
volatile byte iCount;
uint32_t Now;
uint32_t sTime;

unsigned long rpm;
  // Following not used in version 10
const int numReadings= 5;      // How many reading to average over... 
unsigned long int readings[numReadings], maxRPM;  //  DEFINE RPM AND MAXIMUM RPM
int index=0;
int total=0;
int average=0;
int lastaverage=0;
unsigned long time;         //  DEFINE TIME TAKEN TO COVER ONE REVOLUTION
 
int ledPin = 13;           //   STATUS LED on the Nano
 
    
 void setup()
 {
     Serial.begin(115200);   // GET VALUES USING SERIAL MONITOR
     
     lcd.begin(16, 2);     // INITIATE LCD
     
    attachInterrupt(0, incCount, RISING);
    Now = micros();

     
     pinMode(ledPin, OUTPUT);
     
     lcd.print("TACHOMETER");           //   STARTUP TEXT
     lcd.setCursor(0, 1);
     lcd.print("#110414-V10");          
     delay(2000);
     lcd.clear();
 }
 

 
void loop(){
  if(iCount >= sampleCounts){
    iCount -= sampleCounts;
    sTime = micros()-Now;
    Now += sTime;
    rpm = (60*E6*sampleCounts)/sTime;
    
   // Serial.println(rpm);   instead format display or the LCD...
    lcd.setCursor(0, 0);
    lcd.print("Sensor");  
    lcd.setCursor(8, 0);
    lcd.print(rpm/60);  

    lcd.setCursor(0, 1);
    lcd.print("Reads");  

    lcd.setCursor(8, 1);
    lcd.print(rpm);  
  }//if(iCount)
}//loop()
 
void incCount(){
  iCount++;
}//incCount()

Any idea what this might not correlate linearly? My Arduino is a Nano, powered from 5V on USB port. The generator is putting out a square wave (50% duty cycle) 3.5-4V pk-pk. Scope is 500MHz bandwidth.

I changed the "samplecounts from 10 to 15, and the Hz calculation (RPM*60) jumped from 447 to 670. The input signal remained unchanged at about 702 H. I am probably tire, but can you explain how the SampleCounts is effecting this,and is there a particular sample count that could work best. (what are the trade-offs from increasing the sample count?).... thank you much for the help so far.

This was really strange, the sample count should have no effect on the value, only the update rate. I can imagine two error sources:

  • Something in your code causes a overflow. How does the code perform with only serial instead of lcd?
  • There is something strange in the hardware setup. The most common reasons for this would be intermittent contact errors and fluctuating voltages

Yes it is very strange.

I did go back to using serial.print (jumped around the LCD.print commands) and was able to get good tracking of results up to max generator output of 1.88KHz, Serial output 1888 (28.95Hz, Arduino 28). So for some reason printing to the LCD display is causing errors.
The setup I have is a 16x2 LCD, driven by a Serial converter card. "SainSmart IIC/I2C/TWI 1602 Serial LCD Module Display " Driven by SDA & SCL. Looking at a scope the signal to the Arduino is a pretty clean Square Wave, with little noise on it.

I then added back in the LCD print statements, and noticed a Large Slowdown in the loop (i.e. the serial monitor output was not updating nearly as fast. Was outputting 28RPM = 1739Hz at low end.

At the higher frequency was getting serial updates much faster, showing 867 RPM = 520XX (Scope 1.8k to 1.9kHz). So is error here again. I AM STUMPED. My printouts are Serial.print(rpm/60); and lcd.print(rpm/60,DEC).

Here is the code I am running

#include <Wire.h>  // Comes with Arduino IDE
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x27, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE);  // Set the LCD I2C address
/*  rs=0x27 RS pin on LCD. 
  2 = enable pin, 
*/
int sampleCounts = 20;

const unsigned long E6 = 1000000;
volatile byte iCount;
uint32_t Now;
uint32_t sTime;

unsigned long rpm;
  
const int numReadings= 5;      // How many reading to average over... 
unsigned long int readings[numReadings], maxRPM;  //  DEFINE RPM AND MAXIMUM RPM
int index=0;
int total=0;
int average=0;
int lastaverage=0;
unsigned long time;         //  DEFINE TIME TAKEN TO COVER ONE REVOLUTION
 
int ledPin = 13;           //   STATUS LED on the Nano
 
    
 void setup()
 {
     Serial.begin(115200);   // GET VALUES USING SERIAL MONITOR
     
     lcd.begin(16, 2);     // INITIATE LCD
     
    attachInterrupt(0, incCount, RISING);
    Now = micros();

     
     pinMode(ledPin, OUTPUT);
     pinMode(A5, OUTPUT);
     pinMode(A4, OUTPUT);    //SDA or SCL
     
     lcd.print("TACHOMETER");           //   STARTUP TEXT
     lcd.setCursor(0, 1);
     lcd.print("#110414-V10");          //  Add smoothing with running average
     delay(2000);
     lcd.clear();
 }
 

 
void loop(){
  if(iCount >= sampleCounts){
    iCount -= sampleCounts;
    sTime = micros()-Now;
    Now += sTime;
    rpm = (60*E6*sampleCounts)/sTime;
    
    
    Serial.print("RPM=");
    Serial.print(rpm/60);
    Serial.print(" = ");
    Serial.println(rpm);
    
  
  
    lcd.setCursor(0, 0);
    lcd.print("Sensor");  
    lcd.setCursor(8, 0);
    lcd.print(rpm/60,DEC);  

    lcd.setCursor(0, 1);
    lcd.print("Reads");  

    lcd.setCursor(8, 1);
    lcd.print(rpm,DEC); 
    
  }//if(iCount)
}//loop()
 
void incCount(){
  iCount++;
}//incCount()


/*  110414-V10:  Based on info from nilton61 who says the code works 
nice up to 5kHz (even more if you adjust te sampleCounts constant)...
See Aurduino Formum at http://forum.arduino.cc/index.php?topic=275426.new#new
*/

I modified the code to print out on ONLY the serial monitor port for 500 loops, then for the next 500 loops to ONLY print out to the LCD display.
When printing only to the Serial-monitor I was getting RPM count of 1876 With Hz=112612. When prnting to the LCD was getting 8917 RPM, with 535044. On the scope this signal in is 1.8xx KHz, 4.8V pk-pk Square wave.

Here is the rel code...

    rpm = (60*E6*sampleCounts)/sTime;
    
    if (TestCount <= 499) {
    Serial.print("RPM=");
    Serial.print(rpm/60);
    Serial.print(" = ");
    Serial.println(rpm);
    } 
    
    if (TestCount >= 500 && TestCount < 999) {
  
    lcd.setCursor(0, 0);
    lcd.print("Sensor");  
    lcd.setCursor(8, 0);
    lcd.print(rpm/60,DEC);  

    lcd.setCursor(0, 1);
    lcd.print("Reads");  

    lcd.setCursor(8, 1);
    lcd.print(rpm,DEC); 
    }
    
    if (TestCount >= 999) {
      TestCount=0;
      Serial.println("============== Serial Only ================");
    } else {
      TestCount++;
    }

Thats in line with what i suspected, that the time consumed by the lcd display is somehow causing overflows. Since i dont have one it is hard for me to be of any further assistance.
One method of measuring timing with an oscilloscope is toggling a output pin at certain positions in the code. This can be achieved very fast by writing a "1" to the PIN (Not the PORT....) That way you can monitor the timing of the code with an oscilloscope and work on the individual sections.

Now I am just back to the original problem of getting a circuit that will "tame" the signal from spark wire to input into the Arduino. I'm still waiting for a Schmitt trigger, which I am guessing is a key essential part of this.

I'm interested to see how this goes. My bike is a '76 Yamaha with points ignition. Figured it'd be easiest to tap into the 12V between the points and ground.
Hadn't gotten further than the idea though.