Arduino-FreeRTOS for AVR (Part 2)#

บทความนี้ (ตอนที่ 2) นำเสนอตัวอย่างการเขียนโค้ดโดยใช้ FreeRTOS Library สำหรับบอร์ดไมโครคอนโทรลเลอร์ Arduino ที่ใช้ชิป 8-bit Atmel AVR (เช่น บอร์ด Uno | Nano | MEGA2560) และใช้ซอฟต์แวร์ Arduino IDE ในการเขียนโค้ด รวมถึงสาธิตการจำลองการทำงานด้วย Web-based Wokwi AVR Simulator

Keywords: Arduino, 8-bit AVR, RTOS, FreeRTOS, Wokwi Simulator


การสร้างทาสก์และการกำหนดระดับความสำคัญของทาสก์#

จากเนื้อหาในบทความก่อนหน้านี้ (ตอนที่ 1) เราได้เห็นตัวอย่างการเขียนโค้ด เพื่อสร้างทาสก์ด้วย FreeRTOS โดยใช้คำสั่ง xCreateTask() ไปบ้างแล้ว

BaseType_t xTaskCreate( TaskFunction_t pvTaskCode,
                        const char * const pcName,
                        configSTACK_DEPTH_TYPE usStackDepth,
                        void *pvParameters,
                        UBaseType_t uxPriority,
                        TaskHandle_t *pxCreatedTask );

 

ระดับความสำคัญของทาสก์ (Task Priority Level) เป็นหนึ่งอาร์กิวเมนต์สำหรับฟังก์ชันดังกล่าวเมื่อมีการสร้างทาสก์ขึ้นใหม่

โดยทั่วไปแล้วก็ ระดับความสำคัญของทาสก์ใน FreeRTOS จะมีค่ามากกว่า 0 และค่าต่ำสุดคือ 0 ซึ่งใช้กับ Idle Task (tskIDLE_PRIORITY = 0) และมีค่าสูงสุดคือ configMAX_PRIORITIES-1 (ค่าของ configMAX_PRIORITIES เท่ากับ 4 ในไฟล์ FreeRTOSConfig.h)

การทำงานของ FreeRTOS โดยทั่วไป จะอยู่ในโหมด Preemptive Scheduling (ดูได้จากการกำหนดค่า configUSE_PREEMPTION ให้เป็น 1 ในไฟล์ FreeRTOSConfig.h)

ทาสก์มีความสำคัญแตกต่างกันได้ และทาสก์ที่มีความสำคัญสูงกว่าและพร้อมที่จะทำงาน (READY) จะได้รับการจัดลำดับให้ทำงานได้ก่อนทาสก์ที่มีความสำคัญต่ำกว่า

ทาสก์ที่มีความสำคัญต่ำกว่าอาจจะถูกหยุดการทำงานชั่วคราว หรือถูกแทรกกลางคัน (Preemption) โดยทาสก์ที่มีความสำคัญมากกว่าและมีความจำเป็นที่ต้องรีบทำงานหรือตอบสนองต่อเหตุการณ์ได้ทันตามระยะเวลาที่กำหนดไว้

โดยทั่วไป RTOS จึงใช้วิธีกำหนดความสำคัญให้แก่ทาสก์ เพื่อให้ตอบสนองต่อเหตุการณ์หรือได้ทำงานทันเวลา ทาสก์ที่มีความสำคัญมากกว่า จะไม่ทำงานตลอดเวลา เพราะจะทำให้ทาสก์ที่มีความสำคัญน้อยกว่าไม่มีโอกาสได้ทำงาน

ในกรณีที่มีหลายทาสก์ที่มีระดับความสำคัญเท่ากัน ก็จะใช้วิธีการเลือกและจัดสรรเวลาการทำงานของซีพียูแบบ Round-Robin Scheduling (วนไปตามลำดับ)

การทำงานของ FreeRTOS นอกจากโหมด Preemptive Scheduling แล้วยังมีโหมด Co-operative Scheduling ให้เลือกใช้ได้เช่นกัน ในโหมดนี้ ในกรณีนี้ ทาสก์ทั้งหมดมีความสำคัญเท่ากัน และจะไม่มีการหยุดตามจังหวะของ RTOS Timer เพื่อเปลี่ยนให้ทาสก์อื่นได้ทำงาน ทาสก์ทำงานไปจนกว่าจะยอมปล่อย (Task Yield) ให้ทาสก์อื่นได้ทำงานต่อไป

 


ตัวอย่างที่ 1: การสร้างทาสก์เพื่อเปลี่ยนสถานะที่ขาเอาต์พุต#

ลองมาดูตัวอย่างการสร้างทาสก์ T0 และ T1 ตามลำดับ ทั้งสองทาสก์มีระดับความสำคัญเท่ากัน เพื่อทำให้วงจร LED แต่ละดวง ที่ต่อกับขาเอาต์พุต D5 และ D6 กระพริบได้ด้วยอัตราคงที่ แต่ใช้อัตราการกระพริบที่แตกต่างกัน

#include <Arduino_FreeRTOS.h> 

#define LED0_PIN  5 // D5 pin
#define LED1_PIN  6 // D6 pin

// task function prototypes
void task0( void *pvParameters ); 
void task1( void *pvParameters );

void sys_info() {
  String str;
  str = "CPU frequency: ";
  str += configCPU_CLOCK_HZ;
  str += " Hz";
  Serial.println( str.c_str() ); // 16 MHz

  str = "FreeRTOS Tick Rate: ";
  str += configTICK_RATE_HZ;
  str += " Hz";
  Serial.println( str.c_str() );  // 62 Hz

  str = "FreeRTOS Tick Period: ";
  str += portTICK_PERIOD_MS;
  str += " ms";
  Serial.println( str.c_str() );  // 1000/62 = 16
}

void setup() {
  Serial.begin( 115200 );
  sys_info();
  // Create two tasks of the same priority level
  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() {} 

// task enty function for T0
void task0( void *pvParameters ) { 
   boolean state = false;
   pinMode( LED0_PIN, OUTPUT );
   while (1) { 
      // toggle LED0 output
      digitalWrite( LED0_PIN, state = !state );
      // delay for one tick
      vTaskDelay( 1 /* ticks */ );
   }
}

// task entry function for T1
void task1( void *pvParameters ){ 
   boolean state = false;
   pinMode( LED1_PIN, OUTPUT );
   // delay for 100 ticks
   vTaskDelay( 100 /* ticks */ );
   while (1) { 
      // toggle LED1 output
      digitalWrite( LED1_PIN, state = !state );
      // delay for two ticks
      vTaskDelay( 2 /* ticks */ );
   }
}

ทาสก์ T0 จะทำให้เกิดการสลับสถานะลอจิกของเอาต์พุต D5 ทุก ๆ 1 tick × 16ms per OS tick = 16ms และทาสก์ T1 จะอัปเดทเอาต์พุต D6 ทุก ๆ 2 ticks × 16ms per OS tick = 32ms ดังนั้นสัญญาณเอาต์พุตที่ขา D5 จะมีความถี่สูงกว่าเป็นสองเท่าของสัญญาณเอาต์พุตที่ขา D6

สังเกตว่า ทาสก์ T1 (เอาต์พุตที่ขา D6) เมื่อเริ่มต้นทำงานจะถูกหน่วงเวลาไว้ก่อน นับไป 100 ticks ก่อนเข้าสู่ while(1){…} ในช่วงเวลานี้ จะไม่มีการเปลี่ยนแปลงที่ขาเอาต์พุต D6 แต่ในขณะที่ทาสก์ T0 เริ่มต้นทำงานแล้วเข้าสู่ while(1){…} โดยไม่มีการรอ และเกิดการสลับลอจิกที่ขาเอาต์พุต D5 ก่อนที่จะเห็นการเปลี่ยนเปลี่ยนแปลงที่ขา D6 ในช่วงเวลาถัดไป

 

มาดูตัวอย่างผลการจำลองการทำงานด้วย Wokwi Simulator และบันทึกการเปลี่ยนแปลงที่ขาเอาต์พุต ด้วย Wokwi Logic Analyzer

รูป: การจำลองการทำงานของโค้ดตัวอย่างด้วย Wokwi Simulator

รูป: คลื่นสัญญาณที่ได้บันทึกลงในไฟล์ .vcd แล้วแสดงผลด้วยโปรแกรม GTKWave

จากรูปคลื่นสัญญาณจะเห็นได้ว่า ที่ขาเอาต์พุต D6 ในช่วงแรกยังไม่มีการเปลี่ยนแปลงใด ๆ แต่ในขณะที่เอาต์พุต D5 มีการเปลี่ยนแปลงตั้งแต่เริ่มต้น

รูป: คลื่นสัญญาณและการวัดระยะเวลาด้วย Markers

จากรูปคลื่นสัญญาณจะเห็นได้ว่า ที่ขาเอาต์พุต D6 จะเริ่มมีการเปลี่ยนแปลงที่เอาต์พุตเมื่อเวลาผ่านไปประมาณ 1600ms (= 100 ticks × 16 ms per OS tick)

รูป: การวัดความกว้างของคาบสัญญาณที่ขาเอาต์พุต D5 ซึ่งได้เท่ากับ 32ms (= 2 ticks × 16ms per OS tick)

รูป: การวัดความกว้างของพัลส์ช่วงที่เป็น Low ที่ขาเอาต์พุต D5 ซึ่งได้ประมาณ 16ms (= 1 OS tick)

รูป: การวัดความกว้างของคาบสัญญาณที่ขาเอาต์พุต D6 ซึ่งได้เท่ากับ 64ms (= 4 OS ticks)

รูป: การวัดความกว้างของพัลส์ช่วงที่เป็น High ที่ขาเอาต์พุต D6 ซึ่งได้ประมาณ 32 ms (= 2 OS ticks) ช่อง 1 (สีเหลือง) คือ เอาต์พุตที่ขา D5 และช่อง 2 (สีฟ้าอมเขียว) คือ เอาต์พุตที่ขา D6

รูป: คลื่นสัญญาณที่ได้จากการทดลองโดยใช้ฮาร์ดแวร์จริงและวัดสัญญาณด้วยเครื่องออสซิลสโคป

ข้อสังเกต: ความกว้างของ 1 OS tick ที่ได้จากการวัดคลื่นสัญญาณเอาต์พุตจริง มีค่าประมาณ 17ms ซึ่งมากกว่า 16ms ตามที่ระบุไว้ใน Datasheet ของ AVR และจากผลการจำลองการทำงาน

 


ตัวอย่างที่ 2: การสร้างทาสก์ที่มีระดับความสำคัญแตกต่างกัน#

ตัวอย่างถัดไป เป็นการสร้างทาสก์ เช่น ตั้งชื่อว่า T0 และ T1 ทั้งสองทาสก์จะทำให้เกิดการเปลี่ยนสถานะที่เอาต์พุตสำหรับ LED สำหรับขา D5 และ D6 ตามลำดับ แต่มีความสำคัญแตกต่างกัน โดยกำหนดให้ค่าความสำคัญดังนี้ T0 ต่ำกว่า T1

#include <Arduino_FreeRTOS.h>

#define LED0_PIN  5 // D5
#define LED1_PIN  6 // D6

#define DELAY_TICKS  (6)

void 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() {} 

// task entry function for T0
void task0( void *pvParameters ) { 
  boolean state = false;
  pinMode( LED0_PIN, OUTPUT );
  while (1) { 
    // toggle LED0 output as fast as possible
    digitalWrite( LED0_PIN, state = !state );
  }
}

// task entry function for T1
void task1( void *pvParameters ){ 
  boolean state = false;
  pinMode( LED1_PIN, OUTPUT );
  vTaskDelay( DELAY_TICKS );
  while (1) {  
    // toggle LED1 output as fast as possible
    digitalWrite( LED1_PIN, state = !state );
  }
}

 

ถ้าสังเกตการทำงานของโค้ดตัวอย่างนี้ ในส่วนของ task1(){…} สำหรับทาสก์ T1 จะมีการทำคำสั่ง vTaskDelay( DELAY_TICKS ) ก่อนเข้าสู่ while(1){…} ดังนั้นทาสก์ T1 จะถูกหยุดการทำงานชั่วคราวเป็นเวลา DELAY_TICKS (= 6 OS Ticks) และอยู่ในสถานะ BLOCKED

ในขณะที่ทาสก์ T0 จะได้ทำงานทันที ในช่วงเวลานี้ สังเกตได้ว่า สัญญาณเอาต์พุตจะเกิดการเปลี่ยนแปลงที่ขา D5 (แสดงว่า ทาสก์ T0 กำลังทำงาน)

หลังจากนั้นเมื่อเวลาผ่านไป WAIT_TICKS ทาสก์ T1 ที่เคยอยู่ในสถานะ BLOCKED ก็เปลี่ยนมาอยู่ในสถานะ READY และเมื่อเกิด Tick Interrupt ในครั้งถัดไป จะมีการเลือกให้ทาสก์ T1 ได้ทำงาน เนื่องจากมีความสำคัญสูงกว่า T0 ที่กำลังทำงานอยู่ในสถานะ RUNNING ในขณะนั้น และ T0 ก็จะถูกหยุดไว้ชั่วคราว (แต่อยู่ในสถานะ READY)

ทาสก์ T1 เมื่อได้ทำงานแล้วจะทำต่อเนื่องไป จนกว่าจะเกิด Tick Interrupt เพื่อให้ Task Scheduler ได้ทำงานช่วงสั้น ๆ (หรืออาจหยุดชั่วคราวเป็นช่วงเวลาสั้น ๆ เนื่องจากมีเหตุการณ์ที่เป็นอินเทอร์รัพท์ของ Arduino เกิดขึ้นในระบบ) หลังจากนั้น ทาสก์ T1 ก็จะได้รับเลือกให้ทำงานอีกเป็นลำดับแรก หรือทำงานต่อไป เพราะมีความสำคัญสูงกว่าทาสก์ T0
ถ้าดูจากคลื่นสัญญาณเอาต์พุต ในช่วงเวลานี้จะเห็นได้ว่า มีการเปลี่ยนแปลงสถานะลอจิกที่ขา D6 (ทาสก์ T1 กำลังทำงาน) แต่ไม่เกิดขึ้นกับขา D5 (ทาสก์ T0 ไม่ได้ทำงาน)

 

รูป: คลื่นสัญญาณเอาต์พุตที่ได้จากการจำลองการทำงานด้วย Wokwi Simulator

จากรูปจะเห็นได้ว่า ในช่วงแรกมีการทำงานของ T0 และเกิดการเปลี่ยนแปลงที่ขา D5 เป็นระยะเวลาประมาณ 95.926ms หรือประมาณ 96ms (= 6 OS ticks × 16 ms per OS tick) แล้วจึงเปลี่ยนให้ทาสก์ T1 ได้ทำงาน

รูป: การสลับสถานะลอจิกที่ขาเอาต์พุต เกิดขึ้นทุก ๆ 4.438us (ไมโครวินาที) โดยประมาณ

รูป: แสดงช่วงเวลาที่มีการเปลี่ยนจากการทำงานของทาสก์ T0 ไปสู่การทำงานของทาสก์ T1 จับเวลาจากเหตุการณ์ที่ขาเอาต์พุต D5 ครั้งสุดท้าย และเหตุการณ์ที่ขาเอาต์พุต D6 ครั้งแรก ใช้เวลาประมาณ 38.375 us ในกรณีตัวอย่างนี้

 

ข้อสังเกต: ความกว้างของพัลส์ที่เกิดขึ้นกับสัญญาณเอาต์พุต จะมากหรือน้อยขึ้นอยู่กับการทำคำสั่งภายใน while(1){...} เช่น สำหรับ LED1_PIN ที่ใช้กับทาสก์ T1

  while (1) {  
    // toggle LED1 output as fast as possible
    digitalWrite( LED1_PIN, state = !state );
  }

ในโค้ดตัวอย่างนี้ ขาเอาต์พุต LED1_PIN ตรงกับขา D6 ของ Arduino Pin และตรงกับขา PD6 (พอร์ต PORTD บิตที่ 6) ของชิป ATmega328P หากลองเปลี่ยนรูปแบบการเขียนโค้ดใหม่ดังนี้ จะทำส่งผลอย่างไรบ้างต่อสัญญาณเอาต์พุตเมื่อจำลองการทำงาน

  while (1) {  
    // toggle LED1 / PD6 output as fast as possible
    PIND |= _BV(PD6);
  }

 

รูป: คลื่นสัญญาณที่ได้จากการจำลองการทำงานและเปรียบเทียบกันระหว่างเอาต์พุตที่ขา D5 และ D6 จะเห็นได้ว่า เอาต์พุตที่ขา D6 มีการเปลี่ยนแปลงในอัตราที่เร็วกว่าอย่างเห็นได้ชัด ซึ่งแสดงให้เห็นถึง Overhead จากการใช้คำสั่ง digitalWrite() ของ Arduino API

รูป: การวัดความกว้างของพัลส์ช่วงที่เป็น High ของเอาต์พุตที่ขา D6 และได้เท่ากับ 250ns หรือ 0.25us (= 4 CPU cycles per loop @ 16MHz)

 

ถ้าเพิ่มคำสั่งในลูปของฟังก์ชัน task1(){…} ของทาสก์ T1 เช่น vTaskDelay(0) หรือ taskYIELD() เพื่อให้เปลี่ยนการควบคุมกลับไปยัง Task Scheduler แต่ T1 จะถูกเลือกให้ทำงานอีกเช่นเดิม

void task1( void *pvParameters ){ // task function for T1
  boolean state = false;
  pinMode( LED1_PIN, OUTPUT );
  vTaskDelay( DELAY_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( DELAY_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 จึงมีโอกาสได้ทำงาน และสามารถสังเกตเห็นได้จากผลการจำลองการทำงานต่อไปนี้

รูป: คลื่นสัญญาณสำหรับเอาต์พุต D5 และ D6 ตามลำดับ ซึ่งจะเห็นได้ว่า ในช่วงแรกมีเพียงทาสก์ T0 เท่านั้นที่ทำงาน ในขณะที่ทาสก์ T1 หยุดรอเวลาในช่วงแรก จากนั้นจึงเริ่มทำงาน

รูป: คลื่นสัญญาณที่ขาเอาต์พุตที่แสดงให้เห็นว่า ทาสก์ T1 ทำให้เกิดการเปลี่ยนสถานะลอจิกที่ขา D6 ทุก ๆ 1 OS tick = 16ms ในขณะที่ทาสก์ T0 ซึ่งมีระดับความสำคัญน้อยกว่า แต่ก็ยังได้ทำงานเช่นกัน

รูป: แสดงช่วงเวลาที่มีการเปลี่ยนบริบทการทำงานระหว่างทาสก์ T0T1T0 ตามลำดับ จะเห็นได้ว่า ทาสก์ T0 ถูกขัดจังหวะโดยทาสก์ T1 ที่เข้ามาแทรกกลางเนื่องจากถึงเวลาที่กลับมาทำงาน และเมื่อ T1 ได้ทำงานแล้วในสถานะ RUNNING ก็เปลี่ยนเป็นสถานะ BLOCKED ส่งผลให้ T0 ได้กลับมาทำงานต่อ

 


ตัวอย่างที่ 3: การเปลี่ยนบริบทการทำงานของทาสก์#

จากตัวอย่างที่ 2 ถ้าให้ทั้งสองทาสก์ T0 และ T1 มีความสำคัญเท่ากัน (เช่น เท่ากับ tskIDLE_PRIORITY+1) และต่างก็พยายามทำให้เอาต์พุตที่เกี่ยวข้อง (ขา D5 และ D6) เปลี่ยนสถานะลอจิกให้เร็วที่สุด ไม่มีการหน่วงเวลาหรือยอมคืนการควบคุมการทำงานโดยซีพียู จะส่งผลอย่างไรต่อเอาต์พุต ?

#include <Arduino_FreeRTOS.h>

#define LED0_PIN  5 // D5
#define LED1_PIN  6 // D6

void 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+1, NULL );
  // Note the FreeRTOS task scheduler is started automatically.  
}

void loop() {} 

// task entry function for T0
void task0( void *pvParameters ) { 
  DDRD |= _BV(DDD5); // output direction for PD5
  while (1) { 
    PIND |= _BV(PD5); // toggle PD5 output
  }
}

// task entry function for T1
void task1( void *pvParameters ){ // task function for T1
  DDRD |= _BV(DDD6); // output direction for PD6
  while (1) { 
    PIND |= _BV(PD6); // toggle PD6 output
  }
}

 

หากทาสก์ T0 หรือ T1 ที่มีความสำคัญเท่ากัน เมื่อทาสก์ใดได้ทำงานจนครบหนึ่งช่วงเวลา (1 OS tick) จะต้องถูกสลับให้ทาสก์อื่นที่มีความสำคัญเท่ากันทำงานในลำดับถัดไป ในลักษณะ Round-Robin

เหตุการณ์ที่เกิดขึ้นเรียกว่า การเปลี่ยนบริบทการทำงานของทาสก์ (Task Context Switching) ซึ่งเกี่ยวข้องกับขั้นตอนหลักสองขั้นตอนที่สำคัญคือ การบันทึกบริบทของทาสก์ปัจจุบัน (Context Saving) และการนำบริบทของทาสก์ถัดไปกลับมา (Context Restoring) ทั้งสองขั้นตอนเกี่ยวข้องกับการทำคำสั่งของ FreeRTOS ดังนี้

รูป: คลื่นสัญญาณเอาต์พุตที่ได้จากการจำลองการทำงาน ซึ่งจะเห็นได้ว่า ทาสก์ T0 เริ่มทำงานก่อน แล้วจึงถูกสลับให้ทาสก์ T1 ทำงานบ้าง ใช้เวลาเท่า ๆ กัน สลับกันไป

รูป: แต่ละทาสก์จะได้เวลาทำงานประมาณ 16ms (= 1 OS tick) ก่อนสลับให้อีกทาสก์หนึ่งทำงาน

รูป: ช่วงเวลาที่มีการเปลี่ยนบริบทการทำงานจาก T0 ไปยัง T1 จับเวลาจากเหตุการณ์ที่ขาเอาต์พุต D5 ครั้งสุดท้าย และเหตุการณ์ที่ขาเอาต์พุต D6 ครั้งแรก ใช้เวลาประมาณ 20.5us

รูป: ความกว้างของพัลส์ช่วงที่เป็น High เท่ากับ 0.25us (250ns)

 


ตัวอย่างที่ 4: การใช้คำสั่ง taskYIELD()#

จากโค้ดตัวอย่างที่ 3 ถ้ามีการเพิ่มคำสั่ง taskYIELD() หรือ portYIELD() ในการทำงานของแต่ละทาสก์ หลังจากที่มีการเปลี่ยนสถานะของลอจิกที่ขาเอาต์พุตในแต่ละครั้ง จะส่งผลอย่างไรต่อการทำงานโดยรวม

#include <Arduino_FreeRTOS.h>

#define LED0_PIN  5 // D5
#define LED1_PIN  6 // D6

void 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+1, NULL );
  // Note the FreeRTOS task scheduler is started automatically.  
}

void loop() {}

// task entry function for T0
void task0( void *pvParameters ) { 
  DDRD |= _BV(DDD5); // output direction for PD5
  while (1) { 
    PIND |= _BV(PD5); // toggle PD5 output
    taskYIELD(); // <-----------
  }
}

// task entry function for T1
void task1( void *pvParameters ){ // task function for T1
  DDRD |= _BV(DDD6); // output direction for PD6
  while (1) { 
    PIND |= _BV(PD6); // toggle PD6 output
    taskYIELD(); // <-----------
  }
}

เนื่องจาก T0 และ T1 มีระดับความสำคัญเท่ากัน เมื่อทาสก์ใดได้ทำงาน สลับสถานะที่ขาเอาต์พุตหนึ่งครั้ง จะทำคำสั่ง taskYIELD() เป็นคำสั่งสุดท้าย ก่อนวนลูปในรอบใหม่ ดังนั้นทาสก์ จะเปลี่ยนจากสถานะ RUNNING ไปเป็น READY และให้อีกทาสก์หนึ่งทำงานต่อ สลับกันไประหว่างทั้งสองทาสก์

จากผลการจำลองการทำงานซึ่งเป็นไปตามรูปคลื่นสัญญาณต่อไปนี้ จะเห็นได้ว่า มีการเปลี่ยนสถานะที่ขาเอาต์พุต D5 และ D6 สลับกันไป มีช่วงเวลาห่างกันประมาณ 16.563us และความกว้างของพัลส์ช่วงที่เป็น High หรือ Low ประมาณ 33.688us

รูป: คลื่นสัญญาณเอาต์พุตที่ได้จากการจำลองการทำงานของโค้ด

 

ข้อสังเกต: ในตัวอย่างนี้ การทำงานของแต่ละทาสก์ ในหนึ่งรอบการวนซ้ำ มีเพียงคำสั่งเดียวที่ใช้สำหรับการสลับสถานะของลอจิกที่ขาเอาต์พุต แล้วตามด้วยคำสั่ง taskYIELD() หากทำได้เร็ว จะเกิดการสลับบริบทในการทำงานของทาสก์เร็วขึ้น ดังนั้นช่วงเวลานี้จะใช้เวลามากหรือน้อยก็ขึ้นอยู่กับความเร็วในการทำคำสั่งของซีพียูและผลจากการทำงานของ FreeRTOS Kernel

คำถาม: หากแทนที่คำสั่ง PIND |= _BV(PD6); ด้วย PIND = _BV(PD6); โค้ดตัวอย่างจะให้ผลการทำงานที่แตกต่างเดิมหรือไม่ เช่น ความกว้างของสัญญาณพัลส์ที่วัดได้ ?

 


กล่าวสรุป#

บทความนี้ได้นำเสนอตัวอย่างการเขียนโค้ดสาธิตการใช้งาน FreeRTOS สำหรับบอร์ด Arduino-AVR เช่น การสร้างทาสก์ การกำหนดความสำคัญของทาสก์ และศึกษาพฤติกรรมการทำงานของทาสก์ โดยใช้วิธีการจำลองการทำงาน และดูการเปลี่ยนแปลงที่ขา I/O

 


This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.

Created: 2021-12-27 | Last Updated: 2021-12-30