2012-09-22

Echolokacija naudojant HC-SR04 ultragarso sensorių bei Atmega

Vienas iš įdomesnių garso taikymo būdų – atstumui nustatyti, t.y. echolokacija. Išsiuntus garso signalą iki objekto ir užregistravus jo grįžtantį aidą galime apskaičiuoti ir atstumą iki to objekto. Elementarus fizikos kursas. Tereikia fiksuoti šiuos laiko momentus, žinoti garso greitį aplinkoje ir atstumą bus galima apskaičiuoti. Tokiais metodais naudojasi ir kai kurie gyvūnai gamtoje, pavyzdžiui, šikšnosparniai ar delfinai pasitelkdami ultragarsą. Kalbant apie elektroniką, įmanoma įsigyti pigius ultragarso modulius, skirtus atstumui iki objektų matuoti. Šįkart kaip tik apie tokius taikymus ir pakalbėsime analizuodami HC-SR04 sensorių.




I. Apie sensorių

Mėgėjiškų ultragarso HC-SR04 sensorių galima rasti ebay.com ir pan. Tai vienas iš populiariausių žaisliukų, galintis pasitarnauti atstumų matavimui, robotikoje ir pan.



Pagal skelbiamus duomenis, sensorius pasižymi dideliu tikslumu, net iki +/-3 mm, bet realybėje nėra skirtas preciziniams matavimams, nes matavimai gana smarkiai svyruoja. Matuoja atstumus iki nuo 2cm iki 5m, yra lengvai valdomas (kaip pamatysime vėliau). Veikimo kampas į šoną – koncentruotas 15 laipsnių priešais modulį. Tik keturi išvadai, du iš jų VCC ir GND, kiti du – Trigger matavimo pradžios impulsui generuoti bei Echo signalo priėmimui. Matavimo funkcijas atlieka nugarinėje modulio dalyje esančios mikroschemos.



Belieka tik pasižiūrėti, kaip komunikuoti su tokiu moduliu per du duomenų išvadus.

II. Komunikavimas


Padavus maitinimą moduliui - niekas nevyksta, todėl visada reikia pirmiausia inicijuoti matavimus. Šiam veiksmui atlikti reikia paduoti ne trumpesnį nei 10us aukšto lygio signalą (5V) į Trigger įšvadą. Žinoma, prieš pradedant Trigger reikšmė turi būti 0V. Po impulso padavimo, šiame išvade įtampos lygį galima vėl pakeisti į 0V. Realizuoti visą šį veiksmą nesudėtinga pasinaudojus _delay_us(10) standartine funkcija. Toliau modulis automatiškai siunčia 8 garso (40kHz) impulsus ir juos, atsispindėjusius nuo kliūties, vėl priima. Tuo metu jau reikia būti pasiruošus nuskaityti įtampos reikšmę iš Echo išvado. Kuomet modulis baigia impulsų priėmimą, Echo išvade įtampa pasikeičia į aukštą lygį. Toliau šis signalas išlaikomas tam tikrą laiko tarpą ir vėl pakeičiamas į žemą (žiūr. paveikslą). Priklausomai nuo atstumo iki kliūties signalo trukmė kinta. Kuo toliau bus kliūtis, tuo ilgiau šis signalas bus išlaikomas aukštame lygyje. Artimoms kliūtims signalo trukmė gali būti mikrosekundžių eilės, didesnėms – milisekundžių ir pan.

Man betestuojant būdavo ir taip, jog signalas visada išlieka aukštame lygyje – kliūtis labai tolima arba dėl triukšmų ultragarsiniai impulsai nebegrįžta arba grįžta pavėlavę ir pan. Todėl reikia visada įvertinti, kokį max laiką skirti matavimui, nuo to priklausys ir matuojamo atstumo ilgis. Šiam atvejui reikia pasiruošti, ir jei signalo lygis išvade nenukrenta iki žemo per tam tikrą max laiką (200ms, 1s ar pan.) – imtis atitinkamų veiksmų (fiksuoti, kad kliūčių nėra, jos labai tolimos ar grąžinti max atstumą) ir baigti matavimą.

Kitas esminis dalykas – tarp matavimų daryti pauzę (~50ms ar pan.). Tai reikalinga tam, kad grįžtantys pavėlavę ultragarso impulsai nesutrukdytų naujam matavimui ar neiškreiptų naujo matavimo duomenų. Taigi, reikia prisiminti – po įvykdyto matavimo iškart kito geriau nedaryti, o palaukti.

Taigi uždavinys manau aiškus – reikia surasti impulso trukmę, o iš jos po to bus galima paskaičiuoti ir atstumą iki objekto. Šiam reikalui gerai pasitarnauja Atmega esantys išorinių pertraukimų įvestys – INT0, INT1. Jomis pasinaudojus automatiškai galima nustatyti, kada įtampa Atmega išvade pasikeičia (iš žemo į aukštą, iš aukšto į žemą arba fiksuoti abu įvykius). Pasikeitus įtampai INTx išvade gaunamas INTx_vect  pertraukimas programoje ir galima atlikti reikiamus veiksmus.

Taigi, viską apibendrinus, po matavimo paleidimo, reikės laukti įtampos pasikeitimo į aukštą lygį, tuomet įjungti Atmegoje esantį TIMER laikmatį ir laukti įtampos pasikeitimo į žemą. Jei signalas Echo išvade visgi pasikeis į 0V, tada galėsime bevargo apskaičiuoti atstumą ir matavimas bus baigtas. Kitu atveju – jei matavimui skirtas laikas bus viršytas, t.y. timeout (nesumaišyti su overflow), tai matavimą teks baigti ir sakyti, kad kliūtis labai tolima ir nepatenka į matavimo diapazoną ar pan. Na ir po kiekvieno užbaigto matavimo palauksime tam tikrą laiko tarpą, kad aplinka išsivalytų nuo pašalinių ultragarsinių impulsų pėdsakų.

III. Realizacija


Be abejo, viską, apie ką kalbėjau, prieš tai reikia sukonfigūruoti ir įjungti mikrokontroleryje. Tai daroma redaguojant registrų reikšmes. Savo bandymuose modulį prijungiau taip: PB0 – Trigger, INT1 – Echo, kaip pavaizduota paveiksle. Pradžioje nustatome žemą 0V signalą PB0 išvestyje (matavimas nevyksta).


Toliau reikia nustatyti pertraukimų režimą priklausomai nuo įtampos pasikeitimų INT išvade. Šiam projektui būtų galima naudoti abu INT0 ir INT1 išvadus. Vienam nustatyti pertraukimus, kai išvade pasiekiamas aukštas lygis, kitam, kai įtampa nukrenta į žemą lygį 0V. Siekiant sutaupyti, pasinaudojau tik vienu – INT1, o kitą palikau laisvą(bus galima prijungti dar vieną modulį). Taigi INT1 nustačiau, jog pertraukimai būtų iškviečiami ant bet kokio įtampos lygio pasikeitimo, o koks lygis yra esamu momentu, bus sekama pačioje programoje vienu kintamuoju.


GICR registre nustatome, kokius naudosime INTx išvadus. Mano atveju INT1 pasirinktas. MCUCR register nustatome, kokiam pasikeitimui esant yra iškviečiamas pertraukimas.




Toliau reikia sukonfigūruoti laikmatį, kuris padės garantuoti, jog matavimas neužsitęs be galo ilgai ir skaičiuos impulso laiką. Pasirinkau 8 bitų laikmatį, tačiau galima naudoti ir 16 bitų. Skirtumas būtų tik toks, jog su didesnės rezoliucijos persipildymų skaičius būtų mažesnis. Laikmačio daliklis nustatomas į 1 pakeičiant TCCR0 registrą, t.y. laikmačio dažnis nebus dalinamas ir sutaps su mikrokontrolerio veikimo dažniu. Taip pat laikmačio reikšmė TCNT0 išvaloma į 0 ir įjungiamas laikmačio persipildymo pertraukimas nustatant TIMSK pertraukimų bitą. Prie visų nustatymų nereikia pamiršti įjungti pertraukimų vykdymą komanda sei().





Na štai, manau užteks šių įžanginių nustatymų. Toliau pasižiūrėsime, kaip vykdyti sensoriaus paleidimą bei matavimus pertraukimų kode. Kaip ir minėjau anksčiau, sensoriaus paleidimas yra gana paprastas – 10us impulsas į Trigger išvadą. Nepamirštant vėl nustatyti išvado lygio į žemą 0V. Papildomai galime sekti, jog matavimas paleistas nustatant kintamąjį.


Dabar peržiūrėsime pertraukimų kodą. INT1 pertraukime pirmiausia registruojame aukštą Echo išvado lygį. Išvalome laikmatį ir nustatome, jog pradedame Echo impulso laiko matavimą. Sekantį kartą, kai šis petraukimas yra iškviečiamas, sustabdome matavimus, apskaičiuojame atstumą iki kliūties pagal laikmatyje sukauptą laiką, atstumą iki kliūties išsaugome "result" kintamajame.



Kitas pertraukimas yra vykdomas, kai laikmatis perpildomas, t.y. kai TCNT0 baito reikšmė iš 255 pereidama į 256 tampa 0. Šiame pertraukimo kode skaičiuojame persipildymų skaičių, kai ultragarso sonaras vykdo matavimus.  Skaičiuoti persipildymų skaičių reikia tam, kad paskui galėtume tiksliai atstatyti, kiek laiko praėjo, nes laikmačio reikšmė po persipildymo tampa 0. Taip pat čia paskaičiuojame tiksliai, kiek laiko praėjo ir tikriname, ar matavimas neužtruko pernelyg ilgai (timeout), nes vis dėlto nesiruošiame laukti amžinybę rezultato nuo labai tolimų kliūčių. Šiuo atveju matavimo atstumas nustatomas į -1, tai nusako, jog kliūčių nėra.


Na ir galiausiai pakalbėsime apie matavimų paleidimą bei laukimą po kiekvieno matavimo. Taigi visa tai realizuoti galima pagrindinėje programos dalyje. Nutariau tai padaryti su delay funkcija, kol praeina reikiamas 50 ms laiko tarpas. Žinoma, galima realizuoti ir kitaip - panaudojant tą patį laikmatį arba tą patį uždelsimą vykdyti po 1ms per 50 iteracijų - taip nebūtų ilgam laikui blokuojamas kitas kodo vykdymas cikle. Kam gi be reikalo švaistyti mikroprocesoriaus ciklus nenaudingam miegojimui? Paprastumo dėlei pavyzdyje žemiau tiesiog panaudojama uždelsimo funkcija.


Visą projekto kodą rasite straipsnio pabaigoje.

IV. Atstumo apskaičiavimas ir rezultatai

Žinodami Echo impulso trukmę galime apskaičiuoti atstumą iki kliūties. Tai daroma pagal žemiau pateiktą formulę:

Laikas t formulėje bus mikrosekundėmis, garso greitis v ore ~340m/s. Kadangi matavimo vienetai nesutampa, tai tenka atlikti porą pakeitimų, po kurių bendrą formulę gauname tokią (atstumas iki kliūties milimetrais):

Rezultatas kode saugomas skaitine reikšme kintamajame "result". Kaip naudosite ar išvesite šitą informaciją - LCD ekrane, siųsite į kompiuterį ar pan. palieku spręsti pačiam skaitytojui. Aš savo testų informaciją persiunčiau į kompiuterį per USB, kad lengviau būtų analizuoti duomenis, tačiau sensoriaus veikimui tai visiškai nėra būtina. Projekto kode ši dalis praleista ir paliekama realizuoti savarankiškai pagal poreikius.

V. Veikimas

Na ir realizuoto projekto demonstraciją galite pamatyti žemiau video. Su šiuo sensoriumi taip pat galima ir vykdyti atvirkščius matavimus – nustatinėti garso greitį, kai atstumas tarp modulio ir kliūties yra tiksliai žinomas ir pan. O jeigu primontuojame jį prie servo motoro, sukti 360 laipsnių kampu ir padaryti 5 m spindulio radarą ir pan. Na fantazijai ribų nėra, taigi jeigu dabar išmokote naudotis šiuo moduliu ir gaminate kažką įdomaus – pasidalinkite savo atradimais ir su manimi. Sėkmės.


21 komentaras:

  1. Sveiki,

    išsibandžiau viskas veikia. Iškilo klausimas, kodėl negalima naudoti "delay" pagrindinėje programos dalyje? Jutiklis nustoja matuoti. Gal yra galimybė apeiti šią problemą? Jei taip, kokius pakeitimus atlikti programoje?

    Pvz.:
    for(;;){

    _delay_ms(200);

    if(running ==0){ ......

    Dėkoju už atsakymą.

    AtsakytiPanaikinti
    Atsakymai
    1. Viską galima naudoti. Pagal dabartinį kodą įdėjus toje vietoje 200ms delay vienas matavimas bus atliekamas tik po (200 x N + N) ms, t.y. po ~10sek., čia N pagal mano pavyzdį - 50. Taip padaryta tam, jog kaskart 50ms neužblokuoti ciklo vykdymo, o tai daryti palaipsniui po 1ms per 50 ciklo iteracijų (50 x 1ms = 50ms).
      Geras klausimas būtų, kam to reikia? (pvz.: jeigu toliau ciklo viduje yra vykdomas kitas kodas, kuris negali toleruoti didesnio nei >5ms uždelsimo vienu metu).

      Tokiu atveju manau tiesiog galima supaprastinti, kad būtų aiškiau:

      for (;;) {
      _delay_ms(200);
      // kiti veiksmai ...

      if (running == 0) {
      _delay_ms(50); // būtina užtikrinti tarp matavimų bent >=50ms pauzę,
      // jei pauzė atliekama prieš tai, galima ir pašalinti

      sonar(); // paleidimas
      }
      }

      Panaikinti
  2. Dėkui už atsakymą. To reikia tam, kad galėčiau tuo pačiu metu valdyti variklį ir jutiklį (lygiagrečiai). Variklio valdymo funkcijoje yra keletas delay su 0,5s uždelsimu. Rezultatas toks: kol varikliai sukasi, tol jutiklis nematuoja. Galbūt išeitų ta pačią sonar() funkciją perkelti į pertrauktį (tokiu atveju pagrindinėje programos dalyje būtų galima naudoti uždelsimą?

    Principas toks:
    for(;;){
    variklis_i_kaire();
    _delay_ms(500);
    variklis_i_desine();
    _delay_ms(500);

    if(running == 0){
    _delay_ms(50);
    sonar();
    }
    }

    AtsakytiPanaikinti
    Atsakymai
    1. Jeigu reikia lygiagrečiai, tai siūlau pasinaudoti dar vienu taimeriu TCNT2 (atmega8), kuris tarkim persipildytų kas 50 ms ir tikrintų, ar galima paleisti matavimą, t.y. sonar() metodą.
      Vėlgi reiktų aprašyti analogišką TIMER2_OVF_vect petraukimą ir ten perkelti sonaro paleidimą. Taip pat svarbu ilgai neužtrukti pačių pertraukimų vykdyme, nes tuo metu kiti ateinantys pertraukimai nėra apdorojami.

      Kažkas panašaus čia: http://bsiswoyo.lecture.ub.ac.id/2012/10/timer-2-interrupt-event-timer2_ovf_vect/

      Panaikinti
  3. swx, as darau su atmega328 gal nujauciate kokius nustatymus reiketu atlikti, nes sie GICR |= (1 << INT1); kaip ir netinka ir meta klaida...

    AtsakytiPanaikinti
    Atsakymai
    1. Na i6kilus tokiems klausimams reiktų geriausia pasikonsultuoti su oficialiu Atmega328 datasheet aprašymu, va: http://www.atmel.com/Images/doc8161.pdf

      Taip, kagandi Atmegos modelis kitas, tai nevisada yra viskas suderinama. Kaip dokumente 12.2 skyriuje parašyta, pertraukimas INT1 tiek krentančiam/kylančiam įtampos lygiui nustatomas pakeičiant registrą EICRA taip:
      EICRA = (0<<ISC11)|(1<<ISC10);
      Tada INT1 pertraukimas įjungiamas pakeičiant kaukės bitą:
      EIMSK = (1<<INT1);
      Tada belieka įjungti petraukimus nustatant SREG registro I bitą su komanda: sei().

      Panaikinti
  4. Is the Circuit same for Atmega32 ??

    AtsakytiPanaikinti
    Atsakymai
    1. No, as atmega8 has 28 pins, while atmega32 - 40 pins, though it is possible to convert it to work with atmega32 - I suggest checking both datasheets for differences.

      Panaikinti
  5. Hi,

    I wanted to modify your code so that it would display "result" in my PC (with one sec period) over serial connection (using USB<->RS232 TTL converter). Here is what I did:

    after "// other code here..." I put:

    const char* p;
    sprintf(p, "%lu\r\n", result); //convert long "result" variable into string
    for ( ; *p; ++p )
    {
    while(!(UCSRA & 1<
    UBRRH = UBRRH_VALUE;
    UBRRL = UBRRL_VALUE;
    #if USE_2X
    UCSRA |= (1<<U2X);
    #else
    UCSRA &= ~(1<<U2X);
    #endif
    UCSRB = (1<<RXEN)|(1<<TXEN);
    // 8 bit data; 1 stop
    UCSRC = (1<<URSEL)|(3<<UCSZ0);
    }

    but what I get on terminal is "0" (repeated in new line after each second).

    What I'm doing wrong???

    Could you please send me your code that displays "result" in terminal?

    AtsakytiPanaikinti
    Atsakymai
    1. Hi,
      first of all you should check that your variable "p" is only a pointer, not a char buffer - it has no length, so using sprintf will not work & I suspect that's why you get "0" everytime. Change "p" to at least: "char p[256];". Then try again.

      You can find more info about using the terminal here: http://www.wzona.info/2012/10/usb-duomenu-perdavimas-atmega.html

      Panaikinti
    2. Tinklaraščio administratorius pašalino šį komentarą.

      Panaikinti
    3. Please, use pastebin.com or similar services for pasting code - just a link would be fine. Will remove the message as there's no formatting and it's hard to analyze it.

      Panaikinti
    4. Hi,
      Here is the code: http://pastebin.com/ZVTVEVEb
      Could you please tell me why I get always "0" in my PC ?
      On the other hand, can you share with us version of your code that displays result in PC over serial connection?
      Thanks,
      Maciej

      Panaikinti
    5. There's a bunch of mistakes in your code:

      1) First of all it's obvious that TIMER0 has been messed up and the TIMER1_OVF_vect interrupt is never even called as TIMER1 (and its interrupt) is not even enabled! This explains why you always get 0 for "result".

      More details: In the main function TIMER0 is first configured for HC-SR04 - this is correct, then you change TIMER0 (which again - is used by HC-SR04) prescaler value to 1024 & use TIMER0 for your own needs - this is not good (by default - no prescaler should be used & you can't use TIMER0). Even if it "somehow" worked, the the time measurement would be incorrect and the "result" value would not contain the true distance value - just junk.

      So, leave TIMER0 for ultrasound sensor & use TIMER1 for your own needs, don't use TIMER0 for both.

      2) Secondly, it's a bad design to use delay functions inside an interrupt vector, like: _delay_ms(50). Never do this! If you wan't to create a delay - do it in the main loop using extra logic. Interrupts should contain only fast non-blocking code & never freeze the whole AVR mcu. Also use SIGNAL instead of ISR, to eliminate cli/sei in interrupt code.

      For future: analyze the given code & try to understand what resources(timers/interrupts) are used before using them for your own debugging or etc. needs. If something does not work - check your code again. The current HC-SR04 interfacing implementation is working and was tested a lot of times in other projects.

      Your mentioned code uses V-USB library & works over USB, not over USART. See the previously mentioned article(with code) to understand the technique. The decision of how to implement & display the result (as mentioned in this article) is left as an exercise for the reader.

      Panaikinti
  6. Hi, my friend Do you have de library for atmega32?
    o How can I use this one in atmega32?

    thanks
    =)

    AtsakytiPanaikinti
  7. Hello, Thank you so much, I'm from Colombia, your code was a great base to the project I'm developing. I just want to tell you something about it, I had to modify the code, because I thought there was a mistake: If you have an external clock of 12MHz I reckon you have to divide your equation of "result" by 12, in my case, I had to divide by 16 because I've used a 16MHz oscillator, afterwards it worked perfectly.

    Thanks!

    AtsakytiPanaikinti
  8. Hey man, thanks for the code and the whole post, this helped me so much.
    I just don't understand one thing, can you explain me the use of wdt.h library?

    Thanks!

    AtsakytiPanaikinti
  9. Hi, I used this tutorial logic and applied it to my microcontroller and it worked, however I wanted to change it to the interrupt INT2 and it is not working. I just changed the following:
    EIMSK |= (1 << INT1); to EIMSK |= (1 << INT2);
    and
    SIGNAL(INT1_vect) to SIGNAL(INT2_vect)

    It should be the only thing that I need to adjust right?

    AtsakytiPanaikinti
    Atsakymai
    1. What microcontroller are you using? You also need to connnect to Echo to INT2 pin.
      Btw, Atmega8 does not have INT2.

      Panaikinti