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 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 |
---|---|---|---|---|---|---|---|
– | – | – | – | – | PCIE2 | PCIE1 | PCIE0 |
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 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 |
---|---|---|---|---|---|---|---|
– | PCINT7 | PCINT6 | PCINT5 | PCINT4 | PCINT3 | PCINT2 | PCINT1 |
PCMSK1
Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 |
---|---|---|---|---|---|---|---|
– | PCINT14 | PCINT13 | PCINT12 | PCINT11 | PCINT10 | PCINT9 | PCINT8 |
PCMSK2
Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 |
---|---|---|---|---|---|---|---|
PCINT23 | PCINT22 | PCINT21 | PCINT20 | PCINT19 | PCINT18 | PCINT17 | PCINT16 |
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 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 |
---|---|---|---|---|---|---|---|
– | – | – | – | – | PCIF2 | PCIF1 | PCIF0 |
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)
Pin | Port | INT | Arduino Pin |
---|---|---|---|
2 | PD2 | INT0 | 2 |
3 | PD3 | INT1 | 3 |
Pin | Port | PCINT | Pin | Port | PCINT | Pin | Port | PCINT |
---|---|---|---|---|---|---|---|---|
2 | PD2 | PCINT18 | 8 | PB0 | PCINT0 | A0 | PC0 | PCINT8 |
3 | PD3 | PCINT19 | 9 | PB1 | PCINT1 | A1 | PC1 | PCINT9 |
4 | PD4 | PCINT20 | 10 | PB2 | PCINT2 | A2 | PC2 | PCINT10 |
5 | PD5 | PCINT21 | 11 | PB3 | PCINT3 | A3 | PC3 | PCINT11 |
6 | PD6 | PCINT22 | 12 | PB4 | PCINT4 | A4 | PC4 | PCINT12 |
7 | PD7 | PCINT23 | 13 | PB5 | PCINT5 | A5 | PC5 | PCINT13 |
Atmega 32u4 (Arduino Leonardo y Micro)
Pin | Port | INT |
---|---|---|
0 | PD2 | INT2 |
1 | PD3 | INT3 |
2 | PD1 | INT1 |
3 | PD0 | INT0 |
7 | PE6 | INT6 |
Pin | Port | PCINT |
---|---|---|
SCK/15 | PB1 | PCINT1 |
MOSI/16 | PB2 | PCINT2 |
MISO/14 | PB3 | PCINT3 |
8/A8 | PB4 | PCINT4 |
9/A9 | PB5 | PCINT5 |
10/A10 | PB6 | PCINT6 |
11 | PB7 | PCINT7 |
Atmega2560 (Arduino Mega)
Pin | Port | INT | Arduino Pin |
---|---|---|---|
2 | PE4 | INT4 | 6 |
3 | PE5 | INT5 | 7 |
21 | PD0 | INT0 | 43 |
20 | PD1 | INT1 | 44 |
19 | PD2 | INT2 | 45 |
18 | PD3 | INT3 | 46 |
n/c | PE6 | INT6 | 8 (fake 75) |
n/c | PE7 | INT7 | 9 (fake 76) |
Pin | Port | PCINT | Pin | Port | PCINT | Pin | Port | PCINT |
---|---|---|---|---|---|---|---|---|
10 | PB4 | PCINT4 | SS | PCINT0 | PB0 | A8 | PK0 | PCINT16 |
11 | PB5 | PCINT5 | SCK | PCINT1 | PB1 | A9 | PK1 | PCINT17 |
12 | PB6 | PCINT6 | MOSI | PCINT2 | PB2 | A10 | PK2 | PCINT18 |
13 | PB7 | PCINT7 | MISO | PCINT3 | PB3 | A11 | PK3 | PCINT19 |
14 | PJ1 | PCINT10 | A12 | PK4 | PCINT20 | |||
15 | PJ0 | PCINT9 | A13 | PK5 | PCINT21 | |||
A14 | PK6 | PCINT22 | ||||||
A15 | PK7 | PCINT23 |
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.