logo_elektromys.eu

/ Přerušení I |

Ř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í.

/ 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í

Všechny piny z jednoho portu sdílejí jednu rutinu přerušení (opět shodný mechanismus jako PCINT na starých AVR). Pokud si zapnete na jednom portu přerušení od více pinů, musíte si v rutině sami zjistit který z pinů událost vyvolal. K tomu vám slouží registr INTFLAGS. Nastavený bit v tomto registru vás informuje, že na daném pinu nastala vámi zvolená událost. Jakmile to vezme program na vědomí, musí příslušnou vlajku smazat (zápisem log.1). Když to neudělá bude se přerušení volat stále dokola.

/ Interrupt Controller |

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é.

/ Příklad A) |

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).

Modelová situace kdy hrana na PB0 přichází o něco dřív jak hrana na PC0. Rutina PORTB se vykonává jako první.
Detail na okamžik příchodu hran na vstupy (žlutá a červená).

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.

Opačná situace - hrana na PB0 přichází o něco později jak hrana na PC0. Rutina PORTC se vykonává jako první.

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.

Zahltíme PB2, program tráví všechen svůj čas v rutině od PORTB a rutina od PORTC se nikdy nedostane na řadu

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

Přístup do CPUINT.CTRLA je chráněný a z kapitoly o clocku už víte, že před přístupem ho musíme odemknout v CPP. Teď bude průběh našeho zahlcení vypadat o něco optimističtěji. Na PB0 přijde hrana, aktivuje přerušení od PORTB. V tom okamžiku se do registru LVL0PRI automaticky zapíše číslo tohoto vektoru přerušení čímž se mu dočasně sníží priorita. Během výkonu "užitečné práce" přijde další hrana na PB0 i na PC0. Rutina od PORTB skončí a rozhoduje se o tom které ze dvou čekajících přerušení se obslouží teď. Protože PORTB má dočasně sníženou prioritu na "nejnižší" začne se vykonávat rutina od PORTC. S jejím začátkem se jí zase dočasně sníží priorita, což nás ale už nemusí zajímat. Tím pádem se zpět "obnoví" běžná priorita pro PORTB. A vše se pak rozběhne nanovo. Tenhle mechanismus dokáže zcahránit situaci v níž je jedno přerušení zahlceno. Pokud bychom zahltili i PC0, byl by to definitivní konec, žádné další přeuršení s nižší prioritou (což sjou až na PORTA a Brown-out všechny) už nedostane šanci. I tak je to ale slušná pomůcka. Výsledek může te vidět níže.

Round-Robin mechanismus zachraňuje situaci se zahlcením PB

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

Zahlcené PC0 externím přerušením, i bez round-robin se na přerušení od PORTB najde čas (protože má přirozeně vyšší prioritu)

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;

Od této chvíli reaguje software na vzestupnou hranu na PC0 ihned a to i za cenu toho, že přeruší právě probíhající rutinu přerušení. Zkusíme si to tak, že na PC0 pošleme vzestupnou hranu opožděnou o pár desítek mikrosekund za hranou na PB0. Nejprve se tedy začne zpracovávat přerušení PB0, ale po cca 70us "užitečné činnosti" se vyvolá přerušení od PC0, program odskočí do rutiny s vysokou prioritou a vykoná "užitečnou" práci indikovanou pulzem na PC2. Pak se vrátí a dokončí svou činnost v rutině od PORTB. Pulz na PB2 bude dlouhý 400us, protože v sobě bude obsahovat svých 200us + 200us které program tráví v prioritní rutině od PORTC.

Přerušení od PC0 má prioritu LVL1 a může přerušit probíhající přerušení od PB0

| Závěr /

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ů :)

| Odkazy /

Home
| V1.01 21.1.2019 /
| By Michal Dudka (m.dudka@seznam.cz) /