ublo
bogdan's (micro)blog

bogdan » arduino: detector de metale

04:08 pm on Dec 27, 2018 | read the article | tags:

în urma rugăminții profesorului Stamatin, de câțiva ani țin un curs la Facultatea de Fizică din Măgurele. dacă inițial numele «modelare și simulare» ascundea ecuații matematice și metode numerice, recent am înlocuit diferențialele cu Arduino, în ovațiile celor câtorva studenți care frecventează cursul. dotările limitate m-au făcut să devin creativ cu materialele de curs, iar rezultatul mi s-a părut suficient de interesant pentru a-l reproduce aici. așa că:

pe scurt, sărind puțin peste partea de fizică, metalele influențează o mărime fizică numită permeabilitate magnetică în preajma lor. această mărifme fizică definește capacitatea unui mediu de a susține formarea câmpuri magnetice. în preajma metalelor feroase, permeabilitatea magnetică crește în timp ce în jurul nemetalelor aceasta scade. detectorul de metale pe care îl prezint în continuare ia în calcul acestă mărime.

permeabilitatea magnetică influențează inductanța unei bobime (inductor) direct proporțional. o formulă utilă pentru inductori cilindrici este:

$$ L \approx \mu N^2 \frac {D} {2} (ln(\frac {8D}{d}) – 2) $$

, unde:
L este inductanța electrică a bobinei (măsurată în Henry);
D este diametrul mediu al bobinei (metri);
d este diametrul conductorului din care este realizată bobina (metri);
N este numărul de înfășurări ale bobinei (spire);
μ este permeabilitatea magnetică a bobinei (Henry / metru);

cum convertesc permeabilitate magnetică în frecvență?

permeabilitatea magnetică influențează direct inductanța electrică. spre deosebire de cazul unui capacitor, ecuația care descrie comportamentul unui inductor nu e de mare ajutor, în special din cauza timpilor implicați prea mici pentru a putea măsura mărimile implicate direct cu un Arduino. dacă ești curios, asta e ecuația de care vorbeam:

$$ U(t) = L \frac {dI(t)} {dt} $$

, unde
U(t) este tensiunea la bornele inductorului (volți);
L este inductanța electrică a bobinei (Henry);
I(t) este curentul care circulă prin inductor (Amperi);

cu toate astea, întotdeauna există o soluție. un capacitor în paralel cu un inductor formează un circuit oscilant a cărui perioadă este dependentă doar de mărimile caracteristice ale celor două componente.

încărcând capacitorul și lăsându-l să se descarce în inductor obții oscilații a căror perioadă poate fi măsurată direct cu Arduino. în mod normal ai conecta circuitul oscilant la unul dintre terminalele digitale ale Arduino, ai trimite un scurt impuls pozitiv urmat de transformarea terminalului în intrare și măsurarea numărului de impulsuri într-un interval de timp. problema cu scenariul anterior e că lumea reală e puțin diferită de modelele matematice, iar circuitul oscilant este atenuat în așa fel încât după încărcarea capacitorului, numărul de oscilații care pot fi folosite este mult prea mic. cu toate astea, pentru că are valoare didactică, poți recrea experimentele mele cu următorul mic program:

void setup() {
  pinMode (7, OUTPUT); // <- vom conecta circuitul la terminalul 7
}

void loop() {
  digitalWrite (7, HIGH); // <- stabilim potentialul la 5V
  delayMicroseconds (10); // <- pentru 10us
  digitalWrite (7, LOW); // <- restabilim potentialul la 0V
  delayMicroseconds (990); // <- pentru 990us
}

forma tensiunii în funcție de timp prezentă la terminalul 7 este dreptunghiulară, stând 1% din timp la valoarea de 5V și 99% la valoarea 0V, totul cu o perioadă de 1ms. pentru tipul ăsta de undă, raportul între timpul în care valoarea e 5V și lungimea perioadei se numește factor de umplere.

Arduino: pin 7 waveform

pentru a proteja terminalul 7 al Arduino este necesară o diodă înseriată cu acesta. circuitul oscilant variază intensitatea tensiunii la borne simetric față de 0V, în intervalul -5V – 5V, putând deteriora circuitul de intrare al terminalului 7. dioda mai folosește și ca întrerupător, deconectând circuitul oscilant de la terminalul 7 imediat după ce acesta este stabilit la 0V (LOW) și lăsându-l să oscileze liber.

Arduino: Metal Detector 1st Try

inductorul este realizat dintr-un cablu de cupru cu miez solid (monofilar), cu diametrul de 0,25mm, folosit la sonerii, care se găsește gata înfășurat la Hornbach. capacitorul folosit este ceramic, de 100nF în timp ce dioda poate fi de orice fel, influența acesteia fiind mică. în cadrul experimentului am folosit totuși o diodă foarte rapidă, 1N5189, care poate fi înlocuită la fel de bine cu 1N4148 sau 1N4001.

pierderile din conductoarele electrice, dielectricul capacitorului și miezul inductanței fac oscilațiile să fie atenuate exponențial, lucru care face detecția variației de frecvență indusă de modificarea permeabilității miezului inductanței aproape imposibilă fără amplificare.

LC Circuit Wave Form

varianta a doua și cea pe care ți-o recomand e construirea unui mic oscilator, conectând circuitul oscilant într-o buclă cu reacție pozitivă, care să compenseze atenuarea. o buclă cu reacție pozitivă va culege o fracțiune din tensiunea oscilantă, o va amplifica și o va suprapune peste cea din circuitul oscilant, generând interferențe constructive care mențin nivelul oscilațiilor. condiția pentru ca oscilațiile să se mențină este ca amplificarea să compenseze cel puțin atenuarea.

în trecut cel mai probabil ți-aș fi recomandat un tranzistor pe post de amplificator. însă lucrurile sunt mult mai simple în prezent, mai ales că în ceea ce privește amplificarea poți folosi la același preț un amplificator operațional. un astfel de dispozitiv este un mic circuit care în afară de alimentare are 3 terminale: o intrare negativă, o intrare pozitivă și o ieșire. ecuația care guvernează acest circuit este:

$$ U_{iesire} = \gamma \times (U_{intrare{}+{}} – U_{intrare{}-{}}) $$

, unde:
Uieșire e diferența de potențial între ieșire și punctul considerat 0V;
Uintrare+ e diferența de potențial între intrarea pozitivă și punctul considerat 0V;
Uintrare- e diferența de potențial între intrarea negativă și punctul considerat 0V;
γ e factorul de amplificare în buclă deschisă al amplificatorului operațional, o proprietate ce depinde de tipul de circuit folosit, care în cazul LM358N folosit la curs este aproximativ 32000, fără unitate de măsură;

în mod obișnuit, un amplificat operațional se conectează în configurația de amplificator inversor, în care intrarea pozitivă e conectată la 0V în timp ce pe intrarea negativă este aplicat semnalul care trebuie amplificat. o mică observație o reprezintă inversarea polarității semnalului de ieșire față de cel de intrare, conform formulei:

$$ U_{iesire} = {} – \gamma \times U_{intrare{}-{}} $$

pentru ca interferența să fie constructivă, semnalul preluat la intrarea amplificatorului trebuie să aibă semn schimbat față de semnalul de ieșire, altfel interferența este distructivă și oscilațiile încetează.

revin puțin la faptul că la intrarea amplificatorului e necesară o fracțiune din tensiunea la bornele circuitului oscilant. în mod obișnuit prima idee care îmi vine este un divizor rezistiv. problema cu el e că absoarbe energie din circuit, atenuând și mai mult oscilațiile. o variantă mult mai elegantă e să creez un divizor capacitiv, introducând un electrod suplimentar în capacitor care are un potențial proporțional cu distanța electrodului față de terminalul conectat la potențialul 0V. în practică e imposibil să modific un capacitor ceramic, însă pot înseria două capacitoare și folosi terminalul lor comun pentru a fracționa semnalul. tensiunea între acest terminal și potențialul 0V este raportul dintre capacitatea condensatorului situat către 0V și suma celor două capacități. cu toate acestea, cele două tensiuni au același semn.

un artificiu îl reprezintă considerarea punctului intermediar al capacitoarelor ca având un potențial fix, tensiunile la capatele inductorului fiind acum în opoziție. un divizor rezistiv va stabili potențialul “0V” al intrării pozitive a amplificatorului operațional la 2,5V față de GND (0V). tot la intrarea pozitivă va fi conectat un capacitor către GND (0V) pentru filtrarea posibilelor perturbații.

tot ce mai trebuie făcut este limitarea curentului prin circuit folosind două rezistoare, unul pentru intrare și unul pentru ieșire – amplificatorul operațional este o componentă mult mai simplă decât Arduino, neavând limitări implicite.

cum construiesc oscilatorul?

Arduino: Metal Detector

pentru amplificatorul operațional poți folosi orice amplificator operațional. am preferat LM358N (”N” reprezintă varianta normală, care poate fi insearată într-o plăcuță de prototipare) deoarece e ieftin și funcționează bine cu tensiuni mici. privindu-l de deasupra, unul dintre piciorușe este marcat cu un punct (gaură), fiind primul picioruș (terminalul 1) al circuitului. celelalte terminale sunt numerotate crescător în sens invers acelor de ceasornic, atunci când circuitul e privit de deasupra. terminalele care ne interesează sunt 4 (GND), 8 (+5V), 5 (intrarea pozitivă, IN+), 6 (intrarea negativă, IN-) și 7 (ieșirea).

pentru început vrei ca circuitul să fie alimentat, așa că vei conecta terminalul 4 la GND și 8 la +5V. pentru a-l transforma într-un amplificator, este necesară conectarea terminalului 5 (IN+) la un potențial intermediar care permite inductorului să oscileze în jurul lui fără a depăși limitele tensiunii de alimentare. poți face acest lucru conectând doi rezistori sub forma unui divizor rezistiv, ambii de aceeași valoare 10kΩ, stabilind potențialul terminalului +5V la 2,5V. pentru mai multă stabilitate, un mic capacitor de 100nF poate fi conectat în paralel cu unul dintre rezistori (din experimentele inițiale am observat că nu e obligatoriu).

următorul pas îl constituie realizarea circuitului oscilant format dintr-un inductor în paralel cu două capacitoare înseriate. punctul comun al celor două capacitoare va fi conectat la GND (0V) în timp ce fiecare dintre terminalele inductorului vor fi conectate prin rezistori de 1kΩ la terminalele 6 și 7 ale circuitului integrat. terminalul 7 se va conecta la terminalul 5 al Arduino pentru a măsura frecvența.

în imaginea de mai jos se vede forma de undă generată de oscilator și modul în care apropierea unei doze din aluminiu de bobină influențează (crește) frecvența oscilatorului.

LC Colpitts Oscillator

notă:

din nefericire, terminalul 5 al Arduino UNO este singurul care poate fi utilizat pentru măsurarea frecvenței semnalului cules. acesta e conectat la ceasul intern Timer 1 pentru microcontrollerul în jurul căruia este construit Arduino UNO. un ceas intern nu e neapărat un ceas în sensul naiv al cuvântului. mai degrabă un dispozitiv care poate număra impulsuri interne sau externe și genera diferite semnale interne în cazul în care un prag bine stabilit (alarmă) sau numărul maxim de impulsuri este depășit. impulsurile interne sunt sincronizate cu ceasul de bază al microcontrollerului, putând seta frecvența cu care acestea sunt generate ca fracțiuni ale frecvenței de bază (16MHz în cazul Arduino UNO). impulsurile externe pot fi culese prin intermediul terminalului 5. avantajul folosirii ceasului Timer 1 îl reprezintă numărarea hardware a semnalelor, adică în timp real, fără întârzieri, complet predictibil din punct de vedere fizic.

cum măsor frecvența?

Arduino poate face destul de multe lucruri utilizând software, așa cum am arătat în cursurile precendente. însă atunci când este vorba de evenimente care se petrec în timp real, acest lucru nu ajută foarte mult deoarece momentul execuției instrucțiunilor nu mai este predictibil, făcând imposibilă măsurarea frecvenței cu exactitate. cu toate acestea întotdeauna există o soluție, mai ales atunci când componenta care trebuie programată este un microcontroller. ATMega328P, circuitul principal al Arduino UNO conține trei ceasuri interne independente și metode prin care acestea pot fi accesate în timp real, numite întreruperi. întreruperile sunt evenimente generate de componentele hardware ale microcontrollerului care suspendă execuția programului curent în favoarea rezolvării unor mici sarcini cu prioritate. un exemplu, de altfel și cel utilizat în detectorul de metale, îl reprezintă întreruperea generată de depășirea alarmei unuia dintre ceasuri, cu ajutorul căruia pot stabili cu precizie fereastra de timp în care sunt numărate impulsurile generate de oscilator.

astfel, algoritmul de măsurare al frecvenței poate fi schițat în următorii pași: stabilirea ceasului intern Timer 1 ca fiind controlat extern de semnalul oscilatorului, stabilirea ceasului intern Timer 2 ca fiind controlat de un semnal derivat din ceasul general al Arduino, iar cu o întrerupere care se execută în momentul depășirii pragului pentru Timer 2 voi salva valoarea numărată. deși pot să măsor cu precizie frecvența, nu mă interesează în mod implicit valoarea ei absolută, ci variația cauzată de metalele așezate în preajma bobinei.

majoritatea codului care va rula pe Arduino va fi conținut în setup. în bucla loop doar voi verifica dacă am reușit să termin o numărătoare de impulsuri și să o trimit prin interfața serială pentru a o citi.

notă:

în bucățelele de cod chemate prin intermediul întreruperilor, care poartă numele de Interrupt Service Routines (ISRs), pot modifica locuri din memorie accesibile întregului program. sigurul lucru de care trebuie ținut cont este ca în declararea acestor locuri din memorie să fie specificat faptul că acestea pot fi modificate în timpul întreruperilor folosind cuvântul cheie special ”volatile”.

volatile uint16_t count_prev = 0; // valoarea numărată de Timer 1 anterior
volatile uint16_t count_output = 0; // valoarea frecvenței măsurate de Timer 1
                                    // volatile e un cuvânt cheie special care definește tipul
                                    // variabilei count_output ca fiind invariabil la întreruperi
volatile uint8_t count_ready = 0; // indicator că am reușit să măsurăm un ciclu complet
uint16_t gate_index = 0; // numărul de perioade ale Timer 2 în care Timer 1 va număra impulsurile

// pentru că voi modifica valorile unor regiștrii speciali, va trebui să-i salvez undeva
uint8_t saveTCCR1A; // variabila în care voi salva valoarea registrului special TCCR1A
uint8_t saveTCCR2A; // variabila în care voi salva valoarea registrului special TCCR2A
uint8_t saveTCCR1B; // variabila în care voi salva valoarea registrului special TCCR1B
uint8_t saveTCCR2B; // variabila în care voi salva valoarea registrului special TCCR2B

poate pare destul de complicat, însă e destul de simplu. TCCR1A, TCCR1B, TCCR2A, TCCR2B sunt locuri din memorie predefinite care controlează modul în care ATMega328 funcționează, numite regiștrii. explicația pentru comportamentul fiecărui registru se găsește în foaia de catalog ATMega328.
TCCR1A, TCCR2A – sunt regiștrii care stabilesc comportamentul terminalelor conectate la Timer 1, respectiv Timer 2;
TCCR1B, TCCR2B – sunt regiștrii care stabilesc modul în care cele două ceasuri sunt controlate: oprit, controlate de ceasul general al Arduino sau de ceas extern; Timer 1 va fi controlat extern, la tranziția între 0V și 5V, în timp ce Timer 2 va fi controlat intern, de o fracțiune a frecvenței ceasului general; fracțiunile posibile sunt 1, 8, 64, 256 sau 1024;
SREG – e un registru general de stare care printre altele controlează modul în care funcționează întreruperile; nu-l vom modifica direct, ci vom folosi funcția predefinită cli; cu toate astea, va trebui să-i restabilim valoarea după modificările făcute, lucru destul de simplu;
TCNT1, TCNT2 – sunt regiștrii care conțin valoarea numărată de fiecare dintre ceasuri, Timer 1, respectiv Timer 2; TCNT1 e un registru cu lungimea de 16 biți în timp ce TCNT2 are doar 8 biți;
OCR1A, OCR1B, OCR2A, OCR2B – sunt regiștrii în care se pot stabili alarmele pentru fiecare dintre Timer 1 și Timer 2; fiecare ceas are două alarme configurabile;
TIFR1, TIFR2 – sunt regiștrii în care pot fi citite evenimentele care au generat întreruperile pentru fiecare dintre Timer 1 și Timer 2;
TIMSK1, TIMSK2 – sunt regiștrii în care pot fi stabilite ce întreruperi sunt generate de către fiecare dintre Timer 1 și Timer 2;
GTCCR – este un registru în care controlează modul în care ceasurile interne pot fi sincronizate;

notă:

vei observa că de multe ori, în codul următor, apare expresia 1<<CONSTANTĂ. constantele sunt predefinite și se găsesc în foaia de catalog ATMega328P, reprezentând poziția într-un octet (grup de 8 biți) a bitului de interes. spre exemplu, TOV1 este 0 fiind primul bit din registrul special TIFR1. WGM21 are valoarea 1 fiind al doilea bit din registrul special TCCR2A. operația << reprezintă translatarea numărului din stânga catre stânga cu numărul din dreapta cifre, în reprezentarea binară. astfel, 1<<TOV1 va fi 1 deoarece TOV1 este 0 și nu se va translata nimic. 1<<WGM21 va fi 2 (10 în binar), deoarece 1 va fi translatat către stânga cu o poziție. echivalentul matematic al operației este:
$$ {a}\ll{b} = a \times 2^{b} $$
în cazul numerelor care sunt stocate pe 8 biți, operația anterioară se va face modulo 256; în cazul numerelor stocate pe 16 biți, operația se va face modulo 65536.
alte operații întâlnite mai jos sunt:
a|b va avea ca rezultat un număr care în reprezentarea binară va avea drept componente operația binară sau aplicată biților corespunzători din a și b. un exemplu simplu 1|2 = 3 deoarece 01|10 = 11.
a&b va avea ca rezultat un număr care în reprezentarea binară va avea drept componente operația binară și aplicată biților corespunzători din a și b. un exemplu simplu 1&2 = 0 deoarece 01&10 = 00.

void setup() {
  uint8_t status; // variabila în care voi salva valoare registrului special de stare SREG
  Serial.begin (9600); // urmează să citesc frecvența folosind conexiunea serială

  // pentru că vom modifica niște regiștrii speciali, le vom salva starea în primă fază
  saveTCCR1A = TCCR1A; // TCCR1A este un registru special de control al Timerului 1,
                       // care specifică modul în care funcționează Timerul 1
  saveTCCR1B = TCCR1B; // TCCR1B este al registru special de control al Timerului 1,
                       // care specifică sursa și frecvența impulsurilor

  TCCR1B = 0; // resetez valoarea registrului TCCR1B
  TCCR1A = 0; // resetez valoarea registrului TCCR1A
  TCNT1 = 0; // resetez numărătorul Timerului 1 (îl aduc la valoarea 0)
  TIFR1 = (1 << TOV1); // resetez indicatorul de depășire a valorii maxime pentru Timer 1
  TIMSK1 = 0; // dezactivez întreruperile generate de Timer 1
  
  // pentru că vom modifica alți regiștrii speciali, le vom salva starea în acest moment
  saveTCCR2A = TCCR2A; // TCCR2A este registrul de control al Timerului 2,
                       // are un rol similar cu TCCR1A
  saveTCCR2B = TCCR2B; // TCCR2B este registrul de control al Timerului 2,
                       // are un rol similar cu TCCR2B

  TCCR2B = 0; // resetez valoarea registrului TCCR2B
  TCCR2A = (1 << WGM21); // stabilesc modul de funcționare al Timerului 2, ca fiind generator
                         // de evenimente atunci când se depășește valoarea din OCR2A 
  OCR2A = 124; // stabilesc valoarea lui OCR2A astfel încât fereastra pentru măsurarea frecvenței
               // este (124 + 1) / (16000000 / 1024 (Hz)) = 8ms
  TIFR2 = (1 << OCF2A); // resetăm și pregătim întreruperea generată de depășirea valorii
                        // stabilite în OCR2A
  TCNT2 = 0; // resetez numărătorul pentru Timer 2
  
  status = SREG; // salvez valoarea registrului special de stare SREG
  cli(); // dezactivez toate întreruperile

  GTCCR = (1 << PSRASY); // pregătesc Timerul 2 pentru a fi comandat de ceasul intern
  TCCR2B = (1 << CS22) | (1 << CS21) | (1 << ); // cu frecvența de 1024 de ori mai mică decât cea a ceasului
  TIMSK2 = (1 << OCIE2A); // stabilesc ca microcontrollerul să genereze o întrerupere la depășirea lui OCR2A

  TCCR1B = (1 << CS12) | (1 << CS11) | (1 << CS10); // stabilesc ceasul extern pentru Timer 1
                                                    // ca numărând impulsurile de trecere de la 0V la 5V
  SREG = status; // restabilesc valoarea registrului special de stare SREG 
}

în interiorul buclei Arduino singurul lucru pe care îl voi verifica este dacă am o numărătoare completă (count_ready = 1), moment în care resetez și pregătesc ceasurile pentru o nouă măsurătoare. aah, da. și bineînțeles, trimit prin interfața serială valoarea măsurată.

void loop() {
  if (count_ready) { // dacă numărarea impulsurilor s-a terminat
    uint8_t status = SREG; // salvez valoarea registrului special de stare
    cli(); // dezactivez toate întreruperile
    count_ready = 0; // resetez starea numărătorului de impulsuri
    SREG = status; // reactivez întreruperile
    Serial.println (count_output); // trimit frecvența prin portul serial
  }
}

ISR este un macro valabil pentru toate procesoarele AVR prin care se pot stabili mici rutine care sunt activate de întreruperi. parametrul macro-ului este numele vectorului generat de întrerupere, în cazul de față depășirea OCR2A de către Timer 2, TIMER2_COMPA_vect. rutina doar verifică valoarea contorului Timer 1, pe care îl stochează în cazul în care a trecut 1 secundă sub eticheta count_output, specificând programului principal că numărătoarea a fost cu succes sub eticheta count_ready.

ISR(TIMER2_COMPA_vect){ // această rutină pentru întreruperi e activată de întreruperile generate
                        // atunci când Timer 2 depășește valoarea din OCR2A, adică la fiecare
                        // 1024 x (124 + 1) / 16000 = 8 ms
  uint16_t count; // locul din memorie în care salvez valoarea curentă a contorului Timer 1

  count = TCNT1; // salvez valoarea contorului Timer 1
  if (TIFR1 & (1<<TOV1)) { // dacă cumva depășește valoarea 65536
    TIFR1 = (1<<TOV1); // resetez indicatorul de depășire
  }

  gate_index++; // pentru a măsura mai exact timpul și pentru a avea suficient timp pentru
                // a sesiza micile schimbări în frecvență, voi aduna toate numerele găsite
                // pe parcursul mai multor măsurători; gate_index ține minte câte măsurători
                // am făcut până în prezent
  if (gate_index >= 125) { // dacă au fost 125 de măsurători, adică după aproximativ 1s = 125 * 8ms
                              // atenție! e o coincidență că 125 de aici e același cu 125 din setup
    gate_index = 0; // resetez contorul de măsurători,
    count_output = count - count_prev; // calculez numărul de impulsuri ca diferența între valoarea
                                       // numărată și valoarea anterioară
    count_prev = count; // stabilesc valoarea anterioară
    count_ready = 1; // spun programului că am găsit frecvența
  }
}

ultimul pas, care depășește puțin scopul acestui articol, este calibrarea detectorului de metale așezând diferite obiecte din metal în apropierea inductorului și citind frecvența. valorile pot fi furnizate unui clasificator care să construiască un model pentru detecția tipului de metal.

aceast sait folosește cookie-uri pentru a îmbunătăți experiența ta, ca vizitator. în același scop, acest sait utilizează modulul Facebook pentru integrarea cu rețeaua lor socială. poți accesa aici politica mea de confidențialitate.