บทความนี้แนะนำการเริ่มต้นใช้งาน FreeRTOS Library และเลือกใช้บอร์ดไมโครคอนโทรลเลอร์ Arduino (เช่น Uno หรือ Nano) และใช้ Arduino IDE ในการเขียนโค้ด เนื่องด้วยอุปกรณ์ฮาร์ดแวร์และซอฟต์แวร์ที่เข้าถึงได้ง่ายและมีใช้แพร่หลายสำหรับคนทั่วไป จึงเหมาะสำหรับผู้ที่สนใจอยากจะลองศึกษาการหลักการทำงานและใช้งานระบบปฏิบัติการเวลาจริงหรือเรียลไทม์-โอเอส (Real-Time OS: RTOS) ในเชิงปฏิบัติ
ขั้นตอนการติดตั้ง FreeRTOS สำหรับ Arduino IDE
ในการเตรียมซอฟต์แวร์เพื่อเขียนโค้ด ให้เปิดใช้งาน Arduino IDE แล้วไปที่เมนู Sketch > Include Library > Manage Libraries ค้นหาไลบรารีด้วยคำว่า freertos
จะปรากฏรายการตามรูปภาพตัวอย่าง จากนั้นเลือกเวอร์ชันล่าสุดแล้วกดปุ่มคลิก Install
โค้ดตัวอย่างแรก: ทำให้ LED สองดวงกระพริบ
ตัวอย่างแรกเป็นโค้ด Arduino Sketch ที่สาธิตการทำให้เกิดการสลับสถานะ LOW และ HIGH ที่ขาเอาต์พุต D12 และ D13 ตามลำดับ (มี 2 ช่องสัญญาณเอาต์พุต) ในโค้ดตัวอย่างนี้ ได้ใช้คำสั่ง millis()
ของ Arduino เพื่อคอยอ่านเวลาของระบบ (หน่วยเป็นมิลลิวินาที) จากนั้นจะนำไปเทียบกับเวลาอ้างอิงที่บันทึกเอาไว้ (Timestamp: ts
)
ถ้าอ่านเวลาปัจจุบัน (now
) ลบด้วยเวลาอ้างอิง (ts
) แล้วผลต่างที่ได้มากกว่าหรือเท่ากับช่วงเวลาที่กำหนดไว้ (INTERVAL_MSEC
) ในกรณีนี้คือ 17 มิลลิวินาที จะต้องสลับสถานะลอจิกของเอาต์พุตหนึ่งครั้ง แต่ให้ทำเพียงขาเดียวและสลับช่วงเวลากัน และให้อัปเดทและบันทึกเวลาอ้างอิงล่าสุดด้วย
#include <Arduino_FreeRTOS.h> // tested on Uno#define NUM_LEDS (2)
#define INTERVAL_MSEC (17)
const int LED_PINS[ NUM_LEDS ] = { 12, 13 }; // D12 and D13uint32_t ts, now, cnt = 0;
void setup() {
for ( int i=0; i < NUM_LEDS; i++ ) { // configure output pins
pinMode( LED_PINS[i], OUTPUT );
digitalWrite( LED_PINS[i], LOW );
}
ts = millis();
}void loop() {
now = millis(); // get the current timestamp (in msec)
// Do we need to update output now ?
if ( now - ts >= INTERVAL_MSEC ) {
ts = now; // keep the timestamp
int p = LED_PINS[cnt % NUM_LEDS]; // select the pin
cnt++; // increment the counter by 1
// toggle the selected pin either D12 or D13
digitalWrite( p, !digitalRead( p ) );
}
}
ถ้าทดสอบกับฮาร์ดแวร์จริง ใช้บอร์ด Arduino Uno หรือ Nano และวัดสัญญาณเอาต์พุตด้วยเครื่องออสซิลโลสโคป จะเห็นว่า คลื่นสัญญาณเอาต์พุตที่ขา D12 และ D13 จะเป็นคลื่นสี่เหลี่ยม มีคาบ (Period) ค่อนข้างคงที่ ครึ่งหนึ่งของคาบ (Half Period) ของแต่ละสัญญาณกว้างประมาณ 34 มิลลิวินาที
คำถาม: ทำไมจึงเลือกการทำให้ LED กระพริบ มาเป็นตัวอย่าง ?
- I/O Toggle เป็นการทำงานที่จะเกิดขึ้นซ้ำ ด้วยอัตราคงที่ ดังนั้นจึงเป็นตัวแทนของการทำงานเสมือนโปรแกรมย่อย (Task) ที่เกิดซ้ำต่อเนื่องกันไป (Cyclic) และมีการเว้นช่วงเวลาคงที่ (Periodic)
- ถ้ามีมากกว่าหนึ่งงาน เช่น ทำให้ I/O เกิดการสลับสถานะที่ขาเอาต์พุตมากกว่าหนึ่งขา ก็เป็นการกำหนดงานย่อยหลายงาน (Multi-Tasking) ที่ทำงานอิสระจากกัน และอาจเกิดขึ้นพร้อม ๆ กันได้ (Concurrent Independent Tasks)
- พฤติกรรมการทำงานของทาส์ก สามารถสังเกตได้ที่ขา I/O เช่น สามารถดูว่า เกิดการเปลี่ยนสถานะหรือไม่ และถ้าใช้เครื่องออสซิลโลสโคปวัดสัญญาณเอาต์พุต ก็จะเห็นการเปลี่ยนแปลงเชิงเวลาที่ขา I/O แต่ละขาได้ในรูปของกราฟสัญญาณ เช่น สามารถทราบได้ว่า ขอบขาขึ้นหรือขาลง (Rising or Falling Edge) เกิดขึ้นเมื่อใด และสามารถวัดความกว้างของพัลส์หรือคาบได้ เป็นต้น
โค้ดตัวอย่างที่ 2: เริ่มสร้าง FreeRTOS Task
ตัวอย่างที่ 2 ถ้าเราลองเขียนโค้ด Arduino Sketch ใหม่ โดยใช้วิธีการสร้างทาส์ก (Task) โดยใช้คำสั่งต่าง ๆ ของ FreeRTOS Library แทนการทำงานในฟังก์ชัน loop(){…}
ก็สามารถทำได้ดังนี้
#include <Arduino_FreeRTOS.h> // tested on Uno#define NUM_LEDS (2)
const int LED_PINS[ NUM_LEDS ] = { 12, 13 };
void task( void *pvParameters );void setup() {
for ( int i=0; i < NUM_LEDS; i++ ) {
pinMode( LED_PINS[i], OUTPUT ); // configure output pins
digitalWrite( LED_PINS[i], LOW );
}
// create a new task
xTaskCreate( task, "Task", 128, NULL, tskIDLE_PRIORITY+1, NULL );
// Note the task scheduler is started automatically.
}void loop() {} // do nothingsvoid task( void *pvParameters ) {
uint32_t cnt = 0; // a counter variable, increment by 1
while(1) {
int p = LED_PINS[ cnt%2 ]; // select the pin
cnt++;
// toggle the selected pin
digitalWrite( p, !digitalRead( p ) );
vTaskDelay( pdMS_TO_TICKS(17) ); // task delay for 1 tick
}
}
- ฟังก์ชันสำหรับการทำงานของทาส์กดังกล่าว ในกรณีนี้เป็นฟังก์ชันชื่อ
void task( void *pvParameters ){…}
ที่เราได้สร้างขึ้นมา เพื่อทำหน้าที่สลับสถานะของเอาต์พุตที่ขาหมายเลข 12 และ 13 - ชื่อทาส์ก (Task Name) ที่เป็นข้อความแบบ String ในภาษา C
- ขนาดของหน่วยความจำแบบ Stack สำหรับของแต่ละทาส์ก (Task Stack Depth) ถ้าตั้งค่าไว้น้อยเกินไป ถ้าฟังก์ชันของทาส์กทำงานแล้วเรียกฟังก์ชันซ้อนกัน อาจจะทำให้เกิด Stack Overflow ได้ แต่ถ้าตั้งค่ามากเกินไป ก็อาจเป็นใช้ SRAM ไม่เหมาะสม
- พารามิเตอร์ (Task Parameter) ที่จะนำไปใช้กับฟังก์ชันของทาส์กได้ ถ้าไม่มี ก็ให้ระบุเป็น
NULL
- ระดับความสำคัญของทาส์ก (Task Priority Level) เป็นเลขจำนวนเต็มบวก โดยทั่วไปก็จะให้มีค่าสูงกว่า ระดับความสำคัญของ Idle Task (
tskIDLE_PRIORITY
)
ข้อสังเกต: Idle Task เป็นทาส์กที่จะถูกเรียกโดยตัวจัดการงานของ FreeRTOS เมื่อไม่มีทาส์กใดพร้อมที่จะทำงาน) - และพอยน์เตอร์ (เรียกว่า Task Handle) ที่ใช้อ้างอิงทาส์กที่จะถูกสร้างขึ้นใหม่ ถ้าไม่ต้องการ ก็ให้ระบุเป็น
NULL
ภายในฟังก์ชัน task(){…}
มีประโยคคำสั่ง while(1){…}
ดังนั้นทาส์กจะทำงานซ้ำไปเรื่อย ๆ ทาส์กจะหยุดการทำงานชั่วคราวได้ตามระยะเวลาที่ต้องการ เช่น ถ้ามีการทำคำสั่ง vTaskDelay()
การหน่วงเวลาของทาส์ก จะใช้วิธีเรียกฟังก์ชัน vTaskDelay()
ของ FreeRTOS และระบุค่าเป็นจำนวนครั้งของการนับโดย Tick Timer ของ FreeRTOS หรือเรียกว่า Tick Count และเมื่อเกิด Tick Event ในแต่ละครั้ง จะเกิดการจัดลำดับทาส์กที่พร้อมจะทำให้ และเลือกทาส์กที่จะได้ช่วงเวลาในการทำงานถัดไป การตัดสินใจและกำหนดว่าทาส์กใดจะได้ทำงานเป็นหน้าที่ตัวจัดการงานของ FreeRTOS หรือที่เรียกว่า Task Scheduler
ในกรณีของ Arduino (Atmel AVR) อัตราการนับขึ้นหรือความถี่ของ Tick Timer จะถูกตั้งค่าไว้เท่ากับ 62 Hz (default) หรือมีคาบในการนับแต่ละครั้งเท่ากับ 16 มิลลิวินาที (msec) โดยประมาณ
ดังนั้นการทำคำสั่ง vTaskDelay( pdMS_TO_TICKS(17) )
เป็นการหน่วงเวลาไว้เท่ากับ 1 Tick และ pdMS_TO_TICKS()
จะใช้สำหรับแปลงช่วงเวลา (หน่วยเป็นมิลลิวินาที) ให้เป็นจำนวนนับตามจังหวะของ Tick Timer ในกรณีนี้ 17 มิลลิวินาที จะได้เท่ากับ 1 Tick
การทำคำสั่ง vTaskDelay()
จะทำให้ทาส์กดังกล่าวหยุดทำงานชั่วคราว โดยเปลี่ยนจากสถานะ “กำลังทำงาน” (RUNNING) ไปเป็นสถานะ “หยุด” (BLOCKED) และจะกลับมาอยู่ในสถานะ “พร้อม” (READY) เมื่อเวลาผ่านไปตามที่กำหนดไว้ เพื่อรอจัดลำดับให้ทำงานอีกครั้งในสถานะ “กำลังทำงาน” (RUNNING) ตามลำดับ
ข้อสังเกต: การเปลี่ยนสถานะจาก LOW->HIGH หรือ HIGH->LOW เมื่อเปรียบเทียบกันทั้งสองสัญญาณ จะเห็นว่า ไม่ตรงกัน (มี Phase Shift) เลื่อนเวลาไป 90 องศา
โค้ดตัวอย่างที่ 3: FreeRTOS Mulit-Tasking
ตัวอย่างที่ 3 สาธิตการสร้างทาส์กจำนวน 2 ชุด (T0 และ T1) โดยให้ทำงานอิสระจากกัน มีระดับความสำคัญเท่ากัน ให้ทาส์ก T0 ทำหน้าที่สลับสถานะลอจิกที่ขา 12 และทาส์ก T1 สำหรับขา 13 … ลองมาดูว่า จะเขียนโค้ดอย่างไร
#include <Arduino_FreeRTOS.h> // tested on Uno#define NUM_LEDS (2)
const int LED_PINS[ NUM_LEDS ] = { 12, 13 };
const char *TASK_NAMES[ NUM_LEDS ] = { "T0", "T1" };
void task( void *pvParameters );void setup() {
for ( int id=0; id < NUM_LEDS; id++ ) {
xTaskCreate(
task, TASK_NAMES[ id ], 128,
(void *)id, tskIDLE_PRIORITY + 1, NULL );
}
// Note the task scheduler is started automatically.
}void loop() {}void task( void *pvParameters ) {
int id = (int)pvParameters;
pinMode( LED_PINS[ id ], OUTPUT );
digitalWrite( LED_PINS[id], LOW );
vTaskDelay( id );
while(1) {
int p = LED_PINS[id];
digitalWrite( p, !digitalRead( p ) );
vTaskDelay( NUM_LEDS );
}
}
ในโค้ดตัวอย่างนี้ จะเห็นได้ว่า มีการสร้างทาส์ก ตามจำนวนของ LED (2 ดวง สำหรับขา 12 และ 13) และมีการป้อนพารามิเตอร์ให้ฟังก์ชันของทาส์ก โดยใช้เลขจำนวนเต็ม (id
มีค่าเป็น 0 หรือ 1) เป็นตัวจำแนกว่า เมื่อฟังก์ชัน task(){…}
ทำงาน เป็นของทาส์กใด เช่น ถ้าค่า id
เป็น 0 ทาส์กนั้นจะใช้ขา 12 เป็นเอาต์พุต แต่ถ้าเป็น 1 จะใช้ขา 13 เป็นต้น และสังเกตว่า เมื่อเริ่มต้น ทาส์กจะถูกหน่วงเวลาไว้เป็นจำนวน Ticks ที่ขึ้นอยู่กับค่าของ id
ก่อนทำงานลำดับถัดไป และถ้านำไปทดสอบกับฮาร์ดแวร์จริง ก็จะได้ผลเหมือนกับโค้ดตัวอย่างที่ 2
ข้อสังเกต: จะเห็นได้ว่า มีการประกาศและสร้างฟังก์ชัน task(){…}
เพียงฟังก์ชันเดียว แต่ก็สามารถใช้ร่วมกันระหว่างทาส์กได้มากกว่าหนึ่งทาส์กได้ (T0 แะล T1) เนื่องจากฟังก์ชันนี้ มีคุณสมบัติที่เรียกว่า Function Reentrancy และเมื่อทาส์กทำงานตามโค้ดของฟังก์ชันที่กำหนด (แม้ว่าจะเป็นฟังก์ชันเดียวในกรณีนี้) ทาส์กแต่ละอัน จะมี “Context” แยกกันสำหรับการทำงาน
คำถาม: ถ้าจะเพิ่มขา I/O และมีจำนวนของทาส์กตามจำนวน I/O ที่ต้องการ (เช่น เพิ่มเป็น 4 ขา) จะต้องแก้ไขโค้ดในตัวอย่างที่ 3 อย่างไร และรูปคลื่นสัญญาณที่ปรากฏที่ขา I/O จะมีลักษณะเป็นอย่างไร ?
ถ้าสนใจและมีเวลา โปรดอ่านต่อเนื้อหาตอนที่ 2
Creative Commons, Attribution-Non Commercial-Share Alike 4.0 International (CC BY-NC-SA 4.0)