ublo
bogdan's (micro)blog

bogdan » arduino: un fel de theremin (senzor capacitiv de poziție)

06:46 pm on Dec 30, 2018 | #more | 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ă:

ce e un theremin?

thereminul e un instrument muzical derivat din primele cercetări cu privire la senzorii de proximitate: două antene controlează tonul și intensitatea sunetelor prin modificarea distanței între acestea și mâinile cânterețului. principiul de funcționare este realtiv simplu: două conductoare separate de un izolator alcătuiesc un capacitor, lucru valabil pentru orice materiale conductoare, cum ar fi o antenă din metal sau corpul uman. la fel ca în cazul detectorului de metale discutat anterior, conectând antena în paralel cu circuitul oscilant al unui oscilator, frecvența acestuia se va modifica prin mișcarea mâinii în raport cu antena prin modificarea capacității.

variația tonului se obține prin mixarea semnalului oscilatorului cu frecvența variabilă cu cel al unui oscilator cu frecvența stabilă, dând naștere fenomenului de bătăi – generarea unui semnal cu frecvența cât jumătate din diferența celor două frecvențe inițiale. pentru variația intensității, e suficientă integrarea semnalului obținut din cel de-al doilea oscilator, folosind valoarea acestuia pentru controlul intensității.

cum detectez poziția mâinii cu arduino?

ideea e simplă și vine din descrierea anterioară: măsor capacitatea condensatorului alcătuit dintr-o antenă și mâna mea. formula fizică de la care pornesc este simplă – un capacitor alcătuit din două plăci paralele are capacitatea invers proporțională cu distanța dintre plăci, urmărind formula:

$$ C = \epsilon \frac {A} {d} $$

, unde:
C este capacitatea electrică exprimată în Farazi;
ε este permitivatea mediului care se găsește între cele două plăci paralele (Farazi / metru);
A este suprafața comună a celor două plăci, măsurată în metri pătrați;
d este distanța între cele două plăci, măsurată în metri;

într-un articol precendent, am arătat cum poate fi măsurată capacitatea. pe scurt, dacă aplic o diferență de potențial pe plăcile capacitorului, acesta va păstra diferența de potențial atât timp cât nu va curge curent electric între cele două plăci. conectând un rezistor în paralel cu terminalele capacitorului, circuitul se închide, iar tensiunea electrică dintre plăcile condesatorului va scădea exponențial, respectând următoarea formulă:

$$ U_{capacitor}(t) = U_{initial} e^{- \frac {t} {RC}} $$

, unde:

Ucapacitor(t) este tensiunea electrică dintre plăcile capacitorului, la momentul t de la închiderea circuitului, în Volți;
Uinitial este tensiunea electrică dintre plăcile capacitorului, imediat înainte de a închide circuitul, în Volți;
t este timplul scurs de la momentul închiderii circuitului, măsurat în secunde;
R este rezistența electrică a rezistorului folosit pentru a închide circuitul, exprimată în Ohmi;
C este capacitatea electrică a capacitorului, exprimată în Farazi;

urmărind ultima formulă, timpul în care tensiunea electrică între plăcile capacitorului atinge o anumită valoare depinde de produsul între capacitate și rezistența electrică, denumit datorită unităților de măsură implicate, constanta de timp a circuitului. luând spre exemplu o capacitate de 10pF, un rezistor de 4,7MΩ, o tensiune inițială de 5V și una finală de 2,1V, timpul în care tensiunea între plăcile condensatorului atinge acest prag este:

$$ t = RC \times log \left(\frac {U_{initial}} {U_{capacitor}}\right),\\ t = 10^{-11} (F) \times 4,7 \times 10^6 (\Omega) \times log \left(\frac {5(V)}{2,1(V)}\right) = 1,77 \times 10^{-5} (s)$$

valorile nu sunt alese întâmplător. fiecare instrucțiune Arduino se execută în aproximativ 62,5ns, asta înseamnă că pot executa 283 instrucțiuni în timpul necesar descărcării capacitorului, mai mult, 5V e tensiunea de alimentare a Arduino, disponibilă la VCC sau pe oricare dintre terminale când sunt configurate ca ieșiri și stabilite drept HIGH, 2,1V e tensiunea sub care un terminal configurat ca intrare digitală este considerat ca fiind LOW, iar 10pF e capacitatea minimă a unei mâini față de un electrod metalic cu suprafața cel puțin egală, la distanța de aproximativ 10cm.

deja în acest moment cred că e destul de clar ce urmează să fac: la unul dintre terminalele digitale ale Arduino voi conecta o antenă, care va fi legată printr-un rezistor cu valoarea de 4,7MΩ către GND. în acest fel obțin circuitul alcătuit din capacitor și rezistorul prin care se descarcă. pentru a încărca inițial capacitorul, voi stabili potențialul terminalului digital la 5V pentru 2ms, de aproximativ 100 de ori constanta de timp a circuitului. imediat, voi transforma terminalul în intrare digitală și voi număra câte operații pot efectua până când tensiunea electrică între antenă și GND scade sub 2,1V, intervalul fiind direct proporțional cu capacitatea și invers proporțional cu distanța de la mână la antenă.

void setup() {
  Serial.begin (9600); // in primul rand, pornesc interfata seriala pentru a citi valorile masurate
}

void loop() {
  uint16_t t = 0; // voi folosi un contor cu dimensiunea de 16 biti
  pinMode (2, OUTPUT); // stabilesc terminalul 2 al Arduino ca fiind iesire digitala
  digitalWrite (2, HIGH); // stabilesc potentialul terminalului 2 la 5V
  delay (2); // astept 2ms pentru a incarca capacitorul
  pinMode (2, INPUT); // comut terminalul 2 al Arduino ca intrare digitala
  while (digitalRead (2) == HIGH && t < 0xFFFF) { // atat timp cat terminalul 2 are tensiunea peste 2,1V,
                                                  // dar in acelasi timp contorul nu a ajuns la capat,
                                                  // capatul fiind FFFF(baza 16) = 65535(baza 10)
    t++; // incrementez contorul de timp cu o unitate
  }
  // in momentul asta, t contine timpul in care s-a descarcat capacitorul format de antena si corp
  pinMode (2, OUTPUT); // configurez terminalul 2 ca iesire digitala
  digitalWrite (2, LOW); // stabilesc potentialul antenei ca fiind 0V
  delay (498); // astept 498 de ms, in mare pentru ca vreau sa pot citi cu usurinta datele
               // si ca in perioada asta sa anulez orice sarcina stocata in antena
  Serial.println (t); // trimit prin interfata seriala, valoarea obtinuta
}

valorile pe care le-am obținut folosind ca antenă o bucată de 25×25cm din folie alimentară din aluminiu au fost următoarele:

Capacitive Sensor Results

notă:

din datele obținute se vede o dependență clară a numărului obținut în funcție de distanța mâinii. cu toate acestea, valorile obținute prezintă zgomot, care influențează negativ modul în care poate fi folosită informația. o metodă relativ simplă de atenuare a zgomotului este aceea de a colecta mai multe valori decât este necesar și în locul unei singure măsurători să utilizez media valorilor obținute. în acest fel, zgomotul este atenuat. din nefericire, îmbunătățirea repetabilității afectează precizia măsurătorilor.

cum folosesc datele obținute?

având în vedere zgomotul observat și faptul că sunt mai mulți factori care influențează valorile componentelor din circuit, cum ar fi valoarea tensiunii de alimentare, valoarea pragului minim pentru ca un terminal să fie considerat LOW, dar și lungimea firelor sau umiditatea aerului, primul pas constă în calibrarea senzorului. în acest sens, în momentul în care Arduino pornește, știind că în acel moment nu am mână aproape de senzor, voi culege suficient de multe date, pe care le voi media, valoarea obținută urmând s-o folosesc ca nivel de referință.

ca să îmi ușurez munca, citirea timpului o voi include într-o funcție, definită astfel:

uint16_t read_one_capacity (byte pin) { // functia se numeste read_one_capacity,
                                        // ia ca parametru numarul terminalului Arduino 
                                        // si intoarce valoarea timpului, ca un numar intre 0 si 65535
  uint16_t t = 0; // in locul de memorie specificat cu t voi tine minte timpul
  pinMode (pin, OUTPUT); // configurez terminalul pin ca iesire
  digitalWrite (pin, HIGH); // stabilesc potentialul terminalului pin la VCC (+5V)
  delay (2); // astept 2ms
  pinMode (pin, INPUT); // transform terminalul pin in intrare digitala
  while (digitalRead (pin) == HIGH && t < 0xFFFF) { // atat timp cat pin este considerat ca fiind HIGH
                                                    // si contorul e mai mic de 65535
    t++; // incrementez contorul cu o unitate
  }
  pinMode (pin, OUTPUT); // configurez terminalul pin ca iesire
  digitalWrite (pin, LOW); // conectez terminalul pin la GND
  delay (2); // astept 2ms, in felul asta durata unei masuratori va fi de aprox. 5ms
  return t; // ies din functie si intorc rezultatul obtinut
}

pentru calibrarea masuratorilor, in blocul setup voi efectua 100 de masuratori pe care le voi media pentru a obține valoarea de referință, corespunzătoare distanței infinit.

uint16_t reference; // definesc locul unde tin minte referinta

void setup() {
  reference = 0; // initial, stabilesc referinta cu valoarea 0
  byte c = 0; // c e un contor care numara de cate ori am cules date
  while (c++ < 100) { // atat timp cat c e strict mai mic ca 100, incrementeaza c cu o unitate
    reference += read_one_capacity (2); // si aduna la valoarea referinte rezultatul masuratorii
  }
  reference = reference / 100; // in referinta am suma masuratorilor si e suficient sa impart cu
                               // numarul de masuratori pentru a obtine media
  Serial.begin (9600); // cum voi citi datele prin consola, pornesc interfata seriala
}

folosind acelați principiul al medierii, voi defini o funcție care să întoarcă valoarea medie pentru 32 de măsurători. prefer puteri ale lui 2 pentru numărul de mostre deoarece diviziunea va fi mai rapidă, putând fi transpusă de compilator într-o simplă translatare a biților către dreapta.

uint16_t read_capacity (byte pin) { // definesc o functie numita read_capacity,
                                    // care ia ca parametru terminalul arduino la care e conectata antena
                                    // si intoarce timpul de descarcare
  uint16_t average = 0; // in locul din memorie average voi avea rezultatul masuratorii
  byte c = 0; // c e un contor care imi spune cate masuratori am efectuat
  while (c++ < 32) { // atat timp cat contorul nu depaseste 32,
    average += read_one_capacity (pin); // citesc valoarea timpului si il adun la average
  }
  average = average / 32; // pentru ca am citit 32 de mostre, calculez media
  return average > reference ? average - reference : 0; // daca media e mai mare decat referinta,
                                                        // intorc diferenta lor (care va fi pozitiva)
                                                        // daca nu, intorc 0
}

notă:

poate părea puțin ciudat că nu folosesc bucle de tip for. țin minte că la curs am fost întrebat asta. am pornit de la ideea din teorema Böhm-Jacopini care spune că orice program de calculator poate fi scris utilizând atribuiri, condiții și bucle. altfel spus, pentru a scrie absolut orice aplicație trebuie să știi cum să atribui o variabilă, cum să folosești if/the/else și cum să folosești while. din acest motiv, prefer ca orice program scris pentru curs să folosească întotdeauna numai cele trei tipuri de construcții.

având în vedere cele scrise mai sus, loop devine foarte simplu:

void loop() {
  uint16_t t = read_capacity (2); // citesc timpul necesar descarcarii capacitorului
  Serial.println (t); // il trimit prin interfata seriala
  delay (500); // astept 500 de ms
}

Capacitive Sensor Improved

cum îl fac să cânte?

Arduino Theremin

pentru a scoate sunete am nevoie de un traductor piezoelectric, care nu e altceva decât micul difuzor care se găsea în toate jucăriile chinezești, sub forma unui mic disc de metal. în mod normal, traductorul este polarizat, însă în practică n-am observat diferențe între conectarea lui în ambele sensuri. așa că, unul dintre firele traductorului se conectează la unul dintre terminalele libere ale Arduino, să spunem 9, în timp ce celălalt se va conecta la GND. dacă traductorul nu are fire, va trebui să conectezi un fir la discul de metal și unul la electrodul aflat în centrul discului.

Arduino dispune de o funcție care permite generarea de tonuri folosind un traductor piezoelectric. funcția se numește tone și are trei parametrii: terminalul Arduino care este conectat la traductor, frecvența notei în Herzi și durata în milisecunde. pentru a determina frecvența notelor poți folosi un tabel ca cel din wikipedia sau poți rezolva problema matematic: LA-ul diapazonului aparține octavei 4, normală și are 440Hz. LA-ul din octava 5 are 2×440 = 880Hz, iar cel din octava 3 440/2 = 220Hz. restul notelor urmează o progresie geometrică, iar o octavă are 12 note, incluzând ambele clape, albe și negre de la pian.

orice variantă aș alege, notele din octava 4 sunt 262 (do), 294 (re), 330 (mi), 350 (fa), 392 (sol), 440 (la) și 494 (si) Hz. voi alege încă o notă cu frecvența 0 care are semnificația că instrumentul construit nu scoate niciun sunet. din observațiile făcute mai sus, valoarea măsurată de senzor se încadrează între 0 și 32, iar cum am 7 note și un non-sunet, înseamnă că pot să transform valoarea obținută în notă prin împărțirea la 4. cum discutam la realizarea detectorului de metale, împărțirea la 4 înseamnă translatarea numărului cu 2 biți spre dreapta folosind operatorul >>.

acum, observațiile pe care le-am făcut s-ar putea să nu fi fost chiar bune, iar valoarea obținută, chiar divizată cu 4, poate depăși ca valoare 7. pentru a elimina o condiție suplimentară, am folosit un mic artificiu: numărul obținut, divizat cu 4 dacă i se aplică operația și pe biți cu reprezentarea binară a lui 7 (111) vor fi păstrați doar ultimii 3 biți, adică 8 combinații, reprezentând cifre între 0 și 7.

int notes[8] = { 0, 262, 294, 330, 350, 392, 440, 494 }; // definesc frecvențele notelor
void loop() {
  uint16_t cap = read_capacity (2); // citesc distanta
  int note = notes[(byte) 0x07 & (cap >> 2)]; // gasesc frecventa notei
                                              // cap are o valoare intre 0 și 32
                                              // cap >> 2 va fi între 0 si 8
                                              // 0x07 & (cap >> 2) va fi intre 0 și 7 întotdeauna
                                              // deoarece 0x07 e 111 in binar si aplicând si la nivel
                                              // de biti, singurii care nu vor fi 0, vor fi ultimii 3
                                              // biti ai cap >> 2
  if (note > 0) { // daca frecventa notei e mai mare ca 0
    tone (9, note, 200); // reproduct nota cu o durata de 200ms prin traductorul conectat la terminalul 9
  }
  else { // daca frecventa e 0
    delay (200); // fac o pauza de 200ms
  }
}

notă:

n-aș spune că thereminul realizat astfel e foarte bun. e mai mult un exemplu de ce poate fi făcut cu un minimum de componente, așa că să nu ai așteptări prea mari. poate fi îmbunătățit cu un algoritm mai bun pentru detectarea zgomotului de măsură, cu un capacitor de 10pF pus în paralel cu rezistorul de 4,7MΩ pentru o mai bună stabilitate, cu o diodă zenner în paralel cu rezistorul pentru protecția la supratensiuni și acumulări de sarcină și tot așa. în schimb, pentru a construi un simplu senzor de atingere capacitiv cu un minimum de componente e mai mult decât suficient - vezi Makey Makey.

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.