Programszálak (Threads)
A fejezet tartalma:- Programszálak létrehozása
- Programszálak futási állapotai
- A Thread objektumosztály tagfüggvényei
- Programszálak indítása az új szintaxis szerint (Mbed OS 5.4.5)
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 függvény végtelen ciklust tartalmaz, vagyis sohasem "térhet vissza".
- A függvény void típusú, azaz nincs visszatérési értéke.
- A függvény egyetlen paramétert vár, ami void const *arg típusú, azaz típus nélküli mutató.
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 |
Programszálak futási állapotai
Egy Thread objektum az alábbi állapotok valamelyikében lehet:- RUNNING: A programszál pillanatnyilag futó állapotban van. Egyszerre csak egyetlen programszál lehet ebbe az állapotban.
- READY: A programszál futásra kész állapotban van (egyidejűleg több programszál is lehet ebben az állapotban). Amint az éppen futó programszál abbahagyja a tevékenységét, az ütemező a READY állapotú programszálak közül a legmagasabb prioritásúnak adja át a vezérlést (az lesz az új RUNNING állapotú programszál).
- WAITING: A programszál valamilyen eseményre várakozik.
- INACTIVE: Nem létrehozott, vagy leállított programszál(ak).

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 |
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 |
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.- 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.
- 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.
- 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:
- FRDM-KL25Z kártya
- Piezo csipogó a D3 (PTA12) kivezetés és GND közé kötve
- MCP9700A analóg hőmérő az A0 (PTB0) bemenetre kötve
#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:- 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.
- 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:
- priority: A programszál futási prioritása (alapértelmezetten osPriorityNormal).
- stack_size: A szál saját veremtárának mérete bájtokban (alapértelmezetten OS_STACK_SIZE = 2048 bájt).
- stack_mem: Saját (előre lefoglalt) veremtár memóriacímére mutató pointer. Ha értéke NULL, a rendszer automatikusan foglalja le a veremtárat a dinamikus memóriából (heap).
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));
- task: A szálként futtatandó függvény, amely lehet hagyományos C függvény, C++11 lambda kifejezés vagy Callback objektum.
- argument: Opcionális, típus nélküli mutató (void*) a szál feladatának átadandó adatokra.
- obj / member: Osztálypéldányra mutató pointer és a hozzá tartozó tagfüggvény, amennyiben egy objektum metódusát kívánjuk külön szálként indítani.
Ö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). |