Hangsugárzók valós effektív terhelésének mérése mikrovezérlővel 3.

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:

ATmega88 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…