.
จากบทความที่แล้ว เราได้เรียนรู้ขั้นตอนการใช้งาน Microchip Studio IDE เพื่อลองเขียนโค้ดภาษา C ที่ใช้ไลบรารี AVR libc และจำลองการทำงาน นอกจากนั้นยังได้เห็นว่า เราสามารถนำเข้าไฟล์ Arduino Sketch (.ino) ที่สาธิตการใช้งาน Arduino FreeRTOS Library ในเบื้องต้น มาสร้างเป็นโปรเจกต์และจำลองการทำงานใน Microchip Studio ได้ด้วย
บทความนี้ต่อจากบทความที่แล้ว จะมาสาธิตตัวอย่างเพิ่มเติมในการใช้ AVR Simulator จำลองการทำงานของโค้ดที่ใช้ Arduino FreeRTOS และช่วยในการเรียนรู้ก่อนนำไปใช้งานกับฮาร์ดแร์จริงอย่างไรได้บ้าง เช่น การจับเวลาโดยใช้ Stop Watch ร่วมกับการกำหนดตำแหน่งของ Breakpoint ในโค้ด
การทำงานของ Watchdog Timer สำหรับ Arduino FreeRTOS
.
การทำงานของ Arduino FreeRTOS สำหรับ AVR นั้น จะใช้ Watchdog Timer (WDT) เป็นตัวกำหนดจังหวะการทำงานของ FreeRTOS และถ้าศึกษาดูโค้ดในไฟล์ <PROJECT>\ArduinoCore\include\libraries\FreeRTOS\FreeRTOSVariant.h
จะพบว่า มีการกำหนดค่าให้ portUSE_WDTO
เท่ากับ WDTO_15MS
ซึ่งหมายถึง 16 msec โดยประมาณ และเป็นค่าของ WDT Timeout Period สั้นที่สุดที่เราสามารถเลือกใช้ได้สำหรับ AVR และใช้เป็นคาบเวลาของ FreeRTOS Tick หรือกล่าวได้ว่า 1 Tick จะเท่ากับ 16 msec โดยประมาณ
#define portUSE_WDTO (WDTO_15MS)
ในไฟล์เดียวกัน มีการกำหนดค่า configTICK_RATE_HZ
เพื่อระบุความถี่ของ Tick Rate (Hz) ไว้ดังนี้ และจะได้เท่ากับ (128000/2048) จะได้ 62.5 แต่จะถูกปัดเศษทิ้งให้เป็นเลขจำนวนเต็มและได้ 62
#define configTICK_RATE_HZ \
((TickType_t)((uint32_t)128000 >> (portUSE_WDTO + 11)))
ค่าของ configTICK_RATE_HZ
จะถูกนำไปใช้ในการคำนวณเพื่อแปลงระยะเวลา (มิลลิวินาที) ให้กลายเป็นจำนวนของ Ticks เช่น
/* Converts a time in milliseconds to a time in ticks. */
#define pdMS_TO_TICKS( xTimeInMs ) \
((TickType_t)(((TickType_t)(xTimeInMs)*configTICK_RATE_HZ)/1000))
การใช้คำสั่งต่อไปนี้ เป็นการทำให้ทาสก์ของ FreeRTOS ที่ทำคำสั่งนี้ หยุดรอให้เวลาผ่านไป 1 Tick
vTaskDelay( 1 );
หรือถ้าจะเขียนคำสั่งแบบนี้ ระบุเวลาในหน่วยเป็นมิลลิวินาที
vTaskDelay( pdMS_TO_TICKS( 17 ) );
เราจะต้องเลือกค่าเป็น 17 ไม่ใช่ 16 สำหรับ 1 Tick เพื่อใช้กับคำสั่ง vTaskDelay()
(16*62)/1000 = 0.992 => 0 (type casted to integer)(17*62)/1000 = 1.054 => 1 (type cased to integer)
ปัญหาเมื่อใช้ AVR Simulator
.
ถ้าเลือกใช้ความถี่ 16MHz ตามความเป็นจริง แทนการใช้ความถี่ 1 MHz (default for AVR simulator) การทำงานของ WDT จะทำให้เกิดอินเทอร์รัพท์เกิดเร็วขึ้น 16 เท่า ถ้าใช้ Stop Watch ของ AVR Simulator จับเวลาเหตุการณ์ของ WDT Interrupt ในแต่ละครั้ง
ดังนั้นเราจะแก้ปัญหานี้ เราอาจใช้วิธีเลือกค่า WDT Timeout ช้าลง 16 เท่า จากเดิมใช้ตัวหารความถี่ของ 128kHz หรือ Prescaler เท่ากับ 2K Cycles ให้ใช้เป็น 32K Cycles หรือคิดเป็น 16 เท่าช้าลง
เดิม: 128kHz /(2*1024) = 62.5 Hz (หรือระยะเวลาเท่ากับ 16 msec)
ใหม่: 128kHz /(32*1024) = 3.90625 Hz (หรือระยะเวลาเท่ากับ 256 msec)
ภายในไฟล์ FreeRTOSVariant.h
จากเดิม
#define portUSE_WDTO (WDTO_15MS)#define configTICK_RATE_HZ \
( (TickType_t)((uint32_t)128000 >> (portUSE_WDTO + 11)) )
ให้แทนที่ด้วยโค้ดต่อไปนี้
#define portUSE_WDTO (WDTO_250MS)#define configTICK_RATE_HZ \
( (TickType_t)((uint32_t)128000 >> ((portUSE_WDTO-4) + 11) )
เพื่อทำให้ผลการจำลองการทำงานโดย AVR Simulator ใน Microchip Studio เมื่อจับเวลาด้วย Stop Watch และเลือกใช้ความถี่ 16 MHz ทำงานได้อย่างถูกต้องนอกจากนั้นค่าของ configTICK_RATE_HZ
ยังคงเดิมเท่ากับ 62
จากไฟล์ Sketch.cpp ที่สร้างโดยอัตโนมัติเมื่อนำเข้าไฟล์ Arduino Sketch (ดัดแปลงจากโค้ดตัวอย่างในบทความนี้แล้วเล็กน้อย)
#include <Arduino.h>
#include <Arduino_FreeRTOS.h>#define LED_PIN 13void task( void *pvParameters );void setup() {
xTaskCreate(
task /* task function */,
"BlinkTask" /* task name */,
128 /* stack size */,
NULL /* no parameters */,
2 /* priority level */, NULL /* no task handle */
);
// Note the FreeRTOS task scheduler is started automatically.
}void loop() {}void task( void *pvParameters ) {
boolean state = false;
pinMode( LED_PIN, OUTPUT );
while(1) { // toggle and update LED output
digitalWrite( LED_PIN, state = !state ); // toggle LED
vTaskDelay( pdMS_TO_TICKS( 100 ) ); // wait for ~100msec
}
}
ถ้าเราอยากศึกษาพฤติกรรมการทำงานของทาส์กนี้ เช่น การทำคำสั่ง vTaskDelay()
ในแต่ละครั้ง จะส่งผลต่อการหยุดรอของทาส์กที่ทำคำสั่งดังกล่าว ก็ให้จำลองการทำงานแล้วเลือกบรรทัดที่มีคำสั่งดังกล่าวให้เป็น Breakpoint แล้วดูค่า Stop Watch ว่าเพิ่มขึ้นจากเดิมเท่าไหร่ (หน่วยเป็นไมโครวินาที)
ในตัวอย่างนี้เราคาดหวังว่า 100 (msec) จะให้ผลตรงกับ (TickType_t)(62*100/1000)
หรือเท่ากับ 6 Ticks ซึ่งจะเป็นตัวกำหนดอัตราการสลับสถานะที่ขา LED_PIN
ถ้าจำลองการทำงานตามที่กล่าวไป จะได้ตัวเลข Stop Watch เช่น 554.88
, 98843.94
และ 197156.81
usec สามครั้งแรกตามลำดับ เมื่อหยุดการทำงานชั่วคราวโดย Breakpoint
98843.94 - 554.88 = 98289.06 usec (~98.29 msec)
197156.81 - 98843.94 = 98312.87 usec (~98.31 msec)
ระยะเวลาในการเกิดอินเทอร์รัพท์จาก WDT
.
เราสามารถใช้วิธีการจำลองการทำงานและหาอัตราหรือระยะเวลาในการเกิดเหตุการณ์ WDT Interrupt ซึ่งเป็นพื้นฐานสำคัญในการทำงานของ Arduino FreeRTOS
ในไฟล์ <PROJECT>\ArduinoCore\src\libraries\FreeRTOS\port.c
มีโค้ดบรรทัดต่อไปนี้
#define portSCHEDULER_ISR WDT_vect
และจะเห็นว่า WDT_vect
ซึ่งเป็นชื่อของ Macro Definition สำหรับ WDT Interrupt Vector เอาไว้เขียนโค้ดในส่วนที่เป็น ISR (Interrupt Service Routine) สำหรับ WDT
ถ้าเราค้นหาคำนี้ต่อไปในโค้ด จะพบว่า มีการสร้างฟังก์ชัน ISR สำหรับ WDT ไว้ดังนี้ (ตัดมาเพียงบางส่วน เฉพาะส่วนที่เกี่ยวข้องกับโหมดทำงานของ FreeRTOS แบบ Preemption)
ISR(portSCHEDULER_ISR)
{
vPortYieldFromTick();
__asm__ __volatile__ ( "reti" );
}
คำสั่งแรกภายใน ISR เป็นการเรียกฟังก์ชัน vPortYieldFromTick()
ทุก ๆ ครั้งที่เกิด Tick Interrupt จะเรียกฟังก์ชันดังกล่าว
ถ้าเรากำหนดให้บรรทัดนี้เป็น Breakpoint เราจะดูค่า Stop Watch ในแต่ละครั้ง แล้วนำมาควรระยะเวลาในการเกิดอินเทอร์รัพท์ หรือ Tick Period
ลองมาดูผลการจำลองการทำงาน จะเห็นว่า ค่าของ Stop Watch เช่น 5 ครั้งแรกตามลำดับ ดังนี้
554.88, 16874.19, 33259.81, 49645.25, 66030.75 usec
และนำมาคำนวณเป็นผลต่างหรือระยะเวลาได้ดังนี้ ดังนั้นจะได้เวลา Tick Period = 16.385 msec โดยประมาณ
16874.19 - 554.88 = 16319.31 usec
33259.81 - 16874.19 = 16385.62 usec
49645.25 - 33259.81 = 16385.44 usec
66030.75 - 49645.25 = 16385.50 usec
การตั้งค่า Task Priority มีผลอย่างไรต่อการทำงาน ?
.
มาดูตัวอย่างการเขียนโค้ดเพื่อสร้างทาส์กสำหรับ FreeRTOS จำนวน 2 ทาสก์ ที่มีความแตกต่างในระดับความสำคัญหรือ Task Priority Level
สำหรับ Arduino FreeRTOS Library ได้มีการกำหนดระดับของความสำคัญไว้ในไฟล์ FreeRTOSConfig.h
เท่ากับ 4 ระดับ โดยที่ 0 หมายถึง ต่ำสุด (lowest) และค่าสูงสุดคือ (configMAX_PRIORITIES-1)
#define configMAX_PRIORITIES (4)
เนื่องจาก Arduino FreeRTOS ทำงานในโหมด Preemptive Scheduling ซึ่งจะทำให้ทาสก์ที่พร้อมทำงานและมีความสำคัญสูงกว่า ได้ทำงานโดยใช้ทรัพยากรของซีพียู
มาลองดูโค้ดตัวอย่างถัดไป เริ่มต้นด้วยการสร้างทาสก์ task1
และ task2
ตามลำดับ และให้ task2
มีระดับความสำคัญสูงกว่า task1
แต่มีฟังก์ชันการทำงานไม่ซับซ้อน และแตกต่างกันเล็กน้อย
#include <Arduino.h>
#include <Arduino_FreeRTOS.h>void task1( void *pvParameters );
void task2( void *pvParameters );#define TASK1_PRIORITY 1
#define TASK2_PRIORITY 2void setup() {
xTaskCreate(
task1, "Task1", 128, NULL, TASK1_PRIORITY, NULL );
xTaskCreate(
task2, "Task2", 128, NULL, TASK2_PRIORITY, NULL );
// Note the FreeRTOS task scheduler is started automatically.
}void loop() {}// global variables
volatile uint32_t task1_counter = 0;
volatile uint32_t task2_counter = 0;void task1( void *pvParameters ) {
task1_counter++;
while(1) {
task1_counter++;
}
}void task2( void *pvParameters ) {
task2_counter++;
vTaskDelay( 1 );
while(1) {
task2_counter++;
}
}
เนื่องจากทั้งสองทาสก์เมื่อถูกสร้างขึ้นมา ก็พร้อมที่จะทำงานทันที แต่ task2
จะเริ่มต้นทำงานก่อน เพราะมีความสำคัญสูงกว่า task1
แต่เมื่อ task2
ทำคำสั่งไปแล้วจะถูกหยุดเนื่องจากทำคำสั่ง vTaskDelay(1)
ต้องหยุดรอให้ผ่านไป 1 Tick ก่อน
เมื่อ task2
ถูกหยุดไว้ ทาส์ก task1
ที่พร้อมทำงานและมีความสำคัญในลำดับถัดมา จึงได้ทำงาน โดยทำคำสั่งเพื่อเพิ่มค่าของตัวแปร task1_counter
ทุกครั้งที่มีการวนซ้ำไปเรื่อย ๆ และค่าจะเพิ่มขึ้นต่อเนื่อง
การทำงานของ task1
จะทำต่อเนื่องไป ยกเว้นว่า มีทาสก์อื่นใดที่มีความสำคัญสูงกว่าและพร้อมจะทำงาน
ทาสก์ task1
จะถูกหยุดทำงาน เมื่อเวลาผ่านไปและเป็นเวลาที่ task2
กลับมาทำงานต่อไป เมื่อเวลาผ่านไป 1 Tick แล้ว คราวนี้ task2
จะทำคำสั่งต่อจากที่หยุดค้างไว้ และทำคำสั่งเพื่อเพิ่มค่าของตัวแปร task2_counter
ซ้ำไปเรื่อย ๆ และไม่หยุด ทาส์กอื่นที่มีความสำคัญต่ำกว่า จึงไม่มีโอกาสได้ทำงาน เพราะ task2
มีระดับความสำคัญสูงกว่าทาส์กอื่น
ดังนั้น task1
จะไม่ได้ทำงานอีก และค่าของตัวนับ task1_counter
จึงไม่เพิ่มขึ้นอีก ต่อจากนั้น
ลองมาดูตัวอย่างการจำลองการทำงาน และที่สำคัญคือ จะต้องกำหนดตำแหน่งของ Breakpoints ด้วย
เมื่อเริ่มต้นทำงาน จะมาหยุดอยู่ที่ฟังก์ชัน Task Function Entry ของ task2
มีการเพิ่มค่าของตัวแปร task2_counter
หนึ่งครั้ง (จาก 0 เป็น 1) และทำคำสั่ง vTaskDelay(1)
ซึ่งจะทำให้ task2
หยุดทำงานเพื่อรอเวลาชั่วคราว
ถ้าทำต่อไป จะเห็นว่า task1
จะได้ทำงาน โดยมาถึง Breakpoint ที่เป็น Function Entry Point ของ task1
ถ้าทำต่อไป จะเห็นว่า ค่าของตัวแปร task1_counter
เพิ่มขึ้นตามลำดับ แต่ค่าของตัวแปร task2_counter
ยังคงเดิม
แต่ถ้าเวลาผ่านไปประมาณ 17 msec จะเห็นได้ว่า task2
จะได้กลับมาทำงานต่อ
และถ้าไว้เวลาผ่านไปอีก จะเห็นค่า task2
ก็ยังทำงานต่อไป และค่าของตัวแปร task2_counter
ได้เพิ่มขึ้นอีก ในขณะที่ค่าของตัวแปร task1_counter
จะยังเดิม
จากตัวอย่างนี้ เราพอจะมองเห็นระดับความสำคัญที่ส่งผลต่อการทำงานของทาส์ก และการจัดสรรเวลาในการทำงานของทาส์ก
นอกจากนั้นเรายังได้เห็นว่า ซอฟต์แวร์ Microchip Studio และ AVR Simulator ช่วยในการศึกษาทำความเข้าใจการทำงานของโค้ดได้อย่างไร แม้ว่าจะมีข้อจำกัด เช่น การดูค่าสถานะและการเปลี่ยนแปลงเชิงเวลา เช่น Waveform Viewer ยังทำไม่ได้
ก่อนจบ ขอแนะนำคลิปการใช้งาน Microchip Studio IDE สำหรับ AVR
- ตอนที่ 1: https://youtu.be/I_pOjAEeeOw
- ตอนที่ 2: https://youtu.be/m8EkTMpA3RI
กล่าวสรุป
.
ในการใช้งาน RTOS นั้นมีประเด็นที่สำคัญคือ “เวลา” เราควรจะเข้าใจพฤติกรรมการทำงานของโค้ดที่เขียน เช่น ระยะเวลาการทำงาน หรือการเกิดขึ้นของเหตุการณ์ต่าง ๆ ในระบบ นอกจากความถูกต้องเชิงฟังก์ชันการทำงานแล้ว (Functional Correctness) ความถูกต้องในเชิงเวลา (Timing Correctness) ก็สำคัญเช่นกัน
การใช้ซอฟต์แวร์จำลองการทำงาน อาจไม่เหมือนจริง 100% แต่ก็เป็นหนึ่งวิธีการตรวจสอบความถูกต้องหรือทำความเข้าใจการทำงานของโค้ด และถ้ามีฮาร์ดแวร์จริง คือ มีบอร์ดทดลอง และมีอุปกรณ์สำหรับ In-Circuit Programmer /Debugging รวมถึงเครื่องมือวัด เช่น ออสซิลโลสโคป ก็เป็นอีกวิธีหรือตัวเลือกเสริมสำหรับการทดสอบ