Confused by oscilloscope - square wave signal to RPM

Hello everyone

My project is a gearshift-indicator using 2 Adafruit neopixels LED strips and an Arduino Nano. Everything pretty much works as expected, save for a few issues.

A while ago I made a thread about measuring my engine's RPM from the ignition wire from the ECM Here.

The gist is that with your help I managed to etablish a method of detecting the time between pulses on an Arduino nano, using the pulsein() command. Arduino reads a square-wave signal from an ignition output from the ECM (square wave 0-5v) and converts it into RPM. Neopixels change quantity/color with raising RPM.

For a while, this worked quite well, but it was never perfect. The signal was always erratic, with the RPM number rising and falling seemingly at random. It was steady enough to measure with accuracy sufficient to render the shift light reliable, but not steady enough to be consistent or react fast enough, and required significant smoothing in the code to stop the leds flashing randomly.

I recently discovered that using digitalread() instead of pulsein() to detect each pulse was allegedly a more reliable and accurate way to measure the RPM with the added benefit of having a non-blocking program as pulsein() is no longer used (which blocks the whole program each time it waits for a peak/trough in the wave.)

However, this new sketch led to an even more erratic output, and I have since regressed to the pulsein() sketch. After trying a few different programming ideas, none of which worked, I decided to go back to square 1.

I went to view the ignition wire signal with my oscilloscope, and as it turns out, when the oscilloscope is set to 200ms/div or more, the signal showed the same erratic behaviour as the new sketch. However, when the oscilloscope is set to 100ms/div or lower, the signal is perfect, with clean uninterrupted pulses.

Now this baffles me, because on the new sketch (and the pulsein() one for that matter) i had everything in my void_loop() in an if() statement that would trigger if millis() was 1ms greater than at the end of the if() statement. So the arduino should be looking for pulses every 1ms, giving it more than enough time to detect every pulse, right? But the sketches are seemingly providing the same erratic output as the oscilloscope does when set at 200ms... I believe I have a critical lack of understanding of what's going on here, hence why I am asking for help.

I had previously hypothesized that perhaps the signal was unclean, and i need to modify things electrically but after stumbling upon the perfectly clean signal at 100ms/div and 50ms/div on the oscilloscope, I believe now it should simply be a matter of getting the Arduino to see that clean signal too through programming, right?

tl;dr, my oscilloscope shows (doesn't show?) missing waves in the signal when set to 200ms/div, but misses none when set to 100ms/div or lower - signal is perfect. This incomplete waveform happens to be also seemingly what the arduino sees/outputs despite my coding attempts. I need help trying to figure out how to get the clean waveform regsitered by the Arduino.Processing: 86 revs 1.mp4...
Processing: 86 revs 2.mp4...

edit: latest schematic

Post some pictures.

Looks like a cheap trinket 'scope with a low sampling rate. Sometimes you get what you pay for...

1 Like

I guess that your problem is not primarily the oscilloscope. If you use the lower resolution on it some (narrow) spikes may just disappear.
The Neopixel library does some quite low level operations to support the strict timing requirements of the Neopixels themselves. It globally suspends interrupts and may block which could disturb your attempts to accurately measure the engine speed.
Attempt to measure the engine speed first without any Neopixel code. Simply write the results to the serial monitor.

You may also be able to try using an external interrupt, rather than pulseIn() or any polling technique, to sense pulses from the engine. But you have to experiment, because this depends on the impact of the Neopixel library, maybe taking the shortest gap between adjacent (debounced) pulses as the indicator of the engine speed and discarding long gaps which could imply missing pulses. In general, working beside an application which suppresses interrupts can be difficult.

from (interrupts disabled):

// In order to make this code runtime-configurable to work with any pin,
// SBI/CBI instructions are eschewed in favor of full PORT writes via the
// OUT or ST instructions. It relies on two facts: that peripheral
// functions (such as PWM) take precedence on output pins, so our PORT-
// wide writes won't interfere, and that interrupts are globally disabled
// while data is being issued to the LEDs, so no other code will be
// accessing the PORT. The code takes an initial 'snapshot' of the PORT
// state, computes 'pin high' and 'pin low' values, and writes these back
// to the PORT register as needed.

Your problem is you are measuring pulse width when you should be measuring time between two rising edge of pulse.

Personally i would use d-flip flop connected to toggle mode and use those pulses to clock it, then you could use pulse width measurement.

Also those cheap Chinese "oscilloscopes" are usually only good for audio frequencies and fail miserably with anything higher then that. Do to their way of measuring signal. Proper digital scopes uses much much higher sampling rates to capture the signal.

Whats the maximum frequency they promise to that device to measure?

Thank you for the info, I didn't know that, I had never considered the neopixels might be causing trouble. I dont think I've tried a sketch without them, either. Ill try what you said and make a sketch with just the RPM serial output.

As mentioned I tried a second sketch doing just this, it only made things worse. Perhaps I wrote it poorly. Ill try it again when doing just the RPM serial output sketch.

Whats the maximum frequency they promise to that device to measure?

I dont know, but It's apparently enough that I can see that the waveform is consistent and perfect now so my main concern is getting the Arduino to see that too. I'll create the new sketch doing just the serial output with no neopixels involved and report back.

Ok then, your most pressing matter is reliably detecting raising edge of pulse then. this why i was suggesting d-flipflop in toggle mode, turn those pulses into clock pulses and turn clocks in changes of lenght of signal from d-latch. But that's just me.

You can always take it small step at time, condition pulse properly and see if you can even detect it. Then add timing needed to measure distance between pulses and you can calculate frequency if needed. There is several ways to do this, hope you find one that works for you.

Are you using an UNO or Nano? If so, this sketch for measuring pulse waves on Pin 8 (the Input Capture pin) might give better results because it uses a timer hardware feature and not just interrupts. Give it a try to see if you get a fairly steady result.

// Measures the HIGH width, LOW width, frequency, and duty-cycle of a pulse train
// on Arduino UNO Pin 8 (ICP1 pin).  

// Note: Since this uses Timer1, Pin 9 and Pin 10 can't be used for
// analogWrite().

void setup()
  while (!Serial);

  // For testing, uncomment one of these lines and connect
  // Pin 3 or Pin 5 to Pin 8
  // analogWrite(3, 64);  // 512.00, 1528.00, 2040.00, 25.10%, 490.20 Hz
  // analogWrite(5, 64);  // 260.00, 764.00, 1024.00, 25.39%, 976.56 Hz

  noInterrupts ();  // protected code
  // reset Timer 1
  TCCR1A = 0;
  TCCR1B = 0;
  TCNT1 = 0;
  TIMSK1 = 0;

  TIFR1 |= _BV(ICF1); // clear Input Capture Flag so we don't get a bogus interrupt
  TIFR1 |= _BV(TOV1); // clear Overflow Flag so we don't get a bogus interrupt

  TCCR1B = _BV(CS10) | // start Timer 1, no prescaler
           _BV(ICES1); // Input Capture Edge Select (1=Rising, 0=Falling)

  TIMSK1 |= _BV(ICIE1); // Enable Timer 1 Input Capture Interrupt
  TIMSK1 |= _BV(TOIE1); // Enable Timer 1 Overflow Interrupt
  interrupts ();

volatile uint32_t PulseHighTime = 0;
volatile uint32_t PulseLowTime = 0;
volatile uint16_t Overflows = 0;


  static uint32_t firstRisingEdgeTime = 0;
  static uint32_t fallingEdgeTime = 0;
  static uint32_t secondRisingEdgeTime = 0;

  uint16_t overflows = Overflows;

  // If an overflow happened but has not been handled yet
  // and the timer count was close to zero, count the
  // overflow as part of this time.
  if ((TIFR1 & _BV(TOV1)) && (ICR1 < 1024))

  if (PulseLowTime == 0)
    if (TCCR1B & _BV(ICES1))
      // Interrupted on Rising Edge
      if (firstRisingEdgeTime)  // Already have the first rising edge...
        // ... so this is the second rising edge, ending the low part
        // of the cycle.
        secondRisingEdgeTime = overflows; // Upper 16 bits
        secondRisingEdgeTime = (secondRisingEdgeTime << 16) | ICR1;
        PulseLowTime = secondRisingEdgeTime - fallingEdgeTime;
        firstRisingEdgeTime = 0;
        firstRisingEdgeTime = overflows; // Upper 16 bits
        firstRisingEdgeTime = (firstRisingEdgeTime << 16) | ICR1;
        TCCR1B &= ~_BV(ICES1); // Switch to Falling Edge
      // Interrupted on Falling Edge
      fallingEdgeTime = overflows; // Upper 16 bits
      fallingEdgeTime = (fallingEdgeTime << 16) | ICR1;
      TCCR1B |= _BV(ICES1); // Switch to Rising Edge
      PulseHighTime = fallingEdgeTime - firstRisingEdgeTime;

void loop()
  uint32_t pulseHighTime = PulseHighTime;
  uint32_t pulseLowTime = PulseLowTime;

  // If a sample has been measured
  if (pulseLowTime)
    // Display the pulse length in microseconds
    Serial.print("High time (microseconds): ");
    Serial.println(pulseHighTime / 16.0, 2);
    Serial.print("Low time (microseconds): ");
    Serial.println(pulseLowTime / 16.0, 2);

    uint32_t cycleTime = pulseHighTime + pulseLowTime;
    Serial.print("Cycle time (microseconds): ");
    Serial.println(cycleTime / 16.0, 2);

    float dutyCycle = pulseHighTime / (float)cycleTime;
    Serial.print("Duty cycle (%): ");
    Serial.println(dutyCycle * 100.0, 2);

    float frequency = (float)F_CPU / cycleTime;
    Serial.print("Frequency (Hz): ");
    Serial.println(frequency, 2);

    delay(1000);  // Slow down output

    // Request another sample
    PulseLowTime = 0;

I'm actually using a chinese clone NANO. Aside from the problems I am having now they have worked fine for me in the past, but I won't discount it from being the problem.

Thanks for the code, but I can't see where to put in the input pin number i'm using? (pin 4)

I went back to the car today to test the new sketck with no neopixels or anything, just to sense the signal, and send it to the usb serial output on the laptop.

In this video, I have set no delay in the loop() and the rpm readings are showing the right numbers, but only like half the time (rpm goes from idle 700 to about 1600)

In this video, I have set a 1ms delay (through millis(), not delay() )

At least now I know that the neopixels are not presenting any issues... But the fact remains that the Arduino simply isn't reading the waveform accurately. I measured the wire both attached to and disconnected from the Arduino, and it was the same perfect waveform, so I am confident nothing is amiss electrically.

I hypothesize that the nano I am using is - for whatever reason, coding or otherwise - simply not polling the digital pin fast enough to "see" every rising wave, akin to when the oscilloscope is set to 200ms/div, as the outputs on both devices are eerily similar.

For your info, this is the code I used to measure the RPM in the videos. In my arduino simulator, it reads a pulsed waveform perfectly and reports the revs steadily, at whatever spacing.

//RPM only, using ECU Wire IGT4

//Pulses from IGT4 - has a 5v pulse that ECU sends when wants to trigger spark on cylinder #4, arduino reads
//and counts time between pulses
          int IGT4 = 4;
//unsigned long duration; //for pulsein()
//float duration = 0; //for pulsein()
        float sensorRPM = 0;
        float sparksPerSec = 0;
unsigned long millisStart = 0;

//non-blocking pulseIn(): returns the pulse lenght in microseconds when the falling edge is detected. Otherwise returns 0.
static unsigned long rising_time ;  //time of each rising edge
       unsigned long last_time = 0;        //previous edge time
                 int last_state = LOW;               //previous pin state
       unsigned long pulse_width = 0;      //time between pulses in microseconds
               float small_pulse = 0;              //time between pulses in milliseconds
void setup() 
   //assign input for IGT4 wire

   millisStart = millis();

void loop() 
// if ( (millis() - millisStart) > 1 ) {

  int state = digitalRead(IGT4);     //current pin state
  //on rising edge: record current time
  if (last_state == LOW && state == HIGH)
      rising_time = micros();
      pulse_width = rising_time - last_time;
      small_pulse = (float)pulse_width*0.001;
      last_time = rising_time;
      last_state = state;
  if (last_state == HIGH && state == LOW)
      last_state = LOW;
  //measure revs from pulse_length
  //first calculate how many sparks happening per second, 1000ms divided by delay between sparks
  sparksPerSec = 1000/small_pulse;
  //next multiply by 60 for sparks per minute - two revs per spark
  sensorRPM = sparksPerSec*120;

//millisStart = millis();   }

You can't. The input MUST be on Pin 8 because that is the one pin with that hardware feature.

OK I wired the IGT wire to pin 8, and tried your code johnwasser, but it just resulted in a garbage output:

I also managed to get an interrupt-based sketch working, but alas the results were the same as before.

I am starting to think that perhaps this is not a programming issue, but a hardware issue. I am going to create a new thread in the appropriate area to see if I can verify that I have wired everything up properly.

Does this match the baud rate in the sketch which produced "garbage" ?

Don't open a new thread. Use this one for any continuation of this topic, even hardware related aspects.

He opened a new thread.

A simulator is probably perfectly capable of creating a nice clean pulse train. In your real world situation, you are not getting such a clean pulse train. You don't appear to have any debouncing logic in your program to filter out spurious results.

has the OP made this change?

I ask because in the new thread the OP created, with a diagram, he does not show he has done the thing.

Many times, I will write try this or try that. Which the OP never does, so I wait on a response for the thing do I recommended to be done. Which means no more help from me till the thing do has be done.

OP, I'd use some sort of isolation between the car's input signal and the MCU. An opto-isolator would do the thing.

I apologize, I posted before I saw your reply. You were correct in that the baud rate was wrong, after setting it to 9600 the output worked correctly, idahowalker yes I connected the input to pin 8 also :

Revs start at idle/700rpm and later increase to about 1200rpm. It seems to be actually pretty accurate, the most accurate method so far. But its not perfect, there are still random results and I haven't tried altering the delay. 6v6gt, you mentioned debouncing logic. I will look into that, thank you.

This still is taken at 0:37 in the video in post #16.


It is a glitch, certainly caused by a spurious spike in the input data. 221 Hz is over 13,000 RPM.
You've simply got to filter out results which deviate too much from the average or are otherwise implausible.

I tried averaging as a sort of filter in my original pulsein() and digitalread() sketches, it worked to a certain degree but even then the result still bounced around. I also tried filtering out doubles, halves of the average etc. but it always resulted in a messy output regardless.

I just read a little bit about debouncing and I will try and see if i can get a 16ms debounce time (16ms per wave is about 7500 revs, 100 higher than the RPM limiter) into the sketch to try and filter out any results shorter than that and see how it turns out.

My recommendation is that you proceed with the input capture solution presented by @johnwasser
You've said it gives generally good results, You can't filter the input directly, but you can throw away out of range results. It can probably be simpler because you don't need the duty cycle so you need only look for rising edges. Also, the input capture is less likely to be affected by the neopixel library behaviour.

It appears that you have a fairly noisy data source. I would:

  1. Change the code to only look for rising edges.
  2. Throw out data points more than 'x' away from the current moving average.
  3. Keep a moving average of the non-glitched readings.