Girino - Oscilloscopio Arduino veloce

Sono un fisico e la parte più bella del lavoro in questo campo è che riesco a costruire i miei strumenti. Con questo modo di pensare, ho deciso di costruire un oscilloscopio Arduino homebrew. Questo istruttore è stato scritto con lo scopo di insegnare un po 'su microcontrollori e acquisizione dati. Questo è un progetto estremo perché volevo spremere da Arduino tutta la velocità che potevo, non ho visto nessun altro oscilloscopio Arduino più veloce di questo.

Qualche tempo fa stavo lavorando a un progetto Arduino e avevo bisogno di vedere se il segnale di uscita era conforme alle specifiche. Così ho trascorso un po 'di tempo su Internet alla ricerca di oscilloscopi Arduino già implementati, ma non mi è piaciuto quello che ho trovato. I progetti che ho trovato erano per lo più composti da un'interfaccia utente grafica per il computer scritta in Processing e un semplicissimo sketch arduino. Gli schizzi erano qualcosa del tipo: void setup () {

Serial.begin (9600);

}

void loop () {

int val = analogRead (ANALOG_IN);

Serial.println (val);

} Questo approccio non è sbagliato e non voglio insultare nessuno, ma questo è troppo lento per me. La porta seriale è lenta e l'invio di ogni risultato di un analogRead () attraverso di essa è un collo di bottiglia.

Ho studiato i digitalizzatori di forme d'onda per un po 'di tempo e so abbastanza bene come funzionano, quindi mi sono ispirato a loro. Questi erano i punti di partenza dell'oscilloscopio che volevo creare:

  • il segnale in arrivo dovrebbe essere disaccoppiato dall'Arduino per preservarlo;
  • con un offset del segnale è possibile vedere segnali negativi;
  • i dati dovrebbero essere bufferizzati;
  • è necessario un trigger hardware per catturare i segnali;
  • un buffer circolare può dare la forma del segnale prima del trigger (ne seguiranno altri);
  • utilizzando le funzioni della leva inferiore che quelle standard rendono il programma più veloce.

Lo schizzo per l'Arduino è allegato a questo passaggio, insieme allo schema del circuito che ho realizzato.

Il nome che mi è venuto in mente, Girino, è un gioco di parole frivolo in italiano. Giro significa rotazione e aggiungendo il suffisso -ino si ottiene una piccola rotazione, ma Girino significa anche girino . In questo modo ho avuto un nome e una mascotte.

Passaggio 1: Dichiarazione di non responsabilità

L'AUTORE DI QUESTA STRUTTURA NON FORNISCE ALCUNA GARANZIA DI VALIDITÀ E NESSUNA GARANZIA .

L'elettronica può essere pericolosa se non sai cosa stai facendo e l'autore non può garantire la validità delle informazioni trovate qui. Questo non è un consiglio professionale e tutto ciò che è scritto in questo manuale può essere inaccurato, fuorviante, pericoloso o sbagliato. Non fare affidamento su alcuna informazione trovata qui senza verifica indipendente.

Spetta a voi verificare qualsiasi informazione e ricontrollare che non state esponendo voi stessi o nessuno a qualsiasi danno o esposizione a qualsiasi danno; Non mi assumo alcuna responsabilità. Devi seguire da solo le opportune precauzioni di sicurezza, se vuoi riprodurre questo progetto.

Usa questa guida a tuo rischio e pericolo!

Passaggio 2: cosa serve

Ciò di cui abbiamo veramente bisogno per questo progetto è una scheda Arduino e il foglio dati di ATMega328P.
Il foglio dati è ciò che ci dice come funziona il microcontrollore ed è molto importante mantenerlo se vogliamo una leva di controllo inferiore.

La scheda tecnica è disponibile qui: //www.atmel.com/Images/doc8271.pdf

L'hardware che ho aggiunto ad Arduino è parzialmente necessario, il suo scopo è solo quello di formare il segnale per l'ADC e fornire un livello di tensione per il trigger. Se lo si desidera, è possibile inviare il segnale direttamente ad Arduino e utilizzare alcuni riferimenti di tensione definiti da un partitore di tensione, o anche i 3, 3 V forniti dallo stesso Arduino.

Passaggio 3: output di debug

Di solito metto un sacco di output di debug nei miei programmi perché voglio tenere traccia di tutto ciò che accade; il problema con Arduino è che non abbiamo uno stdout su cui scrivere. Ho deciso di utilizzare la porta seriale come stdout.

Tieni presente, tuttavia, che questo approccio non funziona sempre! Perché la scrittura sulla porta seriale richiede un po 'di tempo per l'esecuzione e può cambiare radicalmente le cose durante alcune routine ragionevoli.

Di solito definisco gli output di debug all'interno di una macro del preprocessore, quindi quando il debug è disabilitato scompaiono semplicemente dal programma e non rallentano l'esecuzione:
  • dprint (x); - Scrive sulla porta seriale qualcosa come: # x: 123
  • dshow ("Some string"); - Scrive la stringa

Questa è la definizione:

#if DEBUG == 1
#define dprint (espressione) Serial.print ("#"); Serial.print (#expression); Serial.print (":"); Serial.println (espressione)
#define dshow (espressione) Serial.println (espressione)
#altro
#define dprint (espressione)
#define dshow (espressione)
#finisci se

Passaggio 4: impostazione dei bit di registro

Allo scopo di essere veloce, è necessario manipolare le funzionalità del microcontrollore con funzioni a leva inferiore rispetto a quelle standard fornite dall'IDE di Arduino. Le funzioni interne sono gestite attraverso alcuni registri, ovvero raccolte di otto bit in cui ciascuna regola qualcosa di particolare. Ogni registro contiene otto bit perché ATMega328P ha un'architettura a 8 bit.

I registri hanno alcuni nomi che sono specificati nel foglio dati a seconda dei loro significati, come ADCSRA per il registro delle impostazioni ADC A. Anche ogni bit significativo dei registri ha un nome, come ADEN per il bit di abilitazione ADC nel registro ADCSRA.

Per impostare i loro bit potremmo usare la solita sintassi C per l'algebra binaria, ma ho trovato su internet un paio di macro che sono molto belle e pulite:

// Definisce per impostare e cancellare i bit di registro
#ifndef cbi
#define cbi (sfr, bit) (_SFR_BYTE (sfr) & = ~ _BV (bit))
#finisci se
#ifndef sbi
#define sbi (sfr, bit) (_SFR_BYTE (sfr) | = _BV (bit))
#finisci se

Usarli è molto semplice, se vogliamo impostare su 1 il bit di abilitazione dell'ADC possiamo semplicemente scrivere:

sbi (ADCSRA, ADEN);

Mentre se vogliamo impostarlo su 0 ( id est clear it) possiamo semplicemente scrivere:

cbi (ADCSRA, ADEN);

Passaggio 5: quali sono gli interrupt

Come vedremo nei prossimi passi, in questo progetto è richiesto l'uso di interrupt. Gli interrupt sono segnali che indicano al microcontrollore di interrompere l'esecuzione del loop principale e di passarlo ad alcune funzioni speciali. Le immagini danno un'idea del flusso del programma.

Le funzioni che vengono eseguite sono chiamate Interrupt Service Routines (ISR) e sono funzioni più o meno semplici, ma che non accettano argomenti.

Vediamo un esempio, qualcosa come contare alcuni impulsi. ATMega328P ha un comparatore analogico a cui è associato un interrupt che viene attivato quando un segnale supera una tensione di riferimento. Prima di tutto devi definire la funzione che verrà eseguita:

ISR (ANALOG_COMP_vect)
{
contatore ++;
}

Questo è davvero semplice, l'istruzione ISR () è una macro che dice al compilatore che la seguente funzione è una routine di servizio di interruzione. Mentre ANALOG_COMP_vect si chiama Interrupt Vector e indica al compilatore quale interruzione è associata a quella routine. In questo caso è l'interrupt del comparatore analogico. Quindi ogni volta che il comparatore vede un segnale più grande di un riferimento, dice al microcontrollore di eseguire quel codice, id est in questo caso per incrementare quella variabile.

Il prossimo passo è abilitare l'interrupt associato. Per abilitarlo dobbiamo impostare il bit ACIE (Analog Comparator Interrupt Enable) del registro ACSR (Analog Comparator Setting Register):

sbi (ACSR, ACIE);

Nel seguente sito possiamo vedere l'elenco di tutti i Vettori di interruzione:
//www.nongnu.org/avr-libc/user-manual/group__avr__interrupts.html

Passaggio 6: acquisizione continua con un buffer circolare

Il concetto di utilizzare un buffer circolare è piuttosto semplice:

Acquisisci continuamente fino a trovare un segnale, quindi invia il segnale digitalizzato al computer.

Questo approccio consente di avere la forma del segnale in ingresso anche prima dell'evento trigger.


Ho preparato alcuni diagrammi per chiarirmi. I seguenti punti si riferiscono alle immagini.
  • Nella prima immagine possiamo vedere cosa intendo con acquisizione continua . Definiamo un buffer che memorizzerà i dati, nel mio caso un array con 1280 slot, quindi iniziamo a leggere continuamente l'annuncio del registro di uscita ADC (ADCH) riempiendo il buffer di dati. Quando arriviamo alla fine del buffer, ricominciamo dall'inizio senza cancellarlo. Se immaginiamo la matrice disposta in modo circolare è facile capire cosa intendo.
  • Quando il segnale supera la soglia, viene attivato l'interrupt del comparatore analogico. Quindi iniziamo una fase di attesa in cui continuiamo ad acquisire il segnale ma teniamo un conteggio dei cicli ADC che sono passati dall'interruzione del comparatore analogico.
  • Quando abbiamo aspettato N cicli (con N <1280), abbiamo bloccato la situazione e interrotto i cicli ADC. Quindi finiamo con un buffer riempito con la digitalizzazione della forma temporale del segnale. La gran parte di questo, è che abbiamo anche la forma prima dell'evento trigger, perché stavamo già acquisendo prima.
  • Ora possiamo inviare l'intero buffer alla porta seriale in un blocco di dati binari, invece di inviare le singole letture ADC. Ciò ha ridotto il sovraccarico necessario per inviare i dati e il collo di bottiglia degli schizzi che ho trovato su Internet.

Passaggio 7: attivazione dell'oscilloscopio

Un oscilloscopio mostra sul display un segnale, sul quale siamo tutti d'accordo, ma come può mostrarlo costantemente e non mostrarlo saltando sullo schermo? Ha un trigger interno in grado di mostrare il segnale sempre nella stessa posizione dello schermo (o almeno la maggior parte delle volte), creando l'illusione di un diagramma stabile.

Il trigger è associato a una soglia che attiva uno sweep quando il segnale lo supera. Una scansione è la fase in cui l'oscilloscopio registra e visualizza il segnale. Dopo uno sweep si verifica un'altra fase: il holdoff, in cui l'oscilloscopio rifiuta qualsiasi segnale in arrivo. Il periodo di holdoff può essere composto da una parte del tempo morto, in cui l'oscilloscopio non è in grado di accettare alcun segnale, e una parte che può essere selezionata dall'utente. Il tempo morto può essere causato da vari motivi come la necessità di disegnare sullo schermo o la necessità di memorizzare i dati da qualche parte.

Guardando l'immagine abbiamo la sensazione di ciò che accade.
  1. Il segnale 1 supera la soglia e attiva lo sweep;
  2. il segnale 2 è all'interno del tempo di scansione e viene catturato dal primo;
  3. dopo il holdoff, il segnale 3 riattiva lo sweep;
  4. invece il segnale 4 viene rifiutato perché rientra nella regione di holdoff.
La ragion d'essere della fase di holdoff è di impedire ad alcuni segnali indesiderati di entrare nella regione di sweep. È un po 'lungo per spiegare questo punto ed elude lo scopo di questo istruibile.

La morale di questa storia è che abbiamo bisogno di:
  1. un livello di soglia al quale possiamo confrontare il segnale in arrivo;
  2. un segnale che indica al microcontrollore di avviare la fase di attesa (vedere il passaggio precedente).
Abbiamo diverse possibili soluzioni per il punto 1.:
  • usando un trimmer possiamo impostare manualmente un livello di tensione;
  • utilizzando il PWM di Arduino possiamo impostare il livello tramite software;
  • utilizzando la 3.3 V fornita dallo stesso Arduino;
  • usando il riferimento bangap interno possiamo usare un livello fisso.
Per il punto 2. abbiamo la soluzione giusta: possiamo usare l'interrupt del Comparatore analogico interno del microcontrollore.

Passaggio 8: come funziona l'ADC

Il microcontrollore Arduino presenta un singolo ADC ad approssimazione successiva a 10 bit. Prima dell'ADC esiste un multiplexer analogico che ci consente di inviare all'ADC i segnali da diversi pin e sorgenti (ma solo uno alla volta).

L'ADC per approssimazione successiva significa che l'ADC impiega 13 cicli di clock per completare la conversione (e 25 cicli di clock per la prima conversione). C'è un segnale di clock dedicato all'ADC che viene "calcolato" dall'orologio principale dell'Arduino; questo perché l'ADC è un po 'lento e non riesce a tenere il passo con le altre parti del microcontrollore. Richiede una frequenza di clock in ingresso tra 50 kHz e 200 kHz per ottenere la massima risoluzione. Se è necessaria una risoluzione inferiore a 10 bit, la frequenza di clock in ingresso all'ADC può essere superiore a 200 kHz per ottenere una frequenza di campionamento più elevata.

Ma quanti tassi più alti possiamo usare? Ci sono un paio di buone guide sull'ADC agli Open Music Labs che suggerisco di leggere:
  • //www.openmusiclabs.com/learning/digital/atmega-adc/
  • //www.openmusiclabs.com/learning/digital/atmega-adc/in-depth/
Poiché il mio scopo è quello di ottenere un oscilloscopio veloce, ho deciso di limitare la precisione a 8 bit. Questo ha diversi bonus:
  1. il buffer di dati può memorizzare più dati;
  2. non si sprecano 6 bit di RAM per dato;
  3. l'ADC può acquisire più velocemente.
Il prescaler ci consente di dividere la frequenza, per alcuni fattori, impostando i bit ADPS0-1-2 del registro ADCSRA. Vedendo la trama della precisione dall'articolo di Open Music Labs, possiamo vedere che per la precisione a 8 bit la frequenza potrebbe arrivare a 1, 5 MHz, bene! Ma poiché la possibilità di modificare il fattore prescaler ci consente di modificare la velocità di acquisizione, possiamo usarla anche per modificare la scala temporale dell'oscilloscopio.

C'è una buona caratteristica dei registri di uscita: possiamo decidere la regolazione dei bit di conversione, impostando il bit ADLAR nel registro ADMUX. Se è 0 sono regolati a destra e viceversa (vedi l'immagine). Poiché volevo una precisione a 8 bit, l'ho impostato su 1 in modo da poter leggere solo il registro ADCH e ignorare ADCL.

Ho deciso di avere un solo canale di input per evitare di cambiare canale avanti e indietro ad ogni conversione.

Un'ultima cosa sull'ADC, ha diverse modalità di esecuzione ognuna con una diversa sorgente di trigger:
  • Modalità corsa libera
  • Comparatore analogico
  • Richiesta di interruzione esterna 0
  • Timer / Contatore0 Confronta corrispondenza A
  • Timer / Counter0 Overflow
  • Timer / Contatore1 Confronta corrispondenza B
  • Timer / Counter1 Overflow
  • Timer / Counter1 Capture Event
Mi interessava la modalità di corsa libera che è una modalità in cui ADC converte continuamente l'input e genera un interrupt alla fine di ogni conversione (vettore associato: ADC_vect).

Passaggio 9: buffer di ingresso digitale

I pin di ingresso analogico di Arduino possono anche essere usati come pin di I / O digitali, quindi hanno un buffer di input per le funzioni digitali. Se vogliamo usarli come pin analogici, dovresti disabilitare questa funzione.

L'invio di un segnale analogico a un pin digitale induce a alternare tra gli stati HIGH e LOW, specialmente se il segnale è vicino al confine tra i due stati; questa commutazione induce un po 'di rumore ai circuiti vicini come l'ADC stesso (e induce un consumo di energia più elevato).

Per disabilitare il buffer digitale dovremmo impostare i bit ADCnD del registro DIDR0:

sbi (DIDR0, ADC5D);
sbi (DIDR0, ADC4D);
sbi (DIDR0, ADC3D);
sbi (DIDR0, ADC2D);
sbi (DIDR0, ADC1D);
sbi (DIDR0, ADC0D);

Passaggio 10: impostazione dell'ADC

Nello schizzo, ho scritto una funzione di inizializzazione che imposta tutti i parametri del funzionamento dell'ADC. Mentre tendo a scrivere codice pulito e commentato, supererò la funzione qui. Possiamo fare riferimento al passaggio precedente e ai commenti per il significato dei registri. void initADC (vuoto)
= (ADCPIN & 0x07);

// ------------------------------------------------ ---------------------
// Impostazioni ADCSRA
// ------------------------------------------------ ---------------------
// Scrivere questo bit su uno abilita l'ADC. Scrivendolo a zero, il
// ADC è disattivato. Disattivazione dell'ADC mentre è in corso una conversione
// progress, terminerà questa conversione.
cbi (ADCSRA, ADEN);
// In modalità Conversione singola, scrivi questo bit su uno per iniziare ciascuno
// conversione. In modalità Free Running, scrivi questo bit su uno per avviare
// prima conversione. La prima conversione dopo che ADSC è stato scritto
// dopo che l'ADC è stato abilitato o se ADSC è scritto nello stesso
// l'ora quando l'ADC è abilitato, richiederà 25 cicli di clock ADC anziché
// il normale 13. Questa prima conversione esegue l'inizializzazione di
// ADC. ADSC leggerà fino a quando è in corso una conversione.
// Al termine della conversione, torna a zero. Scrivere zero in
// questo bit non ha alcun effetto.
cbi (ADCSRA, ADSC);
// Quando questo bit viene scritto su uno, l'attivazione automatica dell'ADC è
// abilitato. L'ADC inizierà una conversione su un lato positivo del
// segnale di trigger selezionato. La sorgente di trigger viene selezionata impostando
// i bit di selezione trigger ADC, ADTS in ADCSRB.
sbi (ADCSRA, aDate);
// Quando questo bit è scritto su uno e l'I-bit in SREG è impostato, il
// L'allarme completo di conversione ADC è attivato.
sbi (ADCSRA, ADIE);
// Questi bit determinano il fattore di divisione tra l'orologio di sistema
// frequenza e clock di ingresso nell'ADC.
// ADPS2 ADPS1 ADPS0 Division Factor
// 0 0 0 2
// 0 0 1 2
// 0 1 0 4
// 0 1 1 8
// 1 0 0 16
// 1 0 1 32
// 1 1 0 64
// 1 1 1 128
sbi (ADCSRA, ADPS2);
sbi (ADCSRA, ADPS1);
sbi (ADCSRA, ADPS0);

// ------------------------------------------------ ---------------------
// Impostazioni ADCSRB
// ------------------------------------------------ ---------------------
// Quando questo bit è scritto in uno logico e l'ADC è spento
// (ADEN in ADCSRA è zero), il multiplexer ADC seleziona il negativo
// input per il comparatore analogico. Quando questo bit è scritto zero logico,
// AIN1 viene applicato all'ingresso negativo del comparatore analogico.
cbi (ADCSRB, ACME);
// Se ADATE in ADCSRA è scritto su uno, il valore di questi bit
// seleziona quale sorgente attiverà una conversione ADC. Se ADATE è
// cancellato, le impostazioni ADTS2: 0 non avranno alcun effetto. Una conversione lo farà
// essere attivato dal fronte di salita del Flag di interruzione selezionato. Nota
// quel passaggio da una sorgente di trigger che viene cancellata a un trigger
// sorgente impostata, genererà un vantaggio positivo sul trigger
// segnale. Se è impostato ADEN in ADCSRA, verrà avviata una conversione.
// Passare alla modalità Free Running (ADTS [2: 0] = 0) non causerà a
// evento trigger, anche se è impostato il flag di interruzione ADC.
// ADTS2 ADTS1 ADTS0 Sorgente del trigger
// 0 0 0 Modalità corsa libera
// 0 0 1 Comparatore analogico
// 0 1 0 Richiesta di interruzione esterna 0
// 0 1 1 Timer / Contatore0 Confronta corrispondenza A
// 1 0 0 Timer / Contatore0 Overflow
// 1 0 1 Timer / Contatore1 Confronta corrispondenza B
// 1 1 0 Timer / Counter1 Overflow
// 1 1 1 Timer / Counter1 Capture Event
cbi (ADCSRB, ADTS2);
cbi (ADCSRB, ADTS1);
cbi (ADCSRB, ADTS0);

// ------------------------------------------------ ---------------------
// Impostazioni DIDR0
// ------------------------------------------------ ---------------------
// Quando questo bit è scritto in uno logico, il buffer di input digitale su
// il pin ADC corrispondente è disabilitato. Il corrispondente PIN Register
// bit leggerà sempre come zero quando questo bit è impostato. Quando un analogo
// il segnale viene applicato al pin ADC5..0 e l'ingresso digitale da questo
// pin non è necessario, questo bit deve essere scritto in logica uno per ridurlo
// consumo di energia nel buffer di ingresso digitale.
// Nota che i pin ADC ADC7 e ADC6 non hanno buffer di ingresso digitale,
// e quindi non richiedono bit di disabilitazione dell'ingresso digitale.
sbi (DIDR0, ADC5D);
sbi (DIDR0, ADC4D);
sbi (DIDR0, ADC3D);
sbi (DIDR0, ADC2D);
sbi (DIDR0, ADC1D);
sbi (DIDR0, ADC0D);

Passaggio 11: funzionamento del comparatore analogico

Analog Comparator è un modulo interno del microcontrollore e confronta i valori di ingresso sul pin positivo (Digital Pin 6) e negativo (Digital Pin 7). Quando la tensione sul pin positivo è superiore alla tensione sul pin negativo AIN1, il comparatore analogico emette un 1 nel bit ACO del registro ACSR.

Facoltativamente, il comparatore può attivare un interrupt, esclusivo del comparatore analogico. Il vettore associato è ANALOG_COMP_vect.

Possiamo anche impostare l'interrupt da lanciare su un fronte di salita, un fronte di discesa o su un interruttore dello stato.

Il comparatore analogico è proprio ciò di cui abbiamo bisogno per innescare la connessione del segnale di ingresso al pin 6, ora ciò che resta è un livello di soglia sul pin 7.

Passaggio 12: impostazione del comparatore analogico

Nello schizzo, ho scritto un'altra funzione di inizializzazione che imposta tutti i parametri del funzionamento del comparatore analogico. Lo stesso problema relativo ai buffer digitali ADC si applica al comparatore analogico, come possiamo vedere in fondo alla routine.

void initAnalogComparator (void)
{
// ------------------------------------------------ ---------------------
// Impostazioni ACSR
// ------------------------------------------------ ---------------------
// Quando questo bit è scritto in uno logico, la potenza dell'analogico
// Il comparatore è spento. Questo bit può essere impostato in qualsiasi momento per girare
// fuori dal comparatore analogico. Ciò ridurrà il consumo di energia in
// Modalità attiva e inattiva. Quando si cambia il bit ACD, l'analogico
// L'interruzione del comparatore deve essere disabilitata cancellando il bit ACIE
// ACSR. Altrimenti può verificarsi un interrupt quando il bit viene modificato.
cbi (ACSR, ACD);
// Quando questo bit è impostato, una tensione di riferimento di larghezza di banda fissa sostituisce
// input positivo per il comparatore analogico. Quando questo bit viene cancellato,
// AIN0 viene applicato all'ingresso positivo del comparatore analogico. quando
// il riferimento alla larghezza di banda viene utilizzato come input per il comparatore analogico
// richiederà un certo tempo per stabilizzare la tensione. Altrimenti
// stabilizzata, la prima conversione potrebbe dare un valore errato.
cbi (ACSR, ACBG);
// Quando il bit ACIE è scritto uno logico e l'I-bit nello stato
// Il registro è impostato, l'interrupt del comparatore analogico è attivato.
// Quando si scrive zero logico, l'interrupt è disabilitato.
cbi (ACSR, ACIE);
// Quando viene scritto uno logico, questo bit abilita la funzione di acquisizione dell'input
// in Timer / Contatore1 per essere attivato dal comparatore analogico. Il
// L'output del comparatore è in questo caso collegato direttamente all'ingresso
// cattura la logica del front-end, facendo in modo che il comparatore utilizzi il rumore
// annulla e seleziona le caratteristiche dell'ingresso Timer / Counter1
// Cattura interruzione. Quando si scrive zero logico, nessuna connessione tra
// esiste il comparatore analogico e la funzione di acquisizione dell'ingresso. Per
// fa in modo che il comparatore attivi la cattura ingresso timer / contatore1
// interrupt, il bit ICIE1 nel registro della maschera di interruzione del timer
// (TIMSK1) deve essere impostato.
cbi (ACSR, ACIC);
// Questi bit determinano quali eventi del comparatore attivano l'analogico
// Interruzione comparatore.
// ACIS1 Modalità ACIS0
// 0 0 Attiva / disattiva
// 0 1 Riservato
// 1 0 Fronte di discesa
// 1 1 Fronte di salita
sbi (ACSR, ACIS1);
sbi (ACSR, ACIS0);

// ------------------------------------------------ ---------------------
// Impostazioni DIDR1
// ------------------------------------------------ ---------------------
// Quando questo bit è scritto in uno logico, il buffer di input digitale su
// Il pin AIN1 / 0 è disabilitato. Il bit corrispondente del registro PIN sarà
// legge sempre come zero quando questo bit è impostato. Quando un segnale analogico è
// applicato al pin AIN1 / 0 e l'ingresso digitale da questo pin non lo è
// necessario, questo bit dovrebbe essere scritto in modo logico uno per ridurre la potenza
// consumo nel buffer di input digitale.
sbi (DIDR1, AIN1D);
sbi (DIDR1, AIN0D);
}

Passaggio 13: soglia

Ricordando ciò che abbiamo detto sul trigger, possiamo implementare queste due soluzioni per la soglia:
  • usando un trimmer possiamo impostare manualmente un livello di tensione;
  • utilizzando il PWM di Arduino possiamo impostare il livello tramite software.
Nell'immagine possiamo vedere l'implementazione hardware della soglia in entrambi i percorsi.

Per la selezione manuale è sufficiente un potenziometro multigiro tra +5 V e GND.

Mentre per la selezione del software abbiamo bisogno di un filtro passa-basso che filtra un segnale PWM proveniente dall'Arduino. I segnali PWM (ne seguiranno altri) sono segnali quadrati con una frequenza costante ma una larghezza di impulso variabile. Questa variabilità porta un valore medio variabile del segnale che può essere estratto con un filtro passa-basso. Una buona frequenza di taglio per il filtro è circa un centesimo della frequenza PWM e ho scelto circa 560 Hz.

Dopo le due sorgenti di soglia ho inserito un paio di pin che mi permettono di selezionare, con un ponticello, quale sorgente volevo. Dopo la selezione ho anche aggiunto un follower di emettitore per disaccoppiare le fonti dal pin di Arduino.

Passaggio 14: Funzionamento della modulazione della larghezza dell'impulso

Come affermato in precedenza, un segnale PWM (Pulse Width Modulation) è un segnale quadrato con frequenza fissa ma larghezza variabile. Nell'immagine vediamo un esempio. Su ciascuna riga è presente uno di tali segnali con un ciclo di lavoro diverso (vale a dire la parte del periodo in cui il segnale è alto). Prendendo il segnale medio per un periodo, otteniamo la linea rossa che corrisponde al ciclo di lavoro rispetto al massimo del segnale.

"Prendere la media di un segnale" elettronicamente può essere tradotto in "passandolo a un filtro passa-basso", come visto nel passaggio precedente.

In che modo Arduino genera un segnale PWM? C'è un ottimo tutorial su PWM qui:
//arduino.cc/en/Tutorial/SecretsOfArduinoPWM
Vedremo solo i punti necessari per questo progetto.

In ATMega328P ci sono tre timer che possono essere utilizzati per generare segnali PWM, ognuno con caratteristiche diverse che è possibile utilizzare. Ad ogni timer corrispondono due registri chiamati Output Compare Registers A / B (OCRnx) che vengono utilizzati per impostare il duty cycle del segnale.

Per quanto riguarda l'ADC c'è un prescaler (vedi immagine), che rallenta l'orologio principale per avere un controllo preciso della frequenza PWM. L'orologio rallentato viene inviato a un contatore che incrementa un Timer / Counter Register (TCNTn). Questo registro viene continuamente confrontato con l'OCRnx, quando sono uguali viene inviato un segnale a un generatore di forme d'onda che genera un impulso sul pin di uscita. Quindi il trucco sta impostando il registro OCRnx su un valore per cambiare il valore medio del segnale.

Se vogliamo un segnale a 5 V (massimo) dobbiamo impostare un duty cycle del 100% o 255 nell'OCRnx (massimo per un numero a 8 bit), mentre se vogliamo un segnale a 0, 5 V dobbiamo impostare un duty cycle del 10% o un 25 nell'OCRnx.

Poiché l'orologio deve riempire il registro TCNTn prima di iniziare dall'inizio per un nuovo impulso, la frequenza di uscita del PWM è:

f = (Main clock) / prescaler / (TCNTn massimo)

esempio per il Timer 0 e 2 (8 bit) senza prescaler sarà: 16 MHz / 256 = 62, 5 KHz mentre per il Timer 1 (16 bit) sarà 16 MHz / 65536 = 244 Hz.

Ho deciso di utilizzare il Timer numero 2 perché
  • Il timer 0 viene utilizzato internamente dall'IDE di Arduino per funzioni come millis ();
  • Il timer 1 ha una frequenza di uscita troppo lenta perché è un timer a 16 bit.

In ATMega328P ci sono diversi tipi di modalità operativa dei timer, ma quello che volevo era quello Fast PWM senza prescaling per ottenere la massima frequenza di uscita possibile.

Passaggio 15: impostazione di PWM

Nello schizzo, ho scritto un'altra funzione di inizializzazione che imposta tutti i parametri del funzionamento del timer e inizializza un paio di pin. void initPins (vuoto)
{
// ------------------------------------------------ ---------------------
// Impostazioni TCCR2A
// ------------------------------------------------ ---------------------
// Questi bit controllano il comportamento del pin Output Compare (OC2A). Se uno o
// sono impostati entrambi i bit COM2A1: 0, l'uscita OC2A ha la precedenza su
// normale funzionalità della porta del pin I / O a cui è collegato.
// Tuttavia, si noti che il bit DDR (Data Direction Register)
// corrispondente al pin OC2A deve essere impostato per abilitare il
// driver di output.
// Quando OC2A è collegato al pin, la funzione di COM2A1: 0 bit
// dipende dall'impostazione di WGM22: 0 bit.
//
// Modalità PWM veloce
// COM2A1 COM2A0
// 0 0 Funzionamento normale della porta, OC2A disconnesso.
// 0 1 WGM22 = 0: operazione porta normale, OC0A disconnesso.
// WGM22 = 1: attiva / disattiva OC2A su Confronta corrispondenza.
// 1 0 Cancella OC2A su Confronta partita, imposta OC2A su BOTTOM
// 1 1 Cancella OC2A su Confronta partita, cancella OC2A su BOTTOM
cbi (TCCR2A, COM2A1);
cbi (TCCR2A, COM2A0);
sbi (TCCR2A, COM2B1);
cbi (TCCR2A, COM2B0);

// Combinati con il bit WGM22 trovato nel registro TCCR2B, questi bit
// controlla la sequenza di conteggio del contatore, la fonte per il massimo
// (TOP) valore contatore e quale tipo di generazione della forma d'onda utilizzare
// Le modalità di funzionamento supportate dall'unità Timer / Contatore sono:
// - Modalità normale (contatore),
// - Cancella timer in modalità Confronta corrispondenza (CTC),
// - due tipi di modalità PWM (Pulse Width Modulation).
//
// Modalità WGM22 WGM21 WGM20 Funzionamento TOP
// 0 0 0 0 Normale 0xFF
// 1 0 0 1 PWM 0xFF
// 2 0 1 0 CTC OCRA
// 3 0 1 1 PWM 0xFF veloce
// 4 1 0 0 Riservato -
// 5 1 0 1 PWM OCRA
// 6 1 1 0 Riservato -
// 7 1 1 1 PWM OCRA veloce
cbi (TCCR2B, WGM22);
sbi (TCCR2A, WGM21);
sbi (TCCR2A, WGM20);

// ------------------------------------------------ ---------------------
// Impostazioni TCCR2B
// ------------------------------------------------ ---------------------
// Il bit FOC2A è attivo solo quando i bit WGM specificano un non PWM
// modalità.
// Tuttavia, per garantire la compatibilità con i dispositivi futuri, questo bit
// deve essere impostato su zero quando TCCR2B è scritto quando si opera in PWM
// modalità. Quando si scrive uno logico nel bit FOC2A, uno immediato
// Il confronto della corrispondenza viene forzato sull'unità di generazione della forma d'onda. OC2A
// l'output viene modificato in base all'impostazione COM2A1: 0 bit. Nota che
// il bit FOC2A è implementato come strobo. Pertanto è il valore
// presente in COM2A1: 0 bit che determina l'effetto di
// confronto forzato.
// Uno strobo FOC2A non genererà alcun interrupt, né si cancellerà
// il timer in modalità CTC usando OCR2A come TOP.
// Il bit FOC2A viene sempre letto come zero.
cbi (TCCR2B, FOC2A);
cbi (TCCR2B, FOC2B);

// I tre bit Clock Select selezionano la sorgente di clock da utilizzare
// il timer / contatore.
// CS22 CS21 CS20 Prescaler
// 0 0 0 Nessuna sorgente di clock (Timer / Contatore arrestato).
// 0 0 1 Nessun prescaling
// 0 1 0 8
// 0 1 1 32
// 1 0 0 64
// 1 0 1 128
// 1 1 0 256
// 1 1 1 1024
cbi (TCCR2B, CS22);
cbi (TCCR2B, CS21);
sbi (TCCR2B, CS20);

pinMode (errorPin, OUTPUT);
pinMode (resholdPin, OUTPUT);

analogWrite (resholdPin, 127);
}

Passaggio 16: variabili volatili

Non ricordo dove, ma leggo che le variabili che sono modificate all'interno di un ISR dovrebbero essere dichiarate volatili .

Le variabili volatili sono variabili che possono cambiare nel tempo, anche se il programma in esecuzione non le modifica. Proprio come i registri di Arduino che possono cambiare valore per alcuni interventi esterni.

Perché il compilatore vuole conoscere tali variabili? Questo perché il compilatore cerca sempre di ottimizzare il codice che scriviamo, per renderlo più veloce e lo modifica un po ', cercando di non cambiarne il significato. Se una variabile cambia da sola potrebbe sembrare al compilatore che non viene mai modificata durante l'esecuzione, per esempio, di un ciclo e potrebbe ignorarla; mentre potrebbe essere cruciale che la variabile cambi il suo valore. Quindi dichiarando variabili volatili impedisce al compilatore di modificare il codice che li riguarda.

Per qualche informazione in più suggerisco di leggere la pagina di Wikipedia: //it.wikipedia.org/wiki/Volatile_variable

Passaggio 17: scrivere il kernel dello schizzo

Finalmente siamo arrivati ​​al kernel del programma!

Come abbiamo visto prima, volevo un'acquisizione continua e ho scritto la routine del servizio di interruzione ADC per archiviare continuamente nel buffer circolare i dati. Si ferma ogni volta che raggiunge l'indice uguale a stopIndex. Il buffer è implementato come circolare utilizzando l'operatore modulo.

// ------------------------------------------------ -----------------------------
// Interruzione completa conversione ADC
// ------------------------------------------------ -----------------------------
ISR (ADC_vect)
{
// Quando viene letto ADCL, il registro dati ADC non viene aggiornato fino a ADCH
// viene letto. Di conseguenza, se il risultato viene lasciato regolato e non di più
// è richiesta una precisione di 8 bit, è sufficiente leggere ADCH.
// In caso contrario, è necessario leggere prima ADCL, quindi ADCH.
ADCBuffer [ADCCounter] = ADCH;

ADCCounter = (ADCCounter + 1)% ADCBUFFERSIZE;

if (aspetta)
{
if (stopIndex == ADCCounter)
{
// Congela la situazione
// Disabilita ADC e interrompe la modalità di conversione in esecuzione libera
cbi (ADCSRA, ADEN);

congelare = vero;
}
}
}

La routine di servizio di interrupt del comparatore analogico (che viene chiamata quando un segnale supera la soglia) si disabilita e dice all'ISR ADC di avviare la fase di attesa e imposta lo stopIndex.

// ------------------------------------------------ -----------------------------
// Interruzione del comparatore analogico
// ------------------------------------------------ -----------------------------
ISR (ANALOG_COMP_vect)
{
// Disabilita l'interruzione del comparatore analogico
cbi (ACSR, ACIE);

// Attiva errorPin
// digitalWrite (errorPin, HIGH);
sbi (PORTB, PORTB5);

wait = true;
stopIndex = (ADCCounter + waitDuration)% ADCBUFFERSIZE;
}


Questo è stato davvero facile dopo tutto quel radicamento!

Passaggio 18: formazione del segnale in entrata

Vediamo ora l'hardware. Il circuito può sembrare complicato ma è davvero semplice.
  • C'è un resistore da 1 MΩ sull'ingresso, per dare un riferimento di massa al segnale e avere un ingresso ad alta impedenza. Un'alta impedenza "simula" un circuito aperto se lo si collega a un impedenza inferiore, quindi la presenza del Girino non interferisce troppo con il circuito che si desidera misurare.
  • Dopo il resistore c'è un seguace dell'emettitore per disaccoppiare il segnale e proteggere i seguenti componenti elettronici.
  • C'è un semplice offset che genera un livello di 2, 5 V con un divisore di tensione. È collegato a un condensatore per stabilizzarlo.
  • C'è un somma-amplificatore non invertente che somma il segnale in ingresso e l'offset. Ho usato questa tecnica perché volevo essere in grado di vedere anche segnali negativi, in quanto l'Arduino ADC poteva vedere segnali solo tra 0 V e 5 V.
  • Dopo l'amplificatore somma c'è un altro seguace dell'emettitore.
  • Un ponticello ci consente di decidere se alimentare o meno il segnale con un offset.
L'amplificatore operazionale che intendevo utilizzare era un LM324 in grado di funzionare tra 0 V e 5 V ma anche tra, per esempio, tra -12 V e 12 V. Questo ci dà più possibilità con gli alimentatori. Ho anche provato un TL084 che è molto più veloce dell'LM324 ma richiede un doppio alimentatore. Entrambi hanno lo stesso pinout, quindi possono essere modificati senza alcuna modifica del circuito.

Passo 19: Bypass condensatori

I condensatori di bypass sono condensatori utilizzati per filtrare gli alimentatori dei circuiti integrati (IC) e devono essere posizionati il ​​più vicino possibile ai pin di alimentazione dell'IC. Sono usati di solito in coppia, una ceramica e una elettrolitica perché possono filtrare frequenze diverse.

Passaggio 20: fonti di alimentazione

Ho usato un doppio alimentatore per TL084 che può essere convertito in un singolo alimentatore per LM324.

Nell'immagine possiamo vedere che ho usato un paio di regolatori di tensione a 7812, per +12 V e a 7912, per -12 V. I condensatori sono, come al solito, usati per stabilizzare i livelli e i loro valori sono quelli suggeriti nei fogli dati.

Ovviamente per avere un ± 12 V dobbiamo avere almeno circa 30 V sull'ingresso perché i regolatori di tensione richiedono un ingresso più alto per fornire un'uscita stabilizzata. Dato che non avevo un tale alimentatore, ho usato il trucco di utilizzare due alimentatori da 15 V in serie. Uno dei due è collegato al connettore di alimentazione di Arduino (quindi alimenta sia Arduino che il mio circuito) e l'altro direttamente al circuito.

Non è un errore collegare i +15 V del secondo alimentatore al GND del primo! In questo modo otteniamo un -15 V con alimentatori isolati .

Se non voglio portare in giro un Arduino e due alimentatori, posso comunque usare il +5 V fornito dall'Arduino cambiando quei jumper (e usando l'LM324).

Passaggio 21: Preparazione di un connettore di schermatura

Sono sempre stato infastidito dai connettori che ho potuto trovare per creare uno scudo Arduino, perché hanno sempre pin troppo corti e le schede che utilizzo possono essere saldate solo su un lato. Quindi ho inventato un piccolo trucco per allungare i pin in modo che possano essere saldati e inseriti nell'Arduino.

Inserendo la pin strip nella scheda, come nella foto, possiamo spingere i pin, per averli solo su un lato della plastica nera. Quindi possiamo saldarli dallo stesso lato in cui verranno inseriti nell'Arduino.

Passaggio 22: saldatura e test

Non sono in grado di mostrarti tutta la procedura di saldatura del circuito perché ha subito un sacco di tentativi ed errori. Alla fine è diventato un po 'disordinato ma non troppo male, anche se non mostrerò il lato inferiore perché è davvero disordinato.

In questa fase non c'è molto da dire perché ho già spiegato in dettaglio tutte le parti del circuito. L'ho provato con un oscilloscopio, che un amico mi ha prestato, per vedere i segnali in ogni punto del circuito. Sembra che tutto funzioni bene e sono abbastanza soddisfatto.

Il connettore per il segnale in ingresso potrebbe sembrare un po 'strano per qualcuno che non proviene dalla fisica dell'alta energia, è un connettore LEMO. È il connettore standard per i segnali nucleari, almeno in Europa come negli Stati Uniti ho visto principalmente connettori BNC.

Passaggio 23: test dei segnali

Per testare il circuito e il Data AcQuisition (DAQ) ho usato un secondo Arduino con un semplice schizzo che genera impulsi quadrati con lunghezze diverse. Ho anche scritto uno script Python che parla con Girino e gli dice di acquisire alcune serie di dati e di salvarne una su un file.
Sono entrambi collegati a questo passaggio.

allegati

  • Scarica TaraturaTempi.ino
  • readgirino.py Scarica

Passaggio 24: calibrazione dell'ora

Usando i segnali di test ho calibrato la scala orizzontale dei grafici. Misurando le larghezze degli impulsi (che sono noti perché sono stati generati) e tracciando le larghezze degli impulsi misurati rispetto ai valori noti, otteniamo un grafico, si spera, lineare. In questo modo per ogni impostazione prescaler abbiamo la calibrazione del tempo per tutte le velocità di acquisizione.

Sulle immagini possiamo vedere tutti i dati che ho preso e analizzato. Il diagramma "Piste inclinate" è il più interessante perché ci dice l'effettivo tasso di acquisizione del mio sistema ad ogni impostazione prescaler. Le pendenze sono state misurate come un numero [ch / ms] ma questo equivale a un [kHz], quindi i valori delle pendenze sono in realtà kHz o anche kS / s (chilo di campioni al secondo). Ciò significa che con il prescaler impostato su 8 otteniamo un tasso di acquisizione di:

(154 ± 2) kS / s

Non male, eh?

Mentre dalla trama "Intercettazioni y adattate" otteniamo una visione della linearità del sistema. Tutte le intercettazioni y dovrebbero essere zero perché a un segnale con lunghezza zero dovrebbe corrispondere un impulso con lunghezza zero. Come possiamo vedere nel grafico, sono tutti compatibili con zero, ma non con il set di dati 18 prescaler. Questo set di dati, tuttavia, è il peggiore perché contiene solo due dati e la sua calibrazione non può essere considerata attendibile.

Segue una tabella con i tassi di acquisizione per ciascuna impostazione prescaler.

prescalerTasso di acquisizione [kS / s]
1289, 74 ± 0, 04
6419, 39 ± 0, 06
3237, 3 ± 0, 6
1675, 5 ± 0, 3
8153 ± 2
Gli errori citati provengono dal motore di adattamento di Gnuplot e non ne sono sicuro.

Ho anche provato un adattamento non ponderato dei tassi perché si può vedere che raddoppiano all'incirca quando la metà del prescaling si dimezza, questo sembra una legge di proporzionalità inversa. Quindi ho adattato le tariffe rispetto alle impostazioni prescaler con una semplice legge di

y = a / x

Ho ottenuto un valore per un di

a = 1.223

con un χ² = 3, 14 e 4 gradi di libertà, ciò significa che la legge è accettata con un livello di confidenza del 95%!

Passo 25: Fatto! (Quasi)

Alla fine di questa lunga esperienza, mi sento molto soddisfatto perché
  • Ho imparato molto sui microcontrollori in generale;
  • Ho imparato molto di più sull'Arduino ATMega328P;
  • Ho avuto un'esperienza pratica di acquisizione dei dati, non usando qualcosa di già fatto ma facendo qualcosa;
  • Ho realizzato un oscilloscopio amatoriale che non è poi così male.
Spero che questa guida sia utile a chiunque la legga. Volevo scriverlo così dettagliatamente perché ho imparato tutto nel modo più duro (navigando in Internet, leggendo la scheda tecnica e con molte prove ed errori) e vorrei risparmiare qualcuno da quell'esperienza.

Passaggio 26: continuare ...

Tuttavia, il progetto è tutt'altro che completato. Quello che manca è:
  1. Un test con diversi segnali analogici (mi manca un generatore di segnali analogici);
  2. Un'interfaccia utente grafica per il lato computer.
Mentre per il punto 1. Non sono sicuro di quando sarà completato, perché non ho intenzione di acquistarne / costruirne uno nel prossimo futuro.

Per il punto 2. la situazione potrebbe essere migliore. Qualcuno è disposto ad aiutarmi in questo? Ho trovato un bel oscilloscopio Python qui:
//www.phy.uct.ac.za/courses/python/examples/moreexamples.html#oscilloscope-and-spectrum-analyser
Vorrei modificarlo per adattarlo a Girino, ma sto accettando suggerimenti.

Articoli Correlati