Measuring Time Between pulses - Using Pin Change interrupts

Hello Everyone,
I am making a little program that measures the time between pulses. First pulse starts the timer the middle pulses are snapshots of the timer and the last pulse stops the timer.
The idea being i want to the order of the pulses as the come in and the time between each. The number of pulses are always the same. In this example i am only using three pulses on three different pins.

The code works but i am getting significant time variation in the output based on order of input pulses (in this case A2,A1,A0 compared to A0,A1,A2). I was expecting slight changes in time due to code operations (and the variation on that) as well as clock instability but not in the order of uS?!?! Is this correct?

I tested a few if statements etc and they only take 2 clock cycles to process each the big time sucker is copying over the timer1 values which takes around 60 clock pulses. (based on my own testing)

I averaged some results out and came up with the following results

Order - A2,A1,A0 with the Max Min times below

Max Snap_shot =125.19
Min Snap_shot = 116.19

Max Final = 325.94
Min Final = 317

Order - A0,A1,A2 with the Max Min times below

Max Snap_shot =274
Min Snap_shot = 260

Max Final = 407.63
Min Final = 401.13

Is the code actually working?

Test Setup. I have two nano's,
A - is the main nano that i plan on using Inputs are A0,A1,A2
B - Test nano that sends pulses to A on pins 7,8,12 to 'A' on pins.

Code for 'A'

volatile boolean TimedOut = false;
volatile boolean Success = false;
volatile boolean Listening = true;

volatile boolean First_Trigger = true;
volatile boolean Second_Trigger = true;
volatile long Snap_shot = 0;

volatile int Pin_A0_Place = 0;
volatile int Pin_A1_Place = 0;
volatile int Pin_A2_Place = 0;

volatile char PBNOW;
volatile char PBLAST;

ISR (PCINT1_vect)// handle pin change interrupt for A0 to A5 here
{

  PBNOW = PINC ^ PBLAST; // Find changed pins by doing a XOR with last state. any bits that changed will be 1 all else 0. e.g. 1000 ^ 0010 = 1010 - Thanks ARWARE
  PBLAST = PINC;
  switch (PBNOW) { // only one of these could have changed, first come first served
    //************************************************************************************************************A0
    case (1 << PINC0):
      //A0 Triggered the Interrupt
      if (Pin_A0_Place == 0) // checks if we have placed the interrupts
      {
        if (First_Trigger)
        {
          TCCR1B =  0x01; // Timer 1 precaller '1' starts timer 1
          First_Trigger = false;
          Pin_A0_Place = 1;

        }
        else if (Second_Trigger)
        {
          Snap_shot = TCNT1; // grabs the current time for our snap shot
          Second_Trigger = false;
          Pin_A0_Place = 2;

        }
        else
        {
          TCCR1B =  0x00; // Timer 1 precaller '0' Stops timer 1
          Pin_A0_Place = 3;
          Success = true;
          PORTD &= 0b01111111;
        }
        PCMSK1 &= 0b11111110;  // Disable PCINT8 =  A0 but leave others alone (A2,A1)
      }
      break;

    //************************************************************************************************************A1
    case (1 << PINC1):
      ////A1 Triggered the Interrupt
      if (Pin_A1_Place == 0) // checks if we have placed the interrupts
      {
        if (First_Trigger)
        {
          TCCR1B =  0x01; // Timer 1 precaller '1' starts timer 1
          First_Trigger = false;
          Pin_A1_Place = 1;

        }
        else if (Second_Trigger)
        {
          Snap_shot = TCNT1;
          Second_Trigger = false;
          Pin_A1_Place = 2;

        }
        else
        {
          TCCR1B =  0x00; // Timer 1 precaller '0' Stops timer 1
          Pin_A1_Place = 3;
          Success = true;

        }
        PCMSK1 &= 0b11111101;  // Disable PCINT9 =  A1 but leave others alone (A2,A0)
      }
      break;

    //************************************************************************************************************A2
    case (1 << PINC2):
      ////A2 Triggered the Interrupt
      if (Pin_A2_Place == 0) // checks if we have placed the interrupts
      {
        if (First_Trigger)
        {
          TCCR1B =  0x01; // Timer 1 precaller '1' starts timer 1
          First_Trigger = false;
          Pin_A2_Place = 1;

        }
        else if (Second_Trigger)
        {
          Snap_shot = TCNT1;
          Second_Trigger = false;
          Pin_A2_Place = 2;

        }
        else
        {
          TCCR1B =  0x00; // Timer 1 precaller '0' Stops timer 1
          Pin_A2_Place = 3;
          Success = true;

        }
        PCMSK1 &= 0b11111011;  // Disable PCINT10 =  A2 but leave others alone (A1,A0)
      }
      break;
    default:
      break;
  }

}

void prepTiming ()
{
  // reset Timer 1
  TCCR1A = 0; // Houses compare settings plus how it counts, here we want to count 0 - max (normal mode is WGMxx bits set to 0 - WGM10, WGM11 are in TCCR1A, WGM12, WGM13 are in TCCR1B
  TCCR1B = 0; // prescalers = 0 = means timer 1 is off/stopped
  TIMSK1 = 0x01;   // Turn On Overflow interrupt on Timer 1 so i know if i missed my pulse
  TCNT1 = 0;     // sets Timer1 Counter to zero
  // We are stopping interrupts on timers 0 and 2 so other timing functions will not work but they will not interrupt us (millis etc)
  TIMSK0 = 0x00;   // Turn off all interrupts on Timer 0
  TIMSK2 = 0x00;   // Turn off all interrupts on Timer 2

  // Reset prescalers
  GTCCR = bit (PSRASY);        // reset prescaler - seen other people doing it not sure why? data sheets has it greyed out???

}  // end of startTiming


//******************************************************************
//  Timer1 Overflow service routine will trigger 16Mhz with no prescaler, overflow would cause it to trigger every 4.096ms
ISR (TIMER1_OVF_vect)
{
  TCCR1B =  0x00; // sets prescaler to 0 stoping timer1
  TCNT1 = 0;     // sets Timer1 Counter to zero
  TimedOut = true;
}

void setup ()
{
  //pinMode(11, OUTPUT); // used to manually test / debug
  pinMode(13, OUTPUT); // used to assert so that we can 'add a delay' debug

  //don't think this is needed - PCINT's for those have been turned off
  pinMode(A3, OUTPUT);// going to write these high so no noise input
  pinMode(A4, OUTPUT);
  pinMode(A5, OUTPUT);
  digitalWrite(A3, INPUT_PULLUP);// so the is no possibility of noise on these pins that can float high and create pin change interrupts...
  digitalWrite(A4, INPUT_PULLUP);
  digitalWrite(A5, INPUT_PULLUP);

  // Setting up Pin Change Interrupts
  PCMSK1 = 0b00000111;  // want PCINT's on pins A2,A1,A0
  PCIFR  |= bit (PCIF1);   // clear any outstanding interrupts
  PCICR |= 0b00000010; // bit PCIE1 to 1 all others zero --- enable pin change interrupts for A0 to A5

  Serial.begin(115200);

}

void loop ()
{
  
  PBLAST = PINC; // sets the state of Pin C in terms of which pins are high and which are low
  prepTiming();

  while (!TimedOut && !Success) // Wait here until we either time out or we have a successful timed event
  {}
  //  displaying the results

  if (TimedOut)
  {
    TIFR1 &= 0b11111110; // clears the TOV1 (Timer 1 Overflow Flag)
    Serial.println ("Overflow");
  }
  if (Success)
  {
    Serial.println(String(((Snap_shot) * 62.5) / 1000)+":"+String(((TCNT1) * 62.5) / 1000));
    //Serial.println (" ");
  }

  //delay(500);
  //variables that i need to reset in order to loop around
  TimedOut = false;
  Success = false;
  First_Trigger = true;
  Second_Trigger = true;
  Pin_A0_Place = 0;
  Pin_A1_Place = 0;
  Pin_A2_Place = 0;
  Snap_shot = 0;
  PCMSK1 = 0b00000111;  // want PCINT's on pins A2,A1,A0
  PCICR |= 0b00000010; // last thing re enable all PCINT's on

}

Code for 'B'

void setup() {
  // put your setup code here, to run once:
pinMode(7,OUTPUT);
pinMode(8,OUTPUT);
pinMode(12,OUTPUT);
pinMode(13,OUTPUT);
PORTD &= 0b01111111; // sets PortD LOW (D0-7) - only want D7 LOW ( PD7)
PORTB &= 0b00000000; // sets PortB LOW (D8-13) - all
}

void loop() {

PORTD |= 0b10000000; // asserts pin 7 = A0
delayMicroseconds(202);

PORTB |= 0b00000001; // asserts pin 8 = A1
delayMicroseconds(202);

PORTB |= 0b00010000; // asserts pin 12 = A2
delayMicroseconds(202);
 

PORTD &= 0b01111111; // sets PortD LOW (D0-7) - only want D7 LOW ( PD7)
PORTB &= 0b00100000; // sets PortB LOW (D8-13) - all except LED Pin 13
delayMicroseconds(202);

}

Any help would be greatly appreciated!!!

Cheers

Chris

Maybe look here:
https://www.gammon.com.au/timers

And search for the title: Timing an interval using the input capture unit

The input capture unit uses hardware to capture the time it is triggered.

Thanks for the reply.
If i understand you correctly you are suggesting the use of a hardware input capture.

Could be wrong here but the input capture on the Nano is only on one pin and I need to be able to distinguish the order of the pulses not just the duration... Is there another way to do this using the hardware input capture pin?

Thanks again for your response

I'd think that you get more consistent readings without using interrupts. Did you ever try?

Yes. I didn't look closely enough at your code. Your correct that, in this case where you need multiple input pins, you cannot use the input capture registers on the ATmega328P.

Just to validate your program logic in part "A", it may be good to print an error:
a) if the default case is reached
b) if a case is entered where the corresponding Pin_AX_Place is not 0 (it is not actually clear to me what you are guarding against here)

It may also be cleaner to get 3 identical pulses (mark and space) from part "B" if you did not use the loop() but did everything in a while statement in setup(). The loop does some housekeeping (not much) but does check the serial port on each iteration. (see the Arduino main() function for details)

DrDiettrich, So you are suggesting just reading the Pins directly? I guess i can do that if lock it into a whilst reading loop. One issue that i am faced with is additional pulses after the first... but i might be able to take care of those by using a latching comparator feed back loop. Will give it some more thought.

6V6gt I have tested the code with some debug serial print lines out. The order is always correct using the code below for "A". Just print out Pin_A0_Place, Pin_A1_Place, Pin_A2_Place.

With regards to your point b) i am using this as a snubber as my actual application might have multiple pulses, I only care about the first Pin Change on this pin. So after that pin has triggered and been placed i don't care if it triggers again. wrapping each case allows me to move with minimal distrubtion?... that was my idea anyway

Also not sure if anyone has a good oscilloscope but just using code B. My really old O scope does not have input capture but when I drop the delay between pulses to about 10 uS I see a variation on the output pulses by about 6uS, not sure if anyone could test this. It appears as if its only every other pulse that is off. The majority of the pulses are bang on.

Thanks again for the replies

DrDiettrich:
I'd think that you get more consistent readings without using interrupts. Did you ever try?

After some additional thought, wouldn't using the constant polling method not provide more sporadic measurements since the time between pulses can be very small. I was hoping to get a few microsecond delay. or would this depend on the method in the while loop?

I also did a clock stability verification. Running on the 16Mhz clock i found i am actually Max=15,991,502 and Min=15,991,197 so a swing of 305 pulses which equates 19.06 ppm or +-9.53 ppm. So if correct this is actually very stable for the generic Nano clock (this is from 3500 samples at a 1s interval)... too good to be true?

Would appreciate any feedback

vermin_sapper:
I was hoping to get a few microsecond delay.

If the time between pulses is only a few microseconds, you may well be still in the interrupt sequence when the second pulse arrives.

You probably cannot win either way, even using machine code. :roll_eyes:

If you can start with slower pulses you can find out the minimum handling time achievable. Then calculate the controller clock frequency required for handling your fastest pulses. There exist Arduinos with more than 16MHz clock.

Hello,
Thanks for all the responses. DrDiettrich, i relooked at a practical "acceptable error" (3-6uS) and i am happy with the error that i get at 16Mhz. So far i have been getting pretty reliable results.

There is one thing is is bugging me though.

By ONLY changing the order of the pulses from 'B' i get significant different times. I was expecting 1-2uS different due to the additional computing but my different is a lot more. I did compare clock sources an the total different came out to about 37,000 over 1 second. Over 500uS seconds this comes out to about 1 uS difference so it cannot be this.

For example.

A0 Placed= 1
A1 Placed= 3
A2 Placed= 2
Snapshot= 283.50
T1 Stopped at= 401.00

A0 Placed= 1
A1 Placed= 2
A2 Placed= 3
Snapshot= 280.63
T1 Stopped at= 401.38

A0 Placed= 2
A1 Placed= 1
A2 Placed= 3
Snapshot= 122.88
T1 Stopped at= 323.38

A0 Placed= 3
A1 Placed= 1
A2 Placed= 2
Snapshot= 122.38
T1 Stopped at= 322.88

Any ideas what is causing this?

Thanks

If the readings persist, something is wrong with your code - either with the generator or the scanner.