Using FreeRTOS with Arduino AVR [1]

<rawat.s>
5 min readFeb 3, 2020

--

‍‍ ‍‍‍‍‍‍ ‍‍‍‍‍‍ ‍‍

บทความนี้แนะนำการเริ่มต้นใช้งาน FreeRTOS Library และเลือกใช้บอร์ดไมโครคอนโทรลเลอร์ Arduino (เช่น Uno หรือ Nano) และใช้ Arduino IDE ในการเขียนโค้ด เนื่องด้วยอุปกรณ์ฮาร์ดแวร์และซอฟต์แวร์ที่เข้าถึงได้ง่ายและมีใช้แพร่หลายสำหรับคนทั่วไป จึงเหมาะสำหรับผู้ที่สนใจอยากจะลองศึกษาการหลักการทำงานและใช้งานระบบปฏิบัติการเวลาจริงหรือเรียลไทม์-โอเอส (Real-Time OS: RTOS) ในเชิงปฏิบัติ

ขั้นตอนการติดตั้ง FreeRTOS สำหรับ Arduino IDE

ขั้นตอนการติดตั้งไลบรารี FreeRTOS สำหรับ Arduino ที่พัฒนาโดย Richard Barry

ในการเตรียมซอฟต์แวร์เพื่อเขียนโค้ด ให้เปิดใช้งาน 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 D13
uint32_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 มิลลิวินาที

รูปคลื่นสัญญาณที่ได้จากการทำงานของโค้ดตัวอย่างแรก (TIME/DIV = 20 msec)

คำถาม: ทำไมจึงเลือกการทำให้ 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) ตามลำดับ

ไดอะแกรมแสดงสถานะและการเปลี่ยนสถานะของทาส์กใน FreeRTOS
รูปคลื่นสัญญาณที่ได้จากการทำงานของโค้ดตัวอย่างที่ 2

ข้อสังเกต: การเปลี่ยนสถานะจาก 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)

--

--

<rawat.s>
<rawat.s>

Written by <rawat.s>

I'm Thai and working in Bangkok/Thailand.

No responses yet