La routine del servizio di interruzione AVR non viene eseguita velocemente come previsto (istruzioni generali?)


8

Sto sviluppando un piccolo analizzatore logico con 7 ingressi. Il mio dispositivo di destinazione è un ATmega168con una frequenza di clock di 20 MHz. Per rilevare le modifiche logiche uso gli interrupt di modifica pin. Ora sto cercando di scoprire la frequenza di campionamento più bassa che posso rilevare queste modifiche ai pin. Ho determinato un valore di minimo 5,6 µs (178,5 kHz). Ogni segnale al di sotto di questa frequenza non riesco a catturare correttamente.

Il mio codice è scritto in C (avr-gcc). La mia routine è simile a:

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

Il mio cambio di segnale acquisito si trova in pinc. Per localizzarlo ho un valore di timestamp lungo 4 byte.

Nel foglio dati ho letto che la routine di servizio di interruzione impiega 5 orologi per saltare e 5 orologi per tornare alla procedura principale. Suppongo che ogni comando nel mio ISR()stia prendendo 1 clock da eseguire; Quindi in breve dovrebbe esserci un sovraccarico di 5 + 5 + 5 = 15orologi. La durata di un clock dovrebbe essere in base alla frequenza di 20 MHz 1/20000000 = 0.00000005 = 50 ns. L'overhead totale in secondi dovrebbe essere quindi: 15 * 50 ns = 750 ns = 0.75 µs. Ora non capisco perché non riesco a catturare nulla al di sotto di 5,6 µs. Qualcuno può spiegare cosa sta succedendo?


forse 5 orologi per inviare il codice ISR, che include il salvataggio del contesto e il ripristino di epilog / prolog che non si vede nel sorgente C. Inoltre, cosa sta facendo l'hardware quando si interrompe l'interrupt? È in qualche stato di sonno. (Non conosco AVR, ma in generale, l'interruzione dell'elaborazione di alcuni stati può richiedere più tempo.)
Kaz

@arminb Vedi anche questa domanda per ulteriori idee su come acquisire eventi esterni con maggiore precisione. Anche [questa nota] (www.atmel.com/Images/doc2505.pdf) potrebbe essere interessante.
angelatlarge

Risposte:


10

Ci sono un paio di problemi:

  • Non tutti i comandi AVR richiedono 1 orologio per essere eseguito: se si guarda il retro del foglio dati, ha il numero di orologi necessari per ogni istruzione da eseguire. Quindi, per esempio, ANDè un'istruzione a un clock, MUL(moltiplicare) prende due clock, mentre LPM(carica la memoria del programma) è tre, ed CALLè 4. Quindi, rispetto all'esecuzione dell'istruzione, dipende davvero dall'istruzione.
  • 5 orologi per saltare e 5 orologi per tornare possono essere fuorvianti. Se guardi il tuo codice disassemblato, scoprirai che oltre al salto e alle RETIistruzioni, il compilatore aggiunge ogni sorta di altro codice, il che richiede anche tempo. Ad esempio, potresti aver bisogno di variabili locali create nello stack e che devono essere spuntate, ecc. La cosa migliore da fare per vedere cosa sta realmente succedendo è guardare lo smontaggio.
  • Infine, ricorda che mentre sei nella routine ISR, i tuoi interrupt non si attivano. Questo significa che non sarai in grado di ottenere il tipo di prestazione che stai cercando dal tuo analizzatore logico, a meno che tu non sappia che i tuoi livelli di segnale cambiano ad intervalli più lunghi di quanto ci vuole per servire l'interruzione. Per essere chiari, una volta calcolato il tempo necessario per l'esecuzione del tuo ISR, questo ti dà un limite superiore di quanto velocemente puoi catturare un segnale . Se devi catturare due segnali, allora inizi a correre nei guai. Per essere eccessivamente dettagliati al riguardo, considera il seguente scenario:

inserisci qui la descrizione dell'immagine

Se xè il tempo necessario per eseguire l'interruzione, il segnale B non verrà mai catturato.


Se prendiamo il tuo codice ISR, lo inseriamo in una routine ISR (ho usato ISR(PCINT0_vect)), dichiariamo tutte le variabili volatilee compiliamo per ATmega168P, il codice disassemblato appare come segue (vedi la risposta di @ jipple per maggiori informazioni) prima di arrivare al codice che "fa qualcosa" ; in altre parole il prologo al tuo ISR è il seguente:

  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

così, PUSHx 5, inx 1, clrx 1. Non male come i jars a 32 bit di jipple, ma ancora niente.

Alcuni di questi sono necessari (espandere la discussione nei commenti). Ovviamente, poiché la routine ISR può verificarsi in qualsiasi momento, deve preconfigurare i registri che utilizza, a meno che non si sappia che nessun codice in cui può verificarsi un interrupt utilizza lo stesso registro della routine di interrupt. Ad esempio la seguente riga nell'ISR disassemblato:

push r24

C'è perché tutto passa r24: il tuo pincviene caricato lì prima che vada in memoria, ecc. Quindi devi prima averlo. __SREG__viene caricato r0e poi spinto: se ciò potesse passare, r24potresti salvarti aPUSH


Alcune possibili soluzioni:

  • Utilizzare un ciclo di polling stretto come suggerito da Kaz nei commenti. Questa sarà probabilmente la soluzione più veloce, sia che tu scriva il ciclo in C o assembly.
  • Scrivi il tuo ISR in assemblea: in questo modo puoi ottimizzare l'utilizzo del registro in modo tale che sia necessario salvarne il minor numero durante l'ISR.
  • Dichiara le tue procedure ISR ISR_NAKED , anche se questo risulta essere più una soluzione di aringhe rosse. Quando dichiari le routine ISR ISR_NAKED, gcc non genera il codice prologo / epilogo e sei responsabile del salvataggio di tutti i registri modificati dal tuo codice, nonché della chiamata reti(ritorno da un interrupt). Purtroppo, non c'è modo di usare i registri in avr-gcc C direttamente (ovviamente è possibile in assemblea), tuttavia, che cosa si può fare è variabili di bind a registri specifici con i register+ asmparole chiave, come questo: register uint8_t counter asm("r3");. Se lo fai, per l'ISR saprai quali registri stai utilizzando nell'ISR. Il problema quindi è che non c'è modo di generare pushepopper salvare i registri usati senza assembly inline (vedi punto 1). Per assicurarti di dover salvare un minor numero di registri, puoi anche associare tutte le variabili non ISR anche a registri specifici, tuttavia, non ti imbatti in un problema che gcc utilizza i registri per spostare i dati dalla e alla memoria. Ciò significa che, a meno che non si osservi lo smontaggio, non si saprà quali registri utilizza il codice principale. Quindi, se stai considerando ISR_NAKED, potresti anche scrivere l'ISR in assemblea.

Grazie, quindi il mio codice C rende il sovraccarico enorme? Sarebbe più veloce se lo scrivessi in assembler? Per quanto riguarda la seconda cosa, ne ero consapevole.
Arminb

@arminb: non ne so abbastanza per rispondere a questa domanda. La mia ipotesi sarebbe che il compilatore è ragionevolmente intelligente e fa quello che fa per una ragione. Detto questo, sono sicuro che se passassi un po 'di tempo con il montaggio, potresti spremere qualche altro ciclo di clock dalla tua routine ISR.
angelatlarge

1
Penso che se si desidera la risposta più veloce, generalmente si evitano gli interrupt e si esegue il polling dei pin in un circuito stretto.
Kaz

1
Con in mente obiettivi specifici, è possibile ottimizzare il codice utilizzando assembler. Ad esempio il compilatore inizia con l'inserimento di tutti i registri utilizzati nello stack, quindi inizia l'esecuzione della routine effettiva. Se si dispone di elementi critici per il cronometraggio, è possibile spostare alcune delle spinte indietro e spostare in avanti le cose critiche. Quindi sì, puoi ottimizzare utilizzando l'assemblatore, ma il compilatore in sé è anche abbastanza intelligente. Mi piace usare il codice compilato come punto di partenza e modificarlo manualmente per i miei requisiti specifici.
jippie

1
Davvero una bella risposta. Aggiungerò che il compilatore aggiunge tutti i tipi di archiviazione e ripristino dei registri per soddisfare le esigenze della maggior parte degli utenti. È possibile scrivere il proprio gestore di interruzioni bare-bone, se non è necessario tutto questo. Alcuni compilatori possono anche offrire un'opzione per creare un interrupt "veloce", lasciando gran parte della "contabilità" al programmatore. Non andrei necessariamente a un ciclo stretto senza ISR se non potessi rispettare il mio programma. Prima prenderei in considerazione un uC più veloce, e poi capirei se potessi usare una sorta di hardware per colla, come un fermo e RTC.
Scott Seidman,

2

Ci sono molti registri PUSH e POP da impilare prima che inizi il tuo ISR effettivo, che è in cima ai 5 cicli di clock che menzioni. Dai un'occhiata allo smontaggio del codice generato.

A seconda della toolchain che usi, scarica l'assembly che ci elenca fatto in vari modi. Lavoro sulla riga di comando di Linux e questo è il comando che uso (richiede il file .elf come input):

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

Dai un'occhiata a un codice sniplet che ho usato di recente per un ATtiny. Ecco come appare il codice C:

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

E questo è il codice assembly generato per questo:

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;

Ad essere sincero, la mia routine in C usa un paio di variabili in più che causano tutti questi push e pop, ma hai l'idea.

Il caricamento di una variabile a 32 bit è simile al seguente:

  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'aumento di una variabile a 32 bit di 1 è simile al seguente:

  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

La memorizzazione di una variabile a 32 bit è simile alla seguente:

  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

Quindi ovviamente devi far apparire i vecchi valori una volta usciti dall'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

Secondo il riepilogo delle istruzioni nel foglio dati, la maggior parte delle istruzioni sono a ciclo singolo, ma PUSH e POP sono a doppio ciclo. Ti viene l'idea da dove viene il ritardo?


Grazie per la tua risposta! Ora sono consapevole di ciò che sta accadendo. Soprattutto grazie per il comando avr-objdump -C -d $(src).elf!
Arminb,

Prenditi qualche istante per capire le istruzioni di assemblaggio che avr-objdumpfuoriescono, sono brevemente spiegate nel foglio dati in Riepilogo istruzioni. Secondo me è buona pratica familiarizzare con la mnemonica in quanto può aiutare molto durante il debug del codice C.
jippie

In effetti, il disassemblaggio è utile come parte del tuo default Makefile: così ogni volta che costruisci il tuo progetto viene anche smontato automagicamente, quindi non devi pensarci o ricordare come farlo manualmente.
angelatlarge
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.