2014 m. lapkričio 26 d., trečiadienis

AVR ir asemblerio iššūkis II

Tęsiu vakar pradėtą temą apie užduoti asembleriu parašyti programą AVR šeimos mikrovaldikliui. Laikysiu, kad vakarykštį straipsnį esate perskaitę, taip pat esate įsidiegę bent jau WinAVR bei AVRStudio (rekomenduoju 4 versiją - mažiau apkraus kompiuterį, o funkcionalumo iki valiai) - šios programos bus reikalingos programos kodo įvedimui, kompiliavimui į HEX failą, be to, AVRStudio programoje įmanoma ir išbandyti veikimą stebint prievadų bei registrų būsenas. Idealiu atveju turėtumėte būti susikonstravę minėtame straipsnyje nurodytą schemą su trimis šviesos diodais, nes patikėkite - pamatyti realiai mirksinčius diodus, kuriuos mirksina paties parašyta programa, yra žymiai maloniau, nei stebėti ekrane vaizduojamus skaičiukus.


Taigi, įjungus AVRStudio programą matysime tokį vaizdą:

Paspaudus "New Project" atsiras sekanti lentelė:

Čia galima pasirinkti, kokia kalba rašysime programą mikrovaldikliui. Šiuo atveju pažymėtas "Atmel AVR Assembler" punktas, kitu gi atveju galima rašyti programą C kalba, tada reiktų pažymėti "AVR GCC". Nurodome projekto pavadinimą ir vietą. Aš paprastai liepiu sukurti atskirą katalogą kiekvienam projektui, nes paskui labai greit susimaišo viskas, ir po poros mėnesių norint atsirinkti būna pakankamai sudėtinga. Pavadinsime projektą "RGB1".
Jeigu neturite susikonstravę realaus mikrovaldiklio, ir jei norėsite paleisti programą vykdyti stebėdami registrų bei prievadų pokyčius - spauskite NEXT, kitu atveju - FINISH.
Paspaudus NEXT, atsiras simuliatoriaus pasirinkimo lentelė:

Čia reikia pasirinkti naudojamą mikrovaldiklio tipą, mano atveju - ATtiny2313 (tiesiog tokį turiu namie, todėl ir programą jam rašysiu). Ir tada jau FINISH.
Atsiras baltas langas, kuriame ir reikės rašyti programos kodą. Rašysime programą, aptartą vakar, t.y. trijų LED įjungimas vienas po kito - anksčiau mes tokius efektus vadindavome "bėganti šviesa" :) Programos kodas atrodo štai taip:

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
.include "tn2313def.inc";Nurodomas mikrovaldiklio tipas   .def laikinas = R16     ;registrams suteikiami prasmingesni vardai .def laikinas1  = R17.def laikinas2  = R18.def laikinas3  = R19 .equ raudonas = 0        ;prievado bitams suteikiami pavadinimai.equ zalias = 1.equ melynas = 2 .CSEG                     ;naudojama programine atmintis.ORG $0000                 ;programa prasideda nuo adreso 0x0000    rjmp  RESET         ;naudojamas tik vienas pertraukimas - RESET     RESET:                     ;programos pradzia                            ;nurodoma STACK vieta SRAM atmintyje:   ldi  laikinas,low(RAMEND)    ;i laikina registra irasomas SRAM adresas   out  SPL,laikinas    ;laikinas registras ikeliamas i specialuji registra SPL ;  Naudojamas prievadas PortB, 0, 1 ir 2 bitai nustatomi kaip isejimai;                   76543210   ldi  laikinas, 0b00000111   out  DDRB, laikinas                            ;uzgesinami visi sviesos diodai   cbi PortB, raudonas   cbi PortB, zalias   cbi PortB, melynas MAIN:                     ;pagrindines programos pradzia    sbi  PortB, raudonas    ;uzdegamas raudonas diodas   cbi PortB, zalias    ;uzgesinamas zalias diodas   cbi PortB, melynas    ;uzgesinamas melynas diodas   rcall velinimas    cbi  PortB, raudonas    ;uzgesinamas raudonas diodas   sbi PortB, zalias    ;uzdegamas zalias diodas   cbi PortB, melynas    ;uzgesinamas melynas diodas   rcall velinimas    cbi  PortB, raudonas    ;uzgesinamas raudonas diodas   cbi PortB, zalias    ;uzgesinamas zalias diodas   sbi PortB, melynas    ;uzdegamas melynas diodas   rcall velinimas      rjmp MAIN             ;grizimas i pagrindines programos pradzia VELINIMAS:                ;velinimo paprograme ldi laikinas1, 100        ;i registra laikinas1 ikeliamas skaicius 100 zyme1:                    ;programos vietos zyme ldi laikinas2, 50        ;i registra laikinas2 ikeliamas skaicius 50   zyme2:                ;programos vietos zyme   ldi laikinas3, 20    ;i registra laikinas2 ikeliamas skaicius 50     zyme3:    ;programos vietos zyme     dec laikinas3;skaicius registre laikinas3 sumazinamas vienetu     brne zyme3;jeigu registras laikinas3!=0, programa persoka i zyme3   dec laikinas2 ;skaicius registre laikinas2 sumazinamas vienetu   brne zyme2;jeigu registras laikinas2!=0, programa persoka i zyme2 dec laikinas1;skaicius registre laikinas1 sumazinamas vienetu brne zyme1;jeigu registras laikinas1!=0, programa persoka i zyme1    ret     ;jeigu registras laikinas1=0, programa grizta i iskvietusia programa

Oi, koks didelis ir baisus programos kodas gavosi :) Ir viso labo tai tik trys mirksintys diodai :) Tačiau, kaip sako protingi žmonės, akys bijo - rankos daro. Tai prie tų darbų, o jei konkrečiau - prie kiekvienos eilutės aiškinimo.
Nusikopijavę šią programą į AVRStudio pamatysite kiek kitaip paspalvotus žodžius - man asmeniškai gražiau, nei šis kodo paspalvojimas, tačiau dabar neturiu nei laiko, nei noro ieškot, kaip pakeisti spalvas.  Žodžiu, nukopijuotą programos kodą dar reikės sukompiliuoti, kas reikalauja
vienintelio mygtuko paspaudimo: F7. Apatinėje lango dalyje atsiras tekstas apie kompiliavimo būseną, ir turėtumėte pamatyti galutinį verdiktą:



Matome, keik sunaudota atminties baitais ir procentais, ir linksmiausia eilutė - "Assembly complete". Projekto kataloge atsirado failas RGB1.HEX, kurį ir reikia įrašyti į mikrovaldiklį (mūsų atveju - bent jau atiduoti dėstytojui :) ). Tačiau apie tai - vėliau, dabar gi aiškinu programos tekstą.

1
.include "tn2313def.inc";Nurodomas mikrovaldiklio tipas

Su tašku prasidedantys žodžiai dar nėra programa - tai tik direktyvos kompiliatoriui. Šia eilute nurodomas naudojamo mikrovaldiklio tipas, kompiliatorius tuo vadovaudamasis atsidaro failą nurodytu pavadinimu, kuris instaliuojamas kartu su AVRStudio. Tame faile nurodomi atminties adresai, I/O prievadų kiekis, atminties kiekiai ir kiti dalykai, kuriuos prisiminti būtų nelengva. Naudojant šią direktyvą daug lengviau kodą, veikiantį mažesniame mikrovaldiklyje, perkelti į didesnį - tereikia direktyvoje nurodyti kitokį valdiklio tipą.

5678910
.def laikinas = R16     ;registrams suteikiami prasmingesni vardai .def laikinas1  = R17.def laikinas2  = R18.def laikinas3  = R19 

Programoje galima ir tiesiogiai nurodyti naudojamus registrus, tačiau suteikus jiems kažkokius pavadinimus tiek rašyti, tiek skaityti programą daug lengviau. Be to, jeigu dėl kokių nors priežasčių ateityje norėsime naudoti kitą registrą, užteks jį pakeisti tik pačioje programos pradžioje. Kitu atveju reikėtų pakeisti visur, kur tas registras buvo naudotas. Vardai gali būti duodami bet kokie, tačiau rekomenduočiau labai neįsifantazuoti :) Visas tokio vardų suteikimo grožis pasimato, kai po poros metų grįžti prie programos kodo ir paskui spėlioji - ką galėtų reikšti, takim, "lknprm" registras, kai analogiškas laikinas1 atrodo daug išvaizdžiau.

111213
.equ raudonas = 0        ;prievado bitams suteikiami pavadinimai.equ zalias = 1.equ melynas = 2

Čia - panaši istoriją, kaip ir su registrų pavadinimais. Vėlesniame programos tekste matysis, kad daug aiškiau skaityti, kai nurodomas pavadinimas, o ne prievado bito numeris. Taip pat galioja ir ateities keitimų situacija: jeigu aš po kiek laiko, kai programa sieks ne šimtą ie ne du šimtus eilučių, sugalvosiu siodus perjungti prie kitų išvadų, man užteks tai pakeisti tik programos pradžioje.
Reikia atkreipti dėmesį, kad tokio tipo prilyginimai rašomi atvirkščiai, nei šnekamojoje kalboje: .equ raudonas = 0 reiškia, kad nuliniam ("0") bitui suteikiamas vardas "raudonas".

1516
 .CSEG                     ;naudojama programine atmintis.ORG $0000                 ;programa prasideda nuo adreso 0x0000

paskutinės direktyvos, kurios mūsų atveju galėtų būti ir praleistos - kompiliatorius jas paskirs pagal nutylėjimą. .CSREG reiškia, kad naudojamas programinės atminties segmentas (yra dar .ESEG - EEPROM segmentas bei .DSEG - SRAM segmentas). .ORG $0000 nurodo, kad programos kodą kompiliatorius turi pradėti rašyti nuo 0x0000 adreso, t.y. nuo pačios atminties pradžios (0x0000 reiškia, kad tai yra skaičius 0000 šešioliktainiu formatu, kaip kad 0b00000000 yra dvejetainiu). Nenurodžius šių direktyvų paprastoje programoje nieko nenutiks, tačiau rašant didesnės apimties programas jų gali prireikti.

17
 rjmp  RESET         ;naudojamas tik vienas pertraukimas - RESET 

Čia surašomi visi naudojami pertrauklimo vektoriai. Pertraukimas yra signalas, kurį gavęs mikrovaldiklis peršoka į nurodytą vietą. Pertraukimai gali būti išoriniai ir vidiniai, šiuo atveju naudojamas vienintelis RESET pertraukimas. Kitų vidinių pertraukimų pavyzdys gali būti, pavyzdžiui, taimerio perpildymas - tarkim, 8 bitų taimeris, kuris gali suskaičiuoti iki 255, pasiekęs didžiausią reikšmę, "persiverčia" į nulį. Šis "persivertimas" gali būti naudojamas pertraukimui ir peršokimui  į kažkokią programos vietą. Tokiu principu galima daryti tikslius vėlinimus, nes programuojant asembleriu žinomas tikslus programos instrukcijų vykdymo laikas. Kitas pavyzdys gali būti, pavyzdžiui, duomenų per UART išsiuntimas -
pavykus išsiųsti duomenis generuojamas pertraukimas, programa peršoka į nurodytą paprogramę, tarkim, uždegti šviesos diodo, ir grįžta į pagrindinę programą. Išorinių pertraukimų pavyzdžių mažiau. Čia naudojamame mikrovaldiklyje yra du išvadai, INT0 ir INT1, kuriuos prijungus prie maitinimo minuso arba maitinimo pliuso galima generuoti pertraukimą. Realus pavyzdys - programa, laukianti mygtuko paspaudimo. Galima tai daryti amžiname cikle, pastoviai užklausinėjant prievadą apie jo būseną, tačiau tada valdiklis negalės užsiimti niekuo, taip švaistydamas laiką. Naudojant pertraukimą galima vykdyti programą ir nelaukti, kada bus paspaustas mygtukas - esant pertraukimui programa bet kuriuo metu peršoks į nurodytą paprogramę.

19
RESET:                     ;programos pradzia

Nuo čia prasideda programos pradžia. Taip pat čia peršoka programa, gavusi RESET pertraukimą (jis gali būti tiek išorinis, tiek vidinis, tačiau dabar nesigilinsiu).

212223
                    ;nurodoma STACK vieta SRAM atmintyje:ldi  laikinas,low(RAMEND)    ;i laikina registra irasomas SRAM adresasout  SPL,laikinas    ;laikinas registras ikeliamas i specialuji registra SPL
Prasideda painiava :) Mikrovaldikliuose yra toks daiktas kaip stekas (nežinau, kaip kalbininkai jį siūlytų vadinti, bet nesivarginsiu laužyti liežuvio naujadarais :) ). Labiausiai į temą vertimas yra turbūt krūva: tai vieta, kurioje gali būti kažkas saugoma, tokia lyg laikonoji saugykla. Tarkim, ten galima pasidėti laikiną skaičių, jeigu visi registrai užimti, taip pat programoje naudojant paprogrames (rcall mūsų atveju) prieš programai peršokant į nurodytą paprogramę steke yra išsaugoma esama programos padėtis, kad kai paprogramė baigsis (komanda ret), programa žinotų, kur grįžti. Duomenys į steką yra rašomi tam tikra tvarka, kuri vadinasi Last In First Out , t.y. paskutinis į steką įrašytas skaičius bus išimtas pats pirmas (individualiai adresuoti įdėtų duomenų nepavyks). Analogija į vamzdį kišamiems kamuoliukams: pirmą įdėtą kamuoliuką pavyks išimti tik paskutinį.
Taigi, mes norime steko atminčiai naudoti SRAM atmintį, kurios mes turime netgi 128 baitus. Tam mes į laikiną registrą komandos ldi (LOAD IMMEDIATE) pagalba įrašome SRAM atminties pradžios adresą. Na, mes jo nežinome, bet čia pasitarnauja pačioje pradžioje įrašyta direktyva, nurodanti mikrovaldiklio tipą. NUrodytame faile kompiliatorius randa atminties adresą, o mums užtenka įrašyti tik RAMEND, visą kitą padaro kompiliatorius. Kadangi mūsų mikrovaldiklis turi mažiau nei 256 baitus atminties (255 - didžiausias skaičius, kurį galima įrašyti į 8 bitų registrą), adresas tebus 8 bitų, todėl, turėdami aštuonių bitų registrą SPL komanda out ir įrašome į jį laikinojo registro turinį. Jeigu atminties daugiau, nei 256 baitai - reikia naudoti dviejų 8 bitų registrų porą, kur į vieną registrą įrašomi pirmi 8 adreso bitai, į kitą - sekantys 8 bitai. Tokiu atveju programos kodas atrodytų taip:

LDI laikinas, HIGH(RAMEND) ; Upper byteOUT SPH,laikinas ;irasomi pirmi 8 bitaiLDI laikinas, LOW(RAMEND) ; Lower byteOUT SPL,laikinas ; irasomi paskutiniai 8 bitai

Toliau seka prievado bitų krypties nurodymas:

2728
ldi  laikinas, 0b00000111out  DDRB, laikinas

Prievadų kryptis (įėjimas arba išėjimas) nurodomi į tam skirtą registrą įrašant 1 arba 0. Kiekvienas prievadas turi savo krypties registrą, mūsų atveju reikalingas PortB krypries registras DDRB (DataDiRection portB), Rašoma saštuonių bitų skaičius, kur kiekvienas bitas nurodo konkretų prievado bitą, taigi ir mikroschemos koją. Mums reikia prijungti diodus prie PB0, PB1 bei prie PB2, todėl į DDRB registrą rašomas dvejetainis skaičius 0b00000111 - pirmi trys bitai yra vienetai, todėl PB0...2 tarnaus kaip išėjimai, t.y. mes galėsime valdyti šviesos diodus. Kitus galima laikyti kaip ir neapibrėžtus, nes jie bus naudojami programatoriui prijungti, tačiau normaliomis sąlygomis nustatant bitą lygų nuliui turėsime įėjimą, pavyzdžiui, prijungti mygtuką. O kode procedūra jau turėtų būti aiški: į laikiną registrą įsikeliame skaičių, tada tame laikinajame registre esančią reikšmę rašome į DDRB.

313233
cbi PortB, raudonascbi PortB, zaliascbi PortB, melynas

Prieš pradedant vykdyti pagrindinę programą pravartu visus bitus nustatyti į pradinę padėtį, todėl šiomis eilutėmis į atitinkamus prievado bitus įrašomi nuliai (komanda cbi - Clear Bit Input/output). Kodėl prie komandos rašoma "raudonas", "zalias" ir "melynas", manau, patys suprasite :)

35
MAIN:         ;pagrindines programos pradzia

Nuo čia prasideda pagrindinė programa. Žodelį "MAIN:" galima laikyti žyme, į kurią peršoksime vykdydami amžiną ciklą.

373839404142434445464748495051525354
sbi  PortB, raudonas    ;uzdegamas raudonas diodascbi PortB, zalias    ;uzgesinamas zalias diodascbi PortB, melynas    ;uzgesinamas melynas diodasrcall velinimas cbi  PortB, raudonas    ;uzgesinamas raudonas diodassbi PortB, zalias    ;uzdegamas zalias diodascbi PortB, melynas    ;uzgesinamas melynas diodasrcall velinimas cbi  PortB, raudonas    ;uzgesinamas raudonas diodascbi PortB, zalias    ;uzgesinamas zalias diodassbi PortB, melynas    ;uzdegamas melynas diodasrcall velinimas   rjmp MAIN     ;grizimas i pagrindines programos pradzia

Ši programos dalis pakankamai aiškiai pakomentuota: komanda sbi įjungia nurodytą bitą (SetBit), komanda cbi jį išjungia. Taip žaisdami mes gauname paeiliui užsidegančius šviesos diodus. Paskutinė komanda rjmp MAIN vykdo amžiną ciklą, programą nukreipdama į pagrindinės programos pradžią (35 eilutė). Tačiau jeigu diodai bus junginėjami visiškai vienas paskui kitą, be jokių pauzių, tai turint omenyje mikrovaldiklio greitį nieko įdomaus mes nepamatysime. Tam reikalingas vėlinimas. Šiuo atveju jis iškviečiamas po kiekvieno diodo uždegimo / užgesinimo etapo (rcall velinimas). Programa, išsisaugojusi esamą vykdymo adresą steke, peršoka į nurodytą paprogramę, pavadintą "velinimas", kuri prasideda nuo 58 eilutės.


56575859606162636465666768697071
VELINIMAS:                ;velinimo paprograme ldi laikinas1, 100        ;i registra laikinas1 ikeliamas skaicius 100 zyme1:                    ;programos vietos zyme ldi laikinas2, 50        ;i registra laikinas2 ikeliamas skaicius 50   zyme2:                ;programos vietos zyme   ldi laikinas3, 20    ;i registra laikinas2 ikeliamas skaicius 50     zyme3:    ;programos vietos zyme     dec laikinas3;skaicius registre laikinas3 sumazinamas vienetu     brne zyme3;jeigu registras laikinas3!=0, programa persoka i zyme3   dec laikinas2 ;skaicius registre laikinas2 sumazinamas vienetu   brne zyme2;jeigu registras laikinas2!=0, programa persoka i zyme2 dec laikinas1;skaicius registre laikinas1 sumazinamas vienetu brne zyme1;jeigu registras laikinas1!=0, programa persoka i zyme1    ret     ;jeigu registras laikinas1=0, programa grizta i iskvietusia programa

Vėl painiava... :)Iš tikro tai visai ne. Komentaruose viskas paaiškinta, tereikia įsigilinti. Vyksta trys ciklai, vienas kito viduje, t.y. ciklas cikle. Analogija - elementarus skaitliukas, kaip kad automobiliuose kilometrams matuoti. Kiekvienas ratukas prilygsta vienam ciklui. Tarkim, šimtų metrų ratukas turi apsisukti 10 kartų, kad kilometrų ratukas apsisuktų 1 kartą, o tam, kad apsisuktų vieną kartą dešimčių kilometrų skaitliukas, kilometrų skaitliukas turi apsisukti 10 kartų, o šimtų metrų - 100 kartų. Taip ir su šiuo vėlinimu: 63-65 eilutės atitinka šimtų metrų skaitliuką, 61-67 - kilometrų, 59-69 - dešimčių kilometrų. Vyksta viskas paprastai: 58 eilutėje laikinas1 registrui priskiriamas skaičius 100, tada 60 eilutėje laikinas2 priskiriamas 50 ir 62 eilutėje priskiriamas 20 registrui laikinas3. Prasideda ciklas: 64 eilutėje laikinas3 sumažinamas vienetu, 65 žiūrima, ar jis lygus nuliui. Jeigu ne, programa žoka į pažymėtą vietą "zyme3", ir taip sukasi, kol registre "laikinas3" skaičius sumažėja iki nulio. Tada 65 eilutėje tikrinant randamas nuli, ir programa, neperšokusi į "zyme3" eina toliau. 66 eilutėje sumažinamas skaičius registre "laikinas2", ir tikrinamas, ar jis nėra lygus nuliui. Kadangi jis kol kas lygus 49, programa šoka į "zyme2", po kurios iš naujo nustatomas registras "laikinas3", ir pats mažiausias ciklas turi apsisukti 20 kartų, kol "laikinas2" vėl bus sumažintas vienetu. Iš viso iki "laikinas2" nulio mažiausias ciklas turi "prasisukti" 50*20=1000 kartų. Tai nėra tiek daug, kai mikrovaldiklis per sekundę tokių apsisukimų gali padaryti iki 8 milijonų! Tačiau čia yra dar trečias ciklas, kurio registrui "laikinas1" sumažinti iki nulio reikia 100*50*20=100.000 mažojo ciklo apsisukimų, o tai jau pastebimas vėlinimas.
Registrui "velinimas1" sumažėjus iki nulio programa gauna komandą grįžti į pagrindinę programą (ret). Iš steko imamas buvęs vykdymo adresas ir į jį grįžtama.

Suprantu, kad iš pirmo žvilgsnio suprasti programą tikrai nėra paprasta, ypač neturint jokios programavimo patirties, tačiau įsigilinus viskas turėtų pasirodyti kur kas paprasčiau.
Dar, pabaigai, šiek tiek apie komentarus. Visur labai rekomenduojama, aš taip pat paantrinsiu, kad būtina išsamiai ir tvarkingai komentuoti visą programos tekstą - visų pirma dėl savęs, kad sugrįžus prie kodo būtų galima nesunkiai suigaudyti, kas ir kur daryta. O ir rodant kodą kam nors daug paprasčiau suprasti ir paaiškinti. Asembleryje komentarai rašomi po kabliataškio, galima komentuoti kiek nori ir kaip nori. Pradžiai, o ypač sudėtingesnėse programose komentuokite kiekvieną eilutę, nes kai, trkim, vienu ciklu keičiami keli registrai, o dar jeigu ciklas cikle - pakankamai sudėtinga netgi iškart susigaudyti, nekalbant apie vėlesnį grįžimą prie kodo.
Dabar, jei neturite realaus mikrovaldiklio, programą galima simuliuoti AVRStudio aplinkoje. Tam reikia pasirinkti tokius meniu punktus:
Debug -> Start Debugging
Dešinėje lango pusėje susiraskite "PORTB", ant jo paspaudę kiek žemiau pamatysite kvadratėlius, kurie simbolizuoja atitinkamus prievado bnitus.
Tada pasirinkite Debug->Auto Step ir pamatysite kažkokį veiksmą.
Rekomenduoju prieš simuliuojant veikimą susimažinti ciklų skaičių, nes teks ilgai laukti :) Pvz., 58 eilutėje laikinas1 priskirti 1, 62 eilutėje laikinas 2 irgi 1, o laikinas3 palikti tą patį.

Na va. Pirma programa parašyta. Gal kiek prašviesėjo? Man - taip! :) Gero programavimo - likusias užduotis, manau, pasidarysite patys!





Nuorodos:
http://www.avr-asm-tutorial.net/avr_en/index.html
http://www.avrbeginners.net/
Visų AVR ASSEMBLER instrukcijų išsamus aprašymas
ATtiny2313 aprašymas (datasheet)

Komentarų nėra:

Rašyti komentarą