Pin-Change interrupts for Sonar

I had a hard time figuring out how pin-change interrupts work. They are rather simple, but difficult to generalize. I got them working for some sonar sensors on an Arduino Mega board. Thought I'd post here to help others.

To summarize, You only need to do three simple things to set up pin-change interrupts:

PCICR |= 1; // enable PCI0 pin group interrupt
PCMSK0 |= 0b1100; // enable PC interrupts on arduino pins 50,51, which are in group PCI0
...
/// Pin-Change interrupt, for group PCI0 (arduino mega pins 13..10, and 50..53)
ISR(PCINT0_vect) {
// do what you need to do when one of the pins in this group changes.
// usually... check to see which pin changed here... then do something.
}

There are three interrupt "groups". I can post tables for which pins are in which group later, if desired.
The three low bits of PCICR control which of these groups have pin-change interrupts enabled.

Each of these groups has a mask, for which pins in this group will trigger the interrupt.
PCMSK0, PCMSK1, PCMSK2.

Each group gets one interrupt vector... so when the interrupt is triggered, your
code must figure out which pin changed, and take appropriate action.
The interrupt vector for each PCI group is:ISR(PCINT0_vect), ISR(PCINT1_vect), and ISR(PCINT2_vect) respectively.

Here is some sample code, which used pin-change interrupts to record the timing for a set of sonar sensors.
We need to note the time of the falling edge of the monitored pins.

// settings for mega testing an HC-SFR0 sonar
//
// when user sends a character over serial, report range timings
//
// This version tries to use Pin-Change interrupts to get sonar timings

#define PIN_TRIGGER 31
#define PIN_ECHO0 50
#define PIN_ECHO1 51
#define PIN_ECHO2 52
#define PIN_ECHO3 53

#define NUM_SONAR_SENSORS 2  // assume starting pins 50-53, then 13..10
#define TIMEOUT_US 30000
//----------------------------------------------- SonarState
const char TAB = 't';

class SonarState {
public:
  byte pinEcho[NUM_SONAR_SENSORS], nDone;
  bool done;
  int dt[NUM_SONAR_SENSORS];
  unsigned long ping, start;
  long nPulses;

  void begin()
  {
    start = ping = 0;
    dt[0]=dt[1]=0;
    pinEcho[0]=PIN_ECHO0;
    pinEcho[1]=PIN_ECHO1;
    nPulses = 0;
    nDone = 0;
    done = false;

    pinMode(PIN_TRIGGER,OUTPUT);
    pinMode(PIN_ECHO1,INPUT);
    pinMode(PIN_ECHO0,INPUT);
    digitalWrite(PIN_TRIGGER,LOW);

    PCMSK0 |= 0b1100; // enable PC interrupts on arduino pins 50,51
  }

  void sendPing()
  {
    digitalWrite(PIN_TRIGGER,HIGH);
    start = micros();
    nPulses++;
    dt[0] = dt[1] = 0;
    nDone = 0;
    done = false;
    delayMicroseconds(2);  // whole pulse must be at least 10us, but delay can be less because of overhead and above code
    digitalWrite(PIN_TRIGGER,LOW);
    ping = micros();

    // consider a delay here, to allow echo pins to go high?
    PCICR  |= 1;  // enable PCI0 pin group interrupt
  }

  // too much time passed.  reset even if not done, and set timeout vals
  void timeout(int longTime)
  {
    PCICR  &= 0xfe;  // disable PCI0 pin group interrupt
    done = true;
    nDone = NUM_SONAR_SENSORS;
    for (int i=0; i < NUM_SONAR_SENSORS; i++)
      if (dt[i] == 0) dt[i] = longTime;
  }

  /// blocks until reading is complete
  void getReadingBLOCK()
  {
    sendPing();
    int et;
    do {
      et = (int)(micros() - ping);
    } while ((!done) && (et < TIMEOUT_US));
    if (!done) timeout(et);
  }

  // convert current dt[idx] to cm
  int cm(byte idx)
  {
    int t = dt[idx];
    if ((t<=0)||(t>=TIMEOUT_US)) return(9999);
    return( (int)((t-464)/58) );
  }

  /// diagnostic method to do a sonar reading, and print results
  void printReading()
  {
    getReadingBLOCK();

    Serial.print(nPulses); Serial.print(") ");
    Serial.print(start);  Serial.print(TAB);
    Serial.print(ping-start);
    for (int i=0; i < NUM_SONAR_SENSORS; i++)
      {
        Serial.print(TAB); Serial.print(dt[i]);
        Serial.print("(");Serial.print(cm(i));Serial.print(")");
      }
    Serial.println();
  }

} Sonar;

//------------------------------------------ setup/loop
void setup()
{
  cli();  // disable interrupts
  Sonar.begin();
  sei();  // enable interrupts?
  Serial.begin(9600);
}

/// Pin-Change interrupt, for arduino pins 50..53, and 13..10
ISR(PCINT0_vect) {
  unsigned long t = micros();
  int dt = t - Sonar.ping;

  // It has been measured as taking 464us from falling edge of trigger
  // to rising edge of echo duration pulse
  if (dt < 470) return;  // still waiting for leading edge

  // check which pin fell here, and note time
  Sonar.nDone = 0;
  for (int i=0; i < NUM_SONAR_SENSORS; i++)
    {
      if (Sonar.dt[i] == 0)
        {
          if (digitalRead(Sonar.pinEcho[i]) == LOW)
            {
              Sonar.dt[i] = dt;
              Sonar.nDone++;
            }
        }
      else Sonar.nDone++;
    }
  if (Sonar.nDone == NUM_SONAR_SENSORS)
    {
      Sonar.done = true;
      PCICR  &= 0xfe;  // disable PCI0 pin group interrupt
    }
}


void loop()
{
  if (Serial.read() < 0) return;  // no update command
  Sonar.printReading();
}

pins in group PCI2 correspond to bits of PCMSK2, etc.
The first pin listed is the MSB of PCMSKn, and the last pin listed corresponds to the LSB of PCMSKn

PCINT23  Analog15             group PCI2
PCINT22  Analog14
PCINT21  Analog13
PCINT20  Analog12
PCINT19  Analog11
PCINT18  Analog10
PCINT17  Analog9
PCINT16  Analog8

PCINT15  N/A                  group PCI1
PCINT14  N/A
PCINT13  N/A
PCINT12  N/A
PCINT11  N/A
PCINT10  DIO 14
PCINT9   DIO 15
PCINT8   DIO 0

PCINT7   DIO 13            group PCI0
PCINT6   DIO 12
PCINT5   DIO 11
PCINT4   DIO 10
PCINT3   DIO 50
PCINT2   DIO 51
PCINT1   DIO 52
PCINT0   DIO 53

If/when I find a similar table for AVR328/168... I'll post it, unless somebody else does.

There exists a PINCHANGEINT lib - Arduino Playground - PinChangeInt -

robtillaart:
There exists a PINCHANGEINT lib - Arduino Playground - PinChangeInt -

yes, but it is inefficient and inapporpriate for my app. The above shows how to do it using pin-change interrupts.

yes, but it is inefficient and inappropriate for my app.

OK, fair enough,
Did you test it and did it prove inadequate? just curious.

Mr-What:

robtillaart:
There exists a PINCHANGEINT lib - Arduino Playground - PinChangeInt -

yes, but it is inefficient and inapporpriate for my app. The above shows how to do it using pin-change interrupts.

It's my understanding that PINCHANGEINT is fairly efficient. The author seems very concerned with speed and from what I remember, the library is almost all port register driven. With that said, I haven't used it much nor done my own speed checks to know with certainty. My guess is that the overhead has a lot to do with trying to make a general-purpose library that works on multiple microcontrollers instead of hard-coded like you have done. I've considered adding a pin change interrupt method to NewPing, but it was overly complex to do with multiple pins and multiple microcontrollers, so that could be the issue.

Also, I've lost my desire now that I have a $19 Teensy 3.0 that has a hardware pin interrupt on every pin.

On a side note, have you considered using only 6 sensors so you can use just the hardware pin interrupts instead of messing with PINCHANGEINT?

Tim

The main issue was indeed hard-coding to the specific MCU. Very hard to generalize. Once I figured out how to do pin-change interrupts in straight AVR, I found it to be just as easy, if not easier, to understand than the libraries, so I wanted to share.

The pin-change interrupts are perfect for my array-of-sonars. One interrupt for all sonars is quite convenient. I wanted to save the external interrupts for other things. This is a group project robot, and who knows what other sensors we might add.

I have not checked the latest sonar libraries, but in the past most of them blocked. This does not. The interrupts record ping times, and I process them in my polling loop when I need them.
It is also unique in that I have the whole array sharing a single trigger signal.

I have nothing against the libraries, but I wanted to share code to do it directly with AVR. I found it very difficult to figure out how to do this, but now that I understand it, I find it at least as simple to do as the libraries. One big feature is enable/disabling the interrupt with a single command, no function call overhead: (PCICR |= 1; PCICR &= 0xfe;)