Řekněte pravdu, koho z vás neštve když vás někdo vyruší od práce ? Mě tedy určitě. Něco jiného si o tom myslí naše Attiny :D Ta si v přerušování své práce přímo libuje, čehož je důkazem propracovaný systém přerušení. Nejenže ji tedy můžete přerušit od probíhající práce a přidělit ji dočasně práci novou, vy ji můžete přerušit i během této nové práce a přidělit ji ještě další ! Ale teď vážně. Toto je jedna ze základních odlišností od starých AVR. V moderních AVR můžete jedno (typicky velmi důležité) vámi vybrané přerušení nadřadit ostatním. A to není všechno. K dispozici máte i "round-robin" systém přerušování, který vám může zachránit krk když se vám povede zahltit čip příliš častým voláním některého z přerušení. Nejlépe to pochopíte na příkladech. Pojďme tedy projít klasické "externí" přerušení.
Moderní AVR už nemají specifické piny určené k externímu přerušení (jako byly INT0,INT1,INT2 atd.). Podoba externích přerušení se spíš podobá tomu co znáte ze starých AVR jako PCINT (Pin change interrupt). Každému pinu v registru PINnCTRL smíte skupinou bitů ISC (Input Sense Configuration) nastavit jaká událost má (či nemá) volat přerušení. Možnosti jsou následující
Chování všech přerušení řídí CPUINT (CPU Interrupt Controller). V jeho konfiguračním registru CTRLA můžete bitem LVL0RR zapnout již zmíněné round-robin schéma (o němž si víc povíme v příkladu). Bitem CVT zredukujete všech 25 vektorů přerušení na tři. Na nemaskovatelné přerušeni (NMI), které vás zatím nemusí zajímat. Na prioritní přerušení LVL1 a na jednu rutinu pro všechna ostatní přerušení. Smyslem je asi ušetřit trochu místa ve flash paměti a my se touto funkcí zabývat nebudeme. Dovolím si přeskočit smysl bitu IVSEL protože mu nerozumím. V registru STATUS si pomocí bitů LVL0EX, LVL1EX a NMIEX můžete zjistit které ze tří typů přerušení probíhá. LVL0 jsou "běžná" přerušení (tedy všechny timery, ADCčka, RTCčka, USARTy atd.). Prioritní přerušení (LVL1) je jen jedno a volíte si ho v registru LVL1VEC. Zapíšete tam číslo vektoru přerušení který má mít vyšší prioritu. Například pokud bych chtěl prioritně obsloužit příjem znaku po USARTu, zvednu prioritu vektoru USART0_RXC (č. vektoru 22) - viz tabulka Table 7-2. Interrupt Vector Mapping. Zajistím si tak, že i kdyby program vykonával rutinu nějakého jiného přerušení, odskočí si do rutiny od USARTu a vyzvedne znak. Zkušenosti z ARMů (běžně vybavených prioritami přerušení) mi dávají tušit, že tohle bude užitečné.
Všechny klíčové funkce si předvedeme na jednom bohatě ilustrovaném příkladě. Na piny PB0 a PC0 přivedeme z vnějšku signál (v mém případě z generátoru) a připravíme si několik modelových situací. Naše aplikace bude detekovat vzestupnou hranu na obou pinech a volat externí přerušení. V rutině přerušení od PORTB (tedy od PB0) nastaví pin PB2 do log.1, vymaže příslušnou vlajku, začne vykonávat nějakou "užitečnou" práci, spočívající v tupém čekání (200us) a pak vynuluje PB2. Stav PB2 nám tedy bude vyznačovat kdy rutina začala a kdy skončila. Totéž se bude odehrávat v rutině přerušení od PORTC (tedy od PC0), jen indikaci provedeme na pinu PC2. Při prvním pokusu necháme obě přerušení typu LVL0. Kód je vcelku jednoduchý.
/* tutorial TinyAVR 1-Series * Externí přerušení * PB0, PC0 vstupy pro vnější signály (z generátoru) * PB2, PC2 výstupy indikující "užitečnou aktivitu" * * Ve fuses zvolen 20MHz oscilátor (lze volit ještě 16MHz) */ #define F_CPU 20000000UL #include <util/delay.h> #include <avr/io.h> #include <avr/interrupt.h> void clock_20MHz(void); int main(void){ clock_20MHz(); // jedeme na plný výkon // PC0 a PB0 vstupy PORTB.DIRCLR = PIN0_bm; PORTC.DIRCLR = PIN0_bm; // přerušení na vzestupnou hranu, pull-up protože nechceme nechat pin "v luftě" PORTC.PIN0CTRL = PORT_PULLUPEN_bm | PORT_ISC_RISING_gc; PORTB.PIN0CTRL = PORT_PULLUPEN_bm | PORT_ISC_RISING_gc; // PB2,PC2 výstupy pro indikaci činnosti PORTB.DIRSET = PIN2_bm; PORTC.DIRSET = PIN2_bm; sei(); // globální povolení přerušení while (1){ // není co dělat... } } // Rutina přerušení od PORTB (PB0) ISR(PORTB_PORT_vect){ PORTB.OUTSET = PIN2_bm; // indikovat vstup do rutiny přerušení PORTB.INTFLAGS = PORT_INT0_bm; // vyčistit vlajku přerušení od PB0 _delay_us(200); // "užitečná" činnost PORTB.OUTCLR = PIN2_bm; // vypnout indikaci } // Rutina přerušení od PORTC (PC0) ISR(PORTC_PORT_vect){ PORTC.OUTSET = PIN2_bm; // indikovat vstup do rutiny přerušení PORTC.INTFLAGS = PORT_INT0_bm; // vyčistit vlajku přerušení od PC0 _delay_us(200); // "užitečná" činnost PORTC.OUTCLR = PIN2_bm; // vypnout indikaci } // Nastaví clock 20MHz (z interního 20MHz bez děličky) void clock_20MHz(void){ // v případě potřeby zde dočasně vypněte přerušení CCP = CCP_IOREG_gc; // odemyká zápis do chráněného registru CLKCTRL.MCLKCTRLA = CLKCTRL_CLKSEL_OSC20M_gc; // vybírá 20MHz oscilátor CCP = CCP_IOREG_gc; // odemyká zápis do chráněného registru CLKCTRL.MCLKCTRLB = 0; // vypne prescaler (děličku) }
Teď na oba vstupy (PB0,PC0) přivedeme dvojici vzestupných hran. S předstihem necelé mikrosekundy přijde hrana na PB0 a chvíli po ní hrana na PC0. Nejprve se tedy začne vykonávat přerušení od PORTB, což poznáme podle 200us pulzu na PB2. Po celou dobu čeká aktivované přerušení od PORTC až se dostane na řadu. Jakmile skončí rutina od PORTB, ihned se rozběhne rutina od PORTC a vygeneruje 200us pulz na PC2. Že se tak stane si můžete prohlédnout na dvou následujících oscilogramech. Všimněte si také jak dlouho trvá než dojde dojde k nastavení PB2 do log.1. Uplyne přibližně 1us, což při 20MHz odpovídá cca 20ti instrukcím. Tak velký je "overhead" rutiny přerušení. Pro velmi jednoduché akce se dá tenhle "overhead" obejít (parametr NAKED u rutiny přerušení), ale to říkám jen pro úplnost (bez znalosti assembleru to nedělejte).
V dalším pokusu pošleme vzestupnou hranu nejprve na PC0 a zlomek mikrosekundy poté na PB0 (tedy v opačném pořadí než v předchozím pokusu). Nejprve se začne vykonávat rutina od PORTC a vytvoří pulz na PC2. Přerušení od PORTB čeká na dokončení první rutiny a teprve pak se pouští do akce a vytváří pulz na PB2. Výsledek si můžete prohlédnout na následujícím oscilogramu.
Teď si zkusíme co se stane když budeme hranu na PB0 posílat příliš často (6kHz). Protože obsluha trvá přibližně 200us, nestihne skončit před příchodem další hrany a přerušení se tak bude vykonávat stále dokola. Dojde k zahlcení a nastane nemilá situace. Nezbude žádný čas na reakci na signály z PC0 ! Během vykonávání zdlouhavé "užitečné" činnosti v rutině od PORTB přijde nová hrana na PB0 a pak i hrana na PC0. Jakmile rutina od PORTB skončí, čekají ve frontě na obsloužení dvě přerušení (od PORTB i od PORTC). Podle jakého klíče se rozhodne která z nich se má vyvolat ? Podle stejného jako na starých AVR. To přerušení které má nižší vektor (Table 7-2) má přednost. V našem případě je to tedy opět PORTB. Kvůli tomu ale náš program "nikdy" neobslouží přerušení od PORTC. A to je škoda. Už takhle vynechává přibližně každé šesté přerušení z PB a asi by jste byli určitě raději kdyby se čas od času dostalo i na PORTC.
Tohle je přesně ta situace, kterou zachraňuje round-robin schéma. Zapneme ho jednoduchou dvojicí příkazů
CCP = CCP_IOREG_gc; // odemyká zápis do chráněného registru CPUINT.CTRLA = CPUINT_LVL0RR_bm; // zapíná round-robin Priority
Pokud situaci otočíme a zahltíme vzestupnými hranami PC0, není to takový problém i bez round-robin. Díky tomu, že má přerušení od PORTB vyšší prioritu, najde se na něj po skončení rutiny od PORTC vždycky čas. Takže jen pro kompletnost oscilogram, že to tak opravdu je
Teď si představte, že externí přerušení od PC0 je kritická funkce, která musí reagovat s co nejmenší latencí (má například vypnout nějaký výkonový prvek a podobně). V takovém případě se nabízí přiřadit přerušení od PORTC vysokou prioritu. Stačí vektor PORTC_PORT_vect_num (5) zapsat do registru CPUINT.LVL1VEC. Tedy obohatit naši aplikaci o jediný řádek.
CPUINT.LVL1VEC = PORTC_PORT_vect_num;
Systém perušení se nezdá složitý. I způsob externího přerušení vypadá o poznání flexibilněji než na starých AVR. Čas a praxe ukážou jestli je to pravda. Doufám že jsem informace podal srozumitelnou formou a že jsem vás tímto tutoriálem posunul zase o krůček směrem k moderním AVR. Na shledanou u dalších dílů :)
Home
| V1.01 21.1.2019 /
| By Michal Dudka (m.dudka@seznam.cz) /