interrupciones-en-todos-los-pines-de-arduino-con-pcint

Interrupciones en todos los pines de Arduino con las PCINT

En esta entrada vamos a ver cómo usar las interrupciones Pin Change en Arduino. Esto, por ejemplo, nos va a permitir tener interrupciones en todos los pines en placas basadas en el Atmega328P.

Pero un momento ¡Sacrilegio, los Arduino tienen sólo 2 pines de interrupciones! Buenos, la historia no es exactamente así.

En esta entrada veremos qué son y cómo funcionan las interrupciones Pin Change (PCINT), unas interrupciones distintas a las interrupciones normales (INT) a las que estamos acostumbrados.

Por supuesto también veremos algún ejemplo de código. Sin embargo, normalmente usaremos una librería para gestionar las PCINT. Esta visión más práctica la veremos al final del artículo, y veréis que es muy sencillo usar las interrupciones Pin Change.

Qué son las PCINT

Los procesadores como los Atmel tienen distintos tipos de interrupciones tanto internas como externas. En nuestro caso estamos interesado en interrupciones externas, es decir, las que disparan al cambiar el estado de uno de los pines.

Tenemos dos tipos de interrupciones externas:

  • INT, interrupciones de hardware externo.
  • PCINT, interrupciones pin change (Pin Change INTerrupt).

Normalmente, cuando se habla de interrupciones nos referimos a las interrupciones externas de tipo INT, que ya vimos en esta entrada. Y es cierto que de estas tenemos un número muy limitado de pines con interrupciones INT.

Mucho menos conocidas son las interrupciones pin change (PCINT), cuyo modo de funcionamiento es similar, pero actúan en número muy superior de pines del procesador.

Lógicamente no todo iba a ser tan bonito y las PCINT también tienen algunas desventajas respecto a las habituales INT. Pero nada que no podamos salvar o impida que las usemos.

En primer lugar, a diferencia de las interrupciones INT que actúan sobre un único pin, las PCINT actúan sobre un grupo de pines de forma simultánea (normalmente sobre un puerto).

Sí tenemos un único pin asociado en cada PCINT podremos deducir sin más que se ha actuado sobre este pin. Pero, en general, tendremos más de un pin y deberemos hacer una consulta posterior a un registro para saber el pin sobre el que ha actuado.

En segundo lugar, a diferencia de las interrupciones INT que permiten configurar el disparo CHANGE, FALLING, RISING, LOW y HIGH, las interrupciones INT únicamente distinguen eventos de CHANGE.

Si queremos detectar flancos de subida o de bajada deberemos guardar el estado del registro en una variable y realizar la comparación con el estado anterior en la ISR.

Finalmente, por los motivos anteriores, son ligeramente más lentas que las interrupciones INT. Pero en general no es algo que nos deba preocupar, es una diferencia irrelevante salvo en casos muy extremos.

Cómo usar las PCINT

Hay varios registros implicados en la activación y uso de las interrupciones pin change. Vamos a ver el proceso paso a paso, empleando de referencia el Atmega 328p por ser el más empleado en Arduino Uno y Nano. Aunque más abajo veremos cómo extrapolarlo a otros procesadores Atmel.

Activar o desactivar las PCINT

En primer lugar, podemos activar o desactivar las PCINT asociadas a un grupo de pines con el registro PCICR (Pin Change Interrupt Control Register).

Aquí tenemos 3 bits, que controlan la activación o desactivación de las PCINT para cada grupo de pines. PCICR

Bit 7Bit 6Bit 5Bit 4Bit 3Bit 2Bit 1Bit 0
PCIE2PCIE1PCIE0

Activar o desactivar para un pin

Una vez activada la PCINT para un grupo de pines, debemos decir que pines del grupo pueden disparar la interrupción. Para eso tenemos los registros PCMSK0, PCMSK1 y PCMSK2 (Pin Change Mask), en los que cada bit indica si el pin dispara o no la PCINT. PCMSK0

Bit 7Bit 6Bit 5Bit 4Bit 3Bit 2Bit 1Bit 0
PCINT7PCINT6PCINT5PCINT4PCINT3PCINT2PCINT1

PCMSK1

Bit 7Bit 6Bit 5Bit 4Bit 3Bit 2Bit 1Bit 0
PCINT14PCINT13PCINT12PCINT11PCINT10PCINT9PCINT8

PCMSK2

Bit 7Bit 6Bit 5Bit 4Bit 3Bit 2Bit 1Bit 0
PCINT23PCINT22PCINT21PCINT20PCINT19PCINT18PCINT17PCINT16

Limpiar el registro de flag

Por otro lado, tenemos el registro PCIFR (Pin Change Interrupt Flag Register). Los bits de este registro se activan cada vez que ocurre un cambio en un pin del grupo. PCIFR

Bit 7Bit 6Bit 5Bit 4Bit 3Bit 2Bit 1Bit 0
PCIF2PCIF1PCIF0

Para reiniciarlo, tenemos que poner un ‘1’ en el registro correspondiente. Los flag se reinician automáticamente cuando se lanza la ISR asociada.

Definir las ISR

Por último, en el código tenemos que asociar las ISR que queramos emplear. Así, en el caso del Atmega 328p tenemos las funciones

  • ISR (PCINT0_vect) para grupo de pines D8 a D13
  • ISR (PCINT1_vect) para grupo de pines A0 a A5
  • ISR (PCINT2_vect) para grupo de pines D0 a D7

Estas ISR están asociadas, respectivamente, con cada uno de los grupos indicados.

Funcionamiento de la PCINT

Ya tenemos todos los componentes para explicar el funcionamiento de las interrupciones pin change. En modo resumen, cuando se dispara un cambio un pin de uno de los grupos se activa el flag correspondiente en PCIFR.

Si este grupo está activado en el PCICR, el pin que originado el disparo está activado en su PCMSKx, y el grupo tiene su ISR oportuna definida en el código, se dispara la ISR.

Tras la ejecución de la ISR se limpia el registro de flags PCIFR, dejando el sistema listo para recibir otro evento de pin change.

PCINT ejemplo de código

Vamos a poner todo lo anterior junto en un código con ejemplo sencillo sobre el uso de las interrupciones pin change. De momento vamos a seguir usando el Atmega389p como referencia.

El siguiente ejemplo muestra cómo activar las tres ISR disponibles para los tres grupos y cómo asociarlas a ciertos pines de cada grupo.

// Activar PCINT en un PIN
void pciSetup(byte pin)
{
    *digitalPinToPCMSK(pin) |= bit (digitalPinToPCMSKbit(pin));  // activar pin en PCMSK
    PCIFR  |= bit (digitalPinToPCICRbit(pin)); // limpiar flag de la interrupcion en PCIFR
    PCICR  |= bit (digitalPinToPCICRbit(pin)); // activar interrupcion para el grupo en PCICR
}

// Definir ISR para cada puerto
ISR (PCINT0_vect) 
{    
    // gestionar para PCINT para D8 a D13
}

ISR (PCINT1_vect) 
{
    // gestionar PCINT para A0 a A5
}  

ISR (PCINT2_vect) 
{
    // gestionar PCINT para D0 a D7
}  

void setup() 
{  
  // Activar las PCINT para distintos pins
  pciSetup(7);
  pciSetup(8);
  pciSetup(9);
  pciSetup(A0);
}

void loop() 
{
}

En este ejemplo solo hemos activado las tres ISR, pero no distinguimos en que pin ha disparado, ni el tipo de evento. Tenéis un ejemplo completo con la gestión en este enlace.

El código resultante es, digamos, poco intuitivo. Afortunadamente, la comunidad ha desarrollado varias librerías que nos evitan el trabajo de tener que manejar este código por nosotros mismos. Las veremos al final de la entrada.

PCINT en otros procesadores

En los ejemplos hemos empleado el procesador Atmega 328p pero ¿qué pasa en los otros modelos de Atmega? Bueno, en general es muy parecido pero cada uno tiene su propia definición de pines.

A continuación, tenéis unas tablas con las INT y PCINT de algunos de los procesadores Atmega más frecuentes.

Atmega 128/328p (Arduino Uno y Nano)

PinPortINTArduino Pin
2PD2INT02
3PD3INT13
PinPortPCINTPinPortPCINTPinPortPCINT
2PD2PCINT188PB0PCINT0A0PC0PCINT8
3PD3PCINT199PB1PCINT1A1PC1PCINT9
4PD4PCINT2010PB2PCINT2A2PC2PCINT10
5PD5PCINT2111PB3PCINT3A3PC3PCINT11
6PD6PCINT2212PB4PCINT4A4PC4PCINT12
7PD7PCINT2313PB5PCINT5A5PC5PCINT13

Atmega 32u4 (Arduino Leonardo y Micro)

PinPortINT
0PD2INT2
1PD3INT3
2PD1INT1
3PD0INT0
7PE6INT6
PinPortPCINT
SCK/15PB1PCINT1
MOSI/16PB2PCINT2
MISO/14PB3PCINT3
8/A8PB4PCINT4
9/A9PB5PCINT5
10/A10PB6PCINT6
11PB7PCINT7

Atmega2560 (Arduino Mega)

PinPortINTArduino Pin
2PE4INT46
3PE5INT57
21PD0INT043
20PD1INT144
19PD2INT245
18PD3INT346
n/cPE6INT68 (fake 75)
n/cPE7INT79 (fake 76)
PinPortPCINTPinPortPCINTPinPortPCINT
10PB4PCINT4SSPCINT0PB0A8PK0PCINT16
11PB5PCINT5SCKPCINT1PB1A9PK1PCINT17
12PB6PCINT6MOSIPCINT2PB2A10PK2PCINT18
13PB7PCINT7MISOPCINT3PB3A11PK3PCINT19
14PJ1PCINT10A12PK4PCINT20
15PJ0PCINT9A13PK5PCINT21
A14PK6PCINT22
A15PK7PCINT23

Podemos adaptar nuestro código para los distintos procesadores o, mucho mejor, usar una librería que se encargue de ello y nos evite los quebraderos de cabeza como veremos a continuación.

Para más información consultar el Datasheet del procesador

PCINT en una librería

Como decíamos, hay muchas librerías para gestionar las interrupciones pin change disponibles en el gestor de librarías. Algunos ejemplos son Sodaq_PcInt, PinChangeInterrupt, EnableInterrupt, PciManager.

Personalmente, a mí me gusta la librería YetAnotherArduinoPcIntLibrary, porque es fácil de usar, el código es pequeño y eficiente, y está bien escrita. Además, distingue entre modos RISING/FALLING/CHANGE y permite pasar variables a las funciones de callback de las ISR.

La verdad es que es una maravilla de librería y hace que usar las interrupciones pin change sea tan cómodo como una INT normal. Y aquí tenemos un ejemplo de cómo usar la librería.


#define PCINT_PIN A5

#include <YetAnotherPcInt.h>

void pinChanged(const char* message, bool pinstate) {
  Serial.print(message);
  Serial.println(pinstate ? "HIGH" : "LOW");
}

void setup() {
  Serial.begin(115200);
  pinMode(PCINT_PIN, INPUT_PULLUP);
  PcInt::attachInterrupt(PCINT_PIN, pinChanged, "Pin has changed to ", CHANGE);
}

void loop() {}

¡Más cómodo no puede ser! Así de fácil podemos usar las interrupciones pin change en nuestros proyectos lo que permite, en el caso del Atmega 328p, disponer de interrupciones en todos los pines.

Descarga el código

Todo el código de esta entrada está disponible para su descarga en Github. github-full