časovač

Ahoj všem,
mám začátečnický dotaz. Začal jsem se učit v asm není mi jasné kdy začne běžet časovač. Níže je uveden část kódu, kterým časovač nastavím. Veškeré nastavení i registry si myslím, že chápu, ale v případě, že tuto rutinu budu mít na začátku programu, tak po které instrukci nebo od kdy začne tento časovač čítat? Druhá věc která s tím úzce souvisí. Dovedu si představit situaci za běhu programu, kdy není vhodné aby se provedlo přerušení. Pokud by nastala situace kdy zakážu přerušení a v tu chvíli by došlo k přerušení, co se stane?
Díky za odpověď

zde kód:

		LDI		R19,12       ; nastavi zdroj signalu 
		OUT		TCCR0,R19    ; casovace 0 na 31250Hz
		LDI		R19,250      ; nastavi porovnavanou
		OUT		OCR0,R19     ; hodnotu na 250 (preruseni 125/s)
		LDI		R19,02       ; povoli preruseni,
		OUT		TIMSK,R19    ; kdyz TCNT0=OCR

Ahoj.

Nejdříve pár důležitých věcí, které VŽDY MUSÍŠ uvést.

  1. O jaký jde procesor - AVR jádro má velké množství procesorů, tak je potřeba vědět, o jaký jde.
  2. Nastavený kmitočet

Eventuálně i zdroj hodin (IntRC, Ext, krystal).

Zde podle konfigurace čítače, použitých registrů a výpočtů frekvence přerušení tipuju na ATmega16 nastaveným na 8 MHz.

Teďka pro Tebe :

LDI R19,12 ; nastavi zdroj signalu OUT TCCR0,R19 ; casovace 0 na 31250Hz LDI R19,250 ; nastavi porovnavanou OUT OCR0,R19 ; hodnotu na 250 (preruseni 125/s) LDI R19,02 ; povoli preruseni, OUT TIMSK,R19 ; kdyz TCNT0=OCR
vypadá dost nepřehledně. Co to zkusit takto :

[code].def Temp=R19 ; Pracovní registr
#define MaximumTimeru 249

  LDI      Temp, (1<<WGM01)|(1<<CS02)       ; nastavi zdroj signalu
  OUT      TCCR0, Temp                      ; casovace 0 na 31250Hz
  LDI      Temp, MaximumTimeru              ; nastavi porovnavanou
  OUT      OCR0, Temp                       ; hodnotu na 249 (preruseni 125/s)
  LDI      Temp, (1<<OCIE0)                 ; povoli preruseni,
  OUT      TIMSK, Temp                      ; kdyz TCNT0=OCR [/code]

Trocha vystětlení :
.def Temp=R19 - Je celkem vhodné si pojmenovat registry stejně, jako si ve vyšších jazycích pojmenováváš proměnné. Kód je pak daleko přehlednější a snáze se v něm hledá chyba.
#define MaximumTimeru 249 - pojmenovaná konstanta. Na první pohled je zbytečné pojmenovávat konstantu, která se v kódu vyskytne jenom jednou, ale konstanty můžeš použít i na více místech. Je mnohem jednodušší přepsat 1 číslo někde v definicích, než hledat, kde se toto číslo kde v programu vyskytuje a zda je to právě to, co chceš změnit. Například :

LDI R19, 130 . LDI R19, 130 . . . . LDI R19, 130 . . . LDI R19, 130 . . . . . . LDI R19, 130

Jas LEDky je 130 a otáčky motoru také. Teď chceš změnit jas LEDky na 90. Jak, která stotřicítka to je, kolik je jich v programu ? Oříšek, co ?

A co takhle :

[code].def Temp=R19 ; Pracovní registr
#define JasLEDky 130
#define OtackyMotoru 130

  LDI      Temp, JasLEDky
  .
  LDI      Temp, OtackyMotoru
  .
  .
  .
  .
  LDI      Temp, OtackyMotoru
  .
  .
  .
  LDI      Temp, JasLEDky
  .
  .
  .
  .
  .
  .
  LDI      Temp, JasLEDky

[/code]

Tady bys to zvládnul ? Já myslím, že celkem bez problémů, že ?

A ještě proč MaximumTimeru nastavuji na 249 a ne na 250. Je to proto, že když chceš dostat 125Hz z 8 MHz, tak výpočet máš správně : 8000000/256/250, jenže timer čítá od 0, takže maximum je 249 (0-249 => 250 kroků).

Tak a teďka ke Tvým dotazům :

Časovač začíná čítat v okamžiku, kdy do registru čítače (v tomto případě TCCR0) vložíš nastavení prescaleru. Je tedy velmi vhodné prescaler nastavovat až jako poslední. V případě, že měníš nastavení čítače, je vhodné prescaler čítače jako první krok nastavit na (CSx2:0)=000, pak přenastavit čítač a jako poslední nastavit prescaler na požadovanou hodnotu.

Přerušení funguje tak, že se nastaví příznak a v okamžiku, kdy to lze se vektor přerušení provede. Je úplně jedno, jestli požadavek přišel přišel při zakázaném přerušení nebo během zpracování některé instrukce (AVR má řadu instrukcí trvajících déle, než jen 1 cyklus). Přerušení není zakázané jenom použitím instrukce CLI nebo zápisem log. 0 do SREG na pozici příznaku I, ale i během zpracování přerušení. Z tohoto důvodu je důležité, aby rutina obsluhující přerušení byla pokud možno co nejkratší.

Děkuji za cenné rady. Typ MCU jsem opravdu zapomněl uvést, ale zcela správně si vyhodnotil o jaký MCU jde. Pokud tedy můžu pokračovat tak tedy s Atmega 16, IntRC=8Mhz.

V rámci učení jsem zkusil následující nastavení

	.EQU		SET_OCR0 = 24
	.DEF		TMP = R16

...

;NASTAVENÍ ČASOVAČE 0, přerušení 40kHz
	LDI			TMP,SET_OCR0
	OUT			OCR0,TMP
	LDI			TMP,02				;set OCIE0
	OUT			TIMSK,TMP			;TCNT0=OCR0
	LDI			TMP,0b00001010		;set WGM01,CS01
	OUT			TCCR0,TMP

...

PRERUSENI:
	ADIW		XL,1
	RETI

Ve smyčce si poté kontroluji XL pro časy do 6,4ms nebo XH pro časy do 1,6s což je dostatečný rozsah pro většinu aplikací.
Registr X jsem použil abych si vyzkoušel práci s “16bitovým” registrem.
Testováno v AVR Studiu a vše funguje jak má.

Otázka tedy je

1)Používá se takový algoritmus i při reálných aplikacích
2)Není takto časté přerušení už příliš

Díky za každou kritickou radu

  1. v podstatě se takovýto algoritmus používá, ale pro menší náročnost se používá spíš než čítání od 0 do kontrolované hodnoty, tak přednastavení registru na požadovanou hodnotu a odečítání do 0. Pokud načítáš nahoru, pak musí přičíst 1 a zkontrolovat registr (případně dvojregistr) na požadovanou hodnotu a pokud souhlasí, tak registr (dvojregistr) vynulovat, provést akci a znova. U registru to tak velký problém není, u dvojregistru je pak takového porovnávání náročnější. V případě, že to uděláš opačně - tedy nastavíš registr/dvojregistr na požadovanou hodnotu a od té odečítáš, pak jakmile se dostaneš na 0, tak už v okamžiku toho odečtení máš nastavený příznak Z => víš, že jsi se dopočítal kam jsi chtěl. Nastavíš znova registr/dvojregistr na požadovanou hodnotu a provedeš akci.

  2. Použití čítače s takto vysokou frekvencí přerušení není nic proti ničemu. Musíš jenom počítat s tím, že na ostatní práci máš poměrně málo času. Dělal jsem 4-kanálový PWM generátor přepínatelný 100/200/400 Hz s rozlišením 100 (0-100%) na ATmega8. Nastavil jsem si kmitočet na 40 kHz a PWM kanály jsem SW počítal. K tomu ještě procesor musel zvládat příjem dat po SPI (kvůli zapojení opět řešeném pomocí SW) a tyto data zpracovávat. Těch dat bylo sice jenom minimum (nastavení PWM, zapnutí/vypnutí výstupů), ale musel to zvládnout. Na to vše má mcu (při 8 MHz) od přerušení do přerušení k dispozici čas na maximálně 200 (jednocyklových) instrukcí (reálně tedy cca 120-150 instrukcí) - a to VČETNĚ obsluhy přerušení… Nicméně takto časté přerušení nebývá úplně obvyklé. Pokud řídíš třeba 4-místný LED displej, pak Ti bohatě pro 8MHz mcu stačí 8-bitový čítač s prescalerem 64. Při TOV přerušení vždy přepneš na následující číslici a je to. Dostáváš se na 488,28125 Hz, což je 122,0703125 Hz refresh celého displeje. Pokud chceš, displej opravdu neblikal, pak stačí dát prescaler na 8 a dostaneš se s přerušením na 3,906 kHz a refresh celého displeje bude 976,5625 Hz. Vylepšit to ještě můžeš tím, že AD převodníkem budeš snímat intenzitu okolního osvětlení a hodnotu z něj budeš dávat do OCR registru a při OCF přerušení zhasneš celý displej. Tím získáš k tomu ještě automatické řízení jasu displeje v závislosti na okolním osvětlení, aniž by ses o to musel nějak starat v hlavním programu. Jakmile dělám něco s LED displejem nebo s něčím, kde je podsvícení, tak to tam používám.

Nejčastěji ovšem používám kombinaci obojího. Nastavím si čítač tak, abych dostal přerušení, které nejčastěji potřebuji (vetšinou refresh displeje) a všechno ostatní pak od něj odvozuji. Pokud například dělám kuchyňskou minutku a potřebuju 500ms na blikání LEDkou, 1 sek. na vteřiny a nějaký vyšší kmitočet pro displej, tak to řeším tak, že nastavím režim CTC (Fmcu=8MHz, Prescaler=64, OCRxA=249 => 8000000/64/250=500) na 500Hz (tím dostávám refresh displeje 125Hz - je mi jedno, jestli mám refresh 122,0703125Hz nebo 125Hz - tady je důlezitější přesnost kmitočtu pro LEDku a pro počítání vteřin). Přitom odečítám nějaký registr a jakmile dosahnu 0, přepnu LEDku a nastavím registr na 250. Pokud jsem v tuto chvíli LEDku rozsvítil, tak mám celou vteřinu a podle toho se zachovám. Hotovo. I tady se dá použít AD převodník k řízení jasu (použiješ přerušení od OCRxB), jenom musíš počítat s tím, že tady čítač nečítá do 255, ale jenom do 249.

A ještě jedna důležitá věc. Pokud v přerušení děláš nějaký výpočet (ADIW X, 1) nebo cokoliv, co ovlivňuje SREG, pak VŽDY na začátku přerušení ukliď SREG do nějakého registru a na konci ho zase obnov.

[code].def ShovavaciRegistr=R0

Preruseni:
in ShovavaciRegistr, SREG
.
.
.
.
out SREG, ShovavaciRegistr
reti
[/code]

Vůbec nic nezkazíš tím, pokud si vyhradíš 1 registr právě na tuto úschovu a budeš to používat v KAŽDÉ obsluze přerušení. Nezapomeň, že během zpracování přerušení jsou přerušení zakázána, tudíž se nemusíš strachovat o to, že by se Ti v průběhu jeho zpracování vyvolalo jiné a to Ti tento registr přepsalo.

A pokud se Ti zdá zbytečné “vyhazovat” registr jenom kvůli úshově SREG během přerušení, můžeš to udělat třeba takhle :

[code].def Temp=R16

Preruseni:
push Temp
in Temp, SREG
.
.
.
.
out SREG, Temp
pop Temp
reti
[/code]

A v případě, že chceš i registr Temp v přerušení použít, pak :

[code].def Temp=R16

Preruseni:
push Temp
in Temp, SREG
push Temp
.
.
.
.
pop Temp
out SREG, Temp
pop Temp
reti
[/code]

Celkem by mě zajímalo jak se kontrolují příznaky (např. Z) v C. Jestli to jde a jak?

To Jenda25:
Kontrolovat příznaky v C by asi také mělo jít (nikdy jsem to v Cčku nepotřeboval) - SREG je registr jako každý jiný. Je s ním asi budeš muset zacházet přes nějakou proměnnou, nicméně v Cčku je ale kontrola stavového registru něco, o co by se uživatel starat neměl. Nicméně geraldg10 psal, že začíná s ASM. Jinak je mi jasné, že narážíš na to doporučení čítat nějaký příznak od přednastavené hodnoty do 0. Řekl bych, že i v Cčku bude pro optimalizaci jednodušší napsat

if (!(--Pocitadlo)) { Pocitadlo = PrednastavenaHodnota; InvertujLedku; }

případně

if (--Pocitadlo==0) { Pocitadlo = PrednastavenaHodnota; InvertujLedku; }

než

if (++Pocitadlo==KoncovaHodnota) { Pocitadlo = 0; InvertujLedku; }

V Cčku AVRka téměř neprogramuju, ale co jsem tak měl tu čest vidět, tak by to tak mělo být…

S Cčkem boužel neporadím, ale pokud jde o tvé rady, tak jsem vyzkoušel odečítání do nuly. Náročnost, teď myslím strojová je téměř stejná. Nemusím sice používat instrukci pro porovnávání, ale zase musím mazat příznak Z. Nemusím však nastavovat navíc registr s kterým se to má porovnávat, tak že odečítání ve výsledku vychází opravdu méně náročné. Já to použil možná trochu nešikovně pro inicializaci LCD kdy mám různě dlouhé prodlevy mezi jednotlivými příkazy. Nejspíše by bylo lepší vytvořit nějakou delay smyčku. Tohle bylo čistě pro testovací účely.
Spíše mi zaujalo to uklízení SREG. Naprosto chápu, že v případě, že přijde přerušení uprostřed výpočtů tak je dost blbý aby mi v přerušení něco ovlivňovalo SREG. Nicméně pokud mám v přerušení například ADIW a v hlavní smyčce testuji příznak Z, tak si obnovením SREG na konci přerušení tento příznak smažu. Řešením by nejspíše bylo testování ještě v přerušení a nastavení příznaku do vlastního registru. Vím, že přerušení by mělo být co nejkratší aby mělo nějaký smysl, kolik je ještě rozumně dlouhé přerušení a kdy už se zastavit a vymyslet jiný algoritmus?
Jinak díky za praktické rady

Já teda přešel právě z ASM na C, zálohování SREG (POP a PUSH) bylo standard. Taky doporučuji používat pokud se používá přerušení.

A proč bys ho měl mazat ? Příznaky v SREG se nastavují/mažou podle výsledku instrukce, takže mazat Z je holý nesmysl. Prostě uděláš dec registr nebo sbiw dvojregistr a máš nastavený/vynulovaný příznak Z.

Jak bys tohle řešil v reálu, pokud by procesor dělal ještě něco jinýho, než jenom testoval v hlavní smyčce příznak Z ? Tohle je hodně nestandartní postup. Na přerušení a hlavní program musíš koukat jako na zcela nezávislé programy, které sice pracují se stejnými daty, ale v žádném případě se NESMÍ přes SREG vzájemně ovlivňovat. Registr SREG ovlivňují prakticky veškeré instrukce, kromě skoků, load, store a pár dalších (viz datasheet - kapitola “Instruction Set Summary”).

Na to, abys mohl efektivně programovat, musíš vědět, co která instrukce dělá, co ovlivňuje a hlavně jak se chová procesor jako takový. Psal jsi, že se učíš, proto se Ti snažím vysvětlit, nejenom co je jak proveditelné.

Myslím, že bude jednodušší dát sem celý program, aby bylo vidět čeho sem se snažil dosáhnout. Původně jsem měl delay smyčku. Poté jsem to zkusil za použití časovače s inkrementací viz. předchozí příspěvky. Nakonec jsem to přepsal s dekrementací. Smyslem bylo jen krátká obsluha přerušení a zbytek řešit mimo. Přečetl jsem si co která instrukce dělá i co ovlivňuje. Právě proto jsem psal o tom mazání příznaku (evidentně špátná úvaha).
Zde je kod

	.EQU		SET_OCR0 = 24
	.EQU		SET_PORTA = 0b11111111			;datové piny LCD DB0-DB7
	.EQU		SET_PORTC = 0b11100000			;řídící piny LCD RS = bit7, RW = bit6, EN = bit5
 	.EQU		LCD_RS = 7
	.EQU		LCD_RW = 6
	.EQU		LCD_EN = 5
	.EQU		LCD_FUNKCE = 0b00111000
	.EQU		LCD_MOD = 0b00001100
	.EQU		LCD_CLEAR = 0b00000001
	.EQU		LCD_ENTRY = 0b00000110

	.DEF		TMP = R16						;temporary registr
	.DEF		LCD_CMD = R17					;registr pro příkaz na LCD

	.NOLIST
	.INCLUDE "m16def.inc"
	.LIST
	.CSEG

	.ORG 0
	RJMP		RESET

	.ORG OC0addr
	RJMP		PRERUSENI

;NASTAVENÍ ZÁSOBNÍKU
RESET:
	LDI			TMP,LOW(RAMEND)
	OUT			SPL,TMP
	LDI			TMP,HIGH(RAMEND)
	OUT			SPH,TMP

;NASTAVENÍ PORTŮ
	LDI			TMP,SET_PORTA
	OUT			DDRA,TMP
	LDI			TMP,SET_PORTC
	OUT			DDRC,TMP

;NASTAVENÍ ČASOVAČE 0, přerušení 40kHz
	LDI			TMP,SET_OCR0
	OUT			OCR0,TMP
	LDI			TMP,02				;set OCIE0
	OUT			TIMSK,TMP			;TCNT0=OCR0
	LDI			TMP,0b00001010		;set WGM01,CS01
	OUT			TCCR0,TMP

	SEI								;globální povolení přerušení

;INICIALIZACE DISPLEJE
	RCALL		LCD_INIT

;HLAVNI SMYČKA
MAIN:

	RJMP		MAIN

;PODPROGRAM INICIALIZACE DISPLEJE
LCD_INIT:

	LDI			XH,8
	LDI			XL,1				;nastavení zpoždění cca 50ms
	SET_LCD_FCE:

	BRNE		SET_LCD_FCE
	LDI			LCD_CMD,LCD_FUNKCE	;nastavení fce displeje
	RCALL		LCD_EXE
	CLZ								;vymaže příznak Z

	LDI			XH,0
	LDI			XL,2				;nastavení zpoždění cca 50us
	SET_LCD_MODE:
	BRNE		SET_LCD_MODE
	LDI			LCD_CMD,LCD_MOD		;nastavení modu displeje
	RCALL		LCD_EXE
	CLZ								;vymaže příznak Z

	LDI			XH,0
	LDI			XL,2				;nastavení zpoždění cca 50us
	SET_LCD_CLEAR:
	BRNE		SET_LCD_CLEAR
	LDI			LCD_CMD,LCD_CLEAR	;vymazání displeje
	RCALL		LCD_EXE
	CLZ								;vymaže příznak Z

	LDI			XH,0
	LDI			XL,80			;nastavení zpoždění cca 2ms
	SET_LCD_ENTRY:
	BRNE		SET_LCD_ENTRY
	LDI			LCD_CMD,LCD_ENTRY	;nastavení Entry modu
	RCALL		LCD_EXE

	RET								;návrat z inicializace LCD


LCD_EXE:							;provedení příkazu displeje
	OUT			PORTA,LCD_CMD
	SBI			PORTC,LCD_EN
	NOP
	NOP
	NOP
	CBI			PORTC,LCD_EN
	RET

PRERUSENI:
	SBIW		XL,1
	RETI

Tak nějak nevím, kde mám začít a jestli si ze mě neděláš legraci (klid - to chce čas - žádnej učenej z nebe nespadnul, ale občas se čertit musím :smiling_imp:) …

Psal jsem Ti, že psát čísla přímo do programu, že je nepřehledné a nějaké následné korekce jsou jen těžko řešitelné s obrovskou možností zavlečení chyby a Ty tam napíšeš tohle :

[code] .
.
LDI XH,8
LDI XL,1 ;nastavení zpoždění cca 50ms
.
.

.
.
LDI XH,0
LDI XL,2 ;nastavení zpoždění cca 50us
.
.

.
.
LDI XH,0
LDI XL,2 ;nastavení zpoždění cca 50us
.
.

.
.
LDI XH,0
LDI XL,80 ;nastavení zpoždění cca 2ms
.
.[/code]

Co třeba takhle (všimni si, že nejsou ani nutné komentáře…) :

[code].equ Wait50ms = 0x0801
.equ Wait2ms = 0x0050
.equ Wait50us = 0x0002

.
.
LDI XH, High(Wait50ms)
LDI XL, Low(Wait50ms)
.
.

.
.
LDI XH, High(Wait50us)
LDI XL, Low(Wait50us)
.
.

.
.
LDI XH, High(Wait50us)
LDI XL, Low(Wait50us)
.
.

.
.
LDI XH, High(Wait2ms)
LDI XL, Low(Wait2ms)
.
.[/code]

To samé tady :

LDI TMP,02 ;set OCIE0 OUT TIMSK,TMP ;TCNT0=OCR0 LDI TMP,0b00001010 ;set WGM01,CS01 OUT TCCR0,TMP

Podívej se do prvního příspěvku, jak by to mělo vypadat. Když budeš přecházet na jiný procesor, tak může se stát, že čísla budeš muset přepsat. Když to tam budeš mít pomocí názvů jednotivých bitů (např. OCIE0), tak se to upraví samo. Navíc když se bude tento bit v jiném procesoru jmenovat jinak, napíše Ti chybu. Když tam budeš mít číslo, tak se může stát, že strávíš mraky času hledáním, proč Ti to nechodí.


Přerušení :

Vybírat do programu jeden přerušovací vektor ze všech dostupných sice spoří místo, ale přesto se to (většinou) nedělá. Je to hlavně kvůli tomu, kdyby sis náhodně povolil přerušení od jiného zdroje, než od toho, který používáš. Z tohoto důvodu by bylo lepší, kdyby sis místo tohoto :

[code] .ORG 0
RJMP RESET

.ORG OC0addr
RJMP PRERUSENI
[/code]

upravil a použil kód z datasheetu (to už je upravený kód pro ATmega16) :

[code].org 0
jmp RESET ; Reset Handler
jmp EXT_INT0 ; IRQ0 Handler
jmp EXT_INT1 ; IRQ1 Handler
jmp TIM2_COMP ; Timer2 Compare Handler
jmp TIM2_OVF ; Timer2 Overflow Handler
jmp TIM1_CAPT ; Timer1 Capture Handler
jmp TIM1_COMPA ; Timer1 CompareA Handler
jmp TIM1_COMPB ; Timer1 CompareB Handler
jmp TIM1_OVF ; Timer1 Overflow Handler
jmp TIM0_OVF ; Timer0 Overflow Handler
jmp SPI_STC ; SPI Transfer Complete Handler
jmp USART_RXC ; USART RX Complete Handler
jmp USART_UDRE ; UDR Empty Handler
jmp USART_TXC ; USART TX Complete Handler
jmp ADC_INT ; ADC Conversion Complete Handler
jmp EE_RDY ; EEPROM Ready Handler
jmp ANA_COMP ; Analog Comparator Handler
jmp TWSI ; Two-wire Serial Interface Handler
jmp EXT_INT2 ; IRQ2 Handler
jmp TIM0_COMP ; Timer0 Compare Handler
jmp SPM_RDY ; Store Program Memory Ready Handler

;------------------------------------------------------------------------------
; Nepoužité vektory přerušení

EXT_INT0: ; IRQ0 Handler
EXT_INT1: ; IRQ1 Handler
TIM2_COMP: ; Timer2 Compare Handler
TIM2_OVF: ; Timer2 Overflow Handler
TIM1_CAPT: ; Timer1 Capture Handler
;TIM1_COMPA: ; Timer1 CompareA Handler - Použitý vektor je zapoznámkovaný, aby překladač nehlásil duplicitu návěští.
TIM1_COMPB: ; Timer1 CompareB Handler
TIM1_OVF: ; Timer1 Overflow Handler
TIM0_OVF: ; Timer0 Overflow Handler
SPI_STC: ; SPI Transfer Complete Handler
USART_RXC: ; USART RX Complete Handler
USART_UDRE: ; UDR Empty Handler
USART_TXC: ; USART TX Complete Handler
ADC_INT: ; ADC Conversion Complete Handler
EE_RDY: ; EEPROM Ready Handler
ANA_COMP: ; Analog Comparator Handler
TWSI: ; Two-wire Serial Interface Handler
EXT_INT2: ; IRQ2 Handler
TIM0_COMP: ; Timer0 Compare Handler
SPM_RDY: ; Store Program Memory Ready Handler
reti ; Return from accidentaly called interrupt

;------------------------------------------------------------------------------
; Použité vektory přerušení

TIM1_COMPA: ; Timer1 CompareA Handler
…Tady je to Tvoje přerušení…
reti
[/code]

Důležitá poznámka - vždy musíš použít kód z datasheetu procesoru, který programuješ. Jednak nejsou vektory u všech procesorů stejné a jednak jenom některé procesory mají JMP. Některé mají RJMP. Rozdíl mezi nima je v tom, že RJMP zabírá v programové paměti 1 word, JMP zabírá 2 wordy. Například ATmega8 má RJMP a ATmega16 JMP - a to i přesto, že jsou prakticky z jedné řady. Nejmarkantnější je to u řady ATmega48/88/168/328. U této řady jsou názvy a počet vektorů shodné. Jenže ATmega48 a ATmega88 mají vektory od sebe vzdálené jen 1 word (RJMP adresa), ale ATmega168 a ATmega328 mají vektory od sebe vzdálené 2 wordy - JMP adresa (nebo RJMP adresa + NOP). Je to kvůli adresaci. Zatímco RJMP může skákat ±2K wordů - tedy procesory se 4KB nebo 8KB zvládne naadresovat celé, pro adresaci 16KB (8K wordů) už to nestačí. Proto u těchto procesorů tabulka vektorů počítá s instrukcí JMP adresa - tedy 2 wordy (4 byty).


A teďka ke Tvému provedení časování :

Použít 40Khz (25us) čítač kvůli načasování delaye v programu, jehož přesnost není kritická je sice poněkud nezvyklé, nicméně proč ne. V tomto provedení ale musíš (kromě časové náročnosti) počítat s odchylkou až -25us od nastaveného času (pozice Timeru v okamžiku zápisu do “časovacího dvojregistru”). Může se totiž stát, že zapíšeš 2 (50us) do XH:XL takt před přetečením timeru a v tu chvíli máš z 50us jenom 25us, protože první odečtení proběhne těsně po nastavení hodnoty. Program obsluhy přerušení by měl vypadat asi nějak takhle :

TIM1_COMPA: ; Timer1 CompareA Handler in ShovavaciRegistr, SREG sbiw XH:XL, 1 out SREG, ShovavaciRegistr reti

Pak bych napsal čekací smyčku nějak takhle :

Delay: ; Čekací smyčka movw Y, X mov Temp, YH or Temp, YL brne Delay ret

A volání by pak mohlo vypadat takhle :

. . LDI XH, High(Wait50ms) LDI XL, Low(Wait50ms) RCALL Delay . .

Nebo ještě lépe

. . LDI YH, High(Wait50ms) LDI YL, Low(Wait50ms) MOVW X, Y RCALL Delay . .

Instrukce MOVW pro čtení a zápis z/do časovacího registru XH:XL je použita proto, aby nemohlo dojít k tomu, že by v XH:XL byla zapsána jenom polovina dat. A to v případě, že by se stalo, že provedeš instrukci LDI XH, High(Wait50ms) a v tuto chvíli dojde k přerušení. Pokud byla v XL zrovna 0, pak se v přerušení provede dekrementace XH:XL (0x0800-0x0001=0x07FF) a po návratu z přerušení by se provedla instrukce LDI XL, Low(Wait50ms). V tuto chvíli by v XH:XL nebyla hodnota 0x0801, ale 0x0701 - a to je velký rozdíl. V krajním případě, pokud by navíc došlo k dopočítání do 0 a nastavil by se příznak Z, pak by se v (případě Tvého provedení) čekání vůbec neprovedlo.

Další (a asi ještě lepší) variantou by byl následující zápis, používající ProgramStatusRegistr (vyhradíš si registr a používáš ho na indikaci různých stavů a probíhajících akcí v programu) :

TIM1_COMPA: ; Timer1 CompareA Handler in ShovavaciRegistr, SREG sbrs ProgramStatusRegistr, PSR_DelayInAction rjmp TIM1_COMPA_Exit sbiw XH:XL, 1 brne TIM1_COMPA_Exit cbr ProgramStatusRegistr, (1<<PSR_DelayInAction) TIM1_COMPA_Exit: out SREG, ShovavaciRegistr reti

Čekací smyčka by pak mohla být jednodušší :

Delay: ; Čekací smyčka sbr ProgramStatusRegistr, (1<<PSR_DelayInAction) DelayLoop: sbrc ProgramStatusRegistr, PSR_DelayInAction rjmp DelayLoop ret

A volání :

. . LDI XH, High(Wait50ms) LDI XL, Low(Wait50ms) RCALL Delay . .

Výhodou je, že když to uděláš takhle, tak máš XH:XL volné k běžnému použití a nemusíš řešit nedělitelnost zápisu hodnoty do XH:XL, protože přerušení s ní začne pracovat až v okamžiku, kdy nastavíš bit PSR_DelayInAction v registru ProgramStatusRegistr. V tuto chvíli s XH:XL zase nepracuješ v hlavním programu, protože běží čekací smyčka. XH:XL je blokované na odečítání delaye pouze v případě, že je nastavený bit PSR_DelayInAction v registru ProgramStatusRegistr. Těch možností provedení je nepřeberné množství, ale tohle by byly asi dvě nejjednodušší a na rozdíl od toho Tvého řešení i čisté a korektní.

Když jsem psal, že obsluha přerušení musí být co nejkratší, to ještě neznamená, že tam smí být maximálně pár instrukcí, nicméně v rutině obsluhující přerušení by nemělo být žádné čekání. Pokud by bylo nutné na něco čekat, je třeba nastavit si nějaký příznak a přerušení ukončit a v příštím přerušení dokončit rozdělanou práci = rozfázovat si to, co potřebuješ udělat. Ale tímhle se zatím zabývat nemusíš, to přijde na řadu někdy později.