Az előző két bejegyzésemben végigmentünk az elvi részleteken, most pedig a programkód ismertetése következik. Tervezek egy negyedik részt is, ahol az eddig általam észrevett kisebb problémák, furcsaságok, azok okait és lehetséges megoldásait vetem fel, ill. a jövőbeli esetleges fejlesztésekről lesz szó. A projekt és a sorozat természetesen ezután sem lesz lezártnak tekintve, amint publikálásra érdemes fejlesztés következik, az publikálva is lesz. Azt az aprócska dolgot se felejtsük, hogy a készülék még mindig fejlesztési stádiumban van, azaz nincs konkrét késznek vett változata, csak próbanyákos tesztelés alatt álló. Ez azonban nem jelenti azt, hogy ebből az állapotából valaki ne építse készre és ne fogja hadra, mert ehhez már most is minden feltétel adva van.
A kód ismertetése alatt bizonyos részletekbe most se kívánok elmerülni, így a kódban lévő pl. LCD kijelző inicializáló és meghajtó rutinok csak érintve lesznek, kivesézve nem. Van a kódban egy 16 bites osztó algoritmus, amit szintén nem ebben a cikkben kívánok ismertetni, ilyesmire külön cikksorozatot fogok indítani a későbbiekben.
Eszközválasztás
A három eszköz közül, amit rendeltem az ATtiny2313 kiesik, mivel nincs A/D konvertere. Az ATmega8A és ATmega88PA közül a 8A az olcsóbb (és nem is kicsit, vagy inkább a 88PA az, amit kicsit túláraztak a képességein), a fejlesztés tehát ezzel az olcsóbb eszközzel indult. Azonban a sebesség most döntött; a mega8A legfeljebb 16MHz-en, a mega88PA pedig 20MHz-el is tud ketyegni. És bizony 20MHz-nél feljebb tudunk lépni egy szintet; 16 helyett 32 előosztást beállítva az ADC órajelre, 48kHz analóg mintavétellel kétszer több műveletre képes két mintavétel között. (az eddigi 208 helyett 416 órajel áll rendelkezésre) Most tehát fejlesztési időszakban ezen az eszközön dolgozom tovább, de az itt publikálásra kerülő verzió a mega8A-n is elfut, ha 10MHz órát adunk neki és visszaállítjuk 16-ra az ADCclk előosztót. (16MHz-en 32-es előosztóval sajnos még csak 38kHz körüli az analóg mintavét, de pl. ha kicsit 10MHz fölé emelve és a 16-os előosztóval tudunk egy enyhe túlmintavételezést is csinálni, ami aliasing frekik szempontjából kedvező lehet, és igazából elég lesz az a 208 CPUórajel/analóg mintavét) Hangdobozépítős weblapomon amúgy a programkód egy jóval korábbi változata elérhető, és az még az ATmega8A-ra van kódolva, de én mégis inkább ennek a mostani frissebb kódnak a tanulmányozását javasolnám. (sok dolog változott azóta, az IIR szűrő már nem 40 bites akksival működik, csak 24-el, ennek következtében több regiszter felszabadult, van zéró blanking a számkijelzésben, overload kijelzés, finomodott, tisztult a kód is.)
A forráskód részenkénti ismertetése
Először fussunk végig a változókon! Minden változó regiszterben van a következő kiosztás szerint:
;Regiszterkiosztás: (változókezelés a memóriában nincs, minden változó regiszterben van tartva)
; R0 ; nincs elnevezve, szorzás eredményregisztere (gyárilag)
; R1
.def samplePK = R2 ; mintavételezett csúcsérték gyujto (eloosztó ciklusban gyujti be a max értéket)
.def filter2SAL = R3 ; IIR szuro kimenete (lefelé 1 bájttal kitolt számtartomány, pontatlan volt enélkül)
.def filter2PK = R4 ; Csúcsérték (10s kitartott ill. kijelzett)
.def filter1PK = R5 ; Csúcsérték (aktuális (FS leosztott) érték)
.def sampleSA0 = R6 ; sampled sqrt avg - mintavételezett négyzetes átlag gyujto (eloosztó ciklusban) 32 bites egész
.def sampleSA1 = R7
.def sampleSA2 = R8
.def sampleSA3 = R9
.def filter1SA0 = R10 ; Leosztott effektív telj (leosztott átlag) alsó 16 bit (IIR szuro bemenete)
.def filter1SA1 = R11
.def divRemL = R12 ; osztó rutin maradékérték eredményregisztere (16bit)
.def divRemH = R13
; R14
; R15
.def filter2SA0 = R16
.def filter2SA1 = R17
.def temp = R18 ; két db ált. célú munkaregiszter
.def temp2 = R19
.def holdTimer = R20 ; csúcstartás idozíto regisztere
.def divDenL = R21 ; osztó rutin hányados (osztó) bemeneti regiszter (16 bites)
.def divDenH = R22
.def delayTimer = R23 ; késlelteto rutinok számlálóregisztere
; R24
; R25
.def divNumL = R26;X osztó rutin osztandó szám és kimeneti regiszter (16 bites)
.def divNumH = R27
.def samplerCntL = R28;Y mintavevo eloosztó IRQ rutin mintaszámlálója
.def samplerCntH = R29
; R30;Z nincs elnevezve, memóriakezeléshez használatban
; R31
A korai változathoz képest látjuk, hogy nincs elnevezve az R14, R15 és R24, R25 regiszter, ezeket sikerült felszabadítani az időközben az IIR szűrőn eszközölt fejlesztésekkel. A regiszterkiosztás nagyban hasonlít a spektrumanalizátors kódéban már megismerttel. A sample kezdetű regiszterek a mintavételi munkaregiszterek, azaz samplePK a csúcsértékdetektor 8 biten, sampleSA az átlagoló összegzőregiszter 32 biten. A filter kezdetű regiszterek a digitális szűrők kimeneti regiszterei, ennek megfelelően filter1 az 1. szűrő (ez a csak összegző/átlagoló előszűrő) és filter2 a 2. szűrő kimenete (a 10s időállandójú IIR szűrőé) Az elnevezések a következő szabályok szerint történtek: filter[1/2][PK/SA][bájt szám], azaz filter után a szűrő száma 1 vagy 2, az hogy csúcs vagy négyzetes átlag (PK mint peak vagy SA mint squrase average) ill. a több bájt hosszú regiszterek esetén számozás, mely L,0,1,2,3 értékeket vehet fel. (az L egy alacsony törtrészes rész) Talán lehetne kicsit következetesebb is az elnevezésrendszer, ha most újra kellene írni az egészet a nulláról, talán kicsit más lenne. Amivel kell még foglalkoznunk, az a samplerCntL ill samplerCntH regiszterek, mely egy 16 bites szoftveres számláló, ebben számoljuk az 1. szűrő beösszegzett minták számát. Ez a számláló egy kezdeti értékről számol visszafelé addig, amíg el nem éri a nullát. Ha átnézzük a fenti kódrészletet, akkor igazából a többi regiszter nagyjából önmagáért beszél, nem kell külön kommentálni. Az osztórutinnak ill. a késleltető rutinoknak vannak fenntartva regiszterek, és az általános munkaregiszterek vannak még. Az R0 és R1 regiszter név nélkül van több helyen felhasználva, ugye ebbe keletkezik pl. a szorzó utasítás eredménye. Az R30 és R31 azaz a Z regiszter programmemória pointernek van felhasználva szövegkiíratásra. Ami talán még szót érdemel, az a holdTimer regiszter, mely a csúcsjel 10s csúcstartásának időzítőregisztere, ebből fakadóan fontos része a működésnek.
Érdemes lehet átfutni a forráskód konstansait is:
.equ osszszamlL = low(2048) ; eloosztó számláló
.equ osszszamlH = high(2048)
.equ holdTime = 235 ; 10 sec csúcsérték tartás (leosztott 23Hz-es órajelben)
.equ displayTimerValue = 8 ; LCD kijelzés lassító olvashatóság miatt (25Hz-hez számol)
.equ posLine1 = $80 ; line1 kurzorpozíció parancsa
.equ posLine2 = $80+$40 ; line2 kurzorpozíció parancsa
.equ posPP = $80+10 ; peak power eredmény LCD pozícióparancs
.equ posLTP = $80+$40+10 ; long term pwr eredmény LCD pozícióparancs
; LCD vezérlés portok
.equ lcdPort4 = portD
.equ lcdPort5 = portD
.equ lcdPort6 = portD
.equ lcdPort7 = portB
.equ lcdPortE = portB
.equ lcdPortRS = portB
.equ lcdDdr4 = ddrD
.equ lcdDdr5 = ddrD
.equ lcdDdr6 = ddrD
.equ lcdDdr7 = ddrB
.equ lcdDdrE = ddrB
.equ lcdDdrRS = ddrB
.equ lcd4 = portD5 ; 11 láb - LCD egység csatlakozásai a uC-hez
.equ lcd5 = portD6 ; 12 láb LCD modul D0-D3 adatlábak és az
.equ lcd6 = portD7 ; 13 láb R/W láb GND-hez van kötve!
.equ lcd7 = portB0 ; 14 láb
.equ lcdE = portB1 ; 15 láb
.equ lcdRS = portB2 ; 16 láb
Az osszszamlL és osszszamlH az 1. szűrő összegző konstansa, 2048 érték van benne két bájtra bontva. A holdTime konstans a holdTimer időzítő regiszter kezdőértéke (talán nem túl szerencsés, hogy csak a végén az r betű különbözteti meg őket, de most már így marad) Lényegében ennyi 23Hz-en futó ciklusig tartja ki a csúcsértéket. A displayTimerValue az LCD kijelző frissítő lassító konstans, szintén a 23Hz-hez időzítve. (Néhol 25Hz szerepel a kommentekben, de a jelen verzióban ezek 23-nak tekinthetők, a korábbi változat 10.7MHz-es órajelű és 51.44kHz mintavételű változatokból visszamaradt, nem frissített értékek.) A beállított konstanssal 3Hz a kijelzőn megjelenő számértékek frissítési frekvenciája, ezzel tényleg szépen olvasható, a korábbi 4Hz kicsit gyors volt, de a 2 már nekem indokolatlanul lassúnak látszott. A többi konstans az LCD vezérlés részei, ezekkel ebben a cikkben nem kívánok foglalkozni, van róla bőséges anyag a neten, ill. talán majd egy későbbi cikk szólhat kizárólag erről.
Az 1. szűrő a mintavételezővel – IRQ rutin
Következzen a mintavételező és az 1. szűrőt működtető megszakítási rutin:
; *** sampler (irq rutin) ***
;
; Ugró utasítás nélkül közvetlenül a ugróvektortól kezdve!
;
.org ADCCaddr
push R0 ; mul eredményreg mentés (mert a main is hasznlája)
push R1
in R0, SREG ; SREG mentése
push R0
push temp ; temp ált. reg mentése
lds R0,ADCL ; 10 bites ADC beolvasása
lds temp,ADCH
lsl R0 ; balra rotál, C<<elojel a hasznos bitek most kitölrtik az egész bájtot
rol temp
brcs PC+4 ; polaritásvizsgálat
neg temp ; negáljuk
brcs PC+2 ; ha átfordult (-256) akkor klippeltetjuk (-255)
dec temp ; ezzel a -256 +1 bitet igénylo értéket -255-be írjuk át
cp samplePK, temp ; csúcsértékkezelés (nemnégyzetes!)
brcc PC+2
mov samplePK, temp ; új csúcsérték rögzítése
mul temp, temp ; négyzetemelo szorzás
clr temp
add sampleSA0, R0 ; filter1 szuro: átlagoló összegzés
adc sampleSA1, R1
adc sampleSA2, temp
adc sampleSA3, temp
pop temp
pop R0 ; vigyázat, ebben még csak az SREG van, nem az eredeti tartalma!
pop R1 ; R1 ezzel viszont visszaállítódott
; összegzés számláló kezelése
sbiw samplerCntL, 1 ; összegzés számláló léptetés
brne skip_sampler_1 ; számláló lejárt figyelés
; a számláló lejárt -> mérési eredmény mentése (átadása a foprognak)
ldi samplerCntH, osszszamlH ; összegzés számláló újraindítása
ldi samplerCntL, osszszamlL
lsr sampleSA3 ; kerek bájtra igazítás
ror sampleSA2
ror sampleSA1
lsr sampleSA3
ror sampleSA2
ror sampleSA1
lsr sampleSA3
ror sampleSA2
ror sampleSA1
mov filter1SA0, sampleSA1 ; kerek bájt átírás a kimenetre
mov filter1SA1, sampleSA2
mov filter1PK, samplePK ; csúcsérték átírása a kimenetre
clr sampleSA0 ; munkaregiszterek nullázása
clr sampleSA1
clr sampleSA2 ; sampleSA3 kinullázódik a bittolások alatt
clr samplePK
out SREG, R0; SREG visszaállítása
pop R0 ; R0 visszaállítása
set ; T jelzõbít be (kész az elõosztott minata)
reti
skip_sampler_1:
out SREG, R0; SREG visszareállítása
pop R0 ; R0 visszaállítása
reti
A megszakítási vektortáblába nem került ugró utasítás, hanem magán a vektorcímen kezdődik. Nyilván ezt csak akkor lehetséges megcsinálni, ha a vektortábla soron következő megszakításai nincsenek használatban. Elsőként vermeljük a használni kívánt regiszterek egy részét (amit a main program is használ) valamint a státusz regisztert. Beolvassuk az ADC konverter eredményregiszteréből a 10 bites mintát. Nincs külön erre a célra dedikált regiszter, azt használjuk rá, ami van (itt most R0 és temp). Elrotáljuk balra, amivel az előjelről árulkodó legnagyobb helyiértékű bit a carry-be kerül, ill az R0 alsó bájtból, ami csak a két alsó bitet tartalmazza, egy bit felrotálódik a tempbe. R0 ezután el lesz dobva, a carry-vel együtt így 9 bites mintával folytatódik a játék. (a 10-ik bit amúgy sem beszámítható, mivel overclockban megy az A/D konverter) Ha carry-be 1 került, akkor a pozitív félhullámba esett a minta, és a temp lényegében ennek abszolút nagyságát tartalmazza, így tobább lehet ugrani a brcs utasítással. Ellenkező esetben bejárjuk az if ágat, ahol előállítjuk a negatív félhullámba eső abszolút nagyságot. Ez az érték 1-256 közé eshet, a 256 esetében 0 a regiszter értéke, amit 255-re módosítunk, így (1-255) értéktartományra csonkolunk. Lényegében a 255-ös nagyságot amúgy is már overload eseménynek fogjuk tekinteni, tehát az OVER! felirat fog majd megjelenni a kijelzőn. A következő három sor csúcsérték-detektálást végez a samplePK változóba. Következik a négyzetreemelő szorzás a mul parancs segítségével. Mivel itt már nem előjeles az értékünk, hanem abszolút nagyságot tartunk számon, így az előjel nélküli szorzást lehet alkalmazni. A szorzás bemenete 8 bites, eredménye pedig 16 biten jelenik meg, és mind a 16 bitet fel is használjuk az összegzésben. (tehát ezt itt még nem csonkoljuk kisebbre) Elvégezzük az összegzőgyűjtést, azaz hozzáadsjuk a szorzás 16 bites eredményét a 32 bites sampleSA összegzőregiszterhez. Mivel a temp, R0 és R1 regiszterekre a továbbiakban már nincs szükség, visszaállítjuk őket a veremből. Célszerűnek látszana ezt a rutin végére írni, de a rutin a továbbiakban kettéágazik, és az IF és ELSE ágnak külön kilépője van, ezért duplázni kellene ezt a kódrészt, ez az oka, hogy az IF elé került. Jön tehát az 1. szűrő összegző számlálójának kezelése, erre egy nagyon célszerű megoldás, hogy a word-ös (16 bites) kivonó művelettel veszünk belőle el 1-et, ugyanis ez 16 bitesen kezeli a státuszregiszter bitjeit is, így pl. a Z bit csak akkor lesz 1, ha a 16 bites érték 0, azaz mindkét bájtja nulla. Ha nem járt le az időzítőnk, akkor az ELSE ágra ugrunk egy nagyobbacskát, ahol visszaállítjuk a státusz regisztert és a maradék felhasznált regisztereket és kilépünk a megszakítási rutinból. Amennyiben lejárt a számláló, úgy az IF ágban elvégezzük az ekkor fellépő “adminisztrációt”; átírjuk a sample változók tartalmát a kimeneti filter1 regiszterekbe, nullázzuk a sample regisztereit a következő összegző és csúcskereső ciklusra, és újraindítjuk a számlálót a kezdőérték beállításával. Elsőnek pont az utolsót látjuk, azaz a számlálót, majd az előző részben említett 16 bitre faragás mellett a 32 bites összegző akksi kerül át a 16 bites filter1SA-ba, de ezt talán egy ábrán jobban lehet látni. Tehát, a legnagyobb összegzett érték 133171200, ami binárisan 27 számjeggyel írható le, de 32 bites regiszterben gyűlt össze, azaz az 5 legfelső helyiérték mindig nulla.
bájt 3 bájt 2 bájt 1 bájt 0 [00000XXX][XXXXXXXX][XXXXXXXX][XXXXXXXX]
Ezt elshifteljük jobbra hárommal, és a két középső 16 bitet vesszük ki belőle:
bájt 3 bájt 2 bájt 1 bájt 0 [00000XXX][XXXXXXXX][XXXXXXXX][XXXXXXXX] [000000XX][XXXXXXXX][XXXXXXXX][XXXXXXXX] X [0000000X][XXXXXXXX][XXXXXXXX][XXXXXXXX] XX [00000000][XXXXXXXX][XXXXXXXX][XXXXXXXX] XXX ↓ ↓ [XXXXXXXX][XXXXXXXX]
Bízom benne, hogy érthető az ábra, látjuk, hogy ha a 32 bites értéket hárommal megtoljuk jobbra, akkor a 3-as bájt nullán lesz, a 2 bájtba kerül az MSB bit, ill a 0 bájt után lemorzsolódik 3 bit. Lényegében a 0-ás bájtot már nem is kell bevennünk a műveletbe, azaz csak a 3-2-1 bájtokon rotálunk. A fenti művelet amúgy teljesen ekvivalens a 2048-al való egész-osztással. Miután filter1SA és filter1PK megvan, nullázzuk a sample regisztereket, melyek közül a sampleSA3 a fentiek szerint már nulla, így azon már felesleges lenne clr-t alkalmazni. Az IF ágban is helyre kell állítani a felhasznált regisztereket a veremből, valamit a sátusz regisztert is. A státusz reg után pedig be kell billenteni annak T bitjét, megtriggerelve ezzel a main rész 2. szűrőjének indítását.
A kódban a továbbiakban szubrutinokat látunk, főleg LCD kezelésre és az LCD kezeléshez köthető delay időzítő rutinokat látunk. Amiről itt beszélni kell, az a writeValue nevű rutin, mely az értékek igazítást és decimális 0-204.8 közötti 0.1 felbontású átalakítását és ASCII kijeleztetését végzi, de stílszerűen erre a rutinra a maga helyén térünk ki. A szövegkiírató rutinok meg úgy gondolom nem igényelnek különösebb magyarázatot, overload kiirja hogy “OVER!“, writeText pedig programmemóriából képes kiírni szöveget a Z pointer segítségével a szövegvégjelző nullbájtig.
A program init rész
Következzen a programkód inicializáló része. Talán nem volt róla eddig szó, azért az érthetőség kedvéért érdemes kitérni rá. Az eddigi programjaim a következő szakaszokra bonthatók: Mindegyikben van egy init vagy inicializálás rész. A mikrovezérlő bekapcsolásakor ez fur először, és csak egyszer fut le. Vannak, akik ezt setup résznek nevezik. Ezután ráfut a vezérlés a főprogramra, ami nálam main nevet visel. Ez a rutin vagy végtelenített ciklusban ismétli önmagát (végtelenített main eset), vagy egyszer végigfut, és a végén megáll egy önmagára ugró ugróutasítással (egyszer futó main eset). Ezen felül a program szubrutinokkal és eseményvezérelt megszakítási rutinokkal van még ellátva, amiket tehetünk a kód elejére, ekkor a main lesz a végén, vagy fordítva. Olyan is előfordulhat (bár nem szerencsés) hogy a kód eleje és vége is rutinokat tartalmaz, és középen van az init és a main. Ez talán akkor jó, ha két csoportra bonthatók a rutinok, azaz egyrészt alacsony szintű rutinokra, melyek amolyan szolgáltatás félék (pl. időzítő, LCD meghajtó rutinok, matematikai függvények stb.) és magasabb szintű, a programhoz közelebb álló, annak tényleges alprogramjait képező szubrutinok. Bonyolultabb programoknál azért ilyenkor előtérbe kerül a magasabb szintű programnyelvek, pl. a C alkalmazása is, vagy akár egy kevert Assembly-C program (main C-ben, de a sebességérzékeny részek, itt pl. a sampler nevű IRQ rutin Assemblyben) Ezután a kis kitérő után térjünk vissza a tárgyhoz, lássuk először a kódrészt, majd következzen az ismertetése:
; *** inicializálás ***
initializer:
; verem
ldi temp, low(RAMEND)
out SPL, temp
ldi temp, high(RAMEND)
out SPH, temp
; kezd értékek def
ldi holdTimer, holdTime
ldi samplerCntL, osszszamlL
ldi samplerCntH, osszszamlH
ldi delayTimer,displayTimerValue
clr filter2SAL
clr filter2SA0
clr filter2SA1
; idozíto elindítása
ldi temp,3 ; osztás: clk/64
out TCCR0B,temp
; ADC konverter és megszakításkezelés
ldi temp,(1<<REFS0)|(1<<ADLAR)
sts ADMUX,temp
ldi temp,(1<<ADPS0)|(1<<ADPS2)|(1<<ADIE)|(1<<ADATE)|(1<<ADEN)
sts ADCSRA,temp
; LCD kijelzo inicializálása
sbi lcdDdr7,lcd7
sbi lcdDdr6,lcd6
sbi lcdDdr5,lcd5
sbi lcdDdr4,lcd4
sbi lcdDdrE,lcdE
sbi lcdDdrRS,lcdRS
_lcd0 E
_lcd0 RS
; itt még 8 bites módban fogad parancsokat az LCD
_lcd0 7 ; function reset parancs bebitelése
_lcd0 6
_lcd1 5
_lcd1 4
_lcdE ; elso reset kiküldés
rcall delay10ms
_lcdE ; második reset kiküldés
rcall delay200us
_lcdE ; harmadik reset kiküldés
rcall delay200us
_lcd0 4 ; D4=0 -> 4 bites módra váltunk
_lcdE ; negyedik reset kiküldés 4 bit móddal
rcall delay100us
; innentol 4 bites módban kell küldeni a parancsokat
_write 0b00101000 ; func set
_write 0b00001000 ; Off
_write 0b00000001 ; Clear
rcall delay10ms
_write 0b00000110 ; Entry Mode
_write 0b00001100 ; On
_write 0b10000000 ; pos $0
_lcdChr
; ADC mintavételezés elindítása
sei ; megszakítás be
ldi temp,(1<<ADPS0)|(1<<ADPS2)|(1<<ADIE)|(1<<ADATE)|(1<<ADEN)|(1<<ADSC)
sts ADCSRA,temp
; Kezdoképernyo kiiratása az LCD-re
ldi ZL,low(startText*2)
ldi ZH,high(startText*2)
rcall writeText
_lcdSendCmd posLine2
rcall writeText
; kezdokép késleltetés (Az ADC már megy, de IIR feldolgozás még nincs)
ldi temp,47 ; 2 sec
brtc PC ; megvárunk egy 23.5Hz-es leosztott blokkot
clt ; töröljük az ezt jelzo flag-et
dec temp ; számoljuk
brne PC-3 ; ha még tart az idozítés, akkor vissza a ciklus elejére
clr filter2PK ; az elso hibás csúcs törlése
; Mérés szövegkörnyezetének kiiratása
_lcdSendCmd posLine1 ; elso sor elejére (nem törlés!)
rcall writeText
_lcdSendCmd posLine2 ; második sor elejére
rcall writeText
Csak átfutva, a lényegre törekedve: Látjuk, hogy a timer0B HW időzítőt bekapcsoljuk CPUclk/64 ütemmel, ez a késleltető rutinoknak kell, bár igazából kidobhattam volna már a kódból, hisz a késleltetések leginkább csak az LCD inicializáláskor kellenek, a menet közbeni használat, kiírkáláshoz egyszerűbb késleltetések is teljesen jók lennének. A HW időzítő amúgy azért lenne szükséges, mert a megszakítás bekapcsolása és a mintavételezés elindulása után az IRQ rutin megnyújtaná az időzítéseket, ha viszont szoftvertől független HW számlálóval oldjuk meg, akkor ez áthidalható. Az ADC periféria beállítása nem igényel túl sok magyarázatot, 32-es órajel leosztást állítunk be, free running üzemmódot, valamint a 10 bites eredményt felfelé igazítjuk. A spektrumanalizátornál elég részletesen írtam róla, nem ismételném önmagam. Talán ami látható eltérés, hogy ott még a mega8 alapokon beszéltünk a kódról, és bitenként kapcsoltuk be a funkcióka sbi utasítással. Itt ez nem lehetséges, mert a mega88 más címen kezeli az ADC és a HW időzítők regisztereit, amiket nem lehet bitenként állítgatni, hanem memóriakezelő utásításokkal (sts, lds). (Tulajdonképpen a két mikrovezérlő közötti átíráskor lényegében ezeket, a beinklúdolt definíciós fájlt, és a timer0 egy két regiszterelnevezését kell csak átírni, és a kód már fordul is a másik eszközre. – ezt egy mellékletbe össze fogom írni!) A további részeket nem kommentálom ebben a cikkben, egyre térek csak ki, a kezdőképernyő késleltetőre. Ez a rutin számol a leosztott mintavét ütemében (23Hz) és ez alapján lehet beállítani 1-5 mp késést, hogy el is lehessen olvasni a bemutatkozó képernyőt. (a 47*23=1081ms azaz kb 1 mp. De mivel az utóbbi időben mindig buzeráltam az órajeleken, így folyamatosan valami elavult érték marad itt bent (meg néha még máshol is), nyugodtan át lehet írni -23.475Hz-el számolva, ha fontos a pontosság- mondjuk 2000ms/23.475Hz=81-re, így pontosan 2 sec lesz a késés)
A Main program
Végtelenszer körözgető main-ben van leprogramozva a 23Hz-en működő 2. szűrő, valamint a kijelzési funkciók. Az eredeti elképzelésben vázolt védelmi és relévezérlési funkció még nincs a kódban megvalósítva. A kódot részekre bontva nézzük teljes részletességgel:
; *** ciklikus foprogram ***
; ~23Hz-en ismétli magát, ez a filter1 kimeneti mintavétje
sbi ddrB,portB4 ; kontroll (debug) LED portjának kimenetre állítása
main:
brtc PC ; Várakozik, amíg a Filter1 elkészül az eloszurt adattal
clt
Ugyanazt látjuk, mint a spektrumanalizátornál, egy kivétellel: A kontroll LED lábának kimenetre állítása még itt van (bár az init-ben lenne a helye), de a LED ki/be gyújtása eltűnt, átkerült az overload kijelzésbe. Így nem csak a kijelzőre írja ki, hogy OVER!, hanem egy LED-el is indikálja, ami feltűnőbb visszajelzés. Az önmagára ugró brtc várja be az 1. szűrő T bittel jelzett triggerjelét, majd a clt-vel úgymond le is nyugtázza, így a következő trigger fogadhatóvá válik.
Jelfeldolgozás, csúcsjel és 2. szűrő
A main kódrész jelfeldolgozó része lényegében a csúcsjelkezelés és a 2. szűrő működtetését végzi, mégpedig az 1. szűrő IRQ rutinjának 23Hz-es ütemjelére.
; *** Jelfeldolgozás ***
; Peak jel feldolgozása (csúcstartás)
dec holdTimer ; peak hold idozíto kezelése
tst holdTimer ; idozíto lejárt?
breq changepeak ; igen, peak értéket cserélni, idozítot újraindítani
cp filter2PK, filter1PK ; vagy nagyobb új peak értéket kaptunk?
brcs changepeak ; igen, peak értéket cserélni, idozítot újraindítani
rjmp skip_main_1 ; egyik sem, korábbi peak értéket tartani
changepeak:
mov filter2PK, filter1PK
ldi holdTimer, holdtime
skip_main_1:
A csúcstartás 10mp-ig tart. Ha ezalatt az idő alatt érkezik nagyobb csúcs, akkor kicseréli, és a 10mp-es időzítést újraindítja. Ha lejár az időzítő újabb (nagyobb) csúcs nélkül, akkor nullázza a regiszterét, ezzel felépülhet benne a következő csúcsérték, ami ezesetben egy kisebb érték is lehet, mint a korábbi volt. A kód bemenete a filter1PK, ami a sampler ciklus csúcsértékét tárolja, és kimenete a filter2PK, ami a 10mp-el kitartott, kijelezendő csúcs. A 10mp időzítő (mint már volt róla szó) a holdTimer regiszter visszafelé számláltatásával működik, minden futáskor egyel csökkentve annak értékét, az időzítés így a 23Hz-hez számolandó.
A 2. szűrő négyzetes átlag IIR szűrője kicsit bonyolultabb, de még így is egyszerűbb, mint a korábbi hangdobozépítős oldalon publikált, ezen a téren sikerült egyszerűsíteni. Ezzel kapjuk tehát a készreszűrt un. Long Term Power féle hosszú idejű termikus átlagteljesítményt, ő a történet főszereplője.
; * Long Term Power jel feldolgozása (IIR szuro) *
; Ti=10sec IIR szûrõ - long term power jel feldolgozás
push filter1SA0; mentjük az x[n]-t
push filter1SA1
movw filter1SA0,filter2SA0
sub filter2SAL,filter1SA0 ; y[n]-=a0*y[n-1]
sbc filter2SA0,filter1SA1
sbci filter2SA1,0 ; átvitelkezelés
pop filter1SA1
pop filter1SA0
add filter2SAL,filter1SA0 ;y[n]+=a0*x[n]
adc filter2SA0,filter1SA1
brcc PC + 2
inc filter2SA1
A szűrő akksi regisztere 24 bites, és 16 biten érkezik az új bemenet. Mint látjuk, se szorzó, se bitrotáló művelet nincs benne, direkt erre hajaztunk. Az 1/a0 együtthatót 256-ra kerekítve 8 bittel kell jobbra rotálni, ami egyszerűen átcímzéssel történik meg. Mivel itt most nem akartam segédregisztereket, és mivel megoldható vermeléssel is a dolog, így első lépésként a filter1SA regiszterét elmentjük a verembe. Ezután belemásoljuk a filter2SA felső 16 bitjét (törtrész nélkül, azaz az 1 és 0 bájtot, az L-t nem). A további magyarázathoz elő kell venni az előző részben közölt működési egyenletet:
y = y - y/A0 + x/A0
A sub, sbc és sbci utasítások lényegében a fenti egyenlet y=y-y/A0 részét hajta végre. Ez a műveletrész ugyebár nem okozhat negatív irányba túlcsordulást, hiszen az y/A0 nem lehet nagyobb az y-nál. A művelet mint látjuk 3 bájton át megy, a teljes 24 bites műveleti akksin, és a regisztercímek el vannak csúsztatva:
[filter2SA1][filter2SA0],[filter2SAL] - 0 [filter1SA1],[filter1SA0]
Ezután a veremből visszahozzuk a szűrő bemenőjelének számító filter1SA-t és azt is hasonló eltolás mellett hozzáadjuk, megvalósítva ezzel az +x/A0 részműveletet, és ezzel a szűrés kész is. Ha eltekintünk attól, hogy átmenetileg a filter1SA-t vermeltük, akkor így írhatnánk fel egyszerűbben a folyamatot:
műveleti bittartomány |ez a rész kicsordul, elveszik [filter2SA1][filter2SA0],[filter2SAL]| - 0 [filter2SA1],[filter2SA0]|[filter2SAL] + 0 [filter1SA1],[filter1SA0]|
Természetesen a műveletnek mindig lesz kicsorduló része, egy rekurzív matematikai művelet ugyanis a végtelenségig generálja a tizedesrészt, mindig egyre hosszabbat. Annyit kell csak tárolni, amennyi a korrekt működéshez szükséges, a többi csak pazarlás. Talán kicsit finomabb lenne a működés, ha a leszakadó filter2SAL részben megvizsgálnánk a 7-es bitet, és ha az 1, akkor egy felfelé kerekítés keretében egyet hozzáadnánk az akkuhoz, de igazából így se vesz észre az ember a működésében problémát.
Kijelzés
Következik a kijelzés, ami egy kijelzés lassító kóddal indul. Nagyon egyszerű; számlálunk, és ha még nem jött el a kijelzés ideje, visszaugrunk a main elejére, kihagyva a main kód végén lévő kijelzési utasításokat. Természetesen, ha relévezérlés, beavatkozás is programozva lesz, azt ne ebbe a 3Hz-re lelassított részbe írjuk, hanem még ez elé!
; *** kapott értékek kezelése, kijelzése ***
; kijelzés frissítés lassító ciklus (túl gyorsan futnak a számok probléma)
dec delayTimer ; számlálás
breq PC+2 ; továbblépés a kijelzési kódrészre
rjmp main ; ebben a ciklusban nincs kijelzés, vissza a main fociklus elejére
ldi delayTimer,displayTimerValue ; idozítés újraindítása
A peak jel kijelzése egy kissé kuszának tűnhet, miután ebbe került be az overload detektálás és lekezelés:
; * peak power jel kijelzése túlvezérlés jelzéssel *
_lcdSendCmd posPP ; kurzor pozicionálás a csúcstelj. mezõre
cbi portB,portB4 ; Overload LED ki
ldi temp,255
cp filter2PK,temp ; túlvezérlés ellenõrzés
brne PC + 5
; if
sbi portB,portB4 ; Overload LED be
clr filter2PK ; töröljük, hogy ha megszûntetjük a túlvezérlést, ne tartsa még 10s-ig
rcall overload ; kijelezzük a túlvezértés tényét az LCD-re is (opc)
rjmp skip_main_2
; else
mul filter2PK, filter2PK ; peak jel U->P konverziója itt történik (eddig csak feszültség csúcs volt kezeltük, nem pedig (négyzetes) teljesítménycsúcs)
movw divNumL, R0
rcall writeValue
skip_main_2:
Először rápozicionáljuk az LCD kurzorát a peak mezőre. Kikapcsoljuk az overload LED-et, ha esetleg a korábbi ciklus bekapcsolta volna. Ebből az is látszik, hogy a LED kigyújtási ideje megegyezik a kijelzőfrissítési idővel, vagyis nincs olyan rövid felvillanás, ami nem is látszik. Valamint overload állapotban a 10s csúcstartás is törölve lesz, így ha a kezelő visszaveszi a jelet, akkor azonnal új csúcsérték kerül a kijelzőre. Normális kivezérlési esetben mindig az ELSE ágon fut a kód, ahol először négyzetre emeljük a csúcsértéket, majd meghívjuk a kijelző rutint. A writeValue rutin az osztórutin bemeneti regiszterében kapja a kijelezni valót, ami nem véletlen, hiszen a bináris-decimális átalakításhoz masszívan használni kell az osztó algoritmust, így a writeValue rutin mindjárt el is kezdi átpasszolni ezt a div-nek. (ld. később) A négyzetszorzás indokolhat még némi magyarázatot: Nos, ha a csúcsot teljesítményarányosan négyzetesen kezeljük, akkor 16 bites regiszter kell neki. Azonban mivel ez csak csúcsérték-detektálásokban, komparálásokban szerepel, és mivel a csúcs négyzete ugyanúgy monoton növekvő függvény, mint maga a csúcs, ezért ezen műveletek 8 bites módban is tökéletesen végezhetőek, és így elég csak közvetlenül a kijelzés előtt négyzetre emelni, hogy ne a villamos feszültséggel legyen egyenes arányosságban, hanem a villamos teljesítménnyel, azaz a feszültség négyzetével. Tehát ez egy amolyan U→P értékkonvertálás.
A Long Term Power kijelzése egyszerűbb, mert azzal semmit nem kell csinálni, csak átmásolni a megfelelő regiszterbe, és meghívni a writeValue értékkijelző szubrutint:
; * long term power jel kijelzése *
_lcdSendCmd posLTP ; kurzor pozícionálás a longterm mezõre
movw divNumL, filter2SA0
rcall writeValue
Ezután rjmp utasítás visszaugrik a main elejére, ahol bevárjuk a következő 23Hz-es triggert, és minden kezdődik előröl.
Értékkijelző szubrutin
Visszamaradt még a decimális értékkijelző szubrutin ismertetése:
; *** 000.0 alakú numerikus érték kiiratása (PeelPwr, LongTermPwr) ***
writeValue:
lsr divNumH ; 2048-as szintre hozás
ror divNumL
lsr divNumH
ror divNumL
lsr divNumH
ror divNumL
lsr divNumH
ror divNumL
lsr divNumH
ror divNumL
ldi temp2, 48 ; ascii átkódoláshoz
ldi divDenH, 0
ldi divDenL, 100 ; osztás 100-al -> két részre bontás eredmény=[ezres,százas] és maradék=[tízes,egyes]
rcall div
ldi divDenL, 10 ; ez után mindenhol 10-el osztunk
push divRemL ; ebben vam az [tízes,egyes] számrész, vermelni a késobbi használatra
tst divNumL
brne PC + 5
ldi temp,' ' ; __n.n
rcall lcdWrite
rcall lcdWrite
rjmp skip_writeValue_1
rcall div
tst divNumL
brne PC + 4
ldi temp,' ' ; _nn.n
rcall lcdWrite
rjmp skip_writeValue_2
mov temp,divNumL
or temp, temp2 ; ezres decimális számjegy, itt már csak 0-9 közé esik, ASCII értékre hozás
rcall lcdWrite ; és kiiratása
skip_writeValue_2:
mov temp,divRemL
or temp, temp2 ; százas decimális számjegy ASCII-be
rcall lcdWrite ; és kiiratása
skip_writeValue_1:
pop divNumL ; korábban vermelt maradék, ami a 10-es és 1-es helyiértékeket tartalmazza
rcall div ; ezt is 10-es osztjuk
mov temp,divNumL
or temp, temp2 ; tízes decimális helyiérték ASCII-be
rcall lcdWrite ; és kiiratása
ldi temp, '.' ; tizedespont kiiratása
rcall lcdWrite
mov temp,divRemL
or temp, temp2 ; egyes decimális helyiértéke ASCII-be
rjmp lcdWrite ; egyes decimális helyiérték kiiratása / RET a hívó rutinban
Először át kell váltani a 16 bites értéket 0-200.0 közötti kijelezhető, erre kalibrált értékre. Hogy egyszerű legyen a művelet, ezért egy közeli kettes számrendszerben kerek értéket választunk inkább, ami a 2048 lesz. Ha a 16 bites értéket erre akarjuk alakítani, akkor osztani kell 25-el, azaz 5 bináris helyiértékkel jobbra kell tolni. Mivel a legnagyobb értékünk 65025 (és nem 65535), ezért a legnagyobb kijelzett érték 2032 lesz, a fix helyre tett tizedesponttal 203.2 jelenik meg a kijelzőn. Mi úgy fogunk kalibrálni, hogy 200.0 legyen a teteje, az hogy kicsit feljebb is megy, az nem okoz gondot. Ennek megfelelően a kijelzőn alkalmazhatunk majd százalékjelet, vagy Watt mértékegységet. A tényleges kalibrációt az analóg bemeneti fokozatban kell majd megcsinálni, de valószínűleg nem lesz szükség potméteres pontos beállításra, 1% tűrésű ellenállásokkal és pontos 5V-os táppal fixen is mindjárt kalibráltra lehet készíteni. Az 5-el eltolás után temp2 regiszterbe 48 konstanst veszünk fel, ez az ASCII átkódoláshoz kell majd. (48 az ASCII kódja a nulla karakternek, vagyis ennyit kell hozzáadni majd a számjegyekhez) Szükséges pár fontos infót ejteni az osztó algoritmusról: divNum változó (alsó és felső bájt) a div osztó rutin bemeneti és egyben kimeneti regisztere, vagyis ebben adjuk át az osztandó számot, és kapjuk vissza az eredményt is. Hívás előtt tehát belemásoljuk az osztandó számot (az un. numerátort) majd a divDen 16 bites regiszterbe az osztót (denumerátor). Hívásból visszatérve az eredmény egész része a divNum változóban lesz, míg az osztás maradéka a divRem (remain) 16 bites regiszterben érkezik. A négy helyiértékű decimáslisnak megfelelő bináris számot először 100-al osztjuk el. Most az egyszerűség kedvéért tekintsük négy helyiértékes egésznek, azaz ezres nagyságrendűnek. Az osztás után a divNum-ban visszajön az ezres és százasnak megfelelő két helyiértékű tízes nagyságrendő rész (0-99), a divRem maradék pedig az egyes és tízes helyiértéknek megfelelő rész (szintén 0-99). A maradékot verembe mentjük, az egészt tovább osztjuk 10-el. Ekkor a divNum-ban jön az ezres rész immáron egy decimális helyiértéken egyes nagyságrendben (0-9 között), a divRem-ben pedig a százas helyiérték, így ez a két számjegy egymás után kijelezhető lenne. A régi verzióban ki is lett jelezve, akkor is, ha 0 értéket adtak, tehát nem volt zéró blanking, nullák jelentek meg a szám előtt. (pl. egy 5.7W értéket 005.7W-nak jelzett ki) A közben módosított verzió ellenőrzi az első két helyiértéket, és szóközöket ír ki a bevezető nullák helyett. Ha a 100-al való osztás után a divNum nulla, akkor __n.n alakban kell kijelezni, és ennek megfelelően kiküld két szóközt és ugrik egy nagyot előre. Ellenkező esetben pedig, ha a 10-es osztás után az ezres helyiérték nulla, akkor csak egy szóközt kell kiírni, és a százas helyiértéket megjeleníteni. (_nn.n alak) A 10-el való osztás után 0-9 közötti bájtot kapunk, de nekünk a számkarakterek ASCII kódja kell, azaz 48-57 közötti értékre kell növelni. Erre nem összeadást, hanem OR logikai műveletet fogunk használni. A harmadik számjegy után egy pont karakter is kiíratásra kerül tizedesvessző gyanánt, majd végül a negyedik decimális számjegy.
2048 / :100 \ egész maradék 20 48 / \ :10 / \ egész maradék egész maradék 2 0 4 8
Az analóg bemeneti fokozat:
Egyelőre passzív és analóg szűrő nélküli bemeneti fokozatot képzelek el, de a következő részben taglalni fogom, miért nem feltétlen jó ez így. A bemenet jelen állapotban így van kialakítva: (ez ugyanaz az ábra, ami a spektrumanalizátornál is van)
Az R1 75kΩ soros szintillesztő ellenállás jelen állapotban ki van iktatva, ezen keresztül egy 100W szinuszos kimenőteljesítményű 8Ω-os végfokozat kimeneti feszültségéhez illeszkedik. Egy ilyen végfok ±40V-ot ad le csúcsban, ezt kell ±2.5V csúcsértékre leosztani. Én a végfokhoz csak a melegpontját kötöttem, a GND-t ne kössük közvetlenül végfok kimenetre, ha be akarjuk kötni, tegyünk be pl. egy 100R soros ellenállást a GND ágra is. Nyilván korrekt illesztéshez ennél igényesebb áramköri kialakítás szükséges.
Folytatása következik…
Mellékletek:
A teljes kód csomagolva: apa_m88pa.zip
ATmega88PA-PU biztosítékbitek beállítása:

Megj.: A Ponyprog2000 (nálam legalábbis) nem ismeri az ATmega88PA eszközt, csak az ATmega88 állítható be. A program figyelmeztet is, hogy a chipből kiolvasott típus nem egyezik azzal amit beállítottunk, ill. nem támogatott típus. Ezt a hibát ignorálva az átállítást meg lehet csinálni, de azért nem szerencsés ilyen manőverbe bocsátkozni…