Interruzione di Arduino (al cambio pin)


8

Uso la funzione di interruzione per riempire un array con i valori ricevuti da digitalRead().

 void setup() {
      Serial.begin(115200);
       attachInterrupt(0, test_func, CHANGE);
    }

    void test_func(){
      if(digitalRead(pin)==HIGH){
          test_array[x]=1;  
        } else if(digitalRead(pin)==LOW){
          test_array[x]=0;  
        }
         x=x+1;
    }

Il problema è che quando stampo test_arrayci sono valori come: 111o 000.

A quanto mi risulta, se utilizzo l' CHANGEopzione nella attachInterrupt()funzione, la sequenza di dati dovrebbe essere sempre 0101010101senza ripetizione.

I dati cambiano abbastanza rapidamente poiché provengono da un modulo radio.


1
Gli interrupt non rimandano il pulsante. Stai usando il debouncing hardware?
Ignacio Vazquez-Abrams,

Si prega di inviare il codice completo, inclusi pin, xe test_arraydefinizione, e anche loop()il metodo; ci consentirebbe di vedere se questo può essere un problema di concorrenza quando si accede alle variabili modificate da test_func.
jfpoilpret,

2
Non dovresti digitalRead () due volte nell'ISR: pensa a cosa succederebbe se ricevi BASSO alla prima chiamata e ALTO sulla seconda. Invece, if (digitalRead(pin) == HIGH) ... else ...;o, meglio ancora, questa singola linea ISR: test_array[x++] = digitalRead(pin);.
Edgar Bonet,

@EdgarBonet nice! +1 a quel commento. Spero non ti dispiaccia di aver aggiunto qualcosa alla mia risposta per includere ciò che hai menzionato qui. Inoltre, se decidi di inserire la tua risposta, incluso questo dettaglio, rimuoverò la mia aggiunta e darò un voto positivo in modo da ottenere il rappresentante.
Clayton Mills,

@Clayton Mills: Sto preparando una risposta (eccessivamente lunga e marginalmente tangenziale), ma puoi mantenere la tua modifica, per me va benissimo.
Edgar Bonet,

Risposte:


21

Come una sorta di prologo a questa risposta troppo lunga ...

Questa domanda mi ha profondamente affascinato dal problema della latenza di interruzione, al punto da perdere il sonno nei cicli di conteggio anziché nelle pecore. Sto scrivendo questa risposta più per condividere i miei risultati che non solo per rispondere alla domanda: la maggior parte di questo materiale potrebbe in realtà non essere ad un livello adatto per una risposta adeguata. Spero che sarà utile, tuttavia, per i lettori che arrivano qui alla ricerca di soluzioni per i problemi di latenza. Le prime sezioni dovrebbero essere utili a un vasto pubblico, incluso il poster originale. Quindi, diventa peloso lungo la strada.

Clayton Mills ha già spiegato nella sua risposta che c'è una certa latenza nel rispondere agli interrupt. Qui mi concentrerò sulla quantificazione della latenza (che è enorme quando si usano le librerie Arduino) e sui mezzi per minimizzarla. La maggior parte di ciò che segue è specifico dell'hardware di Arduino Uno e di schede simili.

Riduzione al minimo della latenza di interruzione su Arduino

(o come andare da 99 a 5 cicli)

Userò la domanda originale come esempio funzionante e riaffermerò il problema in termini di latenza degli interrupt. Abbiamo qualche evento esterno che innesca un interrupt (qui: INT0 sul cambio pin). Dobbiamo intervenire quando viene attivato l'interrupt (qui: leggi un ingresso digitale). Il problema è: c'è un certo ritardo tra l'attivazione dell'interrupt e la nostra azione appropriata. Questo ritardo viene chiamato " latenza di interruzione ". Una lunga latenza è dannosa in molte situazioni. In questo esempio particolare, il segnale di ingresso può cambiare durante il ritardo, nel qual caso si ottiene una lettura errata. Non c'è niente che possiamo fare per evitare il ritardo: è intrinseco al modo in cui interrompe il lavoro. Possiamo, tuttavia, provare a renderlo il più breve possibile, il che dovrebbe sperare di ridurre al minimo le conseguenze negative.

La prima cosa ovvia che possiamo fare è intraprendere al più presto l'azione critica, all'interno del gestore degli interrupt. Ciò significa chiamare digitalRead()una volta (e una sola volta) all'inizio del gestore. Ecco la versione zeroth del programma su cui costruiremo:

#define INT_NUMBER 0
#define PIN_NUMBER 2    // interrupt 0 is on pin 2
#define MAX_COUNT  200

volatile uint8_t count_edges;  // count of signal edges
volatile uint8_t count_high;   // count of high levels

/* Interrupt handler. */
void read_pin()
{
    int pin_state = digitalRead(PIN_NUMBER);  // do this first!
    if (count_edges >= MAX_COUNT) return;     // we are done
    count_edges++;
    if (pin_state == HIGH) count_high++;
}

void setup()
{
    Serial.begin(9600);
    attachInterrupt(INT_NUMBER, read_pin, CHANGE);
}

void loop()
{
    /* Wait for the interrupt handler to count MAX_COUNT edges. */
    while (count_edges < MAX_COUNT) { /* wait */ }

    /* Report result. */
    Serial.print("Counted ");
    Serial.print(count_high);
    Serial.print(" HIGH levels for ");
    Serial.print(count_edges);
    Serial.println(" edges");

    /* Count again. */
    count_high = 0;
    count_edges = 0;  // do this last to avoid race condition
}

Ho testato questo programma e le versioni successive inviandogli treni di impulsi di larghezza variabile. Vi è una spaziatura sufficiente tra gli impulsi per garantire che nessun fronte venga perso: anche se il fronte di discesa viene ricevuto prima dell'interruzione precedente, la seconda richiesta di interruzione verrà messa in attesa e infine revisionata. Se un impulso è più breve della latenza di interruzione, il programma legge 0 su entrambi i fronti. Il numero riportato di livelli HIGH è quindi la percentuale di impulsi letti correttamente.

Cosa succede quando viene attivato l'interrupt?

Prima di provare a migliorare il codice sopra, daremo uno sguardo agli eventi che si svolgono subito dopo l'attivazione dell'interrupt. La parte hardware della storia è raccontata dalla documentazione di Atmel. La parte software, smontando il binario.

Il più delle volte, l'interrupt in arrivo viene revisionato immediatamente. Tuttavia, può accadere che l'MCU (che significa "microcontrollore") si trovi nel mezzo di un compito critico in termini di tempo, in cui la manutenzione degli interrupt è disabilitata. Questo è in genere il caso in cui sta già eseguendo la manutenzione di un altro interrupt. Quando ciò accade, la richiesta di interruzione in entrata viene messa in attesa e gestita solo quando viene eseguita quella sezione critica. Questa situazione è difficile da evitare completamente, perché ci sono alcune di quelle sezioni critiche nella libreria principale di Arduino (che chiamerò " libcore"di seguito). Fortunatamente, queste sezioni sono brevi e vengono eseguite solo ogni tanto. Pertanto, la maggior parte delle volte, la nostra richiesta di interruzione verrà gestita immediatamente. Di seguito, assumerò che non ci interessiamo di quei pochi casi in cui non è così.

Quindi, la nostra richiesta viene immediatamente soddisfatta. Ciò comporta ancora molte cose che possono richiedere parecchio tempo. Innanzitutto, esiste una sequenza cablata. L'MCU terminerà l'esecuzione dell'istruzione corrente. Fortunatamente, la maggior parte delle istruzioni sono a ciclo singolo, ma alcune possono richiedere fino a quattro cicli. Quindi, l'MCU cancella un flag interno che disabilita l'ulteriore manutenzione degli interrupt. Questo ha lo scopo di prevenire interruzioni nidificate. Quindi, il PC viene salvato nello stack. Lo stack è un'area di RAM riservata per questo tipo di memoria temporanea. Il PC (che significa " Contatore programmi "") è un registro interno contenente l'indirizzo dell'istruzione successiva che la MCU sta per eseguire. Questo è ciò che consente alla MCU di sapere cosa fare dopo, e salvarlo è essenziale perché dovrà essere ripristinato per programma per riprendere da dove era stato interrotto.Il PC viene quindi caricato con un indirizzo cablato specifico per la richiesta ricevuta, e questa è la fine della sequenza cablata, il resto è controllato da software.

L'MCU ora esegue le istruzioni da quell'indirizzo cablato. Questa istruzione è chiamata " vettore di interruzione " ed è generalmente un'istruzione "jump" che ci porterà a una routine speciale chiamata ISR (" routine di servizio di interruzione "). In questo caso, l'ISR è chiamato "__vector_1", alias "INT0_vect", che è un termine improprio perché è un ISR, non un vettore. Questo particolare ISR proviene da libcore. Come ogni ISR, inizia con un prologo che salva nello stack un sacco di registri CPU interni. Ciò gli consentirà di utilizzare quei registri e, una volta fatto, ripristinarli ai loro valori precedenti per non disturbare il programma principale. Quindi, cercherà il gestore di interrupt con cui è stato registratoattachInterrupt()e chiamerà quel gestore, che è la nostra read_pin()funzione sopra. La nostra funzione chiamerà quindi digitalRead()da libcore. digitalRead()esaminerà alcune tabelle per mappare il numero di porta Arduino sulla porta I / O hardware che deve leggere e il numero di bit associato da testare. Controllerà anche se esiste un canale PWM su quel pin che dovrebbe essere disabilitato. Quindi leggerà la porta I / O ... e abbiamo finito. Bene, non abbiamo davvero terminato la manutenzione dell'interrupt, ma il compito critico in termini di tempo (leggere la porta I / O) è fatto, ed è tutto ciò che conta quando guardiamo alla latenza.

Ecco un breve riassunto di tutto quanto sopra, insieme ai ritardi associati nei cicli della CPU:

  1. sequenza cablata: completare le istruzioni correnti, prevenire interruzioni nidificate, salvare il PC, caricare l'indirizzo del vettore (≥ 4 cicli)
  2. eseguire il vettore di interrupt: passa a ISR (3 cicli)
  3. Prologo ISR: salvataggio registri (32 cicli)
  4. Corpo principale ISR: localizza e chiama la funzione registrata dall'utente (13 cicli)
  5. read_pin: call digitalRead (5 cicli)
  6. digitalRead: trova la porta e il bit da testare (41 cicli)
  7. digitalRead: leggi la porta I / O (1 ciclo)

Assumeremo lo scenario migliore, con 4 cicli per la sequenza cablata. Questo ci dà una latenza totale di 99 cicli, o circa 6,2 µs con un clock a 16 MHz. Di seguito, esplorerò alcuni trucchi che possono essere utilizzati per ridurre questa latenza. Arrivano all'incirca in ordine crescente di complessità, ma hanno tutti bisogno di noi per scavare in qualche modo all'interno del MCU.

Usa l'accesso diretto alla porta

Il primo obiettivo ovvio per abbreviare la latenza è digitalRead(). Questa funzione fornisce una piacevole astrazione all'hardware MCU, ma è troppo inefficiente per un lavoro che richiede tempo. Sbarazzarsi di questo è in realtà banale: dobbiamo solo sostituirlo con digitalReadFast(), dalla libreria digitalwritefast . Questo riduce la latenza quasi della metà al costo di un piccolo download!

Beh, è ​​stato troppo facile per essere divertente, preferirò mostrarti come farlo nel modo più duro. Lo scopo è di farci iniziare a fare cose di basso livello. Il metodo si chiama " accesso diretto alla porta " ed è ben documentato sul riferimento Arduino alla pagina su Registri delle porte . A questo punto, è una buona idea scaricare e dare un'occhiata al foglio dati ATmega328P . Questo documento di 650 pagine può sembrare alquanto scoraggiante a prima vista. È, tuttavia, ben organizzato in sezioni specifiche per ciascuna delle periferiche e funzionalità MCU. E dobbiamo solo controllare le sezioni pertinenti a ciò che stiamo facendo. In questo caso, è la sezione denominata I / O ports . Ecco un riassunto di ciò che apprendiamo da queste letture:

  • Il pin 2 di Arduino è in realtà chiamato PD2 (cioè porta D, bit 2) sul chip AVR.
  • Otteniamo subito l'intera porta D leggendo uno speciale registro MCU chiamato "PIND".
  • Quindi controlliamo il bit numero 2 eseguendo un logico bit a bit e (l'operatore C '&') con 1 << 2.

Quindi, ecco il nostro gestore di interrupt modificato:

#define PIN_REG    PIND  // interrupt 0 is on AVR pin PD2
#define PIN_BIT    2

/* Interrupt handler. */
void read_pin()
{
    uint8_t sampled_pin = PIN_REG;            // do this first!
    if (count_edges >= MAX_COUNT) return;     // we are done
    count_edges++;
    if (sampled_pin & (1 << PIN_BIT)) count_high++;
}

Ora, il nostro gestore leggerà il registro I / O non appena viene chiamato. La latenza è di 53 cicli della CPU. Questo semplice trucco ci ha salvato 46 cicli!

Scrivi il tuo ISR

Il prossimo obiettivo per il ciclo di taglio è l'ISR INT0_vect. Questo ISR è necessario per fornire la funzionalità di attachInterrupt(): possiamo cambiare i gestori di interrupt in qualsiasi momento durante l'esecuzione del programma. Tuttavia, anche se bello da avere, questo non è davvero utile per il nostro scopo. Pertanto, invece di fare in modo che l'ISR del libcore localizzi e chiami il nostro gestore di interruzioni, salveremo alcuni cicli sostituendo l'ISR con il nostro gestore.

Non è così difficile come sembra. Gli ISR ​​possono essere scritti come normali funzioni, dobbiamo solo essere consapevoli dei loro nomi specifici e definirli usando una ISR()macro speciale da avr-libc. A questo punto sarebbe bene dare un'occhiata alla documentazione di avr-libc sugli interrupt e alla sezione del datasheet denominata Interrupt esterni . Ecco il breve riassunto:

  • Dobbiamo scrivere un po 'in un registro hardware speciale chiamato EICRA ( registro di controllo interrupt esterno A ) per configurare l'interrupt da attivare in caso di modifica del valore del pin. Questo sarà fatto in setup().
  • Dobbiamo scrivere un po 'in un altro registro hardware chiamato EIMSK ( registro MaSK di interrupt esterno ) per abilitare l'interrupt INT0. Anche questo sarà fatto in setup().
  • Dobbiamo definire l'ISR con la sintassi ISR(INT0_vect) { ... }.

Ecco il codice per l'ISR e setup(), tutto il resto è invariato:

/* Interrupt service routine for INT0. */
ISR(INT0_vect)
{
    uint8_t sampled_pin = PIN_REG;            // do this first!
    if (count_edges >= MAX_COUNT) return;     // we are done
    count_edges++;
    if (sampled_pin & (1 << PIN_BIT)) count_high++;
}

void setup()
{
    Serial.begin(9600);
    EICRA = 1 << ISC00;  // sense any change on the INT0 pin
    EIMSK = 1 << INT0;   // enable INT0 interrupt
}

Questo ha un bonus gratuito: poiché questo ISR è più semplice di quello che sostituisce, ha bisogno di meno registri per fare il suo lavoro, quindi il prologo che salva i registri è più breve. Ora siamo scesi a una latenza di 20 cicli. Non male considerando che abbiamo iniziato vicino a 100!

A questo punto direi che abbiamo finito. Missione compiuta. Quello che segue è solo per coloro che non hanno timore di sporcarsi le mani con qualche assemblaggio AVR. Altrimenti puoi smettere di leggere qui e grazie per essere arrivato così lontano.

Scrivi un ISR nudo

Ancora qui? Buona! Per procedere ulteriormente, sarebbe utile avere almeno qualche idea di base su come funziona l'assemblaggio e dare un'occhiata al ricettario Inline Assembler dalla documentazione di avr-libc. A questo punto, la nostra sequenza di immissioni di interrupt si presenta così:

  1. sequenza cablata (4 cicli)
  2. vettore di interrupt: passa a ISR (3 cicli)
  3. Prologo ISR: save regs (12 cicli)
  4. prima cosa nel corpo dell'ISR: leggere la porta IO (1 ciclo)

Se vogliamo fare di meglio, dobbiamo spostare la lettura della porta nel prologo. L'idea è la seguente: la lettura del registro PIND bloccherà un registro CPU, quindi è necessario salvare almeno un registro prima di farlo, ma gli altri registri possono attendere. Dobbiamo quindi scrivere un prologo personalizzato che legge la porta I / O subito dopo aver salvato il primo registro. Hai già visto nella documentazione di interrupt avr-libc (l'hai letto, vero?) Che un ISR può essere reso nudo , nel qual caso il compilatore non emetterà prologo o epilogo, permettendoci di scrivere la nostra versione personalizzata.

Il problema con questo approccio è che probabilmente finiremo per scrivere l'intero ISR in assemblea. Non è un grosso problema, ma preferirei che il compilatore scrivesse quei noiosi prologhi ed epiloghi per me. Quindi, ecco il trucco sporco: divideremo l'ISR in due parti:

  • la prima parte sarà un breve frammento di assemblaggio che lo farà
    • salva un singolo registro nello stack
    • leggi PIND in quel registro
    • memorizzare quel valore in una variabile globale
    • ripristinare il registro dallo stack
    • saltare alla seconda parte
  • la seconda parte sarà un codice C regolare con prologo ed epilogo generati dal compilatore

Il nostro precedente ISR INT0 è quindi sostituito da questo:

volatile uint8_t sampled_pin;    // this is now a global variable

/* Interrupt service routine for INT0. */
ISR(INT0_vect, ISR_NAKED)
{
    asm volatile(
    "    push r0                \n"  // save register r0
    "    in r0, %[pin]          \n"  // read PIND into r0
    "    sts sampled_pin, r0    \n"  // store r0 in a global
    "    pop r0                 \n"  // restore previous r0
    "    rjmp INT0_vect_part_2  \n"  // go to part 2
    :: [pin] "I" (_SFR_IO_ADDR(PIND)));
}

ISR(INT0_vect_part_2)
{
    if (count_edges >= MAX_COUNT) return;     // we are done
    count_edges++;
    if (sampled_pin & (1 << PIN_BIT)) count_high++;
}

Qui stiamo usando la macro ISR () per avere lo strumento del compilatore INT0_vect_part_2con il prologo e l'epilogo richiesti. Il compilatore si lamenterà che "'INT0_vect_part_2' sembra essere un gestore di errori di ortografia", ma l'avviso può essere tranquillamente ignorato. Ora l'ISR ha una singola istruzione a 2 cicli prima della lettura della porta effettiva e la latenza totale è di soli 10 cicli.

Utilizzare il registro GPIOR0

E se potessimo avere un registro riservato per questo specifico lavoro? Quindi, non avremmo bisogno di salvare nulla prima di leggere la porta. Possiamo effettivamente chiedere al compilatore di associare una variabile globale a un registro . Ciò, tuttavia, richiederebbe di ricompilare l'intero core di Arduino e libc per assicurarsi che il registro sia sempre riservato. Non proprio conveniente. D'altra parte, l'ATmega328P sembra avere tre registri che non sono utilizzati dal compilatore né da alcuna libreria e sono disponibili per la memorizzazione di ciò che vogliamo. Sono chiamati GPIOR0, GPIOR1 e GPIOR2 (Registri I / O per uso generale ). Sebbene siano mappati nello spazio degli indirizzi I / O dell'MCU, in realtà non lo sonoRegistri I / O: sono semplicemente memoria, come tre byte di RAM che in qualche modo si sono persi in un bus e sono finiti nello spazio degli indirizzi sbagliato. Questi non sono capaci come i registri interni della CPU e non possiamo copiare PIND in uno di questi con l' inistruzione. GPIOR0 è interessante, tuttavia, in quanto è indirizzabile in bit , proprio come PIND. Questo ci consentirà di trasferire le informazioni senza ostruire alcun registro interno della CPU.

Ecco il trucco: faremo in modo che GPIOR0 sia inizialmente zero (in realtà è cancellato dall'hardware al momento dell'avvio), quindi useremo sbic(Salta la prossima istruzione se qualche Bit in qualche registro I / o è Clear) e il sbi( Impostare su 1 alcuni bit in alcune istruzioni I / o) come segue:

sbic PIND, 2   ; skip the following if bit 2 of PIND is clear
sbi GPIOR0, 0  ; set to 1 bit 0 of GPIOR0

In questo modo, GPIOR0 finirà per essere 0 o 1 a seconda del bit che volevamo leggere da PIND. L'esecuzione dell'istruzione sbic richiede 1 o 2 cicli a seconda che la condizione sia falsa o vera. Ovviamente, al primo PIN si accede al bit PIND. In questa nuova versione del codice, la variabile globale sampled_pinnon è più utile, poiché è sostanzialmente sostituita da GPIOR0:

/* Interrupt service routine for INT0. */
ISR(INT0_vect, ISR_NAKED)
{
    asm volatile(
    "    sbic %[pin], %[bit]    \n"
    "    sbi %[gpio], 0         \n"
    "    rjmp INT0_vect_part_2  \n"
    :: [pin]  "I" (_SFR_IO_ADDR(PIND)),
       [bit]  "I" (PIN_BIT),
       [gpio] "I" (_SFR_IO_ADDR(GPIOR0)));
}

ISR(INT0_vect_part_2)
{
    if (count_edges < MAX_COUNT) {
        count_edges++;
        if (GPIOR0) count_high++;
    }
    GPIOR0 = 0;
}

Va notato che GPIOR0 deve essere sempre resettato nell'ISR.

Ora, il campionamento del registro I / O PIND è la prima cosa fatta all'interno dell'ISR. La latenza totale è di 8 cicli. Questo è il meglio che possiamo fare prima di essere macchiati di kludges terribilmente peccaminosi. Questa è di nuovo una buona opportunità per smettere di leggere ...

Inserisci il codice time-critical nella tabella vettoriale

Per quelli ancora qui, ecco la nostra situazione attuale:

  1. sequenza cablata (4 cicli)
  2. vettore di interrupt: passa a ISR (3 cicli)
  3. Corpo ISR: leggere la porta IO (al 1 ° ciclo)

C'è ovviamente poco margine di miglioramento. L'unico modo in cui potremmo accorciare la latenza a questo punto è sostituendo il vettore di interruzione stesso con il nostro codice. Tieni presente che ciò dovrebbe essere immensamente sgradevole per chiunque apprezzi la progettazione pulita del software. Ma è possibile e ti mostrerò come.

Il layout della tabella vettoriale ATmega328P può essere trovato nel foglio dati, sezione Interrupt , sottotitoli Vettori di interruzione in ATmega328 e ATmega328P . O smontando qualsiasi programma per questo chip. Ecco come appare. Sto usando le convenzioni di avr-gcc e avr-libc (__init è il vettore 0, gli indirizzi sono in byte) che sono diversi da quelli di Atmel.

address  instruction      comment
────────┼─────────────────┼──────────────────────
 0x0000  jmp __init       reset vector 
 0x0004  jmp __vector_1   a.k.a. INT0_vect
 0x0008  jmp __vector_2   a.k.a. INT1_vect
 0x000c  jmp __vector_3   a.k.a. PCINT0_vect
  ...
 0x0064  jmp __vector_25  a.k.a. SPM_READY_vect

Ogni vettore ha uno slot di 4 byte, riempito con una singola jmpistruzione. Questa è un'istruzione a 32 bit, a differenza della maggior parte delle istruzioni AVR che sono a 16 bit. Ma uno slot a 32 bit è troppo piccolo per contenere la prima parte del nostro ISR: possiamo adattare le istruzioni sbice sbi, ma non le rjmp. Se lo facciamo, la tabella vettoriale finisce così:

address  instruction      comment
────────┼─────────────────┼──────────────────────
 0x0000  jmp __init       reset vector 
 0x0004  sbic PIND, 2     the first part...
 0x0006  sbi GPIOR0, 0    ...of our ISR
 0x0008  jmp __vector_2   a.k.a. INT1_vect
 0x000c  jmp __vector_3   a.k.a. PCINT0_vect
  ...
 0x0064  jmp __vector_25  a.k.a. SPM_READY_vect

Quando INT0 viene attivato, verrà letto PIND, il bit rilevante verrà copiato in GPIOR0 e quindi l'esecuzione passerà al vettore successivo. Quindi, verrà chiamato l'ISR per INT1, anziché l'ISR per INT0. Questo è inquietante, ma dal momento che non stiamo usando INT1 comunque, semplicemente "dirotteremo" il suo vettore per servire INT0.

Ora non ci resta che scrivere la nostra tabella vettoriale personalizzata per sovrascrivere quella predefinita. Si scopre che non è così facile. La tabella vettoriale predefinita è fornita dalla distribuzione avr-libc, in un file oggetto chiamato crtm328p.o che viene automaticamente collegato a qualsiasi programma creato. A differenza del codice della libreria, il codice del file oggetto non è pensato per essere sovrascritto: provando a farlo, verrà generato un errore del linker sulla tabella definita due volte. Ciò significa che dobbiamo sostituire l'intero crtm328p.o con la nostra versione personalizzata. Un'opzione è scaricare l'intero codice sorgente avr-libc , fare le nostre modifiche personalizzate in gcrt1.S , quindi crearlo come libc personalizzato.

Qui ho optato per un approccio più leggero e alternativo. Ho scritto un crt.S personalizzato, che è una versione semplificata dell'originale da avr-libc. Manca alcune funzionalità utilizzate raramente, come la possibilità di definire un ISR "catch all" o di essere in grado di terminare il programma (ad esempio congelare Arduino) chiamando exit(). Ecco il codice Ho tagliato la parte ripetitiva della tabella vettoriale per minimizzare lo scorrimento:

#include <avr/io.h>

.weak __heap_end
.set  __heap_end, 0

.macro vector name
    .weak \name
    .set \name, __vectors
    jmp \name
.endm

.section .vectors
__vectors:
    jmp __init
    sbic _SFR_IO_ADDR(PIND), 2   ; these 2 lines...
    sbi _SFR_IO_ADDR(GPIOR0), 0  ; ...replace vector_1
    vector __vector_2
    vector __vector_3
    [...and so forth until...]
    vector __vector_25

.section .init2
__init:
    clr r1
    out _SFR_IO_ADDR(SREG), r1
    ldi r28, lo8(RAMEND)
    ldi r29, hi8(RAMEND)
    out _SFR_IO_ADDR(SPL), r28
    out _SFR_IO_ADDR(SPH), r29

.section .init9
    jmp main

Può essere compilato con la seguente riga di comando:

avr-gcc -c -mmcu=atmega328p silly-crt.S

Lo schizzo è identico al precedente, tranne per il fatto che non esiste INT0_vect e INT0_vect_part_2 è sostituito da INT1_vect:

/* Interrupt service routine for INT1 hijacked to service INT0. */
ISR(INT1_vect)
{
    if (count_edges < MAX_COUNT) {
        count_edges++;
        if (GPIOR0) count_high++;
    }
    GPIOR0 = 0;
}

Per compilare lo schizzo, è necessario un comando di compilazione personalizzato. Se hai seguito finora, probabilmente sai come compilare dalla riga di comando. Devi richiedere esplicitamente a silly-crt.o di essere collegato al tuo programma e aggiungere l' -nostartfilesopzione per evitare il collegamento nell'originale crtm328p.o.

Ora, la lettura della porta I / O è la prima istruzione eseguita dopo l'attivazione dell'interrupt. Ho provato questa versione inviandole brevi impulsi da un altro Arduino e può catturare (anche se non in modo affidabile) l'alto livello di impulsi di soli 5 cicli. Non c'è altro che possiamo fare per abbreviare la latenza di interrupt su questo hardware.


2
Buona spiegazione +1
Nick Gammon

6

L'interrupt viene impostato per attivarsi in seguito a una modifica e test_func viene impostato come routine di servizio di interruzione (ISR), chiamata al servizio di tale interruzione. L'ISR quindi stampa il valore dell'input.

A prima vista ti aspetteresti che l'output sia come hai detto, e alternando una serie di minimi alti, dato che arriva all'ISR solo con una modifica.

Ma ciò che ci manca è che c'è un certo tempo necessario affinché la CPU serva un interrupt e si dirami verso l'ISR. Durante questo periodo la tensione sul pin potrebbe essere cambiata di nuovo. Soprattutto se il pin non è stabilizzato dall'hardware che rimuove il rimbalzo o simile. Poiché l'interrupt è già contrassegnato e non è ancora stato revisionato, questo cambiamento extra (o molti di essi, poiché un livello di pin può cambiare molto rapidamente rispetto alla velocità di clock se ha una bassa capacità parassita) verrà perso.

Quindi, in sostanza, senza una qualche forma di de-rimbalzo, non abbiamo alcuna garanzia che quando l'input cambia e l'interrupt viene contrassegnato per la manutenzione, che l'input sarà ancora allo stesso valore quando arriveremo a leggere il suo valore nell'ISR.

A titolo di esempio generico, la scheda tecnica ATmega328 utilizzata su Arduino Uno indica i tempi di interruzione nella sezione 6.7.1 - "Tempo di risposta agli interrupt". Indica per questo microcontrollore il tempo minimo di diramazione di un ISR per la manutenzione è di 4 cicli di clock, ma può essere maggiore (extra se si eseguono istruzioni multi-ciclo in caso di interruzione o 8 + tempo di riattivazione in standby se l'MCU è in modalità di sospensione).

Come menzionato da @EdgarBonet nei commenti, il pin potrebbe anche cambiare in modo simile durante l'esecuzione di ISR. Poiché l'ISR legge dal pin due volte, non aggiungerebbe nulla al test_array se incontrasse un LOW nella prima lettura e un HIGH nella seconda. Ma x aumenterebbe comunque, lasciando invariato quello slot nell'array (possibilmente come dati non inizializzati a seconda di ciò che è stato fatto in precedenza nell'array).

Il suo ISR a una riga test_array[x++] = digitalRead(pin);è la soluzione perfetta per questo.

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.