Arduino-FreeRTOS for AVR (Part 4)#

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

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


การใช้ฟังก์ชันเพื่อควบคุมการทำงานของทาสก์#

จากบทความในตอนที่แล้ว (ตอนที่ 1 | 2 | 3) เราได้เห็นตัวอย่างการเขียนโค้ด Arduino Sketch สำหรับ Arduino Boards (AVR MCU) ใช้งานร่วมกับไลบรารี Arduino-FreeRTOS Library และได้ลองใช้ Web-based Wokwi Simulator และ Web-based GDB-AVR debugger เพื่อจำลองการทำงานของโค้ดและตรวจสอบการทำงาน

บทความนี้นำเสนอเนื้อหาเกี่ยวกับการควบคุมการทำงานของทาสก์ (Task Control) เช่น การหยุดชั่วคราวหรือให้ทำงานต่อ (Task Suspend & Resume) การเปลี่ยนระดับความสำคัญของทาสก์ เป็นต้น

ลองมาดูตัวอย่างฟังก์ชันพื้นฐานของ FreeRTOS ที่เกี่ยวข้องกับการทำงานของทาสก์ (Task Control Functions)

  • xTaskCreate(...) สร้างทาสก์ใหม่
  • vTaskDelete(...) ยกเลิกการใช้งานทาสก์ในระหว่างการทำงานของโปรแกรม
  • vTaskDelay(...) ให้ทาสก์หยุดรอเวลาตามระยะเวลาที่กำหนดไว้
  • vTaskDelayUntil(...) หรือ xTaskDelayUntil(...) ให้ทาสก์หยุดรอจนถึงเวลาที่กำหนดไว้
  • vTaskPrioritySet(...) กำหนดระดับความสำคัญของทาสก์
  • vTaskPriorityGet(...) อ่านค่าระดับความสำคัญของทาสก์ในระหว่างการทำงานของโปรแกรม
  • vTaskSuspend(...) หยุดการทำงานของทาสก์ชั่วคราว
  • vTaskResume(...) ให้ทาสก์ทำงานต่อ xTaskResumeFromISR() หรือให้ทาสก์ทำงานต่อจากการทำงานของ ISR (Interrupt Service Routine) ที่กำลังทำงานอยู่ในขณะนั้น

หากสังเกตการตั้งชื่อของฟังก์ชันเหล่านี้ ตัวอักษรแรกของชื่อฟังก์ชัน จะหมายถึง ชนิดของข้อมูลสำหรับค่าของฟังก์ชัน (Return Type) เช่น

  • ถ้าขึ้นต้นด้วย x หมายถึง ให้ค่าเป็น Non-standard Integer Types เช่น ชนิดข้อมูล BaseType_t และ TickType_t ที่ได้มีการประกาศไว้ใช้สำหรับ FreeRTOS
  • ถ้าขึ้นต้นด้วย v หมายถึง void ไม่ให้ค่ากลับคืนมาเมื่อจบการทำงานของฟังก์ชัน

แนะนำให้ศึกษารายละเอียดเพิ่มเติมได้จาก: "FreeRTOS Naming Convention"

ข้อสังเกต: ในการใช้งานคำสั่งหรือฟังก์ชันของ FreeRTOS API จะต้องมีการตรวจสอบดูก่อนว่า สามารถใช้งานได้หรือไม่สำหรับ FreeRTOS Port (ในกรณีนี้คือ FreeRTOS สำหรับ AVR) โดยดูได้จากไฟล์สำหรับการปรับและตั้งค่าใช้งาน (FreeRTOS Customization) ที่เรียกว่า FreeRTOSConfig.h

ตัวอย่างการกำหนดค่าสำหรับ Arduino-FreeRTOS Library (FreeRTOS Kernel V10.4.6) มีตัวอย่างดังนี้ (ตัดมาบางส่วน)

#define INCLUDE_vTaskPrioritySet                1
#define INCLUDE_uxTaskPriorityGet               1
#define INCLUDE_vTaskDelete                     1
#define INCLUDE_vTaskCleanUpResources           1
#define INCLUDE_vTaskSuspend                    1
#define INCLUDE_vResumeFromISR                  1
#define INCLUDE_xTaskDelayUntil                 1
#define INCLUDE_vTaskDelay                      1
#define INCLUDE_xTaskGetSchedulerState          0
#define INCLUDE_xTaskGetIdleTaskHandle          0
#define INCLUDE_xTaskGetCurrentTaskHandle       0
#define INCLUDE_uxTaskGetStackHighWaterMark     1

จากตัวอย่างสัญลักษณ์ (Macros) ที่มีชื่อขึ้นต้นด้วย INCLUDE_ หากมีค่าเป็น 1 หมายความว่า มีฟังก์ชันตามชื่อที่เกี่ยวข้องให้เรียกใช้งานได้ใน FreeRTOS เช่น INCLUDE_vTaskPrioritySet มีค่าเป็น 1 ดังนั้นในการเขียนโค้ด ผู้ใช้สามารถทำคำสั่ง vTaskPrioritySet(...) ได้

ลองมาพิจารณาตัวอย่างการกำหนดค่าใช้งานสำหรับ FreeRTOS-AVR ต่อไปนี้

#define configMAX_PRIORITIES            4
#define configUSE_PREEMPTION            1
#define configUSE_TIME_SLICING          1
#define configUSE_IDLE_HOOK             1
#define configMINIMAL_STACK_SIZE        ( 192 )

ซึ่งมีคำอธิบายดังนี้

  • ระดับความสำคัญของทาสก์ (ต่ำสุด-สูงสุด): 0 ถึง 3 หรือ (configMAX_PRIORITIES-1)
  • ใช้วิธีจัดลำดับการทำงานของทาสก์แบบ Preemptive Scheduling (ไม่ได้เลือกใช้วิธี Co-operative Scheduling)
  • มีการแบ่งเวลาให้ทาสก์ที่มีระดับความสำคัญเท่ากันได้ทำงานสลับกันไป (Time Slicing)
  • มีการเปิดใช้งานฟังก์ชันที่เรียกว่า Idle Hook Function — เมื่อไม่มีทาสก์ใดอยู่ในสถานะพร้อมจะทำงาน ทาสก์ของระบบที่เรียกว่า Idle Task ซึ่งเป็นทาสก์ที่ถูกสร้างขึ้นมาในระบบโดยอัตโนมัติ ก็จะได้ทำงาน แล้วไปเรียกฟังก์ชันดังกล่าวที่กำหนดโดยผู้ใช้
  • มีขนาดของ Task Stack อย่างน้อยที่สุด เท่ากับ 192

 


ตัวอย่างที่ 1: Suspend & Resume Task#

โค้ดตัวอย่างแรกสาธิตการเขียนเพื่อสร้างทาสก์ Task1 ในฟังก์ชัน setup() และมีการสร้างฟังก์ชัน task1() สำหรับการทำงานของทาสก์ดังกล่าว ฟังก์ชันนี้จะทำให้เกิดการสลับสถานะลอจิกที่ขาเอาต์พุต Arduino D12 pin ทุก ๆ 500 มิลลิวินาที แต่เมื่อสลับสถานะลอจิกครบ 10 ครั้ง แล้วจะหยุดการทำงานของทาสก์ดังกล่าว ด้วยการทำคำสั่ง vTaskSuspend()

เมื่อทาสก์ Task1 หยุดการทำงานชั่วคราว อยู่ในสถานะ Suspended และก็ไม่มีทาสก์อื่นใดในระบบที่พร้อมจะทำงานอีกก ดังนั้น FreeRTOS Scheduler หรือ ตัวจัดลำดับการทำงานของทาสก์ในระบบ จะเปลี่ยนให้ทาสก์ที่เรียกว่า Idle Task ได้ทำงาน จนกว่าจะมีทาสก์อื่นที่มีระดับความสำคัญสูงกว่าพร้อมที่จะทำงาน

สำหรับ Arduino-FreeRTOS port ได้มีการกำหนดให้เปิดใช้งาน Idle Hook Function และเมื่อ Idle Task ทำงาน ก็จะไปเรียกฟังก์ชัน loop() ของ Arduino Sketch ให้ทำงาน

หากดูเนื้อหาในไฟล์ FreeRTOSConfig.h มีเปิดใช้งาน FreeRTOS Idle Hook Function และจะฟังก์ชัน loop(){...} ของ Arduino Sketch จะถูกเรียกให้ทำงาน เมื่อไม่มีทาสก์ใดอยู่ในสถานะพร้อมที่จะทำงาน ยกเว้นทาสก์ที่เป็น Idle Task ของ FreeRTOS

#define configUSE_IDLE_HOOK             1
#define configIDLE_SHOULD_YIELD         1

ในตัวอย่างนี้ ฟังก์ชัน loop() จะทำงานซ้ำหลายรอบ และมีการนับจำนวนการเรียกฟังก์ชันนี้ หากได้จำนวนครั้งตามที่กำหนดไว้โดย IDLE_CNT_MAX จะมีการทำให้ทาสก์ Task1 ได้กลับมาทำงานอีกครั้ง กล่าวคือ เปลี่ยนจากสถานะ Suspended มาเป็น Ready และจะได้ทำงานในลำดับถัดไป เพราะเป็นทาสก์ที่มีระดับความสำคัญสูงสุดในตัวอย่างนี้

#include <Arduino_FreeRTOS.h> // include the FreeRTOS library
#include <task.h>

#define LED1_PIN   (13)  // Arduino D13 pin (for 'Idle Task')
#define LED2_PIN   (12)  // Arduino D12 pin (for 'Task1`)

TaskHandle_t task1_handle = NULL;

void setup() {
  pinMode( LED1_PIN, OUTPUT );
  Serial.begin( 115200 );

  xTaskCreate(
    task1,                // task-entry function for "Task1" 
    "Task1",              // task name: "Task1"
    128,                  // task stack (allocated on heap)
    (void*)LED2_PIN,      // task param (LED pin)
    tskIDLE_PRIORITY+1,   // task priority 1
    &task1_handle );      // task handle for Task1

  Serial.println( "FreeRTOS started..." );
}

// Note that configUSE_IDLE_HOOK is set to 1.
// In Arduino the loop() function is hooked to the FreeRTOS Idle Task
// and will be called whenever the task scheduler runs its Idle Task.

#define IDLE_CNT_MAX  (100000UL)
static uint32_t idle_cnt = 0;
String str; 

void loop() { // called by the Idle Task
  if ( ++idle_cnt < IDLE_CNT_MAX ) {
    digitalWrite( LED1_PIN, HIGH );
  } else {
    idle_cnt = 0; // reset the counter for idle-hook function calls
    digitalWrite( LED1_PIN, LOW ); // toggle the LED1
    Serial.println( F("Idle-hook function resumes Task1..") );
    str = "Ticks: ";
    str += xTaskGetTickCount();
    Serial.println( str.c_str() ); // show current FreeRTOS ticks
    vTaskResume( task1_handle );   // resume Task1
  }
}

// task-entry function for Task1
void task1( void* pvParameters ) { 
  int blink_cnt = 0;           // the number of LED blinks
  int pin = (int)pvParameters; // use the task parameter for LED pin
  pinMode( pin, OUTPUT );      // set output direction for LED
  while (1) {
     digitalWrite( pin, !digitalRead(pin) ); // toggle LED
     delay( 200 );  // delay for 200 msec
     if ( ++blink_cnt >= 10 ) { // blink the LED up to 10 times 
       blink_cnt = 0;           // reset the LED-blink counter
       vTaskSuspend( NULL );    // let Task1 suspend itself.
     }
  }
}

 

คำถาม: เราจะสังเกตพฤติกรรมการทำงานของโค้ดตัวอย่างนี้ได้อย่างไร ?

เราสามารถใช้ Wokwi AVR Simulator จำลองการทำงานของโค้ดตัวอย่าง และดูสัญญาณการเปลี่ยนแปลงที่ขาเอาต์พุต LED1_PIN และ LED2_PIN (ตรงกับขา D13 และ D12 ตามลำดับ) ในแต่ละช่วงเวลาของการทำงาน และดูข้อความเอาต์พุตผ่านทาง Serial เป็นต้น

รูป: การจำลองการทำงานด้วย Wokwi Simulator

รูป: การใช้โปรแกรม GTKWave แสดงรูปคลื่นสัญญาณที่ขาเอาต์พุต D13 และ D12 จากข้อมูลในไฟล์ .vcd

จากรูปจะเห็นได้ว่า มีสัญญาณพัลส์ที่ขา D13 ซึ่งเกิดจากการทำงานของทาสก์ Task1 มีความกว้างช่วง High 200 มิลลิวินาที โดยประมาณ และเกิดพัลส์ 5 ครั้งตามลำดับ (มีการเปลี่ยนสถานะลอจิก 10 ครั้ง) ก่อนที่จะหยุดการทำงานชั่วคราว (Task Suspended)

ถัดจากนั้น จะเกิดสัญญาณพัลส์ที่ขา D12 ซึ่งเป็นผลมาจากการทำงานของ Idle Task ที่ทำคำสั่งต่าง ๆ ในฟังก์ชัน loop() ไปจนกว่าจะมีการทำให้ Task1 กลับมาอยู่ในสถานะพร้อมทำงานอีกครั้ง (Task Resumed)

 


ตัวอย่างที่ 2: Resume Task from ISR#

ตัวอย่างที่สองนี้มีหลักการทำงานคล้ายกับตัวอย่างแรก แต่มีความแตกต่างตรงที่การทำให้ทาสก์ Task1 ได้กลับมาทำงานต่อไปหลังจากที่หยุดการทำงานชั่วคราว จะเกิดขึ้นเมื่อมีการกดปุ่มสำหรับสัญญาณอินพุตที่ขา D2 (ทำงานแบบ Active-Low)

เมื่อฟังก์ชัน ISR สำหรับเหตุการณ์ดังกล่าวถูกเรียกให้ทำงาน (ทุกครั้งที่เกิดขอบขาลง หรือ Falling-Edge และมีระยะห่างจากเหตุการณ์ครั้งก่อนอย่างน้อย 500 มิลลิวินาที) ก็จะมีการทำคำสั่ง xTaskResumeFromISR() เพื่อให้ทาสก์ Task1 เปลี่ยนสถานะจาก Suspended ไปอยู่สถานะ Ready และทำคำสั่ง portYIELD_FROM_ISR() เพื่อให้เกิดการเปลี่ยนบริบทการทำงานให้ทาสก์อื่นที่พร้อมจะทำงาน ซึ่งก็คือ ทาสก์ Task1 ในกรณีตัวอย่างนี้

#include <Arduino_FreeRTOS.h> // include the FreeRTOS library
#include <task.h>

#define LED1_PIN   (13)  // Arduino D13 pin (for 'Idle Task')
#define LED2_PIN   (12)  // Arduino D12 pin (for 'Task1`)
#define BTN_PIN    (2)   // Arduino D2 pin  (for button input)

TaskHandle_t task1_handle = NULL;

volatile uint32_t ts;  // timestamp

void btn_isr() { // ISR for push button input
  BaseType_t status = pdFALSE;
  uint32_t now = millis();
  if ( now - ts >= 500 ) {
     // toggle LED1 pin
     digitalWrite( LED1_PIN, !digitalRead( LED1_PIN ) );
     status = xTaskResumeFromISR( task1_handle ); // resume Task1
  }
  ts = now; // update timestamp
  if ( status == pdTRUE ) {
     portYIELD_FROM_ISR( ); // yield CPU from ISR
  }
}

void setup() {
  pinMode( LED1_PIN, OUTPUT );
  pinMode( BTN_PIN, INPUT_PULLUP );
  Serial.begin( 115200 );
  // enable external interrupt 0 (EINT0) for button input 
  attachInterrupt( digitalPinToInterrupt(BTN_PIN), btn_isr, FALLING );
  ts = millis(); // save current time (in msec)

  xTaskCreate(
    task1,              // task-entry function for "Task1" 
    "Task1",            // task name: "Task1"
    128,                // task stack (allocated on heap)
    (void*)LED2_PIN,    // task param (LED pin)
    tskIDLE_PRIORITY+1, // task priority 1
    &task1_handle );    // task handle for Task1

  Serial.println( "FreeRTOS started..." );
}

void loop() { }

// task-entry function for Task1
void task1( void* pvParameters ) { 
  int blink_cnt = 0;           // the number of LED blinks
  int pin = (int)pvParameters; // the task parameter for LED pin
  pinMode( pin, OUTPUT );      // set output direction for LED
  while (1) {
     digitalWrite( pin, !digitalRead(pin) ); // toggle LED
     delay( 200 );  // delay for 200 msec
     if ( ++blink_cnt >= 10 ) { // blink the LED up to 10 times 
       blink_cnt = 0;           // reset the LED-blink counter
       vTaskSuspend( NULL );    // let Task1 suspend itself
     }
  }
}

รูป: การจำลองการทำงานสำหรับตัวอย่างที่ 2

รูป: แสดงรูปคลื่นสัญญาณที่บันทึกได้จากขา D13, D12, D2 ตามลำดับ

ช่วงที่สัญญาณ D2 เป็น Low เกิดจากการกดปุ่มในขณะนั้น และเมื่อISR ทำงาน จะทำให้ D12 เปลี่ยนสถานะลอจิกหนึ่งครั้ง และจะทำให้ทาสก์ Task1 เปลี่ยนจาก Suspended เป็น Ready และ Running ตามลำดับ และจะเห็นว่า มีสัญญาณพัลส์ที่ขา D13 เกิดขึ้นตามมา

 


ตัวอย่างที่ 3: Change Task Priority at Runtime#

ตัวอย่างถัดไปสาธิตการเปลี่ยนระดับความสำคัญของทาสก์ในระหว่างการทำงานของโปรแกรม โดยการสร้างทาสก์ Task1 และ Task2 ที่มีระดับความสำคัญในตอนเริ่มต้นเท่ากัน และได้ฟังก์ชันชื่อ task() เพื่อแสดงพฤติกรรมการทำงานของทาสก์ กล่าวคือ ให้สลับสถานะลอจิกที่ขาเอาต์พุตที่เกี่ยวข้อง แต่ละทาสก์จะใช้ขาเอาต์พุตที่ต่างกัน (D11 และ D12 ตามลำดับ)

เมื่อทาสก์ได้ทำงาน จะเปลี่ยนระดับความสำคัญของทาสก์ขึ้นแล้วสลับสถานะลอจิกที่ขาเอาต์พุตทั้งหมด 10 ครั้ง และเว้นระยะเวลาในแต่ละครั้ง ประมาณ 10 มิลลิวินาที โดยทำคำสั่ง delay() ซึ่งทำงานในลักษณะ wait-busy ก่อนเปลี่ยนระดับความสำคัญของทาสก์กลับไปเหมือนเดิม ในช่วงเวลาดังกล่าว จะเกิดสัญญาณพัลส์ จำนวน 5 ครั้ง (ช่วงที่เป็น High กว้างประมาณ 10 มิลลิวินาที ตามด้วยช่วงที่ Low กว้าง 10 มิลลิวินาที)

#include <Arduino_FreeRTOS.h> // include the FreeRTOS library
#include <task.h>

#define LED1_PIN   (11)  // Arduino D11 pin 
#define LED2_PIN   (12)  // Arduino D12 pin

UBaseType_t task_priority = (tskIDLE_PRIORITY+1);

void setup() {
  pinMode( LED1_PIN, OUTPUT );
  pinMode( LED2_PIN, OUTPUT );
  Serial.begin( 115200 );

  xTaskCreate(
    task,              // task-entry function for "Task1" 
    "Task1",           // task name: "Task1"
    128,               // task stack (allocated on heap)
    (void*)LED1_PIN,   // task param (LED pin)
    task_priority,     // task priority
    NULL );            // task handle for Task1

  xTaskCreate(
    task,              // task-entry function for "Task2" 
    "Task2",           // task name: "Task2"
    128,               // task stack (allocated on heap)
    (void*)LED2_PIN,   // task param (LED pin)
    task_priority,     // task priority 
    NULL );            // task handle for Task2

  Serial.println( "FreeRTOS started..." );
}

void loop() { }

#define RAISE_PRIORITY   (1)

// task-entry function
void task( void* pvParameters ) { 
  int pin = (int)pvParameters; // the task parameter for LED pin
  pinMode( pin, OUTPUT );      // set output direction for LED
  while (1) {
    UBaseType_t priority = uxTaskPriorityGet( NULL );
#ifdef RAISE_PRIORITY
    vTaskPrioritySet( NULL, priority+1 ); // raise priority
#endif 
    for ( int i=0; i < 10; i++ ) {
       digitalWrite( pin, !digitalRead(pin) ); // toggle LED
       delay( 10 ); // busy-wait for 10 msec
    }
    vTaskPrioritySet( NULL, priority ); // restore priority
  }
}

การทำงานของ FreeRTOS สำหรับ AVR จะมีระยะเวลาการเกิด OS Tick ประมาณ ~16 msec โดยใช้วงจร WDT เมื่อเกิดเหตุการณ์ OS Tick ในแต่ละครั้ง จะต้องมีการตรวจสอบดูว่า มีทาสก์ใดที่มีระดับความสำคัญสูงกว่า และพร้อมจะทำงานหรือไม่ หากมี ก็จะเกิดการเปลี่ยนบริบทการทำงานของทาสก์ (Task Switching)

ในตัวอย่างนี้ การทำงานของแต่ละทาสก์ เมื่อเปลี่ยนเข้าสู่สถานะ Running จะมีการยกระดับความสำคัญของทาสก์ โดยใช้คำสั่ง vTaskPrioritySet(...) ให้สูงกว่าทาสก์อื่นในระบบที่มีอยู่ในขณะนั้น แล้วจะสลับสถานะลอจิกที่ขาเอาต์พุต 10 ครั้ง ใช้เวลาโดยรวมประมาณ 100 มิลลิวินาที (10 msec 10 iterations / loop) ดังนั้นในช่วงเวลาดังกล่าว จะไม่มีการเปลี่ยนให้ทาสก์อื่นได้ทำงาน

รูป: การต่อวงจรและจำลองการทำงานด้วย Wokwi AVR Simulator

รูป: แสดงสัญญาณที่ขาเอาต์พุต D11 และ D12 ซึ่งได้จากการจำลองการทำงาน (มีการยกระดับความสำคัญ)

แต่ถ้าลองแก้ไขโค้ดตัวอย่าง โดยไม่ให้มีการยกระดับความสำคัญของทาสก์ แล้วทำขั้นตอนจำลองการทำงานอีกครั้ง ก็จะให้ผลการทำงานที่แตกต่างไปจากผลก่อนหน้านี้ ตามรูปต่อไปนี้

รูป: แสดงสัญญาณที่ขาเอาต์พุต D11 และ D12 ซึ่งได้จากการจำลองการทำงาน (ไม่มีการยกระดับความสำคัญ)

ทาสก์ Task1 และ Task2 เมื่อไม่มีการยกระดับความสำคัญให้สูงขึ้นเมื่อทำงาน ทั้งสองจึงมีระดับความสำคัญเท่ากันเหมือนตอนเริ่มต้น และจะไม่สามารถทำงานต่อเนื่องได้นานกว่า ~16 msec หรือ 1 OS Tick เพราะ Task Scheduler จะบังคับให้เกิดการสลับให้อีกทาสก์ได้ทำงานบ้าง (สลับกันไปแบบ Round Robin)

 


ตัวอย่างที่ 4: Resume / Suspend Tasks in Sequence#

โค้ดตัวอย่างนี้สาธิตการสร้างทาสก์ตามจำนวนที่กำหนดไว้โดย NUM_LEDS (มีค่าเท่ากับ 4) แต่ละทาสก์จะสร้างสัญญาณพัลส์กว้างประมาณ 100 msec โดยทำให้ลอจิกเป็น High ก่อน จากนั้นหน่วงเวลาด้วยคำสั่ง vTaskDelay() แล้วทำให้ลอจิกกลับไปเป็น Low ถือว่าจบการทำงานหนึ่งรอบ

นอกจากนั้น ทาสก์ที่ถูกสร้างขึ้นใหม่ จะอยู่ในสถานะ Suspended ในตอนเริ่มต้น แต่จะถูกเปลี่ยนสถานะจาก Suspended เป็น Ready ไปตามลำดับ ดังนั้นทาสก์เหล่านี้ จะได้ทำงานตามลำดับที่กำหนดไว้

#include <Arduino_FreeRTOS.h> // tested on Uno
#include <task.h>

#define LED_ON     HIGH
#define LED_OFF    LOW
#define NUM_LEDS   (4)

const byte LED_PINS[] = { 5,6,7,8 };

TaskHandle_t taskHandles[ NUM_LEDS ];

void setup() {
  char sbuf[4];
  int  priority = (tskIDLE_PRIORITY + 2);
  // create tasks and suspend all tasks initially.
  for ( int id=0; id < NUM_LEDS; id++ ) {
     sprintf( sbuf, "T%d", id );
     xTaskCreate( task, (const char *)sbuf, 64, 
       (void *)id, priority, &taskHandles[id] 
     );
     vTaskSuspend( taskHandles[id] );
  }
  vTaskResume( taskHandles[0] ); // resume the first task
  // Note the task scheduler is started automatically.  
}

void loop() {} 

void task( void *pvParameters ) {
   byte id = (int)pvParameters;
   byte ledPin = LED_PINS[id];
   pinMode( ledPin, OUTPUT );
   digitalWrite( ledPin, LED_OFF ); 
   while(1) { 
      // turn on LED
      digitalWrite( ledPin, LED_ON );
      // delay for approx. 100 msec
      vTaskDelay( pdMS_TO_TICKS(100) );
      // turn off LED
      digitalWrite( ledPin, LED_OFF );
      // resume the next task in the sequence
      vTaskResume( taskHandles[ (id+1) % NUM_LEDS ] ); 
      // suspend itself
      vTaskSuspend( NULL ); 
   }
}

รูป: การจำลองการทำงานด้วย Wokwi AVR Simulator

รูป: สัญญาณเอาต์พุตที่ขา {D5,D6,D7,D8} ตามลำดับ

 


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

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

การรอเวลาสำหรับทาสก์ มีคำสั่งให้ใช้งานได้ 2 รูปแบบ และให้ผลแตกต่างกัน คือ

  • vTaskDelay(): รอตามจำนวน Ticks นับจากจุดเวลาที่ทำคำสั่ง (Relative Delay Time)
  • vTaskDelayUntil(): รอให้ถึงจุดเวลาที่ระบุโดยนับเป็นจำนวน Ticks (Absolute Delay Time)

การทำงานของโค้ดตัวอย่างประกอบด้วยทาสก์ Task1 และ Task2 โดยให้ทำหน้าที่สลับสถานะลอจิกของขาเอาต์พุตที่เกี่ยวข้อง และให้มีการเว้นระยะเวลาในแต่ละครั้ง

การทำงานของทาสก์ มีการใช้คำสั่ง vTaskDelay() เพื่อรอเวลา โดยนับจากจุดเวลาที่ทำคำสั่งในขณะนั้น และในเชิงเปรียบเทียบได้ให้ทาสก์ Task2 มีการใช้คำสั่ง vTaskDelayUntil() เพื่อรอเวลาให้ผ่านไปถึงจุดเวลาในอนาคตตามที่กำหนดไว้ ทั้งสองคำสั่งนี้ของ FreeRTOS API จะทำให้ทาสก์เปลี่ยนสถานะจาก Running ไปเป็น Blocked ในขณะที่รอเวลา

#include <Arduino_FreeRTOS.h> // include the FreeRTOS library
#include <task.h>

#define LED1_PIN   (11)  // Arduino D11 pin 
#define LED2_PIN   (12)  // Arduino D12 pin

UBaseType_t task_priority = (tskIDLE_PRIORITY+1);

void setup() {
  pinMode( LED1_PIN, OUTPUT );
  pinMode( LED2_PIN, OUTPUT );
  Serial.begin( 115200 );

  xTaskCreate(
    task1,                // task-entry function for "Task1" 
    "Task1",              // task name: "Task1"
    128,                // task stack (allocated on heap)
    (void*)LED1_PIN,    // task param (LED pin)
    task_priority,      // task priority
    NULL );             // task handle for Task1

  xTaskCreate(
    task2,              // task-entry function for "Task2" 
    "Task2",            // task name: "Task2"
    128,                // task stack (allocated on heap)
    (void*)LED2_PIN,    // task param (LED pin)
    task_priority,      // task priority 
    NULL );             // task handle for Task2

  Serial.println( "FreeRTOS started..." );
}

void loop() {} // idle

// task-entry function for Task1
void task1( void* pvParameters ) { 
  int pin = (int)pvParameters; // use the task parameter for LED pin
  pinMode( pin, OUTPUT );      // set output direction for LED
  while (1) {
    digitalWrite( pin, HIGH );
    vTaskDelay( 10 ); // wait for 10 ticks (relative delay)
    digitalWrite( pin, LOW ); 
    vTaskDelay( 10 ); // wait for 10 ticks (relative delay)
  }
}

// task-entry function for Task2
void task2( void* pvParameters ) { 
  int pin = (int)pvParameters; // use the task parameter for LED pin
  pinMode( pin, OUTPUT );      // set output direction for LED
  TickType_t ticks = xTaskGetTickCount(); // get current OS tick count
  while (1) {
    digitalWrite( pin, HIGH ); 
    vTaskDelay( 10 ); // wait for 10 ticks (relative delay)
    digitalWrite( pin, LOW );
    // wait and update timestamp every 20 ticks (absolute delay)
    vTaskDelayUntil( &ticks, 20 ); 
  }
}

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

 


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

อีกหนึ่งตัวอย่างที่เปรียบเทียบผลการทำงานระหว่างการใช้คำสั่ง vTaskDelay() และ vTaskUntilDelay() ซึ่งคล้ายกับตัวอย่างที่แล้ว มีการสร้างทาสก์ T1 และ T2 ที่มีระดับความสำคัญเท่ากัน

ทาสก์ T1 เมื่อได้สลับสถานะหนึ่งครั้งจะรอเวลาเท่ากับ 2 OS Tick โดยใช้คำสั่ง vTaskDelay() แต่ทาสก์ T2 จะมีการแทรกคำสั่ง delay() เพิ่มเข้ามา เพื่อทำให้เกิดการทำคำสั่งแบบ busy-wait ตามระยะเวลาที่มีการสุ่มในหน่วยเป็นมิลลิวินาที ซึ่งจะอยู่ระหว่างค่า (portTICK_PERIOD_MS-5) ถึง (portTICK_PERIOD_MS+4) ก่อนที่จะทำคำสั่ง vTaskDelay()

#include <Arduino_FreeRTOS.h>

#define LED1_PIN   (11)  // Arduino D11 pin 
#define LED2_PIN   (12)  // Arduino D12 pin

void setup() {
  xTaskCreate( task1, "T1", 128 , NULL, 2, NULL );
  xTaskCreate( task2, "T2", 128 , NULL, 2, NULL );
  srand( analogRead(A0) ); // read A0 pin for seed
}

void task1( void *pvParameters ) {
  boolean st = false; // LED1 state
  pinMode( LED1_PIN, OUTPUT );
  while(1) { 
    // toggle LED1 output
    digitalWrite( LED1_PIN, st = !st );
    vTaskDelay( 2 /*ticks*/ );
  }
}

void task2( void *pvParameters ) {
  boolean st = false; // LED2 state
  pinMode( LED2_PIN, OUTPUT );
  while(1) { // toggle LED2 output
    digitalWrite( LED2_PIN, st = !st ); 
    // busy-wait for a randomized interval in msec
    delay( (-5 + rand()%10) + portTICK_PERIOD_MS );
    vTaskDelay( 2 /*ticks*/ );
  }
}

void loop() {} // idle

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

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

แต่ถ้าลองเปลี่ยนมาใช้คำสั่ง vTaskDelayUntil() ตามตัวอย่างต่อไปนี้ และนำไปจำลองการทำงานอีกครั้ง

#include <Arduino_FreeRTOS.h>

#define LED1_PIN   (11)  // Arduino D11 pin 
#define LED2_PIN   (12)  // Arduino D12 pin

TickType_t tickCount; // for saving tick count

void setup() {
  xTaskCreate( task1, "T1", 128 , NULL, 2, NULL );
  xTaskCreate( task2, "T2", 128 , NULL, 2, NULL );
  srand( analogRead(A0) ); // read A0 pin for seed
  tickCount = xTaskGetTickCount(); // get the current tick count
}

void task1( void *pvParameters ) {
  boolean st = false;
  TickType_t lastWakeTime = tickCount;
  pinMode( LED1_PIN, OUTPUT );
  while(1) { 
    // toggle LED1 output
    digitalWrite( LED1_PIN, st = !st );
    // delay until the next time point specified by lastWakeTime
    vTaskDelayUntil( &lastWakeTime, 2 );
  }
}

void task2( void *pvParameters ){
  boolean st = false;
  TickType_t lastWakeTime = tickCount;
  pinMode( LED2_PIN, OUTPUT );
  while(1) { 
    // toggle LED2 output
    digitalWrite( LED2_PIN, st = !st );
    // busy-wait for a randomized interval in msec
    delay( (-5 + rand()%10) + portTICK_PERIOD_MS );
    // delay until the next time point specified by lastWakeTime
    vTaskDelayUntil( &lastWakeTime, 2 );
  }
}

void loop() {} // idle

ในตัวอย่างนี้ ค่าของตัวแปร lastWakeTime ใช้สำหรับการบันทึกการนับจำนวน OS Ticks ในปัจจุบัน และจะมีการเพิ่มค่าครั้งละ 2 เพื่อใช้เป็นตัวกำหนดจุดเวลาในอนาคตสำหรับคำสั่ง vTaskDelayUntil() เพื่อให้ทาสก์รอถึงเวลาที่กำหนดไว้ ก่อนที่จะทำงานต่อไป

ผลจากการจำลองการทำงานด้วย Wokwi Simulator จะได้รูปคลื่นสัญญาณลักษณะนี้

รูป: สัญญาณเอาต์พุตที่ขา D11 และ D12 ที่ได้จากการจำลองการทำงานของโค้ด ซึ่งจะได้สัญญาณที่เหมือนกัน

 


กล่าวสรุป#

ในบทความนี้ เราได้เห็นตัวอย่างการเขียนโค้ด FreeRTOS สำหรับ AVR ในรูปแบบของ Arduino Sketch และทดลองใช้คำสั่งหรือฟังก์ชันของ FreeRTOS Library ที่เกี่ยวข้องกับการควบคุมการทำงานของทาสก์ ซึ่งทำให้เกิดการเปลี่ยนสถานะของทาสก์หรือสลับบริบทในการทำงาน เช่น vTaskSuspend() และ vTaskResume() และมีตัวอย่างแสดงให้เห็นความแตกต่างระหว่างการใช้คำสั่ง vTaskDelay() กับ vTaskDelayUntil()

 


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

Created: 2022-01-25 | Last Updated: 2022-02-12