ublo
bogdan's (micro)blog

bogdan » robot wi-fi

08:10 pm on Jan 12, 2020 | read the article | tags:

sărbătorile astea de iarnă au venit la pachet cu o retrospectivă a anului care a trecut, niște rezoluții de care probabil nu o să mă țin și puțin timp liber pe care să-l dedic pasiunilor. în rest, pentru că am neglijat blog-ul ăsta am zis că ar prinde bine să-ți arăt cum să construiești un robot wi-fi. mai exact unul destul de rezistent, ieftin, de preferat pornind de la un set, cu o baterie decentă, pe care-l poți extinde în viitor.

cât m-am zgârcit

bucata asta e ultima adăugată. inițial voiam să o las pentru finalul articolului. dar știu că și mie mi-ar plăcea să văd în primul rând cât m-ar costa proiectul ăsta și de ce am nevoie ca să-l fac. așa că, în lista de achiziții au intrat:

  • un set robot rotund, transparent, să se vadă prin el, 50 de lei (cumpără)
  • un mini-breadboard alb, culoare de cocalar, cum îmi place, 2,40 de lei (cumpără)
  • două seturi de fire mamă-tată de 15 cm, urmează și glumele de șantier, 8,90 de lei (cumpără)
  • un modul ESP8266 WeMos D1 Mini, pentru că m-am zgârcit, 30 de lei (cumpără)
  • o punte H, cu MX1515, pentru că m-am zgârcit mai mult, 3,50 de lei (cumpără)
  • o baretă cu 40 de pini, de care mai aveam, 1 leu (cumpără)
  • o sursă cu încărcător Li-Ion, să fac totul de la zero, 15 lei (cumpără)
  • un suport de acumulator 18650, pentru că mi-e frică de acumulatorii cu litiu, 4 lei (cumpără)
  • un acumulator cu Li-Ion 18650, că n-am chef să cumpăr baterii tot timpul, 38 de lei (cumpără)

pentru un total de 152,80 de lei. link-uri către produse găsești mai jos, cu tot cu alternative. câteva dintre componente poate le ai acasă, iar dacă înlocuiești încărcătorul și acumulatorul cu litiu cu un suport simplu cu 6 baterii AA, scazi 53 de lei, totalul fiind cu puțin sub 100 de lei. mai ai nevoie de un letcon, niște fludor, o șurubelniță în cruce, un calculator cu Arduino și ceva răbdare. hai că încep!

despre roboți la set

cu ce să pornesc? prefer roboții la set pentru că nu trebuie să te chinui cu printare 3D și tăieturi laser. ah! să nu mă înțelegi greșit: îmi place partea asta. doar că uneori durează atât de mult, încât și acum, după aproape doi ani, proiectul meu de aspirator robot e la stadiul de desene pe whiteboard. așa că, pornind de la recomandările pe care mi le face facebook pe baza profilului meu de marketing, am căutat cele mai viabile opțiuni pe saiturile de profil.

robotul pe care-l vreau trebuie să fie suficient de mic, să nu ocupe un spațiu prea mare în apartamentul și-așa înghesuit luat prin prima casă la marginea orașului, să fie ieftin, să conțină aproape toate componentele mecanice necesare, să fie simplu de construit, să se găsească peste tot (îmi pare rău, Pololu, deși Zumo e printre cei mai bine proiectați roboți la set, se găsește rar și e scump). variantele care au rămas nu sunt multe, ci două:

seturile sunt chinezești. motoarele nu sunt foarte puternice, dar își fac treaba minunat. roțile sunt mari, raportul de transmisie e bun și robotul care iese e suficient de rapid. iar ca bonus, motoarele se alimentează cu tensiuni între 3 și 6V, făcând rezonabil numărul de baterii pe care-l poți folosi. personal, m-am oprit la robotul rotund, așa că în continuare pozele și sugestiile sunt pentru el. dacă îți place mai mult mașina inteligentă, n-aș zice că e complicat să adaptezi ceea ce descriu în continuare.

setul conține aproape toate componentele: un suport de baterii complet nefolositor – de ce naiba ai pune un suport pentru două dintre cele mai slabe baterii dintre câte există, AAA?, două motoare, fiecare împreună cu șuruburi, suport de prindere, roată și aproape la fel de nefolositor, un encoder optic de poziție, pe care eu l-am montat ca să nu-l pierd, două plăci din acrilic împreună cu distanțiere pentru a construi corpul robotului și două roți care se rotesc libere, care funcționează ca puncte de sprijin, cu tot cu distanțierele necesare.

foarte puțină teorie: cu două motoare poți deplasa robotul în orice direcție. când cele două motoare se rotesc cu aceeași viteză în aceeași direcție, robotul merge înainte. cum apare o diferență de viteză, robotul va vira către motorul care se rotește mai repede. când motoarele se rotesc cu aceeași viteză în direcții opuse, robotul se va roti pe loc. logic, nu? însă pentru că două roți construiesc o bază de susținere unidimensională – cuvânt mare, dar nu închide pagina încă, e printre ultimele – mai ai nevoie de cel puțin un punct de sprijin în așa fel încât echilibrul să se transforme din precar în stabil. fizică de clasa – de fapt, am uitat. e ca atunci când vrei să așezi o carte în picioare. gata, trec peste.

mai ai nevoie de două capacitoare de 100nF sau p-acolo. valoarea capacității, oricât ar spune florin salam, nu e critică și nici dușmanii nu-ți vor purta pică. capacitoarele astea le vei conecta – un cuvânt ales ca să mă feresc de «a lipi», degeaba, pentru că nu scapi de el – în paralel cu terminalele de alimentare ale motoarelor. tot pentru motoare mai ai nevoie și de niște fire. eu am folosit fire colorate cu conectori dupont mamă – adică cu gaură, hehehe, glume de șantier – la unul dintre capete, cu lungimea de aproximativ 15cm și cu celălalt capăt tăiat și lipit la câte unul dintre terminalele motoarelor. ai nevoie de 4 fire. poți să cumperi fire mai lungi, pe care să le tai la jumătate – de altfel lucrul pe care l-am făcut și eu, mai mult de lene, pentru că aveam unele rămase de la un alt proiect.

după ce ai lipit capacitoarele și firele, e timpul să montezi motoarele pe placa din acrilic aia cu doar două fante perpendiculare pe restul. în fantele astea introduci în același sens, două dintre elementele din plastic în formă de «T» mai anemic, apoi prin celelalte două «T»-uri treci șuruburile lungi, pe care le treci prin motor și apoi prin «T»-ul introdus în placa de acrilic. sună complicat în text, așa că-ți las aici niște poze.

urmează distanțierele pentru roți – mă refer la cele micuțe, care sunt 8 – pe care le fixezi de aceeași parte a plăcii de acrilic cu motoarele prin piulițe. și distanțierele – da, alea mai lungi – pentru placa de sus, pe care le fixezi în partea opusă motoarelor prin piulițe. aici am făcut o prostie. cred că ar fi fost mai bine să pun distanțierele astea mai mari primele și apoi motoarele, dar până la urmă n-a fost așa de complicat să inserez piulița între spațiul dintre motor și placă. ba chiar mi s-a părut că fixează motorul mai bine. e după gust. și nu uita de placa de sus. doar că pentru asta mai bine mai aștepți puțin. e timp și mai sunt componente care vin în interior.

exod de creiere, wi-fi

aici e-aici. probabil ești curios de ce spun asta. ai atât de multe opțiuni, că aproape nu știu cum să încep. fie Raspberry Pi Zero W, care-mi place la nebunie: ai conexiune prin wi-fi, terminal să-l programezi direct în python, o mulțime de software cu tot felul de opțiuni de securitate. da, dacă aș face unul pentru armată, asta ar fi prima opțiune. dar e doar un robot wi-fi. iar Raspberry Pi Zero W consumă mult curent, cât unul dintre motoare la putere normală și e puțin mai complicat de integrat. așa că nu.

varianta doi e Arduino WiFi Rev.2. grozav! e un Arduino. la fel de ușor de programat și de versatil ca celelalte, dar cu wi-fi. foarte utili sunt terminalele care funcționează direct la 5V, lucru la care voi reveni ceva mai târziu, de altfel și singura variantă care are acest avantaj. dezavantajul? imens de scump. wtf? la 40€ e al naibii de scump, așa că next.

RedBearLab WiFi CC3200 în ambele variante e greu de programat. nu folosește Arduino, ci Energia, care e asemănător, dar știu că ultima dată m-am chinuit cu el destul de mult să-l fac să meargă. la preț, deși în varianta originală e cumva scump – oare se mai produce?, la Watterott e în promoția de jumătate din preț. dar cum e greu de programat, next.

și-am ajuns la fix ce nu voiam să ajung, dar care în ultimele luni m-a cucerit definitiv, ESP8266. ieftin, se programează cu Arduino fără niciun stres. doar instalezi suportul pentru ESP din BoardManager și gata. poți să folosești aproape orice bibliotecă software cu el cu care te-ai obișnuit de la Arduino. ce mai! soluția perfectă, cu rezerva că terminalele suportă doar 3,3V. maximum. cu toate astea, asta am ales. variantele care se găsesc la noi sunt:

îmi place mult 12F Witty, pentru că programatorul e detașabil. însă are mici particularități: un senzor și led-uri deja conectate la terminale și aș vrea puțină libertate. ca motiv pentru a-l folosi, nu trebuie să lipești nimic. eu am ales să lipesc. și am mers pe varianta D1 Mini WeMos. ieftin și bun cu 9 GPIO, TX, RX, o intrare analogică, acces facil la 3,3V și programator integrat. după ce am lipit terminalele, am luat un breadboard, am pus doi jumperi ca să multiplic conexiunile la GND și +5V – de fapt, aici sunt 5V doar atunci când se alimentează prin USB, în rest, terminalul funcționează ca alimentare și suportă maximum 24V ceea ce e perfect pentru că nu-mi trebuie o sursă stabilizată de 5V – și apoi peste jumperi, am pus plăcuța cu ESP8266. breadboard-ul l-am lipit pe placa superioară a robotului, ca în poze.

rulez puntea hash

oricât te-ai chinui, motoarele alea nu pot fi controlate direct cu ESP8266. de fapt, cu niciuna dintre variantele de mai sus. dacă încerci, în cel mai bun caz n-o să meargă. în cel mai rău caz, o să cauți pe net «i bricked my board. what can i do?» și răspunsul o să fie aproape întotdeauna: îți iei alta. motoarele electrice de felul ăsta, deși simple – sunt sigur că ai mai văzut și te-ai jucat măcar o dată cu unul pe care-l conectai în diferite feluri la o baterie. wait! cum adică nu ai făcut niciodată asta? well .. – folosind componente electronice nu se controlează atât de ușor. în primul rând pentru că prin componente electronice mă refer la tranzistori. și ăia sunt de fapt întrerupătoare. adică poți cel mult să întrerupi curentul pentru motor – adică să-l oprești – nu să-l faci să se rotească și invers. iar invers, chiar mi-aș dori să se poată roti.

problema are o soluție dacă gândești puțin lateral: dacă aș avea mai multe întrerupătoare – ca să nu te chinui prea mult – 4. acum pot? normal. înseriezi câte două între terminalele de alimentare și conexiunile dintre întrerupătoare le conectezi la motor. în mod evident, nu le conectezi când sunt închise, pentru că scurtcircuitezi alimentarea. și s-ar putea să nu fie foarte plăcut. dar dacă închizi unul dintre întrerupătoarele dinspre terminalul negativ al sursei de alimentare și din celălalt șir de întrerupătoare pe cel dinspre terminalul pozitiv, motorul se va roti într-un sens. când inversezi poziția tuturor întrerupătoarelor, motorul se va roti în sens invers. uhuu! configurația asta se numește punte H.

problema vitezei se rezolvă foarte simplu. pentru unul dintre întrerupătoare folosești un semnal PWM în așa fel încât tensiunea medie va fi proporțională cu raportul între timpul în care terminalul se află la 5V și perioada semnalului PWM. ca atunci când erai mic și aprindeai și stingeai foarte repede becul din cameră, asta dacă apucai să vezi ceva și nu se ardea sau îți luai bătaie de la părinți – dap, nu sunt de acord cu violența fizică, doar că milenial fiind, am crescut în comunism și becurile se ardeau repede, că erau proaste.

nu-ți zic să construiești de la zero un astfel de circuit. departe de mine gândul. iese mult mai scump decât dacă l-ai cumpăra gata făcut și e și destul de complex. sunt multe variante. dar cum sunt ghidat de un buget minim – e frig, gazele s-au scumpit, curentul la fel, inflația a crescut, pe vremea mea .. – variantele care-mi rămân sunt:

îmi place mult DRV8835. se găsește și în Zumo. e integrat cu multe opțiuni de Pololu. curentul maxim suportat e cel mai mic dintre cele trei, însă are tot felul de protecții și se poate controla extrem de simplu cu ESP8266. mai ales că are alimentare separată pentru partea logică și nivelul logic 1 al ESP8266 va fi mai mult decât suficient pentru a obține maximum de performanțe. dar nu l-am ales. e prea scump.

următorul pe listă e L298N. l-am folosit în tot felul de configurații. îmi place la nebunie că îl găsești sub formă de modul din ăla roșu cu tot cu radiator la un preț mai mic decât circuitul separat. dintre toate trei, suportă tensiuni pentru motoare de până la 46V – cine naiba îl folosește cu motoare din astea? mă rog. ce e puțin aiurea e că nivelul logic pentru 1 este stabilit din fabrică la tipic 2,3V, iar cu o variație de 10%, normală de altfel, e posibil că ESP8266 pentru care 1 logic este în jur de 2,7V să nu deschidă suficient logica circuitului. pe lângă asta, fiind conceput pentru versatilitate – în special pentru motoare pas-cu-pas, de unde și tensiunea mare de alimentare – sunt necesari fie pini în plus față de celelalte două variante – 6 în loc de 4 – sau componente externe. să nu mai vorbim de căderea de tensiune de minimum 3,2V pe tranzistoarele de ieșire, care mă obligă să alimentez motoarele cu cel puțin 8,2V pentru a obține pe motor 5V. deci nu.

ultimul – special l-am lăsat la urmă – este un circuit chinezesc – ESP8266 are aceleași origini – conceput pentru jucării. conține tot ce e mai bun dintre cele două de mai sus: logică compatibilă cu 3,3V – 1 logic e de la 2V în sus, etaj de ieșire MOS-FET fără căderi semnificative de tensiune și curent generos. totul cu singura problemă legată de tensiunea maximă de alimentare care e cu 1V mai mică decât în cazul DRV8835, adică 10V în loc de 11V. the horror! not! când includ și prețul infim la care l-am găsit, e circuitul perfect.

față de plăcuța cumpărată am mai avut nevoie de 4 perechi de câte două terminale și două terminale separate – 10 din totalul de 40 cât are o baretă standard, pe care le-am lipit pe plăcuță. plăcuța am fixat-o cu banda dublu-adezivă rămasă de la un dulap pe placa de cu motoarele a robotului, ca în poze. ce-am făcut greșit prima dată a fost că am lipit plăcuța prea aproape de centru, firele incomodând plasarea bateriei. imediat și despre ea.

firele de la motoare le-am introdus peste terminalele «motor A» și respectiv «motor B», cele 4 intrări IN1, IN2, IN3 și IN4 la respectiv D1, D2, D3 și D4 din ESP8266, folosind fire dupont mamă-tată – fără glume de șantier, da? – de 15cm. + și – de pe plăcuță le-am conectat la GND și 5V pe plăcuța ESP8266. cum? ca în poze, bineînțeles.

robotul capătă formă. singurul lucru care lipsește pe partea hardware este sursa de alimentare.

vin cu bateria

din experiență, o bateria trebuie să fie bună. adică măcar să scoată – preferai furnizeze? – măcar cu 50% peste tensiunea de alimentare a creierului. în cazul de față, 5V ar fi minimum. să-ți explic de ce: atunci când motoarele funcționează, acestea consumă destul de mult curent încât să conteze rezistența internă a bateriei și în felul ăsta tensiunea la bornele ei va scădea suficient cât să producă neplăceri de genul resetarea robotului. de-asta n-o să-ți recomand niciodată să alimentezi un robot bazat pe Arduino la 6V. am văzut-o întâmplându-se. simt cum vrei să-mi spui că da, dar Zumo se alimentează la 6V, la el cum de merge? păi merge pentru că are în interior un convertor care indiferent de tensiunea bateriei, produce 7,5V, care e de fapt tensiunea de alimentare a motoarelor și a întregului ansamblu.

nu e exclus să poți să folosești suportul pentru cele două baterii AAA. doar că pentru ca robotul să meargă cât de cât, convertorul folosit trebuie să genereze măcar 5V/1A. adică 5W. cu un randament de 90% – adică bun, în practică e de obicei mai mic, din baterie va consuma 5,6W pe unitatea de timp. bateriile AAA au capacitatea de 1,9Wh și sunt două, deci 3,8Wh, făcând robotul să gâfâie după 20 de minute. dacă vrei să mergi pe varianta asta, îți recomand măcar un suport cu 4 baterii AA, care fiecare înmagazinează 4,2Wh de energie chimică, adică un generos 16,8Wh. împreună cu suportul de baterii – deși poți să-l folosești bine-mersi așa cum e – îți recomand și o sursă coborâtoare/ridicătoare de tensiune de la Pololu, S9V11F5 – 30 de lei, optimus digital, 30 de lei, robofun – ieftină și bună, pe care am folosit-o cu succes pentru a adăuga Raspberry Pi Zero W peste Zumo.

varianta cea mai ieftină e să folosești un suport pentru 6 baterii AA. ai la dispoziție astfel 9V cu 25,2Wh la dispoziție. toate componentele sunt compatibile cu tensiunea asta și poți folosi în suport orice tip de baterii, normale, de 1,5V, producând 9V când sunt noi, baterii alcaline, de 1.5V, care produc aprox. 9,6V când sunt noi, sau acumulatori NiMH care produc 8,4V când sunt noi. deci, suport de 6 plus baterii egal love și costuri mici. mai ai nevoie de două fire de 15cm care de data asta au la un capăt conectori dupont tată, celelalte capete conectându-le la terminalele suportului. doar inserezi alimentarea în breadboard, plasezi suportul în interiorul robotului și-i pui capacul și gata.

varianta pe care am mers eu a fost să folosesc un modul cu care poți să-ți construiești propria baterie externă de 5V. cei 2A de la ieșire sunt mai mult decât suficienți pentru orice alte accesorii aș vrea să adaug pe robot. se încarcă prin USB și am control asupra bateriei cu litiu din interior. am mai folosit un suport de baterie cu Li-Ion, în format 18650 împreună cu o baterie Sony de 2600mAh, adică aproximativ 9,6Wh. fără a conecta bateria, am lipit terminalele suportului la terminalele B+ și B- ale modulului. tot fără a conecta bateria – nu știu dacă știi, dar bateriile cu litiu sunt destul de periculoase, așa că mai bine safe than sorry; oricum, un extinctor cu pulbere nu strică să-ți fie la îndemână – am lipit două terminale pe placa modulului, pe contactele GND și OUT. am folosit mult fludor și un clește mic. între terminalele astea și GND respetiv 5V ale ESP8266 am folosit două fire dupont mamă-tată de 15cm.

pentru a prinde modulul de încărcare pe placa superioară am folosit două distanțiere de plastic, M3, cu piuliță și șurub. deși puteam să folosesc atât șoricei – zip-ties – sau bandă dublu-adezivă, mi-a plăcut mai mult varianta asta, mai ales că trebuie să încarc bateria prin micro-USB. modulul de încărcare are 4 leduri care indică cât de încărcată e bateria și un mic buton pe care-l pot acționa printr-un șoricel. ah. da. și mai are un led aiurea, pe care l-am scurtat. în timpul functionării, led-ul se aprinde și e alb. cred – deși n-am încercat – poți să-l înlocuiești cu orice led de 5mm în culoarea preferată.

zi de soft

cum spuneam înainte, îmi place ESP8266. are fix aceleași funcții ca Arduino, cu niște particularități. prima dintre ele e că include o conexiune wi-fi. ca o placă wireless dintr-un laptop. care pe deasupra e foarte ușor de folosit – incarci două biblioteci, definești rețeaua și parola pentru conexiunea wi-fi, inițializezi conexiunea și gata. ca în continuare:

#include <ESP8266WiFi.h> /** biblioteca prin care ESP8266 acceseaza conexiunea wi-fi */
#include <WiFiClient.h> /** biblioteca prin care ESP8266 folosește wi-fi și își ia adresa de IP */

#ifndef LOCAL_SSID /** protecție să nu definesc rețeaua de două ori */
#define LOCAL_SSID "nume-retea-wireless" /** numele rețelei, așa cum apare el în telefon sau pe calculator */
#define LOCAL_PASS  "parola-de-la-wireless" /** parola de la wireless */
#endif

const char* ssid     = LOCAL_SSID; /** pun în constante de tip char* valorile definite mai sus */
const char* password = LOCAL_PASS;

void setup() { /** functia de setup, specifica Arduino, ruleaza după reset sau la pornire */
  Serial.begin(115200); /** initializez conexiunea serială, să văd ce se întâmplă, viteza 115200 bauds */
  WiFi.begin(ssid, password); /** încerc să mă conectez la wireless */
  Serial.println(""); /** trimit o linie nouă către terminalul serial */
  
  while (WiFi.status() != WL_CONNECTED) { /** atât timp cât încă nu m-am conectat la wireless */
    delay(500); /** aștept jumătate de secundă */
    Serial.print("#"); /** și trimit un # către terminalul serial, după care mai încerc o dată */
  }
  
  Serial.println(""); /** aici înseamnă că m-am conectat, așa că trec la linia următoare în terminal */
  Serial.println(WiFi.localIP()); /** și trimit adresa de IP ca să știu unde mă conectez cu calculatorul */
}

void loop() { /** deocamdată nu pun nimic în loop, vreau doar să văd că se conectează */
}

știu că ți-e lene să scrii, așa că ai aici fișierul întreg: 8266-robot-round-wifi-v1.ino

în mediul Arduino, alegi tipul plăcii cu ESP8266, alegi portul serial care apare atunci când conectezi ESP8266 prin cablu USB și apeși upload. dacă totul merge bine și se uploadează conținutul programului, ar trebui ca în monitorul serial să obții adresa de IP. totul funcționează corect dacă poți să scrii în terminalul de pe calculator «ping IP», unde IP e adresa primită prin serial și să obții reply.

dacă da, hai mai departe. ping de înainte doar îmi arată că ESP8266 s-a conectat la net. mi-ar plăcea în schimb să-i pot trimite comenzi. aici am mai multe variante, dar cea mai simplă se bazează pe un browser, ca ăla prin care accesezi pornhub. cum, tu nu? revenind, mi-ar plăcea să pot să scriu în browser adresa de IP și să îmi apară ceva în care să trimit comenzi către robot.

ESP8266 are o bibliotecă excelentă pentru asta prin care implementează un mic server web. adică, fix chestia care ajută pornhub să poți să le accesezi paginile web. și e un mic server web chiar rapid. dar despre asta într-o altă postare. ca orice server web, va trebui să-i spun cum să interpreteze diferitele căi care vin după adresă. spre exemplu, dacă te uiți în bara de adrese acum ce vine după ublo.ro, începând cu primul slash /. e suficient să îi spun ce să facă atunci când după adresă e doar un slash – pagina asta se cheamă index și e prima pagină a unui sait, pe care o vezi când scrii direct adresa – și ce să facă atunci când nu găsește pagina. pentru asta adaug următoarele lucruri,

imediat după bibliotecile deja incluse, adică sus de tot:

#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h> /** asta e biblioteca nouă, restul le-am pus pentru context */

fix înainte de function setup, definesc obiectul care va reprezenta serverul – îl voi numi creativ, server – și două funcții, una care să se ocupe de pagina index a noului meu server web și una care să se ocupe de paginile care nu există:

ESP8266WebServer server(80); /** aici definesc serverul web, folosind portul 80, adică normal */

void handle_root() { /** functia asta spune ce se intampla cand accesez adresa direct in browser */
  server.send(200, "text/html", "heei, asta e prima pagina!"); /** pur si simplu afisez in browser mesajul */
  /** ce mi se pare interesant, e ca in loc de mesaj poti sa pui codul HTML al oricărei pagini */
  /** primul parametru al server.send, 200, este statusul răspunsului HTML; 200 = OK */
  /** al doilea parametru, "text/html", este tipul raspunsului, în cazul ăsta o pagină web */
  /** al treilea parametru, "heei, asta e prima pagina!", este conținutul răspunsului */
}

void handle_404() { /** functia asta spune ce se intampla cand accesez o pagină care nu există */
  server.send(404, "application/json", "{\"error\":1,\"message\":\"not found\"}");
  /** trimit către browser un mesaj de eroare: */
  /** primul parametru, statusul răspunsului, înseamnă 404 = Page Not Found */
  /** al doilea parametru spune că întorc un obiect de tip json, cu error=1 și message=not found */
  /** al treilea parametru conține obiectul json */
}

void setup() { /** pus aici pentru context */

și în interiorul funcției setup, chiar înainte de acolada de final, leg funcțiile pentru pagini de server și pornesc serverul:

void setup() { /** pus aici pentru context */
  Serial.begin(115200);
  WiFi.begin(ssid, password);
  Serial.println("");
  
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print("#");
  }
  
  Serial.println("");
  Serial.println(WiFi.localIP());
  /** urmează bucata nouă: */
  server.on("/", handle_root); /** conectez funcția pentru index la serverul web */
  server.onNotFound(handle_404); /** conectez funcția pentru pagini care nu există la serverul web */
  server.begin(); /** pornesc serverul web */
  Serial.println("S"); /** trimit S către terminal ca să știu că a pornit și serverul web */
}

doar că spre deoserbire de programul anterior, de data asta mai trebuie să îi spun în loop să aștepte conexiuni de la browsere:

void loop() { /** pus aici pentru context, urmează instrucțiunile noi */
  server.handleClient(); /** asteapta conexiuni de la browsere și dacă sunt, interpretează-le */
  /** ca limitare, serverul web poate interpreta câte o conexiune pe rând */
}

da, n-am uitat. fișierul versiunii de acum e aici: 8266-robot-round-wifi-v2.ino

după ce încarci programul vei putea să accesezi robotul în browser, prin adresa de IP care apare în monitorul serial. dacă totul e ok vei primi mesajul pe care l-ai scris în funcția handle_root. dacă încerci să adaugi ceva la adresă, un slash ceva – adică chiar să scrii /ceva – ar trebui să-ți apară obiectul JSON definit.

cred că din momentul ăsta te-ai prins ce trebuie să faci. serios? nu? păi trebuie să definești diferite adrese prin care să trimiți comenzi la motoare. hai să mă ocup de motoare în primul rând. ca să controlez motoarele, voi defini două variabile globale care vor ține minte viteza motorului pe 8 biți: dacă primul bit e 1, atunci motorul merge înainte, dacă primul bit e 0, atunci motorul merge înapoi. următorii 7 biți reprezintă viteza cu care se deplasează. dacă toți sunt 0 – în hexa înseamnă 0x00 – atunci viteza e 0 și motorul stă pe loc, dacă toți sunt 1 – în hexa înseamnă 0x7f – atunci viteza e maximă. așa că imediat după definirea LOCAL_SSID, adaug definiții pentru terminalele la care este conectată puntea hash:

#ifndef LOCAL_SSID /** pus aici pentru context */
#define LOCAL_SSID "nume-retea-wireless"
#define LOCAL_PASS  "parola-de-la-wireless"
#endif
/** urmează definițiile noi: */
#ifndef MOTOR_PINS /** protecție, ca să nu definesc terminalele de două ori */
#define MOTOR_PINS /** definit doar ca să verific dacă nu l-am definit din nou */
#define MOTOR_A_IN1 D1 /** IN1 al punții H e conectat la D1 al ESP8266 */
#define MOTOR_A_IN2 D2 /** IN2 al punții H e conectat la D2 al ESP8266 */
#define MOTOR_B_IN1 D3 /** IN3 al punții H e conectat la D3 al ESP8266 */
#define MOTOR_B_IN2 D4 /** IN4 al punții H e conectat la D4 al ESP8266 */
#endif

după atribuirea constantelor cu numele rețelei și parola, adaug variabilele globale pentru motor. pe lângă cele două viteze, mai definesc o variabilă, de data asta booleană, care îmi spune dacă vitezele s-au modificat sau nu. ca să optimizez puțin timpii de conexiune.

const char* ssid     = LOCAL_SSID; /** pus aici pentru context */
const char* password = LOCAL_PASS;
/** urmează variabilele noi: */
uint8_t motor_A = 0x80; /** viteza motorului A, înainte cu viteză 0 */
uint8_t motor_B = 0x80; /** viteza motorului B, înainte cu viteză 0 */

bool motor_change = false; /** dacă viteza motorului s-a modificat */

imediat sub definitia lui handle_404, introduc o functie care imi spune cum controlez un motor. am grijă că toate terminalele ESP8266 pot fi programate cu PWM, însă spre deosebire de Arduino, plaja de valori este între 0 și 1023 – 10 biți – spre deoserbire de 0 și 255 – 8 biți.

void handle_404() { /** pus aici pentru context */
  server.send(404, "application/json", "{\"error\":1,\"message\":\"not found\"}");
}
/** urmează definiția funcției pentru controlul motorului */
/** parametrii funcției sunt:
 * motor - întreg pe 8 biți, între 0 și 255 = reprezintă viteza motorului
 * in1 - întreg pe 8 biți = terminalul ESP8266 unde e conectat IN1 (IN3, dacă motorul = B)
 * in2 - întreg pe 8 biți = terminalul ESP8266 unde e conectat IN2 (IN4, dacă motorul = B)
 */
void run_motor (uint8_t motor, uint8_t in1, uint8_t in2) {
  /** cu motor & 0x80 extrag doar primul bit; iar rezultatul va fi 0x80, dacă e 1 sau 0x00 dacă e 0 */ 
  if ((motor & 0x80) == 0x80) { /** deci, dacă primul bit e 1, motorul merge înainte */
    /** din foaia de catalog a MX1515, ca motorul să meargă înainte, */
    /** prima intrare trebuie să fie PWM, iar a două LOW */
    /** 0x7f & motor extrage viteza, iar rezultatul va fi între 0x00 și 0x7f */
    /** cum asta înseamnă un număr între 0 și 127, trebuie să-l înmulțesc cu 8 ca să obțin între 0 și 1023 */
    analogWrite (in1, (0x7f & motor) << 3); /** pe in1 pun semnal PWM */
    digitalWrite (in2, LOW); /** pe in2 scriu LOW */
  }
  else { /** dacă nu merge înainte, motorul merge invers */
    /** rationamentul e asemanator, doar că se inversează in1 și in2 */
    /** altă optimizare e să inversez viteza: când rezultatul 0x7f & motor = 0x7f, viteza să fie 0 */
    /** asta deoarece de la 0 la 127 viteza va scădea */
    /** apoi de la 128 la 255, motorul își schimbă sensul și accelerează */
    digitalWrite (in1, LOW); /** pe in1 de data asta scriu LOW */
    analogWrite (in2, (0x7f - (0x7f & motor)) << 3); /** iar pe in2 pun semnal PWM, cu viteza inversata */
  }
}

imediat după funcția definită anterior, voi defini o funcție care stabilește viteza pentru un anumit motor. funcția va primi doi parametrii: viteza motorului care mă interesează și un parametru de tip String care poate fi "up" sau "dw" pentru a accelera sau decelera motorul, sau direct un număr între 0 și 255.

/** funcția pentru stabilirea vitezei unuia dintre motoare */
/** parametrii funcției sunt:
 * motor - referință către variabila corespunzătoare vitezei motorului
 *       - referință înseamnă că variabila va fi modificată de funcție,
 *       - și noua valoare va fi accesibilă în exteriorul funcției
 * value - String, reprezintă valoarea primită prin intermediul conexiunii wi-fi
 *       - value poate fi dw - și atunci viteza scade cu 8 unități din 255
 *       - value poate fi up - și atunci viteza crește cu 8 unități din 255
 *       - value poate fi un număr între 0 și 255
 */ 
void set_motor (uint8_t &motor, String value) {
  long conversion = 0; /** String::toInt() intoarce o valoare de tip long, eu am nevoie de uint8_t */
  motor_change = true; /** daca am apelat funcția asta, automat viteza motorului s-a modificat */
  
  if (value == String ("up")) { /** dacă valoarea e "up" */
    conversion = motor + 0x08; /** cresc viteza motorului cu 8 */
    if (conversion > 0xFF) { /** dacă ce obțin e mai mare de 255 */
      conversion = 0xFF; /** fac viteza 255 */
      /** aici am folosit o smecherie: dacă conversion era uint8_t, atunci când adunam */
      /** și treceam peste 255 se întorcea din nou de la 0 și n-aș fi știu ce să fac */
      /** dar cum conversion e de tip long, pot să depășesc 255 fără probleme */
    }
    motor = conversion; /** știu sigur că conversion e cel mult 255, așa că stabilesc viteza */
    return; /** nu merg mai departe */
  }
  if (value == String ("dw")) { /** dacă valoarea e "dw" */
    conversion = motor - 0x08; /** scad viteza motorului cu 8 */
    if (conversion < 0x00) { /** dacă ce obțin e mai mic ca zero */
      conversion = 0x00; /** fac viteza 0 */
      /** din nou șmecheria: conversion e long și poate să fie și negativ */
    }
    motor = conversion; /** știu sigur că conversion e cel puțin 0, așa că stabilesc viteza */
    return; /** nu merg mai departe */
  }
  conversion = value.toInt(); /** convertesc șirul de caractere la întreg */
  /** dacă conversia eșuează, conversion = 0 */
  if (conversion < 1 || conversion > 0xFF) { /** dacă conversion e mai mic de 1 sau mai mare de 255 */
    conversion = 0x80; /** opresc motorul */
  }
  motor = conversion; /** altfel, motorul va avea viteza stabilită */
}

va trebui în setup să specific cum sunt folosite terminalele pentru motoare, stabilindu-le pe toate ca ieșiri OUTPUT:

void setup() { /** pus aici pentru context */
  /** urmează inițializarea conexiunii cu puntea H */
  pinMode (MOTOR_A_IN1, OUTPUT); /** stabilesc terminalul conectat la IN1 ca ieșire */
  pinMode (MOTOR_A_IN2, OUTPUT); /** stabilesc terminalul conectat la IN2 ca ieșire */
  pinMode (MOTOR_B_IN1, OUTPUT); /** stabilesc terminalul conectat la IN3 ca ieșire */
  pinMode (MOTOR_B_IN2, OUTPUT); /** stabilesc terminalul conectat la IN4 ca ieșire */
  
  Serial.begin(115200); /** pus aici pentru context */
  WiFi.begin(ssid, password);
  Serial.println("");
  
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print("#");
  }
  
  Serial.println("");
  Serial.println(WiFi.localIP());
  server.on("/", handle_root);
  server.onNotFound(handle_404);
  server.begin();
  Serial.println("S");
}

tot în iterația asta de program voi modifica și funcția loop ca să pot controla motorul. am grijă să setez motorul doar dacă funcția set_motor a fost chemată:

void loop() { /** pus aici pentru context */
  server.handleClient();
  /** urmează bucata nouă de cod care se ocupă de controlul motoarelor */
  if (!motor_change) { /** dacă motor_change e false */
    return; /** întoarce-te la început */
  }
  motor_change = false; /** aici motor_change e true, așa că îl resetez */
  run_motor (motor_A, MOTOR_A_IN1, MOTOR_A_IN2); /** stabilesc viteza pentru motorul A */
  run_motor (motor_B, MOTOR_B_IN1, MOTOR_B_IN2); /** stabilesc viteza pentru motorul B */
}

și să nu uit de fișier. îl poți descărca aici: 8266-robot-round-wifi-v3.ino

ultimul pas în ceea ce privește software-ul e să primesc comenzi prin browser. până acum n-am făcut nimic în sensul ăsta. cum spuneam, poți să definești acum câte un «handle» pentru fiecare motor, dar n-aș vrea să fac asta. așa că voi crea o pagină căreia pot să-i trimit mai mulți parametrii. parametrii îi voi trimite folosind HTTP POST, același protocol care-l folosește un browser atunci când scrii ceva în google și apeși enter. dacă la google parametrul trimis prin HTTP POST se numește q - poți să încerci copiind în browser google.com/?q=ceva - în cazul robotului voi avea doi parametrii, motor_a și motor_b. pagina către care se fac cererile va fi /rpc/ de la «remote procedure call» și va răspunde cu un JSON care conține cele două viteze noi. imediat după funcția set_motor vei introduce:

void handle_404() { /** pus aici pentru context */
  server.send(404, "application/json", "{\"error\":1,\"message\":\"not found\"}");
}
/** funcția nouă care se ocupă de interpretarea comenzilor */
void handle_form () {
  if (server.method() != HTTP_POST) { /** verific ca protocolul sa fie HTTP POST */
    server.send(405, "application/json", "{\"error\":1,\"message\":\"method not allowed\"}");
    /** daca nu, trimit un mesaj de eroare cu statusul 405 = Method Not Allowed */
    return;
  }
  for (uint8_t c = 0; c < server.args(); c++) { /** trec printre toti parametrii, n-am cum altfel */
    if (server.argName(c) == String("motor_a")) { /** daca numele parametrului e motor_a */
      set_motor(motor_A, server.arg(c)); /** apelez set_motor pentru motor_A cu valoarea parametrului */
    }
    if (server.argName(c) == String("motor_b")) { /** daca numele parametrului e motor_b */
      set_motor(motor_B, server.arg(c)); /** apelez set_motor pentru motor_B cu valoarea parametrului */
    }
  }
  /** pur si simplu trimit un raspuns cu statusul 200 = OK */
  /** si care contine noile viteze */
  server.send (200, "application/json",
    "{\"motor_A\":" + String(motor_A) + ",\"motor_B\":" + String(motor_B) + "}");
  /** la fel, trimit si prin interfata seriala cele doua viteze noi */
  Serial.print("A");
  Serial.println(motor_A);
  Serial.print("B");
  Serial.println(motor_B);
}

a mai rămas un singur lucru de făcut. să adaugi în funcția setup conexiunea înte server și noua funcție handle_form:

void setup() { /** pus aici pentru context */
  /** dap, și asta exista până acum. va trebui să mai sari câteva rânduri */
  pinMode (MOTOR_A_IN1, OUTPUT);
  pinMode (MOTOR_A_IN2, OUTPUT);
  pinMode (MOTOR_B_IN1, OUTPUT);
  pinMode (MOTOR_B_IN2, OUTPUT);
  
  Serial.begin(115200);
  WiFi.begin(ssid, password);
  Serial.println("");
  
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print("#");
  }
  
  Serial.println("");
  Serial.println(WiFi.localIP());
  server.on("/", handle_root);
  /** așa, aici e bucata nouă de cod care inițializează interpretarea comenzilor */
  server.on("/rpc/", handle_form); /** aici am adaugat conexiunea intre handle_form si server */
  /** restul e la fel, pus aici pentru context */
  server.onNotFound(handle_404);
  server.begin();
  Serial.println("S");
}

ca și până acum, poți descărca fișierul de aici: 8266-robot-round-wifi-v4.ino

după ce încarci programul în robot poți folosi Postman pentru a trimite cereri către robotul tău alegând la tipul conexiunii HTTP POST, la adresă, http://adresa-de-ip-a-robotului-tău/rpc/, iar în corpul - adică body - cererii să treci parametrii motor_a și motor_b cu ce valori vrei. ai aici un mic ghid despre cum se folosește Postman.

ai putea să te oprești aici. prin Postman poți să trimiți comenzi robotului - să nu uit! s-ar putea ca motoarele să nu se rotească în aceeași direcție când le stabilești pe ambele să meargă înainte. nu-i nimic. poți să inversezi firele care leagă motorul de puntea H și problema se rezolvă. doar că parcă nici mie nu-mi place să apăs tot timpul butoane pe tastatură.

control și telefon

după articolul ăsta imens, dacă te întreb «mai ții minte când ziceam?» sunt șanse mici să răspunzi sincer cu «da!». așa că revin. în handler_root poți să incluzi o pagină web care să se încarce atunci când accesezi dintr-un browser adresa robotului tău. așa că am facut o mică pagină în HTML5 pe care am inclus-o în handler_root și care atunci când o încarci nu afișează nimic. suspans creat degeaba, nu? nu chiar. pentru că atunci când faci gesturi cu degetul peste pagina respectivă - swipe în sus, în jos, la stânga, la dreapta sau în diagonală, robotul va merge în direcția indicată de degetul tău și cu viteza dată de cât de lung ai făcut swipe-ul.

am folosit pentru asta biblioteca jquery.finger a lui Nicolas Gryman care îmi permite să citesc cu ușurință gesturile pe care le faci peste o pagină web.

codul e mai jos și dacă-ți place să te joci cu el, îl am și în versiunea pentru download, aici: 8266-robot-html-interface.html

<script>
var motor = { /** definesc un obiect care va controla motorul prin API */
    u:'/rpc/', /** pagina la care e disponibil API-ul */
    a:128, /** = 0x80, viteza implicită a motorului A */
    b:128, /** = 0x80, viteza implicită a motorului B */
    busy:0, /** îmi indică dacă cererea către robot nu s-a încheiat */
    body:$('body'), /** corpul paginii web */
    move:function(){ /** metodă prin care fac robotul să meargă */
    if(this.busy)return; /** dacă robotul e busy, ies */
    this.body.css({'backgroundColor':'blue'}); /** fac fundalul paginii albastru */
    this.busy=1; /** stabilesc că robotul este busy */
    $.ajax({ /** deschid o conexiune către robot */
        type:'POST', /** conexiunea e de tip POST */
        url:this.u, /** adresa e cea definită mai devreme */
        data:{'motor_a':this.a,'motor_b':this.b}, /** aici sunt parametrii pe care îi trimit */
        context:this, /** contextul îmi spune care e obiectul "this" din success și error de mai jos */
        success:function(r){ /** în cazul în care cererea se termină cu succes */
            this.body.css({'backgroundColor':'transparent'}); /** fac fundalul paginii transparent */
            this.busy=0; /** și stabilesc că robotul nu mai e busy */
        },
        error:function(){ /** în cazul unei erori, fac același lucru ca mai devreme. de lene */
            this.body.css({'backgroundColor':'transparent'});
            this.busy=0;
        }
    });
    },
    stop:function(){this.a=this.b=128;this.move()} /** metoda asta oprește robotul. face ambele viteze 0 */
};
var w=$(window); /** o variabilă în care țin fereastra browserului */
/** dacă îmi târăsc degetul, adică fac swipe */
w.on('drag',motor,function(ev){
    /** calculez procentual cât din ecran am făcut swipe pe orizontală = x și pe verticală = y */
    var x=2*ev.dx/w.width(),y=-2*ev.dy/w.height(),
    /** și calculez unghiul față de orizontală */
    t=Math.atan2(y,x),
    /** convertesc coordonatele x și y în coordonate polare, pe care apoi le convertesc în viteze */
    a=Math.sign(y)*Math.sqrt(x*x+y*y)*(1-Math.cos(t)),
    b=Math.sign(y)*Math.sqrt(x*x+y*y)*(1+Math.cos(t));
    /** vitezele obținute sunt între -1 și 1, și vreau să le convertesc între 0 și 255 */
    motor.a=Math.floor(128+127*(a>1?1:(a<-1?-1:a)));
    motor.b=Math.floor(128+127*(b>1?1:(b<-1?-1:b)));
    /** după ce am noile viteze, mișc robotul în direcția pe care o vreau */
    motor.move();
});
/** iar dacă apăs de două ori repede pe ecran - double tap - robotul se oprește */
w.on('doubletap',motor,function(ev){ev.data.stop()});
</script>

bineînțeles, nu am cum să nu închei cu programul întreg pe care-l poți încărca pe robot, cu tot cu interfața asta HTML5: 8266-robot-round-wifi-final.ino.

spor la construit!

referințe:

  1. detalii despre programarea ESP8266 cu Arduino
  2. wikipedia, despre puntea H
  3. foaie de catalog DRV8835
  4. foaie de catalog L298N
  5. foaie de catalog MX1515
  6. energizer watt-hour battery specs

bogdan » de ce nu faci mai multă cercetare?

08:36 am on Nov 4, 2019 | read the article | tags:

recent, asta e întrebarea pe care o aud destul de des. abilitățile mele în domeniu se încadrează în domeniul “physical computing”: ştiu binişor cum funcționează diferite sisteme fizice încât să le conectez la un calculator şi apoi să procesez datele şi să obțin ceva util. am şi 3 brevete în domeniul ăsta. adica “i did my share”. doar că m-am plictisit. nu de satisfacția unei descoperiri – departe de mine gândul, până la urmă asta e pasiunea mea, ci de cum cercetarea “organizată” vede asta.

prin cercetare organizată înțeleg contractarea unui grup de cercetători de către o instituție, publică sau privată să rezolve o problemă. e nevoie de un grup pentru că nimeni nu are cunoştințe care să acopere tot. la nivel superficial, e bine să ai noțiuni despre părțile întregului proiect, însă atunci când ajungi la detalii, e nevoie de o anume experiență care nu poți să o ai decât dacă ai aprofundat un domeniu. şi nu poți fizic să aprofundezi toate domeniile.

prima problemă pe care o am e cu noțiunea asta de grup: pe scurt, nu toți sunt la fel de competenți. în majoritatea cazurilor nu poți să alegi cu cine lucrezi şi te trezeşti în situația că trebuie să faci compromisuri pentru că cineva nu şi-a făcut treaba, iar în opinia mea, asta diluează extrem de mult rezultatul obținut. pentru că în loc să atingi “state of the art” te opreşti la un românesc “merge şi aşa”.

a doua problemă, mai importantă, e legată de partea financiară, dar nu aşa cum ți-ai imagina: intru într-un proiect de cercetare ca să am acces la o infrastructură pe care altfel nu mi-o permit. eh, pentru că suntem în România şi pentru salarii mai mari tăiem din bugetul de achiziții, mă trezesc că pot să cumpăr aproape tot ce am nevoie doar lucrând puțin mai mult la birou sau cumpărându-mi mai puține lucruri de la Zara. şi e trist. poate şi domeniul e de vină, pentru că tot ce-mi trebuie se găseşte pe AliExpress la prețuri derizorii, de altfel şi sursa originală a majorității achizițiilor pentru un proiect. mai mult, pentru lucruri mai complexe (cum am făcut de altfel, mă refer la tranzistori personalizați), nu ies o lună în club şi contractez un serviciu online, pentru că sunt o mulțime.

în al treilea rând e birocrația unui proiect. rapoarte. achiziții. referate de necesitate. discuții cu finanțatori şi investitori şi managementul aşteptărilor lor nerealiste – ah, o mică paranteză aici, dacă nu e niciun risc implicat, n-ar mai fi cercetare, nu? e la fel ca în prima problemă, din lipsă de competență la nivel de grup, m-am trezit plimbat în întâlniri pe post de maimuță, doar pentru a susține credibilitatea proiectului, lucru fără de care pot să trăiesc bine-mersi.

aşa că una peste alta, dacă vreau să cercetez, mai bine muncesc puțin mai mult, îmi iau fără stres tot ce îmi trebuie din munca mea, stau fără stres birocratic, nu trebuie să fac şi munca “colegilor” mei şi beneficiez doar eu de rezultatele muncii mele. cu un singur compromis, că nu pot să adresez o problemă interdisciplinar, că logic, n-am competențe. ah, da, iar probleme găsesc la tot pasul. mai nou şi centralizat, cum sunt pe kaggle.

bogdan » arduino: senzor de culoare

10:50 pm on Apr 29, 2019 | read the article | tags:

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

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 | read the article | tags:

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

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 | read the article | tags:

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

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

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

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

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

cum convertesc permeabilitate magnetică în frecvență?

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

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

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

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

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

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

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

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

Arduino: pin 7 waveform

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

Arduino: Metal Detector 1st Try

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

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

LC Circuit Wave Form

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

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

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

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

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

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

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

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

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

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

cum construiesc oscilatorul?

Arduino: Metal Detector

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

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

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

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

LC Colpitts Oscillator

notă:

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

cum măsor frecvența?

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

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

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

notă:

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

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

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

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

notă:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

bogdan » arduino: multimetru digital

07:14 pm on Nov 25, 2018 | read the article | tags:

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

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 | read the article | tags:

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

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 | read the article | 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 | read the article | 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 | read the article | tags:

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

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ă.

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.