
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.
Anuncio:
Qué son las PCINT
Los procesadores como los Atmel Atmega 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).
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.
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.
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.
tutoriales de Arduino
Descarga el código
Todo el código de esta entrada está disponible para su descarga en Github.
Anuncio:
Hola! Estupendo el blog. Muchas gracias!!
Interpreto que este tema de las PCINT para micros SAMD (ARM) no es procedente, ¿verdad?
Abría que mirar a qué modelo concreto te refieres. Pero, en general, los procesadores ARM son más avanzados y tienen interrupciones en todos los pines. Por tanto, no tienen PCINT (no tendrian sentido)
Muy interesante, Luis. Y explicado muy claro. Gracias.
Muy interesante tu articulo, me ha resuelto un dilema que tenia. Pero me surge una duda. Yo no soy programador y a veces me cuesta seguir algún código. En este caso, cuando ejecuto el ejemplo tal cual, me funciona con los pines analógicos. Pero si quisiera utilizar los digitales, que cambio debería realizar?, ya que he utilizado #define PCINT_PIN D5 o B5 y me indica “D5 was not declared in this scope”. Entiendo que necesito declarar #define PCINT_INPUT_PORT1 al principio del skectch, pero aun así, no he sido capaz de hacerlos funcionar.
Entiendo que te refieres al caso de usar la librería. En ese caso, simplemente no pongas la ‘D’ ni la ‘B’. En el entorno de Arduino, con las placas estándard, los pines digitales se indican simplemente con el número, no llevan letra delante.
A veces, lo evidente para unos no lo es tanto para otros, jejeje. Acostumbrado a la nomenclatura de Microchip, me obceque con las letras. Ahora todo perfecto. Muchas gracias por tu ayuda.
Un placer!
Hola, tengo puesto en un arduino nano dos encoder rotativos, de los cuales solo me funciona uno, el que está conectado en el pin 2 y 4, el que tengo en el pin 7 y 8 no, supongo que es porque no lo tengo conectado a una interrupción. Si es así, se puede poner una interrupción al pin 7 solo o tengo que ponerlos todos?
Un saludo y gracias.
Hola, de acuerdo a lo que acabo de leer parece que fuera la solución a mis problemas, lo que quiero hacer es esto. Utilizo un servo motor de 360° el cual debe girar girar girar hasta cuando un sensor óptico conectado a un pin me de como entrada 0V (camiba de 1L a 0L), luego de que el servo se detenga y haga un par de acciones más debería volver a girar cosa que no ocurre porque ya se detuvo en ese 0L, ¿si utilizo al pin donde está conectado el sensor como interrupción interna podré trabajar con esa bandera… Read more »