Hoy os voy a traer algo un poco más avanzado en Arduino, y es la forma de utilizar las Interrupciones PCINT (Pin Changes interruptions), en Arduino. Anteriormente ya hablamos de las interrupciones de hardware (INT), las cuales estaban limitadas a ciertos pines dependiendo del modelo de Arduino.

Las ventajas de las interrupciones PCINT son que puedes usar cualquier pin del Arduino para desatarlas (lo cual es bastante útil). Por supuesto tiene sus desventajas, y la principal es que no puedes indicar cuando activar la interrupción como lo harías con las interrupciones de hardware. Estas se activarán siempre que haya algún cambio de estado en el pin, ya sea de HIGH a LOW, o de LOW a HIGH.

Para entenderlo mejor dividiré esta guía en tres partes:

  • Registro de puertos que activarán las interrupciones (PCICR)
  • Registros de pines que activarán las interrupciones (PCMSK)
  • Generar eventos (ISR PCINT Vect)

Registros de puertos que activarán las interrupciones (PCICR)

En esta primera parte aprenderemos cómo activar cada uno de los grupos de interrupciones para poder usarlo. Para ello, lo primero que tenemos que aprender son los distintos grupos de pines que hay, para lo cual podemos echar un vistazo a esta imagen:

Arduino UNO Pinout

En este esquema podemos observar los diferentes grupos de pines que hay, los cuales están nombrados como PB, PC y PD:

  • Primero nos encontraremos con el puerto PB, que corresponde al grupo de pines PCINT0 a PCINT5 y que son los pines D8 a D13.
  • El segundo puerto que vemos es el PC, que corresponde al grupo de pines PCINT8 a PCINT13, correspondientes a los pines A0 a A5.
  • Por último tenemos el puerto PD, que corresponde al grupo de pines PCINT16 a PCINT23, correspondientes a los pines digitales D0 a D7.

Es muy importante saber esto, ya que para activar las interrupciones en un pin tendremos que saber a qué grupo pertenece y activar el grupo. Para ello usaremos el registro PCICR, en el cual indicaremos en qué puerto queremos activar las interrupciones.

No te asustes todavía, que usarlo es muy fácil. Para ello simplemente tendremos que realizar una operación bitwise (si no sabes de que hablo, puedes pasarte por este post para saber más). Vayamos al ejemplo:

PCICR |= B00000001; // Activamos las interrupciones del puerto PB
PCICR |= B00000010; // Activamos las interrupciones del puerto PC
PCICR |= B00000100; // Activamos las interrupciones del puerto PD

Fácil, ¿verdad?, pues además como operación bitwise que es, puedes activar las interrupciones en varios puertos a la vez:

PCICR |= B00000101; // Activamos las interrupciones de los puertos PB y PD

Seguramente te habrás fijado en que el orden de los puertos es inverso al orden de los números y se empieza por la derecha. Esto es debido a que el bit de la derecha es el menos significativo y por lo tanto siempre se empieza por él.

Una vez activado el puerto en el que queremos generar las interrupciones, tendremos que activar también qué pin de ese puerto generará dichas interrupciones, sino sería una locura porque todo cambio en los pines generaría una interrupción. Para ello continuaremos con la parte 2 de esta guía.

Registros de pines que activarán las interrupciones (PCMSK)

Una vez que hemos activado el o los puertos que deseamos que desencadenen interrupciones, es hora de indicar qué pines de esos puertos lo harán. Esto se realiza a través de los registros PCMSK, de los cuales disponemos uno por puerto (PCMSK0, PCMSK1 y PCMSK2). La correspondencia entre puertos y registros es:

  • PCMSK0 -> PB -> Pines D8 a D13
  • PCMSK1 -> PC -> Pines A0 a A5
  • PCMSK2 -> PD -> Pines D0 a D7

Su uso es igual al indicado anteriormente con PCICR, siendo necesario realizar operaciones bitwise para ello. Por supuesto, será igual de fácil que como hemos visto antes:

PCMSK0 |= B00000100; // Activamos las interrupciones en el pin D10
PCMSK1 |= B00001000; // Activamos las interrupciones en el pin A3

Al igual que antes también podremos activar las interrupciones en varios pines en la misma operación bitwise. Para los más despistados, recordemos que los números binario se empiezan a leer por la derecha, por lo que para indicar el número del pin que queremos activar deberá ser de derecha a izquierda. En los ejemplos de arriba podemos ver que sólo la tercera cifra comenzando por la derecha es 1, lo cual significa: D8 -> 0, D9 -> 0, D10 -> 1…

Generar eventos (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

Para poder usarlos simplemente tendremos que usar la rutina ISR, para lo cual yo siempre digo que lo mejor es un ejemplo para entenderlo:

ISR (PCINT0_vect) {
  // código a ejecutar
}

Este código al igual que si de una función se tratase, tiene que ir fuera de setup y main.

Como puedes observar, le indicaremos el vector al que nos suscribiremos para que se ejecute el código cuando se dispare una interrupción en él. Por supuesto, como comentamos anteriormente, no recibiremos de ninguna forma el pin sobre el que se ha hecho la interrupción, por lo que tendremos que detectarlo. Podremos usarlo para por ejemplo subir o bajar un contador:

int counter = 0;

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

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

  PCICR |= B00000100; // Activar interrupciones en puerto PD
  PCMSK2 |= B00110000; // Activar interrupciones en pines D4 y D5
}

void loop() {
  Serial.print("El contador está ahora en: ");
  Serial.println(counter);
  
  delay(100);
}

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

O llevaremos un control sobre el último estado de cada pin para detectar cuál ha cambiado (el causante de la interrupción):

bool pin4 = false;
bool pin5 = false;

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

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

  PCICR |= B00000100; // Activar interrupciones en puerto PD
  PCMSK2 |= B00110000; // Activar interrupciones en pines D4 y D5
}

void loop() {
}

ISR (PCINT2_vect) {
  if (digitalRead(4) != pin4) {
    Serial.print("El pin4 ha cambiado de estado y su nuevo estado es: ");
    Serial.println(!pin4);
    pin4 = !pin4;
  }
  else if (digitalRead(5) != pin4) {
    Serial.print("El pin5 ha cambiado de estado y su nuevo estado es: ");
    Serial.println(!pin5);
    pin5 = !pin5;
  }
}

Así lo podremos utilizar para por ejemplo switches o similares.

Extra

Y para finalizar os dejaré un consejo extra: Las interrupciones como bien su nombre indica, interrumpen el flujo del programa principal. Esto puede afectar al funcionamiento de tu aplicación en casos como por ejemplo que estés controlando un motor a pasos y las interrupciones sean muy largas. Es por ello, que es muy importante que las interrupciones sean lo más breves posibles. Para ello un truquito que podemos usar es evitar las funciones de Arduino y utilizar directamente los registros, como por ejemplo este código equivalente para el contador de arriba:

int counter = 0;

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

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

  PCICR |= B00000100; // Activar interrupciones en puerto PD
  PCMSK2 |= B00110000; // Activar interrupciones en pines D4 y D5
}

void loop() {
  Serial.print("El contador está ahora en: ");
  Serial.println(counter);
  
  delay(100);
}

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

PIND es el registro de estado de los pines del puerto PD, por lo que a través de una operación bitwise podremos detectar si un pin específico está en HIGH o LOW. Esto nos evitará todo el bloque de código que constituye la función digitalRead(), y por lo tanto nuestro programa funcionará más rápido y además como extra, ahorraremos la memoria que ocuparía dicha función.

También durante la ejecución de ciertas partes del código podremos pausar las interrupciones globales, evitando así que las interrupciones interrumpan la ejecución de dicho código. Para ello usaremos las funciones cli y sei:

int counter = 0;

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

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

  PCICR |= B00000100; // Activar interrupciones en puerto PD
  PCMSK2 |= B00110000; // Activar interrupciones en pines D4 y D5
}

void loop() {
  cli(); // Pausamos las interrupciones
  Serial.print("El contador está ahora en: ");
  Serial.println(counter);
  sei(); // Reactivamos las interrupciones
  
  delay(100);
}

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

No es el mejor ejemplo, pero bueno… podéis observar que se pausan las interrupciones antes de enviar datos por el puerto Serial con Serial.print, y luego se reactivan una vez se ha terminado. Es muy importante reactivar las interrupciones de nuevo o sino dejarán de funcionar.

Estas funciones no es necesario que las usemos dentro de las interrupciones, ya que estas pausan automáticamente las demás interrupciones.

Como siempre, espero que os haya gustado y no dudéis en comentar cualquier cosa. ¡Un saludo!

Daniel Carrasco

DevOps con varios años de experiencia, y arquitecto cloud con experiencia en Google Cloud Platform y Amazon Web Services. En sus ratos libres experimenta con Arduino y electrónica.

Deja un comentario

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.