Today I am going to bring you something a little more advanced in Arduino, and it is the way to use PCINT Interrupts (Pin Changes interrupts), in Arduino. Earlier we talked about hardware interrupts (INT), which were limited to certain pins depending on the Arduino model.

The advantages of PCINT interrupts are that you can use any pin on the Arduino to trigger them (which is quite useful). Of course it has its disadvantages, and the main one is that you cannot indicate when to trigger the interrupt as you would with hardware interrupts. These will be activated whenever there is any change of state on the pin, either from HIGH to LOW, or from LOW to HIGH.

To understand it better, I will divide this guide into three parts:

  • Register of ports that will trigger interrupts (PCICR)
  • Pin registers that will trigger interrupts (PCMSK)
  • Generate Events (ISR PCINT Vect)

Port Registers That Will Trigger Interrupts (PCICR)

In this first part we will learn how to activate each of the interrupt groups in order to use it. To do this, the first thing we have to learn are the different groups of pins that there are, for which we can take a look at this image:

Arduino UNO Pinout

In this diagram we can see the different groups of pins that there are, which are named as PB, PC and PD:

  • First we will find the PB port, which corresponds to the group of pins PCINT0 to PCINT5 and which are pins D8 to D13.
  • The second port we see is the PC, which corresponds to the group of pins PCINT8 to PCINT13, corresponding to pins A0 to A5.
  • Finally we have the PD port, which corresponds to the group of pins PCINT16 to PCINT23, corresponding to digital pins D0 to D7.

It is very important to know this, since to activate interrupts on a pin we will have to know which group it belongs to and activate the group. For this we will use the PCICR register, in which we will indicate on which port we want to activate the interrupts.

Don’t be scared yet, using it is very easy. To do this, we will simply have to perform a bitwise operation (if you don’t know what I’m talking about, you can go through this post to find out more). Let’s go to the example:

PCICR | = B00000001; // We activate the interrupts of the PB port
PCICR | = B00000010; // We activate the interrupts of the PC port
PCICR | = B00000100; // We activate the interrupts of the PD port

Easy, right? Well, as a bitwise operation, you can activate interrupts on several ports at the same time:

PCICR |= B00000101; // We activate the interrupts of the PB and PD ports

Surely you have noticed that the order of the ports is the reverse of the order of the numbers and starts from the right. This is because the bit on the right is the least significant and therefore always starts with it.

Once the port in which we want to generate the interrupts is activated, we will also have to activate which pin of that port will generate said interrupts, otherwise it would be crazy because any change in the pins would generate an interrupt. To do this, we will continue with part 2 of this guide.

Pin registers that will trigger interrupts (PCMSK)

Once we have activated the port (s) that we want to trigger interrupts, it is time to indicate which pins of those ports will do it. This is done through the PCMSK registers, of which we have one per port (PCMSK0, PCMSK1 and PCMSK2). The correspondence between ports and registers is:

  • PCMSK0 -> PB -> D8 to D13 pins
  • PCMSK1 -> PC -> A0 to A5 pins
  • PCMSK2 -> PD -> D0 to D7 pins

Its use is the same as indicated above with PCICR, being necessary to perform bitwise operations for this. Of course, it will be just as easy as we have seen before:

PCMSK0 |= B00000100; // We activate the interrupts on pin D10
PCMSK1 |= B00001000; // We activate the interrupts on pin A3

As before, we can also activate interrupts on several pins in the same bitwise operation. For the most clueless, remember that binary numbers begin to be read from the right, so to indicate the number of the pin that we want to activate it must be from right to left. In the examples above we can see that only the third digit starting from the right is 1, which means: D8 -> 0, D9 -> 0, D10 -> 1 …

Generate Events (ISR PCINT Vect)

Ahora sólo nos queda atender a las llamadas que nos hagan nuestros pines a través de las interrupciones, porque sino les dejaríamos gritando sin atenderlos… para ello usaremos el vector de interrupción (PCINT Vect). ¿Qué es el Vector de interrupción), pues básicamente una parte de código parecido a una función que se ejecutará cuando se genere una interrupción. Al igual que para el registro de pines, dispondremos de un vector para cada puerto y que podremos usar de forma independiente. Por supuesto siempre que hayamos activado las interrupciones en dicho puerto. Estos vectores son PCINT0_vect, PCINT1_vect y PCINT2_vect:

  • PCINT0_vect -> PB -> Pines D8 a D13
  • PCINT1_vect -> PC -> Pines A0 a A5
  • PCINT2_vect-> PD -> Pines D0 a D7

To be able to use them we will simply have to use the ISR routine, for which I always say that the best is an example to understand it:

ISR (PCINT0_vect) {
  // code to execute
}

This code, as if it were a function, has to go outside of setup and main.

As you can see, we will indicate the vector to which we will subscribe so that the code is executed when an interrupt is fired in it. Of course, as we discussed earlier, we will not receive in any way the pin on which the interrupt has been made, so we will have to detect it. We can use it to for example raise or lower a counter:

int counter = 0;

void setup() {
  Serial.begin(9600);

  pinMode(4, INPUT_PULLUP);
  pinMode(5, INPUT_PULLUP);

  PCICR |= B00000100; // Enable interrupts on PD port
  PCMSK2 |= B00110000; // Trigger interrupts on pins D4 and D5
}

void loop() {
  Serial.print("The counter is now at: ");
  Serial.println(counter);
  
  delay(100);
}

ISR (PCINT2_vect) {
  if (digitalRead(4)) {
    counter++;
  }
  else if (digitalRead(5)) {
    counter--;
  }
}

Or we will keep a check on the last state of each pin to detect which one has changed (the cause of the interrupt):

bool pin4 = false;
bool pin5 = false;

void setup() {
  Serial.begin(9600);

  pinMode(4, INPUT);
  pinMode(5, INPUT);

  PCICR |= B00000100; // Enable interrupts on PD port
  PCMSK2 |= B00110000; // Trigger interrupts on pins D4 and D5
}

void loop() {
}

ISR (PCINT2_vect) {
  if (digitalRead(4) != pin4) {
    Serial.print("Pin4 has changed state and its new state is: ");
    Serial.println(!pin4);
    pin4 = !pin4;
  }
  else if (digitalRead(5) != pin4) {
    Serial.print("Pin5 has changed state and its new state is: ");
    Serial.println(!pin5);
    pin5 = !pin5;
  }
}

So we can use it for, for example, switches or the like.

Extra

And finally I will leave you an extra tip: Interrupts, as their name suggests, interrupt the flow of the main program. This can affect the operation of your application in cases such as, for example, that you are controlling a stepper motor and the interrupts are very long. That is why it is very important that interrupts are as short as possible. To do this, a little trick that we can use is to avoid the Arduino functions and use the registers directly, such as this equivalent code for the counter above:

int counter = 0;

void setup() {
  Serial.begin(9600);

  pinMode(4, INPUT_PULLUP);
  pinMode(5, INPUT_PULLUP);

  PCICR |= B00000100; // Enable interrupts on PD port
  PCMSK2 |= B00110000; // Trigger interrupts on pins D4 and D5
}

void loop() {
  Serial.print("The counter is now at: ");
  Serial.println(counter);
  
  delay(100);
}

ISR (PCINT2_vect) {
  if (PIND & B00010000) {
    counter++;
  }
  else if (PIND & B00100000) {
    counter--;
  }
}

PIND is the status register of the PD port pins, so through a bitwise operation we can detect if a specific pin is HIGH or LOW. This will avoid the entire block of code that constitutes the digitalRead () function, and therefore our program will work faster and also as an extra, we will save the memory that this function would occupy.

Also during the execution of certain parts of the code we can pause global interrupts, thus preventing interrupts from interrupting the execution of said code. For this we will use the cli and sei functions:

int counter = 0;

void setup() {
  Serial.begin(9600);

  pinMode(4, INPUT_PULLUP);
  pinMode(5, INPUT_PULLUP);

  PCICR |= B00000100; // Enable interrupts on PD port
  PCMSK2 |= B00110000; // Trigger interrupts on pins D4 and D5
}

void loop() {
  cli(); // We pause the interrupts
  Serial.print("The counter is now at: ");
  Serial.println(counter);
  sei(); // We reactivate the interrupts
  
  delay(100);
}

ISR (PCINT2_vect) {
  if (PIND & B00010000) {
    counter++;
  }
  else if (PIND & B00100000) {
    counter--;
  }
}

It is not the best example, but hey… you can see that the interrupts are paused before sending data through the Serial port with Serial.print, and then they are reactivated once it is finished. It is very important to reactivate the interrupts again or else they will stop working.

These functions do not need to be used within interrupts, as they automatically pause the other interrupts.

As always, I hope you liked it and do not hesitate to comment on anything. All the best!

Daniel Carrasco

DevOps with several years of experience, and cloud architect with experience in Google Cloud Platform and Amazon Web Services. In his spare time experimenting with Arduino and electronics.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.