ublo
bogdan's (micro)blog

bogdan » arduino: senzor de culoare

10:50 pm on Apr 29, 2019 | #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ă:

cum funcționează? (puțină teorie)

pe lângă simbolurile folclorice și religioase, un curcubeu poartă și dovada că lumina albă are în componență o multitudine de culori, pornind cu roșu și sfârșindu-se cu violet. când privești un obiect, o parte dintre aceste culori sunt reflectate către fotoreceptorii din retină, în timp ce restul sunt absorbiți. în felul ăsta, un obiect pare alb, dacă reflectă toate culorile înapoi către privitor, pare negru dacă nu reflectă (aproape) nimic și pare verde dacă absoarbe toate celelalte culori cu excepția culorii verzi, pe care o reflectă.

un mic experiment pe care-l poți face pentru a observa mai bine acest fenomen este să privești un obiect verde sau albastru iluminat doar de un led roșu. culoarea obiectului va fi foarte apropiată de negru, semn că obiectele verzi sau albastre absorb mare parte din lumina roșie incidentă.

lumina este radiație electromagnetică, foarte asemănătoare cu ce încălzește mâncarea în cuptorul cu microunde sau ce poartă ultimul snapchat către prietenii tăi. diferența este dată de frecvența cu care acestea oscilează: 2.450.000.000 oscilații pe secundă pentru cuptorul cu microunde, între 700.000.000 și 2.600.000.000 oscilații pe secundă pentru telefoane mobile și între 394.460.000.000.000 și 768.700.000.000.000 oscilații pe secundă pentru lumină. numerele sunt mult prea mari pentru a fi utilizate în practică, așa că în locul numărului de oscilații pe secundă se folosește lungimea de undă, mai exact distanța parcursă de undă într-un interval de timp cât durează o singură oscilație:
$$\lambda = \frac{c}{f}$$
,unde:
λ – e lungimea de undă, exprimată în metri;
c – e viteza cu care se deplasează undele, în acest caz, viteza luminii, ~3×108 metri pe secundă;
f – reprezintă numărul de oscilații pe secundă și se măsoară în herzi sau secunde-1;

fără a intra în detaliile care mă depășesc legate de nuanțe, intervalele de lungime de undă asociate fiecărei culori sunt 415 ± 35nm, 475 ± 25nm, 535 ± 35nm, 585 ± 15nm, 615 ± 15nm, 695 ± 65nm.

o ultimă formulă fizică vine din suprapunerea a două culori. atunci când oscilațiile cu frecvențe diferite se suprapun, apare fenomenul numit bătăi: pentru un observator extern va deveni dominantă o oscilație cu frecvența media aritmetică a frecvențelor oscilațiilor componente, cu amplitudinea oscilând cu frecvența cât jumătate din diferența absolută a frecvențelor oscilațiilor componente. pentru lungimi de undă, formula implică media armonică:
$$\lambda_{rezultat} = \frac {2 \lambda_{1} \lambda_{2}}{\left|\lambda_{1} \pm \lambda_{2}\right|}$$
,unde:
λrezultat – reprezintă lungimea de undă rezultată;
λ1,2 – reprezintă lungimile de undă componente;

pornind de la formula asta și de la particularitățile ochiului uman, combinând fascicule cu lumină cu lungimea de undă corespunzătoare culorilor roșu, verde și albastru poți obține majoritatea culorilor din spectrul vizibil.

cum construiesc un senzorul de culoare?

trecând peste teoria oarecum plictisitoare de mai nainte, drept senzor de culoare voi folosi un simplu fotorezistor (2 lei, 1,95 lei) – un dispozitiv care își modifică conductivitatea în funcție de cantitatea de lumină incidentă. conductivitatea fotorezistorului variază însă și cu lungimea de undă, astfel, dacă sensibilitatea maximă este în jurul culorii verzi, pentru a obține același efect, roșu și indigo au nevoie de fascicule de câteva ori mai puternice, ca în figura care urmează. un alt aspect pe care îl voi reține din foaia de catalog a fotorezistorului este timpul de răspuns maxim de 30ms.

CdS Photorezistor Sensitivity

principiul de funcționare al senzorului va fi următorul: iluminăm obiectul a cărui culoare o voi determina cu un fascicul de lumină de culoare cunoscută, măsor cât din ea se reflectă către senzor și compar cu valoarea dată de iluminarea ambientală. în funcție de rezultat, voi stabili cu exactitatea culoarea.

în lumina discuției de mai sus, voi alege cele trei culori standard: roșu, verde și albastru pe care le folosesc pe rând pentru a ilumina obiectul, măsurând pentru fiecare intensitatea luminii reflectate. dacă dintre cele trei culori, roșu, verde și albastru, obiectul va reflecta mai mult verde, atunci va fi verde, dacă reflectă și roșu și verde în același timp, cel mai probabil va fi galben.

pentru sursele de lumină cu culoare stabilită, voi folosi LED-uri cu lentile colorate în culorile roșu, verde și albastru (50 de lei / 500 bucăți). bineînțeles că poți utiliza mai multe culori de LED-uri. cu puțină răbdare și costuri ceva mai mari, poți găsi cel puțin 16 nuanțe diferite de LED, inclusiv infraroșu (~900nm) și ultraviolet (~280nm), însă va trebui să folosești un senzor mai bun de intensitate luminoasă (cine știe, poate un viitor articol despre asta, până atunci ai aici unul ceva mai avansat).

circuitul este simplu: conectez fiecare dintre LED-uri la unul dintre terminalele digitale 3, 4 și respectiv 5 ale Arduino, în timp ce fotorezistorul formează un divizor rezistiv împreună cu un rezistor de 47KΩ conectat către Vcc (+5V), al cărui punct intermediar e conectat la terminalul analogic A0.

Color Senzor Schematics

notă
modul de aranjare al componentelor pe placa de experimente încearcă să reproducă cât mai fidel realitatea. fotorezistorul e înconjurat de LED-uri pentru a reflecta lumina sub aproximativ același unghi, făcând ca poziția obiectului din fața senzorului să nu influențeze major rezultatul măsurătorii. orice altă poziționare e posibilă, doar că drumul optic între LED-uri și fotorezistor va avea dimensiuni și forme diferite, influențând intensitatea luminoasă recepționată de senzor.
Color Sensor

cum măsor intensitatea culorilor?

pentru început, definind niște constante și un tip de date pentru valoarea măsurată de senzor. constantele sunt:

#define RED_LED 3 // <-- terminalul la care e conectat LED-ul rosu
#define GREEN_LED 4 // <-- terminalul la care e conectat LED-ul verde
#define BLUE_LED 5 // <-- terminalul la care e conectat LED-ul albastru
#define SENSOR_PIN A0 // <-- terminalul analog care masoara tensiunea electrica pe senzor
#define SAMPLES 20 // <-- numarul de masuratori analogice; explicatia urmeaza

tipul de date de care vorbeam este o structură în care voi stoca valoarea măsurată cu LED-urile stinse, pe care o voi numi base (de la bază) și valorile pentru fiecare dintre leduri red, green, blue pentru roșu, verde și respectiv albastru. typedef e cuvântul cheie care mă ajută să definesc tipul color (de la sfârșit). tipul e definit ca o structură definită cu struct, al cărei identificator este _color și ale cărei componente sunt toate de tip întreg. definesc tipul ăsta de date pentru că devine foarte ușor de folosit când e întors sau folosit ca parametru în funcții.

typedef struct _color {
  int base;
  int red;
  int green;
  int blue;
} color;

în bucata de cod de mai sus am introdus constanta SAMPLES ca fiind 20. citirea datelor folosind convertorul analog-digital din Arduino nu e întotdeauna stabilă, așa că voi citi SAMPLES mostre, pe care le voi media pentru a obține o valoarea mai apropiată de valoarea reală a tensiunii măsurate. mai trebuie să țin cont că rezistența electrică a fotorezistorului scade odată cu creșterea intensității luminoase. cum fotorezistorul este conectat către GND, tensiunea pe acesta va fi direct proporțională cu rezistența electrică, adică va scădea la rândul ei cu creșterea intensității fluxului luminos incident. acest lucru e o mică problema în interpretarea datelor, preferând o funcție de conversie crescătoare. ca orice problemă legată de Arduino, am două variante: prima, fizică, în care inversez locul rezistorului și fotorezistorului în divizor, conectând fotorezistorul către Vcc (+5V); a doua, în cod, în care scad din 1023 (valoarea maximă produsă de convertorul analog-digital), valoarea măsurată. cum pregătirea mea de bază e de matematician, am ales-o pe a doua.

int sensor_read () {
  long output = 0; // <-- s-ar putea sa depasesc valoarea maxima pentru intregi simpli
  delay (30); // <-- mai tii minte? senzorul reactioneaza cu 30ms intarziere
  for (byte c = 0; c < SAMPLES; c++) { // <-- citesc 20 de mostre
    output += analogRead (SENSOR_PIN); // <-- adun valoarea masurata pentru calculul sumei
  }
  return 1023 - (int) (output / SAMPLES); // <-- din 1023 scad media masuratorilor
}

cum analogRead produce la iesire un întreg între 0 și 1023, dacă numărul de mostre culese e mai mare de 30, valoarea maximă pentru tipul de date int (întreg stocat pe 2 octeți, având valoarea maximă aprox. 32 de mii) este depășită, rezultatul fiind trunchiat. din acest motiv am utilizat pentru sumă tipul de date long. in cadrul liniei care contine return, am folosit (int) pentru a converti output / SAMPLES din tipul de date long (întreg stocat pe 4 octeți, având valoarea maximă aprox. 2 miliarde) în tipul de date int. în mod obișnuit operația nu e necesară, conversia făcându-se automat. prefer totuși aceast mod de lucru pentru a putea urmări cu atenție tipurile de date folosite și pentru a fi sigur că memoria folosită e suficientă pentru valorile folosite.

până acum am funcția necesară măsurării unei singure valori. pentru citirea tuturor valorilor necesare voi începe cu citirea valorii de bază, urmată pe rând de aprinderea LED-ului corespunzător culorii, efectuarea unei măsurători și apoi stingerea lui, pentru fiecare culoare.

color read_color () {
  color c; // <-- color e noul tip de date definit

  c.base = sensor_read (); // <-- citesc valoarea de baza (lumina ambientala)
                           // fiecare componenta a lui color o accesez cu .nume_componenta
                           // c.base face referire la componenta base din tipul de date color
  digitalWrite (RED_LED, HIGH); // <-- aprind LED-ul rosu
  c.red = sensor_read (); // <-- citesc intensitatea fasciculului rosu reflectat
  digitalWrite (RED_LED, LOW); // <-- sting LED-ul rosu
  digitalWrite (GREEN_LED, HIGH); // <-- repet procesul pentru LED-ul verde
  c.green = sensor_read ();
  digitalWrite (GREEN_LED, LOW);
  digitalWrite (BLUE_LED, HIGH); // <-- repet procesul pentru LED-ul albastru
  c.blue = sensor_read ();
  digitalWrite (BLUE_LED, LOW);

  return c;
}

după ce citesc valorile, mi-ar plăcea să le trimit către interfața serială, pentru procesarea ulterioară și pentru că mi-e mai comod, definesc o funcție care ia ca parametru o structura din noul tip definit:

void print_color (color c) {
  Serial.print (c.base); // <-- trimit catre portul serial valorile
  Serial.print (" "); // <-- separate de cate un spatiu
  Serial.print (c.red);
  Serial.print (" ");
  Serial.print (c.green);
  Serial.print (" ");
  Serial.println (c.blue); // <-- la final, trec pe randul urmator cu "ln" din "println"
}

urmează inițializarea terminalelor Arduino și a portului serial, pentru culegerea datelor.

void setup(){
  pinMode (RED_LED, OUTPUT); // <-- configurarea terminalului RED_LED (3) ca iesire
  pinMode (GREEN_LED, OUTPUT);
  pinMode (BLUE_LED, OUTPUT);

  digitalWrite (RED_LED, LOW); // <-- stabilirea potentialului terminalului RED_LED (3) la 0V
  digitalWrite (GREEN_LED, LOW);
  digitalWrite (BLUE_LED, LOW);
  Serial.begin (9600); // <-- pornesc interfata seriala
}

și ultima bucată de cod, care citește culoarea obiectului din fața senzorului, o dată pe secundă.

void loop() {
  color c; // <-- folosesc noul tip de date definit
  c = read_color (); // <-- citesc culoarea
  print_color (c); // <-- afisez rezultatele
  delay (1000); // <-- astept o secunda (1000ms)
}

notă
rezultatele obținute nu se traduc direct în culori. o metodă simplă pentru detectarea culorii o reprezintă plasarea unei coli de hârtie în fața senzorului și determinarea valorilor pentru cele trei canale. culoarea hârtiei fiind albă, intensitatea fluxului reflectat este maximă pentru fiecare canal. folosind valorile maxime măsurate pentru fiecare canal, din care scad intensitatea de bază, pot să determin pentru fiecare culoare ce procent din fluxul inițial se reflectă. în felul ăsta pot folosi principiul descris la începutul articolului: valori egale pentru roșu și verde și o valoare mult mai mică pentru albastru, înseamnă culoarea galbenă.
$$I_{relativa, culoare} = \frac{I_{culoare} - I_{baza}}{I_{alb, culoare} - I_{alb, baza}}$$
,unde:
culoare - e una dintre culorile roșu, verde sau albastru;
Irelativa, culoare - e intensitatea masurata relativa pentru culoarea culoare; va fi un numar real intre 0 și 1;
Iculoare - e intensitatea masurata pentru pentru culoarea culoare, pentru obiectul a cărui culoare vreau să o determin; un număr întreg între 0 și 1023;
Ibaza - e intesitatea de bază măsurată pentru obiectul a căreui culoare vreau să o determin; un număr întreg între 0 și 1023;
Ialb, culoare - e intensitatea măsurată pentru culoarea culoare, având coala de hârtie în fața senzorului; un număr întreg între 0 și 1023;
Ialb, baza - e intensitatea măsurată de bază, având coala de hârtie în fața senzorului; un număr întreg între 0 și 1023.

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.

bogdan » arduino: detector de metale

04:08 pm on Dec 27, 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ă:

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.

bogdan » arduino: multimetru digital

07:14 pm on Nov 25, 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ă:

lucrul care m-a fascinat prima data când am descoperit Arduino a fost capacitatea lui de a conecta lumea reală prin software, în primul rând folosind informații culese prin senzori. în majoritatea lor, senzorii transformă o mărime fizică oarecare într-una electrică: tensiune, curent, rezistență, capacitate sau inductanță, de unde și ideea transformării unui Arduino într-un dispozitiv pentru măsurărea acestor mărimi – într-un cuvânt, multimetru.

cum măsor tensiune electrică?

toate versiunile de Arduino pot măsura tensiuni continue implicit folosind ca terminale de măsură una dintre conexiunile analogice, notate cu A0, A1 etc. și conexiunea GND (de la ”ground”, adică ”împământare”) – în realitate potențialul de referință pentru 0V al plăcuței. intervalul în care un Arduino poate măsura direct tensiuni electrice este cuprins între 0 și 5V pentru versiunile cele mai comune de Arduino – Duemillanove, Uno R3, Leonardo, Nano, LilyPad, LilyTiny, Pro Mini/Micro 5V sau Mega, sau 0 și 3,3V pentru versiunile Pro Mini 3V3, Zero și Due.

Arduino - Direct Measure Voltage

la curs am folosit de cele mai multe ori Arduino UNO și Leonardo, așa că în continuare, dacă nu specific explicit, mă refer la versiunile care pot măsura între 0 și 5V implicit.

un Arduino poate măsura tensiuni cu o rezoluție de 10 biți. mai exact, va împărți intervalul 0 – 5V în 210 = 1024 de sub-intervale egale și va verifica în care dintre cele 1024 de sub-intervale se regăsește tensiunea aplicată la intrare. lungimea unui sub-interval este de 5V / 1024 = 0,004882V, primul sub-interval fiind 0 – 0,004882V, următorul 0,004883V – 0,009765V, urmat de 0,009766V – 0,014648V ș.a.m.d. pentru a respecta convenția utilizată în majoritatea limbajelor de programare, sub-intervalele sunt numerotate începând cu 0.

void setup() {
}

void loop() {
  // functia analogRead primeste ca parametru conexiunea
  // pentru vei masura tensiunea, intorcand sub-intervalul in care
  // se gaseste aceasta
  int interval = analogRead (A0);
  // pentru conversia in tensiune,
  // poti lua capatul din stanga al sub-intervalului:
  float voltage_left = 0.004882 * interval;
  // sau mijlocul sub-intervalului:
  float voltage_center = 0.002441 + 0.004882 * interval;
  // sau capatul din dreapta al sub-intervalului:
  float voltage_right = 0.004882 * (interval + 1);
}

în micul program de mai sus, analogRead este o funcție care are ca parametru identificatorul terminalului Arduino pentru care vrei să măsori tensiunea, întorcând un număr întreg – specificat de cuvântul cheie int – care corespunde numărului de ordine al sub-intervalului în intervalul de măsură. pentru conversia intr-un numar real e suficient să-l incluzi în operații aritmetice cu numere reale, iar rezultatul să-l atribui unui loc în memorie declarat ca fiind real prin cuvântul cheie float.

o eroare pe care am întâlnit-o destul de des o reprezintă folosirea numerelor întregi în operațiile aritmetice care ar trebui să producă numere reale. Arduino va face automat conversia în ultimul moment posibil, astfel că următoarea secvență va produce ca rezultat numere întregi intre 0 si 4:

void loop() {
  // analogRead intoarce un numar intreg
  int interval = analogRead (A0);
  // operatiile se efectueaza in ordine, pastrand tipul cel mai convenabil
  // 5 * interval va fi un numar intreg intre 0 si 5115, care ulterior va fi
  // impartit ca numar intreg (cu rest) la 1024, rezultand un numar intreg
  // intre 0 si 4, care abia apoi va fi convertit intr-un numar real
  float voltage_left = 5 * interval / 1024;
}

notă
numărul de zecimale care pot fi folosite din tensiunea obținută este dat de numărul de zerouri după virgulă din lungimea unui sub-interval de măsură. în cazul anterior, pot folosi 2 zecimale deoarece lungimea sub-intervalului este 0,004882V

așa cum unul dintre studenți mei a aflat experimental, aplicând tensiuni mai mari decât tensiunea de alimentare la intrarea Arduino de cele mai multe ori va strica ireversibil conexiunea unde a fost aplicată. pentru a evita problemele de acest tip și pentru a putea măsura totuși tensiuni electrice mai mari, metoda cea mai la îndemână este ”divizorul rezistiv”.

Arduino - Voltage Divisor

un divizor rezistiv este alcătuit din doi rezistori, R1 și R2 conectați în serie, ale căror terminale libere devin terminalele de măsură ale multimetrului, în timp ce Arduino va măsura căderea de tensiune doar pe unul dintre rezistorii divizorului, R2.

$$U_{arduino} = U_{necunoscuta} \times \frac{R_2}{R_1 + R_2} \\ U_{necunoscuta} = U_{arduino} \times \frac{R_1 + R_2}{R_2}$$

pentru R2 o valoarea bună este 10kΩ, bazată pe caracteristica circuitului de intrare a Arduino. în funcție de intervalul în care estimezi că se va afla tensiunea necunoscută, poți calcula valoarea rezistorului R1. să presupunem că vrei să măsori tensiunea unui acumulator LiPo 3S – adică, cu trei celule. tensiunea unei celule variază în intervalul 2,8 – 4,3V, făcând tensiunea acumulatorului să varieze în intervalul 8,4 – 12,9V. considerând o marjă de siguranță, capătul superior al intervalului devine 15V. în situația asta, valoarea lui R1 va fi:

$$ R_1 = (\frac{U_{max}}{U_{arduino_max}} – 1) \times R_2 \\ R_1 = (\frac{15V}{5V} – 1) \times 10k\Omega = 20k\Omega $$

notă
știi sigur că o formulă fizică e corectă atunci când unitățile de măsură din stânga egalului sunt aceleași cu cele din dreapta, aplicând regulile aritmetice. în situația de mai sus, R1 se măsoară în ohmi, prima paranteză după egal nu are unitate de măsură (volt se ”simplifică” cu volt), iar factorul rămas se măsoară în ohmi, rezumând formula la ohmi = ohmi.

în același timp, lungimea unui sub-interval de măsură va deveni 0,014648V

void loop() {
  int interval = analogRead (A0);
  float voltage_left = 0.014648 * interval;
  float voltage_center = 0.007324 + 0.014648 * interval;
  float voltage_right = 0.014648 * (interval + 1);
}

din nefericire, 20kΩ nu e o valoare standard pentru un rezistor, însă poate fi ușor replicată cu două rezistoare înseriate de câte 10kΩ. în cazul în care vrei să alegi o valoare standard, cea mai apropiată e 22kΩ. în această situație, tensiunea maximă devine:

$$ U_{max} = U_{arduino_max} \times \frac{R_1 + R_2}{R_2} \\ U_{max} = 5V \times \frac{22k\Omega + 10k\Omega}{10k\Omega} = 16V $$

iar formulele pentru calculul tensiunii electrice folosind Arduino sunt:

void loop() {
  int interval = analogRead (A0);
  float voltage_left = 0.015625 * interval;
  float voltage_center = 0.007812 + 0.015625 * interval;
  float voltage_right = 0.015625 * (interval + 1);
}

notă
recomand măsurarea tensiunilor mari cu extrem de mare atenție. tot ce depășește 32V poate fi extrem de periculos. în nicio situație nu încerca să măsori tensiunea rețelei de curent electric!

o nouă problemă în măsurarea unei tensiuni electrice o reprezintă polaritatea. aparatele de măsură comerciale o detectează automat, afișând semnul corespunzător pe ecran. în cazul Arduino, aplicarea unei tensiuni electrice negative – potențialul conexiunii analogice mai jos decât potențialul de referință 0V (GND) – va distruge circuitul de intrare al terminalului respectiv.

o metodă pentru evitarea acestei situații este simularea unui potențial de referință virtual mai sus, în așa fel încât aplicarea unei tensiuni negative să nu coboare potențialul terminalului de intrare analog sub nivelul 0V (GND). poți stabili un astfel de potențial folosind un divizor rezistiv, care să înjumătățească tensiunea de alimentare de 5V pentru a crea un punct de potențial 2,5V față de 0V, care va fi folosit ca terminal negativ al multimetrului pe care-l vei construi, în timp ce terminalul pozitiv va fi una dintre intrările analogice ale Arduino.

Arduino - Floating Voltage

notă
dezavantajele acestui artificiu sunt consumul suplimentar de energie: pentru a obține un potențial stabil, curentul care trece prin divizor trebuie să fie relativ mare; o variantă posibilă îl reprezintă utilizarea unui divizor alcătuit din doi rezistori de 220 de Ω, asigurând un curent de aproximativ 10mA.
un al doilea dezavantaj de care trebuie ținut cont este că intervalul de măsură al multimetrului a fost translatat cu 2,5V. mai exact, a devenit -2,5V – +2,5V. în aceste condiții formulele trebuie ajustate corespunzător – introducând termenul de translație de -2,5V, iar tensiumea maximă care poate fi aplicată la intrare nu poate depăși 2,5V. în cazul în care o tensiune mai mare este necesară, se poate aplica metoda divizorului rezistiv de mai sus.

ultima situație pe care vrei să o ai acoperită este măsurarea tensiunilor mult mai mici. Arduino pune la dispoziție două metode pentru rezolvarea problemei, una software și una hardware. varianta software nu implică componente suplimentare. folosind funcția analogReference poți seta capătul superior al intervalului în care se poate afla tensiunea necunoscută.

void setup() {
  analogReference (INTERNAL); // <- capatul intervalului devine 1,1V
}

lungimea unui sub-interval de măsură devine în această situație 0,001074V transformând formulele pentru derivarea tensiunii electrice în:

void loop() {
  int interval = analogRead (A0);
  float voltage_left = 0.001074 * interval;
  float voltage_center = 0.000537 + 0.001074 * interval;
  float voltage_right = 0.001074 * (interval + 1);
}

dacă ai nevoie să măsori tensiuni mult mai mici, poți apela la varianta unei referințe fizice. folosind un divizor rezistiv cu ieșirea conectată la terminalul AREF poți stabili valoarea tensiunii capăt de interval de măsură. activarea acestei referințe externe se face prin apelarea funcției analogReference cu parametrul EXTERNAL.

Arduino - External Referrence

în exemplul din imagine, valorile pentru divizorul rezistiv sunt R1 = 1,8kΩ și R2 = 220Ω și stabilesc capătul intervalului tensiunilor măsurate la 0,544544V în timp ce lungimea sub-intervalul de măsură este de 0,000531V.

void setup() {
  // deoarece am conectat AREF la valoare 5V * 220 ohm / (1800 ohm + 220 ohm)
  analogReference (EXTERNAL); // <- capatul intervalului devine 0,5445V
}
void loop() {
  int interval = analogRead (A0);
  float voltage_left = 0.000531 * interval;
  float voltage_center = 0.000265 + 0.000531 * interval;
  float voltage_right = 0.000531 * (interval + 1);
}

cum măsor curent electric?

deși sunt puțini senzori care convertesc mărimi fizice în curent electric, măsurarea acestuia este importantă, cel puțin prin prisma monitorizării energiei consumate într-un circuit. voi porni de la un exemplu simplu: cât curent consumă un LED?

notă
LED-urile sunt dispozitive semiconductoare care conduc curentul electric cu precădere într-o singură direcție și sunt caracterizate de doi parametrii importanți: căderea de tensiune în conducție directă și curentul direct maxim. căderea de tensiune în conducție directă poate fi înțeleasă simplu ca tensiunea necesară aprinderii LED-ului, orice tensiune mai mică păstrând LED-ul stins, în timp ce orice tensiune mai mare îl va face să lumineze, cu observația că LED-ul va încerca să mențină tensiunea aplicată constantă. acest lucru e imposibil, fiind principala cauză pentru ”arderea” LED-urilor. tensiunea în conducție directă variază în funcție de culoarea LED-ului, de la 1,9V pentru cele roșii, la peste 3V pentru cele albastre și albe.
curentul direct maxim îl reprezintă curentul maxim care poate trece prin LED, atunci când acesta este aprins în așa fel încât durata de viață a LED-ului să fie maximă. spre deosebire de alte dispozitive, LED-urile își variază intensitatea luminoasă direct proporțional cu curentul care le străbate.
din aceste motive, LED-urile vor avea nevoie întotdeauna de un circuit care să limiteze curentul maxim prin circuit. un exemplu este un simplu rezistor înseriat cu acesta.

am în vedere cel mai simplu circuit cu LED: un rezistor R în serie cu un LED, conectate între terminalele GND (0V) și 5V (+5V) ale Arduino.

Arduino - Current Measuring

în mod normal vei conecta un rezistor cu o valoare întâlnită des în astfel de circuite, 220 de Ω. curentul prin circuit va depinde de căderea de tensiune în conducție directă pe LED și va respecta legea lui Ohm:

$$U_{arduino,5V} = U_{LED,direct} + R \times I_{circuit} \\ I_{circuit} = \frac{U_{arduino,5V} - U_{LED,direct}}{R} \\ I_{circuit} = \frac{U_{rezistor}}{R}, U_{rezistor} = U_{arduino,5V} - U_{LED,direct}$$

ultima formulă este cea mai convenabilă, mai ales dacă rezistorul are un terminal conectat la GND (0V), situație în care poți măsura direct căderea de tensiune pe acesta folosind experiența câștigată anterior în măsurarea tensiunii. dacă celălalt terminal al rezistorului este conectat la A0, curentul prin circuit poate fi măsurat direct astfel:

void setup() {
}

void loop() {
  // găsim sub-intervalul în care se află tensiunea pe rezistorul R
  int interval = analogRead (A0);
  // estimăm tensiunea pe rezistor ca fiind centrul sub-intervalului de măsură
  float voltage_rezistor = 0.002441 + 0.004882 * interval;
  // aplicăm legea lui Ohm pentru a afla curentul, direct în A
  float current = voltage_rezistor / 220.0;
}

notă
cu un Arduino nu poți măsura direct curentul electric într-un circuit, având în vedere că intrările analogice ale acestuia funcționează ca niște mici aparate care măsoară tensiunea electrică. în această situație, soluția cea mai la îndemână este conversia curentului în tensiune folosind un simplu rezistor cu valoare cunoscută. variante alternative, care se pot adapta în funcție de situație, sunt utilizarea unor alte fenomene fizice care convertesc curentul electric într-o tensiune sau variația acesteia: efectul Hall, folosit în special pentru izolarea circuitului care va fi măsurat de cel care îl măsoară sau variația curbei de încărcare a unui capacitor cu valoare cunoscută.

cum măsor rezistență electrică?

asemănător cazului anterior al curentului electric, nu poți măsura direct rezistența electrică și va trebui să te bazezi pe formula pe care-am menționat-o des până acum, legea lui Ohm. în cazul în care prin rezistorul necunoscut trece un curent cunoscut, diferența de potențial între terminale va fi direct proporțională cu rezistența electrică. soluția se complică atunci când îți dorești o sursă de curent constant. circuitul electronic care se comportă ca o astfel de sursă e complex și costisitor, metoda fiind folosită în unele aparate din laboratoarele cu bugete serioase. cum nu e cazul, următoarea soluție este o sursă de curent predictibil, pentru care poți aplica legea lui Ohm. vei folosi în acest sens sursa de tensiune constantă a Arduino disponibilă între terminalul 5V și GND (0V) și un rezistor cu valoare cunoscută în serie cu cel necunoscut, acesta din urmă având unul dintre terminale conectat la GND (0V), în timp ce Arduino v-a fi folosit pe post de voltmetru, măsurând prin A0 potențialul conexiunii comune a celor doi rezistori.

Arduino - Rezistence Measuring

$$\begin{cases}U_{constant,5V} = (R_{cunoscuta} + R_{necunoscuta}) \times I_{circuit} \\ U_{comun} = R_{necunoscuta} \times I_{circuit}\end{cases} \Rightarrow \\ R_{necunoscuta} = R_{cunoscuta} \times \frac {U_{comun}}{U_{constant,5V} - U_{comun}} $$

tensiunile electrice din formule pot fi convertite direct în valori numerice obținute prin intermediul Arduino astfel: 5V va fi 1023 în timp ce valoarea lui Ucomun va fi rezultatul aplicării funcției analogRead pentru terminalul A0.

$$R_{necunoscuta} = R_{cunoscuta} \times \frac {N_{analogRead(A0)}}{1023 - N_{analogRead(A0)}} (\Omega)$$

// valoare rezistorului cunoscut, in Ohmi
float R = 100000.0;
void setup() {
}

void loop() {
  // in n_comun citim numarul sub-intervalului de masura
  int n_comun = analogRead (A0);
  // in unele situatii, mai exact atunci cand nu este conectat un
  // rezistor, valoarea lui n_comun va fi 1023, generand o eroare
  // rezultata in urma diviziunii cu zero; utilizand operatorul
  // ternar (denumire pompoasa pentru conditionarea atribuirii)
  // poti verifica daca n_comun e mai mic decat 1023 (mai mare
  // e imposibil sa fie) si in situatia asta vei aplica formula,
  // iar in cazul in care n_comun este 1023, valoarea rezistorului
  // va fi -1 (valoare imposibila fizic, dar reprezentand infinit)
  float rezistenta = n_comun < 1023 ?
    R * (float) n / (1023.0 - n) :
    -1;
}

in micul program anterior, (float) urmat de un identificator de variabilă forțează variabila respectivă să fie interpretată ca fiind de tipul specificat între paranteze. parantezele sunt obligatorii. în ceea ce privește secvența

, aceasta va fi evaluată de Arduino ca fiind valoare1 atunci când condiția este îndeplinită și ca valoare2 în caz contrar.

altfel, vei mai observa declararea unui loc în memorie ca număr real - float, în afara celor două blocuri, setup și loop. declarat în acest fel, R poate fi accesat fără nicio problemă din orice parte a programului, în limbaj tehnic numindu-se variabilă globală.

cum măsor capacitatea electrică?

în cazul multimetrelor comerciale, măsurarea capacității electrice se găsește la modelele mai costisitoare. cu ajutorul dispozitivelor hardware pe care un Arduino le conține, capacitatea poate fi măsurată relativ facil.

puțină teorie. formula care vine în ajutor este chiar definiția capacității electrice, ca fiind raportul între sarcina stocată într-un capacitor și diferența de potențial între terminalele acestuia, o formulă ușor de reținut fonetic:

$$Q = C \times U \\ \frac{dQ}{dt} = I$$

unde:
Q este sarcina electrică exprimată în Coulombi; poate puțin abstractă, însă cu o proprietate extrem de interesantă: viteza de variație a sarcinii electrice este chiar curentul electric;
C este capacitatea electrică a capacitorului și se măsoară în Farazi (Farad, la singular); în practică, subunitățile sunt frecvent întâlnite, în timp ce valori de ordinul Farazilor sunt specifice supercapacitorilor;
U este diferența de potențial între terminalele capacitorului, exprimată în volți;
t este timpul, măsurat în secunde, iar;
I este curentul, măsurat în amperi; așa cum îmi atrăgea atenția o studentă (profesoară de fizică), în mod obișnuit, în limbajul de lemn al manualelor școlare, se folosește pentru această mărime litera i (mic). nu-mi plac convențiile, așa că peste tot, curentul este notat cu I (mare, de la intensitate imporantă).

dacă în paralel cu un capacitor necunoscut încărcat la o diferență de potențial inițială U0 conectezi un rezistor R, cu valoare cunoscută, capacitorul se va descărca prin intermediul rezistorului, transformând energia stocată în căldură.

Arduino - Capacitance Measuring

ecuațiile implicate sunt următoarele:

$$ \begin{cases} Q(t) = C \times U(t) \\ U(t) = R \times - \frac{dQ(t)}{dt} \\ U(t=0) = U_{0} \end{cases} \Rightarrow \begin{cases} U(t) = -RC \times \frac{dU(t)}{dt} \\ U(t=0) = U_{0} \end{cases} $$

ultima formulă este o ecuație diferențială ordinară, pentru care e suficient să găsim o soluție particulară. dacă am reușit asta, soluția va fi unică, mulțumită teoremei Cauchy-Lipschitz. semnul minus din a doua ecuație îmi spune că nu am nicio sursă de tensiune în circuit și că rezistorul consumă sarcina din capacitor. interpretarea asta naivă, când e formalizată are denumirea de lege a lui Kirchhoff. având în vedere că în stânga egalului avem funcția, iar în dreapta derivata ei multiplicată cu o constantă, o soluție posibilă este o funcție exponențială, de forma eαt + β.

$$ e^{\alpha t + \beta} = -RC \frac{d}{dt} (e^{\alpha t + \beta}) \Rightarrow \\ e^{\alpha t + \beta} = -RC \alpha e^{\alpha t + \beta} \Rightarrow \alpha = -\frac{1}{RC} $$

parametrul β poate fi dedus din condiția inițială a diferenței de potențial

$$ U(t=0) = U_{0} \Rightarrow e^{-\frac{1}{RC}(t=0) + \beta} = U_{0} \Rightarrow \beta = ln (U_{0}) \\ U(t) = U_{0} e^{-\frac{1}{RC}t} $$

Capacitor Discharge

ultima formulă vine și cu ideea măsurării capacității necunoscute: încărcarea capacitorul până la o tensiune cunoscută, apoi descărcarea acestuia pentru un timp determinat t1, urmată de măsurarea diferenței de potențial între terminalele capacitorului, U1.

$$ U_{1} = U_{0} e^{-\frac{1}{RC}t_1} \Rightarrow C = \frac{t_1}{R}\frac{1}{ln(\frac{U_{0}}{U_{1}})} $$

sau folosind identificatorul sub-intervalului de măsură obținut folosind analogRead și luând în considerare că U0 va fi 5V, formula devine:

$$ C = \frac{t_1}{R}\frac{1}{ln(\frac{1023}{N_{analogRead(A0)}})} $$

terminalele Arduino sunt flexibile și pot fi configurate în funcție de nevoie, chiar în timpul rulării programului. mai exact, poți configura terminalul A0 ca ieșire și să-i stabilești un potențial de 5V față de GND, urmat de configurarea acestuia ca intrare și citirea tensiunii după descărcare.

// timpul de asteptare pentru descarcarea capacitorului, in microsecunde
long t_1 = 100000;
// valoarea rezistorului de descarcare, in ohmi
float rezistenta = 100000.0;

void setup() {
}

void loop() {
  // stabilesc terminalul A0 ca terminal de iesire
  pinMode (A0, OUTPUT);
  // conectam terminalul A0 la +5V pentru a incarca capacitorul
  digitalWrite (A0, HIGH);
  // astept ~1s pentru a fi siguri de incarcarea acestuia
  delay (1000);
  // stabilesc terminalul A0 ca terminal de intrare
  pinMode (A0, INPUT);
  // astept t_1 microsecunde pentru a descarca capacitorul prin rezistorul cunoscut
  // am ales microsecunde, deoarece in formula, capacitatea este direct proportionala
  // cu timpul, iar capacitatile obisnuite sunt de ordinul micro-farazilor
  delayMicroseconds (t_1);
  // citesc identificatorul sub-intervalului de masura
  int n = analogRead (A0);
  // in cazul in care n este 0, adica in acest timp capacitorul s-a descarcat de tot,
  // capacitatea este 0, altfel folosesc formula
  float capacitate = n > 0 ?
    ((float) t_1 / rezistenta) * log (1023.0 / n) :
    0;
}

notă
scala de măsură a capacității nu mai este liniară de această dată, majoritatea valorilor aflându-se în primii 20% din lățimea intervalului de măsură. parametrul pe care-l poți controla atunci când măsori capacități este raportul între timpul de descărcare și valoarea rezistorului de descărcare. cu ajutorul acestui raport poți stabili o scală potrivită valorilor pe care urmează să le măsori.

pentru a vedea cum sunt distribuite valorile măsurate în funcție de rezultatul aplicării analogRead am folosit GNU Octave.

Capacity vs AnalogRead

n = 5:10:1000; # creez un vector cu valori consecutive intre 5 si 1000, din 10 in 10.
plot(n,1./log(1023./n),'+r') # desenez graficul formulei in raport cu n, folosind + si culoarea rosie (r)
xlabel('analogRead(A0)') # pun legenda pentru axa ordonatelor
ylabel('capacitate(uF)') # pun legenda pentru axa absciselor
grid on # afisez un caroiaj pentru observarea mai simpla a valorilor
text (50,31,'max(capacitate) = 31.4660 uF') # afisez un text la pozitia 50 (ordonata) si 31 (abscisa)
text (50,32,'min(capacitate) = 0.1443 uF')

și totuși, cum citesc datele?

deja apreciez că ai citit până aici. a fost lung, dar cred că a meritat. tot ce-am făcut a fost să-ți arăt cum poți măsura, însă fără a putea citi undeva valoarea măsurată. pentru a putea face acest lucru, Arduino pune la dispoziție o conexiune care poate fi accesată prin intermediul cablului USB, interfața serială. așa cum îi spune și numele, prin intermediul ei poți trimite și recepționa secvențial biți. circuitele au de obicei nevoie de 3 fire pentru a putea face acest schimb de informații: unul comun (GND), unul prin care trimit date (TX) și unul prin care recepționează date (RX). pe plăcuța Arduino există un mic circuit care convertește semnalul USB în cele două de care are nevoie Arduino, RX și TX. semnalele sunt disponibile la terminalele digitale 0 (RX) și 1 (TX). viteza de transmitere a informațiilor poate varia între 1200 și 115200 de caractere pe secundă. în cele mai multe cazuri, recomand 9600 ca viteză de comunicație. și calculatorul și Arduino trebuie configurate cu aceeași viteză pentru a putea comunica.

în cele ce urmează vei folosi doar partea prin care Arduino poate trimite date către calculator. bucata de cod care trimite datele din oricare dintre programele de mai sus către calculator este următoarea:

void setup() {
  Serial.begin (9600);
}

void loop() {
  // aici vine codul programului de masura
  // ...
  // urmat de:
  // trimiterea catre interfata seriala a valorii, urmata de un sfarsit de rand
  Serial.println (valoare);
  // si o mica pauza de 1s sa poti citi informatiile mai usor
  delay (1000);
}

instrucțiunile folosite mai sus sunt Serial.begin care inițializează conexiunea serială și stabilește viteza de comunicare care fiind 9600 de caractere pe secundă și care în majoritatea situațiilor trebuie să fie prezentă în blocul setup. instrucțiunea Serial.println care trimite prin interfața serială parametrul dat urmat de un sfârșit de linie, pentru creșterea lizibilității.

ultimul pas e citirea valorii recepționate prin apăsarea butonului care deschide programul de monitorizare al interfeței seriale, în care se pot citi direct valorile:

Arduino - Serial Monitor

bogdan » arduino: termometru

09:21 am on Nov 7, 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ă:

temperatura este una dintre mărimile fizice pe care o conștientizezi imediat: e prea frig afară, e prea fierbinte supa, oare ai febră? pe lângă conștientizarea acestei mărimi, este foarte simplu să achiziționezi un termometru cu care să poți cuantifica aceste senzații. dar unde e farmecul dacă nu e făcut de tine și nu poți înregistra periodic datele pentru a urmări procesele în timp real? în cele ce urmează îți voi prezenta o variantă de termometru pe care o poți realiza cu Arduino.

termistor

un termistor este un dispozitiv electronic în care temperatura modifică modul în care conduce curentul electric: dacă temperatura crește, rezistența pe care acesta o opune trecerii curentului electric se micșorează și invers. un Arduino nu poate măsura rezistența direct. dispozitivele cele mai potrivite din interiorul acestuia pentru o astfel de operație sunt convertoarele analog-digitale, care transformă o tensiune într-un număr. mai exact, pentru fiecare 0,004882V aplicați la intrare, Arduino va înregistra câte o unitate, în așa fel încât pentru 0V va înregistra 0, iar pentru 5V numărul 1023, variația fiind liniară (ex. 1,5V → 307, 3,3V → 676). acest lucru nu te ajută prea mult direct, mărimea care variază cu temperatura fiind rezistența dispozitivului. ajutorul vine din partea legii lui Ohm:

$$U = R_{termistor} I$$

unde:
U este tensiunea electrică la bornele termistorului, în volți (V),
Rtermistor este rezistența acestuia, în ohmi (Ω), iar
I este curentul care trece prin circuit, în amperi (A).

Arduino pune prin conectorii săi mai multe tensiuni pe care le poți utiliza ca referință: 3,3V (3V3), 5V sau Vin. cum Arduino poate măsura tensiuni între 0 și 5V, 3,3V e o tensiune prea mică față de capătul intervalului, pierzând astfel din precizie, Vin poate fi cu mult peste cei 5V și are și dezavantajul că nu este stabilă, rămânând astfel opțiunea 5V.

notă
3,3V poate fi folosit destul de ușor, conectând un fir între terminalul AREF și 3,3V, stabilind în acest fel capătul intervalului la valoarea 3,3V. acest lucru poate fi făcut cu orice sursă de tensiune, cuprinsă între 0 și 5V. trebuie să ții cont însă, că valoarea unei unități se modifică corespunzător. în cazul 3,3V, o unitate va însemna 0,003222V (ex. 1,5V → 465, 3,3V → 1023).

alegând U = 5V, în legea lui Ohm rămân totuși două necunoscute, rezistența termistorului și curentul prin circuit. pentru curent însă putem să aplicăm încă o dată legea lui Ohm, introducând în circuit un rezistor cu valoare cunoscută, în serie cu termistorul. tensiunea la bornele acestui rezistor de referință este direct proporțională cu curentul prin circuit, tensiune pe care o poți măsura direct folosind Arduino.

Simple Thermometer Animation

fie Rreferință valoarea în ohmi a rezistorului de referință și cu Ureferință valoarea măsurată cu ajutorul Arduino:

$$\begin{cases}5V = (R_{termistor} + R_{referinta}) I \\ U_{referinta} = R_{referinta} I\end{cases} \Rightarrow \\ R_{termistor} = R_{referinta} (\frac{5V}{U_{referinta}} – 1)$$

dependența de temperatură a rezistenței termistorului este dată de ecuația Steinhart-Hart, puțin prea complexă pentru nevoi obișnuite. drept urmare îți recomand să folosești ecuația beta, o formă simplificată a ecuației anterioare, dar utilizată de producători în caracterizarea termistorilor comercializați:

$$\beta = \frac{ln(R_{termistor}) – ln(R_{25})}{\frac{1}{T} – \frac{1}{T_{25}}}$$

ecuația anterioară descrie dependența de temperatura absolută T, în Kelvin, a rezistenței termistorului Rtermistor, în ohmi, folosind trei constante: R25, în ohmi, care este rezistența termistorului la 25°C – de altfel și valoarea sub care termistorul se comercializează, T25, în Kelvin, fiind temperatura absolută care corespunde 25°C, adică 298,15K și β care este o constantă proprie fiecărui termistor și care se măsoară tot în Kelvin.

ecuația anterioară poate fi aranjată într-o formă mai prietenoasă pentru calculul direct al temperaturii, cu mențiunea că temperatura obținută se măsoară în Kelvin, astfel:

$$T = \frac{1}{\frac{1}{\beta}ln(\frac{R_{termistor}}{R_{25}}) + \frac{1}{T_{25}}}$$

următorul pas în rezolvarea problemei este identificarea constantelor lipsă. pentru a determina facil aceste constante, îți recomand să achiziționezi termistorul dintr-o sursă care pune la dispoziție și datele de catalog. la curs am ales un termistor cu valoarea R25 de 100KΩ. raționamentul e simplu: 100KΩ deoarece puterea disipată prin trecerea curentului electric prin termistor nu-l va încălzi suficient pentru a afecta măsurătorile, iar în foaia de catalog vei găsi direct valoarea lui β. pentru termistorul pe care l-am ales, β=4600K.

Termistor Beta Characteristic

notă
pentru rezistorul de referință am ales o valoare egală cu R25. raționamentul e simplu, atunci când rezistenețele sunt egale, tensiunea în punctul median este jumătate din tensiunea de alimentare, plasând astfel temperatura de 25°C în centrul scalei. din cauza neliniarității funcției de conversie, îmi convine să am această valoare situată central deoarece este o valoare de interes pentru activitatea umană și măsurătorile din vecinătatea ei vor avea o precizie mai mare. în general, e bine să aleg rezistorul de referință în așa fel încât temperatura medie a intervalului de interes să corespundă jumătății tensiunii de alimentare.

programare

în acest moment ai toate informațiile necesare pentru a putea scrie un mic program Arduino care să citească temperatura cu ajutorul unui termistor conectat în configurația descrisă mai sus. pentru început, orice program Arduino conține o funcție setup și o funcție loop.

conținutul funcției setup este rulat de Arduino atunci când acesta este inițializat, fie după reset, fie atunci când este alimentat, o singură dată. în această funcție se trec de obicei instrucțiuni pentru a inițializa perifericele Arduino. în cazul de față, convertorul analog-digital care poate măsura tensiuni este inițializat automat.

funcția loop este cea care ne interesează aproape întotdeauna. proprietatea care o face interesantă este că atunci când lista de instrucțiuni pe care le conține a fost epuizată, loop se va reseta și o va lua de la început. Arduino va face acest lucru până când este apăsat butonul reset, se încarcă un program nou sau alimentarea plăcuței este întreruptă.

în fragmentul de cod de mai jos apare și funcția analogRead cu parametrul A0. această funcție primește ca parametru eticheta unuia dintre pinii analogici ai Arduino, de la A0 la A5 și întoarce un număr întreg cuprins între 0 și 1023, număr care este proporțional cu tensiunea aplicată intrării A0. având în vedere că numărul întors este întreg, pentru a forța zecimale în calculele realizate de Arduino, am ales să reprezint toate numerele în format zecimal (”cu virgulă”).

cuvântul cheie float din fața etichetei U_referinta anunță Arduino să rezerve memorie în care să stocheze un număr zecimal care poate fi apelat întotdeauna folosind eticheta U_referinta. același lucru se aplică și pentru R_termistor, T și t. vei observa că eticheta acestor fragmente de memorie ține cont de tipul literelor (mici sau mari), astfel încât T și t vor face referire la fragmente separate, deci valori diferite.

am mai utilizat încă două funcții, log, care corespunde logaritmului natural, luând ca parametru un număr zecimal și întorcând tot un număr zecimal și delay, care suspendă execuția listei de instrucțiuni pentru numărul de milisecunde furnizate drept parametru. dacă nu folosești această funcție, operațiile vor fi efectuate extrem de rapid, furnizând o cantitate mare de date, oarecum irelevantă, mai ales datorită vitezei relativ mici de variație a rezistenței termistorului cu temperatura.

void setup() {
}

void loop() {
  // măsor tensiunea în volți înmulțind valoarea citită prin
  // conectorul A0 cu factorul 0.004882V (= 5V / 1024)
  float U_referinta = 0.004882 * analogRead (A0);
  // din U_referință și valoarea lui R_referință = 100k ohmi
  // calculez R_termistor
  float R_termistor = 100000.0 * (5.0 / U_referinta - 1.0);
  // folosim ecuația temperaturii, în care beta = 4600K și
  // R_25 = 100k ohmi, T_25 = 298.15K și obținem temperatura în
  // Kelvin
  float T = 1.0 /
  (1.0 / 4600.0 * log (R_termistor / 100000.0) + 1.0 / 298.15);
  // pentru a afla temperatura în grade Celsius, scădem din
  // temperatura în Kelvin 273.15
  float t = T - 273.15;
  // fac o pauză de 1 secundă, după care repet instrucțiunile
  delay (1000);
}

micul program de mai sus poate fi compilat și încărcat pe plăcuța ta Arduino folosind Arduino IDE. conectezi Arduino prin cablul USB la calculator și aștepți ca sistemul de operare să instaleze automat uneltele pentru conectare. apoi deschizi aplicația Arduino și vei alege utilizând meniul Tools → Boards → modelul de Arduino pe care îl ai și folosind meniul Tools → Port → portul la care ai conectat plăcuța. te prinzi ușor despre ce port e vorba. dacă nu, le poți încerca fără probleme pe toate, până reușești.

în fereastra care s-a deschis, copiezi codul de mai sus, sau mai bine, îți recomand să-l scrii de mână. poți sări peste liniile care încep cu // și sunt colorate cu albastru. ele reprezintă comentarii și nu influențează programul. după ce ai terminat, vei apăsa butonul Verify, așteptând ca în bara de stare a ferestrei să apară cuvintele Done compiling.

Arduino - Verify - Upload

în situația în care au apărut erori, verifică în ordine următoarele lucruri:

  • ai pus ; (punct și virgulă) după fiecare instrucțiune? din experiența cu studenții, e de departe cea mai frecvent întâlnită eroare;
  • fiecare paranteză deschisă, fie că este paranteză rotundă sau acoladă, trebuie să se și închidă la un moment dat, fix ca la matematică, în ordinea inversă deschiderii lor. adică {…(…)…} este corect, în timp ce sunt greșite: {…(…}…), {…(…)…, {…)…}
  • felul literelor contează mult, așa că verifică dacă ai scris cu litere mari și mici ca în exemplul de mai sus; etichetele, cum sunt U_referinta, R_termistor, T și t le poți modifica după plac, cu singura mențiune să le modifici pe peste tot;
  • cu excepția liniilor cu comentarii, în rest nu ai voie să folosești diacritice; știu, nu e așa comun, dar am întâlnit și eroarea asta;

Arduino Done Compiling

ultimul pas este cel de încărcare al programului pe plăcuța Arduino, apăsând butonul Upload. în caz de erori, cele mai întâlnite sunt:

  • nu ai selectat corect portul la care este conectat Arduino;
  • nu ai selectat corect tipul de plăcuță Arduino; pentru a te asigura, tipul plăcuței este scris pe ea, nu ai cum să greșești;
  • portul USB la care ai conectat plăcuța nu funcționează corect și va trebui să alegi altul;
  • cablul USB nu este de calitate și va trebui să-l schimbi sau să folosești unul mai scurt;

note

  • în jargonul programatorilor, loop și setup poartă denumirea de proceduri, pentru că nu returnează nimic. cuvântul cheie void care alcătuiește definiția unei funcții specifică fix acest lucru: că respectiva bucată de cod întoarce ”vid” (nimic). prefer să folosesc denumirea de funcție, care provine din matematică, pentru a simplifica asimilarea noțiunilor.
  • în teorie funcția log are nevoie de o bibliotecă de funcții să funcționeze. din fericire, Arduino știe să o adauge implicit, așa că micul program va funcționa fără probleme așa cum este scris.
  • în ceea ce privește tipul de variabile pe care lucrează implicit log, care se numește double, pentru majoritatea plăcuțelor Arduino, acesta este doar un sinonim pentru float. chiar dacă nu ar fi așa, Arduino se ocupă de conversia implicită între cele două formate.
  • spațiile nu sunt relevante, însă ele asigură lizibilitate programului scris, așa că dacă ți-e lene, le poți omite.

lcd

până acum, Arduino știe ce temperatură măsoară, însă o ține pentru el. pentru a o putea afișa, are nevoie de un dispozitiv extern, cum ar fi un ecran LCD cu 2 rânduri a câte 16 caractere, foarte des întâlnit (și ieftin). în plus, mai ai nevoie de un mic potențiometru semiregrabil necesar în ajustarea contrastului, cu valoarea de 10KΩ, o rezistență de 220 de Ω, câteva fire de conexiune mai lungi, colorate și o plăcuță pentru experimente (breadboard). tot ce ai de făcut în continuare pe partea fizică este să replici construcția din desenul următor, după ce ai deconectat Arduino de la portul USB:

Arduino Thermometer with LCD

sfatul meu este să începi cu firele roșii și negre (alimentarea), plasând apoi rezistența de 100KΩ, cea de 220 de Ω, termistorul și potențiometrul semiregrabil pe plăcuța pentru experimente, urmate de firele mov, galben, portocaliu și roz, iar într-un final de cele verzi. verifici de două ori așezarea firelor și conexiunile, ții degetele încrucișate și conectezi din nou Arduino la portul USB. nu-ți face griji, portul USB este protejat în caz de probleme, așa că nu ai ce strica la calculator. un semn bun, că totul merge, îl reprezintă luminarea ecranului LCD. ajustând potențiometrul semiregrabil, ar trebui să vezi la unul dintre capete 2 rânduri cu câte 16 pătrățele negre alcătuite din pixeli, pe ecran. dacă nu funcționează, verifică din nou conexiunile.

vei modifica acum micul program de mai sus pentru a include ecranul LCD folosind biblioteca inclusă în aplicația Arduino, LiquidCrystal.h. aceasta definește un tip de obiect LiquidCrystal care poate fi folosit pentru a controla ecranul LCD. pentru început, vei ințializa obiectul dându-i o denumire și specificând conexiunile către Arduino. pentru a inițializa ecranul, vei adăuga instrucțiunea de inițializare a acestuia în funcția setup: lcd.begin. inițializarea se face specificând dimensiunile ecranului. Arduino știe prin intermediul LiquidCrystal.h să comunice cu ecrane de mai multe dimensiuni.

// am nevoie de o bibliotecă care știe să comunice cu ecranul LCD
#include <LiquidCrystal.h>
// inițializez un obiect de tip LiquidCrystal pe care îl denumesc
// lcd, căruia îi spun la ce conectori Arduino am conectat firele
// 12 = galben, 11 = portocaliu, 5, 4, 3, 2 = cele 4 fire verzi
LiquidCrystal lcd (12, 11, 5, 4, 3, 2);

void setup() {
  // inițializez ecranul și îi spun că are 2 rânduri cu 16 caractere
  lcd.begin (2, 16);
}

după ce ai calculat temperatura, vei curăța ecranul pentru a te asigura că nu e nimic pe el folosind lcd.clear. vei muta cursorul pe coloana 0, linia 0 (colțul stânga-sus) folosind lcd.setCursor, cu parametri coloană (de la 0 la 15) urmat de linie (de la 0 la 1), după care vei tipări diferite lucruri, în ordine, folosind lcd.print. lcd.print poate primi ca parametri șiruri de caractere delimitate de ghilimele, numere zecimale sau întregi, dar și caractere speciale, prin codul lor, cum este simbolul °, dar pe care trebuie să-l diferențiezi de un număr întreg obișnuit, folosind sintaxa (char). această operație se numește conversia tipului (cast în engleză) și îi spune Arduino să trateze numărul 223 ca un caracter (char).

notă
lcd este o etichetă pe care am ales-o eu în vederea accesării obiectului LiquidCrystal. dacă ai altă preferință, cum ar fi ecran, o poți folosi fără probleme. trebuie să ții cont că funcțiile apelate se vor modifica corespunzător: lcd.begin devine ecran.begin, lcd.clear devine ecran.clear, lcd.setCursor devine ecran.setCursor și lcd.print devine ecran.print. acest tip de funcții, care depind de obiectul folosit, în jargonul programatorilor se numesc metode și se folosesc de starea internă a obiectului de care sunt anexate pentru a-și îndeplini rolul. fără inițializarea unui astfel de obiect, ele nu pot fi folosite, așa că să nu uiți bucata anterioară funcției setup în care alegem o etichetă pentru un obiect de tip LiquidCrystal.

void loop() {
  // ...
  // bucata de până aici rămâne neschimbată
  float t = T - 273.15;
  // dacă e ceva scris pe ecranul LCD, îl șterg
  lcd.clear ();
  // îi spun ecranului LCD că vreau să scrie începând cu colțul stânga-sus
  // primul 0 îl reprezintă coloana (0-15), al doilea rândul (0-1)
  lcd.setCursor (0,0);
  // pornind de la cursor, afiseaza T = 
  lcd.print ("T = ");
  // apoi temperatura în grade Celsius
  lcd.print (t);
  // urmată de simbolul grad
  lcd.print ((char) 223);
  // și apoi de litera C
  lcd.print ("C");
  // fac o pauză de 1 secundă, după care repet instrucțiunile
  delay (1000);
}

după încărcarea programului pe plăcuța Arduino, singurul lucru pe care îl mai ai de făcut este să ajustezi contrastul în așa fel încât literele și cifrele să fie lizibile.

bogdan » arduino: lucruri necesare

05:24 pm on Oct 31, 2018 | #more | tags:

am descoperit Arduino în 2009, într-o reclamă Google. ce m-a uimit e că pe o mică plăcuță am regăsit ceva asemănător primului calculator pe care l-am văzut în viața mea, CIP-03, o clonă de ZX-Spectrum. un calculator care mi-a gravat în minte așteptările pe care trebuie să le am de la o mașină de felul ăsta: să pot să o programez și să interacționeze cu lumea din exterior. Arduino le face pe-amândouă, lucru pentru care l-am inclus în cursul meu de modelare și simulare pe care-l țin periodic la Facultatea de Fizică. recent, am primit la curs întrebarea: ce-ar însemna un set minim cu care să pot experimenta cu Arduino? așa că răspunsul vine în continuare.

arduino

Arduino UNO R3 – prefer varianta cu USB-type-B (conector USB pentru imprimantă) și AtMega328-PU (cip cu piciorușe într-un soclu); raționamentul e simplu: așa cum am văzut în propriul buzunar, când faci experimente cu Arduino poți să-l strici; un cip separat e numai o fracțiune din preț și e majoritar componenta care se strică cel mai ușor; alt motiv este că în timp, vrei să incluzi Arduino în proiecte mai mici și mai statice, iar un Arduino cu soclu îți permite să programezi mai multe cipuri fără bătăi de cap. când ai avansat, poți trece la variantele cu cip miniatură lipit, în diferite forme, dar primul Arduino ar trebui să fie UNO R3 clasic; (29 de lei, 30 de lei, 146 de lei)
LED-uri – pentru că primul lucru pe care-l vei face cu el e să aprinzi luminițe; e simplu și oferă satisfacții imediate; recomand LED-uri de 5mm, cât mai multe, cât mai colorate (49 de lei); câteva IR nu strică (3,5 lei / 5 bucăți);
fire de conexiune – Arduino e făcut să folosească fire de conexiune; dacă LED-urile se pot pur și simplu ”înfige” în sloturile Arduino, celelalte componente sunt mult mai greu de conectat așa că două seturi de fire de conexiune (un set are 10 fire), unul cu pini la ambele capete (”tată-tată”), celălalt cu pini și socluri (”mamă-tată”) sunt valoroase (3,5 lei / 10 bucăți / 15 cm / tată-tată și 4,5 lei / 10 bucăți / 15 cm / tată-mamă);
placă de conexiune (breadboard) – pentru a putea crea mici circuite ai nevoie de o placă de conexiune; recomand una medie (half-size), cu linii pentru alimentare – acestea sunt linii perpendiculare pe rândurile conectate ale plăcii, care pot fi folosite pentru alimentarea circuitului (5 lei, 5 lei, 33 de lei (calitate mai bună));
ecran LCD – după ce ai aprins LED-uri și te-ai jucat puțin cu interfața serială, vei fi uimit că poți scrie mesaje pe un mic ecran LCD; îl recomand pe cel ieftin, cu conexiune paralelă; necesită în plus un potențiometru de 10KΩ și destul de multe fire, dar are avantajul că e ieftin (11 lei, 19 lei, 35 de lei); o variantă mai bună e versiunea I2C a acestuia, dar ceva mai scumpă (18 lei, 29 de lei, 76 de lei);
butoane – știi să afișezi lucruri, deci să obții informație din Arduino; e momentul să și introduci și ce e cel mai simplu decât să conectezi butoane cu contact momentan la el (push-buttons); un simplu meniu are nevoie de minimum 3 butoane: sus/jos pentru a naviga în el și un buton pentru a selecta opțiunea (4,5 lei / 3 bucăți, 6 lei / 3 bucăți);
potențiometre – când butoanele nu te mulțumesc, vei vrea să folosești un potențiometru; la un Arduino poți conecta între 1 și 6 (9 lei / 6 bucăți, 15 lei / 3 bucăți);
rezistori – Arduino limitează curentul prin terminalele sale, însă de multe ori ai nevoie fie să limitezi curentul mai mult, fie să le folosești pentru a forța anumite intrări într-o poziție (pull-up, pull-down) fie pentru a limita tensiunile analogice; valorile pe care le recomand sunt 470Ω (0,6 lei), 1KΩ (0,6 lei), 4.7KΩ (0,6 lei), 10KΩ (0,6 lei), 100KΩ (0,6 lei) și 1MΩ (0,6 lei). câte 10 din fiecare, pentru că oricum sunt ieftine;
punte H – una dintre aplicațiile cele mai spectaculoase ale Arduino este aceea de a face lucrurile să se miște; iar pentru a putea comanda motoare folosind Arduino, ai nevoie de o punte H; de preferat dublă pentru că e foarte ușor să controlezi mișcarea unui robot utilizând două motoare (10 lei);
tranzistori MOS-FET – de la LED-uri mici la LED-uri mai mari e un singur pas, așa că MOS-FET-urile sunt extrem de utile pentru controlul consumatorilor mari de curent; fie ca vrei să creezi o mini-seră încălzită sau să controlezi un element Peltier pentru a-ți răci băutura preferată, un MOS-FET e ideal; numărul minim recomandat este 2 (5 lei / 2 bucăți);
relee – când vezi că poți controla atâtea lucruri în jurul tău, te gândești cum poți trece la nivelul următor: poate să aprinzi o veioză? un releu te ajută să separi Arduino de tensiunile periculoase (priză, de cele mai multe ori). 2 sunt suficiente, de preferat cu tranzistorii și diodele necesare pentru control; dacă nu, se mai trec în listă câte un tranzistor și o diodă de curent mic pentru fiecare releu (7 lei / 2 bucăți, 19 lei / 2 bucăți);
conector baterie – la un moment dat proiectele vor deveni independente de interfața USB; în această situație, un conector de la o baterie de 9V la borna de alimentare a Arduino devine esențial (6 lei); o variantă mai bună, este obținearea unei cutii pentru baterii AA minimum 4 (3,5 lei), deși recomand 6 (5 lei) cu conector pentru alimentare – deoarece Arduino consumă destul de mult curent, iar o baterie de 9V are o capacitate relativ mică;

câteva tipuri de senzori

fotorezistori – așa cum îi spune numele, un fotorezistor își variază rezistența cu lumina incidentă: cu cât e mai multă lumină, cu atât rezistența e mai mică; poate fi extrem de util pe post de ”ochi” pentru un mic proiect Arduino; doi sunt utili (3,9 lei / 2 bucăți, 4 lei / 2 bucăți);
termistor – pe lângă lumină, temperatura e o altă mărime fizică fundamentală care poate fi convertită ușor în rezistență; un termistor este o rezistență care crește atunci când e frig și scade când e cald; recomand un termistor cu valoarea de 100KΩ; unul e suficient (3 lei);
microfon – poate nu la fel de facil de utilizat, însă cu siguranță spectaculos, un senzor util este cel pentru sunet; recomand un microfon cu electret și amplificator integrat, pentru a putea trimite semnalul acestuia direct, către un Arduino; util în proiectele în care vrei ca Arduino să ”simtă” muzica (36 de lei);
senzor de distanță – aici sunt mai multe opțiuni: optic sau cu ultrasunete; îl recomand pe cel din urmă, având o rază mai mare de acțiune, cele optice putând fi ușor replicate cu simple leduri (20 de lei, 25 de lei, 29 de lei);

câteva tipuri de elemente de control

difuzor / buzzer – după semnale optice, a doua metodă de a obťine feedback de la Arduino sunt sunetele; conectat direct la unul dintre piciorușele Arduino, un mic difuzor poate fi elementul care poate face proiectul tău mai interesant (2 lei, 6 lei, 10 lei);
servomotoare – sunt mici motoare care au inclus un circuit de feedback și cu ajutorul cărora poți interacționa prin mișcări precise cu lumea reală; recomand două, în special utile pentru a poziționa un senzor (ultrasonic sau optic) cu două grade de libertate (pe suprafața unei sfere) pentru, spre exemplu, un radar 3D (40 de lei / 2 bucăți, 46 de lei / 2 bucăți, 58 de lei / 2 bucăți);
motoare – sunt elementele care asigură autonomie unui robot controlat de Arduino; recomand două, cu reductor și roată inclusă (24 de lei / 2 bucăți);

câteva elemente esențiale, dar care pot fi achiziționate ulterior

fire de conexiune pentru breadboard (jumperi) – deși nu sunt necesare, sunt extrem de utile atunci când dezvolți circuite complexe folosind o placă pentru conexiuni; permit evitarea interferențelor parazite cauzate de firele prea lungi (15 lei, 25 de lei, 32 de lei);
un programator ICSP – am luat unul atunci când am stricat primul Arduino; e util pentru reprogramarea fără bătăi de cap a bootloader-ului pe cip și nu numai (60 de lei, 20 USD – include convertor USB-serial); poate fi simulat cu un alt Arduino;
un multimetru – nu neapărat necesar, este util în depanarea circuitelor și identificarea mai ușoară a problemelor: fie e un fir întrerupt, fie o tensiune nu e suficientă, fie un curent e prea mare; multimetrul identifică cu ușurință aceste probleme; ca să nu mai vorbim de rezistorii cu peliculă metalică pe care eu nu disting culorile (55 de lei, 61 de lei); poate fi emulat cu puțină îndemânare cu un Arduino;
convertor USB-serial – nu e esențial, dar incredibil de util pentru reprogramarea proiectelor care folosesc variante miniaturizate de Arduino (15 lei, 39 de lei, 75 de lei – include ICSP); poate fi emulată cu un Arduino;
o sursă coborâtoare/ridicătoare de tensiune – pentru proiectele mobile e o necesitate; Arduino conține un stabilizator de tensiune liniar, ieftin și robust, dar lipsit de eficiență; un astfel de circuit permite alimentarea de la acumulatori; recomand o variantă care să poată susține 5V la cel puțin 1.5A (30 de lei, 30 de lei);

raspberry pi

Raspberry PI 3 – e un mic calculator pe o plăcuță de mărimea unui card bancar; dacă Arduino e echivalentul unui ZX-Spectrum, Raspberry PI 3 e un fel de Pentium 3; adică destul de puternic pentru a vedea filme și a naviga pe internet; mai mult, are 4 porturi USB pentru periferice normale, o mulțime de spațiu de stocare și interfețe de rețea: bluetooth, wi-fi și ethernet; altfel, face cam ce face Arduino, dar cu un sistem de operare obișnuit, care suportă compilator C, limbaje de scripting (PHP, Ruby, Perl, JS) și servere http și ssh (209 lei, 245 de lei, 259 de lei);
RPI-UNO-HAT – produs de echipa din spatele watterott.com, RPI-UNO-HAT este o extensie a Raspberry PI care conține un Arduino împreună cu circuitele necesare interfațării; deși poate fi emulat cu un Arduino și câteva componente discrete, recomand varianta asta pentru simpliatatea în utilizare; poți astfel să construiești lucruri complexe, conectate la internet; RPI-UNO-HAT necesită un mic circuit pentru alimentarea Raspberry PI prin conectorul comun, care în această situație trece în lista de componente obligatorii (16 EUR);

software

Arduino – așa cum îi spune numele e complementul software al plăcuței; cu ajutorul acestuia poți programa o mulțime de plăcuțe compatibile cu Arduino (download: windows, linux, mac os x);
GNU Octave – dacă ești familiarizat cu MatLab, GNU Octave e o variantă open-source a acestuia; altfel, imaginează-ți-l ca pe un calculator foarte avansat care face în primul rând operații cu matrice (download: windows, linux, mac os x); sunt utile pachetele suplimentare io (download) și instrument-control (download);
native script – este o extensie de node.js care permite dezvoltarea de aplicații mobile; foarte util atunci când vrei să conectezi proiectul tăubla telefon (ghid instalare);
Microsoft Visual Studio Code – de departe cel mai bun editor text pentru programatori; chiar dacă e făcut de Microsoft, e open source, rulează cam pe orice platformă și e bun! (download);
Git/GitHub – atât clientul desktop cât și cel de consolă, împreună cu un cont gratuit, pentru a salva și a împărtăși cu toată lumea experimentele tale (download);

bogdan » how to build a maze

01:42 pm on Oct 7, 2018 | #more | tags:

in this post i’ll use C to construct a maze, that will be later used for my swarm experiments.

i’ll be using a graph-model for the maze: each vertex is a room from the maze, which is connected by edges to other rooms. in a 2-D classic maze of size m×n, with m spanning from top to bottom and n spanning from left to right, each room has at most 4 neighbors. this means each vertex in my graph will have at most 4 edges: top, bottom, left and right. the total number of edges is m&times(n-1) for vertical edges (top, bottom) and (m-1)×n for horizontal edges (left, right). in order to create a maze, in the previously described graph, i’ll choose using a version of Dijkstra algorithm a maximum spanning tree. to make the maze random, i’ll mark each edge with a random weight and then run the maximum spanning tree algorithm.

maze model

i’ll be using matrices as data structures. this means that each vertex will be labeled with a pair (i,j) with i from 0 to m-1 and j from 0 to n-1. as the graph is not oriented, i’ll have:

  • if (i,j) is linked to (i,j+1), than i’ll store in my matrix, on position (i,j) the weight of the edge [(i,j),(i,j+1)];
  • if (i,j) is linked to (i+1,j), than i’ll store in my matrix, on position (i,j+n) the weight of the edge [(i,j),(i+1,j)];
  • no need to store the [(i,j),(i,j-1)] edge as it’s equivalent to [(i,(j-1)),(i,(j-1)+1)];
  • also, no need to store connections at the lower and right edge of the maze;

maze storage

the number of required edges’ weights can be held in a m×(2×n-1)-n vector, corresponding to an incomplete m×(2×n-1) matrix.

building the weight matrix

this step is quite easy. just loop over all m×(2×n-1)-n and assign them a random value between 1 and 255. i’ll be using 0 to represent edges that are missing.

/**
 * the function initializes the weights matrix as described above;
 * @param m (int) the dimension of the maze on top-bottom axis;
 * @param n (int) the dimension of the maze on left-right axis;
 * @returns: an uint8_t matrix, of size (m-1)*2*(n-1) filled with random data;
 */
uint8_t * maze_weight_init (int m, int n) {
        /** @var maze the weight matrix */
        uint8_t * maze;
        /** @var c is  a variable index */
        int c;

        /** i reserve the space in the computer memory for the matrix */
        maze = (uint8_t *) malloc ((m * (2 * n - 1) - n) * sizeof (uint8_t));
        /** if memory reservation is successful, i'm filling the matrix */
        if (maze != NULL)
                for (c = 0; c < m * (2 * n - 1) - n; c++)
                        *(maze + c) = (uint8_t) ((1 + rand ()) % 256);

        return maze;
}

tip:
the matrix is stored as a single vector, meaning a cell on the position (i,j) can be accessed by finding as the (i×cols+j)-th component of the vector. this map is bijective, meaning that the i-th component of the vector corresponds to position (i/cols, i%cols) in the matrix, where / is the integer division and % is the reminder operator.

sorting the weight matrix

the matrix is not useful like this. it needs to be sorted. i'm using a simple (inefficient) bubble sort algorithm. i'll take advantage of how i stored the matrix so i can simply arrange the associated vector. i'll create a vector into which i'll keep the association of indices.

bubble sort works like this: take the first weight from the vector, and if it's heavier that the one above, swap their places. this has to be repeated until the entire vector is sorted: this happens when there are no swaps on a swipe.

/** the function is sorting the associated vector V, returning a mapping of the
 * sorted indices, M, with V(i) being the original vector, while V(M(i)) is the sorted one
 * @param weights (uint8_t *) is the original vector
 * @param m (int) is the top-bottom size of the maze
 * @param n (int) is the left-right size of the maze
 * @returns: an int mapping vector between the sorted version and the original one
 */
int * maze_edges_sort (uint8_t * weights, int m, int n) {
        /** @var indices the vector containing the sorted mapping */
        int * indices;
        /**
         * @var c a variable index
         * @var t a temporary variable used in swapping two indices
         * @var o a temporary variable storing last index swapped
         * @var l, initially the length of the weights vector,
         *        after, the length of the unsorted vector
         */
        int c, t, o, l = m * (2 * n - 1) - n;

        /** initialize indices with an identity mapping i->i */
        indices = (int *) malloc (l * sizeof (int *));
        for (c = 0; c < l; c++)
                *(indices + c) = c;

        if (indices != NULL)
                /** while there's still some unsorted part of the vector */
                while (l > 0) {
                        /** o holds the last swapped value */
                        o = 0;
                        /** i'm looping until the end of the unsorted vector */
                        for (c = 1; c < l; c++) {
                                /** if the weights don't respect the sort order, swap them */
                                if (*(weights + *(indices + c - 1)) > *(weights + *(indices + c))) {
                                        /**
                                         * of course, i'm swapping indices in the mapping,
                                         * not real values, as the initial vector remains
                                         * unchanged, only the mapping function deviates from
                                         * identity map
                                         */
                                        t = *(indices + c - 1);
                                        *(indices + c - 1) = *(indices + c);
                                        *(indices + c) = t;
                                        /** store the last swapped index */
                                        o = c;
                                }
                        }
                        /** the length of the unsorted vector equals the last swapped index */
                        l = o;
                }

        return indices;
}

building the maximum spanning tree

this algorithm is more complex that the previous ones. for easier understanding, i need to explain some notions:

  • a graph is a collection of vertices and edges, each edge connecting two vertices; it can and usually has loops - meaning starting from one vertex, it can be reached again going edge-by-edge; like a city with ring-roads;
  • a tree is a graph with no loops; it means that starting with a vertex, and going edge-by-edge you'll never get to the same vertex again; like a computer network;
  • in a graph (including a tree), a sub-tree is a collection of vertices and edges from the initial graph that behave like a tree - there are no loops;

i'll start with an empty set of sub-trees from the original graph. this will be called a forest set. forests have at least one tree. at the end of the algorithm i'll have in this set a tree that will link all vertices from the original graph. this is called a maximum spanning tree.

with each sorted edge, lighter to heavier, i'll do the following:

  • if the edge is not connected to any sub-tree from my sub-tree set, i'll add the edge to the sub-tree set, coloring it with a new color;
  • if the edge has one vertex connected to a sub-tree in my sub-tree set, i'll add the edge to the sub-tree set, coloring it with the same color as the rest of the sub-tree;
  • if the edge has vertices on different sub-trees, than i'll add the edge and color both sub-trees, as well as the edge, with a common color. i'll use indexed colors, so i'll choose the minimum color;
  • if the edge has vertices on the same sub-tree, i'll skip this edge; in this situation, the edge is creating a loop in the tree, making it a graph;
/**
 * the procedure takes as parameters a weight matrix together with its size
 * and modifies in place the weights matrix, removing the unnecessary edges
 * an edge is kept, if the weight is strict positive, while an edge is
 * deleted, if its weight is zero.
 * @param weights (uint8_t *) is the weight vector
 * @param m (int) is the maze top-bottom size
 * @param n (int) is the maze left-right size
 */
void maze_init (uint8_t * weights, int m, int n) {
        /**
         * @var indices is the sorted mapping for the weights vector;
         * @var matrix is a vector-stored matrix that has 0 on (i,j)
         *         0 on (i,j) position, if the (i,j) vertex was not visited
         *         color>0 on (i,j) position, if the (i,j) vertex was visited
         *         and (i,j) is part of the "color" sub-tree
         */
        int * indices, * matrix;
        /**
         * @var c,d are variable indices;
         * @var row,col are the row and column indices in the @see matrix
         * @var n_row,n_col are the indices for the vertex connected to
         *        (row,col);
         * @var min_color is the minimum "color" when merging an existing
         *        sub-tree with a new edge. the edge can link two existing
         *        sub-trees, meaning i'll have to choose a single color for both
         * @var max_color is analog with @see min_color;
         * @var color is the current available color for new sub-trees;
         */
        int c, d, row, col, n_row, n_col, min_color, max_color, color = 1;

        /** reserve memory for the is-visited? matrix */
        matrix = (int *) malloc (m * n * sizeof (int));
        for (c = 0; c < m * n; c++)
                *(matrix + c) = 0;

        /** sort the graph weights */
        indices = maze_edges_sort (weights, m, n);

        /** loop through sorted edges */
        for (c = 0; c < m * (2 * n - 1) - n; c++) {
                /** get the edge first vertex label as (row, col) */
                row = *(indices + c) / (2 * n - 1);
                col = *(indices + c) % (2 * n - 1);

                /**
                 * based on how is stored, get the label for the
                 * second vertex associated with the edge
                 */
                if (col < n - 1) {
                        n_row = row;
                        n_col = col + 1;
                }
                else {
                        col = col - n + 1;
                        n_row = row + 1;
                        n_col = col;
                }

                /**
                 * check if the current edge can be added to the forest:
                 * the edge needs to fulfill all requirements:
                 * - the edge is not already part of a tree, meaning that both
                 *        vertices are the same non-zero color
                 */
                if (
                        (*(matrix + row * n + col) == *(matrix + n_row * n + n_col)) &&
                        *(matrix + row * n + col) > 0
                ) {
                        *(weights + *(indices + c)) = 0;
                        continue;
                }

                /**
                 * find the color of the new vertices, by getting minimum and
                 * maximum color. both can be zero, if we start a new sub-tree
                 */
                if (*(matrix + row * n + col) < *(matrix + n_row * n + n_col)) {
                        min_color = *(matrix + row * n + col);
                        max_color = *(matrix + n_row * n + n_col);
                }
                else {
                        min_color = *(matrix + n_row * n + n_col);
                        max_color = *(matrix + row * n + col);
                }

                if (min_color == 0) {
                        /**
                         * here, min = max = 0, this means we have a new sub-tree
                         * so i color it with the next available color
                         */
                        if (max_color == 0) {
                                *(matrix + row * n + col) =
                                *(matrix + n_row * n + n_col) = color ++;
                        }
                        /** here, the edge has an open end, the other is connected */
                        else {
                                *(matrix + row * n + col) =
                                *(matrix + n_row * n + n_col) = max_color;
                        }
                }
                else {
                        /**
                         * here min, max > 0, both different, this means that the
                         * edge is connecting two different color sub-trees, so i
                         * make both the same color (min color)
                         */
                        *(matrix + row * n + col) =
                        *(matrix + n_row * n + n_col) = min_color;

                        for (d = 0; d < m * n; d++)
                                if (*(matrix + d) == max_color)
                                        *(matrix + d) = min_color;
                }
        }

        /** remember to free the reserved memory */
        free (indices);
        free (matrix);
}

putting it all together

piece of cake. first, i'll have to initialize my weights. than, to build the maximum spanning tree. a good exercise is to display the maze on the screen using ASCII art.

#include  /** required for malloc, srand and rand functions */
#include  /** useful for printing stuff, with printf */
#include  /** i need the uint8_t definition */
#include  /** i need the time function */

/**
 * this is the normal, unix-style format for the program entrypoint
 * @param argc (int) is the number of arguments from the command line;
 * @param argv (char **) is an array of strings, containing the command line arguments;
 * @returns: an integer, zero if no error has occurred.
 */
int main (int argc, char ** argv) {
        /** @var weights (uint8_t *) my weights matrix */
        uint8_t * weights;
        /** @var m, n (int) the maze size */
        int m = 4, n = 4;

        /** make the random numbers random 🙂 */
        srand (time (NULL));

        /** initialize the weights matrix */
        weights = maze_weight_init (m, n);
        /** build a maximum spanning tree from the matrix */
        maze_init (weights, m, n);

        /** tell the operating system that there's no error */
        return 0;
}

bogdan » arduino: conectarea la octave

10:29 pm on Apr 9, 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ă:

GNU Octave:
GNU Octave este o alternativa gratuită a MathWorks MatLab. ambele fac în mare același lucru pentru utilizatorul de rând: efectuează extrem de eficient operații matematice cu matrice. mai mult, în cele mai multe cazuri, codul scris pentru unul funcționează fără probleme în celălalt. GNU Octave poate fi descărcat de aici.

recomand varianta zip, pentru arhitectura potrivită calculatorului tău cu mențiunea că în cazul în care nu știi ce înseamnă arhitectură, poți alege cu încredere varianta care are în numele fișierului w32. fișierul descărcat este o arhivă de tip zip și necesită dezarhivare. dacă utilizezi Microsoft Windows 10, poți folosi cu încredere utilitarul integrat în Windows Explorer. vei obține un director care conține octave.bat, fișierul pe care trebuie să dai dublu-clic pentru a porni aplicația.

instrument control:
GNU Octave poate comunica cu Arduino prin intermediul interfeței seriale, dar nu o poate face în starea inițială. la fel ca în cazul MatLab, e necesară o bibliotecă externă cu instrucțiuni care să-i permită citirea datelor seriale. cea pe care am utilizat-o în curs se numește instrument control și e disponibilă aici.

biblioteca se instalează ușor, descărcând fișierul și mutându-l în directorul src (de la source) al GNU Octave. în cazul în care directorul nu există, îl poți crea pur și simplu. urmează să pornești GNU Octave și cu ajutorul browserului de fișiere din stânga ferestrei principale, găsești și intrii în directorul src. ultimul pas constă în introducerea următoarei comenzi în consola GNU Octave:

>> pkg install instrument-control-0.3.0.tar.gz

instalarea instrument control durează destul de mult așa că poți să iei o pauză de câteva minute.

read_arduino.m:
pentru citirea interfeței seriale a Arduino am scris o mică bucățică de cod în formatul GNU Octave, read_arduino.m care îți pune la dispoziție o funcție cu ajutorul căreia poți citi datele într-o matrice. presupunem că îmi doresc să citesc tensiunea pe fiecare dintre cele 6 intrări analogice și să îi urmăresc evoluția în timp, la intervale de o secundă.

codul pentru Arduino este următorul:

void setup () {
  Serial.begin (9600); // <-- read_arduino.m suportă doar viteza de 9600 bauds
}

void loop () {
  byte port = 0; // <-- am nevoie de o variabila care specifica numarul portului analog
  while (port < 6) { // <-- cel mult voi citi date de la portul 5
    Serial.print (analogRead (port)); // <-- citesc si trimit valoarea citita catre portul serial
    Serial.print ("\t"); // <-- pentru a separa valorile, folosesc caracterul TAB (\t)
    port = port + 1; // <-- trec la portul urmator
  }
  Serial.print ("\n"); // <-- pentru a separa sirurile de date, folosesc caracterul NEW LINE (\n)
  delay (1000); // <-- aștept o secundă pentru a relua ciclul
}

care ar trebui să producă la fiecare secundă câte o linie cu valori cuprinse între 0 și 1023, corespunzătoare diferenței de potențial între pinul GND și pinii de la A0 la A5.

pentru a citi 10 rânduri conținând cele 6 valori, vei folosi:

>> A = read_arduino ('COM5', 10, 6)
A =

   339   339   326   319   312   324
   328   330   319   313   309   319
   327   329   319   314   310   319
   324   327   317   312   308   318
   323   326   316   311   307   317
   324   327   317   312   309   318
   322   324   314   310   306   315
   323   326   317   312   307   317
   324   327   317   312   308   318
   323   326   316   311   307   317

note de final:
COM5 este portul serial pe care se conectează plăcuța mea Arduino. în cazul tău, acesta va fi cel mai probabil diferit. verifică în Device Manager care este portul corect. read_arduino.m funcționează și pe Linux și pe MAC, însă în locul COM5 vei folosi denumirea portului corespunzătoare platformei. portul serial trebuie să fie liber, drept pentru care amintește-ți ca înainte să rulezi read_arduino să închizi monitorul serial al Arduino. ca în cazul MathWorks Matlab, fișierul read_arduino.m în directorul curent pentru ca funcția să devină accesibilă.

bogdan » arduino: senzor de lumină cu led

11:20 am on Mar 25, 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ă:

senzorii de lumină domină lumea simțurilor electronice. cu mici modificări, aceștia pot răspunde unei multitudini de întrebări, de la banalul ”e lumină afară?”, la ”ce culoare are un obiect?” sau ”la ce distanță am un obstacol în față?”. la prima vedere funcționează complex: un fragment de siliciu reacționează la lumina incidentă, modificând o mărime electrică. Einstein a luat premiul Nobel pentru explicarea principiului în 1921, deci trebuie să fie complicat. cu toate acestea, tehnologia s-a dezvoltat pe parcursul secolului care a trecut suficient de mult ca să putem reface experimentul în bucătărie.

materiale necesare:

  • un Ardunio, de orice fel, în funcție de cât de familiarizat ești cu el; la curs l-am folosit pe ăsta (31 de lei);
  • un led obișnuit, orice culoare și orice mărime, dar să fie un led simplu; știi că e un led simplu (30 de bani), dacă e foarte ieftin;
  • două fire pentru a lega ledul la Arduino; mie îmi plac astea (3 lei), dar pot fi de orice fel;

puțină teorie:
un led este un dispozitiv semiconductor, de obicei din siliciu, al cărui element activ este vizibil. elementul activ poartă numele de joncțiune, adică locul de întâlnire pentru două materiale cu proprietăți diferite. ce e important de reținut pentru construcția de față este că cele două materiale formează un sandviș cu un mic spațiu între ele, asemeni unui condensator de la fizică. în funcționarea normală, trecerea curentului electric prin acel mic spațiu produce lumină. cu siguranță ai observat că polaritatea e importantă, deoarece lumina și circulația curentului se produc doar într-un singur sens.

ce se întâmplă în schimb când polaritatea e inversată? lipsa curentului electric duce la acumularea de sarcini electrice pe fețele sandvișului, încărcând condensatorul. cum naturii îi plac simetriile, orice rază de lumină incidentă generează perechi de sarcini, care se vor deplasa în direcții opuse, datorită atracției electrostatice. ajunse pe suprafețele sandvișului, acestea vor scădea sarcina acumulată pe condensator, scăzând proporțional și tensiunea electrică.

în mod normal, procesele se întâmplă extrem de repede și sunt extrem de mici ca intensitate. dar aici intervine genialitatea oamenilor care au proiectat Arduino: acesta e suficient de rapid și suficient de sensibil pentru a face față experimentului.

ce se va întâmpla:
Arduino va încărca ledul, alimentându-l invers, după care va număra cât îi ia condesatorului format în jurul joncțiunii ledului pentru a se descărca. pentru a repeta experimentul de la curs, vei conecta ledul cu plusul (piciorușul mai lung, anodul) la GND (ground, 0V) și minusul (piciorușul mai scurt, catodul) la unul dintre terminalele Arduino, cu excepția pinilor 0,1 – care sunt responsabili pentru comunicarea serială și pinul 13, care are deja un led conectat intern și care te va încurca. eu am ales pinul 2.

în pregătirea Arduino, am definit un loc în memorie pentru stocarea informațiilor primite de la led. tipul de date folosit va fi întreg (int), numărând câte perioade de timp condensatorul a fost încărcat. pentru a fi accesibilă de oriunde, definiția se va afla în afara și înaintea celor două funcții speciale Arduino, setup și loop.

int value; // <- asa definesc un loc in memorie, int este tipul, iar value este numele
// pentru ca am definit-o în afara setup și loop, voi putea să o accesez de oriunde, prin nume
void setup() {}
void loop() {}

pentru citirea informațiilor, am folosit interfața serială a Arduino, care va fi inițializată prin:

int value;
void setup() {
  Serial.begin(9600); // <- aici initializez conexiunea seriala cu viteza de 9600 caractere / s
}
void loop() {}

periodic, am încărcat ledul pentru un interval de timp. experimental am ales 1ms.

int value;
void setup() {
  Serial.begin(9600);
}
void loop() {
  pinMode(2, OUTPUT); // <- aici definesc pinul 2 ca fiind pin de iesire
  digitalWrite(2, HIGH); // <- setez tensiunea pe pinul 2 la tensiunea de alimentare a Arduino
  delay(1); // <- astept 1ms, sa incarc condensatorul ledului
}

am măsurat în cât timp tensiunea la bornele ledului scade sub un anumit prag. aici aș fi putut să folosesc convertorul analog-digital din Arduino, dar în unele situații este prea lent. așa că am folosit proprietatea unui pin digital configurat ca intrare de a-și schimba starea în jurul jumătății tensiunii de alimentare a Arduino. astfel, dacă tensiunea la intrare scade sub 2.5V față de GND (pentru un Arduino alimentat la 5V), valoarea citită intern va fi LOW, în timp ce dacă tensiunea crește peste 2.5V, valoarea citită va fi HIGH.

pentru a determina în cât timp tensiunea la bornele ledului scade, am verificat la intervale scurte de timp dacă a scăzut. dacă a scăzut, trimit prin conexiunea serială valoarea înregistrată, altfel, mai aștept puțin timp și verific din nou. experimentând, am folosit ca timp de așteptare 40uS.

int value;
void setup() {
  Serial.begin(9600);
}
void loop() {
  pinMode(2, OUTPUT);
  digitalWrite(2, HIGH);
  delay(1);
  pinMode(2, INPUT);
  value = 0; // <- aici resetez ce stochez în memorie
  while(digitalRead(2) == HIGH) { // <- citesc pinul 2 si verific daca tensiunea depasește pragul
    delayMicroseconds(40); // <- daca e peste prag, lumina e prea slabă și aștept 40uS
    value = value + 1; // <- cresc cu o unitate valoarea stocată
  }
  // <- aici tensiunea la bornele ledului a scazut sub prag
  Serial.println(value); // <- așa că trimit valoarea prin conexiunea serială
}

în funcție de led și de condițiile de iluminare, valorile primite de calculator sunt uneori negative. acest lucru se întâmplă deoarece numărul de repetări depășește valoarea maximă care poate fi stocată într-un segment de memorie de tip întreg. pentru a preveni această situație, am introdus o limitare la 255 a numărului de cicluri pentru care verificarea are loc:

int value;
void setup() {
  Serial.begin(9600);
}
void loop() {
  pinMode(2, OUTPUT);
  digitalWrite(2, HIGH);
  delay(1);
  pinMode(2, INPUT);
  value = 0;
  while((digitalRead(2) == HIGH) && (value < 255)) { // <- verific în plus dacă valoarea stocată e sub 255
    delayMicroseconds(40);
    value = value + 1;
  }
  Serial.println(value);
}

note de final:
ledul este sensibil la același tip de lumină pe care o emite. folosind acest principiu, poți foarte ușor să-l transformi într-un senzor de culoare. de asemenea, un led are de obicei o lentilă care dirijează razele de lumină, limitându-i astfel câmpul vizual, la fel ca în cazul emisiei. folosind această informație, poți adapta foarte ușor un led pentru a măsura distanța.

bogdan » php string output speed test

11:54 pm on Feb 14, 2017 | #more | tags:

during the development of a new resource-sensitive project, i wondered what is the best PHP string output method and thus compared the following:

<?php
/*  A: */	printf ('some value %d != %d' . "\n", $c, $c+1); 
/*  B: */	vprintf ('some value %d != %d' . "\n", [ $c, $c+1 ]); 
/*  C: */	vprintf ('some value %d != %d%s', [ $c, $c+1, "\n" ]); 
/*  D: */	echo sprintf ('some value %d != %d' . "\n", $c, $c+1); 
/*  E: */	echo vsprintf ('some value %d != %d' . "\n", [ $c, $c+1 ]); 
/*  F: */	echo vsprintf ('some value %d != %d%s', [ $c, $c+1, "\n" ]);
/*  G: */	echo 'some value ' . $c . ' != ' . ($c+1) . "\n";
/*  H: */	echo "some value $c != " . ($c+1) . "\n";
/*  I: */	?>some value <?php echo $c; ?> != <?php echo $c+1; ?>
<?php ?>

each statement was run inside a for loop, for one million cycles, for a total number of five times. the results were as follows:

A B C D E F G H I
0.4260 0.5836 0.6412 0.4333 0.5896 0.6781 0.4449 0.4182 0.0361
0.4220 0.6012 0.6631 0.4302 0.6210 0.6805 0.4385 0.4100 0.0361
0.4205 0.6083 0.6674 0.4306 0.6245 0.6935 0.4379 0.4168 0.0378
0.4364 0.6098 0.6697 0.4456 0.6752 0.7455 0.4530 0.4134 0.0361
0.4299 0.5964 0.6594 0.4384 0.5793 0.6538 0.4485 0.4185 0.0365

on average, the best I is 11.37 times faster than the next, followed by H, A, D, G, B, E, C and F, which is worst, 18.90 times slower than I.

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.