Sunteți pe pagina 1din 23

5.

Sisteme de operare

5.1. Introducere

Sistemul de operare (SO) este o sumă de programe (subrutine și funcții sau în mod
global servicii) ce asigură interfațarea între un utilizator (mai specific, dintre programele pe
care un utilizator le rulează) și sistemul hardware – ce conține drept componentă principală
un: microprocesor, microcontroler sau DSP.
Sistemul de operare permite dezvoltatorilor să se concentreze pe aplicația în sine pe
care doresc să o dezvolte (pe funcționaslitățile pe care vor să le ofere utilizatorilor) și nu pe
elementele colaterale, oferindule pentru aceasta suport pentru: (a) managementul memoriei
(stivă, heap, cache, etc.), (b) dezvoltarea de aplicații multitasc – ce permit rularea a mai
multor fire de execuție, cu priorități diferite, care trebuie să se sincronizeze între ele, etc.
(c) o serie largă de elemente de conectivitate precum: TCP/IP, Wi-Fi, BLE sau USB, (d)
un sistem eficient de management a consumului sistemului, (e) gestinarea driverelor
sistemului în mod unitar etc.
Din punctul de vedere a oricărui programator un sistem de operare (SO) este o colecție
de librării care permit dezvoltatorilor și aplicației să utilizeze foarte mult cod dezvoltat deja
și, ce este mai important, cod care a fost testat și utilizat de mii-milioane de ori pe o perioadă
lungă de timp.
Funcția fundamentală a SO este de a gestiona resursele hardware existente și de a
permite un acces concurențial al diferitelor programe la aceste resurse.

5.2. Sisteme de operare în timp real

În accepțiunea generală a publicului, prin sisteme de operare în timp real – RTOS (Real
Time Operating System) – se subînțeleg acele sisteme de operare rapide și foarte rapide
capabile să rezolve o problemă într-un interval scurt de timp. Dar, de exemplu, în cazul
reglării temperaturii dintr-un furnal elementul de control de tip embedded trebuie să citească
și să regleze temperatura doar de câteva ori într-o secundă sau chiar mai rar în conformitate
cu algoritmul ales. În acest caz, un sistem care este capabil că furnizeze comanda corectă
de fiecare dată când este necesară și cerută este un sistem în timp real dar nu este unul foarte
rapid – chiar putem spune că este un sistem lent sau foarte lent comparativ cu posibilitățile
existente la ora actuală.
Orice sistem embedded preia, în primul pas, un flux de date1 de la una sau mai multe
intrări, flux pe care îl procesează și, ulterior, pe baza rezultatelor obținute oferă diferite date
utilizatorilor sau comandă anumite dispozitive2. Printr-o procesare în timp real se înțelege
în primul rând capabilitatea dispozitivului de a reacționa (oferi date corecte

1
Aceste date pot fi de orice tip și de orice natură de la curentul ce trece printr-o ramură a unei punți trifazată,
la valoarea unei accelerații, până la informația tactilă preluată de la un touchscreen.
2
Un tiristor (prin intermediul unghiului de comandă), prezintă cantitatea de benzină, acționează un sistem
hidraulic de frânare etc.

1
utilizatorului sau de a oferi comanda în mod corect unui anumit dispozitiv) într-un anumit
interval de timp bine prestabilit astfel încât să nu apară diferite efecte nedorite3.
Factori fundamentali necesari unui RTOS: o latență cât mai mică a răspunsului (de ex.
timp minim de răspuns la o întrerupere) și latență minimă în schimbarea contextului între
firele de execuție. De aici tragem concluzia că RTOS trebuie să fie cât mai predictibil,
determinist. Indiferent de evenimentele care au loc în sistem, SO trebuie să răspundă la o
cerere de întrerupere într-un interval foarte bine definit de timp și, evident, dacă s-ar putea
cât mai repede. Aceste elemente sunt cele definitorii pentru un RTOS și nu cantitatea de
operații pe care o poate executa într-un anumit interval de timp.
În cadrul oricărui SO în timp real vor exista anumite funcții care trebuiesc executate
în timp real (de exemplu, monitorizarea unor senzori pentru sesizare unui scurtcircuit) și
alte funcții care nu sunt critice (de exemplu, afișarea unor date pe interfața grafică) și care
pot fi întârziate într-o anumită fereastră de timp. În momentul în care 2 astfel funcții, una
ce necesită o execuție în timp real iar cea de a doua care nu este critică, sunt gata de rulare
simultan SO trebuie să aibă un mecanism de prioritizare a lor și chiar de oprire a unor
funcții ne critice și de executare cât mai rapidă a celei în timp real. De aici se observă
necesitatea existenței în cadrul SO a unei componente capabilă să execute funcțiile critice
atunci când sunt necesare și să întrerupă, în aceste momente de timp, execuția celorlalte
componente necritice – programatorul (scheduler) SO.
Sistemele de procesare în timp real se împart în principal în două clase majore: soft și
hard.
Sistemele de tip soft real time sunt acelea care nu determină comportări critice sau poate
chiar catastrofe, rezultante în urma generării întârziate sau poate chiar de loc (din când în
când) a comenzii sau a rezultatului procesării4. Deci, sistemele de tip soft real time pot
tolera întârzieri și/sau variații ale momentului realizării efective a comenzii, generate de
sistemul de întreruperi, timer-ele sistemului, programatorului nucleului sistemului de
operare (scheduler) sau a lipsei de determinism a codului ce procesează datele.
Sistemele de tip hard real time, denumite și sisteme critice de procesare în timp real,
sunt acele sisteme ce trebuie să-și finalizeze rezultatul procesării în mod obligatoriu într-un
anumit interval de timp foarte clar delimitat, nerespectarea unei astfel de cerințe putând
genera rezultate catastrofale5. Această incapacitate a sistemului de a furniza răspunsul
atunci când este necesar este considerată un eșec complet al sistemului – deci, o situație
care nu este considerată acceptabilă.
Dacă luăm în considerare faptul că cea mai mare parte a evenimentelor, datelor etc. (ce
trebuiesc procesate, analizate și ulterior în urma procesării pe baza acestor rezultate se va
da o comandă) se preiau la intervale regulate de timp sau se cunoaște cu certitudine
intervalul minim de timp după care poate avea loc un nou eveniment atunci vom spune că
un sistem este de tip hard real time dacă timpul de așteptare până la preluarea noului set de
date va fi întotdeauna mai mare ca zero, vezi Figura 5.1.

3
Prezentare sacadată a datelor (de exemplu a unor imagini sau cadre) sau o comandă întârziată sau
inoportună a sistemului mecanic, chimic etc.
4
Un astfel de sistem este de exemplu un DVD player. El procesează în timp real fluxul de date de intrare
dar în cazul în care din când în când nu reușește să decodeze corect un cadru nu se întâmplă nici o tragedie.
5
De exemplu, în cazul sistemelor de tip ESP (Electronic Stability Program) existente în cadrul
automobilelor.

2
Timp necesar preluării, procesării Timp de
și comenzii dispozitivului așteptare
t

n n+1
Figura 5.1. Comportarea temporară a unui sistem de tip embedded

Din perspectiva celor afirmate în paragraful anterior și a Figura 5.1 putem afirma că
pentru un anumit sistem embedded întotdeauna putem găsi o aplicație pentru care să nu
avem un timp de așteptare mai mare ca zero și deci sistemul respectiv să nu fie unul de tip
hard real time. Dar, din fericire nu aceasta este metoda de abordare a unei probleme –
pentru o anumită problemă se găsește acel sistem capabil să o rezolve și nu invers.
În principal un SO de tipul RTOS își propune să îndeplinească următoarele obiective:
1. Latențe cât mai mici în execuție – intervalele de timp din momentul în care un
eveniment a fost generat și până când acesta este tratat să fie cât mai mici;
2. Să fie un SO determinist – de ex. de fiecare dată când un eveniment are loc în
sistem trebuie să avem același număr de cicli procesor până acesta este tratat
indiferent de ce alte funcții se execută în sistem;
3. Să fie un sistem scalabil – indiferent dacă avem o aplicație care are la un anumit
moment de timp are 150 de procese care rulează sau avem doar 2, indiferent
dacă rulăm pe un sistem multicore sau single core SO trebuie să facă față la toate
aceste provocări;
4. SO trebuie să gestioneze cât mai multe din aspectele existente într-un sistem
(gestionarea puterii consumate, managementul tabelei vectorilor de întrerupere,
sau a memoriei etc.) și să permită dezvoltatorului să se concentreze pe
dezvoltarea propriei aplicații.

5.3. Dispozitive cu sau fără sistem de operare

În general putem dezvolta aplicații care să necesite sau nu existența unui sistem de
operare.
În situația inexistenței sistemului de operare aplicația6 are o structură similară cu cea
din Figura 5.2. Acestea au avantajul că sunt în general aplicații de o mică amploare, foarte
rapide și foarte ușor de înțeles.
În general o astfel de aplicație are trei secțiuni principale. În prima secțiune se
inițializează/configurează atât dispozitivele hardware (porturi, interfețe de comunicare
etc.) cât și componentele software. Rolul acestei componente este de a avea o comportare
predictibilă a sistemului; deci, ne dorim ca sistemul să plece întotdeauna dintr-o stare bine
definită.
Urmează o buclă (de ex. while) infinită în care se implementează o mașină cu un
număr finit de stări. Trecerea de la o “stare” la alta se realizează funcție de îndeplinirea
unei anumite condiții specifice rezultată în urma: (a) generării unei anumite întreruperi

6
Aplicațiile care nu utilizează un SO sunt cunoscute, în literatura de specialitate, sub numele bare-metal.

3
(obținută atunci când un nou caracter a sosit pe magistrala I2C și a fost preluat în întregime,
un timer și-a epuizat ciclul de numărare, etc.) sau (b) interogării (polling) stării unui
dispozitv (citirea valorii unui ADC, a unui pin extern sau funcție de valoarea obținută de la
un port serial). În principal aici este codul principal al aplicației.

Inițializarea

Mașină cu stări
finite

ISR-uri

Figura 5.2. Structura unei aplicații dezvoltată fără ajutorului unui SO

Subrutinele ISR (interrupt service routine) execută anumite coduri specifice atunci când
un periferic și-a finalizat a funcționalitate specifică sau când o anumită excepție a apărut în
sistem.

Fără SO SO minimal SO cu tascuri

Figura 5.3. Comparație între sistemele ce rulează SO și cele care nu au așa ceva

Comparând situația lipsei SO cu cea a unui SO minimal observăm că toate cele trei elemente
existente în cadrul aplicație ce nu este susținută de un SO se regăsesc în cadrul SO minimal.
O deosebire totuși există. În cadrul SO minimal vom avea întreruperi software și hardware

4
în timp ce dacă nu am utiliza SO vom avea doar întreruperi hardware. Un SO de operare
complet mai are în plus și tascuri. Diferența între toate aceste elemente va fi prezentată
puțin mai târziu.

5.4. Sistemul de operare TI-RTOS

Componentele SO TI-RTOS

Sistemul de operare TI-RTOS este compus dintr-un număr de componente. Acestea


sunt:

1. Kernelul TI-RTOS – cunoscut sub numele de SYS/BIOS;


2. Driverele și stratul HAL (Hardware Abstraction Layer);
3. Componentele de analiză ale TI-RTOS – componenta UIA (Unified Instrumentation
Architecture);
4. Structura de protocoale și servicii de rețea;
5. Sistemul de fișiere.

Kernel-ul sistemului de operare TI-RTOS

Nucleul SO (kernel-ul) este o componentă de bază a oricărui SO. Nucleul este la rândul
lui un program dar care trebuie să respecte anumite cerințe restricitive și de performanță
(foarte ridicate), fiind astfel partea cea mai critică a unui SO. Rolul lui principal este de a
de a executa și gestiona diferitele fire de execuție (priorități, memorie, schimbarea
contextelor etc.).
Pentru trecerea execuției de la un fir de execuție la altul (discutăm în acest context de
SO de tip multitasking), nucleul SO conține o componentă ce are rolul gestionării acestor
procese numită dispecer sau programator – denumirea în engleză este scheduler.
În principal există două mari abordări în implementarea acestei componente:
 De tip preemptive – în care un fir de execuție este executat până când:
1. Se termină;
2. Un fir de execute cu o prioritate mai mare devine activ;
3. Firul de execuție se blochează așteptând ca o anumită resursă specifică
să-i fie disonibilă.
Acest tip de dispecer este cel mai utilizat în cadrul SO în timp real și este cel
implementat în cadrul SO TI_RTOS.
 Fiecare fir de execuție are o anumită felie de timp bine determinate în care
rulează (felie de timp cunoscută sub numele de quantum sau tick). Când această
felie temporară este epuizată dispecerul determină cărui thread i se va aloca
următoarea felie de timp pentru rulare.
Există de asemenea și abordări combinate în implementarea programatorului SO.
Funcție de diferitele influențe sau condiționări, un fir de execuție poate fi în una din
următoarele 4 stări:
I. Se execută – codul existent intern firului de execuție este executat de procesor.
5
II. Pregătit – firul de execuție poate fi rulat de processor dar își așteaptă rândul.
III. Blocat – toate aceste fire de execuție sunt în așteptarea unui anumit eveniment
pentru a-și schimba starea în pregătit. Execuția lor este suspendată. Evenimentul
poate fi un anumit mesaj de la alt fir de execuție, schimbarea stării unui element
de sincronizare etc.
IV. Terminat – atunci când toate resursele (structurile de date, stiva, heap-ul etc.)
utilizate de firul de execuție au fost eliberate de SO datorită finalizării propriei
execuții sau eliminării acestuia (ștergerii) de pe listele interne gestionate de
dispecer.

Figura 5.4. Stările în care se poate găsi un fir de execuție

Dispecerul menține măcar două liste diferite pentru urmărirea stării fiecărui fir de
execuție. Astfel avem o listă pentru firele de execuție ce sunt pregătite de rulare și o altă
listă pentru tascurile blocate. Aceste liste conțin și multe alte structuri de date – de exemplu,
condiția/condițiile care atunci când este îndeplinită sau sunt îndeplinite determină
deblocarea firului de execuție, trecerea acestuia în starea pregătit de a fi rulat (ready) și
transferarea lui pe lista firele de execuție ce așteaptă să fie rulate.
Atunci când un fir de execuție nu se va mai executa, vezi Figura 5.4, datorită blocării
se va executa primul fir de execuție din lista firelor de execuție ce așteaptă să fie rulate.
Pentru aceasta trebuie să se realizeze o schimbare de context de la primul fir de execuție
către cel de al doilea. Pentru aceasta se vor realiza pașii:
1. Salvarea contextului de execuție pentru firul de execuție curent – se salvează
pe stivă starea CPU (regiștri), valorile datelor sau a structurilor de date din
cache-urile interne procesorului etc.;
2. Selectarea firuluide execuție ce va fi rulat – dispecerul realizează aceasta în
conformitate cu informațiile prezentate anterior;
3. Încărcarea contextului noului task – operațiile inverse de la punctul 1.

6
4. Execuția noului fir de execuție – se realizează prin încărcarea registrului PC
cu valorile salvate anterior – astfel, acest fir de execuție își reia rularea din
punctul anterior în care a rămas.
Sistemul de operare TI-RTOS este capabil să lucreze cu 4 tipuri diferite de fire de
execuție – fiecare cu specificitățile lui. Acestea sunt: (a) întreruperi hardware, (b)
întreruperi software, (c) tascuri și (d) tascul de tip idle. Datorită importanței acestora vor
fistudiate și prezentate pe larg într-un subcapitol independent.

Driverele și stratul HAL

Compania TI oferă diferite drivere pentru diferitele perifirice existente și specifice


fiecărei platforme individuale pe care rulează TI-RTOS.
Toate aceste drivere se constituie drept un strat ierarhic superior susținut de HAL
(Hardware Abstraction Layer). Stratul HAL este cunoscut sub diferite nume funcție de
familia de dispozivie pe care rulează – generic definite sub numele *Ware. Astfel avem:
TivaWare, MSPWare, MWare, CC26xxWare și CC3200SDK Driverlib. Scopul HAL este
de a abstractiza accesul la elementele hardware în special accesul la nivel de registru. Aceste
librării distribuite cu TI-RTOS au fost reduse în dimensiuni (comparativ cu versiunile
distribuite și necesare programelor care rulează pe aceste dipozitive fără suportul unui SO)
prin includerea doar a anumitor elemente necesare TI-RTOS [TI, 2016].

Figura 5.5. Stratul HAL și raportarea lui la celelalte componente

Toate aceste drivere sunt thread-safe7 în utilizarea lor de către firele de execuție
(threads) gestionate de către SYS/BIOS. În plus, există versiuni ale driverelor atât în
varianta în care se analizează (investighează) funcționarea SO dar și în varianta de release
a aplicație și a sistemului.
Analizând cazul particular al comunicației prin intermediul magistralei I2C vom
observa, la nivelul, DriverLib, funcții precum (există peste 50 de astfel de funcții):
void I2CSlaveInit(…); 
void I2CSlaveAddressSet(…); 
void I2CMasterEnable(…); 

7
Prin acest termen înțelegem o bucată de cod care gestionează în mod corect variabilele și structurile de
date care sunt accesate de mai multe secvențe de cod (din fire de execuție diferite) – permite doar accesul
unei singure secvențe de cod la date.

7
void I2CSlaveEnable(…); 
void I2CIntRegister(…); 
void I2CMasterControl(…); 
uint32_t I2CMasterDataGet(…); 
void I2CSlaveACKValueSet(…); 
……………………………………………………………………… 

Toate aceste funcții permit interacțiunea la nivel de registu cu interfața I2C.


La nivelul superior driverele sunt dezvoltate specific și de o așa natură care să permită
existența mai mai multor instanțe simultan, să aibe cerințe minime (RAM sau putere de
procesare), să fie ușor de configurat, să fie eficiente (fără polling, pe sistemul de întreruperi,
să permită punerea pe o perioadă cât mai lungă a procesorului în sleep) și ușor de lucrat cu
ele de către TI-RTOS etc.

Figura 5.6. Arhitectura software a sistemului

Funcțiile API (Application Programming Interface) ale fiecărui driver trebuie să fie
independente de dispozitivul pe care lucrează sau de placa de dezvoltare. Toate aceste API
sunt același pentru toate microcontrolerele (sau SoC-urile) și pentru toate plăcile de
dezvoltare – în acest mod orice aplicație care este dezvoltată pe orice alt dispozitiv este
portabilă pe oricare alt dispozitiv.
Dacă ne referim, din nou, în mod specific la portul I2C toate funcțiile API ale acestui

8
driver sunt următoare:
int_fast16_t I2C_control(…); 
void I2C_init(void); 
void I2C_Params_init(…); 
I2C_Handle I2C_open(…); 
bool I2C_transfer(…); 
void I2C_close(…); 
void I2C_cancel(…); 
 

Tipuri de fire de execuție

Aplicațiile de timp real trebuie să realizeze un anumit număr de funcții, unele în mod
independent altele dintre ele corelate. Aceste funcționalități sunt încapsulate în diferite fire
de execuție. Termenul fir de execuție este definit în mai multe moduri. În cadrul SYS/BIOS
prin fir de execuție înțelegem orice secvență de instrucțiuni executate de procesor.
Nucleul sistemului de operare TI-RTOS (SYS/BIOS) permite structurarea unei aplicații
sub forma unei sume de fire de execuție (thread-uri) – fiecare dintre ele implementează o
anumită funcționalitate a programului. Apare astfel un program de tip multithread. Astfel
de programe pot rula pe un singur procesor sau pe mai multe. În adrul TI-RTOS va rula
întotdeauna firul de execuție cu prioritatea ce mai mare care va întrerupe excuția firelor cu
o prioritate mai mică. Între aceste fire de execuție se permite interacțiunea (blocarea,
comunicarea de date și sincronizarea).

Figura 5.7. Tipuri de fire de execuție și prioritățile asociate

Sistemul de operare TI-RTOS este capabil să lucreze cu 4 tipuri diferite de fire de


execuție. În continuare sunt prezentate acestea, începând cu cele care au prioritatea maximă.
Acestea sunt: (a) întreruperi hardware (hardware interrupts sau Hwi) în care se includ și
funcțiile de tip Timer, (b) întreruperi software (software interupts sau Swi) care includ și
funcțiile de tip Clock, (c) tascuri și (d) firul de execuție de tip activitate de fond
(background), denumit generic Idle.

9
Firele de execuție de tip întreruperi hardware - Hwi

Firele de execuție de tip întrerupere hardware (Hwi), denumite și Interrupt Service


Routines (ISR) sunt fire de execuție ce au cea mai mare prioritate în cadrul nucleului SO
(SYS/BIOS). Firele Hwi sunt folosite pentru a efectua sarcini critice în timp care sunt
supuse unor limitări temporare stricte. Acest tip de thread este lansat în execuție ca răspuns
la un eveniment extern asincron. De exemplu, Hwi pot fi declanșate de:
a. un dispozitiv extern SoC-ului (care determină, de exemplu, apariția unui front pe un
pin – generat de apsarea unui buton) sau
b. de un dispozitiv intern SoC-ului sau microcontrolerului (un port UART care
generează o întrerupere la finalizarea recepționării corecte a unui character).
Codul din firele de execuție de tipul întreruperi hardware se execute până la
finalizarea acestuia, dar acest cod poate fi întrerupt temporar de alte Hwi cu o prioritate
mai mare generate de alte cereri de întrerupere.
Dar pentru a ajunge la execuția unui Hwi, anterior o serie de evenimente au loc – pentru
a înțelege toate mecanismele implicate, în cele ce urmează vom detalia procesul. În primul
pas un anumit eveniment asicron are loc – valoarea unui anumit pin se schimbă, un timer a
ajuns la zero cu procesul de decrementare, s-a recepționat un caracter pe portul UART sau
I2C etc. În pasul doi, într-un anumit registru un bit își va schimba starea semnalizând astfel
apariția acestui eveniment – fiecare astfel de evenimente au asociate biți specifici într-un
anumit registru (când bitul asociat este setat întreruperea a avut loc), în general acești
regiștri sunt cunoscuți sub numele generic de IFR (interrupt flag register).

Figura 5.8. Mecanismul validării unei întreruperi

În pasul trei, sistemul va lua sau nu în considerare întreruperea care tocmai a apărut. În
generat există cel puțin 2 nivele de control a acestei decizii: (a) prin intermediul
registrului/regiștrilor de tip IER (interrupt enable register) vom putea valida sau invalida
fiecare cerere de întrerupere în parte (există funcții specifice care realizează acest lucru) și
(b) prin intermediul unui bit global de tipul GIE (general interupt enable) prin care vom
valida/invalida în mod global toate întreruperile (se poate face din cod prin funcții
specifice).
Dacă o întrerupere individuală este validată, global am validat toate întreruperile și
întreruperea apare (deci, bitul correspondent în IFR este pus pe 1) abia acum, în pasul patru,
CPU-ul acceptă cererea de întrerupere oprindu-se din rularea oricărui alt fir de execuție de

10
o prioritate mai mică (Hwi, Swi, tasc sau fir Idle). Imediat se dezactivează în mod global
toate întreruperile (prin intermediul GIE), în pasul cinci. Mai departe, bitul asociat
întreruperii trebuie pus pe zero (în IFR), pasul șase. Funcție de arhitectura sistemului, acest
lucru se realizează în mod automat pentru toate întreruperile sau numai pentru o parte din
întreruperi sau acest lucru nu se realizează deloc iar această funcționalitate trebuie realizată
prin cod în funcția de tratare a întreruperilor. În pasul următor, șapte, se salvează adresa de
întoarcere. În pasul opt, procesorul determină sursa întreruperii și lansează în execuție
funcția de tratare a întreruperii (ISR – Interrupt Service Routine).
În ISR se salvează contextul firului de execuție care a fost întrerupt, pasul nouă – în
acest mod la revenire din funcția de tratare a întreruperii, firul de execuție întrerupt își va
urma execuția din punctul în care a fost lăsată fără nici o problemă. Se execută codul din
ISR (Hwi) – pasul zece. Iar la sfârșit, se validează global toate întreruperile (prin
intermediul GIE), se reface contextul sistemului și se revine în firul de execuție întrerupt –
totul în pașii unsprezece, doisprezece și treisprezece.
Salvarea contextului firului de execuție întrerupt (indiferent de tipul acestuia Hwi, Swi,
tasc sau Idle) și refacerea acestuia este un proces care este gestionat de SYS-BIOS. Pentru
programele care rulează cu suportul unui sistem de operare, procesul de salvare și refacere
a contextului firelor de execuție întrerupte este genstionat de nucleul (kernel-ul) acestora.
În alte situații acest cod trebuie implementat de dezvoltatorul aplicației în ISR.

Figura 5.9. Mecanismul “smart return”

Revenirea la firul de execuție care tocmai a fost întrerupt, deci pașii doisprezece și
treisprezece, se face printr-un așa zis mecanism de tip “smart return” – o metodă inteligentă
de întoarcere. Să presupunem că la un anumit moment de timp se execută un fir de execuție
de tipul Swi_a cu prioritatea 7. Acest Swi_a este întrerupt de o cerere de întrerupere externă
(un Hwi) care în mod implicit are o prioritate mai mare. În acest moment SO va executa
pașii de la unu la unsprezece – prezentați anterior. În mod normal, după finalizarea execuției
codului din Hwi se va restabili contextul Swi_a. Dar, să presupunem că Hwi anterior
inițializează o cerere către dispecerul SO pentru executarea unei Swi_b de prioritate 29
(valoarea exactă nu contează foarte mult, poate fi și 14 sau orice altă valoare, dar această
valoare trebuie să fie obligatoriu mai mare decât 7 pentru a ne atinge obiectivul acestei
prezentări). În mod normal imediat după refacerea contextului Swi_a, dispecerul sesizează
existența unui fir de execuție Swi_b pregătit pentru a fi rulat și de o prioritate superioară

11
Swi_a. În acest moment contextul și adresa de întoarcere a Swi_a sunt salvate încă o dată
și se dă controlul execuției lui Swi_b. Pentru a se evita tot acest proces, care este generator
de timpi de latență mari, nucleul SO verifică la fiecare întoarcere din Hwi dacă prioritatea
noului Swi (Swi_b în exemplul prezentat) care este pregătit de execuție (în urma cererii de
execuție din Hwi curent) nu este mai mare decât a Swi întrerupt (Swi_a în exemplul
nostru). Într-o astfel de situație nu se mai reface contextul Swi întrerup (Swi_a) și se dă
direct controlul Swi (Swi_b) a cărei execuție tocmai a fost cerută de Hwi curent. În acest
mod, de exemplu, se reduc latențele în execuția unui nou Swi de o prioritate mai mare.

Studiu de caz: Pentru a înțelege mai profund modul de funcționare a sistemului de


întreruperi în cadrul procesoarelor Cortex-M3 și Cortex-M4 se prezintă în
continuare modul de configurare a tratării unei întreruperi hardware generată de
apăsarea unui buton conectat la una din liniile unui port a procesorului CC3200.
Aici se vor sublinia și se va insista doar pe acele aspecte care determină necesitatea
înțelegerii conceptuale ale modalităților de configurare și funcționare a sistemului
de întreruperi. Aspectele tehncice vor fi prezentate în cadrul laboratorului.
Pentru simplitatea abordării vom alege modalitatea grafică, statică de configurare
a subrutinei care va fi executată în momentul apariției întreruperii externe.
Situația existentă: Un buton este conectat pe pinul 4 al SoC-ului CC3200. Pinul 4
(linia GPIO13 sau GPIOA1.5) este conectată la masă printr-un rezistor. În
momentu apăsării butonului acesta va “trage” linia GPIO13 la VDD (+3.6V).
Un LED roșu conectat la pinul 64, (GPIO09 sau GPIOA1.1).

Figura 5.10. Configurarea grafică a tipurilor de fire de execuție cu care


SYS-BIOS-ul va lucra

Pentru configurarea statică a funcțiilor asociate cu o anumită întrerupere va


trebui să modificăm fișierul .cfg din cadrul proiectului. Acest fișier conține toate
configurările statice ale SYS-BIOS (de ex. firele de execuție asociate cu
întreruperile externe sau interne, toate Swi care există la boot-are etc.). Deschideți
acest fișier (click dreapta  Open With  XGCONFG). Aici asigurați-vă că SYS-
BIOS-ul este configurat să conțină toate acele elemente capabile să susțină
funcționarea firelor de execuție de tip Hwi (într-o astfel de situație un semn mic
bifat verde va apare în colțul din stânga jos, Figura 5.10).
Creați o nouă Hwi și configurați-o. Pentru configurarea corectă a Hwi aveți în
principal o serie de câmpuri în care trebuie să puneți acea informație corectă
necesară funcționării acesteia, Figura 5.11.

12
Figura 5.11. Parametri de configurare a unei Hwi

Tabelul 5.1. Sursele de întreruperi existente în procesoarele


Cortex-M3 și Cortex-M4 [TI, 2014]

Prin câmpul “Handle” vom da un nume obiectului nostru (Hwi), nume ce trebuie
să fie unic în sistem. În câmpul “ISR function” se va trece numele funcției (din codul
programului dumneavoastră) care va fi executată atunci când va apare întreruperea
asociată.

13
Cel mai important parametru din panoul de configurare din Figura 5.11 este
numărul întreruperii. Deoarece fiecare sursă de întrerupere are un număr unic de
întrerupere asociat, cunoașterea acestui număr va determina lansarea corectă în
execuție a Hwi atunci când evenimentu dorit va avea loc.
Analizând foaia de catalog, [TI, 2014], în special tabelul cu sursele vectorilor de
întrerupere (vezi Tabelul 5.1) pentru portul A1, unde este poziționată linia GPIO
13, vom trage concluzia că va trebui să utilizăm valoare asociată din prima coloană
(1 în situația noastră particulară, IRQ1) în câmpul “Interrupt number” din Figura
5.11. Dar din păcate nu este cazul.

Figura 5.12. Tabelul vectorilor de întrerupere. Asocierea dintre întreruperi și


adresele apelurilor către subrutinele de tratare a întreruperilor [TI, 2014]

Dacă, am merge mai în adânc cu studiul mecanismului de tratare a întreruperilor


vom observa că după acceptarea unei cereri de întrerupere și decodificarea sursei de
întrerupere, procesorul va realiza un salt la o adresă asociată cu întreruperea
respectivă – toate întreruperile au asociate astfel de adrese în tabelul vectorilor de
întrerupere, vezi Figura 5.12. Pentru portul A1 avem asociată IRQ1 (vezi Tabelul
5.1) iar saltul se va realiza la adresa 0x0044 de unde se va executa codul. La
diferitele adrese din tabelul vectorilor de întrerupere trebuie plasate o serie de salturi
către zonele de cod unde se vor realiza tratarea întreruperilor respective.

14
Concluzionând în câmpul “Interrupt number” din Figura 5.11 va trebui să punem
una din valorile din secțiunea “Exception number” din Figura 5.12. Pentru linia
GPIO13, parte a portului A1, vom trece valoarea 17 în câmpul “Interrupt number”
din Figura 5.11.
Sitemul de întreruperi al procesorului Cortex-M4 împarte registul de priorități
al fiecărei întreruperi în două câmpuri: (a) biții ce definesc prioritatea de grup și (b)
biții ce definesc prioritățile în cadrul unui grup - subprioritatea. Cu cât prioritate de
grup este mai mare cu atât cererea respectivă va putea întrerupe coduri asociate cu
întreruperi ce au priorități de grup mai mici. Dacă există mai multe întreruperi în
așteptare ce au aceeași prioritate de grup, câmpul de subprioritate determină ordinea
în care aceste cereri sunt procesate. Dacă există mai multe întreruperi în așteptare
care au aceeași prioritate de grup și subprioritate, întreruperea cu cel mai mic număr
IRQ este procesată mai întâi.
Pentru procesorul nostru (Cortex-M4) există 8 priorități de grup: 0 (prioritatea
cea mai mare), 1, 2, 3 ..., 7 (prioritatea cea mai mică – prioritate marcată cu -1).
Astfel în câmpul “Interrupt priority” (din Figura 5.11) introduceți prioritatea de
grup.
Câmpul “Event Id” (din Figura 5.11) nu are nici o semnificație pentru pentru
procesorul nostru. Acest câmp este necesar doar atunci când vom lucra cu un DSP,
precum oricare DSP din cadrul familiei C6000.
Check box-ul “Enable at startup” (din Figura 5.11) va valida toate întreruperile
generate de portul A1.
Prin toate aceste elemente noi am modificat structura fișierului .cfg cu ajutorul
interfeței grafice (GUI), vezi Figura 5.11. Astfel observăm apariția următoarelor
linii suplimentare în acest fișier care includ toate configurările realizate anterior prin
intermediul diferitelor câmpuri ale panoului din Figura 5.11:
var halHwi0Params = new halHwi.Params(); 
halHwi0Params.instance.name = "halHwi0"; 
halHwi0Params.priority = 2; 
Program.global.halHwi0 = halHwi.create(17, "&myHwi", halHwi0Params);
Dar în plus va trebui să validăm și întreruperile generate de pinul 4, cunoscut
sub numele de linia GPIO13 sau GPIOA1.5. Aceasta se realizează prin următoarea
instrucțiune:
GPIOIntEnable(GPIOA1_BASE, GPIO_INT_PIN_5); 
În funcția de tratare a întreruperilor generate de liniile portului A1, trebuie să
verificăm prima dată care linie a generat întreruperea, iar ulterior să invalidăm bitul
asociat din IFR – altfel după finalizarea Hwi vom reveni la infinit în același Hwi
deoarece un bit pe 1 în IFR semnifică existența unei întreruperi (sistemul nu are
mecanismele de a sesiza că această cerere a fost servită deja).
Observăm din codul de mai jos că subrutina Hwi primește un argument de
intrare. Acesta este valoarea din câmpul “Argument passed to ISR function” din
panoul de configurare, vezi Figura 5.11.
Void myHwi (UArg arg0) 

    unsigned long ulPinState = GPIOIntStatus(GPIOA1_BASE, 1); 
 

15
    if(ulPinState & GPIO_PIN_5) 
        { 
        GPIO_toggle(Board_LED0); 
        GPIOIntClear(GPIOA1_BASE, GPIO_INT_PIN_5); 
        } 
}

Timere

După cum se prezintă în Figura 5.7 SYS/BIOS oferă utilizatorilor și oportunitatea de a


lucra cu module de tip timer. Aceste module permit crearea și utilizarea elementelor de tip
timer (crearea software – configurare și oferire servicii către aplicații). Aceste timere pot
apela funcții la finalizarea procesului de numărare. Elementele de tip timer sunt elemente
ce aparțin HAL. Funcțiile de tip timer apelate sunt similar cu cele de tip Hwi, având aceleași
proprietăți ca și acestea. Avantajul fundamental al acestor module este dat de faptul că nu
sunt necesare configurări specifice. Există două moduri de configurare a acestora: rulare o
singură data (one-shot – după atingerea numărului stabilit se va lansa în execuție funcția
asociată dar acest process va avea loc doar o singură dată), rulare contunuă (continuous
mode – după atingerea numărului stabilit, timer-ul se reîncarcă cu valoarea inițială, acest
proces continuând la infinit după finalizarea fiecărui ciclu se execută funcția asociată).
Perioada de numărare poate fi specificată în număr de pulsuri cuantizate sau în
microsocunde.
Elementele de tip timer pot fi create în mod graphic (static) sau prin cod (dynamic, în
momentul execuției programului – runtime) prin intermediul funcției Timer_create().
Deoarece numărul emenetelor de tip timer este dependent în principal de tipul dispozitivului
pe care rulează codul există o funcție care să ne permit aflarea cu exactitate a timer-elor
disponibile: Timer_getNumTimers(). Există funcții pentru pornirea (Timer_start()), oprirea
(Timer_stop()) sau modificarea perioadei (Timer_setPeriod()) unui timer.

Firele de execuție de tip întrerupere software - Swi

Firele de execuție de tip întrerupere software (Swi) au priorități inferioare Hwi dar
superioare tascurilor. Astfel, o funcție Swi care se execute poate fi întreruptă în orice
moment: (a) de orice fir de execuție de tip Hwi sau (b) de o funcție Swi de prioritate
superioară. În mod similar o funcție Swi va întrerupe orice funcție de tip task sau Idle în
orice moment al execuției lor.
Firele de execuție de tip SWI nu trebuie să fie încurcate cu instrucțiunea SWI, această
instrucțiune există pe multe procesoare (de exemplu pe cele din familia ARM). Acest modul
al SYS-BIOS (nucleului SO TI-RTOS) este unul independent de arhitectura procesorului
pe care rulează TI-RTOS și nu are nici o legătură cu instrucțiunea SWI.
Deosebirea între Hwi și Swi este data de cel care inițializează executarea codului. Dacă
Hwi sunt executate atunci când apare o întrerupere hardware, Swi sunt lansate în execuție
din program prin apelarea unor funcții specific API-ului nucleului SO (SYS/BIOS) de către
sevențe de cod care, de exemplu, sunt plasate în Hwi.
Codul existent în Swi este și el supus, de cele mai multe ori, unor constrângeri de
timp și din acest motiv nu va fi introdus în fire de execuție de tip tasc dar termenele limită

16
nu sunt așa de dure ca la Hwi.
Swi vor rula până în momentul finalizării lor, mai mult în cadrul unei funcții de tip
Swi nu trebuie să existe nici un mechanism de blocare a firului de execuție. În corpul
întreruperilor software se introduce o parte din codul care ar exista în Hwi dacă programul
ar rula fără un SO. Scopul urmărit este de a minimza timpul petrecut de processor în
Hwi, timp în care celelalte Hwi nu vor fi validate. Swi nu au o stivă proprie, ele utilizează
stiva sistemului de operare, și necesită doar îndeajuns de mult spațiu pentru salvarea
contextului fiecărei Swi atunci când se comută către un alt fir de execuție.
În capitolul în care s-au prezentat întreruperile de tip hardware s-a prezentat un exemplu
în care cele două fire de execuție Swi_a și Swi_b au priorități diferite, vezi Figura 5.9.
Mecanismul “smart return”. În continuare vom analiza o altă situație în care firele de
execuție Swi_a și Swi_b au aceeași prioritate.

Figura 5.13. Comportarea sistemului la priorități egale ale Swi

În acest caz Swi_a este firul care rulează (de prioritate 17) și la un anumit moment apare o
cerere de întrerupere care va fi deservită de firul de execuție Hwi (fir de execuție de o
prioritate superioară oricărui Swi), vezi Figura 5.13. În cadrul Hwi se inițializează o cerere
către dispecerul SO pentru executarea unei Swi_b de prioritate 17 – aceeași prioritate ca și
Swi_a care tocmai a fost întreruptă. Deoarece firele de execuție de aceeași prioritate sau
firul de execuție de o prioritate inferioară nu poate întrerupe un alt fir de execuție, vom
reveni din Hwi în Swi_a. Swi_a va rula până în momentul în care se va finaliza iar ulterior
Swi_b se executa până la finalizare – ca în cazul nostru sau până când un alt fir de o
prioritate superioară va exista în sistem. În final când nu mai avem nimic de executat (Hwi,
Swi sau tascuri) se revine la firul de tip Idle.
În concluzie, firele de execuție de aceeași prioritate se vor executa conform principiului
primul venit primul servit – first-in first-out (FIFO).
Un avantaj fundamental al existenței firelor de execuție de aceeași prioritate este dat de
posibilitatea partajării aceleiași resurse comune (un vector de date, un port de tip I2C sau
UART, etc.) fără nici o problemă. Deoarece firele de execuție au aceeași prioritate după
finalizarea utilizării resursei partajate de către primul fir, abia ulterior cel de al doilea va
putea să utilizeze resursa – atunci cand primul fir de execuție și-a încheiat în totalitate
rularea și deci orice acces la ea.

17
Cu cât există mai multe fire de execuție în sistem, de priorități diferite, cu atât mai mult
stiva va crește în momentul în care diferite fire de execuție sunt întrerupte de alte fire de
execuție. Pentru a avea o idee a numărului de octeți ce sunt salvați pe stivă atunci când se
realizează o schimbare de context vă rog să studiați Tabelul 5.2. Evident că aceste date sunt
dependente de arhitectura pe care se lucrează. Datele prezentate în Tabelul 5.2 sunt doar
pentru procesoarele de tipul Cortex-M3, M4.
Utilizarea mai multor fire de execuție de tip Swi, de aceeași prioritate, va genera
automat o creștere mai moderată a cantităților de date care se salvează pe stivă – acesta este
un alt avantaj al acestei abordări.

Tabelul 5.2. Numărul de octeți salvați pe stivă la schimbarea


contextului de lucru [TI, 2017]
Cortex-M3, Octeți salvați pe stivă la prima Octeți salvați pe stivă la apelări
M4 apelare succesive de prioritate superioară
Hwi 176 80
Swi 104 88

Studiu de caz: Crearea și configurarea unui fir de tip Swi.

Figura 5.14. Configurarea unei funcții de tip Swi

Având configuratorul grafic înserați un nou Swi. Configurarea unui nou Swi este foarte
ușoară. Mânerul (“Handle”) este o variabilă unde se va stoca un identificator al acestui nou
element care va fi creat: un fir de execuție de tipul Swi.
Cu ajutorul câmpul “Interrupt priority” se va seta prioritatea acestui nou Swi. Pentru
procesorul Cortex-M4 avem în principal 32 de priorități – de la 0 la 31. Cea mai mica
prioritate este 0.
Cererea de execuție a unei Swi se poate realiza prin intermediul a mai multor funcții:
Swi_post(), Swi_dec(), Swi_inc(), Swi_andn() și Swi_or(). Apelul acestor funcții se poate realiza
de oriunde din program: funcții Hwi, timere, funcții Idle sau Clock, alte Swi etc. Chiar din
interiorul funcției unei Swi o Swi se poate posta pe ea însăși.
Dacă se inițializează o cerere către dispecerul SO pentru executarea unei Swi, atunci
acesta:

18
1. Se adaugă în lista Swi pregătite să fie executate;
2. Se verifică dacă firele de execuție de tip întreruperi software sunt validate. Dacă
nu sunt validate, ca atunci când se execută un Hwi, dispecerul permite
continuarea rulării firului de execuție curent.
3. Dacă firele de execuție de tip întreruperi software sunt validate, se verifică
prioritatea Swi tocmai pregătită spre execuție față de prioritatea firului de
execuție care tocmai rulează.
4. Dacă firul de execuție curent este de tip Idle, task sau o Swi de prioritate
inferioară atunci programatorul SO elimină noul Swi din lista Swi pregătite să
fie executate, schimbă contextul și noua Swi va fi executată.
5. Dacă firul de execuție curent este unul de tipul Swi de aceeași prioritate sau de
o prioritate mai mare, programatorul SO dă controlul firului de execuție curent,
iar noul Swi va fi executat după ce toate Swi de prioritate mai mare sau egală își
vor finaliza execuția.
6. Dacă există multiple Swi de prioritate similară atunci acestea se vor executa în
ordinea în care ele au fost înregistrate în lista Swi pregătite să fie executate.
Dacă o funcție de tip întrerupere software este postată de mai multe ori, atunci când ea
este deja în lista firelor de execuție pregătite să fie executate – deci anterior execuției ei
(când este înlăturată din această listă), ea se va executa doar o singură dată. Comportarea
Swi în această situație este similară cu cea a unui Hwi care este executat doar o singură dată
chiar dacă evenimentul extern generează mai multe cereri de întreruperi anterior resetării
bitului corespunzător din IFR (interrupt flag register) de către CPU.
Fiecare funcție de tipul Swi are asociată o variabilă de tipul trigger (vezi Figura 5.14)
– care poate condiționa execuția Swi. Funcție de tipul arhitecturii pe care TI-RTOS rulează
aceasta variabilă poate fi pe 16 sau pe 32 de biți. În funcție de fucția API care este utilizată
(Swi_post(), Swi_dec(), Swi_inc(), Swi_andn() și Swi_or()) în plasarea în execuție a Swi această
variabilă de tip trigger poate determina sau nu execuția Swi sau poate deveni un parametru
care să fie evaluat în cadrul codului intern funcției Swi. Prin intermediul acestei variabile
avem un mecanism de control a condițiilor în care o Swi va fi va fi trecută în lista firelor de
execuție pregătite să ruleze sau de câte ori o vom executa.
În continuare prezentăm principalele caracteristici ale funcțiilor care pot realiza
postarea unei Swi:
1. Swi_post(), Swi_inc() și Swi_or() determină trecerea necondiționată Swi în lista
firelor de execuție pregătite să ruleze – a Swi postate;
2. Swi_post() nu are nici o influență asupra varabilei de tip trigger a Swi;
3. Prin intermediul Swi_or() anumiți biți ai variabilei trigger sunt puși pe 1 (în
funcție de o mască care este trimisă ca argument a funcției Swi_or()) iar ulterior
funcția Swi este postată - Swi este trecută în lista firelor de execuție pregătite să
fie executate;
4. Swi_inc() incrementrează variabila trigger cu o unitate înaintea postării funcției
Swi;
5. Funcțiile Swi_dec() și Swi_andn() trece funcția Swi în lista firelor de execuție
pregătite să ruleze doar în situația în care valoarea variabilei trigger devine zero;
6. Swi_andn() – pune pe zero anumiți biți ai variabilei trigger (în funcție de o mască
care este trimisă ca argument a funcției Swi_andn());
7. Swi_dec() decrementrează variabila trigger cu o unitate.

19
În mod sintetic toate informațiile prezentate anterior pot fi prezentate ca în Tabelul 5.3.

Tabelul 5.3. Influența variabiliei trigger asupra Swi


Firul Swi trecut în lista Variabila trigger
thread-urilor pregătite Tratată ca un Tratată ca o
Ne modificată
să ruleze contor mască de biți
Doar dacă trigger este 0 Swi_dec() Swi_andn() -
Întotdeauna Swi_inc() Swi_or() Swi_post()

Pentru accesarea variabilei de tip trigger se utilizează funcția Swi_getTrigger(). Această


funcție poate fi apelată doar din corpul funcției Swi. Valoarea întoarsă de această funcție
va fi valoarea variabilei trigger înainte de eliminarea funcției Swi din lista firelor de
execuție pregătite să ruleze.
Când SYS-BIOS-ul elimină o Swi din lista firelor de execuție pregătite să ruleze (acest
lucru are loc doar atunci când această Swi este gata să ruleze și execuția ei începe) valoarea
variabilei trigger este setată la valoarea inițială a varibilei care a fost dată în momentul în
care Swi a fost creată.

Mecanismul de sincronizare de tip semafor

Un mechanism fundamental de sincronizare, introdus în 1965 de E.W. Dijkstra (în acea


vreme profesor la Technical University of Eindhoven, în Olanda) este semaforul.
Un semafor constă într-o variabilă cu rol de contor (care întotdeauna este mai mare sau
egală cu zero), o coadă de așteptare (care în cadrul SYS-BIOS poate fi de tipul FIFO sau
pe bază de priorități) şi o pereche de operaţii, de aşteptare şi de eliberare (sau încrementare
a contorului). Un fir de execuție care cere acces la o resursă pe care semaforul o păzeşte,
realizează o operaţie de aşteptare la semafor. Când firul de execuție termină operațiile cu
resursa respectivă sau atunci când este deblocat de alt fir de execuție se efectuează o
operaţie de eliberare. De aici observăm că semafoarele sunt utilizate la sincronizarea între
fire de execuție și la acesarea sigură a anumitor resurse partajate de cel puțin 2 fire de
execuție.
Variabila de tip contor a semaforului determină ce se întâmplă când o operaţie de
aşteptare (Semaphore_pend()) sau de eliberare se execută (Semaphore_post()). Când un fir de
execuție creează un semafor, poate seta contorul semaforului la o valoare iniţială, care este
un număr pozitiv. Această valoare indică numărul de thred-uri pe care semaforul le va lăsa
să treacă înainte de blocare. Ori de câte ori un task efectuează un apel de așteptare pe
semafor, SO verifică dacă variabila contor este mai mare decât zero – în această situație,
Semaphore_pend() decrementează în primul rând contorul cu 1 și permite execuția codului
plasat imediat după Semaphore_pend(). În caz contrar firul de execuție este blocat și așteaptă
un Semaphore_post() care incrementează variabila contor internă semaforului cu o unitate și
ulterior îi va permite funcționarea.
Funcția Semaphore_pend() acceptă și existența unui parametru de tip timeout, care
determină așteptarea existenței unei valori a contorului mai mare ca zero un interval infinit
de timp (BIOS_WAIT_FOREVER), definit de utilizator sau nu va aștepta deloc

20
(BIOS_NO_WAIT).
Dacă discutăm de resurse partajate, atunci când un fir de execuție a terminat folosirea
oricărei resurse pe care semaforul o are în pază, thread-ul emite un apel de eliberare către
semafor. Apelul de eliberare determină SO să incrementeze numărătorul semaforului cu 1
și apoi să verifice dacă mai există și alte fire de execuție blocate. Dacă da, primul dintre
thread-urile blocate, în ordinea în care au ajuns la semafor, va fi deblocat și va primi dreptul
de a accesa resursa comună păzită de acest semafor.
Void Task1 (UArg arg0, UArg arg1) 

      for (;;) { 
   Semaphore_pend (mySem, BIOS_WAIT_FOREVER); 
 

   resursa = 13; 
 

   Semaphore_post (mySem); 
      } 
}   
 
Void Task2 (UArg arg0, UArg arg1) 

      for (;;) { 
   Semaphore_pend (mySem, BIOS_WAIT_FOREVER); 
 

   resursa++; 
 

   Semaphore_post (mySem); 
   Task_sleep (5); 
      } 
}   

Semafoarele pot fi de tip binare sau generalizate (sau de tip contor). Semafoarele binare
permit contorului să ia doar valorile 0 și 1 – incrementarea contorului peste valoarea unu
este imposibilă. Acestea, de exemplu, permit numai unui singur thread la un moment dat
să acceseze o resursă partajată. În cadrul semafoarelor de tip contor, inițializarea variabilei
interne cu un anumit număr definește numărul firelor de execuție ce pot fi deblocate
simultan (atâta timp cât contorul este mai mare ca zero).

Figura 5.15. Interfața cu utilizatorul pentru configurarea semafoarelor

Firele de execuție (task-urile în cadrul SO TI_RTOS) vor aștepta deblocarea


semafoarelor (de tip binare sau de tip contor) în mod FIFO indiferent de prioritatea lor sau
se pot crea semafoare cu prioritate în care lista de așteptare a task-urilor să fie ordonată
funcție de prioritatea fiecărui task, vezi Figura 5.15. Rezultând că task-urile de de aceeași

21
prioritate din listă vor fi deblocate prin intermediul principiului FIFO, în timp ce task-urile
de prioritate mare for fi deblocate primele, înaintea task-urilor de prioritate mai mica.

Firul de execuție de tip task

Task-urile sunt fire de execuție complexe și foarte versatile în comparație cu alte tipuri
de fire de execuție suportate de TI-RTOS.
Un fir de execuție de tip task are întotdeauna o prioritate mai mare decât firul de tip Idle
dar mai mica decât orice întrerupere software. Existând un număr de 32 (sau 16 funcție de
arhitectura CPU8) de potențiale priorități pe care un task le poate lua. Prioritatea minimă
este 1, în timp ce prioritatea maximă este 31. În acest interval de valori, prioritățile unui
task se pot schimba în mod dinamic funcție de cerințele aplicației. Funcție de prioritate unui
anumit task el poate fi întrerupt de un alt task sau poate întrerupe un task de o prioritate mai
scăzută. Întotdeauna un task pregătit de execuție (care nu mai este blocat) de o prioritate
mai mare va întrerupe task-ul care rulează și care are o prioritate mai mică. Un task de o
prioritate mai mare poate apela o funcție de tipul Semaphore_pend(), Task_sleep() sau alte
mechanisme de blocare care să permită task-urilor de prioritate mai scăzută să ruleze.
În principal task-urile sunt gândite să se execute: (a) lungi perioade de timp, (b) în mod
continuu sau să nu-și finalizeze niciodată execuția. Aceasta este una dintre diferențele
fundamentale dintre Swi și tascuri. Dintr-un task se iese de obicei când întreaga aplicație se
termină sau poate atunci când nu mai este necesar acel task. O altă diferență este data de
capacitatea tascurilor de a fi blocate în timpul execuției până în momentul în care resursele
necesare vor deveni din nou disponibile sau alte evenimente externe vor avea loc. Pe
perioada de timp în care un task este blocat el nu va consuma timp de processor
poziționându-se în lista firelor de execuție blocate.

Figura 5.16. Structura unui task

Un task, la modul cel mai generic posibil, este alcătuit din trei secvențe distincte de cod.
În prima secvență de cod, se realizează toate inițializările necesare execuției codului ce va
8
De exemplu, pe procesoarele familiilor MSP430 C28x

22
urma. Cea de a doua secvență de cod, este o buclă while care de cele mai multe ori se
execută la infinit – putând exista și situații în care funcție de existența unei anumite condiții
execuția acesteia să se finalizeze. Aici se plasează codul principal al aplicației. Tot în cadrul
acestei bucle while există și o secvență (care de cele mai multe ori este implementată printr-
un semafor) care determină blocarea task-ului – Semaphore_pend() în Figura 5.16. Codul,
ulterior acestui mecanism de blocare, va fi executat doar când dintr-un alt fir de execuție se
va executa funcția Semaphore_post(). Un fir de execuție blocat va fi plasat în lista firelor de
execuție blocate și va primi timp de procesor doar atunci când el se va debloca și când va
avea prioritatea necesară pentru a fi executat. În ultima secțiune, se plasează secvențele de
cod care eliberează resursele utilizate anterior – de ex. se eliberează zonele de memorie
alocate dinamic sau se “închid” porturile (UART sau I2C) “deschise” anterior.
Ca și celelalte obiecte ale nucleului SO, un task poate fi creat static (prin intermediul
fișierului de configurare a aplicației) sau dinamic (în orice moment este necesar în timpul
rulării unei aplicații).
Complexitatea unui task este data și de faptul că este singurul fir de execuție care
înglobează toate stările și toate tranzițiile între stări care au fost prezentate în Figura 5.4.
Un task este terminat atunci când se execută o funcție precum Task_exit(). Această funcție
este apelată în mod automat la finalizarea unui task. Un task va fi blocat la fiecare apelare
de funcție care determină ca starea lui să treacă în starea de suspendat (Semaphore_pend()
sau Task_sleep()). În plus, față de cele prezentate în Figura 5.4, un task mai are și starea
inactivă. În starea inactivă un task are o prioritate egală cu -1 ce are semnificația că task-ul
este într-o stare de pre-pregătire. O astfel de prioritate poate fi setată prin intermediul
funcției Task_setPri().
Fiecare tasc are stiva lui proprie. Pentru comunicația de date și sincronizarea între
tascuri SYS/BIOS oferă mai mult mecanisme: semafoarele, evenimentele, cozile de mesaje
și cutiile de poștă (mailboxes).

Firul de execuție de tip Idle

Firele de execuție de tip Idle se execute cu prioritatea cea mai mica din system, orice
alte tipuri de tread-uri întrerup thead-ul Idle. Acestea sunt executate unele după altele într-
o buclă infinită în ordinea în care au fost create. Pentru executarea unui astfel de fir de
execuție cel precedent trebuie să își finalizeze execuția. Bucla Idle rulează continuu atâta
timp cât nu este întreruptă de firele de execuție de tip task, Swi sau Hwi.

23

S-ar putea să vă placă și