La rutina de servicio de interrupción AVR no se ejecuta tan rápido como se esperaba (¿sobrecarga de instrucciones?)

8

Estoy desarrollando un pequeño analizador lógico con 7 entradas. Mi dispositivo objetivo es ATmega168con una frecuencia de reloj de 20MHz. Para detectar cambios lógicos, uso interrupciones de cambio de pin. Ahora estoy tratando de encontrar la frecuencia de muestreo más baja que pueda detectar estos cambios de pin. Determiné un valor mínimo de 5.6 µs (178.5 kHz). Cada señal por debajo de esta velocidad no puedo capturar correctamente.

Mi código está escrito en C (avr-gcc). Mi rutina se ve así:

ISR()
{
    pinc = PINC; // char
    timestamp_ll = TCNT1L; // char
    timestamp_lh = TCNT1H; // char
    timestamp_h = timerh; // 2 byte integer
    stack_counter++;
}

Mi cambio de señal capturado se encuentra en pinc. Para localizarlo, tengo un valor de marca de tiempo de 4 bytes.

En la hoja de datos que leí, la rutina de servicio de interrupción toma 5 relojes para saltar y 5 relojes para volver al procedimiento principal. Supongo que cada comando en mi ISR()toma 1 reloj para ejecutarse; En resumen, debería haber una sobrecarga de 5 + 5 + 5 = 15relojes. La duración de un reloj debe estar de acuerdo con la velocidad del reloj de 20MHz 1/20000000 = 0.00000005 = 50 ns. La sobrecarga total en el segundo debe ser a continuación: 15 * 50 ns = 750 ns = 0.75 µs. Ahora no entiendo por qué no puedo capturar nada por debajo de 5.6 µs. ¿Alguien puede explicar lo que está pasando?

arminb
fuente
quizás 5 relojes para despachar el código ISR, que incluye el guardado del contexto y la restauración del epílogo / prólogo que no ve en la fuente C. Además, ¿qué hace el hardware cuando se produce la interrupción? ¿Está en algún estado de sueño? (No conozco AVR, pero en general, interrumpir el procesamiento de ciertos estados puede llevar más tiempo).
Kaz
@arminb Consulte también esta pregunta para obtener más ideas sobre cómo capturar eventos externos con mayor precisión. También [esta nota de aplicación] (www.atmel.com/Images/doc2505.pdf) puede ser de interés.
angelatlarge

Respuestas:

10

Hay un par de problemas:

  • No todos los comandos AVR tardan 1 reloj en ejecutarse: si observa la parte posterior de la hoja de datos, tiene la cantidad de relojes necesarios para ejecutar cada instrucción. Entonces, por ejemplo, ANDes una instrucción de un reloj, MUL(multiplicar) toma dos relojes, mientras que LPM(cargar la memoria del programa) es tres y CALLes 4. Entonces, con respecto a la ejecución de la instrucción, realmente depende de la instrucción.
  • 5 relojes para saltar y 5 relojes para regresar pueden ser engañosos. Si observa su código desmontado, encontrará que además del salto y las RETIinstrucciones, el compilador agrega todo tipo de otro código, lo que también lleva tiempo. Por ejemplo, es posible que necesite variables locales que se crean en la pila y se deben quitar, etc. Lo mejor que puede hacer para ver lo que realmente está sucediendo es mirar el desmontaje.
  • Por último, recuerde que mientras está en su rutina ISR, sus interrupciones no se activan. Esto significa que no podrá obtener el tipo de rendimiento que busca de su analizador lógico, a menos que sepa que sus niveles de señal cambian a intervalos más largos de lo que se necesita para dar servicio a su interrupción. Para ser claros, una vez que calcula el tiempo que tarda su ISR en ejecutarse, esto le da un límite superior de la rapidez con que puede capturar una señal . Si necesita capturar dos señales, entonces comienza a tener problemas. Para ser demasiado detallado sobre esto, considere el siguiente escenario:

ingrese la descripción de la imagen aquí

Si xes el tiempo que lleva reparar su interrupción, entonces la señal B nunca será capturada.


Si tomamos su código ISR, lo pegamos en una rutina de rutina ISR (que usé ISR(PCINT0_vect)), declaramos todas las variables volatiley compilamos para ATmega168P, el código desmontado se ve de la siguiente manera (consulte la respuesta de @ jipple para obtener más información) antes de llegar al código que "hace algo" ; en otras palabras, el prólogo de su ISR es el siguiente:

  37                    .loc 1 71 0
  38                    .cfi_startproc
  39 0000 1F92              push r1
  40                .LCFI0:
  41                    .cfi_def_cfa_offset 3
  42                    .cfi_offset 1, -2
  43 0002 0F92              push r0
  44                .LCFI1:
  45                    .cfi_def_cfa_offset 4
  46                    .cfi_offset 0, -3
  47 0004 0FB6              in r0,__SREG__
  48 0006 0F92              push r0
  49 0008 1124              clr __zero_reg__
  50 000a 8F93              push r24
  51                .LCFI2:
  52                    .cfi_def_cfa_offset 5
  53                    .cfi_offset 24, -4
  54 000c 9F93              push r25
  55                .LCFI3:
  56                    .cfi_def_cfa_offset 6
  57                    .cfi_offset 25, -5
  58                /* prologue: Signal */
  59                /* frame size = 0 */
  60                /* stack size = 5 */
  61                .L__stack_usage = 5

entonces, PUSHx 5, inx 1, clrx 1. No es tan malo como los vars de 32 bits de jipple, pero aún así no es nada.

Algo de esto es necesario (amplíe la discusión en los comentarios). Obviamente, dado que la rutina ISR puede ocurrir en cualquier momento, debe preseleccionar los registros que usa, a menos que sepa que ningún código donde puede ocurrir una interrupción usa el mismo registro que su rutina de interrupción. Por ejemplo, la siguiente línea en el ISR desmontado:

push r24

Está ahí porque todo pasa r24: tu pincse carga allí antes de que vaya a la memoria, etc. Entonces debes tener eso primero. __SREG__se carga r0y luego se empuja: si esto pudiera pasar, r24entonces podría ahorrarse unPUSH


Algunas posibles soluciones:

  • Use un circuito de votación ajustado como lo sugiere Kaz en los comentarios. Probablemente esta sea la solución más rápida, ya sea que escriba el bucle en C o en el ensamblaje.
  • Escriba su ISR en conjunto: de esta manera puede optimizar el uso del registro de tal manera que se necesite guardar la menor cantidad posible durante el ISR.
  • Declare sus rutinas ISR ISR_NAKED , aunque esto resulta ser más una solución de arenque rojo. Cuando declara rutinas ISR ISR_NAKED, gcc no genera un código de prólogo / epílogo, y usted es responsable de guardar los registros que modifique su código, así como de llamar reti(regresar de una interrupción). Desafortunadamente, no hay manera de utilizar registros de avr-gcc C directamente (obviamente se puede en el montaje), sin embargo, lo que puede hacer es variables se unen a registros específicos con los register+ asmpalabras clave, como esto: register uint8_t counter asm("r3");. Si hace eso, para el ISR sabrá qué registros está utilizando en el ISR. El problema es que no hay forma de generar pushypoppara guardar los registros usados ​​sin ensamblaje en línea (ver punto 1). Para asegurarse de tener que guardar menos registros, también puede vincular todas las variables que no son ISR a registros específicos, sin embargo, no tiene ningún problema de que gcc use registros para barajar datos hacia y desde la memoria. Esto significa que, a menos que mire el desmontaje, no sabrá qué registros utiliza su código principal. Entonces, si está considerando ISR_NAKED, también podría escribir el ISR en conjunto.
angelatlarge
fuente
Gracias, ¿entonces mi código C hace la gran sobrecarga? ¿Sería más rápido si lo escribo en ensamblador? Sobre la segunda cosa, estaba al tanto de eso.
arminb 01 de
@arminb: No sé lo suficiente para responder esa pregunta. Mi suposición sería que el compilador es razonablemente inteligente y hace lo que hace por una razón. Una vez dicho esto, estoy seguro de que si pasaste un tiempo con el ensamblaje, podrías exprimir algunos ciclos de reloj más de tu rutina ISR.
angelatlarge 01 de
1
Creo que si desea la respuesta más rápida, generalmente evita las interrupciones y sondea los pines en un circuito cerrado.
Kaz
1
Con objetivos específicos en mente, es posible optimizar el código utilizando el ensamblador. Por ejemplo, el compilador comienza empujando todos los registros utilizados en la pila, luego comienza a ejecutar la rutina real. Si tiene cosas críticas de tiempo, puede mover parte del empuje hacia atrás y tirar cosas críticas de tiempo hacia adelante. Entonces, sí, puede optimizar utilizando el ensamblador, pero el compilador en sí mismo también es bastante inteligente. Me gusta usar el código compilado como punto de inicio y modificarlo manualmente para mis requisitos específicos.
jippie 01 de
1
Muy buena respuesta. Agregaré que el compilador agrega todo tipo de almacenamiento y restauración de registros para satisfacer las necesidades de la mayoría de los usuarios. Es posible escribir su propio manejador de interrupciones, si no necesita toda esa sobrecarga. Algunos compiladores pueden incluso ofrecer una opción para crear una interrupción "rápida", dejando gran parte de la "contabilidad" al programador. No necesariamente iría directamente a un ciclo cerrado sin ISR si no pudiera cumplir con mi horario. Primero consideraría un uC más rápido, y luego pensaría si podría usar algún tipo de hardware de pegamento, como un pestillo y RTC.
Scott Seidman
2

Hay muchos registros PUSH'ing y POP'ing para apilar antes de que comience su ISR real, que está por encima de los 5 ciclos de reloj que menciona. Eche un vistazo al desmontaje del código generado.

Dependiendo de la cadena de herramientas que use, deshacerse del ensamblaje que nos enumera se realiza de varias maneras. Trabajo en la línea de comandos de Linux y este es el comando que uso (requiere el archivo .elf como entrada):

avr-objdump -C -d $(src).elf

Eche un vistazo a un fragmento de código que usé recientemente para un ATtiny. Así es como se ve el código C:

ISR( INT0_vect ) {
        uint8_t myTIFR  = TIFR;
        uint8_t myTCNT1 = TCNT1;

Y este es el código de ensamblaje generado para ello:

00000056 <INT0_vect>:
  56:   1f 92           push    r1
  58:   0f 92           push    r0
  5a:   0f b6           in      r0, SREG        ; 0x3f
  5c:   0f 92           push    r0
  5e:   11 24           eor     r1, r1
  60:   2f 93           push    r18
  62:   3f 93           push    r19
  64:   4f 93           push    r20
  66:   8f 93           push    r24
  68:   9f 93           push    r25
  6a:   af 93           push    r26
  6c:   bf 93           push    r27
  6e:   48 b7           in      r20, TIFR       ; uint8_t myTIFR  = TIFR;
  70:   2f b5           in      r18, TCNT1      ; uint8_t myTCNT1 = TCNT1;

Para ser honesto, mi rutina C usa un par de variables más que causan todos estos empujones y estallidos, pero entiendes la idea.

La carga de una variable de 32 bits se ve así:

  ec:   80 91 78 00     lds     r24, 0x0078
  f0:   90 91 79 00     lds     r25, 0x0079
  f4:   a0 91 7a 00     lds     r26, 0x007A
  f8:   b0 91 7b 00     lds     r27, 0x007B

El aumento de una variable de 32 bits en 1 se ve así:

  5e:   11 24           eor     r1, r1
  d6:   01 96           adiw    r24, 0x01       ; 1
  d8:   a1 1d           adc     r26, r1
  da:   b1 1d           adc     r27, r1

El almacenamiento de una variable de 32 bits se ve así:

  dc:   80 93 78 00     sts     0x0078, r24
  e0:   90 93 79 00     sts     0x0079, r25
  e4:   a0 93 7a 00     sts     0x007A, r26
  e8:   b0 93 7b 00     sts     0x007B, r27

Luego, por supuesto, debe reventar los valores anteriores una vez que abandona el ISR:

 126:   bf 91           pop     r27
 128:   af 91           pop     r26
 12a:   9f 91           pop     r25
 12c:   8f 91           pop     r24
 12e:   4f 91           pop     r20
 130:   3f 91           pop     r19
 132:   2f 91           pop     r18
 134:   0f 90           pop     r0
 136:   0f be           out     SREG, r0        ; 0x3f
 138:   0f 90           pop     r0
 13a:   1f 90           pop     r1
 13c:   18 95           reti

De acuerdo con el resumen de instrucciones en la hoja de datos, la mayoría de las instrucciones son de ciclo único, pero PUSH y POP son ciclos dobles. ¿Tienes idea de dónde viene la demora?

jippie
fuente
¡Gracias por tu respuesta! Ahora estoy al tanto de lo que está sucediendo. Especialmente gracias por el comando avr-objdump -C -d $(src).elf!
arminb 01 de
Tómese unos minutos para comprender las instrucciones de ensamblaje que se avr-objdumpescuchan, se explican brevemente en la hoja de datos en Resumen de instrucciones. En mi opinión, es una buena práctica familiarizarse con los mnemónicos, ya que puede ayudar mucho al depurar su código C.
jippie 01 de
De hecho, es útil tener el desmontaje como parte de su valor predeterminado Makefile: por lo tanto, cada vez que construye su proyecto, también se desmonta automáticamente para que no tenga que pensarlo o recordar cómo hacerlo manualmente.
angelatlarge 01 de