Avtorja: Mag. Vladimir Mitrović, Robert Sedak
E-pošta: vmitrovic12@gmail.com
Za tiste ki želijo vedeti več, bomo v tem nadaljevanju opisali načine, s katerimi lahko mikrokontroler generira impulze, ki so potrebni za krmiljenje servo motorja.
V programih, analiziranih v preteklem nadaljevanju, za generiranje krmilnih impulzov Bascom-AVR in Arduino uporabljata tehniko prekinitev (interrupt). Krmilni priključki mikrokontrolerja se postavljajo v stanja “0” in “1” v prekinitvenih rutinah, ki se periodično izvajajo. V njih se prav tako določa tudi trenutek, v katerem mora krmilni priključek enega od servo motorjev spremeniti stanje.
Ta vsebina je samo za naročnike
Slika 17 ilustrira princip prekinitve na primeru nekega Bascom-AVR programa. Po inicializaciji (C0) se program odvija znotraj neskončne Do-Loop zanke po vrsti C1-C5-C6-C1… V trenutku, ko se dogodi prekinitev, ki je povzročena z nekim zunanjim dogodkom, se izvršitev glavnega programa prekine in prične se izvrševati podprogram (prekinitvena rutina), ki oddela to, kar je potrebno. V našem primeru bo prekinitve generiralo časovno vezje – timer. V prekinitvenih rutinah pa se bodo menjala stanja krmilnih priključkov. Vsak podprogram se zaključi z ukazom vrnitve v glavni program (v tem primeru Return). Nato bo glavni program nadaljeval izvrševati program od mesta, v katerem je bil prekinjen. Vrstni red izvrševanja programa v primeru prekinitve je C1-C2-C3-C4-C5-C6-C1… Identična procedura se odvija tudi v Arduino programih, samo sintaksa je drugačna.
Prekinitve se lahko dogodijo v katerem koli trenutku, tudi takrat, kadar se v registrih mikrokontrolerja nahajajo neke važne vrednosti. Zato mora program, preden prične izvrševati prekinitveno rutino, shraniti vrednosti vseh registrov, da bi jih pred vrnitvijo v glavni program lahko vrnil in neovirano nadaljeval izvrševanje programa od točke prekinitve.
Da bi se motorji zavrteli gladko, brez skokovitih sprememb položaja, in da bi lahko stabilno držali položaj v katerega so postavljeni, se morajo te prekinitvene rutine izvrševati v istih časovnih intervalih in zelo pogosto: parametri, ki smo jih postavili v Bascom-AVR programu Shield-B_5.bas povzročajo ponavljanje prekinitvenih rutin približno vsakih 10 µs. Če mikrokontroler dela samo tisto, kar smo zadali našim programskim nalogam, to ne bo predstavljalo problema. Če pa mora mikrokontroler opravljati neko “resno” delo in hkrati krmiliti nekaj servo motorjev, bodo te prekinitve predstavljale veliko obremenitev za procesor in to bo znatno upočasnilo izvrševanje glavnega programa.
Drugi način, s katerim lahko generiramo potrebne krmilne impulze je z uporabo timerja. Eno uporabo timerja smo že spoznali, ko smo pokazali kako s širinsko moduliranimi impulzi lahko krmilimo s hitrostjo enosmernega motorja (poglejte drugo nadaljevanje). Tukaj bomo uporabili zelo podobno tehniko!
Krmilni priključki za servo motorje na razvojnem sistemu Shield-B, PB.1 in PB.2, so skrbno izbrani: poleg tega, da delajo kot “navadni” digitalni vhodi ali izhodi, so lahko povezani tudi na izhode OC1A in OC1B Timerja1 (slika 18). Shema je identična tisti s slike 7 in dela po istem principu, samo da tukaj uporabljamo Timer1 in vpisane so drugačne vrednosti v vzporedne registre, OCR1A in OCR1B. Timer1 je konfiguriran tako, da šteje impulze frekvence 2 MHz od 0 do 19999 in nato nazaj proti 0. En takšen cikel traja 20 ms, zato bo frekvenca generiranih impulzov prav 50 Hz. Širina impulzov je odvisna od vrednosti, ki so vpisane v vzporedne registre:
Ko se med štetjem navzgor izenači vrednost števca s številom v enem od vzporednih registrov, bo mikrokontroler postavil ustrezni priključek v stanje “0”;
Ko se med štetjem navzdol izenači vrednost števca s številom v enem od vzporednih registrov, bo mikrokontroler postavil ustrezni priključek v stanje «1».
Vrednosti, ki jih vpisujemo v vzporedna registra ustrezajo želenemu trajanju impulzov v mikrosekundah. Lahko vpišemo katero koli vrednost med 0 in 19999, vendar bodo za naše potrebe ustrezale vrednosti v razponu od 700 do 2300: iz njih bodo na ustreznih izhodnih priključkih OC1A in OC1B (~9 in ~10 na Arduino ploščici) nastali impulzi širine 700-2300 µs! Če vežemo servo motorja kot vidimo na sliki 15, ju bomo krmilili s pomočjo potenciometrov RV1 in RV2 na isti način, kot v 5. programski nalogi. Poglejmo ustrezno rešitev!
Program Shield-B_5a.bas
A/D pretvornik in tudi pripadajoči spremenljivki Rv1 in Rv2, v kateri shranimo napetost drsnika potenciometrov z istimi imeni, konfiguriramo na isti način:
Dim Rv1 As Word , Rv2 As Word
Config Adc = Single , Prescaler = Auto
, Reference = Avcc
Timer1 lahko dela na 16 različnih načinov, od katerih je večina podprta iz programskega jezika Bascom-AVR z uporabo ukaza Config Timer1. Na žalost se to ne nanaša na način dela, ki smo ga opisali in ilustrirali na sliki 18, zato bomo želeni način dela lahko dosegli samo z vpisom ustreznih vrednosti v njegove konfiguracijske registre:
Icr1 = 19999
Tccr1a = &B10100010
Tccr1b = &B00010010
Vrednost v ICR1 registru določa mejo, do katere števec šteje, kombinacije bitov v preostalih dveh registrih določajo, da timer dela točno to, kar potrebujemo. Da bi se impulzi pojavili na izhodnih priključkih mikrokontrolerja, jih je potrebno konfigurirati kot izhodne:
Config Portb.1 = Output
Config Portb.2 = Output
V glavni zanki najprej merimo napetost na drsniku potenciometra RV1. Prejete vrednosti so v razponu 0-1023, mi pa potrebujemo razpon 700-2300. Zato bomo vsako prebrano vrednost najprej pomnožiti s faktorjem 61/39, da bi jo privedli v razpon 0-1600, nato pa rezultatu dodamo 700 in ga vpišemo v vzporedni register OCR1A:
Do
Rv1 = Getadc(0) * 61
Rv1 = Rv1 / 39 ‘0-1600
Ocr1a = 700 + Rv1 ‘700-2300
Tako smo v OCR1A vpisali število v razponu 700-2300, točno toliko je potrebno za generiranje ustreznih krmilnih impulzov. Postopek s drugim servo motorjem je identičen, samo tukaj bomo vrednost za vzporedni register OCR1B izračunali na način, da se os servo motorja vrti v nasprotni smeri:
Rv2 = Getadc(1) * 61
Rv2 = Rv2 / 39 ‘0-1600
Ocr1b = 2300 – Rv2 ‘2300-700
Loop
Prednost tega postopka je očitna: ko smo poslali ukaz timerju, bo začel generirati impulze želene širine brez obremenitve za program, ki se lahko posveti reševanju drugih nalog. Slaba stran pa je v tem, da lahko krmilimo samo dva servo motorja, ki morata biti vezana na točno določene priključke mikrokontrolerja. Druga dva timerja, katera ima mikrokontroler ATmega328P, Timer0 in Timer2, sta 8-bitna in sta praktično neuporabna za ta način. V njune vzporedne registre je možno vpisati vrednosti 0-255, zato se bo finost rasterja, s katerim generiramo krmilne impulze, znatno poslabšala; z njuno pomočjo bi servo motorja bilo možno skokovito postaviti v samo nekaj 10 položajev.
Lahko sploh napišemo program, s katerim bomo učinkovito krmilili servo motorje, ki so vezani na priključke mikrokontrolerja, ki niso povezani s timerjem? Lahko, če se omejimo na dva motorja in če napišemo lastne krmilne rutine!
Ilustrirali bomo to s primerom, v katerem bomo servo motorja vezali na konektor J7, originalno predviden za I2C komunikacijo (Slika 19). Krmilne signale bomo generirali na priključkih PC4 (A4) in PC5 (A5), ki nista povezana s timerjem, zato bomo morati uporabiti tehniko prekinitve. Na začetku članka smo pojasnili, kako to deluje in pri tem pokazali na probleme, ki jih takšen pristop povzroči. V naši rešitvi bomo te probleme minimizirali.
Osnovna ideja je prikazana na Sliki 20. Timer1 dela na isti način, kot na Sliki 18, razen tega, da ne generira krmilnih impulzov na svojih OC1A in OC1B priključkih; namesto tega se v trenutkih, ko se števec izenači z vrednostmi v vzporednih registrih OCR1A in OCR1B, zažene ustrezne prekinitvene rutine Comp1a_sub in Comp1b_sub. Prekinitveni rutini sta dela programa, ki sta pridružena nekemu dogajanju in sta na sliki obarvani rumeno, da bi jih razlikovali od dela prikazanega z “modrimi” simboli, ki tvorijo vezja Timer1.
Med enim ciklom štetja, ki traja 20 ms, se vsaka prekinitvena rutina aktivira dvakrat: enkrat pri štetju naprej, drugič pa kadar števec šteje nazaj. V prekinitveni rutini opravljamo zelo enostavno delo: ob vsakem aktiviranju komplementiramo stanje krmilnega priključka (če je v stanju “0”, ga postavimo v “1”, in obratno). Poglejmo kako je to rešeno v programu!
Program Shield-B_5b.bas
Uvodni del programa in konfiguracija A/D pretvornika sta identična kot v predhodnem programu. Timer bomo pa konfigurirali malo drugače, ker sedaj ne sme generirati impulzov na svojih pridruženih priključkih:
Icr1 = 19999
Tccr1a = &B00000010
Tccr1b = &B00010010
Še priključka PC4 in PC5 bomo konfigurirali kot izhodna
Config Portc.4 = Output
Config Portc.5 = Output
in kar je zelo važno, postavili ju bomo v pravilno začetno stanje:
Reset Portc.4
Reset Portc.5
Če tega ne bi naredili, bi namesto pozitivnih lahko dobili negativne krmilne impulze, na katere servo motor ne bi pravilno reagiral.
Moramo še definirati prekinitvene rutine (podprograme), ki so pridruženi prekinitvam. V našem primeru prekinitve nastanejo, ko se stanje števca izenači z vrednostim v vzporednima registroma OC1A in OC1B, ustrezni prekinitveni rutini sta Comp1a_sub in Comp1b_sub:
On Oc1a Comp1a_sub Nosave
On Oc1b Comp1b_sub Nosave
Opcijo Nosave bomo pojasnili na koncu članka. Čeprav so definirane, se prekinitveni rutini ne bosta izvrševali, dokler jim to ne omogočimo z ustreznimi ukazi:
Enable Oc1a
Enable Oc1b
Enable Interrupts
V tem trenutku se prične proces ilustriran na Sliki 20. Sledi glavna programska Do-Loop zanka, identična tisti v predhodnem primeru: mikrokontroler meri napetosti na drsnikih potenciometra in jih pretvarja v vrednosti, ki so primerne za krmiljenje servo motorja, in jih shrani v krmilne registre.
Prekinitvene rutine programiramo po glavni zanki. Ker v njih samo moramo spremeniti stanje krmilnih priključkov, bi lahko izgledale tako:
Comp1a_sub:
Portc.4 = Not Portc.4
Return
Comp1b_sub:
Portc.5 = Not Portc.5
Return
Samo z enim ukazom dosežemo želeni cilj! Vendar tukaj obstaja tudi skrit problem: Bascom-AVR ukazi med izvrševanjem uporabljajo registre za splošne namene. Mi ne vemo, katere registre kateri ukaz uporablja, zato moramo iz previdnosti shraniti vrednosti vseh registrov pred izvršitvijo prekinitvene rutine, da bi jih na zaključku lahko vrnili v originalno stanje. To je predpogoj, da bi se “prekinjen” glavni program lahko neovirano nadaljeval, ko prekinitvena rutina oddela svojo nalogo. Shranjevanje registrov bo za nas napravil Bascom prevajalnik, če pustimo opcijo Nosave, vendar bo to znatno podaljšalo trajanje prekinitvene rutine. Eno od osnovnih pravil dobrega programiranja je, da morajo biti prekinitvene rutine kolikor je možno kratke. Zato bomo prekinitvene rutine napisali v asemblerju. Poglejmo kako to izgleda:
Comp1a_sub:
sbic portc,4
rjmp +3
sbi portc,4
rjmp +2
cbi portc,4
Return
Tukaj nam ni namen spoznavati asembler, poudarili bomo samo osnovne razlike! Če programiramo v asemblerju, moramo pogosto napisati več, včasih tudi veliko več programskih ukazov, da bi dosegli rezultat, ki ga dosežemo s samo enim Bascom-AVR ukazom. Vendar pa če programiramo v asemblerju imamo popolno kontrolo nad delom mikrokontrolerja in tudi nad registri, ki jih posamezni ukaz uporablja.
V tem primeru smo uporabili samo ukaze, ki ne uporabljajo registrov za splošne potrebe, zato je uporaba opcije Nosave opravičena. Izvršitev tako napisane prekinitvene rutine traja točno 1 µs in na noben način ne bo motila izvrševanja glavnega programa. Če jo primerjamo z ekvivalentnim Bascom-AVR ukazom brez Nosave opcije, bi to trajalo 7,5 krat dalj!
Če boste analizirali program Shield-B_5b.bas, boste opazili, da je v njemu predvidena še ena prekinitev, Capture1, kateri je pridružena prekinitvena rutina Capt1_sub. V njej se postavljajo krmilni priključki v stanje “0” v trenutkih, ko števec doseže maksimalno vrednost 19999. To je zamišljeno kot dodatno zavarovanje, da bi impulzi vedno imeli pravilno polariteto. V praksi se je to pokazalo kot nepotrebno, zato so pripadajoči ukazi v končni verziji programa zakomentirani.
Programski jezik Arduino je narejen tako, da nam omogoča čim enostavnejše doseganje želenega cilja, neodvisno od mikrokontrolerja, na katerem se bo izvrševal. Zato so nam procesi znotraj mikrokontrolerja in tehnika, s katero se rezultat doseže bolj skriti, kot pri Bascom-AVR programu. Zaradi istega razloga je v Arduino program težje vnesti del asemblerskega programa, s katerim želimo pohitriti izvrševanje nekega procesa ali imeti nad njim popolnejšo kontrolo, zato smo se v tem nadaljevanju zadržali samo na Bascom-AVR rešitvah!
Opombe: Programe Shield-B_5a.bas in Shield-B_5b.bas lahko brezplačno dobite od uredništva revije Svet elektronike! Če uporabljamo dva servo motorja, je nujno zagotoviti napajanje preko mrežnega adapterja napetosti 7,5-9 V in maksimalnega toka vsaj 1 A. Adapter ne vklopimo v Shield-B, pač pa v Arduino Uno!
Vstopna