Externí přerušení nejen s Attiny24

Úvodem...

V článku budu předpokládat, že máte jisté minimální zkušenosti s programováním AVR v jazyce C, dále předpokládám že zvládáte ovládání I/O portů a že máte představu co je to přerušení. Zdrojové kódy budou psány pro AVR studio . Kdo programuje například v Codevision, bude mít jiné názvy rutin přerušení a typicky využije Codewizzard, který za něj externí přerušení nastaví. Bude-li chtít kód použít, bude si jej muset upravit. Ukázky budu provádět na čipu Attiny24A protože jsou levné a mám jich slušnou zásobu. Ovládání bude ale principiálně podobné jako na většině čipů řady Atmega (ty budou ale bohatěji vybaveny).

K čemu je přerušení ?

Napíšu to jen ve zkratce. V principu k tomu samému jako jakékoli jiné přerušení. Umožňuje mikrokontroléru (zkráceně čipu) reagovat na asynchronní událost. Kdyby nebylo přerušení, čip by vykonával program instrukci za instrukcí a pokud by jste chtěli nějak reagovat na vnější událost, museli by jste neustále kontrolovat zda nenastala. V případě časovače by jste se pořád museli dívat na vlajku zda nepřetekl, u AD převodníku by jste stále museli kontrolovat zda nedokončil převod, u přijímání dat sériovou linkou by jste neustále museli sledovat zda vám nedorazila nová data. Takový styl programování je nejen komplikovaný ale také nespolehlivý. Představte si, že máte provádět relativně náročný výpočet, na který sice nespěcháte, ale trvá vám třeba 5 ms. Ale zároveň chcete kontrolovat příchod dat z vnějšku (třeba z počítače po sériové lince). Sledujte modelovou situaci, zahájíte výpočet, během něj vám přijde první znak zprávy a čeká na vás, vy ale počítáte, chvíli nato přijde druhý znak zprávy ... vy stále počítáte. Přirozeně druhý příchozí znak smaže ten první, protože jste si ho nevyzvedli. Vy totiž počítáte. Aby jste se toho vyvarovali museli by jste do výpočtu vkládat kód který bude sledovat příchod dat po sériové lince. Zdrojový kód takového výpočtu by byl úplným rodištěm různých chyb, byl by neuvěřitelně nepřehledný, těžko modifikovatelný a typicky i nespolehlivý. A přesně proto je tu institut přerušení. Po vyvolání přerušení program odskočí do jiné části vašeho kódu kde vykoná vše potřebné a pak se vrátí na své místo a pokračuje kde přestal (ve vašem případě ve výpočtu).

K čemu může být externí přerušení ?

Zdrojů přerušení, tedy i událostí, jež mohou nějaké přerušení vyvolat, má čip hodně. Jedním z nich je externí přerušení a jak už název napovídá, bude sloužit k reakci na vnější událost. Modelových situací je mnoho. Začneme nějakou rozumnou. Měříme zrychlení čidlem ADXL345. Než abychom se čidla stále dokola doptávali zda už má k dispozici nová data (tzv polling), nakonfigurujeme ho tak aby nám na jednom drátě poslalo logickou úroveň (typicky 0V) ve chvíli kdy bude mít data připravena. My si nakonfigurujeme externí přerušení a kdykoli jsou dat připravena můžeme je okamžitě z čidla vyčíst a zpracovat. Jiný příklad. Používáte teploměr LM75 k detekci přehřátí, když ho vhodně nakonfigurujete, bude také schopen jedním vývodem signalizovat přehřátí (nebo podchlazení). Váš čip se opět nebude muset ptát na teplotu a kontrolovat ji sám, pouze počká na přerušení. Podobná situace může nastat s obvody RTC (hodiny, kalendáře). RTC může pomocí jednoho výstupu čipu signalizovat třeba naprogramovaný alarm (čas vstávat do práce/školy !). Další využití najde externí přerušení v časově kritických aplikacích. V příkladu s RTC vám je celkem jedno jestli budík zazvoní o pár milisekund dříve či později. Můžete, mít ale například aplikaci která přepíná zdroj napájení. Jakmile zadetekuje, že primární zdroj přestává dodávat energii (například pokles napětí, nebo signalizace o poruše) budete potřebovat rychle přepnout na záložní zdroj. Externí přerušení je schopno reagovat už během mikrosekundy. Další užitečné použití externího přerušení najdete u "low power" aplikací. Čip můžete uspat aby jste snížili odběr (klidně na zlomky uA). A jen čas od času ho ze spánku probrat a provést nějakou akci. Čip může spát, čekat na stisk tlačítka (který vyvolá externí přerušení), probrat se a vykonat nějakou akci. Například po stisku změří hodnotu, zobrazí ji na displeji a zase usne. Případně čeká na vyvolání alarmu, pomocí externího přerušení se probudí ze spánku, aktivuje GSM modul a pošle vám SMSku. V poslední řadě bych zmínil ještě třeba obsluhu rotačního enkodéru ("potenciometr" co se může točit furt dokola a má "kroky"). Kdo nevíte co to je strčte si do googlu "rotary encoder". U některých typů enkodérů vám stačí jeden jeho kanál připojit na externí přerušení a v rutině přerušení pak jen sledovat úroveň druhého kanálu a přičítat nebo odečítat kroky.

Co máme na Attiny24 k dispozici ?

K dispozici máme jedno plnohodnotné přerušení na pinu PB2 (INT0). Mikrokontrolér Atmega32 má tři, Atmega128A dokonce osm. Čip umí na INT0 detekovat 4 různé události. Prvně může detekovat přítomnost logické úrovně 0. Přerušení je pak voláno stále dokud není na INT0 zpět log.1. V tomto režimu (a pouze v tomto) je možné budit čip z hlubokého spánku (z režimů POWER DOWN a STAND BY, které mají největší úsporu energie). Dále je možné pomocí INT0 detekovat nástupnou hranu (přechod z log.0 do log.1), sestupnou hranu a jakoukoli změnu (tedy jakoukoli hranu). V těchto třech případech je rutina přerušení zavolána vždy jen jednorázově. Další volání rutiny přerušení proběhne až po detekování další události. Jisté trable mohou nastat pokud přijdou události v rychlém sledu za sebou. To si ale rozebereme později. Krom INT0 nám čip nabízí ještě takzvané "Pin Change Interrupt - PCINT". Pomocí něj může čip reagovat na změnu logické hodnoty na libovolné skupině vstupů. PCINT nemá takový komfort jako INT0, můžeme detekovat jen změnu na vstupu, nikoli třeba konkrétně sestupnou hranu. Chcete-li tedy reagovat pouze na sestupnou hranu pomocí PCINT, musíte si v rutině přerušení situaci ošetřit a sestupnou hranu rozpoznat, přerušení totiž bude vyvoláno i nástupnou hranou. Pozitivní na celé věci je to, že PCINT může hlídat libovolné piny procesoru. Chcete-li tedy detekovat změnu na PB1, PA2 a PA3, máte možnost. Piny pro PCINT jsou rozděleny do dvou skupin. První skupinou jsou piny 0 až 7, ve druhé skupině pak piny 8 až 11 (v případě jiných čipů může být skupin více). Každá skupina má svoji vlastní rutinu přerušení. Tady vás opět může čekat komplikace. Spustíte-li externí přerušení na PA1,PA2 a PA3 (tedy PCINT1, PCINT2 a PCINT3) zavolá se jedna jediná rutina přerušení a vy budete muset sami rozpoznat který z pinů ji vyvolal. Není to až tak obtížné, ale stojí to za trochu rozmyšlení ... které nechám na vás :) Ve stručnosti je to asi tak vše co vám systém externího přerušení na Attiny24 nabízí.

A teď trochu detailněji

Tak jako všechny periferie mikrokontroléru i externí přerušení se řídí nastavováním a čtením bitů ve speciálních registrech. Používat budete tyto:

MCUCR – MCU Control Register
76543210
BODSPUDSESM1SM0BODSEICS01ICS00

Zde pomocí dvojice bitů ICS01 a ICS00 konfigurujete přerušení z ICT0. Všechny čtyři již výše zmiňované kombinace jsou shrnuty v následující tabulce.

Konfigurace INT0
ISC01ISC00Popis
00Log.0 na INT0 vyvolá přerušení
01Jakákoli změna na INT0 vyvolá přerušení
10Sestupná hrana na INT0 vyvolá přerušení
11Nástupná hrana na INT0 vyvolá přerušení

V registru GIMSK pomocí bitů INT0, PCIE1 a PCIE0 konfigurujete které z přerušení chcete povolit nebo zakázat. Zapsáním log.1 příslušné přerušení povolujete, zapsáním log.0 zakazujete. I když přerušení zakážete, může detekce události (tedy například sestupné hrany na ICT0) běžet. Projeví se pak v registru GIFR. O přerušení z pinů 0 až 7 (PCINT0 až PCINT7) se stará bit PCIE0 o piny 8 až 11 pak bit PCIE1. Bit INT0 povoluje a zakazuje přerušení od INT0.

GIMSK – General Interrupt Mask Register
76543210
-INT0PCIE1PCIE0----

Registr GIFR signalizuje stav vlajek přerušení. Jestliže je detekována vybraná událost (například nástupná hrana), dojde k nastavení vlajky v tomto registru. Pokud není přerušení povoleno ať už v registru GIMSK nebo jsou všechna povolení globálně zakázána (existuje na to příkaz cli()), skončí celá akce pouze nastavením vlajky. Nastavením vlajky rozumíme nastavení příslušného bitu do log.1. Váš program může z tohoto registru číst a stav vlajek sledovat a dle potřeby na to reagovat. Pokud jsou přerušení povolena, jakmile dojde k nastavení vlajky je ihned volána rutina přerušení. Jakmile do ní program vstoupí, vlajka se automaticky smaže (nastaví do log.0). Od tohoto okamžiku je možné detekovat další událost. Události vám mohou přijít ale v tak rychlém sledu, že na ně čip nebude schopen zareagovat. Prvním příkladem může být situace kdy chcete pomocí externího přerušení počítat kolik vám přišlo pulzů. Což není elegantní, na to máte čítač. Přijde vám první pulz, dojde k nastavení vlajky a než se procesor dostane do rutiny přerušení, stihne vám přijít ještě jeden pulz. A ten vám unikne. Přišel totiž v době kdy byla ještě stále nastavena vlajka indikující příchod prvního pulzu. Procesoru může několik mikrosekund trvat než se dostane do rutiny přerušení a po celou tu dobu je vlajka nastavena a čip není schopen rozpoznat další událost. Situace může být o to horší, pokud procesor zrovna zpracovává rutinu jiného přerušení, pak si typicky musíte počkat než ji dokončí (ale tohle jde změnit). Jestliže povolíte přerušení a zůstala vám vlajka z nějakého důvodu nastavena, dojde ihned k zavolání rutiny přerušení. To se typicky stává když celý systém inicializujete. Spustíte procesor, začnete nastavovat porty, časovače a kdo ví co ještě a mezi tím se mimo procesor může dít spoustu věcí. Třeba mu můžou na vstup přicházet signály. Takže před inicializací externích přerušení může být vhodné si vlajku pro jistotu smazat. Vlajku můžete mazat tím, že do ní zapíšete log.1. Tohle platí až na výjimky obecně o všech vlajkách všech periferií. Přirozeně můžete mazat více vlajek najednou zápisem příslušné binární hodnoty do registru. Jak rychle reaguje procesor na externí událost zmíním dále v textu. Bit INTF0 indikuje log.1 detekci nastavené události na INT0, bit PCIF1 změnu na povolených pinech v rozsahu PCINT8 až PCINT11. Analogicky bit PCIF0 signalizuje log.1 že nastala změna na povolených pinech z rozsahu PCINT0 až PCINT7.

GIFR – General Interrupt Flag Register
76543210
-INTF0PCIF1PCIF0----


Jistě si dovedete představit, že musí existovat způsob jak si vybrat na kterých pinech chceme PCINT sledovat. Pokud bychom ho sledovali na všech, tak by jakákoli změna jakéhokoli pinu z jakéhokoli důvodu měla za následek vyvolání přerušení. Takže by jste například vysílali data z čipu ven, procesor by měnil hodnotu třeba na PA2 (což je PCINT2) a s každou změnou by mu přišlo přerušení, že na pinu došlo ke změně. To by bylo šílené. Aby jste si mohli filtrovat, které piny chcete sledovat, slouží vám k tomu dva registry PCMSK1 a PCMSK0. Jejich obsah je přesně takový jaký by jste očekávali. Zapsáním log.1 do příslušného bitu zapínáte sledování na příslušném pinu. Zapsáním log.0 jej vypínáte.

PCMSK1 – Pin Change Mask Register 1
76543210
----PCINT11PCINT10PCINT9PCINT8

PCMSK0 – Pin Change Mask Register 0
76543210
PCINT7PCINT6PCINT5PCINT4PCINT3PCINT2PCINT1PCINT0


Pro kompletní povolení externích přerušení na pinech PA2 a PA3 tedy potřebujete nastavit jedničky do bitů PCINT2 a PCINT3, dále musíte povolit přerušení bitem PCIE0. Obecně pak musíte mít ještě globálně povolená přerušení (pomocí fce sei()), ale to už by jste měli znát. Všechna tři přerušení by měla být schopna probrat čip z režimu spánku. INT0 to dokáže ale jen pokud je nastaveno jako detekce log.0.

Jak na to prakticy ?

Začneme první ukázkou. Předvedeme si přerušení na sestupnou hranu INT0 a změříme si za jak dlouho na něj čip zareaguje. Uspořádání bude jednoduché. INT0 se nachází na pinu PB2. Nakonfigurujeme ho jako vstup a zapneme pull-up rezistor. Ten zajistí, že na vstupu bude log.1. Tlačítko pak připojíme mezi PB2 a zem. Stiskem tlačítka se PB2 spojí se zemí a na PB2 bude log.0. Uvolněním tlačítka se pin díky pull-up rezistoru zase vrátí do log.1. Při testech je ale dobré si uvědomit, že tlačítko není nijak filtrované, může tedy zakmitávat (bouncing). Záměrně nějaký bouncing ukážu pro případ že nemáte tu zkušenost (obrázek č.1). Na spuštění přerušení může stačit i velmi krátký jehlový impulz. Minimální šířka může záviset na typu sledované události a na taktu procesoru. Toto je jedno z největších úskalí při použití externího přerušení na detekci stisku tlačítka. Musíte hardwarově nebo softwarově ošetřit zákmity. Třeba na obrázku č.1 je průběh, kde by klidně mohlo dojít k volání rutiny přerušení třikrát, pokud by byla nastavena na detekci nástupné hrany. Podobné problémy může způsobovat i elektrické rušení. V takových případech je výhodné detekovat stisk tlačítka raději pollingem (tedy se například 50x za vteřinu koukat zda je tlačítko stisknuto). Pravděpodobnost, že se s dotazem trefíte právě do nějakého jehlového impulzu (vzniklého ať rušením nebo zákmitem) je mizivá. Obsluha tlačítko většinou stiskne na rozumně dlouhou dobu. Nevýhodou je, že si musíte softwarově ošetřit detekci právě jednoho stisku. Tlačítko je v této situaci voleno spíše pro snadnou dostupnost. Klidně bychom mohli přerušení vyvolávat generátorem.

Obrázek č.1 - zákmity tlačítka

A teď k programu. Komentáře jsou samovysvětlující, ale pro jistotu. Nejprve konfiguruji PB2 jako vstup s pullup rezistorem, PB0 konfiguruji jako výstup. V rutině přerušení si pak na PB0 vytvořím krátký pulz abych na osciloskopu viděl, že čip zareagoval. Klidně si můžete na PB0 připojit LED a blikat s ní. Rutinu přerušení si ale budete muset upravit, tak krátké bliknutí by jste asi okem nezaznamenali. Déle provedeme konfiguraci MCUCR v něm nastavím do log.1 bit ISC01 čímž vyberu detekci sestupné hrany. Dále pak v registru GIMSK nastavením bitu INT0 povolím přerušení od INT0. Následně ještě povolím přerušení globálně. Pak už ve while smyčce nedělám nic. asm("nop"); je příkaz pro "no operation". Mám ho zde jen kvůli snazšímu debugu. Vy pravděpodobně program debugovat nebudete, takže můžete nechat smyčku prázdnou.

// Přerušení na sestupnou hranu na INT0
#include <avr/io.h>
#include <avr/interrupt.h>

ISR(EXT_INT0_vect){
						              // vytvoříme krátký pulz
	PORTB |= (1<<PORTB0);   // nastavujeme na PB0 log.1
	PORTB &= ~(1<<PORTB0);  // PB0 do log.0, zakomentujte pokud chcete jednorázově rozsvítit třeba LED
	}

int main(void){
DDRB |= (1<<DDB0);       // nastavujeme PB0 jako výtup
DDRB &= ~(1<<DDB2);      // nastavujeme PB2 jako vstup
PORTB |= (1<<PORTB2);    // na PB2 zapínáme interní pullup rezistor
MCUCR |= (1<<ISC01);     // nastavujeme přerušení na sestupnout hranu
GIMSK |= (1<<INT0);      // povolujeme přerušení INT0
sei();   // globální povolení přerušení

while(1){
	asm("nop");        // nic nedělej - pro snazší ladění
	}

}

Na následujícím obrázku uvidíte průběh. Žlutá stopa představuje napětí na tlačítku. Modrá stopa pak napětí na PB0, tedy na výstupu z čipu. Při frekvenci 1MHz je vidět rychlost odezvy 19.6us. Je potřeba si uvědomit, že první operace v rutině přerušení se ale neskládá z jediné instrukce. Jde o čtení, oření (logický součet) a opětovný zápis. Pokud mě paměť na assembler neklame bude potřeba instrukce čtení, pak oření a následně zápis. Tedy nejméně tři instrukce. Při taktu 1Mhz jsou to nejméně 3us. Měli by jste tedy brát změřenou hodnotu s rezervou, jen jako řádový odhad.

Obrázek č.2 - Reakce na stisk pomocí INT0 s 1MHz taktem (v aktivním režimu)

Zkusme si přerušení nakonfigurovat jako detekci na nízkou úroveň na vstupu. Bude se nám to hodit k probouzení z režimu spánku. Zdrojový kód zůstane v podstatě stejný. Jen změníme hodnotu bitů ISC01 a ISC00 v registru MCUCR na nuly. Po startu procesoru by měly být tyto hodnoty nulové, nemuseli bychom tedy ručně obě hodnoty nulovat tak jak je ve zdrojovém kódu. Pro názornost a pro obecné použití kdekoli jinde, kde už můžete mít v MCUCR něco zapsáno jsem ale příklad ponechal takto. Opět nehledejte v kódu žádné složitosti. Ty totiž budou až v průběhu za ním. Vzpomeňme na to, že detekce nízké úrovně volá přerušení tak dlouho dokud je vstup INT0 v log.0. A to je po celou dobu co je stisknuté tlačítko. Takže na obrázku č.3 vidíte opakující se sekvenci pulzů. Jakmile přerušení skončí je ihned voláno znovu a tak stále dokola až do uvolnění tlačítka. Tady by přirozeně stálo za to přerušení zablokovat vynulováním bitu INT0 v registru GIMSK. A povolit jej až po uvolnění tlačítka nebo obecně po umlčení zdroje přerušení.

// přerušení INT0 na nízkou úroveň
#include <avr/io.h>
#include <avr/interrupt.h>

ISR(EXT_INT0_vect){
						// vytvoříme krátký pulz
	PORTB |= (1<<PORTB0);  // nastavujeme na PB0 log.1
	PORTB &= ~(1<<PORTB0);   // PB0 do log.0
	}

int main(void){
DDRB |= (1<<DDB0);       // nastavujeme PB0 jako výtup
DDRB &= ~(1<<DDB2);      // nastavujeme PB2 jako vstup
PORTB |= (1<<PORTB2);    // na PB2 zapínáme interní pullup rezistor
MCUCR &= ~((1<<ISC01) | (1<<ISC00));     // nastavujeme přerušení na Low Level (nulujeme ICSC01 a ICSC00)
GIMSK |= (1<<INT0);   // povolujeme přerušení INT0
sei();   // globální povolení přerušení

while(1){
	asm("nop");			// nic nedělej - pro snazší ladění
	}

}
Obrázek č.3 - Reakce na stisk pomocí INT0 s detekcí nízké úrovně

Za ukázku ještě stojí kód předvádějící probuzení z režimu spánku. Není to cílem článku, takže ve stručnosti shrnu, že máte k dispozici defakto dva režimy spánku. Mělký spánek IDLE ze kterého vás může probudit každé přerušení, probuzení je rychlé ale režim nesníží odběr nijak závratně. Hluboký spánek (režim POWER DOWN), ze kterého vás může probudit jen externí přerušení, restart a nebo watchdog, probuzení je relativně pomalé, ale odběr klesá velmi rapidně. My budeme chtít přirozeně zamachrovat a když nám to INT0 umožňuje necháme procesor hluboce usnout. A podíváme se za jak dlouho se nám ho podaří probrat. Na funkce související se spánkem se dívejte jako na kouzelné formule, k jejich popisu se snad dostaneme v jiném článku. Kdo by nevěřil že mu čip spí, tak ať si zkusí připojit k napájení ampérmetr, pokud na to bude mít rozsah může zjistit, že čip při 5V odebírá přibližně 0.6uA a při 1.8V jen 0.28uA. Nezapomeňte u toho ale odpojit programátor. ;)

// přerušení na Low Level s probuzením z režimu spánku
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/sleep.h>

ISR(EXT_INT0_vect){
						// vytvoříme krátký pulz
	PORTB |= (1<<PORTB0);	// nastavujeme na PB0 log.1
	PORTB &= ~(1<<PORTB0);	// PB0 do log.0
	}

int main(void){
DDRB |= (1<<DDB0);       // nastavujeme PB0 jako výtup
DDRB &= ~(1<<DDB2);  // nastavujeme PB2 jako vstup
PORTB |= (1<<PORTB2); // na PB2 zapínáme interní pullup rezistor
MCUCR &= ~((1<<ISC01) | (1<<ISC00));     // nastavujeme přerušení na Low Level (nulujeme ICSC01 a ICSC00)
GIMSK |= (1<<INT0);   // povolujeme přerušení INT0
sei();   // globální povolení přerušení
set_sleep_mode(SLEEP_MODE_PWR_DOWN);   // vybíráme režim spánku

while(1){
	asm("nop"); // nic nedělej - pro snazší ladění
	sleep_mode(); // dobrou noc...                                                
	}
}
Obrázek č.4 - Reakce na INT0 z režimu Power Down

Jak vidíte na průběhu probuzení čipu z režimu spánku přidalo v tomto případě asi 10us k původním necelým 20us. Orientační hodnoty jsou v následující tabulce. Většinou vás ale nebudou vůbec zajímat.

Srovnání doby příchodu do rutiny přerušení
NapětíTaktRežim spánkuPřibližný čas probrání
5V8MHZnespí2.2 us
5V8MHZIdle2.8 us
5V8MHZPower Down5.9 us
5V1MHZnespí19.6 us
5V1MHZIdle23.3 us
5V1MHZPowerDown31.6 us

Pokračujeme...

Když jsme si vyzkoušeli INT0, měli bychom si vyzkoušet i PCINT a rovnou si u toho ukážeme co se bude dít když dojde k vyvolání dvou přerušení "zároveň". Postup nastavení přerušení jsme probrali v předešlém textu, takže by jste měli rozumět předloženému kódu. V rutinách přerušení jsme si schválně udělali malou smyčku aby její vykonávání zabralo nějaký čas. Z toho co za chvíli uvidíte pochopíte, že by jste rutinu měli psát co nejkratší. Čím kratší tím lepší. Neodpustím si dvě malé poznámky. ISR znamená Interrupt Service Routine, tedy v češtině přesně to co tu pořád omíláme - rutina obsloužení přerušení. Slovo volatile před deklarací proměnné slouží k tomu aby jsme překladači vysvětlili, že proměnnou i může měnit nějaká vnější událost. V tomto případě ji sice žádná událost měnit nebude, ale překladač ji pak nebude optimalizovat. On totiž vidí, že celý forcyklus i s proměnnou i je na nic a jinak by ji vyřadil.

// přerušení od INT0 a od PCINT7
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/sleep.h>

// rutina přerušení INT0
ISR(EXT_INT0_vect){
	volatile unsigned int i;	// vytvoříme delší pulz na PB0
	PORTB |= (1<<PORTB0);	// nastavujeme na PB0 log.1
	for(i=0;i<50;i++){i=i;}	// simulujeme práci na nějakém úkolu
	PORTB &= ~(1<<PORTB0);	// PB0 do log.0
	}

// rutina přerušení PCINT (0..7)
ISR(PCINT0_vect){
	volatile unsigned int i;	// vytvoříme delší pulz na PB1
	PORTB |= (1<<PORTB1);	// nastavujeme na PB1 log.1
	for(i=0;i<50;i++){i=i;}	// simulujeme práci na nějakém úkolu
	PORTB &= ~(1<<PORTB1);	// PB1 do log.0
	}
	
int main(void){
DDRB |= (1<<DDB0) | (1<<DDB1);	// nastavujeme PB0 a PB1 jako výtup
DDRB &= ~(1<<DDB2);	// nastavujeme PB2 jako vstup (INT0)
DDRA &= ~(1<<DDA7);	// nastavujeme PA7 jako vstup (PCINT7)
MCUCR |= ((1<<ISC01) | (1<<ISC00));	// nastavujeme přerušení na Nástupnou hranu
GIMSK |= (1<<INT0) | (1<<PCIE0);   // povolujeme přerušení INT0 a PCINT skupiny 0..7
PCMSK0 |= (1<<PCINT7);	// ze skupiny PCINT 0..7 vybíráme pouze PCINT7 (ostatní piny nesledujeme)
sei();   // globální povolení přerušení

while(1){
	asm("nop"); // nic nedělej - pro snazší ladění
	}
}
Obrázek č.5 - Dvojice přerušení, INT0 a PCINT7

Tentokrát jsem si k testování přerušení vzal generátor. Ne že by mačkání tlačítek bylo problematické, ale za chvíli si budeme chtít vyzkoušet co se stane když přijdou dvě přerušení zároveň a stisknout dvě tlačítka v přesném časovém sledu, jedno o pár mikrosekund dřív jak druhé je dosti problém. Věnujme chvíli průběhům na obrázku č.5. Na žluté křivce je vstupní signál na INT0 (to je nastaveno na detekci nástupné hrany). Červený průběh odpovídá rutině přerušení INT0, ve které tvoříme pulz na pinu PB0 (viz zdrojový kód výše). Světle modrá křivka odpovídá signálu odeslanému na PA7 (PCINT7), tmavě modrá křivka je signál na PB1. S PB1 manipulujeme v rutině přerušení od PCINT. Vzhledem k tomu že PCINT detekuje jakoukoli změnu na vstupu, vidíme že rutina přerušení proběhne jak po nástupné hraně vstupního signálu tak po sestupné. Aby jste se toho vyvarovali, tak by pro většinu úkolů stačilo připsat do rutiny podmínku, která zkontroluje stav pinu PA7 a pokud je například nulový, tak nic nevykoná. Tím by jste si efektivně připravili detekci nástupné hrany. Problém by jste měli v případě, kdy by vstupní pulz byl natolik krátký že by stihl změnit stav před tím než zkontrolujete logickou hodnotu na vstupu. Ale s tím stejně nic neuděláte, takže vás to nemusí trápit. Předpokládám že do této chvíle bylo celé chování jasné a předvídatelné. A teď se podíváme co se stane jestliže přerušení přijde v okamžiku kdy se provádí rutina jiného přerušení. S výše uvedeným kódem dopadne výsledek jako na obrázku č.6.


Obrázek č.6 - Nevnořená dvojice přerušení, INT0 a PCINT7

Vidíte že nejprve přichází signál pro přerušení INT0. Ihned se začíná vykonávat rutina přerušení (červený signál). Během jeho vykonávání přichází přerušení pro PCINT7 (světle modrý průběh). K jeho okamžitému obsloužení ale nedochází. Čip nejprve dokončí rutinu od INT0. Až po jejím dokončení se začne provádět rutina PCINT (tmavě modrý signál), která už notnou dobu (1ms) čeká na provedení. To je tím že překladač na začátku každé rutiny zakáže globálně všechna přerušení. Teprve až po skončení celé rutiny zase přerušení povolí. Dělá to právě proto aby nebylo možné program během vykonávání rutiny přerušení znovu přerušit. Může se vám stát, že z nějakého důvodu potřebujete mít přerušení vnořená. Tedy chcete jednomu přerušení dát možnost přerušit probíhající rutinu jiného přerušení. Pak musíte buď hned na začátek rutiny vložit globální povolení přerušení, tedy sei(), a nebo nějak přesvědčit překladač aby nevkládal globální zákaz přerušení. Druhý způsob je slušnější. Ve zdrojovém kódu vidíte ukázku jak na to.

// vnořovaná přerušení od INT0 a od PCINT7 
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/sleep.h>

// rutina přerušení INT0
ISR(EXT_INT0_vect, ISR_NOBLOCK){
	volatile unsigned int i;	// vytvoříme delší pulz na PB0
	PORTB |= (1<<PORTB0);	// nastavujeme na PB0 log.1
	for(i=0;i<50;i++){i=i;}	// simulujeme práci na nějakém úkolu
	PORTB &= ~(1<<PORTB0);	// PB0 do log.0
	}

// rutina přerušení PCINT (0..7)
ISR(PCINT0_vect, ISR_NOBLOCK){
	volatile unsigned int i;	// vytvoříme delší pulz na PB1
	PORTB |= (1<<PORTB1);	// nastavujeme na PB1 log.1
	for(i=0;i<50;i++){i=i;}	// simulujeme práci na nějakém úkolu
	PORTB &= ~(1<<PORTB1);	// PB1 do log.0
	}
	
int main(void){
DDRB |= (1<<DDB0) | (1<<DDB1);	// nastavujeme PB0 a PB1 jako výtup
DDRB &= ~(1<<DDB2);	// nastavujeme PB2 jako vstup (INT0)
DDRA &= ~(1<<DDA7);	// nastavujeme PA7 jako vstup (PCINT7)
MCUCR |= ((1<<ISC01) | (1<<ISC00));	// nastavujeme přerušení na Nástupnou hranu
GIMSK |= (1<<INT0) | (1<<PCIE0);   // povolujeme přerušení INT0 a PCINT skupiny 0..7
PCMSK0 |= (1<<PCINT7);	// ze skupiny PCINT 0..7 vybíráme pouze PCINT7 (ostatní piny nesledujeme)
sei();   // globální povolení přerušení

while(1){
	asm("nop"); // nic nedělej - pro snazší ladění
	}
}
Obrázek č.7 - Vnořená dvojice přerušení, INT0 a PCINT7

Na obrázku č.7 vidíte výsledek, když v rutinách přerušení povolíme další přerušení. Žlutý průběh spustí vykonání rutiny pro INT0 (červený průběh), než ale stihne svoji práci (projít forcyklus) dokončit, přijde signál pro PCINT7 (světle modrý) a program přeruší svoji činnost (tedy vykonávání rutiny pro INT0) a začne vykonávat rutinu přerušení od PCINT7 (tmavě modrý). Po jejím dokončení se vrátí ke své rozdělané práci v rutině INT0. U vnořených přerušení hrozí riziko, že se vnoříte sami do sebe, jinak řečeno že vás přeruší to samé přerušení které zrovna vykonáváte. A pokud se to stane několikrát po sobě, tak to nemusí dopadnout dobře. Ale to jsou jen moje domněnky :) První způsob, kdy nové přerušení čeká na dokončení posledního je bezpečný. Na druhou stranu také vás může potkat situace kde musíte reagovat na několik přerušení, ale na jedno z nich s největší urgencí, pak možnost vnořování oceníte.

Pár poznatků na závěr

Existují ještě různé finty jak si v malém Atmelu počet externích přerušení navýšit. Třeba modul USI jde znásilnit tak aby se jako externí přerušení choval. Externím přerušením také můžete spouštět AD převodník. Například v situaci kdy potřebujete změřit hodnotu napětí v krátkém okamžiku po příchodu vnějšího signálu.

Na čem se testovalo ?

Jak už jsem napsal v úvodu, pro testy jsem používal čip attiny24A. A protože jsem jako student býval tvor spořivý, koupil jsem si atmely raději v SMD provedení, byly totiž levnější. Takže pro tyto pokusy jsem jej musel připájet na malou bastl desku. Tu si koupíte v číně za pár korun nebo si vyrobíte vlastní (doporučuji vyrábět ve větších sadách :D ). K programování můžete používat klasický laciný USBASP (v číně tak od 30 korun). Jiná volba může být i AVR Dragon (dosti drahý +- 1700,-) nebo ATMEL-ICE (k sehnání tak od 1000,-). Oba dva mají možnost využívat debugWire rozhraní. To vám umožní ladit program přímo v čipu, včetně všech breakpointů a podobných vychytávek. Kdo neví ať hledá termíny "in cirtuit debug" nebo "in circuit emulation". Dragon, je ale po elektrické stránce dosti "křehký". Fóra jsou plná hesel "dead avr dragon". Možnosti programování by jistě samy vydaly na delší článek, ale já bych ho nemohl sepsat pocitvě,neboť mám k dispozici jen tři nástroje a to by bylo jen takové polovičaté. Snad pokud se mi časem dostane do rukou více nástrojů. Metod je totiž opravdu hodně. Bootloaderem počínaje přes ISP, JTAG, debugWire a paralelním programováním konče. Celá měření probíhala na různých napětích. Ze začátku se pracovalo s 5V napájením, některé testy jsem provedl s pracovním napětím 1.8V (zvlášť abych viděl spotřebu v režimech spánku). Nakonec jsem pracoval s 3.3V kvůli nízkým úrovním signálů z generátoru. Všechna zapojení byla natolik triviální že jsem se neobtěžoval s kreslením schémat.

Závěrem

Doufám, že jste smysl i použití externího přerušení pochopili a že vám tento článek něco dal. Přirozeně bych vám doporučil provést pár vlastních pokusů aby jste získali důvěru ve vlastní kód. Většinu času při programování člověk stejně hledá chyby a je dobré umět rozpoznat na kterou část programu se můžete spolehnout a na kterou ne. A na to přijdete nejlépe zkouškou. Zdroják takhle na monitoru vypadá úplně jasně, ale když ho píšete sami, vynoří se tolik záludností, že vás to až překvapí :D

Zdroje a relevantní odkazy

Datasheet k Attiny24A
Application note k externím přerušením na Attiny
Zajímavý článek o přerušeních s AVR na mcu.cz


By Michal Dudka