การทำงานแบบหลายทาส์กหรือมัลติทาส์กกิ้งและการกำหนดระดับความสำคัญ
การทำงานของ FreeRTOS โดยทั่วไปจะอยู่ในโหมด Preemptive Scheduling ทาส์กสามารถมีความสำคัญแตกต่างกันได้ ทาส์กที่มีความสำคัญสูงกว่า (Higher-priority Tasks) และพร้อมที่จะทำงาน (READY) จะได้รับการจัดลำดับให้ทำงานได้ก่อนทาส์กที่มีความสำคัญต่ำกว่า (Lower-priority Tasks)
ทาส์กที่มีความสำคัญต่ำกว่าอาจจะถูกหยุดการทำงานชั่วคราว หรือถูกแทรกกลางคัน (Preemption) โดยทาส์กที่มีความสำคัญมากกว่า ซึ่งมีความจำเป็นที่ต้องรีบทำงานหรือตอบสนองต่อเหตุการณ์ได้ทันตามระยะเวลาที่กำหนดไว้ เพราะถ้ารอให้ทาส์กอื่นได้ทำงานก่อน ทาส์กดังกล่าวอาจจะทำงานได้ไม่เสร็จทันเวลา โดยทั่วไป RTOS จึงใช้วิธีกำหนดความสำคัญให้แก่ทาส์กเพื่อให้ตอบสนองต่อเหตุการณ์หรือได้ทำงานทันเวลา
ทาส์กที่มีความสำคัญมากกว่า จะไม่ทำงานตลอดเวลา เพราะจะทำให้ทาส์กที่มีความสำคัญน้อยกว่าไม่มีโอกาสได้ทำงาน
การทำงานแบบ Preemptive Scheduling นอกจากจะต้องมีการกำหนดระดับความสำคัญของทาส์กแล้ว จะต้องมีการใช้งาน Hardware Timer เป็นตัวนับตามจังหวะของสัญญาณ Clock และเมื่อทำงานจะสร้างอินเทอร์รัพท์ให้เกิดขึ้น (Tick Interrupt) เช่น ทุก 1 มิลลิวินาที หรือ 1 kHz (เรียกว่า Tick Rate) และมีการอัปเดทตัวแปร Tick Counter (เพิ่มค่าตัวแปรครั้งละหนึ่ง) ถ้าใช้ไมโครคอนโทรลเลอร์ที่มีซีพียู 32 บิต ตระกูล ARM Cortex-M วงจรภายใน SysTick Timer ขนาด 24 บิต จะถูกใช้เป็นตัวนับตามจังหวะ
ในกรณีของ AVR เนื่องจากไม่มี SysTick Timer และถ้าไม่ต้องการใช้ Timer ขนาด 8 บิต หรือ 16 บิต (Timer0, Timer1) ซึ่งอาจจะถูกใช้งานไปแล้วสำหรับการทำงานของ Arduino ก็จะใช้ Watchdog Timer (WDT) เป็นตัวนับ เช่น ในกรณี Arduino port of FreeRTOS (ported by Richard Barry)
ในกรณีที่มีหลายทาส์กที่มีระดับความสำคัญเท่ากัน ก็จะใช้วิธีการเลือกและจัดสรรเวลาการทำงานของซีพียูแบบ Round-Robin Scheduling (วนไปตามลำดับ)
นอกจากโหมด Preemptive Scheduling แล้วยังมี โหมด Co-operative Scheduling ให้เลือกใช้ได้กับ FreeRTOS เช่นกัน ในโหมดนี้ ทาส์กจะมีความสำคัญเท่ากัน และจะไม่มีการหยุดตามจังหวะของ Timer เพื่อเปลี่ยนให้ทาส์กอื่นได้ทำงาน ทาส์กทำงานไปจนกว่าจะยอมปล่อย (Task Yield) ให้ทาส์กอื่นได้ทำงานต่อไป
ทบทวนการสร้างทาส์ก
การทำงานแบบมัลติทาส์กกิ้ง (Multi-Tasking) โดยใช้ RTOS จะต้องมีการแบ่งโปรแกรมออกเป็นงานย่อยหรือทาส์ก เมื่อถูกสร้างขึ้นมาแล้วจะทำงานต่อเนื่องไปตลอด และไม่มีการยกเลิกหรือถูกทำลายทิ้ง กรณีนี้เรียกว่า Static Task Creation และในอีกกรณีหนึ่ง เราสามารถสร้างทาส์กใหม่ เช่น เมื่อเกิดเหตุการณ์หรือเงื่อนไขบางอย่าง แล้วให้ทาส์กนั้นสามารถจบการทำงาน และถูกทำลายทิ้งได้ กรณีนี้เรียกว่า Dynamic Task Creation / Deletion
แต่โดยทั่วไปแล้ว เราก็มักจะสร้างทาส์กที่ทำงานได้ต่อเนื่องไม่จบการทำงาน (มีการหยุดการทำงานชั่วคราวได้) ดังนั้นภายในฟังก์ชันของทาส์ก จะมีการวนซ้ำด้วยประโยคคำสั่งอย่างเช่น while(1){…}
หรือ for(;;){…}
ทาส์กที่ถูกสร้างและจัดการโดย FreeRTOS kernel จะอยู่ในสถานะได้แก่ {READY, RUNNY, BLOCKED, SUSPENDED} เมื่อทาส์กถูกสร้างขึ้นแล้ว จะอยู่ในสถานะ READY และถ้าได้รับการเลือกจาก Task Scheduler ให้ทำงานในลำดับถัดไป ก็จะเปลี่ยนเป็นสถานะ RUNNING หรือเปลี่ยนเป็นสถานะอื่นได้หลังจากนั้น เช่น
- เปลี่ยนเป็น BLOCKED เช่น เมื่อทำสั่ง
vTaskDelay()
หรือรอเงื่อนไขเหตุการณ์ (Event) บางอย่างที่การประสานการทำงานระหว่างทาส์ก - เปลี่ยนเป็น READY เช่น เมื่อทำคำสั่ง
taskYIELD()
หรือ ถูกหยุดโดยการแทรกกลางคัน - เปลี่ยนเป็น SUSPENDED ทั้งนี้ก็ขึ้นอยู่กับการเรียกใช้คำสั่งของ FreeRTOS
ลองมาดูตัวอย่างการสร้างทาส์ก T0 และ T1 ที่มีระดับความสำคัญเท่ากัน เพื่อทำให้ LED ที่ขา D12 และ D13 กระพริบได้ด้วยอัตราคงที่ แต่ใช้อัตราการกระพริบที่แตกต่างกัน
#include <Arduino_FreeRTOS.h> // tested with Arduino Uno#define LED0_PIN 12
#define LED1_PIN 13// task function prototypes
void task0( void *pvParameters );
void task1( void *pvParameters );void setup() {
Serial.begin(115200);
Serial.println( configCPU_CLOCK_HZ ); // 16 MHz
Serial.println( configTICK_RATE_HZ ); // 62
Serial.println( portTICK_PERIOD_MS ); // 1000/62 = 16 xTaskCreate( task0, "T0", 128, NULL, tskIDLE_PRIORITY+1, NULL );
xTaskCreate( task1, "T1", 128, NULL, tskIDLE_PRIORITY+1, NULL );
// Note the FreeRTOS task scheduler is started automatically.
}void loop() {} // do nothingsvoid task0( void *pvParameters ) { // task function for T0
boolean state = false;
pinMode( LED0_PIN, OUTPUT );
while (1) { // toggle LED0 output
digitalWrite( LED0_PIN, state = !state );
vTaskDelay( 1 /* ticks */ );
}
}void task1( void *pvParameters ){ // task function for T1
boolean state = false;
pinMode( LED1_PIN, OUTPUT );
vTaskDelay( 100 /* ticks */ );
while (1) { // toggle LED1 output
digitalWrite( LED1_PIN, state = !state );
vTaskDelay( 2 /* ticks */ );
}
}
ทาส์ก T0 จะทำงานทุก ๆ 1 Tick และทาส์กจะทำงานทุก ๆ 2 Ticks ดังนั้นสัญญาณเอาต์พุตที่ขา D12 จะมีความถี่สูงกว่าเป็นสองเท่าของสัญญาณเอาต์พุตที่ขา D13 ที่ได้จากการทำงานของทาส์กที่ T1
สังเกตว่า ทาส์ก T1 เมื่อเริ่มต้นทำงานจะถูกหน่วงเวลาไว้ก่อน 100 Ticks ก่อนเข้าสู่ while(1){…}
ในช่วงเวลานี้ จะไม่มีการเปลี่ยนแปลงที่ขาเอาต์พุต D13 แต่ในขณะที่ทาส์ก T0 เริ่มต้นทำงานแล้วเข้าสู่ while(1){…}
และเกิดการสลับลอจิก (LED Toggle) ที่ขา D12 ก่อนที่จะเห็นการเปลี่ยนที่ขา D13
การสร้างทาสก์ที่มีระดับความสำคัญแตกต่างกัน
ลองมาสร้างทาส์ก T0 และ T1 ที่ทำให้เกิด LED Toggle แต่มีความสำคัญแตกต่างกัน โดยกำหนดให้ priority(T0) < priority(T1) ลองมาดูว่า ถ้าเขียนโค้ดแบบนี้จะเกิดอะไรขึ้น
#include <Arduino_FreeRTOS.h> // tested with Arduino Uno#define LED0_PIN 12
#define LED1_PIN 13void task0( void *pvParameters );
void task1( void *pvParameters );void setup() {
xTaskCreate( task0, "T0", 192, NULL, tskIDLE_PRIORITY+1, NULL );
xTaskCreate( task1, "T1", 192, NULL, tskIDLE_PRIORITY+2, NULL );
// Note the FreeRTOS task scheduler is started automatically.
}void loop() {} // do nothingsvoid task0( void *pvParameters ) { // task function for T0
boolean state = false;
pinMode( LED0_PIN, OUTPUT );
while (1) { // toggle LED0 output as fast as possible
digitalWrite( LED0_PIN, state = !state );
}
}void task1( void *pvParameters ){ // task function for T1
boolean state = false;
pinMode( LED1_PIN, OUTPUT );
vTaskDelay( 100 /* ticks */ );
while (1) { // toggle LED1 output as fast as possible
digitalWrite( LED1_PIN, state = !state );
}
}
ถ้าสังเกตการทำงานของโค้ดตัวอย่างนี้ ในส่วนของ task1(){…}
จะมีการทำคำสั่ง vTaskDelay(100)
ก่อนเข้าสู่ while(1){…}
ดังนั้นทาส์ก T1 จะถูกหยุดการทำงานชั่วคราวเป็นเวลา 100 Ticks และอยู่ในสถานะ Blocked ในขณะที่ทาส์ก T0 จะได้ทำงานทันที ในช่วงเวลานี้ สังเกตได้ว่า สัญญาณเอาต์พุตจะเกิด LED Toggle ที่ขา 12 (ทาส์ก T0 กำลังทำงาน) และได้ความถี่ที่สูง
หลังจากนั้นเมื่อเวลาผ่านไป 100 Ticks แล้วทาส์ก T1 ที่เคยอยู่ในสถานะ Blocked ก็เปลี่ยนมาอยู่ในสถานะ Ready และเมื่อเกิด Tick Interrupt จะมีการเลือกให้ทาส์ก T1 ได้ทำงานเนื่องจากมีความสำคัญสูงกว่า และ T0 ที่กำลังทำงานอยู่ ก็จะถูกหยุดไว้ชั่วคราว (แต่อยู่ในสถานะ Ready)
ทาส์ก T1 เมื่อได้ทำงานแล้วจะทำต่อเนื่องไป จนกว่าจะเกิด Tick Interrupt เพื่อให้ Task Scheduler ได้ทำงานช่วงสั้น ๆ หลังจากนั้นทาส์ก T1 จะได้รับเลือกให้ทำงานอีกเป็นลำดับแรก เพราะมีความสำคัญสูงกว่าทาส์ก T0 ถ้าดูจากคลื่นสัญญาณเอาต์พุต ในช่วงเวลานี้จะเห็นได้ว่า เกิด Output Toggle ที่ขา 13 (ทาส์ก T1 กำลังทำงาน) แต่ไม่เกิด Output Toggle ที่ขา 12 (ทาส์ก T0 ไม่ได้ทำงาน)
ถ้าเพิ่มคำสั่งในลูปของฟังก์ชัน task1(){…}
ของทาส์ก T1 เช่น vTaskDelay(0)
หรือ taskYIELD()
เพื่อให้เปลี่ยนการควบคุมกลับไปยัง Task Scheduler แต่ T1 จะถูกเลือกให้ทำงานอีก
void task1( void *pvParameters ){ // task function for T1
boolean state = false;
pinMode( LED1_PIN, OUTPUT );
vTaskDelay( 100 /* ticks */ );
while (1) { // toggle LED1 output as fast as possible
digitalWrite( LED1_PIN, state = !state );
vTaskDelay(0); // <== task delay for 0 tick
taskYIELD(); // <== return control to the task scheduler
}
}
แต่ถ้าเขียนคำสั่ง เช่น vTaskDelay(1)
เพื่อให้เกิดการหน่วงเวลาอย่างน้อย 1 Tick สำหรับการทำงานของ T1 จะได้ผลการทำงานที่แตกต่างจากเดิม
void task1( void *pvParameters ){ // task function for T1
boolean state = false;
pinMode( LED1_PIN, OUTPUT );
vTaskDelay( 100 /* ticks */ );
while (1) { // toggle LED1 output with one-tick delay
digitalWrite( LED1_PIN, state = !state );
vTaskDelay(1); // <== task delay for 1 tick
}
}
ข้อสังเกต: ทาส์ก T0 จะพยายามทำงานตลอดเวลา ไม่มีการยอมคืนการควบคุม แต่เนื่องจากมีความสำคัญต่ำกว่า T1 และทุก ๆ ครั้งที่เกิด Tick Interrupt การทำงานของทาส์ก T0 จะถูกแทรกกลางคัน (preempted) โดยการทำงานของ T1 และเมื่อ T1 ทำไปจนถึงคำสั่ง vTaskDelay(1
) จะต้องหยุดทำงานชั่วคราว รออยู่ในสถานะ Blocked เป็นเวลา 1 Tick จึงจะได้ทำงานอีกครั้ง ในระหว่างนั้น ทาส์ก T0 จึงมีโอกาสได้ทำงาน
การแสดงสถานะของทาส์กในระหว่างทำงาน
ตัวอย่างถัดไปลองมาดูวิธีตรวจสอบสถานะของทาส์ก ในตัวอย่างนี้จะสร้างทาส์ก 4 ชุด แต่ละทาส์กทำหน้าที่สร้างสัญญาณแบบพัลส์ HIGH เป็นเอาต์พุต และเว้นช่วงเวลาประมาณ 500 มิลลิวินาที
เมื่อทาส์กใดได้ทำงาน จะมีการตรวจสอบสถานะการทำงานของทาส์กอื่นด้วย โดยใช้ฟังก์ชันชื่อ showTasksInfo()
และการทำงานของฟังก์ชันนี้ จะต้องเรียกใช้ฟังก์ชัน vTaskGetInfo()
ของ FreeRTOS
ข้อสังเกต: ถ้าจะเรียกใช้ฟังก์ชัน vTaskGetInfo()
จะต้องประกาศเพิ่มในไฟล์ FreeRTOSConfig.h
ดังนี้ (แต่ถ้าไม่ใช้ ก็ให้ปิดการใช้งานส่วนนี้)
#define configUSE_TRACE_FACILITY 1
#define INCLUDE_eTaskGetState 1
เมื่อโค้ดนี้ทำงาน จะส่งข้อความผ่าน Serial ไปยังคอมพิวเตอร์ ซึ่งจะแสดงให้เห็นสถานะการทำงานของทาส์กทั้ง 4 ในขณะนั้น
#include <Arduino_FreeRTOS.h> // tested on Uno
#include <task.h>#define NUM_LEDS (4)const int LED_PINS[ NUM_LEDS ] = { 10, 11,12, 13 };
const char *TASK_NAMES[ NUM_LEDS ] = { "T0", "T1", "T2", "T3" };
const char *STATE_NAMES[] = {
"Run", "Ready", "Blocked", "Suspended" };TaskHandle_t taskHandles[ NUM_LEDS ];void task( void *pvParameters ); // task function prototypevoid setup() {
Serial.begin(115200); int priority = (tskIDLE_PRIORITY + 2);
for ( int id=0; id < NUM_LEDS; id++ ) {
xTaskCreate(
task, TASK_NAMES[ id ], 128, (void *)id,
priority, &taskHandles[id] );
}
// Note the task scheduler is started automatically.
}void loop() {} // do nothingsvoid showTasksInfo() {
char sbuf[40];
TaskStatus_t taskStatus;
uint32_t ticks = xTaskGetTickCount(); Serial.println("-----------");
for ( int i=0; i < NUM_LEDS; i++ ) {
TaskHandle_t task = taskHandles[i];
vTaskGetInfo( task, &taskStatus, pdTRUE, eInvalid );
sprintf( sbuf, "%s %8s [%4d]",
pcTaskGetName( task ),
STATE_NAMES[ taskStatus.eCurrentState ], ticks );
Serial.println( sbuf );
}
}void task( void *pvParameters ) {
int id = (int)pvParameters;
pinMode( LED_PINS[ id ], OUTPUT );
digitalWrite( LED_PINS[id], LOW );
while(1) {
int p = LED_PINS[id];
digitalWrite( p, HIGH );
portENTER_CRITICAL();
showTasksInfo();
portEXIT_CRITICAL();
digitalWrite( p, LOW);
vTaskDelay( pdMS_TO_TICKS( 500 ) );
}
}
จากข้อความที่ได้จากการทำงานโดยใช้บอร์ด Arduino จะเห็นได้ว่า ทาส์ก T0 ได้เริ่มทำงานก่อนและอยู่ในสถานะ RUN ในขณะที่ทาส์กอื่น T1–T3 อยู่ในสถานะ READY หลังจากถูกสร้างขึ้นมาด้วยคำสั่ง xTaskCreate()
เมื่อทาส์ก T0 ทำงานไปจนถึงทำคำสั่ง vTaskDelay()
จะเปลี่ยนไปอยู่ในสถานะ BLOCKED จากนั้น T1 จะได้ทำงานในลำดับถัดไป โดยเปลี่ยนเป็นสถานะ RUN
เมื่อทำงานมาถึงคำสั่ง vTaskDelay()
ทาส์ก T1 จะหยุดการทำงานชั่วคราว โดยเปลี่ยนไปอยู่ในสถานะ BLOCKED และให้ T2 ได้ทำงานเป็นลำดับถัดไป
เมื่อทาส์ก T2 หยุดทำงานเพราะได้ทำคำสั่ง vTaskDelay()
ส่งผลให้ทาส์ก T3 จะได้ทำงานเป็นลำดับถัดไป เปลี่ยนเป็นสถานะ RUN แล้วจึงเปลี่ยนเป็นสถานะ BLOCKED หลังจากนั้น
เมื่อถึงตอนนี้ทาส์ก T0–T3 จะอยู่ในสถานะ BLOCKED ทั้งหมด (แต่ยังมีทาส์กของระบบที่จะได้ทำงานนั้นคือ Idle Task)
เมื่อเวลาผ่านไปสักระยะหนึ่ง ประมาณ 500 มิลลิวินาที หรือ 31 Ticks ทาส์ก T0 จะเปลี่ยนเป็นสถานะ READY เพราะการหน่วงเวลาสำหรับ T0 ได้ผ่านไปครบแล้ว และได้รับเลือกให้ทำงานในสถานะ RUN เป็นลำดับต่อไป และการทำงานในลักษณะนี้จะเกิดวนซ้ำไปเรื่อย ๆ
Creative Commons, Attribution-Non Commercial-Share Alike 4.0 International (CC BY-NC-SA 4.0)