Domanda:
Come posso stimare la velocità di questa sezione di codice per questo microcontrollore?
ty_1917
2020-01-09 17:00:38 UTC
view on stackexchange narkive permalink

Sto usando un ATmega328P per leggere lo stato di un ingresso digitale utilizzando la seguente sezione di codice scritta in C (ci possono essere modi alternativi, ma questo è un esempio). val è un tipo di variabile uint8_t e memorizza lo stato del pin di ingresso digitale:

Ecco la parte del codice:

  if ((PIND & (1 << PIND6)) == (1 << PIND6)) {
    val = 1;
} altro {
    val = 0;
}
 

Ho impostato l'orologio come segue:

  #define F_CPU 16000000UL
 

Immagina che l'ingresso digitale sia un treno di impulsi ON / OFF con un ciclo di lavoro del 50% e che ne aumentiamo gradualmente la frequenza. Ad un certo punto a una certa frequenza il codice precedente non dovrebbe essere in grado di acquisire correttamente lo stato dell'ingresso digitale.

  1. Come possiamo stimare approssimativamente la frequenza di impulsi massima che il codice sopra può gestire per leggere lo stato corretto?
  2. Dovremmo trovare quanti cicli di clock utilizza e moltiplicarli per la frequenza di clock?
  3. E se sì, come posso farlo in pratica?

      int main (void) {
    
        DDRD = B0100000;
        DDRD | = 1<<5;
    
        while (1) {
    
            dati lunghi senza segno = 0;
            uint8_t val;
    
            for (int i = 0; i<25; i ++) {
                dati << = 1;
                PORTD & = ~ (1 << 5);
                _delay_us (2);
                PORTD | = (1 << 5);
                _delay_us (2);
    
                if ((PIND & (1 << PIND6)) == (1 << PIND6)) {
                    val = 1;
                }
                altro {
                    val = 0;
                }
    
                dati | = val;
            }
    
            // Il resto del codice
    
        }
    }
    
Mi aspetto che questo catturi lo stato dell'ingresso digitale correttamente, ogni volta che viene eseguito, a qualsiasi frequenza.La domanda è: quanto spesso puoi eseguire questo codice?
Puoi scrivere il tuo codice semplicemente come `val = ((PIND & (1 << PIND6)) == (1 << PIND6));`, o `val = ((PIND >> PIND6) & 1);` (e probabilmente un sacco di altri modi).È necessario esaminare l'output del compilatore per vedere se si ottiene un codice assembly diverso in ogni caso e quale è il più veloce.
Hai provato un profiler hardware per * misurare * invece di * stimare *?
Il tuo nuovo codice ha lo stesso problema del vecchio codice: - `val` e` data` non vengono utilizzati, quindi verranno ottimizzati.
Cinque risposte:
Swedgin
2020-01-09 17:08:36 UTC
view on stackexchange narkive permalink
  1. Poiché lo snippet di codice che ti interessa non è grande, potresti disassemblare il codice compilato, esaminare tutte le istruzioni di assemblaggio e contare quanti cicli hanno bisogno. Puoi trovare il numero di cicli per ciascuna istruzione nella scheda tecnica.

  2. Se hai un oscilloscopio, puoi attivare un pin prima dell'istruzione if e disattivarlo dopo lo snippet di codice. (Usando la manipolazione diretta della porta PORTB , non la funzione della libreria Arduino) Con uno scope puoi vedere quanto tempo ci vuole per eseguire il codice.

  3. Usa la funzione micros () nella libreria Arduino. Posizionane uno prima e dopo lo snippet di codice. Tuttavia qui avrai un paio di microsecondi di sovraccarico poiché anche "micros ()" deve essere eseguito.

  4. Utilizza un debugger o un simulatore hardware in grado di contare i cicli. Metti un punto di interruzione sulla prima istruzione dello snippet di codice e uno sull'istruzione dopo lo snippet. delta_t = cycles / clock_freq (in linea con la risposta di Oldfart)

C'è anche un sovraccarico quando si attivano e disattivano i perni.Il sovraccarico è particolarmente negativo quando si passa attraverso la libreria Arduino.Penso che preferirei micros ().
@Dampmaskin poiché OP ha usato `PIND6`, ho pensato che non userà la libreria Arduino e userà solo` PORTB6`.Usando questo, l'overhead è molto più piccolo di `micros ()`
Sto usando Atmel Studio
@ty_1917 Non importa quale editor usi, era una risposta all'altro commento.Il fatto è che le funzioni della libreria Arduino hanno un sacco di overhead.Quindi, quando si attiva / disattiva l'impostazione dei bit / cancella `PORTB`, sarà molto più veloce che attivare la funzione di libreria Arduino.Mi fido del mio ambito più che usare le funzioni `micros ()`.
Penso che il punto fosse che l'overhead non è solo la libreria ma l'I / O in generale (forse la parte atmel / microchip in particolare è veloce) richiede un certo numero di clock per fare qualcosa poiché non c'è motivo che funzionino alvelocità CPU core.così mentre puoi accorciare il numero di istruzioni della CPU, ridurre i rami, ecc., potresti essere vincolato all'I / O.Ma questo può essere determinato sperimentalmente.capendo anche che probabilmente non per questo core, ma l'allineamento del codice e altri fattori (non parlando di interrupt) possono far sì che lo stesso codice macchina non esegua lo stesso sullo stesso mcu ogni volta.
@Dampmaskin in modo da misurare il tempo tra le modifiche dei pin senza codice di test per ottenere l'overhead, quindi sottrarlo dal risultato.
Una cosa a cui prestare attenzione è il compilatore che ottimizza il codice.Nell'esempio fornito, "val" non viene utilizzato, quindi non viene generato alcun codice per impostarlo!
Si prega di vedere la mia modifica.Ora potrei caricare la parte più grande del codice.Dove e come dichiarereste val e data per una migliore efficienza?Fuori dal ciclo while e volatile?
@old_timer non ci sono problemi di questo tipo su AVR: le scritture sulle porte richiedono un ciclo (se è necessario leggere, mascherare e scrivere sono tre) e premere immediatamente.
yar
2020-01-10 06:16:27 UTC
view on stackexchange narkive permalink

Facciamolo!

Supponiamo di avere il codice

  int main (void)
{
    volatile uint8_t val = 0;

    mentre (1)
    {
        if ((PIND & (1 << PIND6)) == (1 << PIND6)) {
            val = 1;
        } altro {
            val = 0;
        }
    }
}
 

Supponiamo di utilizzare AVR GCC con flag di ottimizzazione -O1, quindi lo smontaggio della sezione pertinente sarà simile a questo:

  val = 1;
00000046 LDI R24,0x01 Carico immediato, 1 ciclo di clock
    if ((PIND & (1 << PIND6)) == (1 << PIND6)) {
00000047 SBIS 0x09,6 Salta se il bit nel registro I / O è impostato, 1/2 ciclo di clock
00000048 RJMP PC + 0x0003 Salto relativo, 2 cicli di clock
        val = 1;
00000049 STD Y + 1, R24 Memoria indiretta con spostamento, 2 cicli di clock
0000004A RJMP PC-0x0003 Salto relativo, 2 cicli di clock
        val = 0;
0000004B STD Y + 1, R1 Memoria indiretta con spostamento, 2 cicli di clock
0000004C RJMP PC-0x0005 Salto relativo, 2 cicli di clock
 

Ho aggiunto i commenti con il numero di cicli di clock basati sulla scheda tecnica a pagina 281ff. Tieni presente che SBIS può richiedere 1 o 2 cicli a seconda che l'istruzione successiva venga saltata o meno.

Quindi vediamo che il ramo if (righe 0x47, 0x49, 0x4A) richiede 6 cicli di clock e il ramo else (righe 0x47, 0x48, 0x4B, 0x4C) richiede 7 cicli di clock.

Ora prendiamo quello più lungo. Con 16MHz ci vogliono (7 / 16e6) secondi, cioè campionate con una frequenza di 16e6 / 7 Hz. Dato che vuoi avere sempre almeno un punto di campionamento basso / alto, devi campionare con> 2x della frequenza del tuo segnale, cioè la tua frequenza del segnale deve essere <16e6 / (7 * 2) Hz che è ~ 1 Mhz.

Ora nota che questo è un esempio puramente virtuale, poiché val è impostato correttamente, ma non è possibile testarlo in alcun modo. Devi in ​​qualche modo produrre val, che aggiungerà cicli di clock extra.

È meglio o è un vantaggio dichiarare volatile uint8_t al di fuori del ciclo while come hai fatto tu?Nel mio codice è stato dichiarato come uint8_t val = 0;all'interno del ciclo while.Quindi dovrei dichiarare come volatile uint8_t e fuori dal ciclo while come hai fatto tu?
Si prega di vedere la mia modifica.Ho aggiunto una parte più grande del codice.Dove e come dichiarereste val e data per una migliore efficienza?Fuori dal ciclo while e volatile?
@ty_1917: Non dovrebbe fare alcuna differenza, yar lo ha dichiarato solo volatile per impedire al compilatore di ottimizzarlo (poiché la variabile non viene letta nel suo esempio minimo).Puoi sempre guardare il codice dell'assembly (o confrontare il codice) per assicurarti.
@Michael Ho aperto una domanda più dettagliata al riguardo qui: https://electronics.stackexchange.com/questions/475476/stuck-with-deciding-the-location-and-the-type-of-variable-declarations-for-this
Questo è un buon esercizio ed è esattamente il modo in cui dovresti farlo: guarda sempre il codice macchina generato.Tuttavia, penso che l'uso di una variabile di risultato `volatile` in questo esempio potrebbe rovinare un po 'l'ottimizzazione, l'unica parte che deve essere volatile è il registro stesso.Dai un'occhiata a questo [esempio] (https://godbolt.org/z/VhmVUh).Sono effettivamente 4 istruzioni con -O2 attivo.
@ty_1917 `volatile` in questo caso serve solo come un modo artificiale per far sì che il compilatore generi qualsiasi codice con l'ottimizzazione abilitata.Se non fosse per "volatile", l'intero codice verrebbe rimosso dall'ottimizzatore.Tuttavia, come indicato dal mio commento sopra, "volatile" ha anche un leggero impatto sulle prestazioni.È meglio fare come ho fatto nel collegamento godbolt sopra e disassemblare una funzione isolata - quindi il compilatore è costretto a generare il codice, perché non sa come / se quella funzione verrà chiamata.
Si noti inoltre che quando si scrive in C, non in asm, le prestazioni di uno snippet possono dipendere dal codice * circostante *, a seconda di come il compilatore lo ottimizza nel codice precedente / successivo.(Anche se mantieni i tipi uguali).E ovviamente dipende dal livello di ottimizzazione, che la maggior parte delle risposte non ha nemmeno menzionato.
Si noti che, oltre al problema sottolineato da Peter Cordes che è molto probabile che l'assembly generato cambi una volta che l'ottimizzatore acquisisce il codice e le cose inline, questo metodo di conteggio dei cicli funziona solo per i microcontrollori che forniscono conteggi di cicli deterministici perIstruzioni.Potrebbe essere il caso dell'AVR, ma non per molti core ARM e certamente non è vero per x86.Oltre all'ovvio (ad esempio, pipeline e pianificazione), devi anche considerare le latenze di memoria, il precaricamento delle istruzioni e altri fattori.Tuttavia, ti darà una stima approssimativa.
@Lundin Se usi volatile, il compilatore è costretto a scriverlo in SRAM, mentre nel tuo esempio la variabile rimane nel registro.In questo esempio artificiale in realtà non fa la differenza, ma ho usato volatile per essere un po 'più vicino alla realtà.
Oldfart
2020-01-10 07:44:50 UTC
view on stackexchange narkive permalink

Normalmente utilizzo il simulatore integrato di Atmel Studio che ha un contatore di cicli nella finestra di stato del processore. Questo è uno screenshot combinato dall'esecuzione del codice:

Enter image description here]

Come puoi vedere, il contatore dei cicli è 18 prima e 22 dopo aver eseguito le due istruzioni.Quindi secondo il simulatore ci vogliono 4 cicli.

Puoi usarlo per scorrere l'intero ciclo.

La frequenza di clock è di 16 MHz o 1 MHz?L'ho impostato a 16MHz ma come nella tua schermata mostra 1MHz in "Processor Status".Occorrono 4 cicli ma qual è la frequenza per ciclo?
Il numero di cicli per eseguire le istruzioni è indipendente dalla frequenza.
Sì, va bene, ma mi chiedo quanto tempo ci vuole per ciclo.Perché ciò determina la frequenza massima di ingresso del treno di impulsi da leggere che chiedo nella mia domanda.
if ((PIND & (1 << PIND6)) == (1 << PIND6)) { val = 1; } altro { val = 0; } questo legge lo stato e se il treno di impulsi in ingresso è 1GHz non può leggere lo stato corretto, ad esempio, perché il clock mcu è molto più lento di 1GHz.Sto cercando di capire qual è la frequenza massima che può leggere correttamente lo stato del pin.
Yar ha già fornito calcoli di esempio su come passare dai cicli della CPU e dalla frequenza di clock alla frequenza di campionamento.Prendi nota anche del suo avvertimento sulla val.
Curd
2020-01-09 17:35:27 UTC
view on stackexchange narkive permalink

Se la velocità è importante per questo codice, è probabilmente degno di nota quanto segue:

Potresti semplicemente scrivere

val = (PIND & (1 << PIND6))! = 0;

o

val = 1 & (PIND >> PIND6);

Immagino che l'ultimo sia più corto / più veloce.

Per quanto riguarda la stima di velocità / tempo: O

  • lascia che il tuo compilatore generi un file assembler con l'elenco (* .lst) o
  • guarda il codice smontato

, quindi cerca e aggiungi i tempi di esecuzione (cicli di clock) delle istruzioni.

Le frequenze che il tuo codice può "gestire" ovviamente dipende da quanto spesso viene chiamato, cioè dipende dalla velocità del codice che circonda / chiama (cioè, quanto spesso lo snippet di codice viene visitato), non solo dal codicesnippet stesso.

_ "Immagino che l'ultima sia più breve / più veloce." _ - Ho provato tutte e 3 le varianti in AVR GCC 5.4.0 con l'ottimizzazione del sistema operativo, e hanno generato codice identico (4 istruzioni / cicli).
Questo è un segno che il compilatore sta facendo un buon lavoro.
Quindi scrivi il codice più leggibile, che probabilmente è `bool val = PIND & (1u << PIND6);`.
@Lundin: ma quella riga dà risultati diversi: se l'input è alto `val` sarà` 1 << PIND6` invece di `1`.
@Curd No. Usa lo standard C `bool` da stdbool.h che si espande in` _Bool`.
Sì, ma ciò richiede la modifica del tipo di "val".All'inizio OP non ha pubblicato il codice che mostra come viene usato `val`, quindi non era un'opzione e ancora ora OP vuole usarlo come` int`.
val nel mio codice deve essere lo stato di un singolo bit.data è la variabile a 32 bit che deve essere riempita da ogni stato val nel ciclo for.
All'inizio non era chiaro quando hai pubblicato solo le prime 3 righe di codice in cui `val` era impostato ma non ancora utilizzato.Potrebbe essere stato, ad es.che in seguito furono usati anche bit più alti in "val".
Bruce Abbott
2020-01-12 15:35:23 UTC
view on stackexchange narkive permalink

Come possiamo stimare approssimativamente la frequenza massima degli impulsi con il codice sopra può gestire per leggere lo stato corretto?

Leggerà sempre lo stato correttamente. La domanda che penso tu stia cercando di porre è qual è la frequenza massima che può "misurare" senza perdere nessun alto o basso.

Dovremmo trovare quanti cicli di clock utilizza e moltiplicarli per frequenza di clock?

Fondamentalmente sì. Il fattore importante è il tempo tra ogni lettura della porta. Tieni presente che, a seconda di ciò che fa il codice macchina, potrebbe non essere sempre lo stesso, quindi dovresti utilizzare il tempo massimo tra le letture.

E se sì, come posso farlo in pratica?

È possibile disassemblare il codice e calcolare quanto tempo impiega ciascuna istruzione, o esaminarla nel simulatore, o eseguire il codice in un ATmega328p effettivo e monitorare l'output fisico (che potrebbe essere ad es. visualizzazione della frequenza su uno schermo LCD).

Notare che i risultati dipendono in modo critico dal codice macchina generato dal compilatore. Con le ottimizzazioni, tutte le variabili che non contribuiscono all'output possono essere ottimizzate e altre modifiche apparentemente banali possono avere un grande effetto sulla quantità di codice generato. Pertanto l'unico modo accurato garantito per testare il codice è nella sua interezza. L'esecuzione di piccoli frammenti di codice isolato può dare un'idea molto fuorviante delle prestazioni dell'applicazione finita.

Ad esempio, ecco l'elenco del codice nella tua domanda: -

  int main (void) {
  86: 89 e1 ldi r24, 0x19; 25
  88: 90 e0 ldi r25, 0x00; 0

        uint8_t val;

        for (int i = 0; i<25; i ++) {
            dati << = 1;
            PORTD & = ~ (1 << 5);
  8a: 5d 98 cbi 0x0b, 5; 11
 // _delay_us (2);
            PORTD | = (1 << 5);
8c: 5d 9a sbi 0x0b, 5; 11
 // _delay_us (2);

            if ((PIND & (1 << PIND6)) == (1 << PIND6)) {
  8e: 29 b1 in r18, 0x09; 9
  90: 01 97 sbiw r24, 0x01; 1

    while (1) {

        uint8_t val;

        for (int i = 0; i<25; i ++) {
  92: d9 f7 brne.-10; 0x8a <main + 0xa>
  94: f8 cf rjmp.-16; 0x86 <main + 0x6>
 

Nessun codice viene generato per val e data e il ciclo interno ha solo 5 istruzioni che richiedono 9 cicli. Con un clock a 16 MHz, il tempo di loop interno è 62,5 ns * 9 = 562,5 ns, che dovrebbe essere in grado di tenere il passo con una frequenza di ingresso di ~ 888 kHz.

Successivamente metto in output data in PORTD, che forza il compilatore a generare il codice per esso: -

  while (1) {

        uint8_t val;

        for (int i = 0; i<25; i ++) {
            dati << = 1;
  90: 88 0f aggiungi r24, r24
  92: 99 1f adc r25, r25
  94: aa 1f adc r26, r26
  96: bb 1f adc r27, r27
            PORTD & = ~ (1 << 5);
  98: 5d 98 cbi 0x0b, 5; 11
 // _delay_us (2);
            PORTD | = (1 << 5);
  9a: 5d 9a sbi 0x0b, 5; 11
 // _delay_us (2);

            if ((PIND & (1 << PIND6)) == (1 << PIND6)) {
  9c: 49 b1 in r20, 0x09; 9
            }
            altro {
                val = 0;
            }

            dati | = val;
  9e: 46 fb bst r20, 6
  a0: 44 27 eo r20, r20
  a2: 40 f9 bld r20, 0
  a4: 84 2b o r24, r20
  a6: 21 50 subi r18, 0x01; 1
  a8: 31 09 sbc r19, r1

    while (1) {

        uint8_t val;

        for (int i = 0; i<25; i ++) {
aa: 91 f7 brne.-28; 0x90 <main + 0x10>
            }

            dati | = val;
        }

    PORTD = (uint8_t) dati;
  ac: 8b b9 out 0x0b, r24; 11

        // Il resto del codice

    }
  ae: ee cf rjmp.-36; 0x8c <main + 0xc>
 

Il ciclo interno ora ha 14 istruzioni che richiedono 17 cicli e la frequenza massima che può seguire accuratamente è quasi dimezzata.

Infine rendo statici i data per costringere il compilatore a salvarli in memoria (cosa che potrebbe essere richiesta per un programma più complesso): -

  while (1) {

        uint8_t val;

        for (int i = 0; i<25; i ++) {
            dati << = 1;
  9a: 40 91 00 01 lds r20, 0x0100; 0x800100 <_edata>
  9e: 50 91 01 01 lds r21, 0x0101; 0x800101 <_edata + 0x1>
  a2: 60 91 02 01 lds r22, 0x0102; 0x800102 <_edata + 0x2>
  a6: 70 91 03 01 lds r23, 0x0103; 0x800103 <_edata + 0x3>
  aa: 44 0f aggiungi r20, r20
  ac: 55 1f adc r21, r21
  ae: 66 1f adc r22, r22
  b0: 77 1f adc r23, r23
  b2: 40 93 00 01 punti 0x0100, r20; 0x800100 <_edata>
  b6: 50 93 01 01 punti 0x0101, r21; 0x800101 <_edata + 0x1>
  ba: 60 93 02 01 punti 0x0102, r22; 0x800102 <_edata + 0x2>
  essere: 70 93 03 01 m 0x0103, r23; 0x800103 <_edata + 0x3>
            PORTD & = ~ (1 << 5);
  c2: 5d 98 cbi 0x0b, 5; 11
 // _delay_us (2);
            PORTD | = (1 << 5);
  c4: 5d 9a sbi 0x0b, 5; 11
 // _delay_us (2);

            if ((PIND & (1 << PIND6)) == (1 << PIND6)) {
  c6: 29 b1 in r18, 0x09; 9
            }
altro {
                val = 0;
            }

            dati | = val;
  c8: 26 fb bst r18, 6
  ca: 22 27 per r18, r18
  cc: 20 f9 bld r18, 0
  ce: 40 91 00 01 lds r20, 0x0100; 0x800100 <_edata>
  d2: 50 91 01 01 lds r21, 0x0101; 0x800101 <_edata + 0x1>
  d6: 60 91 02 01 lds r22, 0x0102; 0x800102 <_edata + 0x2>
  da: 70 91 03 01 lds r23, 0x0103; 0x800103 <_edata + 0x3>
  de: 42 2b o r20, r18
  e0: 40 93 00 01 punti 0x0100, r20; 0x800100 <_edata>
  e4: 50 93 01 01 punti 0x0101, r21; 0x800101 <_edata + 0x1>
  e8: 60 93 02 01 punti 0x0102, r22; 0x800102 <_edata + 0x2>
  ec: 70 93 03 01 punti 0x0103, r23; 0x800103 <_edata + 0x3>
  f0: 01 97 sbiw r24, 0x01; 1

    while (1) {

        uint8_t val;

        for (int i = 0; i<25; i ++) {
  f2: 99 f6 brne.-90; 0x9a <main + 0xa>
  f4: d0 cf rjmp.-96; 0x96 <main + 0x6>
 

Il codice del ciclo interno è ora gonfiato a 29 istruzioni che richiedono 49 cicli, riducendo la frequenza massima misurabile a ~ 163 kHz. La semplice aggiunta della parola chiave static è stata sufficiente per renderlo 5 volte più lento. Ma questa è la velocità realistica che potresti aspettarti quando il codice viene utilizzato in un'applicazione più grande.

Se hai bisogno della massima velocità possibile indipendentemente dalle stranezze del compilatore, hai 3 opzioni: -

  1. Scrivi codice assembler finemente elaborato che utilizzi ciascuna istruzione nel modo più efficiente possibile (altro codice non critico può ancora essere scritto in C).

  2. Utilizza hardware periferico come l'unità timer / contatore o SPI.

  3. Aggiungi un chip esterno come un prescaler per dividere la frequenza o un registro a scorrimento (es. CD4031) per catturare la forma d'onda.



Questa domanda e risposta è stata tradotta automaticamente dalla lingua inglese. Il contenuto originale è disponibile su stackexchange, che ringraziamo per la licenza cc by-sa 4.0 con cui è distribuito.
Loading...