• 1
  • 2

Interrupts - Multitasking

. .

Postupne, ale isto, sa dostávame k problémom spracovania kódu.
To prečo som zvolil takýto úvod bude každému neskôr jasné.
Zavediem pojmy Interrupts (prerušenia).
 

Táto tématika je tak obsiahla, že nie je možné popísať všetko. Tejto kapitole nepridáva ani fakt, že väčšina ju
obchádza, nepoužíva, a tak je viacej teórie, než ukážkového kódu.

Pravdou je, že bez prerušení sa pri vývoji nezaobídeme.

Nechcem moc písať teórie, nakoľko každý schopný kóder si to nájde sám.



Čo ale tie interrupty sú?

S pojmom Interrupts sa stretávame napríklad u zavádzača biosu.
Každý interrupt je delený podľa adresy v tabuľke prerušení. Napríklad, na adrese 0Ch je prerušenie, ktoré vyvoláva sériovú komunikáciu RS-232.
Inými sú napríklad načítanie sektorov, inicializovanie HDD a podobne.

Prerušenia sa rozdeľujú na vnútorné (SW) a vonkajšie (HW).

Vnútorné interrupty sú generované programovo, synchrónne za behu programu pomocou inštrukcie SW_INTx.

Vonkajšie interrupty sú generované asynchrónne na základe vonkajšej události, ktorá vykoná danú akciu. Medzi takéto prerušenia patrí napríklad reset, ktorý sa vykonáprivedením logickej úrovne na pin MCLR .


Hlbšie sa zameriame na vnútorné interrupty

Terajšie procesory sú veľmi rýchle z hľadiska vykonávania "n" inštrukcií v jednotke času.
Rýchlosť spracovávania MCU je daná v Hz internej/externej oscilácie.

Vnútorné interrupty majú vlastnú adresu, ktorá je uložená v tabuľke prerušení.
Každé prerušenie obsahuje prioritu, ktorá ho uprednostní pred ostatnými prerušeniami s nižšou prioritou.
Zároveň interrupty obsahujú vlastnú, takzvanú rutinu. Pri vyvolaní vnútorného interruptu inštrukciou SW_INTx, sa vyvolá prerušenie, ktoré spustí rutinu, ktorá sa spracuje. Tieto vlastnosti každého prerušenia sú ako vektor definovaný svojou vlastnosťou.

Pre podrobnejší náčrt si predstavme buffer typu LIFO. Každá inštrukcia je vložená do fronty a čaká na vybavenie.
Bez interruptu kód beží v jednom vlákne:




Pri použití vnútorných interruptov bude spracovanie vypadať následovne:



Jednotlivé interrupty sú identifikované podľa vekorov. Interrupt s najväčším ID je zaradený do fronty (pred iným interruptom), skôr a procesor ho vykoná.

 

Môžme povedať, že máme jeden kód. V predpísaných situáciách sa nám zavolá prerušenie. Procesor zistí adresu prerušenia, skočí na túto adresu - do bloku kódu a vykoná spomínanú rutinu. Skoky sú riešené ako GoTo.

Celý princíp môžme znázorniť aj ako:




Vnútorné interrupty si môžme predstaviť ako barikády.
Pri vyvolaní prerušenia sa skočí na adresu vektora daného interruptu a spracuje sa kód rutiny.

Procesor za jednotku času tak rýchlo riadi "barikády", že sa postupne vykonávajú všetky kódy rutýn.

MCU je schopné síce spracovať v reálnom čase len jednu inštrukciu, systém prerušení spracuváva inštrukcie rutýn
nezávisle nasebe a rieši otázku multitasking.

V merítku znamená, že pri hodinovom impulze 4MHZ, MCU vykoná 4,000,000 inštrukcných cyklov za sekundu.
Pri nepoužití iterruptu, MCU spracuje 4,000,000 cyklov zasebov bez zmeny polohy inštrukcie, pričom interrupty zabezpečia pomocou skokov, že inštrukcie, ktoré by boli spracované v 3,999,995. - 3,999,999. cyklu, budú spracované pri 5. inštrukčnom cyklu.
Podrobnejšie príkladové prepočty sú v sekcií: Pravidlá Interruptov.

Simulácia interruptu na dvoch nezávislých vlákien:



Je nutné podotknúť, že interrupty sú použiteľné pri cykloch s nekonečnou podmienkou.
Pri cykloch s "infinity" podmienkou, dochádza síce k zacykleniu daného vlákna, no interrupt dokáže spracovať
túto požiadavku v závislosti na inom paralelnom vlákne.

Príklad takého stavu je pri: Oživujeme tlačidlá #1 | Oživujeme tlačidlá #2

Takouto podmienkou je zacyklenie pri držaní tlačítka:

 
while((Get_Switch(*SW.Switch.Direct.Port,SW.Switch.Config.Pins[0])) !=0)
        {
        }



Ukážka zacyklenie pri nepoužití interuptu:

                                                



Ukážka zacyklenia pri použití dvoch nezávislých vláknach:

 


! --- Poznámka
Je nutné podotknúť, že síce vlákna bežia nezávisle na sebe, pri stálom držaní tlačidla 4 hodiny, by bolo dané vlákno po celú dobu zacyklene! To znamené, že pokiaľ by sme mali vo vlákne niekoľko funkcií, zostali by stáť !
Z tochto dôvodu je použitie podmienok v nepolynomiálnom čase pre MCU kritické a nepoužiteľné !
Príklad som naschvál simuloval a v ďalších projektoch budeme tento fakt nahdradzovať príznakom,
viď. článok >>Tlačidlami ovládame Unipolárny Krokový motor - Multitasking<<
! ---





Konfigurácia prerušení

MCU majú niekoľko prerušení s vlastnými vektormi a časovačmi.
Číslo časovača budeme reprezentovať premennou x.
Každé prerušenie môžme konfigurovať samostatne.

TxCONbits.TON - umožňuje štart časovača (log. 1) a stop (log. 0).
Nastavenie tochto bitu prevádzame ako reset časovača do vypnutného stavu (0) pred ostatnou konfiguráciou prerušení a po nastavení tejto konfigurácie časovač spustíme (1).

TxCONbits.TCS - nastavujeme typ zdroja hodinových signálov.
Hodnotou log. 1 nastavujeme vonkajší zdroj impulzov (v prípade použitia sekundárneho oscilátora SOSC), log. 0 nastavíme pre vnútorný zdroj hodin. impulzov (FOSC/2).

TxCONbits.TGATE - nastavujeme takzvaný Timer Gate režim.
Táto možnosť sa dá nastaviť len v prípade, že používame vnútorný zdroj hodin. impulzov, ale zároveň chceme
prostredníctvom premostenia použiť aj externý zdroj impulzov.
Hodnotou log. 1 túto možnosť povolíme, log. 0 zakážeme.

TxCONbits.TCKPS - nastavujeme Prescaler - delenie hodinových impulzov.
Nastavenie môže nadobúdať:
                                        0b11 = 1:256
                                        0b10 = 1:64
                                        0b01 = 1:8
                                        0b00 = 1:1

TxCONbits.T32 - týmto bitom nastavujeme n-bitový režim daného časovača.
Hodnotou log. 1 nastavujeme časovač ako 32-bitový, hodnotou log. 0 ako 16 - bitový.

TMRx - hodnotou 0x00 zmažeme obsah daného časového registra.

PRx - nastavujeme hodnotu periódy, po ktorej sa má interrupt spustiť, viz. Časový priebeh impulzov.

IPC0bits.TxIP - nastavujeme prioritu danému interruptu.
Hodnota môže byť hexadecimálna alebo decimálna, naprikál 7, 0x07.

IEC0bits.TxIE - hodnotou log. 1 spúštame časovač prerušenia.

IFS0bits.TxIF - u tochto konfiguračného bitu sa pozastavím.
Ide o takzvaný Interrupt Flag - príznak prerušenia.
Pokiaľ dôjde k vyvolaniu prerušenia, spustí sa nadefinovaná funkcia rutiny. Akonáhle sa preskočí na túto funkciu, vyvolá sa príznak interruptu, ktorý má hodnotu log. 1 a identifikuje ňou prerušenie.
Túto hodnotu je potrebné v danom predpise interrupt metódy nastaviť na log. 0 (viz. nižšie).



Pravidlá interruptov

Vnútorným interruptom môžme nastaviť časové razítka. Zrejme to nie je správne výstižné, no každý interrupt má svoj časovač. Sme schopný nastaviť danému interruptu so svojím vektorom frekvenciu a periódu, počas ktorých dôjde k prerušeniu.

Nastavenie časovača má mnoho spôsobov.

Zvolíme vnútorný hodinový zdroj impulzov bitom TCS.

Samotná frekvencia časovača interruptu vychádza z vnútornej frekvencie použitého oscilátora a závisí od nastavení hodnoty delenia bitom TCKPS, a hodnoty periódy PRx.

Vnútorná frekvencia (Fcy) MCU je 4MHz.
Prescaler - hodnotu dtochto integrovaného prostrediaelenia frekvencie 4MHz, nastavíme bitom TCKPS napríklad na hodnotu 0b10, čo je hodnota odpovedajúca deleniu číslom 64, viz. vyššie.
Periódu nastavíme bitom PRx na hodnotu 7.
Výsledná frekvencia daného interruptu (jedného vlákna) sa dopočítá:


        Fcy / TimerClockPrescaler / Periode        čize prvý príklad, napr.

        4,000,000 / 64 / 7 = cca. 8929 p/Hz.

V iných príkladov by sme mohli zvoliť napríklad:

        4,000,000 / 64 / 100 = 625 p/Hz                    alebo
        4,000,000 / 1 / 1 = 4,000,000 p/Hz.

Konfigurácia prvého príkladu znamená, že časovač bude počítať frekvenciou 4,000,000 / 64 =  62500 Hz a periódou 62500 / 7  (čo je vyvolanie interruptu pred každým 7. hodinovým impulzom) = 8929 spustení interruptu za sekundu.

V inom prevode od hodnoty 1000 vydelíme tento výsledok a dostaneme hodnotu v ms:

        8929 Hz --> 1000/8929 --> 0.11 ms
        625 Hz --> 1000/625 --> 1.6 ms
        4,000,000 Hz --> 1000/4,000,000 --> 0.00025 ms --> 250 ns

Je jasné, že vlákno riadené 250 ns časovačom by nemuselo mať opodstatnenie, napríklad pri riadení krokových
motorov.


MCU obsahuje poväčšinou niekoľko interruptov.
Procesor PIC24FJ64GB002 obsahuje 5 prerušení.

Z predošlých výpočtov môžme do jedného interruptu zahrnúť 5 funkcií, ktoré sa vykonajú, napríklad, 5x za sekundu.
Identifikáciu počtu prerušení urobíme inkrementovaným príznakom.


Časový priebeh impulzov:





Prikladová konfigurácia 2 interruptov


konfigurácia časovačov
int main(void)
{
   T1CONbits.TON = 0;
   T2CONbits.TON = 0;


   T1CONbits.TGATE = 0;
   T1CONbits.TCS = 0;
   T1CONbits.TCKPS = 0b10;
   TMR1 = 0x0000;
   PR1 = 80;
   IPC0bits.T1IP = 0x06;
   IFS0bits.T1IF = 0;
   IEC0bits.T1IE = 1;

   T2CONbits.T32 = 0;
   T2CONbits.TGATE = 0;
   T2CONbits.TCS = 0;
   T2CONbits.TCKPS = 0b11;
   TMR2 = 0x0000;
   PR2 = 140;
   IPC1bits.T2IP = 0x05;
   IFS0bits.T2IF = 0;
   IEC0bits.T2IE = 1;


   T1CONbits.TON = 1;
   T2CONbits.TON = 1;

   while(1)
   {
   }
   return 0;
}




Spúšťanie jednotlivých rutyn zabezpečujú jednotlivé prototypy prerušení:

void __attribute__((interrupt, no_auto_psv)) _T1Interrupt(void)

vyvolanie rutyn
void __attribute__((interrupt, no_auto_psv)) _T1Interrupt(void)
{
   Metoda1();
   IFS0bits.T1IF = 0;
}

void __attribute__((interrupt, no_auto_psv)) _T2Interrupt(void)
{
   Metoda2();
   IFS0bits.T2IF = 0;
}




K čomu nám to však je dobré?

Keď ste sa niekedy pozreli okolo seba - mikrovlnná trúba, TV, satelit, mobil,..., nenapadlo vás, ako je možné, že pri
non-stop mačkaní tlačidiel na ovládači/prednom panely nedôjde ku žiadnej odozve?

Displej rádia nám ukazuje aká pieseň hrá + čas + LED efekty a keď vyberieme CD len sa na LCD vypíše "No Disc" bez
toho, aby došlo k zamrznutiu iných zobrazovaných dát?

Na mobilnom telefóne hráme hru, zároveň nám do toho hrá hudba a nato príde SMS bez toho, aby došlo k oneskoreniu celého telefónu?

Avšak najlepšie budem interrupty prezentovať na video-príkladoch.

Názorné ukážky sú uvedené na videách vyššie.




Integrácia vlákien (Threading Enviroment)

Rozhodol som sa, že vytvorím prostredie prerušení ako vlákien a implementujem ho do konfiguračného súboru
config_diall.h.

Implementoval som konfiguráciu 5. vlákien, ktoré bežia pod samostatnými časovačmi.

Predvolené nstavenie vlákien:
                                         Vlákno 1 - Frekvencia spustenia: Fcy / 64 / 80
                                                          Priorita: 6

                                         Vlákno 2 - Frekvencia spustenia: Fcy / 256 / 140
                                                          Priorita: 5

                                         Vlákno 3 - Frekvencia spustenia: Fcy / 64 / 460
                                                          Priorita: 4

                                         Vlákno 4 - Frekvencia spustenia: Fcy / 256 / 680
                                                          Priorita: 3

                                         Vlákno 5 - Frekvencia spustenia: Fcy / 1 / 10000
                                                          Priorita: 2

Hodnota "Fcy" závisí od aktuálne použitej frekvencie MCU.

Prvé vlákno bude s najvyššou prioritou - vykonáva sa prednostne.
Sporadicky vlákna s vyšším indexom majú prioritu o úroveň nižšiu.

 


Nastavenie headeru

1# Defaultné nastavenia interruptov

Pokiaľ budete chcieť využiť toto integrované prostredie, nastavte súbor "main.c" následovne:

main.c
#define __enable__threading_mode__

#include "config_diall.h"
#include "config_words.h"

char *PageSys(void)
{
   return "0x21";
}


void ThreadA(void)
{
}

void ThreadB(void)
{
}

void ThreadC(void)
{
}

void ThreadD(void)
{
}

void ThreadE(void)
{
}


int main_diall(void)
{
   //neaky config

   while(1)
    {
    }
   return 0;
}



Makrom  __enable__threading_mode__ sa povolí možnosť interruptov.

Metódy ThreadA() ... ThreadE() sú metódy, ktoré sú vyvolané jednotlivými interruptmi.

* Je nutné ich definovať všetky, aj pri nevyužití. V opačnom prípade kompilátor vyhodí chybu.

Defaultná konfigurácia interruptov je deklarovaná v internom "int main(void)".
Pri definovaní možnosti threadingu = deklarovaní makra #define __enable__threading_mode__,
je nutné premenovať klasický "int main(void)" na "int main_dial(void)" .
V opačnom prípade kompilátor skončí s chybou!

V bloku "//neaky config" sa nachádza prípadná konfigurácia použitých modulov.

V cykle "while(1)" môže bežať nezávislý modul - služba (niečoho).



2# Vlastné nastavenia interruptov

Pokiaľ chcete zmeniť parametre vlákien, alebo použiť, napríklad, len prvé dve je nutné zakázať vnútornú konfiguráciu časovačov a nastaviť vlastnú:

main.c
#define __enable__threading_mode__
#define __disble_internal_interrupt_cnf__


char *PageSys(void)
{
   return "0x21";
}


void ThreadA(void)
{
}

void ThreadB(void)
{
}

void ThreadC(void)
{
}

void ThreadD(void)
{
}

void ThreadE(void)
{
}


int main_diall(void)
{
   //neaky config

   T1CONbits.TGATE = 0;
   T1CONbits.TCS = 0;
   T1CONbits.TCKPS = 0b11;
   TMR1 = 0x0000;
   PR1 = 5000;
   IPC0bits.T1IP = 0x01;
   IFS0bits.T1IF = 0;
   IEC0bits.T1IE = 1;

   T2CONbits.T32 = 0;
   T2CONbits.TGATE = 0;
   T2CONbits.TCS = 0;
   T2CONbits.TCKPS = 0b00;
   TMR2 = 0x0000;
   PR2 = 100000;
   IPC1bits.T2IP = 0x08;
   IFS0bits.T2IF = 0;
   IEC0bits.T2IE = 1;


   T1CONbits.TON = 1;
   T2CONbits.TON = 1;


   while(1)
    {
    }
   return 0;
}



Makrom __disble_internal_interrupt_cnf__ sa zruší defaultné nastavenia interruptov.

Nové nastavenia je nutné vložiť do "int main_diall(void)", pod "//neaky config" .

!Opäť je nutné definovať všetke metóty ThreadA() ... ThreadE(), avšak spustia sa len ThreadA() a ThreadB() !



3# Spustenie režimu DEVI

Režim DEVI implementuje rozšírenie možnosti kontroly nad spúštaním interruptov.

Vyššie sme si povedali čo-to ohľadne príznakov a aj toho, že môžme jednotlivé spúšťania interruptov počítať a jednotlivé vlákna spúšťať len po určitom počte vyvolaní interruptu.

Pre pokročilých som vytvoril akúsi nadstavbu v tomto duchu, pričom tento režim je možné povoliť len pri použití jedného z nastavení vyššie (použití threadingu).

Celý režim DEVI sa spúšťa definovaním makra __enable__threading_devimode__ :

#define __enable__threading_mode__
#define __enable__threading_devimode__


#include "config_diall.h"
#include "stepper.h"
#include "config_words.h"



Pri povolení sa, pri vyvolávaní interruptov začne inkrementovať premenná a detekuje spúštanie interruptu do externej premennej. Veľkosť premennej závisí od konfigurácií každého časovača interruptu, pričom sa vypočíta počet
vykonávaných spustení.
Ak sa premenná rovná tomúto počte, vynuluje sa.

Prístup k jednotlivým premeným kždého interruptu je uložený v referenciách:

 
Interrupt1;
Interrupt2;
Interrupt3;
Interrupt4;
Interrupt5;



Jednotlivé údaje sú k dispozícií v:

 
Interruptx.Interrupt_Flag //priznak spustenia: log. 0 (nespustený) / log. 1 (spustený)
Interruptx.Interrupt_Count //hodnota reprezentujuca pocet spusteni



Operácie nad jednotlivými vláknami prerušení sa vykonávajú v:

 
PUBLIC void __attribute__((interrupt, no_auto_psv)) _TxInterrupt(void)
{
   ThreadA();
   /* Operation DEVI code */
}



Je možné jednotlivé volania funkcií ThreadA-ThreadE prispôsobiť podľa potreby čítača, prípadne doimplementovať logiku vetvenia a spúšťania viacerých vlákien.



4# Vypnutie nepoužitých vlákien

K celému prostrediu som implementoval možnosť vypnutia jednotlivých vlákien.

Túto možnosť použijeme vždy vtedy, keď nevyužívame všetky vlakná a tým pádom nevyužité vypneme. Vypnutie jednotlivých vlákien spočíva v definícií ich makier:

 
#define _disable_Thread_C
#define _disable_Thread_D
#define _disable_Thread_E



Zostanú nám aktívne vlákna A-B.

! Len táto možnosť nastavenia, narozdieľ od predošlých, nám umožňuje vynechanie deklarácie metód vlákien.
Pri definícií makier na vypnutie vlákien C-D-E, nemusím deklarovať metódy void ThreadC-D-E() !

! Upozornenie !
Je nebezpečné kombinovať možnosti vypnutia vlákien s režimom vlastného nastavenia interruptov - 2#. Pri definovaní makra __disable_internal_interrupt_cnf__, kompilátor jednotlivé časovače nenastaví a nastavenie v rátane spustenia je nutné urobiť manuálne, vid. popis.
Pri externej konfigurácií všetkých vlákien a pri následnom vypnutí neakého vlákna, dochádza k hazardom medzi vláknami - žiadne z piatich vlákien nebude správne pracovať!.
Vlákna, ktoré vypnete, nesmiete externe nastaviť. Nastavia a spustia sa časovače, ale kompilátor vypne možnosti spustenia interných metód prerušení.