Tachometer from spark plug lead and noise

Hi everyone,

I have made some progress since my last post: Tachometer from a car ignition coil (already 3 years have passed...)

I have finally found a working circuit. But now I need your help to "debounce" it.

So first here is the circuit diagram:

Basically a single wire is coiled around the spark plug lead with one end open and the other one to the base of a transistor (2N3904) to form a kind of capacitive switch.
The wire and the spark plug lead are forming a capacitor.

The transistor is connected to one side of an optocoupler (H11L1). And finally, the arduino D2 pin is connected to to the other side of the optocoupler.

The wire is tied like this to the spark plug lead:

I measure the mean time between two spark plug ignitions during a set period with the help of an interrupt. I can then deduce the ignition frequency and deduct from it the RPM of the engine.

Here is an example of what I get:
Screenshot from 2024-10-14 19-03-40
(Counter is the amount of ignitions since the last "refresh", refresh is the time passed between the last "refresh" and RPM the RPM value, "refresh" being the last time a RPM value was calculated)

My problem arise here: the engine was running at 650 RPMish. We kinda see it in my output but sometimes we see the value jumping to 950+ RPM. At higher RPM the value seems to be a bit higher than reality as compared to my cheap digital tachometer.

I suspect it is due to noise coming from the car ignition. It could also be my code... I don't understand the circuit well enough to deduce a cause to the problem. Do you see anything I could improve in the circuit to remove/reduce noise?

Here is also my code in case the problem was there:
sketch_oct13b.ino (1.2 KB)

Thank you in advance for your help!

Timing just 2-3 periods between sparks will be very inaccurate and erratic.

Try recording the the total time for say 20 sparks, then divide the total by 20.

Yes but with a fixed capacitance, which gets discharged at a specific ratio as a result of the current limiting resistor.
I suspect there is just simply a limit to the speed at which that can work.

Normally the cheap tachos work using a circuit which is connected to the base coil. The kickback from the coils should provide you with the signal. Limit the current with a resistor, limit the voltage with a zener diode, that makes the most sense, like the guy in this topic suggested. Have a look here as well, no zener diode in that, but some de-bouncing capacitors.

The complain from someone that the noise from the car itself will influence readings, does make sense. So maybe start out with a fully separate power-supply beyond the opto-coupler, and see if you get good results.

You may need a diode connected emitter to base to discharge the "capacitor." (cathode to base)

How i dislike to have to download things.
But there are some problems.

int lastTick = 0;
int lastBlink = 0;
int refreshInterval = 500;

int totalTime = 0;
int lastInt = 0;
int counter = 0;

void setup() {
  Serial.begin(9600);
  lastTick = lastBlink = lastInt = millis();
  pinMode(13, OUTPUT);
  attachInterrupt(digitalPinToInterrupt(2), countUp, FALLING);
}

void loop() {
  int timeIs = millis();
  if (timeIs - lastTick > refreshInterval) {
    // int rpm = ( (float) counter / ( (float)refreshInterval / 1000) ) * 60 * 2; // * 2 = moteur 4 temps

    // Tr/min = Fréquence (1 / moyenne entre deux explosions) * 60 secondes * 2 pour un moteur 4 cylindres 4 temps (1 explosion tous les deux tours)
    // Donne la vitesse de rotation moyenne sur la période de rafraichissement
    float rpm = 0;
    if (counter > 0) {
        rpm =  1 / ( (float) totalTime / (float) counter) * 60 * 1000 * 2;
    }
    
    Serial.print("Counter: ");
    Serial.print(counter);
    Serial.print("   refresh: ");
    Serial.print(totalTime);
    Serial.print("   RPM: ");
    Serial.println(rpm, 3);

    counter = totalTime = 0;
    lastTick = millis();
  }
}

void countUp() {
  int now = millis();
  totalTime += now - lastInt;
  counter++;
  lastInt = now;
}

first of all any variable that is being modified inside the ISR and used outside of it (and vice-versa) needs to be declared 'volatile' That is probably the crutch, but i would not bother about measuring time during the interrupt. Just count and multiply. every half a second we count the ticks.
If we want to take averages, we can.


#define REFRESH_INTERVAL 500

uint32_t lastTick = 0;

volatile uint8_t counter = 0;

void setup() {
  Serial.begin(9600);
  lastTick = lmillis();
  pinMode(13, OUTPUT);
  attachInterrupt(digitalPinToInterrupt(2), countUp, FALLING);
}

void loop() {
  const uint16_t multiplier = 60 * 1000UL / REFRESH_INTERVAL / 2;  // counting every ignition 4 Cyl.
  // const uint16_t multiplier = 2 * 60 * 1000UL / REFRESH_INTERVAL;  // counting single spark 
  uint32_t timeIs = millis();
  if (timeIs - lastTick > REFRESH_INTERVAL) {
    
    uint8_t counted = counter; // read the counter
    counter = 0;  // reset the counter. Ayn count in between gets lost.
    /*noInterruots();
    uint16_t counted = counter;  // off course counter then also needs to be 16-bit
    interruots();
     */
    lastTick = lastTick + REFRESH_INTERVAL;  // add the refresh time
           //ideally all these things happen straightaway and at the same time
    
    uint32_t rpm = counted * multiplier; // that is all we measure the ticks in half a second
       // if we measure every discharge from the coil, that is 120 frames, and 2 discharges = 60
       // per revolution. (at 4 cylinders) at 10000 RPM that would be 167 ticks which fits into an 8-bit variable
       // if you measure the ignition of a single sparkpluglead, then it would be 
       // uint32_t rpm = counted * 240;
       // so to do it from the refresh interval.
       // 60 * 1000 / REFRESH_INTERVAL / 2 
       // ideally you would actually want the maximum resolution, the smallest multiplier, though 
       // if your counter may exceed your 8-bit variable, then you need to turn interruots off during the 
       // the reading of it.
           
    Serial.print("Counter: ");
    Serial.print(counted, DEC);
    Serial.print("   RPM: ");
    Serial.println(rpm, DEC);
  }
}

void countUp() {  // all we do is count !
  counter++;
}

the last bit of loop() can be something like this + the average function.

    Serial.print("Counter: ");
    Serial.print(counted, DEC);
    Serial.print("   RPM: ");
    Serial.print(rpm, DEC);
    rpm = Average(rpm);
    Serial.print("   Average: ");
    Serial.println(rpm, DEC);
  }
}

#define AVERAGE_OFF 20  // the number of values we take average over

uint16_t Average(uint16_t revs) {
  static uint16_t aValues[AVERAGE_OFF];
  static uint8_t ndx = 0;
  uint32_t average = 0;
  aValues[ndx] = revs;
  ndx++;
  ndx = ndx % AVERAGE_OFF;
  for (uint8_t i = 0; i < AVERAGE_OFF; i++) {
    average += aValues[i];
  }
  average = average / AVERAGE_OFF;
  return average;
}

Hope this helps, i should probably do something like this my self, but all my cars already have one, (and one is a variomatic, the revcounter is rather boring) i may be able to improve on what i have though.

Actually thinking about it, you would probably be off best with a

#define REFRESH_INTERVAL 300

which will result in a multiplier of 100, so your Revs will simply be multiples of 100, which i think is pretty neat.

Hi, @arnakazim

You need to get rid of the noise at the source.
Use a length of shielded or coax cable between the transistor and the ignition lead, ONLY have the coily bit exposed.

Connect the shield to gnd at the transistor.

A better solution is to go low impedance, inductive pickup, rather than high impedance capacitive pickup.
Have the ignition coil go through a ferrite core, you wind 5 to 10 turns on the core and feed that back to gnd and the transistor base resistor.

That is how electronic timing lights work.
It is easily adapted to your circuit.

Tom... :smiley: :+1: :coffee: :australia: