Arduino-FreeRTOS for AVR (Part 3)#

บทความนี้ (ตอนที่ 3) นำเสนอตัวอย่างการเขียนโค้ดโดยใช้ FreeRTOS Library สำหรับบอร์ดไมโครคอนโทรลเลอร์ Arduino ที่ใช้ชิป 8-bit Atmel AVR (เช่น บอร์ด Uno | Nano | MEGA2560) และใช้ซอฟต์แวร์ Arduino IDE ในการเขียนโค้ด รวมถึงสาธิตการจำลองการทำงานด้วย Web-based Wokwi AVR Simulator

Keywords: Arduino, 8-bit AVR, RTOS, FreeRTOS, Wokwi AVR Simulator


การใช้งาน GDB ร่วมกับ Wokwi Simulator#

จากเนื้อหาในบทความที่แล้ว (ตอนที่ 1 | 2) เราได้เห็นตัวอย่างการเขียนโค้ด Arduino Sketch โดยใช้งานร่วมกับไลบรารี Arduino-FreeRTOS Library และได้เห็นตัวอย่างการใช้ Web-based Wokwi Simulator เพื่อจำลองการทำงานของโค้ด บันทึกและดูการเปลี่ยนของสัญญาณที่ขาเอาต์พุตของบอร์ด Arduino โดยใช้ Virtual Logic Analyzer ในขณะที่จำลองการทำงานได้เสมือนจริง

ในบทความนี้ เราจะมาเรียนรู้การใช้ GDB: The GNU Project Debugger แบบออนไลน์ร่วมกับ Wokwi Simulator เพื่อดีบักการทำงานของ Arduino Sketch (ภาษา C/C++) ในเบื้องต้น

จากโค้ดตัวอย่างในบทความที่แล้ว เป็นการสาธิตการสร้างทาสก์ (Task Creation) สำหรับ FreeRTOS จำนวน 2 ทาสก์ (T0 และ T1) ซึ่งมีระดับความสำคัญเท่ากัน และจะต้องมีการแบ่งและสลับช่วงเวลากันทำงานโดยซีพียู และจัดการโดย FreeRTOS Task Scheduler

ฟังก์ชันการทำงานของแต่ละทาสก์ จะมีการทำคำสั่ง taskYIELD() เพื่อให้มีการเปลี่ยนบริบทการทำงานของทาสก์ที่กำลังทำงานอยู่ และให้ทาสก์อื่นที่พร้อมจะทำงาน ได้มีโอกาสทำงานเป็นลำดับถัดไปโดยทันที (Task Context Switching) ดังนั้นในตัวอย่างนี้ T0 และ T1 จะสลับกันทำงานหลังจากทำคำสั่งดังกล่าว

พฤติกรรมการทำงานของทาสก์ทั้งสอง สามารถมองเห็นได้จากการเปลี่ยนแปลงของสถานะลอจิกที่ขาเอาต์พุตที่เกี่ยวข้อง (ขา D5 และ D6 สำหรับทาสก์ T0 และ T1 ตามลำดับ) และบันทึกการเปลี่ยนแปลงได้โดยใช้ Logic Analyzer ของ Wokwi Simulator

 

ตัวอย่างโค้ด

#include <Arduino_FreeRTOS.h>

#define LED0_PIN  5 // D5
#define LED1_PIN  6 // D6

void task0( void *pvParameters );
void task1( void *pvParameters );

void setup() {
  xTaskCreate( task0, "T0", 192, NULL, 
               tskIDLE_PRIORITY+1, NULL ); 
  xTaskCreate( task1, "T1", 192, NULL, 
               tskIDLE_PRIORITY+1, NULL );
  // Note the FreeRTOS task scheduler is started automatically.  
}

void loop() {}

// task entry function for T0
void task0( void *pvParameters ) { 
  DDRD |= _BV(DDD5); // output direction for PD5
  while (1) { 
    PIND |= _BV(PD5); // toggle PD5 output
    taskYIELD(); // yield the CPU to the next ready task of the same priority
  }
}

// task entry function for T1
void task1( void *pvParameters ){ // task function for T1
  DDRD |= _BV(DDD6); // output direction for PD6
  while (1) { 
    PIND |= _BV(PD6); // toggle PD6 output
    taskYIELD(); // yield the CPU to the next ready task of the same priority
  }
}

 

จากตัวอย่างโค้ดจะเห็นได้ว่า ในแต่ละรอบของ while(1){...} ภายในฟังก์ชันของทาสก์ (task0() และ task1()) จะมีการทำ 2 คำสั่ง เท่านั้น คำสั่งแรกคือ การเปลี่ยนสถานะลอจิกของขาเอาต์พุตที่เกี่ยวข้อง (การเขียนค่าบิต 1 ลงในรีจิสเตอร์ PIND จะทำให้เกิดการสลับสถานะลอจิกที่ขาเอาต์พุต) และอีกคำสั่งหนึ่งคือ taskYIELD() ดังนั้นอัตราการเปลี่ยนสถานะลอจิกที่ขาเอาต์พุตของแต่ทาสก์ จะทำได้ช้าหรือเร็ว ก็ขึ้นอยู่กับระยะเวลาที่ใช้ในการทำคำสั่งทั้งสองเป็นหลัก

 

รูป: การเขียนโค้ดและจำลองการทำงานด้วย Wokwi Simulator

 

จากตัวอย่างโค้ดนี้ เมื่อได้สร้างโปรเจกต์พร้อมโค้ดตัวอย่าง Arduino Sketch และต่อวงจรตามรูปตัวอย่างแล้ว เราจะจำลองการทำงานด้วย Wokwi Simulator และเปิดใช้งาน Online GDB Debugger for AVR ไปพร้อมกัน เราสามารถดีบักการทำงานโค้ดได้ เช่น การกำหนดตำแหน่งหยุดชั่วคราวหรือที่เรียกว่า Breakpoints ในซอร์สโค้ด การดูค่าของตัวแปร หรือรีจิสเตอร์ของซีพียู ในขณะที่จำลองการทำงาน เป็นต้น

เริ่มต้นให้คลิกเมาส์บริเวณส่วนที่เขียนโค้ด แล้วกดปุ่มFn + F1 จากนั้นให้พิมพ์ค้นหาคำว่า GDB เลือกรายการ Start GDB Session (debug build) จากนั้นจะมีการเปิดหน้า Tab ใหม่ในเบราว์เซอร์ ตามรูปตัวอย่าง

รูป: เริ่มต้น Wokwi Web GDB เข้า Simulator-based GDB Debug Session และ (gdb) เป็นสัญลักษณ์ GDB prompt เพื่อรับอินพุตสำหรับคำสั่งของ GDB โดยผู้ใช้

 


การกำหนดตำแหน่ง Breakpoints#

เมื่อเข้าสู่ GDB session การทำคำสั่งของซีพียู จะมาหยุดอยู่ที่คำสั่งแรกที่แอดเดรส 0x00000000 ซึ่งเป็น Reset Vector ของ AVR ในบริเวณหน่วยความจำที่เรียกว่า Interrupt vector table ของหน่วยความจำสำหรับโปรแกรม (Program Memory) ถ้าพิมพ์คำสั่ง where จะให้ข้อความเอาต์พุตที่ระบุว่า โปรแกรมได้ทำงานมาหยุดในตำแหน่งใด

ถัดไป เราจะลองมาเพิ่มตำแหน่งของ Breakpoints ในไฟล์ sketch.ino เช่น ให้มี 3 ตำแหน่งดังนี้

  • ตำแหน่งที่เป็นจุดเริ่มต้นของฟังก์ชัน setup() ของ Arduino Sketch ในไฟล์ sketch.ino
  • บรรทัดที่ 22 ในไฟล์ sketch.ino ซึ่งเป็นการเรียกใช้ฟังก์ชัน taskYIELD() ในฟังก์ชันของทาสก์ T0
  • บรรทัดที่ 31 ในไฟล์ sketch.ino ซึ่งเป็นการเรียกใช้ฟังก์ชัน taskYIELD() ในฟังก์ชันของทาสก์ T1

คำสั่งสำหรับ GDB เพื่อกำหนดตำแหน่ง Breakpoints ตามที่กล่าวไป มีดังนี้

break sketch.ino:setup()
break sketch.ino:22
break sketch.ino:31

 

ถ้าต้องการทราบว่า มีการกำหนด Breakpoints ไว้อย่างไรบ้าง ให้ทำคำสั่งต่อไปนี้

info breakpoints

รูป: ตัวอย่างการทำคำสั่งเพื่อเพิ่มและตรวจสอบตำแหน่ง Breakpoints ในไฟล์ sketch.ino

 


การแสดงค่าตัวเลขในรีจิสเตอร์ของซีพียู#

ถ้าต้องการดูค่าของตัวแปรในโค้ดหรือรีจิสเตอร์ของซีพียูในขณะนั้น ก็สามารถใช้คำสั่ง print แล้วตามด้วยชื่อตัวแปรหรือรีจิสเตอร์

การทำงานของโค้ดที่เขียนด้วย Arduino API จะมีการใช้วงจร Timer0 เป็นตัวนับตามจังหวะเพื่อใช้ในการอ่านค่าเวลาของระบบ เช่น คำสั่ง millis() และ micros()

สำหรับ Arduino Uno หรือ Nano ซีพียูจะทำงานด้วยความถี่ F_CPU เท่ากับ 16MHz และได้ตั้งค่าตัวหารความถี่ (Prescaler) ไว้เท่ากับ 64 สำหรับตัวนับ Timer0

ดังนั้นตัวนับนี้ จะมีค่าเพิ่มขึ้นทีละหนึ่ง ทุก ๆ 4 ไมโครวินาที (= 64/16 MHz = 4 us per tick) และมีความละเอียดในการอ่านค่า (Timer Resolution) เท่ากับ 4 ไมโครวินาที ดังนั้น ค่าที่ได้กลับมา (Retun Value) จากการทำคำสั่ง micros() ของ Arduino API จะเป็นเลขจำนวนเต็มและหาร 4 ลงตัว

เนื่องจากตัวนับ TCNT0 ของ Timer0 มีขนาด 8 บิต ดังนั้นจะมีค่าในช่วง 0..255 แล้วไป เมื่อเพิ่มขึ้นจาก 0 ถึงค่าสูงสุดแล้ว จะเริ่มนับที่ค่า 0 ใหม่ และทำให้เกิด Timer0 Overflow

เหตุการณ์อินเทอร์รัพท์ที่เกี่ยวข้อง จะเกิดขึ้นทุก ๆ 1024 us (= 256 × 4 us) รวมถึงการเพิ่มค่าของตัวแปร ที่ใช้ระบุว่า มีการนับครบรอบไปแล้วกี่ครั้ง

การอ่านค่าเวลาของระบบในหน่วยเป็นไมโครวินาที อาจสร้างฟังก์ชันเป็นแนวทางได้ดังนี้ (ดูโค้ดจริงที่ใช้กับ Arduino ได้ในไฟล์ wiring.c) โดยที่ตัวแปร timer0_overflow_count หมายถึง ตัวแปรที่ใช้นับจำนวนครั้งที่เกิด Timer0 Overflow

unsigned long micros() {
   ((timer0_overflow_count << 8) + TCNT0) * (64/16);
}

 

หากต้องการดูค่าในรีจิสเตอร์ของ AVR เช่น TCNT0 | DDRD | PORTD ใน GDB ก็ให้ทำคำสั่งดังนี้ ซึ่งจะแสดงเป็นตัวเลขฐานสิบ (/d Decimal)

print/d TCNT0
print/d DDRD
print/d PORTD

แต่ถ้าต้องการแสดงเป็นตัวเลขในฐานสิบหก (/x Hexademical) ก็มีตัวอย่างดังนี้

print/x TCNT0
print/x DDRD
print/x PORTD

หรือเป็นตัวเลขในฐานสอง ('/t` Binary)

print/t TCNT0
print/t DDRD
print/t PORTD

รูป: ตัวอย่างการทำคำสั่งเพื่อดูค่าของรีจิสเตอร์ TCNT0 และทำคำสั่ง continue เพื่อรันโค้ดจนกว่าจะไปหยุดที่ตำแหน่ง Breakpoint ถัดไป

ถ้าทำคำสั่ง continue เป็นการรันโค้ดต่อไป จนกว่าจะหยุดเมื่อพบตำแหน่ง Breakpoint หากต้องการดูซอร์สโค้ดในขณะนั้นในบริเวณที่มีตำแหน่งของ Breakpoint ก็ให้ทำคำสั่งต่อไปนี้ layout src (Display the source window) หรือ ทำคำสั่ง layout asm เพื่อดูโค้ดที่มีการคอมไพล์ให้เป็นภาษา AVR-Assembly แล้ว

ให้สังเกตบรรทัดในโค้ดที่มีแถบสี Highlight และสัญลักษณ์ +B> ซึ่งก็คือ ตำแหน่งของ Breakpoint ในขณะนั้น (รันโค้ดแล้วมาหยุดที่ตำแหน่งดังกล่าว)

รูป: ตัวอย่างการแสดงตำแหน่งของ Breakpoint ในไฟล์ซอร์สโค้ด

 

หากต้องการให้มีการแสดงค่าของรีจิสเตอร์โดยอัตโนมัติ ทุกครั้งเมื่อหยุดการทำงานชั่วคราวในตำแหน่ง Breakpoint ใด ๆ ก็ให้ใช้คำสั่ง display ตามรูปแบบต่อไปนี้ เช่น แสดงค่าของ TCNT0 เป็นเลขจำนวนเต็ม (/u Unsigned Integer)

display/u TCNT0

 

รูป: การแสดงค่าตัวเลขของรีจิสเตอร์ TCNT0 ในขณะนั้น

 

หากต้องการรันคำสั่งถัดไป โดยเข้าไปสู่ภายในของฟังก์ชันเมื่ออยู่ในบรรทัดที่มีการเรียกใช้ฟังก์ชัน (Function Call) ให้ทำคำสั่งดังนี้

stepi

ถ้าทำคำสั่งในบรรทัดถัดไป ก็ให้ใช้คำสั่งดังนี้

next

ในกรณีที่ได้เข้าไปทำคำสั่งในฟังก์ชัน แล้วจะออกจากฟังก์ชันดังกล่าว (Return from function) ก็ให้ทำคำสั่งดังนี้

finish

รูป: การรันคำสั่งเข้าไปในฟังก์ชัน vPortYield() ซึ่งก็คือ taskYIELD() เป็นฟังก์ชันเดียวกัน

จากตัวอย่างการดีบัก จะเห็นได้ว่า การทำงานของฟังก์ชัน vPortYield() (ดูโค้ดที่เกี่ยวข้องได้ในไฟล์ port.c) ประกอบด้วยการเรียกใช้ 3 ฟังก์ชัน ตามลำดับดังนี้

  • portSAVE_CONTEXT() เป็นการบันทึกบริบทการทำงานของทาสก์ (Task Context) ในขณะนั้น ไปเก็บไว้ในหน่วยความจำสำหรับ Task Stack
    • มีโค้ด (Inline AVR Assembly) ที่เกี่ยวข้องอยู่ในไฟล์ port.c
  • vTaskSwitchContext() เป็นการเลือกทาสก์ถัดไปที่พร้อมจะทำงานและมีระดับความสำคัญสูงกว่าทาสก์อื่นเพื่อรับช่วงต่อ
    • มีโค้ดที่เกี่ยวข้องอยู่ในไฟล์ tasks.c
  • portRESTORE_CONTEXT() เป็นการนำบริบทการทำงานของทาสก์ถัดไปที่ได้จาก Task Scheduler มาใส่ลงในรีจิสเตอร์ต่าง ๆ ของซีพียู เพื่อให้ทำงานในลำดับถัดไป
    • มีโค้ด (Inline AVR Assembly) ที่เกี่ยวข้องอยู่ในไฟล์ port.c

หากต้องการจะเริ่มต้นใหม่ และย้อนกลับไปเริ่มที่ Reset Vector โดยการกำหนดค่า PC (Program Counter) หรือ $pc ให้เป็น 0x0 ให้ทำคำสั่งต่อไปนี้

set $pc=0x0

 


การจับเวลาระหว่างการเกิด Breakpoints#

ถ้าหากต้องการจับเวลาการทำงานของโค้ดระหว่างการเกิด Breakpoint ในแต่ละครั้ง จะมีแนวทางอย่างไร ?

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

จากการทดลองด้วยวิธีนี้พบว่า คำสั่ง taskYIELD() จะใช้เวลาเท่ากับ 4 ticks ของ Timer0 หรือเท่ากับ 16 us (= 4 ticks × 4 us per Timer0 tick) และใกล้เคียงกับค่าที่วัดได้จากการจับเวลาของสัญญาณเอาต์พุตด้วย Virtual Logic Analyzer ในรูปคลื่นสัญญาณต่อไปนี้ (ได้ค่า ~16.5 us)

รูป: ระยะเวลาในการเปลี่ยนบริบทจากทาสก์หนึ่งไปสู่อีกทาสก์หนึ่ง โดยดูจากการเปลี่ยนสถานะลอจิกของสัญญาณเอาต์พุต ของแต่ละทาสก์ (จาก T0 ไปสู่ T1 วัดค่าได้ ~16.5 us)

รูป: ระยะเวลาในการเปลี่ยนบริบทจากทาสก์หนึ่งไปสู่อีกทาสก์หนึ่ง โดยดูจากการเปลี่ยนสถานะลอจิกของสัญญาณเอาต์พุตของแต่ละทาสก์ (จาก T1 ไปสู่ T0 วัดค่าได้ ~17.1 us )

การสลับสถานะลอจิกที่ขาเอาต์พุตของแต่ละทาสก์ จะใช้เวลาเท่ากับ 9 ticks หรือ 36 us และนำไปเปรียบเทียบกับการวัดความกว้างของพัลส์ที่ขาเอาต์พุต ซึ่งได้ระยะเวลา ~33.68 us จากรูปคลื่นสัญญาณต่อไปนี้

รูป: การวัดความกว้างของพัลส์ (ช่วงที่เป็น High) ของสัญญาณเอาต์พุตที่เกิดจากการทำงานของทาสก์ T0

 


กล่าวสรุป#

ในบทความนี้ เราได้เห็นวิธีการใช้งาน Online GDB Debugger ร่วมกับ Wokwi Simulator ในการตรวจสอบ (ดีบัก) และศึกษาพฤติกรรมการทำงานของโค้ด Arduin Sketch ที่มีการใช้คำสั่งต่าง ๆ ของ Arduino-FreeRTOS Library

 


This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.

Created: 2021-12-27 | Last Updated: 2022-01-25