Girino - Snelle Arduino-oscilloscoop

Ik ben een natuurkundige en het leukste aan werken op dit gebied is dat ik mijn eigen instrumenten mag bouwen. Met deze manier van denken besloot ik om een ​​zelfgebouwde Arduino-oscilloscoop te bouwen. Dit instructable is geschreven met als doel een beetje te leren over microcontrollers en data-acquisitie. Dit is een extreem project omdat ik zoveel mogelijk snelheid uit Arduino wilde persen, ik heb geen andere Arduino-oscilloscoop zo snel gezien als deze.

Enige tijd geleden werkte ik aan een Arduino-project en ik moest kijken of het uitgangssignaal aan de specificaties voldeed. Zo heb ik wat tijd op het internet doorgebracht met het zoeken naar al geïmplementeerde Arduino Oscilloscopen, maar ik vond het niet leuk wat ik vond. De projecten die ik vond waren meestal samengesteld uit een grafische gebruikersinterface voor de computer die is geschreven in Processing en een zeer eenvoudige arduino-schets. De schetsen waren zoiets als: ongeldige setup () {

Serial.begin (9600);

}

leegte lus () {

int val = analogRead (ANALOG_IN);

Serial.println (val);

} Deze aanpak is niet verkeerd en ik wil niemand beledigen, maar dit is te traag voor mij. De seriële poort is traag en het verzenden van elk resultaat van een analogRead () erdoorheen is een bottleneck.

Ik studeer al geruime tijd Waveform Digitizers en ik weet redelijk goed hoe ze werken, dus ik kreeg er inspiratie van. Dit waren de uitgangspunten van de oscilloscoop die ik wilde maken:

  • het binnenkomende signaal moet worden losgekoppeld van de arduino om het te behouden;
  • met een offset van het signaal is het mogelijk om negatieve signalen te zien;
  • de gegevens moeten worden gebufferd;
  • een hardwaretrigger is vereist om de signalen op te vangen;
  • een cirkelvormige buffer kan de signaalvorm geven voorafgaand aan de trigger (meer hierover op dit punt);
  • door gebruik te maken van functies met een lagere hendel die de standaardfuncties maken, wordt het programma sneller uitgevoerd.

De schets voor de Arduino is bij deze stap gevoegd, samen met het schema van het circuit dat ik heb gemaakt.

De naam die ik bedacht, Girino, is een frivole woordspeling in het Italiaans. Giro betekent rotatie en met het achtervoegsel -ino krijg je een kleine rotatie, maar Girino betekent ook kikkervisje . Zo kreeg ik een naam en een mascotte.

Stap 1: Disclaimer

DE AUTEUR VAN DEZE INSTRUCTABLE GEEFT GEEN GARANTIE VAN GELDIGHEID EN GEEN ENKELE GARANTIE .

Elektronica kan gevaarlijk zijn als u niet weet wat u doet en de auteur de geldigheid van de hier gevonden informatie niet kan garanderen. Dit is geen professioneel advies en alles wat in dit instructable staat, kan onnauwkeurig, misleidend, gevaarlijk of verkeerd zijn. Vertrouw niet op hier gevonden informatie zonder onafhankelijke verificatie.

Het is aan u om alle informatie te verifiëren en te controleren of u uzelf of iemand anders niet blootstelt aan enige schade of iets blootstelt aan enige schade; Ik neem geen verantwoordelijkheid. U moet zelf de juiste veiligheidsmaatregelen volgen als u dit project wilt reproduceren.

Gebruik deze gids op eigen risico!

Stap 2: Wat je nodig hebt

Wat we echt nodig hebben voor dit project is een Arduino-bord en het gegevensblad van de ATMega328P.
Het gegevensblad vertelt ons hoe de microcontroller werkt en het is erg belangrijk om het te behouden als we een lagere bedieningshendel willen.

De datasheet is hier te vinden: //www.atmel.com/Images/doc8271.pdf

De hardware die ik aan de Arduino heb toegevoegd, is gedeeltelijk nodig, het doel is alleen om het signaal voor de ADC te vormen en een spanningsniveau voor de trigger te bieden. Als je wilt, kun je het signaal rechtstreeks naar de Arduino sturen en een spanningsreferentie gebruiken die wordt gedefinieerd door een spanningsdeler, of zelfs de 3, 3 V die door de Arduino zelf wordt gegeven.

Stap 3: Debug output

Ik stop meestal veel debug output in mijn programma's omdat ik alles wil bijhouden wat er gebeurt; het probleem met Arduino is dat we geen stdout hebben om naar te schrijven. Ik besloot de seriële poort als stdout te gebruiken.

Houd er echter rekening mee dat deze aanpak niet altijd werkt! Omdat het schrijven naar de seriële poort enige tijd kost voor de uitvoering en het gedurende een bepaalde tijd verstandige routine drastisch kan veranderen.

Ik definieer debugging-uitgangen meestal binnen een preprocessormacro, dus als de debug is uitgeschakeld, verdwijnen ze gewoon uit het programma en vertragen ze de uitvoering niet:
  • dprint (x); - Schrijft naar de seriële poort zoiets als: # x: 123
  • dshow ("Some string"); - Schrijft de string

Dit is de definitie:

#if DEBUG == 1
#define dprint (expression) Serial.print ("#"); Serial.print (#expressie); Serial.print (":"); Serial.println (expressie)
#define dshow (expressie) Serial.println (expressie)
#anders
#define dprint (expressie)
#define dshow (expressie)
#stop als

Stap 4: Registerbits instellen

Om snel te zijn, is het noodzakelijk om de microcontrollerfuncties met lagere hendelfuncties te manipuleren dan de standaardfuncties van de Arduino IDE. De interne functies worden beheerd via enkele registers, dat zijn verzamelingen van acht bits waarin elk iets speciaals regelt. Elk register bevat acht bits omdat de ATMega328P een 8-bits architectuur heeft.

De registers hebben een aantal namen die zijn gespecificeerd in het gegevensblad, afhankelijk van hun betekenis, zoals ADCSRA voor het ADC-instellingenregister A. Ook heeft elk betekenisvol deel van de registers een naam, zoals ADEN voor het ADC-inschakelbit in het ADCSRA-register.

Om hun bits in te stellen, konden we de gebruikelijke C-syntaxis gebruiken voor binaire algebra, maar ik vond op internet een paar macro's die erg mooi en schoon zijn:

// Definieert voor het instellen en wissen van registerbits
#ifndef cbi
#define cbi (sfr, bit) (_SFR_BYTE (sfr) & = ~ _BV (bit))
#stop als
#ifndef sbi
#define sbi (sfr, bit) (_SFR_BYTE (sfr) | = _BV (bit))
#stop als

Het gebruik ervan is heel eenvoudig, als we de Enable Bit van de ADC willen instellen op 1, kunnen we gewoon schrijven:

sbi (ADCSRA, ADEN);

Als we het op 0 willen zetten ( id est clear it) kunnen we gewoon schrijven:

cbi (ADCSRA, ADEN);

Stap 5: Wat zijn de onderbrekingen

Zoals we in de volgende stappen zullen zien, is het gebruik van interrupts vereist in dit project. Interrupts zijn signalen die de microcontroller vertellen om de uitvoering van de hoofdlus te stoppen en deze door te geven aan een aantal speciale functies. De afbeeldingen geven een idee van de programmastroom.

De functies die worden uitgevoerd worden Interrupt Service Routines (ISR) genoemd en zijn min of meer eenvoudige functies, maar hiervoor zijn geen argumenten nodig.

Laten we een voorbeeld zien, zoiets als het tellen van enkele pulsen. De ATMega328P heeft een analoge comparator met een bijbehorende interrupt die wordt geactiveerd wanneer een signaal een referentiespanning overschrijdt. Allereerst moet u de functie definiëren die wordt uitgevoerd:

ISR (ANALOG_COMP_vect)
{
counter ++;
}

Dit is heel eenvoudig, de instructie ISR () is een macro die de compiler vertelt dat de volgende functie een Interrupt Service Routine is. Terwijl ANALOG_COMP_vect Interrupt Vector wordt genoemd en het de compiler vertelt welke interrupt aan die routine is gekoppeld. In dit geval is het de Analog Comparator Interrupt. Dus elke keer dat de comparator een signaal ziet dat groter is dan een referentie, vertelt het de microcontroller om die code uit te voeren, id est in dit geval om die variabele te verhogen.

De volgende stap is het inschakelen van de bijbehorende onderbreking. Om het in te schakelen, moeten we het ACIE-bit (Analog Comparator Interrupt Enable) van het ACSR-register (Analog Comparator Setting Register) instellen:

sbi (ACSR, ACIE);

Op de volgende site kunnen we de lijst met alle onderbrekingsvectoren zien:
//www.nongnu.org/avr-libc/user-manual/group__avr__interrupts.html

Stap 6: Continu verwerven met een circulaire buffer

Het concept van het gebruik van een circulaire buffer is vrij eenvoudig:

Zoek continu totdat een signaal is gevonden en stuur het gedigitaliseerde signaal naar de computer.

Deze benadering maakt het mogelijk om de inkomende signaalvorm ook voor de triggergebeurtenis te hebben.


Ik heb wat diagrammen gemaakt om mezelf duidelijk te maken. De volgende punten verwijzen naar de afbeeldingen.
  • Op de eerste afbeelding kunnen we zien wat ik bedoel met continue acquisitie . We definiëren een buffer die de gegevens opslaat, in mijn geval een array met 1280 slots, en dan beginnen we continu het ADC-uitvoerregister (ADCH) te lezen en vullen de buffer met de gegevens. Wanneer we het einde van de buffer bereiken, beginnen we opnieuw vanaf het begin zonder het te wissen. Als we ons de matrix op een cirkelvormige manier voorstellen, is het gemakkelijk te zien wat ik bedoel.
  • Wanneer het signaal de drempel overschrijdt, wordt de analoge vergelijkingsonderbreking geactiveerd. Vervolgens starten we een wachtfase waarin we doorgaan met het verwerven van het signaal, maar een telling bijhouden van de ADC-cycli die zijn verstreken vanaf de Analog Comparator Interrupt.
  • Toen we wachtten op N cycli (met N <1280), bevriezen we de situatie en stoppen we de ADC-cycli. We eindigen dus met een buffer gevuld met de digitalisering van de signaal temporele vorm. Het grote deel hiervan is dat we ook de vorm hebben voorafgaand aan de triggergebeurtenis, omdat we daarvoor al aan het verwerven waren.
  • Nu kunnen we de hele buffer naar de seriële poort sturen in een blok met binaire gegevens, in plaats van de enkele ADC-reads te verzenden. Dit verminderde de overhead die nodig was om de gegevens te verzenden en het knelpunt van de schetsen die ik op internet vond.

Stap 7: Oscilloscoop-triggering

Een oscilloscoop toont op zijn display een signaal, daar zijn we het allemaal over eens, maar hoe kan hij het gestaag laten zien en niet laten springen over het scherm? Het heeft een interne trigger die het signaal altijd op dezelfde positie van het scherm kan tonen (of in ieder geval meestal), waardoor de illusie ontstaat van een stabiele plot.

De trigger is geassocieerd met een drempel die een sweep activeert wanneer het signaal er doorheen gaat. Een sweep is de fase waarin de oscilloscoop het signaal registreert en weergeeft. Na een sweep volgt een andere fase: de holdoff, waarin de oscilloscoop elk inkomend signaal afwijst. De holdoff-periode kan bestaan ​​uit een deel van de dode tijd, waarin de oscilloscoop geen signaal kan accepteren, en een deel dat door de gebruiker kan worden geselecteerd. De dode tijd kan verschillende oorzaken hebben, zoals op het scherm moeten tekenen of de gegevens ergens moeten opslaan.

Als we naar de afbeelding kijken, krijgen we een idee van wat er gebeurt.
  1. Signaal 1 overschrijdt de drempel en activeert de sweep;
  2. signaal 2 is binnen de sweep-tijd en wordt gepakt met de eerste;
  3. na de holdoff activeert signaal 3 de sweep opnieuw;
  4. in plaats daarvan wordt signaal 4 afgewezen omdat het binnen het holdoff-gebied valt.
De bestaansreden van de holdoff-fase is om te voorkomen dat ongewenste signalen in de sweep-regio komen. Het is een beetje lang om dit punt uit te leggen en het ontgaat het doel van dit instructable.

De moraal van dit verhaal is dat we nodig hebben:
  1. een drempelniveau waarmee we het inkomende signaal kunnen vergelijken;
  2. een signaal dat de microcontroller vertelt om de wachtfase te starten (zie voorgaande stap).
We hebben verschillende mogelijke oplossingen voor punt 1.:
  • met behulp van een trimmer kunnen we handmatig een spanningsniveau instellen;
  • met behulp van de PWM van de Arduino kunnen we het niveau door software instellen;
  • met behulp van de 3, 3 V die door de Arduino zelf wordt geleverd;
  • met behulp van de interne bangap-referentie kunnen we een vast niveau gebruiken.
Voor punt 2 hebben we de juiste oplossing: we kunnen de interrupt van de interne analoge comparator van de microcontroller gebruiken.

Stap 8: hoe de ADC werkt

De Arduino-microcontroller beschikt over een enkele 10-bit opeenvolgende benadering ADC. Vóór de ADC is er een analoge multiplexer waarmee we de signalen van verschillende pinnen en bronnen (maar slechts één tegelijk) naar de ADC kunnen sturen.

Opeenvolgende benadering ADC betekent dat de ADC 13 klokcycli nodig heeft om de conversie te voltooien (en 25 klokcycli voor de eerste conversie). Er is een kloksignaal speciaal voor de ADC dat wordt "berekend" vanuit de hoofdklok van de Arduino; dit komt omdat de ADC een beetje traag is en het tempo van de andere delen van de microcontroller niet kan bijhouden. Het vereist een ingangsklokfrequentie tussen 50 kHz en 200 kHz om een ​​maximale resolutie te krijgen. Als een lagere resolutie dan 10 bits nodig is, kan de ingangsklokfrequentie naar de ADC hoger zijn dan 200 kHz om een ​​hogere samplefrequentie te krijgen.

Maar hoeveel hogere tarieven kunnen we gebruiken? Er zijn een paar goede gidsen over de ADC bij de Open Music Labs die ik stel voor om te lezen:
  • //www.openmusiclabs.com/learning/digital/atmega-adc/
  • //www.openmusiclabs.com/learning/digital/atmega-adc/in-depth/
Omdat het mijn doel is om een ​​snelle oscilloscoop te krijgen, besloot ik de precisie te beperken tot 8-bits. Dit heeft verschillende bonussen:
  1. de databuffer kan meer data opslaan;
  2. je verspilt geen 6-bits RAM per datum;
  3. de ADC kan sneller verwerven.
De voorschrijver laat ons de frequentie, door een aantal factoren, delen door de ADPS0-1-2 bits van het ADCSRA-register in te stellen. Als we de plot van de precisie uit het Open Music Labs-artikel zien, kunnen we zien dat voor 8-bits precisie de frequentie tot 1, 5 MHz zou kunnen gaan, goed! Maar aangezien het vermogen van het veranderen van de voorschrijffactor ons in staat stelt de acquisitieratio te veranderen, kunnen we het ook gebruiken om de tijdschaal van de oscilloscoop te veranderen.

Er is een goede functie over de uitvoerregisters: we kunnen de aanpassing van conversiebits beslissen door de ADLAR-bit in het ADMUX-register in te stellen. Als het 0 is, zijn ze goed afgesteld en omgekeerd (zie de afbeelding). Omdat ik 8-bits precisie wilde, stelde ik deze in op 1 zodat ik alleen het ADCH-register kon lezen en de ADCL kon negeren.

Ik besloot om slechts één ingangskanaal te hebben om te voorkomen dat ik bij elke conversie heen en weer moet schakelen.

Een laatste ding over de ADC, het heeft verschillende hardloopmodi, elk met een andere triggerbron:
  • Vrijlopende modus
  • Analoge vergelijker
  • Externe onderbrekingsaanvraag 0
  • Timer / teller 0 Vergelijk wedstrijd A
  • Timer / teller 0 Overloop
  • Timer / teller1 Vergelijk wedstrijd B
  • Timer / teller1 Overloop
  • Timer / teller1 Vastleggebeurtenis
Ik was geïnteresseerd in de vrijlopende modus, een modus waarin de ADC de invoer continu converteert en aan het einde van elke conversie een onderbreking genereert (bijbehorende vector: ADC_vect).

Stap 9: Digitale invoerbuffers

De analoge input pinnen van de Arduino zijn ook te gebruiken als digitale I / O pinnen, hierdoor hebben ze een input buffer voor digitale functies. Als we ze als analoge pinnen willen gebruiken, moet je deze functie uitschakelen.

Door een analoog signaal naar een digitale pin te sturen, schakelt het tussen hoge en lage toestanden, vooral als het signaal de grens tussen de twee toestanden nadert; deze omschakeling veroorzaakt wat ruis in de nabije circuits zoals de ADC zelf (en veroorzaakt een hoger energieverbruik).

Om de digitale buffer uit te schakelen, moeten we de ADCnD-bits van het DIDR0-register instellen:

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

Stap 10: de ADC instellen

In de schets schreef ik een initialisatiefunctie die alle parameters van de ADC-werking instelt. Omdat ik de neiging heb om schone en becommentarieerde code te schrijven, zal ik hier de functie voorbijgaan. We kunnen verwijzen naar de voorgaande stap en naar de opmerkingen voor de betekenis van de registers. nietig initADC (nietig)
= (ADCPIN & 0x07);

// ------------------------------------------------ ---------------------
// ADCSRA-instellingen
// ------------------------------------------------ ---------------------
// Als u dit bit naar één schrijft, wordt de ADC ingeschakeld. Door het op nul te schrijven, de
// ADC is uitgeschakeld. De ADC uitschakelen terwijl een conversie bezig is
// voortgang, zal deze conversie beëindigen.
cbi (ADCSRA, ADEN);
// Schrijf in de Single Conversion-modus dit bit naar één om elk bit te starten
// conversie. In de Free Running-modus schrijft u dit bit naar een bit om het te starten
// eerste conversie. De eerste conversie nadat ADSC is geschreven
// nadat de ADC is ingeschakeld of als ADSC tegelijkertijd is geschreven
// tijd als de ADC is ingeschakeld, duurt 25 ADC klokcycli in plaats van
// de normale 13. Deze eerste conversie voert de initialisatie van de
// ADC. ADSC leest als één zolang er een conversie gaande is.
// Wanneer de conversie is voltooid, keert deze terug naar nul. Nul schrijven aan
// dit bit heeft geen effect.
cbi (ADCSRA, ADSC);
// Wanneer deze bit naar één wordt geschreven, is Auto Triggering van de ADC dat wel
// ingeschakeld. De ADC start een conversie op een positieve rand van de
// geselecteerd triggersignaal. De triggerbron wordt geselecteerd door in te stellen
// de ADC Trigger Select-bits, ADTS in ADCSRB.
sbi (ADCSRA, ADATE);
// Als dit bit naar één wordt geschreven en het I-bit in SREG wordt ingesteld, dan is de
// ADC Conversion Complete Interrupt is geactiveerd.
sbi (ADCSRA, ADIE);
// Deze bits bepalen de delingsfactor tussen de systeemklok
// frequentie en de ingangsklok naar de ADC.
// ADPS2 ADPS1 ADPS0-verdelingsfactor
// 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);

// ------------------------------------------------ ---------------------
// ADCSRB-instellingen
// ------------------------------------------------ ---------------------
// Als dit bit logische geschreven is en de ADC is uitgeschakeld
// (ADEN in ADCSRA is nul), de ADC-multiplexer selecteert het negatief
// invoer naar de analoge vergelijker. Als dit bit logisch nul is geschreven,
// AIN1 wordt toegepast op de negatieve ingang van de analoge comparator.
cbi (ADCSRB, ACME);
// Als ADATE in ADCSRA naar één wordt geschreven, is de waarde van deze bits
// selecteert welke bron een ADC-conversie zal activeren. Als ADATE is
// gewist, de ADTS2: 0-instellingen hebben geen effect. Een conversie zal
// worden geactiveerd door de opgaande flank van de geselecteerde Interrupt Flag. Notitie
// dat overschakelen van een triggerbron die is gewist naar een trigger
// bron die is ingesteld, genereert een positieve voorsprong op de trigger
// signaal. Als ADEN in ADCSRA is ingesteld, start dit een conversie.
// Overschakelen naar de modus Free Running (ADTS [2: 0] = 0) veroorzaakt geen a
// trigger-gebeurtenis, zelfs als de ADC Interrupt Flag is ingesteld.
// ADTS2 ADTS1 ADTS0 Triggerbron
// 0 0 0 Vrijloopmodus
// 0 0 1 Analoge vergelijker
// 0 1 0 Externe onderbrekingsaanvraag 0
// 0 1 1 Timer / teller 0 Vergelijk wedstrijd A
// 1 0 0 Timer / teller 0 Overloop
// 1 0 1 Timer / teller1 Vergelijk wedstrijd B
// 1 1 0 Timer / teller1 Overloop
// 1 1 1 Timer / teller1 Vastleggebeurtenis
cbi (ADCSRB, ADTS2);
cbi (ADCSRB, ADTS1);
cbi (ADCSRB, ADTS0);

// ------------------------------------------------ ---------------------
// DIDR0-instellingen
// ------------------------------------------------ ---------------------
// Wanneer dit bit logisch is geschreven, is de digitale invoerbuffer op de
// bijbehorende ADC-pin is uitgeschakeld. Het bijbehorende PIN-register
// bit zal altijd als nul worden gelezen wanneer dit bit is ingesteld. Bij een analoog
// signaal wordt toegevoerd aan de ADC5..0 pin en de digitale ingang hiervan
// pin is niet nodig, dit bit moet een logische geschreven zijn om te verkleinen
// stroomverbruik in de digitale invoerbuffer.
// Merk op dat ADC-pinnen ADC7 en ADC6 geen digitale invoerbuffers hebben,
// en daarom geen Digital Input Disable bits nodig.
sbi (DIDR0, ADC5D);
sbi (DIDR0, ADC4D);
sbi (DIDR0, ADC3D);
sbi (DIDR0, ADC2D);
sbi (DIDR0, ADC1D);
sbi (DIDR0, ADC0D);

Stap 11: hoe de analoge comparator werkt

De analoge comparator is een interne module van de microcontroller en vergelijkt de ingangswaarden op de positieve pin (digitale pin 6) en negatieve pin (digitale pin 7). Wanneer de spanning op de positieve pin hoger is dan de spanning op de negatieve pin AIN1, geeft de analoge comparator een 1 af in het ACO-bit van het ACSR-register.

Optioneel kan de comparator een interrupt activeren, exclusief voor de analoge comparator. De bijbehorende vector is ANALOG_COMP_vect.

We kunnen ook instellen dat de interrupt wordt gestart op een stijgende flank, dalende flank of op een staatsschakeling.

De analoge vergelijker is precies wat we nodig hebben voor het triggeren van het verbinden van het ingangssignaal met pin 6, wat nu overblijft is een drempelniveau op pin 7.

Stap 12: De analoge vergelijker instellen

In de schets schreef ik nog een initialisatiefunctie die alle parameters van de werking van de analoge comparator instelt. Hetzelfde probleem met ADC digitale buffers is van toepassing op de analoge vergelijker, zoals we onderaan de routine kunnen zien.

nietig initAnalogComparator (nietig)
{
// ------------------------------------------------ ---------------------
// ACSR-instellingen
// ------------------------------------------------ ---------------------
// Wanneer dit bit logische is geschreven, is de stroom naar de analoog
// Comparator is uitgeschakeld. Dit bit kan op elk moment worden ingesteld om te draaien
// van de analoge comparator. Dit vermindert het stroomverbruik in
// Actieve en inactieve modus. Bij het wijzigen van de ACD-bit, de analoge
// Comparator Interrupt moet worden uitgeschakeld door de ACIE-bit in te wissen
// ACSR. Anders kan er een onderbreking optreden wanneer de bit wordt gewijzigd.
cbi (ACSR, ACD);
// Wanneer deze bit is ingesteld, vervangt een referentiespanning met een vaste bandafstand de
// positieve invoer naar de analoge vergelijker. Als dit bit is gewist,
// AIN0 wordt toegepast op de positieve ingang van de analoge comparator. Wanneer
// de bandgap-verwijzing wordt gebruikt als invoer voor de analoge vergelijker
// zal enige tijd duren voordat de spanning is gestabiliseerd. Als niet
// gestabiliseerd, de eerste conversie kan een verkeerde waarde opleveren.
cbi (ACSR, ACBG);
// Als de ACIE-bit is geschreven logische één en de I-bit in de status
// Register is ingesteld, de Analog Comparator interrupt is geactiveerd.
// Bij het schrijven van logische nul is de interrupt uitgeschakeld.
cbi (ACSR, ACIE);
// Bij geschreven logische één, maakt dit bit de invoerinvangfunctie mogelijk
// in Timer / Counter1 om te worden geactiveerd door de analoge comparator. De
// comparatoruitgang is in dit geval direct verbonden met de ingang
// leg front-end logica vast, waardoor de comparator de ruis gebruikt
// opheffen en edge select kenmerken van de Timer / Counter1 Input
// Capture interrupt. Bij het schrijven van logische nul, geen verbinding tussen
// de analoge vergelijker en de invoerregistratiefunctie bestaan. Naar
// laat de comparator de Timer / Counter1 Input Capture activeren
// interrupt, de ICIE1-bit in het Timer Interrupt Mask Register
// (TIMSK1) moet worden ingesteld.
cbi (ACSR, ACIC);
// Deze bits bepalen welke comparatorgebeurtenissen de analoog activeren
// Comparator interrupt.
// ACIS1 ACIS0-modus
// 0 0 Toggle
// 0 1 Gereserveerd
// 1 0 Vallende rand
// 1 1 Stijgende rand
sbi (ACSR, ACIS1);
sbi (ACSR, ACIS0);

// ------------------------------------------------ ---------------------
// DIDR1-instellingen
// ------------------------------------------------ ---------------------
// Wanneer dit bit logisch is geschreven, is de digitale invoerbuffer op de
// AIN1 / 0 pin is uitgeschakeld. De bijbehorende PIN-registerbit zal dat doen
// lees altijd als nul wanneer dit bit is ingesteld. Als er een analoog signaal is
// toegepast op de AIN1 / 0-pin en de digitale ingang van deze pin is dat niet
// nodig, dit bit moet een logische geschreven zijn om het vermogen te verminderen
// verbruik in de digitale invoerbuffer.
sbi (DIDR1, AIN1D);
sbi (DIDR1, AIN0D);
}

Stap 13: Threshold

Herinnerend aan wat we zeiden over de trigger, kunnen we deze twee oplossingen voor de drempel implementeren:
  • met behulp van een trimmer kunnen we handmatig een spanningsniveau instellen;
  • met behulp van de PWM van de Arduino kunnen we het niveau met software instellen.
Op de afbeelding zien we de hardware-implementatie van de drempel in beide paden.

Voor de handmatige selectie is een multi-turn potentiometer tussen +5 V en GND voldoende.

Terwijl we voor softwareselectie een laagdoorlaatfilter nodig hebben dat een PWM-signaal uit de Arduino filtert. PWM-signalen (meer hierover volgt) zijn vierkante signalen met een constante frequentie maar een variabele pulsbreedte. Deze variabiliteit brengt een variabele gemiddelde waarde van het signaal met zich mee die kan worden geëxtraheerd met een laagdoorlaatfilter. Een goede afsnijfrequentie voor het filter is ongeveer een honderdste van de PWM-frequentie en ik koos voor ongeveer 560 Hz.

Na de twee drempelbronnen heb ik een paar pinnen ingevoegd die het mogelijk maken om met een jumper te selecteren welke bron ik wilde. Na de selectie heb ik ook een emittervolger toegevoegd om de bronnen van de Arduino-pin te ontkoppelen.

Stap 14: Hoe de pulsbreedtemodulatie werkt

Zoals eerder vermeld, is een Pulse Width Modulation (PWM) -signaal een vierkant signaal met vaste frequentie maar variabele breedte. Op de afbeelding zien we een voorbeeld. Op elke rij is er een van dergelijke signalen met een andere werkcyclus ( id est het periodegedeelte waarin het signaal hoog is). Als we het gemiddelde signaal over een periode nemen, krijgen we de rode lijn die overeenkomt met de werkcyclus met betrekking tot het signaalmaximum.

Elektronisch "nemen van het gemiddelde van een signaal" kan worden vertaald naar "doorgeven aan een laagdoorlaatfilter", zoals te zien is in de voorgaande stap.

Hoe genereert de Arduino een PWM-signaal? Er is hier een heel goede tutorial over PWM:
//arduino.cc/en/Tutorial/SecretsOfArduinoPWM
We zullen alleen de punten zien die nodig zijn voor dit project.

In de ATMega328P zijn er drie timers die kunnen worden gebruikt om PWM-signalen te genereren, elk met verschillende kenmerken die u kunt gebruiken. Voor elke timer komen twee registers overeen met de naam Output Compare Registers A / B (OCRnx) die worden gebruikt om de signaalcyclus in te stellen.

Wat betreft de ADC is er een voorschrijver (zie afbeelding), die de hoofdklok vertraagt ​​om een ​​nauwkeurige controle van de PWM-frequentie te hebben. De vertraagde klok wordt naar een teller geleid die een timer / tellerregister (TCNTn) verhoogt. Dit register wordt continu vergeleken met de OCRnx, wanneer ze gelijk zijn, wordt een signaal verzonden naar een golfvormgenerator die een puls genereert op de uitgangspin. Dus de truc is om het OCRnx-register op een bepaalde waarde in te stellen om de gemiddelde waarde van het signaal te wijzigen.

Als we een 5 V-signaal (maximum) willen, moeten we een 100% werkcyclus of 255 in de OCRnx instellen (maximum voor een 8-bits nummer), terwijl we een 0, 5 V-signaal moeten instellen als we een 0, 5 V-signaal willen of een 25 in de OCRnx.

Aangezien de klok het TCNTn-register moet vullen voordat hij vanaf het begin begint voor een nieuwe puls, is de uitgangsfrequentie van de PWM:

f = (Hoofdklok) / voorschrijver / (TCNTn maximum)

illustri gratia voor de Timer 0 en 2 (8-bit) zonder voorschrijver zal het zijn: 16 MHz / 256 = 62, 5 KHz terwijl voor Timer 1 (16-bit) 16 MHz / 65536 = 244 Hz zal zijn.

Ik besloot om Timer nummer 2 te gebruiken omdat
  • Timer 0 wordt intern gebruikt door de Arduino IDE voor functies zoals millis ();
  • Timer 1 heeft een te lage uitgangsfrequentie omdat het een 16-bit timer is.

In de ATMega328P zijn er verschillende soorten werkingsmodi van de timers, maar wat ik wilde, was de Fast PWM-versie zonder voorinstelling om de maximaal mogelijke uitgangsfrequentie te krijgen.

Stap 15: Het instellen van de PWM

In de schets schreef ik nog een initialisatiefunctie die alle parameters van de timerfunctie instelt en een paar pinnen initialiseert. nietig initPins (nietig)
{
// ------------------------------------------------ ---------------------
// TCCR2A-instellingen
// ------------------------------------------------ ---------------------
// Deze bits besturen het gedrag van de Output Compare pin (OC2A). Als een of
// beide COM2A1: 0-bits zijn ingesteld, de OC2A-uitvoer overschrijft de
// normale poortfunctionaliteit van de I / O-pin waarmee het is verbonden.
// Merk echter op dat het Data Direction Register (DDR) bit
// die overeenkomt met de OC2A-pin moet worden ingesteld om de
// output stuurprogramma.
// Wanneer OC2A is aangesloten op de pin, de functie van de COM2A1: 0 bits
// hangt af van de WGM22: 0 bit instelling.
//
// Snelle PWM-modus
// COM2A1 COM2A0
// 0 0 Normale poortwerking, OC2A verbroken.
// 0 1 WGM22 = 0: Normale poortwerking, OC0A verbroken.
// WGM22 = 1: schakel OC2A in op Compare Match.
// 1 0 Wis OC2A op Compare Match, zet OC2A op BOTTOM
// 1 1 Wis OC2A bij Compare Match, wis OC2A bij BOTTOM
cbi (TCCR2A, COM2A1);
cbi (TCCR2A, COM2A0);
sbi (TCCR2A, COM2B1);
cbi (TCCR2A, COM2B0);

// Gecombineerd met het WGM22-bit in het TCCR2B-register, deze bits
// controle van de telvolgorde van de teller, de bron voor maximum
// (TOP) tellerwaarde en welk type golfvormgeneratie moet worden gebruikt
// Bedrijfsmodi die worden ondersteund door de timer / teller-eenheid zijn:
// - Normale modus (teller),
// - Timer wissen in CTC-modus (Compare Match),
// - twee soorten pulsbreedtemodulatie (PWM) -modi.
//
// Mode WGM22 WGM21 WGM20 Bediening TOP
// 0 0 0 0 Normaal 0xFF
// 1 0 0 1 PWM 0xFF
// 2 0 1 0 CTC OCRA
// 3 0 1 1 Snelle PWM 0xFF
// 4 1 0 0 Gereserveerd -
// 5 1 0 1 PWM OCRA
// 6 1 1 0 Gereserveerd -
// 7 1 1 1 Snelle PWM OCRA
cbi (TCCR2B, WGM22);
sbi (TCCR2A, WGM21);
sbi (TCCR2A, WGM20);

// ------------------------------------------------ ---------------------
// TCCR2B-instellingen
// ------------------------------------------------ ---------------------
// Het FOC2A-bit is alleen actief wanneer de WGM-bits een niet-PWM specificeren
// modus.
// Echter, om compatibiliteit met toekomstige apparaten te verzekeren, dit bit
// moet worden ingesteld op nul wanneer TCCR2B wordt geschreven tijdens het werken in PWM
// modus. Bij het schrijven van een logische naar de FOC2A-bit, een onmiddellijke
// Compare Match wordt geforceerd op de Waveform Generation-eenheid. De OC2A
// output wordt gewijzigd volgens de instelling van COM2A1: 0 bits. Let daar op
// de FOC2A-bit is geïmplementeerd als een stroboscoop. Daarom is het de waarde
// aanwezig in de COM2A1: 0 bits die het effect van de
// gedwongen vergelijking.
// Een FOC2A-flitser genereert geen onderbreking en wordt ook niet gewist
// de timer in CTC-modus met OCR2A als TOP.
// Het FOC2A-bit wordt altijd gelezen als nul.
cbi (TCCR2B, FOC2A);
cbi (TCCR2B, FOC2B);

// De drie bits voor klokselectie selecteren de klokbron die moet worden gebruikt
// de timer / teller.
// CS22 CS21 CS20 Prescaler
// 0 0 0 Geen klokbron (timer / teller gestopt).
// 0 0 1 Geen voorschrijven
// 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 (thresholdPin, OUTPUT);

analogWrite (thresholdPin, 127);
}

Stap 16: Vluchtige variabelen

Ik kan me niet herinneren waar, maar ik las dat variabelen die binnen een ISR zijn gewijzigd, als vluchtig moeten worden verklaard.

Vluchtige variabelen zijn variabelen die in de loop van de tijd kunnen veranderen, zelfs als het lopende programma ze niet wijzigt. Net als Arduino registreert dat voor sommige externe ingrepen waarde kan veranderen.

Waarom wil de compiler dergelijke variabelen weten? Dat komt omdat de compiler altijd probeert de code die we schrijven te optimaliseren, om hem sneller te maken, en hij verandert deze een beetje, in een poging de betekenis niet te veranderen. Als een variabele op zichzelf verandert, kan het voor de compiler lijken dat deze nooit wordt gewijzigd tijdens de uitvoering van bijvoorbeeld een lus en deze kan deze negeren; terwijl het cruciaal kan zijn dat de variabele van waarde verandert. Door vluchtige variabelen te declareren, voorkomt het dat de compiler de code daarover wijzigt.

Voor wat meer informatie stel ik voor om de Wikipedia-pagina te lezen: //en.wikipedia.org/wiki/Volatile_variable

Stap 17: de kernel van de schets schrijven

Eindelijk zijn we bij de kern van het programma!

Zoals we eerder zagen, wilde ik een continue acquisitie en schreef ik de ADC Interrupt Service Routine om de gegevens continu op te slaan in de circulaire buffer. Het stopt wanneer het de index bereikt die gelijk is aan stopIndex. De buffer is circulair geïmplementeerd met de modulo-operator.

// ------------------------------------------------ -----------------------------
// ADC-conversie voltooid onderbreken
// ------------------------------------------------ -----------------------------
ISR (ADC_vect)
{
// Wanneer ADCL wordt gelezen, wordt het ADC-gegevensregister pas bijgewerkt op ADCH
// is gelezen. Bijgevolg, als het resultaat aangepast blijft en niet meer
// dan 8-bit precisie is vereist, is het voldoende om ADCH te lezen.
// Anders moet ADCL eerst worden gelezen en vervolgens ADCH.
ADCBuffer [ADCCounter] = ADCH;

ADCCounter = (ADCCounter + 1)% ADCBUFFERSIZE;

als (wacht)
{
if (stopIndex == ADCCounter)
{
// Bevries situatie
// Schakel ADC uit en stop de vrijlopende conversiemodus
cbi (ADCSRA, ADEN);

bevriezen = waar;
}
}
}

De Analog Comparator Interrupt Service Routine (die wordt aangeroepen wanneer een signaal de drempel overschrijdt) schakelt zichzelf uit en vertelt de ADC ISR om de wachtfase te starten en stelt de stopIndex in.

// ------------------------------------------------ -----------------------------
// Analoge vergelijker onderbreken
// ------------------------------------------------ -----------------------------
ISR (ANALOG_COMP_vect)
{
// Schakel Analog Comparator interrupt uit
cbi (ACSR, ACIE);

// Schakel errorPin in
// digitalWrite (errorPin, HIGH);
sbi (PORTB, PORTB5);

wacht = waar;
stopIndex = (ADCCounter + waitDuration)% ADCBUFFERSIZE;
}


Dit was heel gemakkelijk na al die aarding!

Stap 18: Inkomend signaal vormen

Laten we nu naar de hardware gaan. Het circuit ziet er misschien ingewikkeld uit, maar het is heel eenvoudig.
  • Er is een weerstand van 1 MΩ aan de ingang, om een ​​massareferentie naar het signaal te geven en een hoge impedantie-ingang te hebben. Een hoge impedantie "simuleert" een open circuit als je het aansluit op een circuit met een lagere impedantie, dus de aanwezigheid van de Girino knoeit niet te veel met het circuit dat je wilt meten.
  • Na de weerstand is er een emittervolger om het signaal te ontkoppelen en de volgende elektronica te beschermen.
  • Er is een eenvoudige offset die een 2, 5 V-niveau genereert met een spanningsdeler. Het is bevestigd aan een condensator om het te stabiliseren.
  • Er is een niet-inverterende somversterker die het binnenkomende signaal en de offset optelt. Ik heb deze techniek gebruikt omdat ik ook negatieve signalen wilde zien, omdat de Arduino ADC alleen signalen kon zien tussen 0 V en 5 V.
  • Na de sum-amp is er nog een emittervolger.
  • Met een jumper kunnen we beslissen of we het signaal met een offset willen voeren of niet.
De operationele versterker die ik wilde gebruiken was een LM324 die tussen 0 V en 5 V kan werken, maar ook tussen bijvoorbeeld -12 V en 12 V. Dit geeft ons meer mogelijkheden met de voedingen. Ik heb ook een TL084 geprobeerd die veel sneller is dan de LM324, maar een dubbele voeding vereist. Ze hebben allebei dezelfde pinout, dus kunnen worden gewijzigd zonder enige wijziging van het circuit.

Stap 19: Bypass-condensatoren

Bypass-condensatoren zijn condensatoren die worden gebruikt om de voedingen van Integrated Circuits (IC) te filteren en ze moeten zo dicht mogelijk bij de voedingspennen van de IC worden geplaatst. Ze worden meestal gebruikt in paren, een keramiek en een elektrolyt omdat ze verschillende frequenties kunnen filteren.

Stap 20: Stroombronnen

Ik heb een dubbele voeding gebruikt voor de TL084 die kan worden omgezet in een enkele voeding voor de LM324.

Op de afbeelding kunnen we zien dat ik een paar spanningsregelaars gebruikte, een 7812, voor +12 V en een 7912, voor -12 V. De condensatoren worden, zoals gewoonlijk, gebruikt om de niveaus te stabiliseren en hun waarden zijn de waarden die worden voorgesteld in de datasheets.

Om een ​​± 12 V te hebben, moeten we natuurlijk minstens 30 V op de ingang hebben, omdat de spanningsregelaars een hogere ingang nodig hebben om een ​​gestabiliseerde uitgang te bieden. Omdat ik niet over een dergelijke voeding beschikte, heb ik de truc gebruikt om twee 15 V-voedingen in serie te gebruiken. Een van de twee is verbonden met de Arduino-voedingsconnector (dus het voedt zowel de Arduino als mijn circuit) en de andere rechtstreeks naar het circuit.

Het is geen fout om de +15 V van de tweede voeding aan te sluiten op de GND van de eerste! Dit is hoe we een -15 V krijgen met geïsoleerde voedingen.

Als ik geen Arduino en twee voedingen mee wil nemen, kan ik nog steeds de +5 V van de Arduino gebruiken om die jumpers te veranderen (en de LM324 te gebruiken).

Stap 21: Een schildconnector voorbereiden

Ik heb me altijd geïrriteerd door de connectoren die ik kon vinden om een ​​Arduino-schild te maken, omdat ze altijd pennen hebben die te kort zijn en de boards die ik gebruik slechts aan één kant kunnen worden gesoldeerd. Dus heb ik een kleine truc bedacht om de pinnen langer te maken, zodat ze kunnen worden gesoldeerd en in de Arduino kunnen worden gestoken.

Door de pinstrip in het bord te steken, zoals op de foto, kunnen we de pinnen duwen, zodat ze maar aan één kant van het zwarte plastic zitten. Vervolgens kunnen we ze aan dezelfde kant solderen waar ze in de Arduino worden gestoken.

Stap 22: Solderen en testen

Ik kan u niet de hele soldeerprocedure van het circuit laten zien omdat het veel vallen en opstaan ​​heeft ondergaan. Uiteindelijk werd het een beetje rommelig maar niet zo erg, al zal ik de onderkant niet laten zien want dat is echt rommelig.

In dit stadium is er niet veel te zeggen omdat ik alle onderdelen van het circuit al in detail heb uitgelegd. Ik testte het met een oscilloscoop, die een vriend me leende, om de signalen op elk punt van het circuit te zien. Het lijkt erop dat alles goed werkt en ik ben redelijk tevreden.

De connector voor het binnenkomende signaal lijkt misschien een beetje vreemd voor iemand die niet afkomstig is van de High Energy Physics, het is een LEMO-connector. Het is de standaard connector voor nucleaire signalen, althans in Europa zoals in de VS heb ik vooral BNC-connectoren gezien.

Stap 23: Test signalen

Om het circuit en de Data AcQuisition (DAQ) te testen, gebruikte ik een tweede Arduino met een eenvoudige schets die vierkante pulsen genereert met verschillende lengtes. Ik schreef ook een pythonscript dat met Girino praat en vertelt dat het een aantal gegevensreeksen moet verwerven en een daarvan in een bestand opslaat.
Ze zijn beide gehecht aan deze stap.

Bijlagen

  • TaraturaTempi.ino Downloaden
  • readgirino.py Downloaden

Stap 24: Tijdkalibratie

Met behulp van de testsignalen heb ik de horizontale schaal van de plots gekalibreerd. Door de breedte van de pulsen te meten (die bekend zijn omdat ze zijn gegenereerd) en de gemeten pulsbreedten uit te zetten tegen de bekende waarden, krijgen we een hopelijk lineaire plot. Door dit te doen voor elke voorinstellingsinstelling hebben we de tijdkalibratie voor alle acquisitieratio's.

Op de afbeeldingen kunnen we alle gegevens zien die ik heb geanalyseerd. Het perceel "Ingerichte hellingen" is het meest interessant omdat het ons de werkelijke acquisitieratio van mijn systeem vertelt bij elke instelling van de voorschrijver. De hellingen werden gemeten als een [ch / ms] -nummer, maar dit komt overeen met een [kHz], dus de hellingswaarden zijn eigenlijk kHz of ook kS / s (kilo-samples per seconde). Dat betekent dat we met de voorschrijver ingesteld op 8 een acquisitiepercentage krijgen van:

(154 ± 2) kS / s

Niet slecht, hè?

Terwijl we van de "Fitted y-intercepts" -plot een inzicht krijgen in de lineariteit van het systeem. Alle y-intercepts moeten nul zijn, omdat bij een signaal met een lengte van nul een puls met een lengte van nul moet overeenkomen. Zoals we in de grafiek kunnen zien, zijn ze allemaal compatibel met nul, maar niet met de dataset met 18 prescriptoren. Deze dataset is echter de slechtste omdat deze slechts twee gegevens heeft en de kalibratie niet kan worden vertrouwd.

Hierna volgt een tabel met de acquisitieratio's voor elke voorinstellingsinstelling.

PrescalerAcquisitiepercentage [kS / s]
1289, 74 ± 0, 04
6419, 39 ± 0, 06
3237, 3 ± 0, 6
1675, 5 ± 0, 3
8153 ± 2
De geciteerde fouten komen van de Gnuplot fit-engine en ik weet het niet zeker.

Ik heb ook een ongewogen aanpassing van de tarieven geprobeerd, omdat je kunt zien dat ze ongeveer verdubbelen wanneer de voorschrijfhelften, dit lijkt een omgekeerde evenredigheidswet. Dus paste ik de tarieven versus de instellingen van de voorschrijver aan met een simpele wet van

y = a / x

Ik heb een waarde voor een van

a = 1223

met een =² = 3, 14 en 4 vrijheidsgraden betekent dit dat de wet wordt geaccepteerd met een betrouwbaarheidsniveau van 95%!

Stap 25: Klaar! (Bijna)

Aan het einde van deze lange ervaring voel ik me zeer tevreden omdat
  • Ik heb veel geleerd over microcontrollers in het algemeen;
  • Ik heb veel meer geleerd over de Arduino ATMega328P;
  • Ik had enige praktische ervaring met data-acquisitie, niet door iets te gebruiken dat al gedaan was, maar door iets te maken;
  • Ik realiseerde me een amateur-oscilloscoop die niet zo slecht is.
Ik hoop dat deze gids nuttig zal zijn voor iedereen die het leest. Ik wilde het zo gedetailleerd schrijven omdat ik dat allemaal op de harde manier heb geleerd (surfen op internet, het gegevensblad lezen en met veel vallen en opstaan) en ik zou iedereen die ervaring willen besparen.

Stap 26: wordt vervolgd ...

Het project is echter nog lang niet voltooid. Wat het mist is:
  1. Een test met verschillende analoge signalen (ik mis een analoge signaalgenerator);
  2. Een grafische gebruikersinterface voor de computer.
Terwijl voor punt 1. Ik weet niet zeker wanneer het zal worden voltooid, omdat ik niet van plan ben om er in de nabije toekomst een te kopen / bouwen.

Voor punt 2. zou de situatie beter kunnen zijn. Is iemand bereid mij daarbij te helpen? Ik heb hier een mooie Python-oscilloscoop gevonden:
//www.phy.uct.ac.za/ourses/python/examples/moreexamples.html#oscilloscope-and-spectrum-analyser
Ik zou het willen aanpassen om het voor Girino te passen, maar ik accepteer suggesties.

Verwante Artikelen