¿Cómo funcionan las interrupciones en el Arduino Uno y tableros similares?


11

Explique cómo funcionan las interrupciones en el Arduino Uno y las placas relacionadas que utilizan el procesador ATmega328P. Tableros como:

  • Uno
  • Mini
  • Nano
  • Pro Mini
  • Lilypad

En particular, discuta:

  • Para qué usar las interrupciones
  • Cómo escribir una rutina de servicio de interrupción (ISR)
  • Problemas de tiempo
  • Secciones críticas
  • Acceso atómico a los datos.

Nota: esta es una pregunta de referencia .

Respuestas:


25

TL; DR:

Al escribir una Rutina de servicio de interrupción (ISR):

  • Mantenlo corto
  • No usar delay ()
  • No hagas impresiones en serie
  • Hacer que las variables compartidas con el código principal sean volátiles
  • Es posible que las variables compartidas con el código principal deban protegerse mediante "secciones críticas" (ver más abajo)
  • No intentes activar o desactivar las interrupciones

¿Qué son las interrupciones?

La mayoría de los procesadores tienen interrupciones. Las interrupciones le permiten responder a eventos "externos" mientras hace otra cosa. Por ejemplo, si está cocinando la cena, puede poner las papas para cocinar durante 20 minutos. En lugar de mirar el reloj durante 20 minutos, puede configurar un temporizador y luego mirar la televisión. Cuando suena el temporizador, "interrumpe" la televisión para hacer algo con las papas.


Ejemplo de interrupciones

const byte LED = 13;
const byte SWITCH = 2;

// Interrupt Service Routine (ISR)
void switchPressed ()
{
  if (digitalRead (SWITCH) == HIGH)
    digitalWrite (LED, HIGH);
  else
    digitalWrite (LED, LOW);
}  // end of switchPressed

void setup ()
{
  pinMode (LED, OUTPUT);  // so we can update the LED
  pinMode (SWITCH, INPUT_PULLUP);
  attachInterrupt (digitalPinToInterrupt (SWITCH), switchPressed, CHANGE);  // attach interrupt handler
}  // end of setup

void loop ()
{
  // loop doing nothing
}

Este ejemplo muestra cómo, aunque el bucle principal no está haciendo nada, puede encender o apagar el LED en el pin 13, si se presiona el interruptor en el pin D2.

Para probar esto, simplemente conecte un cable (o interruptor) entre D2 y Tierra. El pullup interno (habilitado en la configuración) fuerza al pin HIGH normalmente. Cuando está conectado a tierra, se vuelve BAJO. El cambio en el pin es detectado por una interrupción CHANGE, que provoca la llamada a la Rutina de servicio de interrupción (ISR).

En un ejemplo más complicado, el bucle principal podría estar haciendo algo útil, como tomar lecturas de temperatura y permitir que el controlador de interrupciones detecte que se presiona un botón.


Convertir números de pin para interrumpir números

Para simplificar la conversión de números de vector de interrupción a números pin, puede llamar a la función digitalPinToInterrupt(), pasando un número pin. Devuelve el número de interrupción apropiado, o NOT_AN_INTERRUPT(-1).

Por ejemplo, en el Uno, el pin D2 en el tablero es la interrupción 0 (INT0_vect de la tabla a continuación).

Por lo tanto, estas dos líneas tienen el mismo efecto:

  attachInterrupt (0, switchPressed, CHANGE);    // that is, for pin D2
  attachInterrupt (digitalPinToInterrupt (2), switchPressed, CHANGE);

Sin embargo, el segundo es más fácil de leer y más portátil para diferentes tipos de Arduino.


Interrupciones disponibles

A continuación se muestra una lista de interrupciones, en orden de prioridad, para el Atmega328:

 1  Reset
 2  External Interrupt Request 0  (pin D2)          (INT0_vect)
 3  External Interrupt Request 1  (pin D3)          (INT1_vect)
 4  Pin Change Interrupt Request 0 (pins D8 to D13) (PCINT0_vect)
 5  Pin Change Interrupt Request 1 (pins A0 to A5)  (PCINT1_vect)
 6  Pin Change Interrupt Request 2 (pins D0 to D7)  (PCINT2_vect)
 7  Watchdog Time-out Interrupt                     (WDT_vect)
 8  Timer/Counter2 Compare Match A                  (TIMER2_COMPA_vect)
 9  Timer/Counter2 Compare Match B                  (TIMER2_COMPB_vect)
10  Timer/Counter2 Overflow                         (TIMER2_OVF_vect)
11  Timer/Counter1 Capture Event                    (TIMER1_CAPT_vect)
12  Timer/Counter1 Compare Match A                  (TIMER1_COMPA_vect)
13  Timer/Counter1 Compare Match B                  (TIMER1_COMPB_vect)
14  Timer/Counter1 Overflow                         (TIMER1_OVF_vect)
15  Timer/Counter0 Compare Match A                  (TIMER0_COMPA_vect)
16  Timer/Counter0 Compare Match B                  (TIMER0_COMPB_vect)
17  Timer/Counter0 Overflow                         (TIMER0_OVF_vect)
18  SPI Serial Transfer Complete                    (SPI_STC_vect)
19  USART Rx Complete                               (USART_RX_vect)
20  USART, Data Register Empty                      (USART_UDRE_vect)
21  USART, Tx Complete                              (USART_TX_vect)
22  ADC Conversion Complete                         (ADC_vect)
23  EEPROM Ready                                    (EE_READY_vect)
24  Analog Comparator                               (ANALOG_COMP_vect)
25  2-wire Serial Interface  (I2C)                  (TWI_vect)
26  Store Program Memory Ready                      (SPM_READY_vect)

Los nombres internos (que puede usar para configurar las devoluciones de llamada ISR) están entre paréntesis.

Advertencia: si escribe mal el nombre del vector de interrupción, incluso con solo escribir mal las mayúsculas (algo fácil de hacer) , no se llamará a la rutina de interrupción y no obtendrá un error del compilador.


Razones para usar interrupciones

Las principales razones por las que podría usar interrupciones son:

  • Para detectar cambios en los pines (p. Ej. Codificadores rotativos, presionar botones)
  • Temporizador de vigilancia (por ejemplo, si no pasa nada después de 8 segundos, interrumpe)
  • Interrupciones de temporizador: se utilizan para comparar / desbordar temporizadores
  • Transferencias de datos SPI
  • Transferencias de datos I2C
  • Transferencias de datos USART
  • Conversiones de ADC (analógico a digital)
  • EEPROM lista para usar
  • Memoria flash lista

Las "transferencias de datos" se pueden utilizar para permitir que un programa haga otra cosa mientras se envían o reciben datos en el puerto serie, el puerto SPI o el puerto I2C.

Despierta el procesador

Las interrupciones externas, las interrupciones de cambio de pin y la interrupción del temporizador de vigilancia también se pueden usar para activar el procesador. Esto puede ser muy útil, ya que en el modo de suspensión el procesador puede configurarse para usar mucha menos energía (por ejemplo, alrededor de 10 microamperios). Se puede usar una interrupción ascendente, descendente o de bajo nivel para activar un dispositivo (por ejemplo, si presiona un botón), o una interrupción del "temporizador de vigilancia" puede activarlo periódicamente (por ejemplo, para verificar la hora o temperatura).

Las interrupciones de cambio de pines podrían usarse para activar el procesador si se presiona una tecla en un teclado o similar.

El procesador también puede ser despertado por una interrupción del temporizador (por ejemplo, un temporizador que alcanza un cierto valor o se desborda) y ciertos otros eventos, como un mensaje I2C entrante.


Habilitar / deshabilitar interrupciones

La interrupción "restablecer" no se puede deshabilitar. Sin embargo, las otras interrupciones pueden deshabilitarse temporalmente al borrar el indicador de interrupción global.

Habilitar interrupciones

Puede habilitar las interrupciones con la función llamada "interrupciones" o "sei" de esta manera:

interrupts ();  // or ...
sei ();         // set interrupts flag

Deshabilitar interrupciones

Si necesita deshabilitar las interrupciones, puede "borrar" el indicador de interrupción global de esta manera:

noInterrupts ();  // or ...
cli ();           // clear interrupts flag

Cualquiera de los métodos tiene el mismo efecto, usar interrupts/ noInterruptses un poco más fácil de recordar en qué dirección se encuentran.

El valor predeterminado en Arduino es que se habiliten las interrupciones. No los desactive durante largos períodos o cosas como los temporizadores no funcionarán correctamente.

¿Por qué deshabilitar interrupciones?

Puede haber piezas de código de tiempo crítico que no desea interrumpir, por ejemplo, por una interrupción del temporizador.

Además, si un ISR actualiza los campos de varios bytes, es posible que deba desactivar las interrupciones para obtener los datos "atómicamente". De lo contrario, el ISR puede actualizar un byte mientras está leyendo el otro.

Por ejemplo:

noInterrupts ();
long myCounter = isrCounter;  // get value set by ISR
interrupts ();

Desactivar temporalmente las interrupciones asegura que isrCounter (un contador establecido dentro de un ISR) no cambia mientras estamos obteniendo su valor.

Advertencia: si no está seguro de si las interrupciones ya están activadas o no, debe guardar el estado actual y restaurarlo después. Por ejemplo, el código de la función millis () hace esto:

unsigned long millis()
{
  unsigned long m;
  uint8_t oldSREG = SREG;    // <--------- save status register

  // disable interrupts while we read timer0_millis or we might get an
  // inconsistent value (e.g. in the middle of a write to timer0_millis)
  cli();
  m = timer0_millis;
  SREG = oldSREG;            // <---------- restore status register including interrupt flag

  return m;
}

Tenga en cuenta que las líneas indicadas guardan el SREG actual (registro de estado) que incluye el indicador de interrupción. Después de haber obtenido el valor del temporizador (que tiene 4 bytes de longitud) volvemos a poner el registro de estado como estaba.


Consejos

Nombres de funciones

Las funciones cli/ seiy el registro SREG son específicos de los procesadores AVR. Si está utilizando otros procesadores, como los ARM, las funciones pueden ser ligeramente diferentes.

Deshabilitar globalmente vs deshabilitar una interrupción

Si lo usa cli(), deshabilita todas las interrupciones (incluidas las interrupciones del temporizador, las interrupciones en serie, etc.).

Sin embargo, si solo desea deshabilitar una interrupción en particular , debe borrar el indicador de habilitación de interrupción para esa fuente de interrupción en particular. Por ejemplo, para interrupciones externas, llame detachInterrupt().


¿Qué es la prioridad de interrupción?

Como hay 25 interrupciones (que no sean reiniciar), es posible que ocurra más de un evento de interrupción a la vez, o al menos, antes de que se procese el anterior. También puede ocurrir un evento de interrupción mientras las interrupciones están deshabilitadas.

El orden de prioridad es la secuencia en la que el procesador busca eventos de interrupción. Cuanto mayor sea la lista, mayor será la prioridad. Entonces, por ejemplo, una solicitud de interrupción externa 0 (pin D2) sería atendida antes de la solicitud de interrupción externa 1 (pin D3).


¿Pueden ocurrir interrupciones mientras las interrupciones están deshabilitadas?

Los eventos de interrupción (es decir, notar el evento) pueden ocurrir en cualquier momento, y la mayoría se recuerdan al establecer un indicador de "evento de interrupción" dentro del procesador. Si las interrupciones están deshabilitadas, esa interrupción se manejará cuando se habiliten nuevamente, en orden de prioridad.


¿Cómo usas las interrupciones?

  • Escribe un ISR (rutina de interrupción del servicio). Esto se llama cuando ocurre la interrupción.
  • Le dice al procesador cuándo quiere que se dispare la interrupción.

Escribiendo un ISR

Las Rutinas de servicio de interrupción son funciones sin argumentos. Algunas bibliotecas Arduino están diseñadas para llamar a sus propias funciones, por lo que simplemente proporciona una función ordinaria (como en los ejemplos anteriores), por ejemplo.

// Interrupt Service Routine (ISR)
void switchPressed ()
{
 flag = true;
}  // end of switchPressed

Sin embargo, si una biblioteca aún no ha proporcionado un "enlace" a un ISR, puede hacer el suyo, de esta manera:

volatile char buf [100];
volatile byte pos;

// SPI interrupt routine
ISR (SPI_STC_vect)
{
byte c = SPDR;  // grab byte from SPI Data Register

  // add to buffer if room
  if (pos < sizeof buf)
    {
    buf [pos++] = c;
    }  // end of room available
}  // end of interrupt routine SPI_STC_vect

En este caso, utiliza la macro "ISR" y proporciona el nombre del vector de interrupción relevante (de la tabla anterior). En este caso, el ISR está manejando una transferencia SPI completada. (Tenga en cuenta que algunos códigos antiguos usan SIGNAL en lugar de ISR, sin embargo, SIGNAL está en desuso).

Conectar un ISR a una interrupción

Para las interrupciones que ya manejan las bibliotecas, solo use la interfaz documentada. Por ejemplo:

void receiveEvent (int howMany)
 {
  while (Wire.available () > 0)
    {
    char c = Wire.receive ();
    // do something with the incoming byte
    }
}  // end of receiveEvent

void setup ()
  {
  Wire.onReceive(receiveEvent);
  }

En este caso, la biblioteca I2C está diseñada para manejar internamente los bytes I2C entrantes y luego llamar a su función suministrada al final del flujo de datos entrantes. En este caso, recibirEvento no es estrictamente un ISR (tiene un argumento) pero es invocado por un ISR incorporado.

Otro ejemplo es la interrupción del "pin externo".

// Interrupt Service Routine (ISR)
void switchPressed ()
{
  // handle pin change here
}  // end of switchPressed

void setup ()
{
  attachInterrupt (digitalPinToInterrupt (2), switchPressed, CHANGE);  // attach interrupt handler for D2
}  // end of setup

En este caso, la función attachInterrupt agrega la función switchPressed a una tabla interna y, además, configura los indicadores de interrupción apropiados en el procesador.

Configurar el procesador para manejar una interrupción

El siguiente paso, una vez que tenga un ISR, es decirle al procesador que desea que esta condición particular provoque una interrupción.

Como ejemplo, para la Interrupción externa 0 (la interrupción D2) podría hacer algo como esto:

EICRA &= ~3;  // clear existing flags
EICRA |= 2;   // set wanted flags (falling level interrupt)
EIMSK |= 1;   // enable it

Más legible sería usar los nombres definidos, como este:

EICRA &= ~(bit(ISC00) | bit (ISC01));  // clear existing flags
EICRA |= bit (ISC01);    // set wanted flags (falling level interrupt)
EIMSK |= bit (INT0);     // enable it

El EICRA (Registro de control de interrupción externo A) se establecería de acuerdo con esta tabla de la hoja de datos de Atmega328. Eso define el tipo exacto de interrupción que desea:

  • 0: el nivel bajo de INT0 genera una solicitud de interrupción (interrupción BAJA).
  • 1: Cualquier cambio lógico en INT0 genera una solicitud de interrupción (CHANGE interrupt).
  • 2: El flanco descendente de INT0 genera una solicitud de interrupción (interrupción FALLING).
  • 3: El flanco ascendente de INT0 genera una solicitud de interrupción (interrupción RISING).

EIMSK (Registro de máscara de interrupción externa) en realidad habilita la interrupción.

Afortunadamente no necesita recordar esos números porque attachInterrupt lo hace por usted. Sin embargo, eso es lo que realmente está sucediendo, y para otras interrupciones puede que tenga que establecer "manualmente" los indicadores de interrupción.


ISR de bajo nivel frente a ISR de biblioteca

Para simplificar su vida, algunos manejadores de interrupciones comunes están dentro del código de la biblioteca (por ejemplo, INT0_vect e INT1_vect) y luego se proporciona una interfaz más fácil de usar (por ejemplo, attachInterrupt). Lo que adjunta realmente Interrumpir es guardar la dirección de su controlador de interrupción deseado en una variable, y luego llamarla desde INT0_vect / INT1_vect cuando sea necesario. También establece los indicadores de registro apropiados para llamar al controlador cuando sea necesario.


¿Se pueden interrumpir los ISR?

En resumen, no, a menos que quieras que lo sean.

Cuando se ingresa un ISR, las interrupciones se desactivan . Naturalmente, deben haberse habilitado en primer lugar, de lo contrario no se ingresaría el ISR. Sin embargo, para evitar que se interrumpa un ISR, el procesador desactiva las interrupciones.

Cuando sale un ISR, las interrupciones se habilitan nuevamente . El compilador también genera código dentro de un ISR para guardar registros e indicadores de estado, para que no se vea afectado lo que estaba haciendo cuando ocurrió la interrupción.

Sin embargo, puede activar las interrupciones dentro de un ISR si es absolutamente necesario, por ejemplo.

// Interrupt Service Routine (ISR)
void switchPressed ()
{
  // handle pin change here
  interrupts ();  // allow more interrupts

}  // end of switchPressed

Normalmente, necesitaría una buena razón para hacerlo, ya que otra interrupción ahora podría resultar en una llamada recursiva a pinChange, con resultados posiblemente no deseados.


¿Cuánto tiempo lleva ejecutar un ISR?

De acuerdo con la hoja de datos, el tiempo mínimo para dar servicio a una interrupción es de 4 ciclos de reloj (para empujar el contador del programa actual a la pila) seguido por el código que ahora se ejecuta en la ubicación del vector de interrupción. Esto normalmente contiene un salto a donde está realmente la rutina de interrupción, que son otros 3 ciclos. El examen del código producido por el compilador muestra que un ISR hecho con la declaración "ISR" puede tardar alrededor de 2.625 µs en ejecutarse, más lo que haga el propio código. La cantidad exacta depende de cuántos registros deben guardarse y restaurarse. La cantidad mínima sería 1.1875 µs.

Las interrupciones externas (donde usa attachInterrupt) hacen un poco más y toman alrededor de 5.125 µs en total (funcionando con un reloj de 16 MHz).


¿Cuánto tiempo antes de que el procesador comience a ingresar a un ISR?

Esto varía un poco. Las cifras citadas arriba son las ideales donde la interrupción se procesa inmediatamente. Algunos factores pueden retrasar eso:

  • Si el procesador está dormido, hay tiempos designados de "activación", que podrían ser unos pocos milisegundos, mientras que el reloj se pone en marcha nuevamente. Esta vez dependerá de la configuración de los fusibles y de la profundidad del sueño.

  • Si ya se está ejecutando una rutina de servicio de interrupción, no se pueden ingresar más interrupciones hasta que finalice o habilite las interrupciones. Es por eso que debe mantener corta cada rutina de servicio de interrupción, ya que cada microsegundo que gasta en una, puede retrasar la ejecución de otra.

  • Algunos códigos desactivan las interrupciones. Por ejemplo, llamar a millis () desactiva brevemente las interrupciones. Por lo tanto, el tiempo para atender una interrupción se prolongaría por el tiempo que las interrupciones estuvieron apagadas.

  • Las interrupciones solo pueden ser atendidas al final de una instrucción, por lo que si una instrucción en particular toma tres ciclos de reloj y acaba de comenzar, la interrupción se retrasará al menos un par de ciclos de reloj.

  • Un evento que vuelve a activar las interrupciones (por ejemplo, al regresar de una rutina de servicio de interrupción) garantiza la ejecución de al menos una instrucción más. Por lo tanto, incluso si un ISR finaliza y su interrupción está pendiente, aún tiene que esperar una instrucción más antes de que se le dé servicio.

  • Dado que las interrupciones tienen una prioridad, una interrupción de mayor prioridad puede ser atendida antes de la interrupción que le interesa.


Consideraciones de rendimiento

Las interrupciones pueden aumentar el rendimiento en muchas situaciones porque puede continuar con el "trabajo principal" de su programa sin tener que estar constantemente probando para ver si se han presionado los interruptores. Dicho esto, la sobrecarga del servicio de una interrupción, como se discutió anteriormente, en realidad sería más que hacer un "circuito cerrado" sondeando un solo puerto de entrada. Apenas puede responder a un evento dentro de, digamos, un microsegundo. En ese caso, puede deshabilitar las interrupciones (por ejemplo, temporizadores) y simplemente hacer un bucle en busca del pin para cambiar.


¿Cómo se ponen en cola las interrupciones?

Hay dos tipos de interrupciones:

  • Algunos establecen una bandera y se manejan en orden de prioridad, incluso si el evento que los causó se ha detenido. Por ejemplo, una interrupción de nivel ascendente, descendente o cambiante en el pin D2.

  • Otros solo se prueban si están sucediendo "en este momento". Por ejemplo, una interrupción de bajo nivel en el pin D2.

Los que establecen un indicador pueden considerarse como en cola, ya que el indicador de interrupción permanece establecido hasta el momento en que se ingresa la rutina de interrupción, momento en el cual el procesador borra el indicador. Por supuesto, dado que solo hay un indicador, si la misma condición de interrupción ocurre nuevamente antes de que se procese el primero, no se le dará servicio dos veces.

Algo a tener en cuenta es que estos indicadores se pueden configurar antes de adjuntar el controlador de interrupciones. Por ejemplo, es posible que se "marque" una interrupción de nivel ascendente o descendente en el pin D2, y luego, tan pronto como realice una conexión, la interrupción se dispara inmediatamente, incluso si el evento ocurrió hace una hora. Para evitar esto, puede borrar manualmente la bandera. Por ejemplo:

EIFR = bit (INTF0);  // clear flag for interrupt 0
EIFR = bit (INTF1);  // clear flag for interrupt 1

Sin embargo, las interrupciones de "nivel bajo" se verifican continuamente, por lo que si no tiene cuidado, seguirán disparando, incluso después de que se haya llamado la interrupción. Es decir, el ISR saldrá, y luego la interrupción se disparará inmediatamente nuevamente. Para evitar esto, debe hacer una separación Separar inmediatamente después de saber que la interrupción se disparó.


Consejos para escribir ISR

En resumen, ¡mantenlos cortos! Mientras un ISR está ejecutando, otras interrupciones no pueden procesarse. Por lo tanto, podría perder fácilmente la presión de los botones o las comunicaciones seriales entrantes si intenta hacer demasiado. En particular, no debe intentar depurar "impresiones" dentro de un ISR. Es probable que el tiempo necesario para hacerlos cause más problemas de los que resuelven.

Una cosa razonable es establecer un indicador de un solo byte y luego probar ese indicador en la función del bucle principal. O almacene un byte entrante desde un puerto serie en un búfer. Las interrupciones del temporizador incorporado hacen un seguimiento del tiempo transcurrido disparando cada vez que el temporizador interno se desborda y, por lo tanto, puede calcular el tiempo transcurrido al saber cuántas veces se desbordó el temporizador.

Recuerde, dentro de un ISR las interrupciones están deshabilitadas. Por lo tanto, esperar que el tiempo devuelto por las llamadas a la función millis () cambie, conducirá a la decepción. Es válido obtener el tiempo de esa manera, solo tenga en cuenta que el temporizador no se está incrementando. Y si pasa demasiado tiempo en el ISR, el temporizador puede perder un evento de desbordamiento, lo que hace que el tiempo devuelto por millis () se vuelva incorrecto.

Una prueba muestra que, en un procesador Atmega328 de 16 MHz, una llamada a micros () toma 3.5625 µs. Una llamada a millis () toma 1.9375 µs. Grabar (guardar) el valor actual del temporizador es algo razonable en un ISR. Encontrar los milisegundos transcurridos es más rápido que los microsegundos transcurridos (el recuento de milisegundos se recupera de una variable). Sin embargo, el recuento de microsegundos se obtiene agregando el valor actual del temporizador del temporizador 0 (que seguirá incrementándose) a un "conteo de desbordamiento del temporizador 0" guardado.

Advertencia: dado que las interrupciones están deshabilitadas dentro de un ISR, y dado que la última versión del IDE de Arduino usa interrupciones para la lectura y escritura en serie, y también para incrementar el contador utilizado por "millis" y "delay", no debe intentar usar esas funciones dentro de un ISR. Para decirlo de otra manera:

  • No intente retrasar, por ejemplo: delay (100);
  • Puede obtener el tiempo de una llamada a millis, sin embargo, no se incrementará, así que no intente demorar esperando que aumente.
  • No haga impresiones en serie (p. Ej. Serial.println ("ISR entered");)
  • No intentes hacer una lectura en serie.

El cambio de pin interrumpe

Hay dos formas de detectar eventos externos en los pines. El primero son los pines especiales de "interrupción externa", D2 y D3. Estos eventos generales de interrupción discreta, uno por pin. Puede llegar a ellos utilizando attachInterrupt para cada pin. Puede especificar una condición ascendente, descendente, cambiante o de bajo nivel para la interrupción.

Sin embargo, también hay interrupciones de "cambio de pin" para todos los pines (en el Atmega328, no necesariamente todos los pines en otros procesadores). Estos actúan sobre grupos de pines (D0 a D7, D8 a D13 y A0 a A5). También tienen menor prioridad que las interrupciones de eventos externos. Sin embargo, son un poco más complicados de usar que las interrupciones externas porque están agrupadas en lotes. Por lo tanto, si se dispara la interrupción, debe calcular en su propio código exactamente qué pin causó la interrupción.

Código de ejemplo:

ISR (PCINT0_vect)
 {
 // handle pin change interrupt for D8 to D13 here
 }  // end of PCINT0_vect

ISR (PCINT1_vect)
 {
 // handle pin change interrupt for A0 to A5 here
 }  // end of PCINT1_vect

ISR (PCINT2_vect)
 {
 // handle pin change interrupt for D0 to D7 here
 }  // end of PCINT2_vect


void setup ()
  {
  // pin change interrupt (example for D9)
  PCMSK0 |= bit (PCINT1);  // want pin 9
  PCIFR  |= bit (PCIF0);   // clear any outstanding interrupts
  PCICR  |= bit (PCIE0);   // enable pin change interrupts for D8 to D13
  }

Para manejar una interrupción de cambio de pin necesita:

  • Especifique qué pin en el grupo. Esta es la variable PCMSKn (donde n es 0, 1 o 2 de la tabla a continuación). Puede tener interrupciones en más de un pin.
  • Habilite el grupo apropiado de interrupciones (0, 1 o 2)
  • Proporcione un controlador de interrupciones como se muestra arriba

Tabla de pines -> pin change names / masks

D0    PCINT16 (PCMSK2 / PCIF2 / PCIE2)
D1    PCINT17 (PCMSK2 / PCIF2 / PCIE2)
D2    PCINT18 (PCMSK2 / PCIF2 / PCIE2)
D3    PCINT19 (PCMSK2 / PCIF2 / PCIE2)
D4    PCINT20 (PCMSK2 / PCIF2 / PCIE2)
D5    PCINT21 (PCMSK2 / PCIF2 / PCIE2)
D6    PCINT22 (PCMSK2 / PCIF2 / PCIE2)
D7    PCINT23 (PCMSK2 / PCIF2 / PCIE2)
D8    PCINT0  (PCMSK0 / PCIF0 / PCIE0)
D9    PCINT1  (PCMSK0 / PCIF0 / PCIE0)
D10   PCINT2  (PCMSK0 / PCIF0 / PCIE0)
D11   PCINT3  (PCMSK0 / PCIF0 / PCIE0)
D12   PCINT4  (PCMSK0 / PCIF0 / PCIE0)
D13   PCINT5  (PCMSK0 / PCIF0 / PCIE0)
A0    PCINT8  (PCMSK1 / PCIF1 / PCIE1)
A1    PCINT9  (PCMSK1 / PCIF1 / PCIE1)
A2    PCINT10 (PCMSK1 / PCIF1 / PCIE1)
A3    PCINT11 (PCMSK1 / PCIF1 / PCIE1)
A4    PCINT12 (PCMSK1 / PCIF1 / PCIE1)
A5    PCINT13 (PCMSK1 / PCIF1 / PCIE1)

Interrumpir el procesamiento del controlador

El manejador de interrupciones necesitaría determinar qué pin causó la interrupción si la máscara especifica más de uno (por ejemplo, si desea interrupciones en D8 / D9 / D10). Para hacer esto, necesitaría almacenar el estado anterior de ese pin y hacer ejercicio (haciendo una lectura digital o similar) si este pin en particular hubiera cambiado.


Probablemente estés usando interrupciones de todos modos ...

Un entorno Arduino "normal" ya está utilizando interrupciones, incluso si no lo intentas personalmente. Las llamadas a la función millis () y micros () utilizan la función "desbordamiento del temporizador". Uno de los temporizadores internos (temporizador 0) está configurado para interrumpir aproximadamente 1000 veces por segundo e incrementar un contador interno que efectivamente se convierte en el contador millis (). Hay un poco más que eso, ya que se realizan ajustes para la velocidad exacta del reloj.

Además, la biblioteca serial de hardware usa interrupciones para manejar datos seriales entrantes y salientes. Esto es muy útil ya que su programa puede hacer otras cosas mientras las interrupciones se disparan y llenan un búfer interno. Luego, cuando marca Serial.available (), puede averiguar qué, si algo, se ha colocado en ese búfer.


Ejecutando la siguiente instrucción después de habilitar interrupciones

Después de un poco de discusión e investigación en el foro Arduino, hemos aclarado exactamente lo que sucede después de habilitar las interrupciones. Hay tres formas principales en las que puedo pensar que puede habilitar las interrupciones, que anteriormente no estaban habilitadas:

  sei ();  // set interrupt enable flag
  SREG |= 0x80;  // set the high-order bit in the status register
  reti  ;   // assembler instruction "return from interrupt"

En todos los casos, el procesador garantiza que la siguiente instrucción después de habilitar las interrupciones (si se deshabilitaron previamente) siempre se ejecutará, incluso si hay un evento de interrupción pendiente. (Por "siguiente" me refiero al siguiente en la secuencia del programa, no necesariamente el que sigue físicamente. Por ejemplo, una instrucción RETI salta al lugar donde ocurrió la interrupción, y luego ejecuta una instrucción más).

Esto le permite escribir código como este:

sei ();
sleep_cpu ();

Si no fuera por esta garantía, la interrupción podría ocurrir antes de que el procesador durmiera, y luego nunca podría despertarse.


Interrupciones vacías

Si simplemente desea una interrupción para activar el procesador, pero no hace nada en particular, puede usar la definición EMPTY_INTERRUPT, por ejemplo.

EMPTY_INTERRUPT (PCINT1_vect);

Esto simplemente genera una instrucción "retirado" (retorno de la interrupción). Como no intenta guardar ni restaurar registros, esta sería la forma más rápida de obtener una interrupción para activarlo.


Secciones críticas (acceso variable atómico)

Hay algunos problemas sutiles con respecto a las variables que se comparten entre las rutinas de servicio de interrupción (ISR) y el código principal (es decir, el código no está en un ISR).

Dado que un ISR puede dispararse en cualquier momento cuando las interrupciones están habilitadas, debe tener cuidado al acceder a dichas variables compartidas, ya que pueden actualizarse en el momento en que accede a ellas.

Primero ... ¿cuándo usa variables "volátiles"?

Una variable solo debe marcarse como volátil si se usa tanto dentro de un ISR como fuera de ella.

  • Las variables que solo se usan fuera de un ISR no deben ser volátiles.
  • Las variables que solo se usan dentro de un ISR no deben ser volátiles.
  • Las variables utilizadas tanto dentro como fuera de un ISR deben ser volátiles.

p.ej.

volatile int counter;

Marcar una variable como volátil le dice al compilador que no "almacene" el contenido de las variables en un registro del procesador, sino que siempre lo lea de la memoria, cuando sea necesario. Esto puede ralentizar el procesamiento, por lo que no solo hace que cada variable sea volátil, cuando no es necesario.

Desactiva las interrupciones mientras accedes a una variable volátil

Por ejemplo, para comparar countcon algún número, apague las interrupciones durante la comparación en caso countde que el ISR haya actualizado un byte de y no el otro byte.

volatile unsigned int count;

ISR (TIMER1_OVF_vect)
  {
  count++;
  } // end of TIMER1_OVF_vect

void setup ()
  {
  pinMode (13, OUTPUT);
  }  // end of setup

void loop ()
  {
  noInterrupts ();    // <------ critical section
  if (count > 20)
     digitalWrite (13, HIGH);
  interrupts ();      // <------ end critical section
  } // end of loop

¡Lea la hoja de datos!

Se puede obtener más información sobre interrupciones, temporizadores, etc. de la hoja de datos del procesador.

http://www.atmel.com/images/Atmel-8271-8-bit-AVR-Microcontroller-ATmega48A-48PA-88A-88PA-168A-168PA-328-328P_datasheet_Complete.pdf


Ejemplos adicionales

Las consideraciones de espacio (límite de tamaño de publicación) impiden que mi listado tenga más código de ejemplo. Para obtener más código de ejemplo, vea mi página sobre interrupciones .


Una referencia muy útil, que fue una respuesta impresionantemente rápida.
Dat Han Bag

Fue una pregunta de referencia. Tenía la respuesta preparada y hubiera sido aún más rápido si la respuesta no hubiera sido demasiado larga, así que tuve que podarla. Vea el sitio vinculado para más detalles.
Nick Gammon

Sobre el "modo de suspensión", ¿es eficiente hacer que el Arduino duerma, digamos, 500 ms?
Dat Ha

@Nick Gammon Supongo que encender o apagar la alimentación (con automatización o no) para la CPU se puede definir como una interrupción no convencional, si quisieras hacer eso. "Tenía la respuesta preparada", acabas de sacar toda la magia de ese momento que pensé que tenía.
Dat Han Bag

1
Me temo que eso no es cierto. Tengo un ejemplo que usa interrupciones de cambio de pin para despertarse del modo de apagado. Además, como mencioné en mi página sobre las interrupciones, Atmel ha confirmado que cualquier interrupción externa despertará al procesador (es decir, subiendo / bajando / cambiando y bajando ).
Nick Gammon
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.