Interrupción de Arduino (en cambio de pin)

8

Utilizo la función de interrupción para llenar una matriz con los valores recibidos de digitalRead().

 void setup() {
      Serial.begin(115200);
       attachInterrupt(0, test_func, CHANGE);
    }

    void test_func(){
      if(digitalRead(pin)==HIGH){
          test_array[x]=1;  
        } else if(digitalRead(pin)==LOW){
          test_array[x]=0;  
        }
         x=x+1;
    }

Ese problema es que cuando imprimo test_arrayhay valores como: 111o 000.

Según tengo entendido, si uso la CHANGEopción en la attachInterrupt()función, entonces la secuencia de datos siempre debe ser 0101010101sin repetir.

Los datos cambian bastante rápido ya que provienen de un módulo de radio.

usuario277820
fuente
1
Las interrupciones no eliminan el botón. ¿Está utilizando el rebote de hardware?
Ignacio Vazquez-Abrams
Por favor enviar el código completo, incluido pin, xy la test_arraydefinición, y también loop()el método; nos permitiría ver si esto puede ser un problema de concurrencia al acceder a variables modificadas por test_func.
jfpoilpret
2
No debe digitalRead () dos veces en el ISR: piense en lo que sucedería si obtiene BAJO en la primera llamada y ALTO en la segunda. En su lugar, if (digitalRead(pin) == HIGH) ... else ...;o, mejor aún, esta sola línea ISR: test_array[x++] = digitalRead(pin);.
Edgar Bonet
@EdgarBonet agradable! +1 a ese comentario. Espero que no te importe, agregué algo a mi respuesta para incluir lo que has mencionado aquí. Además, si decide presentar su propia respuesta, incluido este detalle, eliminaré mi agregado y le daré un voto positivo para que obtenga el representante.
Clayton Mills
@Clayton Mills: estoy preparando una respuesta (demasiado larga y marginalmente tangencial), pero puedes mantener tu edición, está perfectamente bien conmigo.
Edgar Bonet

Respuestas:

21

Como una especie de prólogo a esta respuesta demasiado larga ...

Esta pregunta me cautivó profundamente con el problema de la latencia de interrupción, hasta el punto de perder el sueño en los ciclos de conteo en lugar de las ovejas. Escribo esta respuesta más por compartir mis hallazgos que solo por responder la pregunta: la mayor parte de este material puede no estar en un nivel adecuado para una respuesta adecuada. Sin embargo, espero que sea útil para los lectores que llegan aquí en busca de soluciones para problemas de latencia. Se espera que las primeras secciones sean útiles para una amplia audiencia, incluido el póster original. Luego, se pone peludo en el camino.

Clayton Mills ya explicó en su respuesta que hay cierta latencia en responder a las interrupciones. Aquí me centraré en cuantificar la latencia (que es enorme cuando se usan las bibliotecas Arduino) y en los medios para minimizarla. La mayor parte de lo que sigue es específico para el hardware del Arduino Uno y placas similares.

Minimizando la latencia de interrupción en el Arduino

(o cómo pasar de 99 a 5 ciclos)

Usaré la pregunta original como un ejemplo de trabajo y volveré a plantear el problema en términos de latencia de interrupción. Tenemos algún evento externo que desencadena una interrupción (aquí: INT0 al cambiar el pin). Necesitamos tomar alguna acción cuando se dispara la interrupción (aquí: leer una entrada digital). El problema es: hay un retraso entre la activación de la interrupción y la adopción de las medidas adecuadas. Llamamos a este retraso " interrupción de latencia ". Una latencia larga es perjudicial en muchas situaciones. En este ejemplo particular, la señal de entrada puede cambiar durante el retraso, en cuyo caso tenemos una lectura defectuosa. No hay nada que podamos hacer para evitar el retraso: es intrínseco a la forma en que interrumpe el trabajo. Sin embargo, podemos intentar que sea lo más breve posible, lo que con suerte debería minimizar las malas consecuencias.

La primera cosa obvia que podemos hacer es tomar la acción de tiempo crítico, dentro del controlador de interrupciones, lo antes posible. Esto significa llamar digitalRead()una vez (y solo una vez) al comienzo del controlador. Aquí está la versión cero del programa sobre el cual construiremos:

#define INT_NUMBER 0
#define PIN_NUMBER 2    // interrupt 0 is on pin 2
#define MAX_COUNT  200

volatile uint8_t count_edges;  // count of signal edges
volatile uint8_t count_high;   // count of high levels

/* Interrupt handler. */
void read_pin()
{
    int pin_state = digitalRead(PIN_NUMBER);  // do this first!
    if (count_edges >= MAX_COUNT) return;     // we are done
    count_edges++;
    if (pin_state == HIGH) count_high++;
}

void setup()
{
    Serial.begin(9600);
    attachInterrupt(INT_NUMBER, read_pin, CHANGE);
}

void loop()
{
    /* Wait for the interrupt handler to count MAX_COUNT edges. */
    while (count_edges < MAX_COUNT) { /* wait */ }

    /* Report result. */
    Serial.print("Counted ");
    Serial.print(count_high);
    Serial.print(" HIGH levels for ");
    Serial.print(count_edges);
    Serial.println(" edges");

    /* Count again. */
    count_high = 0;
    count_edges = 0;  // do this last to avoid race condition
}

Probé este programa y las versiones posteriores enviándole trenes de pulsos de diferentes anchos. Hay suficiente espacio entre los pulsos para garantizar que no se pierda ningún borde: incluso si el borde descendente se recibe antes de que se realice la interrupción anterior, la segunda solicitud de interrupción se pondrá en espera y finalmente se atenderá. Si un pulso es más corto que la latencia de interrupción, el programa lee 0 en ambos bordes. El número reportado de niveles ALTOS es el porcentaje de pulsos leídos correctamente.

¿Qué sucede cuando se dispara la interrupción?

Antes de intentar mejorar el código anterior, veremos los eventos que se desarrollan justo después de que se active la interrupción. La parte del hardware de la historia está contada por la documentación de Atmel. La parte del software, al desmontar el binario.

La mayoría de las veces, la interrupción entrante se repara de inmediato. Sin embargo, puede suceder que la MCU (que significa "microcontrolador") se encuentre en medio de una tarea de tiempo crítico, donde el servicio de interrupción está desactivado. Este suele ser el caso cuando ya está prestando servicio a otra interrupción. Cuando esto sucede, la solicitud de interrupción entrante se pone en espera y se atiende solo cuando se completa esa sección de tiempo crítico. Esta situación es difícil de evitar por completo, porque hay bastantes de esas secciones críticas en la biblioteca principal de Arduino (que llamaré " libcore ""en lo siguiente). Afortunadamente, estas secciones son cortas y se ejecutan solo de vez en cuando. Por lo tanto, la mayoría de las veces, nuestra solicitud de interrupción será atendida de inmediato. En lo siguiente, supondré que no nos importan esos pocos instancias cuando este no es el caso.

Entonces, nuestra solicitud se atiende de inmediato. Esto todavía implica muchas cosas que pueden llevar bastante tiempo. Primero, hay una secuencia cableada. La MCU terminará de ejecutar la instrucción actual. Afortunadamente, la mayoría de las instrucciones son de ciclo único, pero algunas pueden tomar hasta cuatro ciclos. Luego, la MCU borra una bandera interna que deshabilita el servicio adicional de las interrupciones. Esto está destinado a evitar interrupciones anidadas. Luego, la PC se guarda en la pila. La pila es un área de RAM reservada para este tipo de almacenamiento temporal. La PC (que significa " Contador de programas") es un registro interno que contiene la dirección de la próxima instrucción que la MCU está a punto de ejecutar. Esto es lo que le permite a la MCU saber qué hacer a continuación, y guardarla es esencial porque tendrá que restaurarse para que el principal programa para reanudar desde donde fue interrumpido. La PC se carga con una dirección cableada específica a la solicitud recibida, y este es el final de la secuencia cableada, el resto está controlado por software.

La MCU ahora ejecuta la instrucción desde esa dirección cableada. Esta instrucción se denomina " vector de interrupción " y generalmente es una instrucción de "salto" que nos llevará a una rutina especial llamada ISR (" Rutina de servicio de interrupción "). En este caso, el ISR se llama "__vector_1", también conocido como "INT0_vect", que es un nombre inapropiado porque es un ISR, no un vector. Este ISR en particular proviene de libcore. Como cualquier ISR, comienza con un prólogo que guarda un montón de registros internos de la CPU en la pila. Esto le permitirá usar esos registros y, cuando esté listo, restaurarlos a sus valores anteriores para no perturbar el programa principal. Luego, buscará el controlador de interrupciones registrado conattachInterrupt(), y llamará a ese controlador, que es nuestra read_pin()función anterior. Nuestra función llamará digitalRead()desde libcore. digitalRead()buscará en algunas tablas para asignar el número de puerto de Arduino al puerto de E / S de hardware que tiene que leer y el número de bit asociado para probar. También verificará si hay un canal PWM en ese pin que deba desactivarse. Luego leerá el puerto de E / S ... y hemos terminado. Bueno, realmente no hemos terminado de dar servicio a la interrupción, pero la tarea de tiempo crítico (leer el puerto de E / S) está hecha, y es todo lo que importa cuando observamos la latencia.

Aquí hay un breve resumen de todo lo anterior, junto con los retrasos asociados en los ciclos de la CPU:

  1. secuencia cableada: finalizar la instrucción actual, evitar interrupciones anidadas, guardar PC, cargar la dirección del vector (≥ 4 ciclos)
  2. ejecutar vector de interrupción: saltar a ISR (3 ciclos)
  3. Prólogo ISR: guardar registros (32 ciclos)
  4. Cuerpo principal de ISR: localizar y llamar a la función registrada por el usuario (13 ciclos)
  5. read_pin: call digitalRead (5 ciclos)
  6. digitalRead: encuentre el puerto y el bit relevantes para probar (41 ciclos)
  7. digitalRead: lea el puerto de E / S (1 ciclo)

Asumiremos el mejor de los casos, con 4 ciclos para la secuencia cableada. Esto nos da una latencia total de 99 ciclos, o aproximadamente 6.2 µs con un reloj de 16 MHz. A continuación, exploraré algunos trucos que se pueden usar para reducir esta latencia. Vienen más o menos en orden creciente de complejidad, pero todos nos necesitan para profundizar de alguna manera en lo interno de la MCU.

Usar acceso directo al puerto

El primer objetivo obvio para acortar la latencia es digitalRead(). Esta función proporciona una buena abstracción para el hardware de MCU, pero es demasiado ineficiente para trabajos críticos. Deshacerse de este es realmente trivial: solo tenemos que reemplazarlo digitalReadFast(), desde la biblioteca digitalwritefast . ¡Esto reduce la latencia casi a la mitad a costa de una pequeña descarga!

Bueno, eso fue demasiado fácil para ser divertido, prefiero mostrarte cómo hacerlo de la manera difícil. El propósito es comenzar con cosas de bajo nivel. El método se llama " acceso directo al puerto " y está bien documentado en la referencia de Arduino en la página de Registros de puertos . En este punto, es una buena idea descargar y echar un vistazo a la hoja de datos ATmega328P . Este documento de 650 páginas puede parecer algo desalentador a primera vista. Sin embargo, está bien organizado en secciones específicas para cada uno de los periféricos y características de MCU. Y solo necesitamos verificar las secciones relevantes para lo que estamos haciendo. En este caso, es la sección llamada de E / S de los puertos . Aquí hay un resumen de lo que aprendemos de esas lecturas:

  • El pin 2 de Arduino en realidad se llama PD2 (es decir, puerto D, bit 2) en el chip AVR.
  • Obtenemos todo el puerto D a la vez leyendo un registro especial de MCU llamado "PIND".
  • Luego verificamos el bit número 2 haciendo una lógica bit a bit y (el operador C '&') con 1 << 2.

Entonces, aquí está nuestro manejador de interrupciones modificado:

#define PIN_REG    PIND  // interrupt 0 is on AVR pin PD2
#define PIN_BIT    2

/* Interrupt handler. */
void read_pin()
{
    uint8_t sampled_pin = PIN_REG;            // do this first!
    if (count_edges >= MAX_COUNT) return;     // we are done
    count_edges++;
    if (sampled_pin & (1 << PIN_BIT)) count_high++;
}

Ahora, nuestro controlador leerá el registro de E / S tan pronto como se llame. La latencia es de 53 ciclos de CPU. ¡Este simple truco nos salvó 46 ciclos!

Escribe tu propio ISR

El siguiente objetivo para el recorte de ciclo es el INT0_vect ISR. Este ISR es necesario para proporcionar la funcionalidad de attachInterrupt(): podemos cambiar los controladores de interrupciones en cualquier momento durante la ejecución del programa. Sin embargo, aunque es bueno tenerlo, esto no es realmente útil para nuestro propósito. Por lo tanto, en lugar de que el ISR de libcore localice y llame a nuestro controlador de interrupciones, ahorraremos algunos ciclos al reemplazar el ISR por nuestro controlador.

No es tan dificíl como suena. Los ISR pueden escribirse como funciones normales, solo debemos conocer sus nombres específicos y definirlos utilizando una ISR()macro especial de avr-libc. En este punto, sería bueno echar un vistazo a la documentación de avr-libc sobre interrupciones y a la sección de la hoja de datos denominada Interrupciones externas . Aquí está el breve resumen:

  • Tenemos que escribir un bit en un registro de hardware especial llamado EICRA ( Registro de control de interrupción externa A ) para configurar la interrupción que se activará ante cualquier cambio en el valor del pin. Esto se hará en setup().
  • Tenemos que escribir un bit en otro registro de hardware llamado EIMSK ( registro de interrupción externa MaSK ) para habilitar la interrupción INT0. Esto también se hará en setup().
  • Tenemos que definir el ISR con la sintaxis ISR(INT0_vect) { ... }.

Aquí está el código para el ISR y setup(), todo lo demás no ha cambiado:

/* Interrupt service routine for INT0. */
ISR(INT0_vect)
{
    uint8_t sampled_pin = PIN_REG;            // do this first!
    if (count_edges >= MAX_COUNT) return;     // we are done
    count_edges++;
    if (sampled_pin & (1 << PIN_BIT)) count_high++;
}

void setup()
{
    Serial.begin(9600);
    EICRA = 1 << ISC00;  // sense any change on the INT0 pin
    EIMSK = 1 << INT0;   // enable INT0 interrupt
}

Esto viene con una bonificación gratuita: dado que este ISR es más simple que el que reemplaza, necesita menos registros para hacer su trabajo, entonces el prólogo para guardar registros es más corto. Ahora tenemos una latencia de 20 ciclos. ¡No está mal teniendo en cuenta que comenzamos cerca de 100!

En este punto, diría que hemos terminado. Misión cumplida. Lo que sigue es solo para aquellos que no tienen miedo de ensuciarse las manos con algún conjunto AVR. De lo contrario, puede dejar de leer aquí, y gracias por llegar tan lejos.

Escribe un ISR desnudo

¿Aún aquí? ¡Bueno! Para continuar, sería útil tener al menos una idea muy básica de cómo funciona el ensamblaje, y echar un vistazo al Inline Assembler Cookbook de la documentación de avr-libc. En este punto, nuestra secuencia de entrada de interrupción se ve así:

  1. secuencia cableada (4 ciclos)
  2. vector de interrupción: saltar a ISR (3 ciclos)
  3. Prólogo de ISR: guardar registros (12 ciclos)
  4. Lo primero en el cuerpo del ISR: lea el puerto IO (1 ciclo)

Si queremos hacerlo mejor, tenemos que trasladar la lectura del puerto al prólogo. La idea es la siguiente: al leer el registro PIND, se registrará un registro de CPU, por lo tanto, debemos guardar al menos un registro antes de hacerlo, pero los otros registros pueden esperar. Luego, debemos escribir un prólogo personalizado que lea el puerto de E / S justo después de guardar el primer registro. Ya ha visto en la documentación de interrupción de avr-libc (lo ha leído, ¿verdad?) Que un ISR se puede desnudar , en cuyo caso el compilador no emitirá ningún prólogo o epílogo, lo que nos permite escribir nuestra propia versión personalizada.

El problema con este enfoque es que probablemente terminemos escribiendo todo el ISR en conjunto. No es gran cosa, pero prefiero que el compilador escriba esos aburridos prólogos y epílogos para mí. Entonces, aquí está el truco sucio: dividiremos el ISR en dos partes:

  • la primera parte será un fragmento de ensamblaje corto que
    • guardar un solo registro en la pila
    • leer PIND en ese registro
    • almacenar ese valor en una variable global
    • restaurar el registro de la pila
    • saltar a la segunda parte
  • la segunda parte será un código C regular con un prólogo y un epílogo generados por el compilador

Nuestro anterior INT0 ISR es reemplazado por esto:

volatile uint8_t sampled_pin;    // this is now a global variable

/* Interrupt service routine for INT0. */
ISR(INT0_vect, ISR_NAKED)
{
    asm volatile(
    "    push r0                \n"  // save register r0
    "    in r0, %[pin]          \n"  // read PIND into r0
    "    sts sampled_pin, r0    \n"  // store r0 in a global
    "    pop r0                 \n"  // restore previous r0
    "    rjmp INT0_vect_part_2  \n"  // go to part 2
    :: [pin] "I" (_SFR_IO_ADDR(PIND)));
}

ISR(INT0_vect_part_2)
{
    if (count_edges >= MAX_COUNT) return;     // we are done
    count_edges++;
    if (sampled_pin & (1 << PIN_BIT)) count_high++;
}

Aquí estamos utilizando la macro ISR () para tener el instrumento compilador INT0_vect_part_2con el prólogo y el epílogo requeridos. El compilador se quejará de que "'INT0_vect_part_2' parece ser un controlador de señal mal escrito", pero la advertencia se puede ignorar con seguridad. Ahora el ISR tiene una sola instrucción de 2 ciclos antes de la lectura del puerto real, y la latencia total es de solo 10 ciclos.

Use el registro GPIOR0

¿Qué pasaría si pudiéramos tener un registro reservado para este trabajo específico? Entonces, no necesitaríamos guardar nada antes de leer el puerto. De hecho, podemos pedirle al compilador que vincule una variable global a un registro . Sin embargo, esto requeriría que recompilemos todo el núcleo Arduino y libc para asegurarnos de que el registro esté siempre reservado. No es realmente conveniente. Por otro lado, el ATmega328P tiene tres registros que no son utilizados por el compilador ni ninguna biblioteca, y están disponibles para almacenar lo que queramos. Se denominan GPIOR0, GPIOR1 y GPIOR2 ( registros de E / S de uso general ). Aunque están asignados en el espacio de direcciones de E / S de la MCU, en realidad no sonRegistros de E / S: son simplemente memoria, como tres bytes de RAM que de alguna manera se perdieron en un bus y terminaron en el espacio de direcciones incorrecto. Estos no son tan capaces como los registros internos de la CPU, y no podemos copiar PIND en uno de estos con la ininstrucción. Sin embargo, GPIOR0 es interesante, ya que es direccionable , como PIND. Esto nos permitirá transferir la información sin bloquear ningún registro interno de la CPU.

Aquí está el truco: nos aseguraremos de que GPIOR0 sea inicialmente cero (en realidad lo borra el hardware en el momento del arranque), luego usaremos sbic(Omitir la siguiente instrucción si algún Bit en algún registro de E / S es Clear) y el sbi( Establezca en 1 algún bit en algún registro de E / S) de la siguiente manera:

sbic PIND, 2   ; skip the following if bit 2 of PIND is clear
sbi GPIOR0, 0  ; set to 1 bit 0 of GPIOR0

De esta manera, GPIOR0 terminará siendo 0 o 1 dependiendo del bit que queramos leer de PIND. La instrucción sbic tarda 1 o 2 ciclos en ejecutarse dependiendo de si la condición es falsa o verdadera. Obviamente, se accede al bit PIND en el primer ciclo. En esta nueva versión del código, la variable global sampled_pinya no es útil, ya que básicamente se reemplaza por GPIOR0:

/* Interrupt service routine for INT0. */
ISR(INT0_vect, ISR_NAKED)
{
    asm volatile(
    "    sbic %[pin], %[bit]    \n"
    "    sbi %[gpio], 0         \n"
    "    rjmp INT0_vect_part_2  \n"
    :: [pin]  "I" (_SFR_IO_ADDR(PIND)),
       [bit]  "I" (PIN_BIT),
       [gpio] "I" (_SFR_IO_ADDR(GPIOR0)));
}

ISR(INT0_vect_part_2)
{
    if (count_edges < MAX_COUNT) {
        count_edges++;
        if (GPIOR0) count_high++;
    }
    GPIOR0 = 0;
}

Cabe señalar que GPIOR0 siempre debe restablecerse en el ISR.

Ahora, el muestreo del registro PIND I / O es lo primero que se hace dentro del ISR. La latencia total es de 8 ciclos. Esto es lo mejor que podemos hacer antes de mancharnos con errores terriblemente pecaminosos. Esta es otra vez una buena oportunidad para dejar de leer ...

Ponga el código de tiempo crítico en la tabla de vectores

Para aquellos que todavía están aquí, aquí está nuestra situación actual:

  1. secuencia cableada (4 ciclos)
  2. vector de interrupción: saltar a ISR (3 ciclos)
  3. Cuerpo ISR: lea el puerto IO (en el 1er ciclo)

Obviamente hay poco margen de mejora. La única forma en que podríamos acortar la latencia en este punto es reemplazando el vector de interrupción en sí por nuestro código. Tenga en cuenta que esto debería ser inmensamente desagradable para cualquiera que valore el diseño de software limpio. Pero es posible, y te mostraré cómo.

El diseño de la tabla de vectores ATmega328P se puede encontrar en la hoja de datos, sección Interrupciones , subsección Vectores de interrupción en ATmega328 y ATmega328P . O al desmontar cualquier programa para este chip. Así es como se ve. Estoy usando las convenciones avr-gcc y avr-libc (__init es el vector 0, las direcciones están en bytes) que son diferentes de las de Atmel.

address  instruction      comment
────────┼─────────────────┼──────────────────────
 0x0000  jmp __init       reset vector 
 0x0004  jmp __vector_1   a.k.a. INT0_vect
 0x0008  jmp __vector_2   a.k.a. INT1_vect
 0x000c  jmp __vector_3   a.k.a. PCINT0_vect
  ...
 0x0064  jmp __vector_25  a.k.a. SPM_READY_vect

Cada vector tiene una ranura de 4 bytes, llena con una sola jmpinstrucción. Esta es una instrucción de 32 bits, a diferencia de la mayoría de las instrucciones AVR que son de 16 bits. Pero una ranura de 32 bits es demasiado pequeña para contener la primera parte de nuestro ISR: podemos ajustar las instrucciones sbicy sbi, pero no las rjmp. Si hacemos eso, la tabla de vectores termina luciendo así:

address  instruction      comment
────────┼─────────────────┼──────────────────────
 0x0000  jmp __init       reset vector 
 0x0004  sbic PIND, 2     the first part...
 0x0006  sbi GPIOR0, 0    ...of our ISR
 0x0008  jmp __vector_2   a.k.a. INT1_vect
 0x000c  jmp __vector_3   a.k.a. PCINT0_vect
  ...
 0x0064  jmp __vector_25  a.k.a. SPM_READY_vect

Cuando se dispara INT0, se leerá PIND, el bit relevante se copiará en GPIOR0, y luego la ejecución pasará al siguiente vector. Luego, se llamará al ISR para INT1, en lugar del ISR para INT0. Esto es espeluznante, pero como no estamos usando INT1 de todos modos, simplemente "secuestraremos" su vector para dar servicio a INT0.

Ahora, solo tenemos que escribir nuestra propia tabla de vectores personalizada para anular la predeterminada. Resulta que no es tan fácil. La tabla de vectores predeterminada es proporcionada por la distribución avr-libc, en un archivo de objeto llamado crtm328p.o que se vincula automáticamente con cualquier programa que creamos. A diferencia del código de la biblioteca, el código del archivo de objeto no debe anularse: intentar hacerlo generará un error de enlace sobre la tabla que se define dos veces. Esto significa que tenemos que reemplazar todo el crtm328p.o con nuestra versión personalizada. Una opción es descargar el código fuente completo avr-libc , hacer nuestras modificaciones personalizadas en gcrt1.S , luego compilarlo como una biblioteca personalizada.

Aquí fui por un enfoque alternativo más ligero. Escribí un crt.S personalizado, que es una versión simplificada del original de avr-libc. Carece de algunas características raramente utilizadas, como la capacidad de definir un ISR "catch all", o de poder finalizar el programa (es decir, congelar el Arduino) llamando exit(). Aquí está el código. Recorté la parte repetitiva de la tabla de vectores para minimizar el desplazamiento:

#include <avr/io.h>

.weak __heap_end
.set  __heap_end, 0

.macro vector name
    .weak \name
    .set \name, __vectors
    jmp \name
.endm

.section .vectors
__vectors:
    jmp __init
    sbic _SFR_IO_ADDR(PIND), 2   ; these 2 lines...
    sbi _SFR_IO_ADDR(GPIOR0), 0  ; ...replace vector_1
    vector __vector_2
    vector __vector_3
    [...and so forth until...]
    vector __vector_25

.section .init2
__init:
    clr r1
    out _SFR_IO_ADDR(SREG), r1
    ldi r28, lo8(RAMEND)
    ldi r29, hi8(RAMEND)
    out _SFR_IO_ADDR(SPL), r28
    out _SFR_IO_ADDR(SPH), r29

.section .init9
    jmp main

Se puede compilar con la siguiente línea de comando:

avr-gcc -c -mmcu=atmega328p silly-crt.S

El boceto es idéntico al anterior, excepto que no hay INT0_vect, y INT0_vect_part_2 se reemplaza por INT1_vect:

/* Interrupt service routine for INT1 hijacked to service INT0. */
ISR(INT1_vect)
{
    if (count_edges < MAX_COUNT) {
        count_edges++;
        if (GPIOR0) count_high++;
    }
    GPIOR0 = 0;
}

Para compilar el boceto, necesitamos un comando de compilación personalizado. Si ha seguido hasta ahora, probablemente sepa cómo compilar desde la línea de comandos. Debe solicitar explícitamente que silly-crt.o se vincule a su programa y agregar la -nostartfilesopción para evitar el enlace en el crtm328p.o original.

Ahora, la lectura del puerto de E / S es la primera instrucción ejecutada después de que se dispara la interrupción. Probé esta versión enviándole pulsos cortos desde otro Arduino, y puede atrapar (aunque no de manera confiable) el alto nivel de pulsos tan cortos como 5 ciclos. No hay nada más que podamos hacer para acortar la latencia de interrupción en este hardware.

Edgar Bonet
fuente
2
¡Buena explicación! +1
Nick Gammon
6

La interrupción está configurada para activarse en un cambio, y su test_func está configurado como la Rutina de servicio de interrupción (ISR), llamada para dar servicio a esa interrupción. El ISR luego imprime el valor de la entrada.

A primera vista, esperaría que la salida sea como ha dicho, y un conjunto alternativo de mínimos altos, ya que solo llega al ISR en un cambio.

Pero lo que nos falta es que hay una cierta cantidad de tiempo que le toma a la CPU dar servicio a una interrupción y derivar al ISR. Durante este tiempo, el voltaje en el pin puede haber cambiado nuevamente. Particularmente si el pasador no está estabilizado por un rebote de hardware o similar. Debido a que la interrupción ya está marcada y aún no ha sido reparada, se perderá este cambio adicional (o muchos de ellos, porque un nivel de pines puede cambiar muy rápidamente en relación con la velocidad del reloj si tiene una baja capacidad parásita).

Entonces, en esencia, sin alguna forma de eliminación de rebotes, no tenemos garantía de que cuando la entrada cambie, y la interrupción se marque para el servicio, la entrada seguirá teniendo el mismo valor cuando lleguemos a leer su valor en el ISR.

Como ejemplo genérico, la hoja de datos ATmega328 utilizada en el Arduino Uno detalla los tiempos de interrupción en la sección 6.7.1 - "Tiempo de respuesta de interrupción". Establece para este microcontrolador que el tiempo mínimo para ramificarse a un ISR para el servicio es de 4 ciclos de reloj, pero puede ser más (adicional si se ejecuta la instrucción de varios ciclos en la interrupción o 8 + tiempo de reposo si la MCU está en reposo).

Como @EdgarBonet mencionó en los comentarios, el pin también podría cambiar de manera similar durante la ejecución de ISR. Debido a que el ISR lee del pin dos veces, no agregaría nada al test_array si encuentra un LOW en la primera lectura y un HIGH en la segunda. Pero x aún se incrementaría, dejando esa ranura en la matriz sin cambios (posiblemente como datos no inicializados dependiendo de lo que se hizo a la matriz anteriormente).

Su ISR de una línea test_array[x++] = digitalRead(pin);es una solución perfecta para esto.

Clayton Mills
fuente