V 13. programski nalogi smo posamično vključevali RGB diode oziroma smo na vseh sočasno vklopili isto barvo – to je naloga, ki jo lahko rešimo na “običajen” način in to tako, da postavimo priključke mikrokontrolerja v določena logična stanja.
Avtorja: mag. Vladimir Mitrović in Robert Sedak
E-pošta: vmitrovic12@gmail.com
2021-298-30
Če pa želimo na RGB diodah D8-D11 v istem trenutku prikazati različne barve, moramo poseči po postopku, ki ga imenujemo multipleksiranje. O samem postopku smo že govorili v 9. nadaljevanju, tokrat pa ga bomo realizirali v praksi.
Diagram poteka na sliki 41 prikazuje, kako programsko izvajamo postopek multipleksiranja. Program po vrsti preverja, katera RGB dioda je trenutno vklopljena, nato jo izklopi, pripravi barvno kombinacijo, ki jo je treba vklopiti na naslednji diodi, in vklopi naslednjo diodo. Postopek bomo ločili v podprogram z imenom D8_D11. V tem načinu se RGB diode D8-D11 vklopijo ena za drugo, zato vsaka “pride na vrsto” šele pri vsaki četrti izvedbi podprograma. Da bi zavedli oko in dobili vtis, da LED-ice svetijo hkrati, moramo podprogram izvajati pogosto, vsaj 200-krat na sekundo.
Ta vsebina je samo za naročnike
Za naš glavni program bi bilo preveliko breme, če bi moral skrbeti za pravočasno “preklapljanje” diod. Torej smo to delo prepustili vezju znotraj mikrokontrolerja, imenovanemu timer ali časovnik. Časovnik šteje impulze in v določenih situacijah lahko spremeni stanje izhodnega priključka ali pokliče izvajanje povezanega podprograma (v tem primeru se podprogram imenuje prekinitvena rutina). Ko ga nastavimo iz našega programa na želeni način, časovnik deluje samostojno, tako da lahko glavni program dela, kar pač mora narediti.
Mikrokontroler ATmega328P na plošči Arduino Uno ima tri časovnike, od katerih bomo uporabili 8-bitni Timer2. Konfigurirali ga bomo, kot je prikazano na diagramu na sliki 42. Mikrokontroler deluje na frekvenci 16 MHz, ki jo delimo s faktorjem 64, da bi na izhodu delilnika dobili impulze frekvence 250 kHz. Timer2 šteje te impulze tako, da vsak impulz poveča stanje števca za 1. Ko stanje števca naraste na 255, bo naslednji impulz ponastavil števec in spet bo štel od 0. Na tej točki bo Timer2 povzročil prekinitev (angl. interrupt): mikrokontroler bo takoj ustavil izvajanje glavnega programa in začel izvajati ukaze povezane prekinitvene rutine. V našem primeru je to podprogram D8_D11, Timer2 ga pokliče, da se izvede približno 976 krat na sekundo; vsaka LED bo utripala približno 244 krat na sekundo – to je več, kot smo potrebovali! Upoštevajte, da “modre” oblike na sliki 42 kažejo, kaj delajo vezja znotraj mikrokontrolerja, programski del (prekinitvena rutina) pa je skrit za “rumeno” obliko.
Tukaj upoštevajte, da se vsi podprogrami končajo z ukazom Return, ki bo vrnil nadzor v glavni program takoj, ko bo podprogram končan. Če gre za prekinitveno rutino, se lahko prekinitev zgodi kadar koli, pogosto sredi izvajanja nekega ukaza znotraj glavnega programa. Vsi programski jeziki, vključno z Bascom-AVR in Arduino, imajo vzpostavljene mehanizme, ki zagotavljajo, da takšne prekinitve ne ogrozijo celovitosti glavnega programa.
14. programska naloga
Na RGB diodah D8-D11 postavite naslednje kombinacije barv:
- D8 = rdeča, D9 = zelena, D10 = modra, D11 = oranžna, za 1 s;
- D8 = zelena, D9 = modra, D10 = rdeča, D11 = modro-zelena, za 1 s;
- D8 = modra, D9 = rdeča, D10 = zelena, D11 = vijolična, za 1 s;
- D8 = bela, D9 = izklopljena, D10 = izklopljena, D11 = bela, za 0,5 s;
- D8 = izklopljena, D9 = belo, D10 = belo, D11 = izklopljena, za 0,5 s.
Poleg tega vsakih 100 ms spreminjajte barve na RGB diodah v naključnem vrstnem redu.
Bascom-AVR rešitev
(program Shield-A_14a.bas)
Najprej bomo kot izhod konfigurirali vse priključke mikrokontrolerja, na katere so vezani priključki RGB diode:
Config Portb = &B00001111 Config Portd = &B11100000
Nato bomo konfigurirali Timer2 in delilnik v skladu z uvodnim opisom:
Config Timer2 = Timer , Prescale = 64
Timerju bomo dodelili prekinitveno rutino D8_D11:
On Timer2 D8_d11 Enable Timer2 Enable Interrupts
Od tega trenutka se bo prekinitvena rutina D8_D11 začela izvajati 976 krat v sekundi. Prekinitveno rutino napišemo na samem koncu programa:
D8_d11: If Led8 = 0 Then Led8 = 1 Rdeca = D9.0 Zelena = D9.1 Modra = D9.2 Led9 = 0
Tukaj sta Led8 in Led9 alternativni imeni priključkov, katerim vključujemo in izključujemo D8 in D9, pri čemer logična ničla ustreza vklopljenem stanju, logična enica pa izključenemu stanju.
D9 je ime spremenljivke, v kateri je zapisano, katero barvo želimo prikazati na RGB diodi D9. Če uporabimo zgolj bite 0 (D9.0 = rdeča), 1 (D9.1 = zelena) in 2 (D9.2 = modra); logična enica ustreza vklopljenemu stanju, ničla pa izklopljenemu stanju. Isto logiko imamo za ostale RGB diode, zato spremenljivke D10, D11 in D8 vsebujejo “šifro” barve, ki jo želimo vključiti na istoimenskih diodah:
Elseif Led9 = 0 Then Led9 = 1 Rdeca = D10.0 Zelena = D10.1 Modra = D10.2 Led10 = 0 Elseif Led10 = 0 Then Led10 = 1 Rdeca = D11.0 Grn = D11.1 Modra = D11.2 Led11 = 0 Else Led11 = 1 Rdeca = D8.0 Zelena = D8.1 Modra = D8.2 Led8 = 0 End If Return
Ukaz Return zaključuje prekinitveno rutino in nas vrača v glavni program. Glavni program se nahaja znotraj brezkončne Do-Loop zanke, v kateri zgolj postavljamo želene barve v spremenljivke D8-D11 v skladu s postavljeno nalogo:
Dim D8 As Byte Dim D9 As Byte Dim D10 As Byte Dim D11 As Byte Do D8 = 1 'rdeča D9 = 2 'zelena D10 = 4 'modra D11 = 3 'oranžna Wait 1 D8 = 2 'zelena D9 = 4 'modra D10 = 1 'rdeča D11 = 6 'modrozelena Wait 1 D8 = 4 'modra D9 = 1 'rdeča D10 = 2 'zelena D11 = 5 'vijolična Wait 1
Očitno vrednosti 1, 2 in 4 ustrezajo rdeči, zeleni oziroma modri in druge vrednosti njihovim kombinacijam. Na koncu sledi še majhna animacija z belimi diodami:
D8 = 7 'bela D9 = 0 'izklopljena D10 = 0 'izklopljena D11 = 7 'bela Waitms 500 D8 = 0 'izklopljena D9 = 7 'bela D10 = 7 'bela D11 = 0 'izklopljena Waitms 500 Loop
Bascom-AVR rešitev dodatne naloge
(program Shield-A_14b.bas)
Če želimo barve menjati slučajno, bomo izkoristili Bascom-AVR funkcijo Rnd(). Do-Loop zanka je sedaj precej enostavnejša:
Do D8 = Rnd(999) D9 = Rnd(999) D10 = Rnd(999) D11 = Rnd(999) Waitms 100 Loop
Vsak Rnd() generira 16-bitno binarno številko, ki bi morala biti slučajna, oziroma nepredvidljiva. Razpon generiranih številk lahko omejimo s parametrom v oklepaju: nam ustrezajo številke v razponu od 0 do 7, za kar bi glede na Bascom navodila morali uporabiti funkcijo Rnd(8). Vendar številke, ki nam jih generira Bascom-AVR niso popolnoma slučajne, pač pa so rezultat nekakšnega matematičnega algoritma. Pokazalo se je namreč, da na “slučajnost” generiranih številk zelo vpliva parameter, ki smo napisali v oklepaju; z ukazom Rnd(8) se dobi neustrezen rezultat, pri katerem so se nekatere barve pojavljale precej bolj pogosto od drugih. Zato sem malo eksperimentiral s parametrom in na koncu izbral ukaz Rnd(999). Tako napisana funkcija generira številke v precej večjem razponu, kot potrebujemo, od 0 do 998, oziroma od 0 do 1111100110 binarno. Mi bomo od njih v podprogramu D8_d11 izkoristili samo tri zadnje bite, ker njihove kombinacije zajemajo vse barve, ki jih lahko reproduciramo z RGB diodami, “ostanek” generirane številke nas pa ne zanima. Edino, kar nam je pomembno je to, da se ti biti postavljajo v “slučajnih” kombinacijah, kar pomeni, da niti ena od njih ni zastopana večkrat ali manjkrat od drugih kombinacij. Sam podprogram, kot tudi ostanek programa so enaki programu Shield-A_14a.bas!
Arduino rešitev
(program Shield-A_14a.ino)
Arduino IDE definira faktor deljenja frekvence mikrokontrolerja za Timer2 tako, kot je prikazano na shemi s slike 42; mi zgolj moramo omogočiti uporabo te prekinitve, ki v Arduino IDE ima ime TIMER2_OVF_vect. Sedaj nam ostane samo še to, da napišemo funkcijo, ki se bo izvršila, ko je prekinitev aktivirana. Za to bomo uporabili posebno prekinitveno rutino (Interrupt Service Routine ali skrajšano ISR). Kadar pišemo takšne rutine, se moramo držati nekaterih važnih pravil, ki jih tukaj ne bomo podrobno obravnavali. Poudarimo zgolj to, da morajo biti ISR rutine čim krajše, spremenljivke, ki se uporabljajo tudi v glavnem programu in v ISR rutini pa morajo biti označene kot volatile.
Pri pisanju imena prekinitve moramo paziti na velike in male črke – v kolikor slučajno neko črko napišemo za malo namesto z veliko črko ali obratno, prevajalnik ne bo javi napake, prekinitvena rutina pa se ne bo izvrševala tako, kakor pričakujemo.
Sedaj smo spoznali dovolj, da lahko pristopimo reševanju naloge. Najprej bomo definirali vse spremenljivke:
byte led8 = 8; byte led9 = 9; byte led10 = 10; byte led11 = 11; byte rdeča = 5; byte modra = 6; byte zelena = 7; volatile byte D8 = 0; volatile byte D9 = 0; volatile byte D10 = 0; volatile byte D11 = 0;
Nato bomo v funkciji setup() vse priključke mikrokontrolerja, na katere so vezani priključki RGB diod, konfigurirali kot izhodne in vse priključke, na katere so vezane katode RGB diod, postavili v stanje logične enice:
void setup() { DDRD = B11100000; DDRB = B00001111; PORTB = B00001111;
Pred katerimi koli spremembami v registrih, ki so povezani s prekinitvami, moramo zaustaviti izvrševanje prekinitev:
noInterrupts();
Moramo še postaviti bit TOIE2 v registru TIMSK2 v stanje logične enice, da bi s tem aktivirali prekinitev takrat, ko se vrednost števca Timera2 resetira iz 255 na 0.
TIMSK2 |= (1 << TOIE2);
in nato lahko dovolimo izvrševanje prekinitev:
interrupts(); //omogoči prekinitve }
Od tega trenutka dalje se bo prekinitvena rutina ISR(TIMER2_OVF_vect) začela izvrševati 976-krat v sekundi. Prekinitveno rutino pišemo na koncu programa, z uporabo algoritma glede na sliko 41:
ISR(TIMER2_OVF_vect){ if (!digitalRead(led8)){ digitalWrite(led8, HIGH); digitalWrite(rdeča, bitRead(D9, 0)); digitalWrite(modra, bitRead(D9, 1)); digitalWrite(zelena, bitRead(D9, 2)); digitalWrite(led9, LOW); } else if (!digitalRead(led9)){ digitalWrite(led9, HIGH); digitalWrite(rdeča, bitRead(D10, 0)); digitalWrite(modra, bitRead(D10, 1)); digitalWrite(zelena, bitRead(D10, 2)); digitalWrite(led10, LOW); } else if (!digitalRead(led10)){ digitalWrite(led10, HIGH); digitalWrite(rdeča, bitRead(D11, 0)); digitalWrite(modra, bitRead(D11, 1)); digitalWrite(zelena, bitRead(D11, 2)); digitalWrite(led11, LOW); } else { digitalWrite(led11, HIGH); digitalWrite(rdeča, bitRead(D8, 0)); digitalWrite(modra, bitRead(D8, 1)); digitalWrite(zelena, bitRead(D8, 2)); digitalWrite(led8, LOW); } }
Opazili boste, da za branje stanja posameznega bita v spremenljivkah D8, D9, D10 in D11 uporabljamo funkcijo bitRead(), ki uporablja dva argumenta: spremenljivko iz katere želimo prebrati stanje posameznega bita in redno številko tega bita.
Glavni program pišemo znotraj funkcije loop() s tem, da uporabimo isti algoritem, kot v Bascom-AVR rešitvi:
void loop() { D8 = 1; //rdeča D9 = 2; //zelena D10 = 4; //modra D11 = 3; //oranžna delay(1000); D8 = 2; //zelena D9 = 4; //modra D10 = 1; //rdeča D11 = 6; //turkizna delay(1000); D8 = 4; //modra D9 = 1; //rdeča D10 = 2; //zelena D11 = 5; //vijolična delay(1000); D8 = 7; //bela D9 = 0; //izklopljena D10 = 0; //izklopljena D11 = 7; //bela delay(500); D8 = 0; //izklopljena D9 = 7; //bela D10 = 7; //bela D11 = 0; //izklopljena delay(500); }
Arduino rešitev dodatne naloge
(program Shield-A_14b.ino)
Če želimo barve menjati slučajno, bomo izkoristit funkcijo random(). Funkcija loop() je sedaj precej enostavnejša:
void loop() { D8 = random(0,999); D9 = random(0,999); D10 = random(0,999); D11 = random(0,999); delay(100); }
Funkcija random() ni popolna in pri vsakem novem zagonu programa generira isti niz slučajnih številk. Obstajajo načini, s katerimi bi to lahko uredili, vendar je za naše potrebe dovolj dobra tudi takšna rešitev. Ostanek programa je enak programu Shield-A_14a.ino.
Za tiste, ki želijo vedeti več
Na isti način, na katerega smo izkoristili Timer2 za klicanje prekinitvene rutine, ki krmili multipleksiranje, Arduino IDE uporablja Timer0 za časovne funkcije millis() in delay(). V datoteki instalacijska_mapa_AdruinoIDE/hardware/arduino/avr/cores/arduino/wiring.clahko vidite, da je definiran faktor deljenja tajmerja nastavljen na 64, kot tudi, da se uporablja prekinitev glavnega programa takrat, ko se vrednost števca resetira iz 255 na 0, prav tako, kot je prikazano na sliki 42. Razlika je samo v tem, da se tukaj uporablja Timer0, ime prekinitvene rutine pa je drugačno. Z vsako prekinitvijo se povečuje vrednost pridružene spremenljivke za ena, z njo se uporabljata funkciji millis() in delay() za definiranje zadane zakasnitve. Opazili bomo, da se prekinitev ne izvršuje 1000-krat, ampak 976-krat v sekundi, in zato bo trajanje zakasnitve, ki smo ga zadali funkcijam millis() in delay() vedno malce daljše od pričakovane.
Bascom-AVR ne uporablja tajmerjev za svoje časovne ukaze Wait, Waitms in Waitus, pač pa programske zanke, ki trajajo nek določen čas. Potrebno število izvršitev zanke se določa med prevajanjem programa in je odvisno od zadanega časa zakasnitve in frekvence, na kateri dela mikrokontroler ($crystal). Tudi tukaj ni možno doseči točno zadanega časa, vendar so odstopanja praviloma manjša, kot pri Arduinu.
Opomba: Programi Shield-A_14a.bas, Shield-A_14a.ino, Shield-A_14b.bas in Shield-A_14b.ino se lahko dobijo brezplačno v uredništvu revije Svet elektronike.
Shield-A_Mitrovic_11