Microchip Studio: Simulating Arduino FreeRTOS Code (Part 2)

<rawat.s>
5 min readJan 17, 2021

--

.

จากบทความที่แล้ว เราได้เรียนรู้ขั้นตอนการใช้งาน 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

รูป: การใช้ Microchip Studio จำลองการทำงาน

ถ้าจำลองการทำงานตามที่กล่าวไป จะได้ตัวเลข 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)
รูป: แสดงสถานะการทำงานของ Processor

ระยะเวลาในการเกิดอินเทอร์รัพท์จาก 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

รูป: การกำหนดตำแหน่งของ Breakpoint ใน ISR (WDT_vect)

ลองมาดูผลการจำลองการทำงาน จะเห็นว่า ค่าของ 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
รูป: ตัวอย่างสถานะการทำงานใน Processor Status Windows

การตั้งค่า 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 2
void 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 ด้วย

รูป: ตัวอย่างการกำหนดตำแหน่งของ Breakpoints ในโค้ด (มีวงกลมสีแดง)

เมื่อเริ่มต้นทำงาน จะมาหยุดอยู่ที่ฟังก์ชัน Task Function Entry ของ task2 มีการเพิ่มค่าของตัวแปร task2_counter หนึ่งครั้ง (จาก 0 เป็น 1) และทำคำสั่ง vTaskDelay(1) ซึ่งจะทำให้ task2 หยุดทำงานเพื่อรอเวลาชั่วคราว

รูป: ฟังก์ชันของ task2 ได้เริ่มทำงานก่อน

ถ้าทำต่อไป จะเห็นว่า task1 จะได้ทำงาน โดยมาถึง Breakpoint ที่เป็น Function Entry Point ของ task1

รูป: ฟังก์ชันของ task1 เริ่มทำงาน

ถ้าทำต่อไป จะเห็นว่า ค่าของตัวแปร task1_counter เพิ่มขึ้นตามลำดับ แต่ค่าของตัวแปร task2_counter ยังคงเดิม

รูป: task1 ยังคงได้ทำงานต่อไป

แต่ถ้าเวลาผ่านไปประมาณ 17 msec จะเห็นได้ว่า task2 จะได้กลับมาทำงานต่อ

รูป: task2 ได้กลับมาทำงานต่อ

และถ้าไว้เวลาผ่านไปอีก จะเห็นค่า task2 ก็ยังทำงานต่อไป และค่าของตัวแปร task2_counter ได้เพิ่มขึ้นอีก ในขณะที่ค่าของตัวแปร task1_counter จะยังเดิม

รูป: task2 ได้ทำงานต่อเนื่องไป

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

นอกจากนั้นเรายังได้เห็นว่า ซอฟต์แวร์ Microchip Studio และ AVR Simulator ช่วยในการศึกษาทำความเข้าใจการทำงานของโค้ดได้อย่างไร แม้ว่าจะมีข้อจำกัด เช่น การดูค่าสถานะและการเปลี่ยนแปลงเชิงเวลา เช่น Waveform Viewer ยังทำไม่ได้

ก่อนจบ ขอแนะนำคลิปการใช้งาน Microchip Studio IDE สำหรับ AVR

กล่าวสรุป

.

ในการใช้งาน RTOS นั้นมีประเด็นที่สำคัญคือ “เวลา” เราควรจะเข้าใจพฤติกรรมการทำงานของโค้ดที่เขียน เช่น ระยะเวลาการทำงาน หรือการเกิดขึ้นของเหตุการณ์ต่าง ๆ ในระบบ นอกจากความถูกต้องเชิงฟังก์ชันการทำงานแล้ว (Functional Correctness) ความถูกต้องในเชิงเวลา (Timing Correctness) ก็สำคัญเช่นกัน

การใช้ซอฟต์แวร์จำลองการทำงาน อาจไม่เหมือนจริง 100% แต่ก็เป็นหนึ่งวิธีการตรวจสอบความถูกต้องหรือทำความเข้าใจการทำงานของโค้ด และถ้ามีฮาร์ดแวร์จริง คือ มีบอร์ดทดลอง และมีอุปกรณ์สำหรับ In-Circuit Programmer /Debugging รวมถึงเครื่องมือวัด เช่น ออสซิลโลสโคป ก็เป็นอีกวิธีหรือตัวเลือกเสริมสำหรับการทดสอบ

--

--