Using FreeRTOS with Arduino ATSAMD21

<rawat.s>
6 min readJan 30, 2020

--

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

บทความนี้นำเสนอขั้นตอนการใช้งาน FreeRTOS (เวอร์ชัน V8.2.3) ซึ่งเป็นระบบปฏิบัติการเวลาจริง (RTOS: Real-Time Operating Sytem) หรือเรียกชื่อย่อๆ ว่า “อาร์-ทอส” และเป็นซอฟต์แวร์ประเภท Open Source และจะนำมาใช้กับบอร์ดไมโครคอนโทรลเลอร์ ATSAMD21 (ARM Cortex-M0+) ที่ได้มีการติดตั้ง Arduino Bootloader เอาไว้แล้ว (Arduino ZERO Compatible) โดยใช้ Arduino IDE (ที่ลองใช้คือเวอร์ชัน 1.8.9 สำหรับ Windows 10) สำหรับการเขียนโค้ด

ตัวเลือกในการเขียนโปรแกรมสำหรับ ATSAMD21 ในบทความนี้
Code Repository ของ FreeRTOS สำหรับ Atmel SAMD21 / SAMD51 ใน Github

📋 Update (2020–06–10): ทางผู้พัฒนา “BriscoeTech” ได้ทำให้สามารถใช้ FreeRTOS เวอร์ชัน V10.2 สำหรับ SAMD21 ได้แล้ว:
🔗 https://github.com/BriscoeTech/Arduino-FreeRTOS-SAMD21

ขั้นตอนการดำเนินการมีดังนี้

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

  • เปิดใช้งาน Arduino IDE (ถ้ายังไม่มี ให้ดาวน์โหลดและติดตั้งก่อน)จากนั้นให้ทำขั้นตอนติดตั้งไลบรารีสำหรับ Arduino เพิ่ม โดยทำคำสั่งจากเมนู Sketch > Include Library > Manage Libraries .. เพื่อเปิดใช้งาน Arduino Library Manager
  • สร้าง Arduino Sketch เพื่อทดลองใช้ไลบรารี <FreeRTOS_SAMD21.h> โดยสาธิตการสร้างทาส์ก (Task) ที่ทำให้ LED กระพริบด้วยอัตราคงที่ และเรียกใช้ตัวจัดการทาส์ก (FreeRTOS Task Scheduler)
  • ทำขั้นตอน Build แล้วอัปโหลด Arduino Sketch ไปยังบอร์ด RobotDyn SAMD21 M0-Mini ที่ได้เลือกมาทดลอง
ค้นหาไลบรารี FreeRTOS_SAMD21 โดยใช้ Arduino Library Manager
แสดงสถานะการติดตั้งไลบรารี FreeRTOS_SAMD21 ได้สำเร็จแล้ว

ข้อสังเกต: การติดตั้งไลบรารีเพิ่ม อาจใช้วิธีนำเข้าไฟล์ .zip ที่ได้ดาวน์โหลดมาจาก Github (เช่น ชื่อไฟล์ Arduino-FreeRTOS-SAMD21-master.zip)โดยไปที่เมนู Sketch > Include Library > Add .zip Library

ถ้าเป็น Arduino IDE สำหรับระบบปฏิบัติการ Windows เราสามารถดูรายการไลบรารีที่ได้ติดตั้งไปแล้วได้ในไดเรกทอรี

C:\Users\{username}\Documents\Arduino\libraries
ตัวอย่างโค้ดสาธิตสร้างทาส์กชื่อ LED Blink เพื่อทำให้ LED กระพริบที่ขา 13

จากโค้ดตัวอย่างจะเห็นการใช้คำสั่ง xTaskCreate(…) เพื่อสร้างทาส์กสำหรับ FreeRTOS และมีฟังก์ชันสำหรับทาส์กนี้ (Task Function) คือ void led_blink(…){…} ซึ่งได้กำหนดและสร้างขึ้นมาเอง ภายในฟังก์ชันนี้มีการใช้ประโยคคำสั่งวนซ้ำ while(1) {…} ดังนั้นจึงเป็นการทำงานแบบต่อเนื่องไป (เรียกว่า Cyclic Task)

การสร้างทาส์ก (Task Creation) สำหรับ FreeRTOS จะต้องระบุสิ่งต่อไปนี้

  • ชื่อของทาส์ก (จำกัดความยาว)
  • ขนาด Stack Size (Stack Depth) สำหรับทาส์ก
  • พอยน์เตอร์สำหรับระบุพารามิเตอร์สำหรับทาส์ก (หรือ Task Parameter List) ถ้าไม่มี ก็ให้เป็น NULL
  • ระดับความสำคัญของทาส์ก (Task Priority)
  • พอยน์เตอร์สำหรับอ้างอิงทาส์กที่จะถูกสร้างใหม่ (Task Handle) ถ้าไม่มี ก็ให้เป็น NULL

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

ข้อสังเกต ในฟังก์ชันของทาส์ก led_blink() มีการเรียกใช้ฟังก์ชัน vTaskDelete() ซึ่งเป็นการทำลายหรือยกเลิกการทำงานของทาส์กที่เกี่ยวข้อง แต่เนื่องจากภายในฟังก์ชันนี้ มีการวนซ้ำแบบไม่รู้จบ (Endless Loop) ดังนั้นการทำคำสั่ง vTaskDelete() จึงจะไม่เกิดขึ้น แต่ใส่ไว้เป็นตัวอย่าง ในบางกรณีเราอาจต้องการสร้างทาส์กขึ้นมาให้ทำงานเมื่อเกิดเหตุการณ์บางอย่าง และหลังจากนั้นทาส์กสามารถจบและยกเลิกการทำงานได้ (Runtime Task Creation / Deletion)

การหน่วงเวลาการทำงานก่อนทำคำสั่งถัดไป จะไม่ใช่คำสั่ง delay() ของ Arduino แต่จะต้องใช้คำสั่งของ FreeRTOS เช่น vTaskDelay(…) ซึ่งจะต้องระบุค่าที่เป็นเลขจำนวนเต็ม (นับเป็นจำนวน Ticks) การทำคำสั่งเช่นนี้ จะทำให้เกิดการเปลี่ยนสถานะของทาส์ก (Task State) จาก RUNNING ให้เป็น BLOCKED และรอจนกว่าช่วงเวลาที่ระบุจะผ่านไป ทาส์กนี้จึงจะเปลี่ยนสถานะ READY พร้อมที่จะทำงานอีกครั้ง ทั้งหมดนี้อาศัย Task Scheduler ของ FreeRTOS เป็นตัวจัดการ ซึ่งจะต้องเริ่มต้นโดยการทำคำสั่ง vTaskStartScheduler()

เนื่องจาก FreeRTOS ได้รับการพัฒนามาให้สามารถนำไปใช้กับไมโครคอนโทรลเลอร์ได้หลายตระกูล รวมถึงสามารถตั้งค่าการใช้งานได้ ผู้ที่สนใจสามารถลองศึกษาดูจากไฟล์ เช่น FreeRTOSConfig.h ซึ่งจะต้องระบุความถี่ของ CPU (เท่ากับ 48MHz ในกรณีของ Arduino SAMD21) การกำหนดความเร็วของตัวนับ (Tick Timer) ที่ใช้โดย FreeRTOS หรือที่เรียกว่า Tick Rate (default: 1000 Hz) และการเลือกใช้งาน FreeRTOS ในโหมด Preemptive Scheduling เพื่อให้ทาส์กที่มีความสำคัญสูงกว่าสามารถแทรกระหว่างการทำงานของทาส์กที่มีความสำคัญต่ำกว่าได้ เป็นต้น

แสดงเนื้อหาบางส่วนของไฟล์ FreeRTOSConfig.h
เลือกบอร์ด Arduino ZERO (Native USB Port) แล้วทำขั้นตอน Build และ Upload
รูปแสดงตำแหน่งของขา I/O ของบอร์ด RobotDyn M0-Mini

เมื่อทำขั้นตอน Build & Upload ไปยังบอร์ดแล้ว ให้ลองวัดสัญญาณเอาต์พุตที่ขา D13 ด้วยเครื่องออสซิสโลสโคป (Oscilloscope) ถ้าต่อวงจร LED เพิ่มที่ขา D13 จะเห็นว่า LED กระพริบ

ตัวอย่างรูปคลื่นสัญญาณเอาต์พุตที่วัดได้ (ความถี่ 5 Hz, มีคาบเท่ากับ 200 msec, 50% Duty Cycle)

ตัวอย่างการทำงานแบบหลายทาสก์พร้อม ๆ กัน

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

ถัดไปลองมาดูตัวอย่างการสร้างทาส์กแบบหลายทาส์ก โดยให้โปรแกรมมีการทำงานที่ประกอบด้วย Task 1 และ Task 2 ตามลำดับ มีระดับ Priority Level เท่ากัน และให้แต่ละทาส์กทำงานอิสระจากกัน (Independent Tasks) และไม่มีการสื่อสารหรือรอจังหวะกันระหว่างทาส์ก (No Inter-Task Communication & Synchronization)

ฟังก์ชันการทำงาน Task 1 และ Task 2 จะทำให้ LED จำนวน 2 ดวง ที่ขา D13 และ D12 เกิดการกระพริบ (I/O Toggle) และให้ได้ความถี่เท่ากัน แต่ช่วงเวลาที่เกิดการเปลี่ยนสถานะลอจิกของขาเอาต์พุตไม่ตรงกัน

โค้ด Arduino Sketch (.ino) ต่อไปนี้แสดงให้เห็นการสร้าง FreeRTOS Tasks ได้แก่ Task 1 และ Task 2 ตามลำดับ ให้สังเกตว่า ฟังก์ชันสำหรับทาส์กทั้งสองนั้น จะถูกสร้างแยกไว้ในไฟล์ tasks.cpp (ถ้าจะคอมไพล์โค้ด จะต้องใช้ทั้งสองไฟล์)

ไฟล์ Arduino Sketch (.ino)
ไฟล์ tasks.cpp ที่มีการสร้างฟังก์ชันสำหรับ Task 1 และ Task 2

File: samd21_freertos_led_blink-1.ino

#include <FreeRTOS_SAMD21.h>extern void task1( void * ); 
extern void task2( void * );
void setup() {
// Create Task 1 and Task 2
xTaskCreate(task1, "Task1", 256, NULL, tskIDLE_PRIORITY+1, NULL);
xTaskCreate(task2, "Task2", 256, NULL, tskIDLE_PRIORITY+1, NULL);
vTaskStartScheduler();
}
void loop() {
vNopDelayMS(100);
}

File: tasks.cpp (version 1)

#include <Arduino.h>
#include <FreeRTOS_SAMD21.h>
#define LED1_PIN 13
#define LED2_PIN 12
void task1( void *pvParameters ) {
boolean state = false;
pinMode( LED1_PIN, OUTPUT );
while (1) { // toggle LED1 at every 100 msec
digitalWrite( LED1_PIN, state = !state );
vTaskDelay( 100 / portTICK_PERIOD_MS );
}
vTaskDelete( NULL );
}
void task2( void *pvParameters ) {
boolean state = false;
pinMode( LED2_PIN, OUTPUT );
vTaskDelay( 50 / portTICK_PERIOD_MS );
while (1) { // toggle LED2 at every 100 msec
digitalWrite( LED2_PIN, state = !state );
vTaskDelay( 100 / portTICK_PERIOD_MS );
}
vTaskDelete( NULL );
}

ลองมาดูรูปคลื่นสัญญาณเอาต์พุตที่ขา D13 และ D12 ที่ได้จากการวัดสัญญาณด้วยเครื่องออสซิลโลสโคป

รูปคลื่นสัญญาณเอาต์พุตที่ขา D13 (Channel 1) และ D12 (Channel 2), TIME/DIV = 50 msec

คลื่นสัญญาณเป็นแบบสี่เหลี่ยมหรือ Rectangular Wave มีความถี่เท่ากัน และมีค่า Duty Cycle 50% แต่ทั้งสองสัญญาณมีความต่างเฟสกัน (Phase Shift) เมื่อเปรียบเทียบกันที่ขอบสัญญาณขาขึ้นหรือขาลง ดังนั้นลองตอบคำถามดูซิว่า ประโยคคำสั่งใดในโค้ดตัวอย่างที่ทำให้เกิดความต่างเฟสกันระหว่างสัญญาณเอาต์พุตทั้งสอง ?

ถ้าให้ทาส์กไม่ต้องมีการหน่วงเวลาโดยการใช้คำสั่ง vTaskDelay() แต่ให้ทำคำสั่งสลับสถานะเอาต์พุตหนึ่งครั้งแล้วส่งต่อให้ทาส์กอื่นได้มีโอกาสทำงาน โดยทำคำสั่ง taskYIELD() ซึ่งจะทำให้เกิด Task Context Switching ความถี่ของเอาต์พุตที่ขา D13 และ D12 จะเปลี่ยนไปอย่างไร

File: tasks.cpp (version 2)

#include <Arduino.h>
#include <FreeRTOS_SAMD21.h>
#define LED1_PIN 13
#define LED2_PIN 12
void task1( void *pvParameters ) {
boolean state = false;
pinMode( LED1_PIN, OUTPUT );
while (1) {
digitalWrite( LED1_PIN, state = !state );
taskYIELD(); // cause FreeRTOS task context switching
}
vTaskDelete( NULL );
}
void task2( void *pvParameters ) {
boolean state = false;
pinMode( LED2_PIN, OUTPUT );
while (1) {
digitalWrite( LED2_PIN, state = !state );
taskYIELD(); // cause FreeRTOS task context switching
}
vTaskDelete( NULL );
}

จากรูปคลื่นสัญญาณที่วัดได้ จะเห็นว่า สัญญาณเอาต์พุตมีความถี่สูงขึ้น และทั้งสองทาส์กได้มีโอกาสทำงานเท่ากัน (ในตัวอย่างนี้ Task 1 และ Task 2 มีระดับ Priority เท่ากัน)

คราวนี้มาลองใช้วิธีเข้าถึงรีจิสเตอร์ของ GPIO ที่ขา PA17 และ PA19 ของ ATSAMD21 ซึ่งตรงกับขา D13 และ D12 ของ Arduino ตามลำดับ จะทำให้ความถี่ของเอาต์พุตสูงขึ้นได้หรือไม่

File: tasks.cpp (version 3)

#include <Arduino.h>
#include <FreeRTOS_SAMD21.h>
void task1( void *pvParameters ) {
REG_PORT_DIRSET0 = (1<<17); // PA17 output
while (1) {
REG_PORT_OUTTGL0 = (1<<17); // toggle I/O at PA17
taskYIELD(); // cause FreeRTOS task context switching
}
vTaskDelete( NULL );
}
void task2( void *pvParameters ) {
REG_PORT_DIRSET0 = (1<<19); // PA19 output
while (1) {
REG_PORT_OUTTGL0 = (1<<19); // toggle I/O at PA19
taskYIELD(); // cause FreeRTOS to perform context switching
}
vTaskDelete( NULL );
}

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

TIME/DIV = 5 usec

File: tasks.cpp (version 4)

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

การหน่วงเวลาเพื่อเว้นระยะเวลาในการสลับลอจิกของเอาต์พุตของแต่ละทาส์ก จะใช้วิธีทำคำสั่ง nop ตามจำนวนที่กำหนดไว้ (100 ครั้ง) แต่ละทาส์กจะพยายามอยู่ในสถานะทำงาน หรือ RUNNING แต่เนื่องด้วยการจัดการของ FreeRTOS Task Scheduler ทาส์กที่กำลังทำงานอยู่จะถูกหยุดชั่วคราว (เปลี่ยนไปอยู่ในสถานะ READY) แล้วสลับให้ทาส์กที่พร้อมในลำดับถัดไปได้ทำงาน (มีค่า Priority เท่ากัน)

#include <Arduino.h>
#include <FreeRTOS_SAMD21.h>
inline void delay_with_some_nops() {
for(volatile uint32_t i=0; i < 100; ++i) {
__asm volatile( "nop\n\t" );
}
}
void task1( void *pvParameters ) {
REG_PORT_DIRSET0 = (1<<17); // PA17 output
while (1) { // toggle LED1 at PA17 with some "nop" delays
REG_PORT_OUTTGL0 = (1<<17); // toggle I/O at PA17
delay_with_some_nops();
}
vTaskDelete( NULL );
}
void task2( void *pvParameters ) {
REG_PORT_DIRSET0 = (1<<19); // PA19 output
while (1) { // toggle LED2 at PA19 with some "nop" delays
REG_PORT_OUTTGL0 = (1<<19); // toggle I/O at PA19
delay_with_some_nops();
}
vTaskDelete( NULL );
}

จากรูปคลื่นสัญญาณที่วัดได้ เราจะเห็นว่า แต่ละทาส์กจะทำงานได้สักประมาณ 1 มิลลิวินาที (msec) ในระหว่างนั้นจะเห็น I/O Toggle สำหรับขาเอาต์พุตของทาส์กที่เกี่ยวข้อง จากนั้นจะถูกสลับให้อีกทาส์กหนึ่งทำงาน โดยการจัดการของ Task Scheduler (ข้อสังเกต configTICK_RATE_HZ ถูกกำหนดไว้เท่ากับ 1000 ครั้งต่อวินาที)

TIME/DIV = 500 usec

ถ้าไม่มีบอร์ด ATSAMD21 แต่อยากจะลองศึกษาใช้งาน Arduino FreeRTOS Library สำหรับ Uno/Nano/MEGA2560 ได้เช่นกัน แต่ก็มีความแตกต่างจากกรณีที่ใช้ ATSAMD21 ยกตัวอย่างเช่น ถ้าเป็น AVR จะใช้ Watchdog Timer (WDT) เป็น OS Tick Timer ซึ่งมีช่วงเวลาสั้นสุดที่เลือกได้คือ 15 msec และข้อจำกัดอีกประการหนึ่งของ AVR คือ ความจุของ SRAM ซึ่งส่งผลต่อการเลือกใช้ขนาดของ Stack ของแต่ละทาส์ก และ Heap (เมื่อใช้คำสั่ง malloc) เป็นต้น

เมื่อเราได้ติดตั้งและสามารถใช้งาน FreeRTOS สำหรับ ATSAMD21 โดยใช้ Arduino IDE และทดลองตัวอย่างโค้ดเบื้องต้นไปบ้างแล้ว เราก็มีเครื่องมือพร้อมแล้วที่จะเรียนรู้การใช้งาน FreeRTOS ในระดับถัดไป ซึ่งมีหลายหัวข้อที่น่าสนใจที่เกี่ยวกับ RTOS

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