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_array
hay valores como: 111
o 000
.
Según tengo entendido, si uso la CHANGE
opción en la attachInterrupt()
función, entonces la secuencia de datos siempre debe ser 0101010101
sin repetir.
Los datos cambian bastante rápido ya que provienen de un módulo de radio.
arduino-uno
c
isr
usuario277820
fuente
fuente
pin
,x
y latest_array
definición, y tambiénloop()
el método; nos permitiría ver si esto puede ser un problema de concurrencia al acceder a variables modificadas portest_func
.if (digitalRead(pin) == HIGH) ... else ...;
o, mejor aún, esta sola línea ISR:test_array[x++] = digitalRead(pin);
.Respuestas:
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: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 con
attachInterrupt()
, y llamará a ese controlador, que es nuestraread_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:
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 reemplazarlodigitalReadFast()
, 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:
1 << 2
.Entonces, aquí está nuestro manejador de interrupciones modificado:
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:setup()
.setup()
.ISR(INT0_vect) { ... }
.Aquí está el código para el ISR y
setup()
, todo lo demás no ha cambiado: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í:
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:
Nuestro anterior INT0 ISR es reemplazado por esto:
Aquí estamos utilizando la macro ISR () para tener el instrumento compilador
INT0_vect_part_2
con 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
in
instrucció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 elsbi
( Establezca en 1 algún bit en algún registro de E / S) de la siguiente manera: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_pin
ya no es útil, ya que básicamente se reemplaza por GPIOR0: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:
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.
Cada vector tiene una ranura de 4 bytes, llena con una sola
jmp
instrucció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 instruccionessbic
ysbi
, pero no lasrjmp
. Si hacemos eso, la tabla de vectores termina luciendo así: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:Se puede compilar con la siguiente línea de comando:
El boceto es idéntico al anterior, excepto que no hay INT0_vect, y INT0_vect_part_2 se reemplaza por INT1_vect:
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
-nostartfiles
opció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.
fuente
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.fuente