La rutina del 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 de destino es un ATmega168 con una frecuencia de reloj de 20MHz. Para detectar cambios lógicos utilizo 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 tasa no puedo capturar correctamente.

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

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 capturada se encuentra en pinc . Para localizarlo tengo un valor de marca de tiempo largo de 4 bytes.

En la hoja de datos, leí que la rutina de servicio de interrupción requiere 5 relojes para saltar y 5 relojes para volver al procedimiento principal. Supongo que cada comando en mi ISR() está tomando 1 reloj para ejecutarse; Entonces, en suma, debería haber una sobrecarga de 5 + 5 + 5 = 15 relojes. La duración de un reloj debe estar de acuerdo con la frecuencia de reloj de 20MHz 1/20000000 = 0.00000005 = 50 ns . La sobrecarga total en segundos debe ser entonces: 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?

    
pregunta arminb

2 respuestas

10

Hay un par de problemas:

  • No todos los comandos del AVR toman 1 reloj para ejecutarse: si mira la parte posterior de la hoja de datos, tiene la cantidad de relojes que toma para que se ejecute cada instrucción. Entonces, por ejemplo, AND es una instrucción de un reloj, MUL (multiplicar) toma dos relojes, mientras que LPM (cargar la memoria del programa) es tres, y CALL es 4. Entonces, con respecto a la instrucción ejecució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 desensamblado, encontrará que además de las instrucciones jump y RETI , el compilador agrega todo tipo de código, lo que también lleva tiempo. Por ejemplo, es posible que necesite variables locales que se crean en la pila y que deben quitarse, etc. Lo mejor que puede hacer para ver lo que realmente está sucediendo es mirar el desensamblaje.
  • Por último, recuerda que mientras estás en tu rutina de ISR, tus 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 necesario para reparar su interrupción. Para que quede claro, una vez que calculas el tiempo que tarda tu ISR en ejecutarse, esto te da un límite superior de la rapidez con la que puedes capturar una señal . Si necesita capturar dos señales, entonces comienza a tener problemas. Para ser demasiado detallado al respecto, considere el siguiente escenario:

Sixeseltiempoquetardaenatendersuinterrupción,laseñalBnuncasecapturará.

SitomamossucódigoISR,lopegamosenunarutinaISR(youséISR(PCINT0_vect)),declaramostodaslasvariablesvolatile,ycompilamosparaATmega168P,elcódigodesensambladotieneelsiguienteaspecto(consultelarespuestade@jippleparamásinformación)antesdellegaralcódigoque"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, PUSH x 5, in x 1, clr x 1. No es tan malo como los vars de 32 bits de jipple, pero todavía no es nada.

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

push r24

Está ahí porque todo pasa por r24 : tu pinc está cargado allí antes de que entre en la memoria, etc. Por lo tanto, primero debes tener eso. __SREG__ se carga en r0 y luego se presiona: si esto pudiera pasar por r24 , entonces podría ahorrarse un PUSH

Algunas soluciones posibles:

  • Use un circuito de sondeo 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 ensamblaje: de esta manera puede optimizar el uso del registro de tal manera que se necesite guardar el menor número posible durante el ISR.
  • Declare sus rutinas de ISR ISR_NAKED , a pesar de ello de una solución de arenque rojo. Cuando declara rutinas ISR ISR_NAKED , gcc no genera prólogo / código de epílogo, y usted es responsable de guardar cualquier registro que modifique su código, así como de llamar a reti (retorno de una interrupción). Desafortunadamente, no hay forma de usar registros en avr-gcc C directamente (obviamente puede hacerlo en ensamblaje), sin embargo, lo que puede hacer es vincular variables a registros específicos con las palabras clave register + asm , como esto: register uint8_t counter asm("r3"); . Si lo hace, para el ISR sabrá qué registros está utilizando en el ISR. El problema entonces es que no hay manera de generar push y pop para guardar los registros usados sin ensamblaje en línea (véase el 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 se encuentra con un problema que gcc usa registros para mezclar 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 puede escribir el ISR en ensamblaje.
respondido por el angelatlarge
2

Hay una gran cantidad de registros PUSH'ing y POP'ing que se están acumulando antes de que comience su ISR real, que está 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, descarte el ensamblaje que nos enumera 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 utilicé 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 él:

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 de C usa un par de variables más que causan todos estos empujones y pops, 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, debes hacer estallar los valores antiguos una vez que dejes 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 de ciclo dual. ¿Tienes la idea de dónde viene el retraso?

    
respondido por el jippie

Lea otras preguntas en las etiquetas