La routine de service d'interruption AVR ne s'exécute pas aussi rapidement que prévu (surcharge d'instruction?)

8

Je développe un petit analyseur logique avec 7 entrées. Mon appareil cible est un ATmega168avec une fréquence d'horloge de 20 MHz. Pour détecter les changements logiques, j'utilise des interruptions de changement de broche. Maintenant, j'essaie de trouver le taux d'échantillonnage le plus bas que je puisse détecter ces changements de broches. J'ai déterminé une valeur minimale de 5,6 µs (178,5 kHz). Chaque signal en dessous de ce taux, je ne peux pas capturer correctement.

Mon code est écrit en C (avr-gcc). Ma routine ressemble à:

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

Mon changement de signal capturé est situé à pinc. Pour le localiser, j'ai une valeur d'horodatage longue de 4 octets.

Dans la fiche technique, j'ai lu que la routine de service d'interruption prend 5 horloges pour sauter et 5 horloges pour revenir à la procédure principale. Je suppose que chaque commande de mon ISR()prend 1 horloge à exécuter; Donc, en somme, il devrait y avoir une surcharge d' 5 + 5 + 5 = 15horloges. La durée d'une horloge doit être fonction de la fréquence d'horloge de 20 MHz 1/20000000 = 0.00000005 = 50 ns. Le total des coûts indirects en quelques secondes devrait être alors: 15 * 50 ns = 750 ns = 0.75 µs. Maintenant, je ne comprends pas pourquoi je ne peux pas capturer quoi que ce soit en dessous de 5,6 µs. Quelqu'un peut-il expliquer ce qui se passe?

arminb
la source
peut-être 5 horloges pour distribuer le code ISR, qui comprend la sauvegarde du contexte et la restauration de l'épilogue / prologue que vous ne voyez pas dans la source C. De plus, que fait le matériel lorsque l'interruption se déclenche? Est-ce dans un état de sommeil. (Je ne connais pas l'AVR, mais en général, interrompre le traitement de certains états peut prendre plus de temps.)
Kaz
@arminb Voir aussi cette question pour plus d'idées sur la façon de capturer des événements externes avec une plus grande précision. [Cette note] (www.atmel.com/Images/doc2505.pdf) pourrait également être intéressante.
angelatlarge

Réponses:

10

Il y a quelques problèmes:

  • Toutes les commandes AVR ne prennent pas 1 horloge pour être exécutées: si vous regardez à l'arrière de la fiche technique, elle a le nombre d'horloges nécessaires pour chaque instruction à exécuter. Ainsi, par exemple, ANDest une instruction à une horloge, MUL(multiplier) prend deux horloges, tandis que LPM(charger la mémoire du programme) est de trois, et CALLest de 4. Donc, en ce qui concerne l'exécution de l'instruction, cela dépend vraiment de l'instruction.
  • 5 horloges pour sauter et 5 horloges pour revenir peuvent être trompeuses. Si vous regardez votre code désassemblé, vous constaterez qu'en plus du saut et des RETIinstructions, le compilateur ajoute toutes sortes d'autres codes, ce qui prend également du temps. Par exemple, vous pourriez avoir besoin de variables locales qui sont créées sur la pile et doivent être sautées, etc. La meilleure chose à faire pour voir ce qui se passe réellement est de regarder le désassemblage.
  • Enfin, rappelez-vous que pendant que vous êtes dans votre routine ISR, vos interruptions ne se déclenchent pas. Cela signifie que vous ne pourrez pas obtenir le type de performances que vous recherchez de votre analyseur logique, à moins que vous ne sachiez que vos niveaux de signal changent à des intervalles plus longs qu'il n'en faut pour entretenir votre interruption. Pour être clair, une fois que vous avez calculé le temps nécessaire à l'exécution de votre ISR, cela vous donne une limite supérieure de la vitesse à laquelle vous pouvez capturer un signal . Si vous devez capturer deux signaux, vous commencez à rencontrer des problèmes. Pour être trop détaillé à ce sujet, considérez le scénario suivant:

entrez la description de l'image ici

Si xc'est le temps qu'il faut pour réparer votre interruption, le signal B ne sera jamais capturé.


Si nous prenons votre code ISR, le collons dans une routine ISR (j'ai utilisé ISR(PCINT0_vect)), déclarons toutes les variables volatileet compilons pour ATmega168P, le code désassemblé se présente comme suit (voir la réponse de @ jipple pour plus d'informations) avant d'arriver au code qui "fait quelque chose" ; en d'autres termes, le prologue de votre ISR est le suivant:

  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

donc, PUSHx 5, inx 1, clrx 1. Pas aussi mauvais que les vars 32 bits de jipple, mais toujours pas rien.

Une partie de cela est nécessaire (développez la discussion dans les commentaires). De toute évidence, puisque la routine ISR peut se produire à tout moment, elle doit préconfigurer les registres qu'elle utilise, sauf si vous savez qu'aucun code où une interruption peut se produire utilise le même registre que votre routine d'interruption. Par exemple, la ligne suivante dans l'ISR démonté:

push r24

Est là parce que tout passe r24: votre pincest chargé là-bas avant d'être mis en mémoire, etc. Donc, vous devez d'abord l'avoir. __SREG__est chargé dans r0puis poussé: si cela peut passer, r24vous pouvez vous épargner unPUSH


Quelques solutions possibles:

  • Utilisez une boucle de sondage serrée comme suggéré par Kaz dans les commentaires. Ce sera probablement la solution la plus rapide, que vous écriviez la boucle en C ou l'assemblage.
  • Écrivez votre ISR en assembleur: de cette façon, vous pouvez optimiser l'utilisation du registre de telle sorte que le plus petit nombre d'entre eux doit être enregistré pendant l'ISR.
  • Déclarez vos routines ISR ISR_NAKED , bien que cela se révèle être davantage une solution de hareng rouge. Lorsque vous déclarez des routines ISR ISR_NAKED, gcc ne génère pas de code prologue / épilogue, et vous êtes responsable de sauvegarder tous les registres modifiés par votre code, ainsi que d'appeler reti(retour d'une interruption). Malheureusement, il n'y a aucun moyen d'utiliser des registres à C gcc-AVR directement (vous pouvez évidemment dans l' assemblage), cependant, ce que vous pouvez faire est de lier les variables à registres spécifiques avec les register+ asmmots - clés, comme celui - ci: register uint8_t counter asm("r3");. Si vous faites cela, pour l'ISR, vous saurez quels registres vous utilisez dans l'ISR. Le problème est alors qu'il n'y a aucun moyen de générer pushetpoppour sauvegarder les registres utilisés sans assemblage en ligne (cf. point 1). Pour éviter d'avoir à enregistrer moins de registres, vous pouvez également lier toutes les variables non ISR à des registres spécifiques, cependant, vous ne rencontrez pas de problème lorsque gcc utilise des registres pour mélanger les données vers et depuis la mémoire. Cela signifie qu'à moins de regarder le démontage, vous ne saurez pas quels registres votre code principal utilise. Donc, si vous envisagez ISR_NAKED, vous pourriez aussi bien écrire l'ISR dans l'assemblage.
angelatlarge
la source
Merci, donc mon code C fait l'énorme surcharge? Serait-ce plus rapide si je l'écris dans l'assembleur? À propos de la deuxième chose, j'en étais conscient.
arminb
@arminb: Je n'en sais pas assez pour répondre à cette question. Mon hypothèse serait que le compilateur est raisonnablement intelligent et qu'il fait ce qu'il fait pour une raison. Cela dit, je suis sûr que si vous passiez un peu de temps à l'assemblage, vous pourriez extraire quelques cycles d'horloge supplémentaires de votre routine ISR.
angelatlarge
1
Je pense que si vous voulez la réponse la plus rapide, vous évitez généralement les interruptions et interrogez les broches en boucle serrée.
Kaz
1
Avec des objectifs spécifiques à l'esprit, il est possible d'optimiser le code en utilisant l'assembleur. Par exemple, le compilateur commence par pousser tous les registres utilisés sur la pile, puis commence à exécuter la routine réelle. Si vous avez des choses critiques sur le timing, vous pouvez déplacer une partie de la poussée et tirer des choses critiques sur le temps. Donc oui, vous pouvez optimiser en utilisant l'assembleur, mais le compilateur en lui-même est assez intelligent aussi. J'aime utiliser le code compilé comme point de départ et le modifier manuellement pour mes besoins spécifiques.
jippie
1
Réponse vraiment sympa. J'ajouterai que le compilateur ajoute toutes sortes de stockage et de restauration de registres pour répondre aux besoins de la plupart des utilisateurs. Il est possible d'écrire votre propre gestionnaire d'interruption à nu - si vous n'avez pas besoin de tout cela. Certains compilateurs peuvent même offrir une option pour créer une interruption "rapide", laissant une grande partie de la "comptabilité" au programmeur. Je n'irais pas nécessairement droit à une boucle serrée sans ISR si je ne pouvais pas respecter mon calendrier. D'abord, j'envisagerais une uC plus rapide, puis je déterminerais si je pouvais utiliser une sorte de matériel de colle, comme un verrou et un RTC.
Scott Seidman
2

Il y a beaucoup de registres PUSH'ing et POP'ing à empiler avant le démarrage de votre ISR, c'est-à-dire en plus des 5 cycles d'horloge que vous mentionnez. Jetez un oeil au démontage du code généré.

En fonction de la chaîne d'outils que vous utilisez, le vidage de l'assemblage nous énumère de différentes manières. Je travaille sur la ligne de commande Linux et voici la commande que j'utilise (elle nécessite le fichier .elf en entrée):

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

Jetez un oeil à un fragment de code que j'ai récemment utilisé pour un ATtiny. Voici à quoi ressemble le code C:

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

Et voici le code d'assembly généré pour cela:

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;

Pour être honnête, ma routine C utilise quelques variables supplémentaires qui provoquent tous ces push'es et pop, mais vous avez l'idée.

Le chargement d'une variable 32 bits ressemble à ceci:

  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

L'augmentation d'une variable 32 bits de 1 ressemble à ceci:

  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

Le stockage d'une variable 32 bits ressemble à ceci:

  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

Ensuite, bien sûr, vous devez faire apparaître les anciennes valeurs une fois que vous quittez l'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

Selon le résumé des instructions de la fiche technique, la plupart des instructions sont à cycle unique, mais PUSH et POP sont à cycle double. Vous avez l'idée d'où vient le retard?

jippie
la source
Merci pour votre réponse! Maintenant, je sais ce qui se passe. Merci surtout pour la commande avr-objdump -C -d $(src).elf!
arminb
Prenez quelques instants pour comprendre les instructions de montage qui avr-objdumpcrachent, elles sont brièvement expliquées dans la fiche technique sous Résumé des instructions. À mon avis, c'est une bonne pratique de se familiariser avec les mnémoniques car cela peut beaucoup aider lors du débogage de votre code C.
jippie
En fait, le démontage est utile d'avoir comme partie de votre valeur par défaut Makefile: donc chaque fois que vous construisez votre projet, il est également démonté automatiquement afin que vous n'ayez pas à y penser ou à vous rappeler comment le faire manuellement.
angelatlarge