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. Az Mbed korai változatában a main() függvényt is beleértve összesen legfeljebb 6 programszálat hozhattunk létre. A gcc4mbed használatánál azonban már ez a korlát megszűnt, a létrehozható programszálak maximális számát lényegében csak a rendelkezésre álló RAM mérete és a szálak veremtárának mérete korlátozza. Az alapértelmezett 2 kB-os veremtármérettel például a KL25Z mikrovezérlő 16 kB-os RAM-jában valóban nem fér el 6-7 szálinál több, de kisebb (pl. 1 kB-os) veremtárak használatával elvileg több szál is indítható.

A gcc4mbed eszközlánc és az általa használt mbed rev142 (Mbed OS 5.4.5) könyvtár jelentős változást hozott a programszál-nyitás és indítás szintaxisában is. Ebben a tananyagban példaprogramjaink még a programszál-nyitás korábbi, egyszerűbb módját használják. Bár ez a megközelítés az újabb Mbed kiadásokban már elavultnak (deprecated) számít, a meglévő mintakódjaink a gcc4mbed-del lefordíthatók. Új fejesztésekhez természetesen már az új szintaxist célszerű használni, amit a fejezet végén külön bemutatunk.

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
}
}

Ne felejtsük el, hogy a projektünk Makefile állományában engedélyezni kell az RTOS használatát. Ehhez a Makefile-ban a MBED_OS_ENABLE makrót 1 értékkel kell definiálni. A thermometer függvényben pedig lebegőpontos kiíratást használ, ezért a NO_FLOAT_PRINTF makrót 0 értékkel kell definiálni.

A 09_rtos_threads projekt Makefile állománya:


PROJECT         := 09_rtos_threads
DEVICES         := KL25Z
GCC4MBED_DIR    := ../..
GCC4MBED_TYPE   := Release
MBED_OS_ENABLE  := 1
NEWLIB_NANO     := 1
NO_FLOAT_SCANF  := 1
NO_FLOAT_PRINTF := 0

include $(GCC4MBED_DIR)/build/gcc4mbed.mk
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!


Programszálak indítása az új szintaxis szerint (Mbed OS 5.4.5)

A gcc4mbed által használt mbed rev142 (Mbed OS 5.4.5) kiadásban a Thread osztály használata jelentősen átalakult. A korábbi konstrukcióval ellentétben a szál konfigurálását (erőforrások lefoglalása) és a szál tényleges elindítását különválasztották.

A különválasztás miatt a Thread konstruktor korábbi változatának első két paramétere (a függvénymutató és az argumentum-pointer) kiesett. A konstruktor most kizárólag a szál tulajdonságait (prioritás, veremtár mérete és helye) állítja be, míg a futtatandó függvényt és annak argumentumát az új start() tagfüggvénynek kell átadni.

1. Az új konstruktor szintaxisa:
Thread  mythread(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 paraméterei:

2. A start() tagfüggvény használata:
A szál létrehozása után a végrehajtás a start() metódussal indítható el. A Mbed OS C++ Callback sablont használ, így több meghívási formát is támogat:
// Sima C függvény indítása (argumentum nélkül vagy argumentummal)
osStatus start(Callback<void()> task);
osStatus start(Callback<void(void const *)> task, void *argument);

// Osztály tagfüggvényének indítása adott objektumpéldányon
template <typename T>
osStatus start(T *obj, void (T::*member)(void));
A start() függvény visszatérési értéke osStatus típusú kód (sikeres indítás esetén osOK).

Összehasonlító példa:

Összehasonlítás gyanánt megmutatjuk, hogy az 1. listán bemutatott programot hogyan kellene átírni az új szintaxis szabályai szerint

2. lista: A 09_rtos_threads/main.cpp program átírata Mbed 5.4.5 szintaxis szerint

#include "mbed.h"
#include "rtos.h"
#include "MMA8451Q.h"

MMA8451Q acc(PTE25,PTE24,0x3A);             //SDA, SCL, I2C address left 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)                            // nincs kötelető argumentum-átadás
{
    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)                      // nincs kötelező argumentum-átadás
{
    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;    // temperature in Celsius
        printf("voltage: %5.0f mV temp: %5.1f C\r\n",voltage,tempC);
        Thread::wait(2000);
    }
}


int main(void)
{
    // 1. Szál létrehozása magas prioritással és indítása a start() tagfüggvénnyel
    Thread thread2(osPriorityHigh);
    thread2.start(music);

    // 2. Szál létrehozása alacsony prioritással és indítása
    Thread thread3(osPriorityLow);
    thread3.start(thermometer);

    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
    }
}

Az Mbed 5.4.5 Thread osztályának tagfüggvényei

4. táblázat: Kiemeléssel jelöltük a gccmbed-ben használt Thread osztály új tagfüggvényeit.

Tagfüggvény Leírás
start(Callback task)
start(Callback task, void *arg)
Elindítja a szálat a megadott feladattal (C/C++ Callback) és az opcionális paraméterrel.
start(T *obj, T::*member)
start(T *obj, T::*member, *arg)
Elindítja a szálat a megadott C++ osztály objektumpéldányának megnevezett tagfüggvényét futtatva.
join() Blokkolja a hívó szál végrehajtását, amíg az adott szál futása be nem fejeződik.
terminate() Bezárja / megszünteti a szálat.
set_priority(priority) Beállítja a szál prioritását.
get_priority() Lekérdezi a szál aktuális prioritását.
signal_set(signals) Szignál biteket állít be a szál számára.
signal_clr(signals) Törli a megadott szignál biteket a szálon.
get_state() Lekérdezi a szál állapotát (pl. osThreadRunning, osThreadReady).
stack_size() Visszaadja a szál számára lefoglalt teljes veremtár méretét bájtokban.
free_stack() Visszaadja a szál még szabad veremtárkapacitását bájtokban.
used_stack() Visszaadja a szál által éppen használt veremtár méretét bájtokban.
max_stack() Visszaadja a szál által eddig elért maximális veremtár-kihasználtságot bájtokban.
get_id() Visszaadja a szál RTOS azonosítóját (_tid).

Az Mbed 5.4.5 Thread osztályának statikus tagfüggvényei

5. táblázat: Kiemeléssel jelöltük a gccmbed-ben használt Thread osztály új statikus tagfüggvényeit.

Statikus tagfüggvény Leírás
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
osThreadId gettid() Visszaadja az éppen futó szál RTOS azonosítóját.
attach_idle_hook(*fptr) Beállít egy függvénymutatót, ami az RTOS tétlen (Idle) szálának ciklusában fut le.
attach_terminate_hook(*fptr) Beállít egy globális visszahívó függvényt, amely bármelyik szál megszűnésekor meghívásra kerül, és paraméterként megkapja a feladat azonosítóját (id).