Programszálak (Threads)

A fejezet tartalma:
A C programok általános építőelemei a függvények, amelyeket azért hívunk meg, hogy valamilyen eljárást, előírt utasítás-sorozatot elvégezzenek. Az RTOS környezet általános építőköve a végrehajtási szál vagy programszál (thread), ami önálló végrehajtási egységként működő objektum, szekvenciálisan végrehajtható utasítás-sorozat. A programszál nagyon hasonlít a C függvényekhez, de van néhány fontos különbség: A meghívott C függvény mindig visszatér az őt meghívó programrészhez. A programszál viszont egy olyan függvény, amelyből nincs visszatérés: kötelezően tartalmaznia kell egy végtelen ciklust. A programszál tehát úgy vehető, mintha egy független program lenne, ami az RTOS környzeteben fut. Az RTOS alkalmazások több programszálból állnak, amelyek látszólag párhuzamosan futnak, s minden programszál külön feladatot lát el. A programszálak futásának összehangolását az ütemező végzi el, ami az RTOS rendszer központi eleme.

Az mbed-RTOS környezetben a programszálakat a Thread objektumosztály példányaiként hozhatjuk létre, s ezek saját veremtár területtel rendelkeznek. A programszálankénti veremtár alapértelmezett mérete (DEFAULT_STACK_SIZE) = 2 kB.

Az első programszál mindig a main() függvény lesz, amely automatikusan létrejön, ezt nekünk már nem kell Thread objektumként példányosítani. A main() függvényt is beleértve összesen legfeljebb 6 programszálat hozhatunk létre.

Programszálak létrehozása

Programszálakat a Thread objektumosztály példányosításával hozhatunk létre. Csak az első paraméter megadása kötelező, a többi paraméternek van alapértelmezett értéke.
Thread  mythread(void(*task)(void const *argument),    // Függvénymutató
 void *argument=NULL, // Átadandó adatra mutató pointer
 osPriority priority=osPriorityNormal, // Programszál prioritása
 uint32_t stack_size=DEFAULT_STACK_SIZE, // Veremtár mérete
 unsigned char *stack_pointer=NULL // Veremtár mutató

A konstruktor meghívásakor első paraméterként egy speciális függvénymutatót kell megadnunk. Az a függvény, amire rámutatunk, adja meg a programszál végrehajtandó utasítás-sorozatát, s formailag az alábbi feltételeket kell, hogy kielégítse:
A konstruktor második paramétere egy típus nélküli mutató, amelyet  át akarunk adni a programszálnak. Ez többnyire egy struktúrára vagy tömbre mutató pointert, melynek értelmezése és felhasználása a programozó kompetenciájába tartozik. Ennek a paraméternek főleg akkor van szerepe, ha ugyanabból a függvényből több programszálat is létre akarunk hozni. Más esetekben ez a paraméter elhagyható, ekkor az alapértelmezett NULL érték adódik át.

A konstruktor harmadik paramétere a programszál futási prioritását adja meg (alapértelmezetten osPriorityNormal). A programszálakhoz rendelhető prioritás értékeket az alábbi táblázatban foglaltuk össze:

1. táblázat: Egy Thread objektum futási prioritásának lehetséges értékei
Szimbólum
Érték
Prioritás
osPriorityIdle
-3
Tétlen (legalacsonyabb)
osPriorityLow -2
Alacsony
osPriorityBelowNormal -1
Normál alatti
osPriorityNormal 0
Normál
osPriorityAboveNormal 1
Normál fölötti
osPriorityHigh 2
Magas
osPriorityRealtime 3
Valósidejű (legmagasabb)
osPriorityError
0x84
Nem meghatározott, vagy érvénytelen prioritás
A konstruktor negyedik és ötödik paraméterével a programtárhoz rendelt veremtár méretét és helyét szabhatjuk át. Ez a beavatkozás azonban hardverfüggő és mélyebb ismereteket igényel.

Programszálak futási állapotai

Egy Thread objektum az alábbi állapotok valamelyikében lehet:

1. ábra: A programszálak lehetséges állapotai

A Thread objektumosztály tagfüggvényei

A Thread osztály tagfüggvényeit és statikus tagfüggvényeit az alábbi két táblázatban foglaltuk össze. A két típus között az a fő különbség, hogy a publikus tagfüggvények az objektumpéldányhoz rendeltek, így az objektumpéldány metódusaként hívhatók meg, mint pl. a set_priority() tagfüggvény az alábbi példában.
    Thread thread2(music);                  //Define a new task
thread2.set_priority(osPriorityHigh); //Give it high priority
A statikus publikus tagfüggvények ezzel szemben nem tartoznak egyetlen objektumpéldányhoz sem, hanem az osztályhoz tartoznak. Meghívásuk az osztály neve és a scope operátor segítségével történhet, mint az alábbi példában:
    Thread::wait(1000);                     //Wait for 1000 msec

2. táblázat: A Thread  osztály publikus tagfüggvényei
Függvény neve
Funkciója
terminate()
Bezárja a programszál futását és kiveszi az aktív feladatok közül
set_priority(pri)
Beállítja/módosítja a  programszál futási prioritását
get_priority()
Lekérdezi az aktuális programszál prioritását
signal_set(signals)
Beállítja az aktuális programszál megadott jelzőbitjeit
signal_clr(signals)
Törli az aktuális programszál megadott jelzőbitjeit
get_state()
Lekérdezi az aktuális programszál állapotát
stack_size()
Lekérdezi a programszál rendelkezésére álló veremtár méretét
free_stack()
Lekérdezi a programszál rendelkezésére álló, még szabad veremtárterület méretét
used_stack()
Lekérdezi a programszál által felhasznált veremtárterület méretét
max_stack()
Lekérdezi a programszál által a futása során felhasznált veremtárterület maximális méretét
Megjegyzés: A fenti táblázatban az "aktuális programszál" azt az objektumpéldányt jelenti, amelynek a tagfüggvényét meghívtuk. Például: thread2.stack_size() esetén a thread2 programszálat jelenti.


3. táblázat: A Thread  osztály statikus publikus tagfüggvényei
Függvény neve
Funkciója
signal_wait(signals,time)
Várakozás indítása egy vagy több eseményjelzőre vonatkozóan. Az opcionális time paraméter a maximális várakozási időt adja meg ezredmásodpercekben. Alapértelmezett értéke osWaitForever, azaz végtelen várakozás.
wait(time)
Várakozás a time paraméterben ezredmásodpercekben megadott ideig
yield()
Átadja a vezérlést a következő READY állapotú programszálnak.
gettid()
Lekérdezi az éppen futó programszál azonosító számát
Megjegyzés: A statikus publikus tagfüggvények meghívása az osztály neve és a scope operátor segítségével történhet. Például:  Thread::wait(500), vagy Thread::yield.

Mintapélda: Egy többfeladatos RTOS alkalmazás

Az alábbi mintaprogramban három feladat fut párhuzamosan, három programszál felhasználásával.
  1. A főprogram az első szál, ami a FRDM-KL25Z kártya MMA8451Q gyorsulásmérőjét kérdezi le 100 ms-onként, s a gyorsulásvektor X,Y,Z komponensei szerint vezérli a FRDM-KL25Z kártya RGB LED-jének színkomponenseit.
  2. A második programszál a PWM fejezetben bemutatott zenelejátszó mintapéldát futtatja végtelen ciklusban. A zene megszólaltatásához egy piezo csipogót kell  kötni  a D3 (PTA12) kivezetésre.
  3. A harmadik programszál az A0 analóg bemenetre (PTB0 kivezetés) kötött MCP9700A analóg hőmérő jelét méri meg, átlagolja, s az eredményt kiírja a standard kimenetre (az UART0 soros porton keresztül, 9600 bit/s, 8N1 formátum).
 
Hardver követelmények:
1. lista: A 09_rtos_threads/main.cpp program listája
#include "mbed.h"
#include "rtos.h"
#include "MMA8451Q.h"

MMA8451Q acc(PTE25,PTE24,0x3A); //SDA, SCL, I2C address lef shifted
PwmOut rled(LED_RED); //configure RGB LED pins as PWM outputs
PwmOut gled(LED_GREEN);
PwmOut bled(LED_BLUE);
PwmOut buzzer(D3); //used to play music

float frequency[]= {659,554,659,554,550,494,554,587,494,659,554,440}; //frequency array
uint8_t beat[]= {2,2,2,2,2,1,1,2,2,2,2,4}; //beat array

void music(void const *args)
{
while (1) {
for (int i=0; i<12; i++) {
buzzer.period(1/frequency[i]); // set PWM period
buzzer=0.5; // set duty cycle
Thread::wait(250*beat[i]); // hold for beat period
}
}
}

void thermometer(void const *args)
{
AnalogIn ain(A0); // Analog input at PTB0
uint32_t mysum; // Used for summation

printf("\r\nTask3: analog thermometer - with averaging\r\n");
while(1) {
mysum = 0;
for(int i=0; i<3300; i++) {
mysum += ain.read_u16(); // sum up raw 16-bit data
}
float voltage = mysum>>16; // voltage in millivolts
float tempC = (voltage -500)/10; // tempereature in Celsius
printf("voltage: %5.0f mV temp: %5.1f C\r\n",voltage,tempC);
Thread::wait(2000);
}
}


int main(void)
{
Thread thread2(music); //Define a new task
thread2.set_priority(osPriorityHigh); //Give it high priority

Thread thread3(thermometer); //Define another new task
thread3.set_priority(osPriorityLow); //Give it high priority

while (true) { //Run the default task
float x, y, z;
x = abs(acc.getAccX()); //Read X component of acceleration
y = abs(acc.getAccY()); //Read Y component of acceleration
z = abs(acc.getAccZ()); //Read Z component of acceleration
rled = 1.0f - x; //Negative logic is used as the LEDs
gled = 1.0f - y; //are of common anode type...
bled = 1.0f - z;
Thread::wait(100); //Time period is ~ 100 ms
}
}

Megjegyzések:
  1. Ha új projektet hozunk létre, importálnunk kell az  mbed-RTOS programkönyvtárat, a program elején be kell csatolnunk az "rtos.h" fejléc állományt és a main() függvény mellett létre kell hoznunk legalább egy további Thread objektumpéldányt.
  2. A FRDM-KL25Z kártya gyorsulásmérőjének használatához a fenti programba az mbed Components "Szenzorok" gyűjteményéből Emilio Monti MMA8541Q programkönyvtárát importáltuk. Hasonló néven másik programkönyvtár is elérhető, de annak alkalmazásprogramozói felülete más, emiatt nem kompatibilis a programunkkal!