การทดสอบหาระยะเวลาในการทำคำสั่งสำหรับ Arduino ESP32#

Keywords: Arduino, ESP32, ESP32 Performance Measurement, Code Instrumentation


การเขียนโปรแกรมด้วย Arduino ESP32 Core#

บอร์ดไมโครคอนโทรลเลอร์อย่างเช่น ESP32 สามารถนำมาใช้และเขียนโปรแกรมได้โดยใช้คำสั่งของ Arduino API บทความนี้นำเสนอแนวทางการทดลองและตัวอย่างโค้ดเพื่อดูว่า การทำคำสั่งบางคำสั่งสำหรับไมโครคอนโทรลเลอร์ดังกล่าว จะใช้เวลาเท่าไหร่ การทดลองในลักษณะนี้เรียกว่า Code Instrumentation ซึ่งจะต้องมีการแทรกคำสั่ง เพื่อใช้ในการจับเวลา เช่น ก่อนเริ่มทำคำสั่งและหลังจากการทำคำสั่งแล้ว แล้วนำมาคำนวณเป็นระยะเวลาในการทำคำสั่งดังกล่าว การทำคำสั่งควรจะต้องทำหลายครั้งแล้วนำมาหาค่าเฉลี่ย

สำหรับการเขียนโค้ด ให้ติดตั้ง Arduino-ESP32 Core by Espressif Systems (v2.0.14 based on ESP-IDF v4.4.6) สำหรับ Arduino IDE v2.1.0 (หรือใหม่กว่า) แต่ถ้าเป็นเวอร์ชันใหม่ (ในขณะที่เขียนบทความนี้) ก็เป็นเวอร์ชัน Arduino ESP32 Core v3.0.0)

รูป: การติดตั้ง Arduino-ESP32 Core สำหรับ Arduino IDE v2.1.0 (Windows)

การอ่านค่าเวลาของระบบสำหรับ Arduino ESP32 ก็มีมากกว่าหนึ่งวิธี

  1. อ่านค่าเวลาที่เป็นตัวเลขจำนวนเต็ม มีหน่วยเป็นไมโครวินาที
  2. อ่านค่าตัวเลขจำนวนเต็มจากรีจิสเตอร์ตัวนับของซีพียูตามจำนวนไซเคิล (CPU Cycle Count)

การอ่านค่าเวลาของระบบ ก็สามารถใช้คำสั่ง micros() ของ Arduino API ซึ่งจะได้ค่าเป็นตัวเลขจำนวนเต็ม มีหน่วยเวลาเป็นไมโครวินาที (usec) หรือ คำสั่ง esp_timer_get_time() ที่ให้ค่าเป็นเลขจำนวนเต็มและมีหน่วยเป็นไมโครวินาที (ใช้ชนิดข้อมูลเป็น uint64_t)

ถ้าจะอ่านค่าตัวนับไซเคิลของซีพียู ก็สามารถใช้คำสั่ง esp_cpu_get_ccount() หรือ esp_cpu_get_cycle_count() ของ ESP-IDF หรือคำสั่ง ESP.getCycleCount() ซึ่งเป็นส่วนหนึ่งของคลาส ESP แล้วจะนำมาแปลงเป็นค่าเวลาก็ได้ ซึ่งขึ้นอยู่กับความถี่ของซีพียู (เช่น 240MHz)

ถ้าจะใช้คำสั่งแบบ Inline Assembly เพื่ออ่านค่าจากรีจิสเตอร์ตัวนับไซเคิลของซีพียู ก็มีตัวอย่างดังนี้

  uint32_t c0, c1;

  // Read cycle count register.
  asm volatile("rsr %0, ccount" : "=a"(c0));
  _NOP(); // 1 CPU cycle
  _NOP(); // 1 CPU cycle
  _NOP(); // 1 CPU cycle
  _NOP(); // 1 CPU cycle
  // Read cycle count register.
  asm volatile( "rsr %0, ccount" : "=a"(c1) );

  // Compute cycle count difference.
  uint32_t cyc_diff = (c1-c0-1);
  Serial.printf( "Cycle difference: %u\n", cyc_diff );
  // Convert cycle count difference to time duration in nanoseconds.
  Serial.printf( "Time difference: %.3lf nsec\n",
                 ((double)1000*cyc_diff)/ESP.getCpuFreqMHz() );

หรือใช้คำสั่งต่อไปนี้ก็ได้เช่นกัน (แต่ละซีพียูของ ESP32 มีตัวนับ CPU Cycle Count แยกกัน)

  • esp_cpu_get_ccount() ใช้กับ ESP-IDF v4.x
  • esp_cpu_get_cycle_count() ใช้กับ ESP-IDF v5.x หรือ ใหม่กว่า
  uint32_t c0, c1;

  // Read cycle count register.
  c0 = esp_cpu_get_ccount();
  _NOP(); // 1 CPU cycle
  _NOP(); // 1 CPU cycle
  _NOP(); // 1 CPU cycle
  _NOP(); // 1 CPU cycle
  // Read cycle count register.
  c1 = esp_cpu_get_ccount();

  // Compute cycle count difference.
  uint32_t cyc_diff = (c1-c0-1);
  Serial.printf( "Cycle difference: %u\n", cyc_diff );
  // Convert cycle count difference to time duration in nanoseconds.
  Serial.printf( "Time difference: %.3lf nsec\n",
                 ((double)1000*cyc_diff)/ESP.getCpuFreqMHz() );

การทำงานของโค้ดตัวอย่างนี้ จะได้ค่าเท่ากับ 4 ไซเคิล ซึ่งเท่ากับจำนวนครั้งที่มีการทำคำสั่ง NOP (No Operation) หรือคิดเป็น 4 Cycles @240MHz = 16.667 นาโนวินาที

 


โค้ดตัวอย่างที่ 1#

โค้ดตัวอย่างแรกสาธิตการทดลอง เพื่อหาระยะเวลาในการทำคำสั่งของฟังก์ชัน digitalWrite() ของ Arduino API ที่ใช้เพื่อกำหนดค่าลอจิกให้ขา GPIO ที่เป็นเอาต์พุต

const int LED_PIN = 5; // GPIO-5

void setup() {
  Serial.begin( 115200 );
  pinMode( LED_PIN, OUTPUT );
}

void loop() {
  uint32_t c0, c1;

  asm volatile( "rsr %0, ccount" : "=a"(c0) );
  digitalWrite( LED_PIN, HIGH ); // Output high to GPIO-5
  digitalWrite( LED_PIN, LOW  ); // Output low to GPIO-5
  asm volatile( "rsr %0, ccount" : "=a"(c1) );

  // Compute the cycle count difference.
  uint32_t cyc_diff = (c1-c0-1);
  Serial.printf( "Cycle difference    : %lu\n", cyc_diff );
  double t_usec = ((double)cyc_diff)/ESP.getCpuFreqMHz();
  Serial.printf( "Time difference     : %.3lf usec\n",   t_usec );
  Serial.printf( "digitalWrite() call : %.3lf usec\n\n", t_usec/2 );

  // Wait for 5 seconds before repeating the loop.
  delay(5000);
}

 

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

รูป: ตัวอย่างข้อความเอาต์พุตใน Arduino Serial Monitor

 


โค้ดตัวอย่างที่ 2#

โค้ดตัวอย่างนี้สาธิตการจับเวลาที่ใช้ในการทำคำสั่ง for loop โดยกำหนดให้ภายในลูปไม่มีคำสั่งใด ๆ ดังนั้นจึงเป็นการคำสั่งเพื่อเพิ่มค่าของตัวแปรที่ใช้เป็นตัวนับ (ตั้งชื่อตัวแปรภายในเป็น i ที่มีชนิดข้อมูลเป็น counter_t) ตัวนับจะเริ่มต้นจาก 0 และเพิ่มขึ้นครั้งละหนึ่งตามจำนวนรอบในการทำซ้ำ (NUM_ITERS) และมีการตรวจสอบการทำซ้ำตามเงื่อนไขดังกล่าวในแต่ละรอบ ไม่มีการคำสั่งอื่นใด ๆ

ชนิดข้อมูล counter_t ได้ถูกกำหนดด้วยคำสั่ง typedef ให้เป็น uint32_t (หรือ unsigned int) ที่มีขนาด 32 บิต และสำหรับการเปรียบเทียบ ก็อาจเลือกใช้ float หรือ double แทน uint32_t สำหรับตัวแปรที่ใช้เป็นตัวนับสำหรับ for loop

  for (volatile counter_t i=0; i < NUM_ITERS; i++) {
     // empty
  }

ก่อนเริ่มต้นการทำงานของประโยคคำสั่ง for จะมีการปิดไม่ให้เกิดอินเทอร์รัพท์ในระบบด้วยคำสั่ง portDISABLE_INTERRUPTS() เพื่อไม่ให้ถูกขัดจังหวะในการทำงาน และเมื่อจบการทำงานแล้ว จึงเปิดการทำงานของอินเทอร์รัพท์ในระบบตามปรกติด้วยคำสั่ง portENABLE_INTERRUPTS()

typedef uint32_t counter_t;
// typedef float counter_t;
// typedef double counter_t;

const int LED_PIN = 5;
const uint32_t NUM_ITERS = 100000UL;

#define SEP_LINE "=============================="

void setup() {
  Serial.begin( 115200 );
  Serial.flush();
  delay( 100 );
  Serial.println( "\n\n\n" );
  pinMode( LED_PIN, OUTPUT );
  digitalWrite( LED_PIN, LOW );
  Serial.printf( "ESP-IDF %s\n", ESP.getSdkVersion() );
}

void loop() {
  uint32_t t_start, t_exec, t_cycles;

  // Save the start time (in usec).
  t_start  = micros();
  // Save the current CPU cycle count.
  t_cycles = ESP.getCycleCount();
  // Output high to the LED pin.
  digitalWrite( LED_PIN, HIGH );
  // Disable interrupts.
  portDISABLE_INTERRUPTS();
  for (volatile counter_t i=0; i < NUM_ITERS; i++) {
     // empty
  }
  // Enable interrupts.
  portENABLE_INTERRUPTS();
  // Output low to the LED pin.
  digitalWrite( LED_PIN, LOW );

  // Compute the CPU cycles executed.
  t_cycles = ESP.getCycleCount() - t_cycles;
  // Compute the execution time (in usec).
  t_exec   = micros() - t_start;

  // Get the CPU frequency (in MHz).
  uint32_t freq = ESP.getCpuFreqMHz();
  // Show measurement results.
  Serial.println( "\n\n" SEP_LINE  SEP_LINE );
  Serial.printf( "CPU Freq. [MHz]  : %lu\n", freq );
  Serial.printf( "Number of loops  : %lu\n", NUM_ITERS );
  Serial.printf( "Exec time [usec] : %lu\n", t_exec );
  Serial.printf( "Exec time [usec] : %.4lf per loop\n", 
                 ((double)t_exec)/NUM_ITERS );
  Serial.printf( "Exec time [cyc.] : %lu\n", t_cycles );
  Serial.printf( "Exec time [cyc.] : %lu per loop\n", 
                 t_cycles/NUM_ITERS );
  Serial.printf( "Exec time [usec] : %.4lf per loop\n", 
                 ((double)(t_cycles/freq))/NUM_ITERS );

  // Wait for 5 seconds before repeating the loop.
  delay(5000);
}

 

จากการทดลอง ซึ่งดูได้จากตัวอย่างข้อความเอาต์พุตใน Arduino Serial Monitor จะเห็นได้ว่า

  • การทำซ้ำด้วยประโยคคำสั่ง for จำนวน 100,000 ครั้ง ใช้เวลาทั้งหมด 7933 usec (หรือ 7.933 msec)
  • หากดูจำนวนไซเคิลของซีพียูสำหรับการวนซ้ำ จะได้ 1,903,745 ไซเคิล (ซีพียูใช้ความถี่ 240MHz)
  • หากหารเฉลี่ย จะได้ 0.0793 usec (79.3 nsec) ต่อหนึ่งรอบการทำซ้ำ ค่านี้จึงถือว่าเป็นส่วนที่เรียกว่า Loop Overhead (โดยประมาณ)
  • ตัวแปรที่ใช้เป็นตัวนับ ถ้าเปลี่ยนจาก uint32_t มาเป็น double การทำซ้ำแต่ละรอบ จะใช้เวลามากขึ้น ใช้เวลา 0.4506 usec ต่อหนึ่งรอบการทำซ้ำ เพราะว่า การประมวลผลของซีพียูด้วย 32-bit Floating-Point Unit (FPU) จะใช้เวลามากกว่า 32-bit Integer Arithmetic

รูป: ตัวอย่างข้อความเอาต์พุตจากการทดลองด้วยบอร์ด ESP32 และเลือกใช้ uint32_t สำหรับชนิดข้อมูล customer_t

รูป: ตัวอย่างข้อความเอาต์พุต ในกรณีที่ได้เลือกใช้ double สำหรับชนิดข้อมูล customer_t

 


โค้ดตัวอย่างที่ 3#

โค้ดตัวอย่างถัดไป มีการทำคำสั่งด้วยประโยคคำสั่ง for จำนวน 1,000,000 ครั้ง ซึ่งภายในประโยคคำสั่งดังกล่าว จะมีการเรียกใช้คำสั่ง digitalWrite() สองครั้ง เพื่อกำหนดสถานะลอจิกของขา GPIO ให้เป็น High และ Low ตามลำดับ (เลือกใช้ขา GPIO-5 สำหรับเอาต์พุต อ้างอิงด้วยชื่อตัวแปร LED_PIN)

เนื่องจากมีการทำซ้ำที่ต้องใช้เวลานานและมีการปิดอินเทอร์รัพท์ของระบบในช่วงเวลาดังกล่าว ในกรณีนี้ อาจทำให้ ESP32 Watchdog Timer (WDT) ทำงานและรีเซตการทำงานของระบบ ดังนั้นจึงต้องมีการปิดการทำงานของ WDT ก่อนการทำซ้ำ ดังนั้นจึงมีการสร้างและเรียกใช้ฟังก์ชัน disableWDT() ในโค้ดตัวอย่างนี้

#include "soc/timer_group_struct.h"
#include "soc/timer_group_reg.h"

typedef uint32_t counter_t;

const int LED_PIN = 5;
const uint32_t NUM_ITERS = 1000000UL;

#define SEP_LINE "=============================="

void disableWDT() {
  // Disable the interrupt watchdog timer for timer groups: 1 and 0
#if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0)
  TIMERG1.wdtwprotect.wdt_wkey = TIMG_WDT_WKEY_VALUE; // Unlock timer config
  TIMERG1.wdtfeed.val = 1; // Reset feed count.
  TIMERG1.wdtconfig0.wdt_en = 0; // Disable timer.
  TIMERG1.wdtwprotect.wdt_wkey = 0; // Lock timer config.

  TIMERG0.wdtwprotect.wdt_wkey = TIMG_WDT_WKEY_VALUE;
  TIMERG0.wdtfeed.val = 1;
  TIMERG0.wdtconfig0.wdt_en = 0;
  TIMERG0.wdtwprotect.wdt_wkey = 0;
#else
  TIMERG1.wdt_wprotect = TIMG_WDT_WKEY_VALUE; // Unlock timer config
  TIMERG1.wdt_feed = 1; // Reset feed count.
  TIMERG1.wdt_config0.en = 0; // Disable timer.
  TIMERG1.wdt_wprotect = 0; // Lock timer config.

  TIMERG0.wdt_wprotect = TIMG_WDT_WKEY_VALUE;
  TIMERG0.wdt_feed = 1;
  TIMERG0.wdt_config0.en = 0;
  TIMERG0.wdt_wprotect = 0;
#endif
}

void setup() {
  Serial.begin(115200);
  Serial.flush();
  delay(100);
  Serial.println("\n\n\n");
  pinMode( LED_PIN, OUTPUT );
  digitalWrite( LED_PIN, LOW );
  // Disable the Watchdog Timer.
  disableWDT();
}

void loop() {
  uint32_t t_start, t_exec, t_cycles;

  t_start  = micros();
  t_cycles = ESP.getCycleCount();
  portDISABLE_INTERRUPTS(); // Disable interrupts.
  for (counter_t i=0; i < NUM_ITERS; i++){
    digitalWrite( LED_PIN, HIGH ); // ~0.324usec
    digitalWrite( LED_PIN, LOW );
  }
  portENABLE_INTERRUPTS(); // Re-enable interrupts.
  t_exec   = micros() - t_start;
  t_cycles = ESP.getCycleCount() - t_cycles;

  uint32_t freq = ESP.getCpuFreqMHz();
  Serial.println( "\n\n" SEP_LINE  SEP_LINE );
  Serial.printf( "CPU Freq. [MHz]  : %lu\n", freq );
  Serial.printf( "Number of loops  : %lu\n", NUM_ITERS );
  Serial.printf( "Exec time [usec] : %lu\n", t_exec );
  Serial.printf( "Exec time [usec] : %.4lf per loop\n", 
                 ((double)t_exec)/NUM_ITERS );
  Serial.printf( "Exec time [cyc.] : %lu\n", t_cycles );
  Serial.printf( "Exec time [cyc.] : %lu per loop\n", 
                 t_cycles/NUM_ITERS );
  Serial.printf("Exec time [usec] : %.4lf per loop\n", 
                 ((double)(t_cycles/freq))/NUM_ITERS );
  // Wait for 5 seconds before repeating the loop.
  delay(5000);
}

 

ตัวอย่างข้อความเอาต์พุตจากการทดลอง มีดังนี้

รูป: ตัวอย่างข้อความเอาต์พุต

รูป: การวัดสัญญาณที่ขาเอาต์พุตด้วยออสซิลโลสโคปดิจิทัล

จากการทดลองและวัดสัญญาณที่ขาเอาต์พุต โดยวัดความกว้างของพัลส์ จะเห็นได้ว่า การทำคำสั่งของฟังก์ชัน digitalWrite() จะได้ประมาณ 324 นาโนวินาที

 


โค้ดตัวอย่างที่ 4#

โค้ดตัวอย่างถัดไปเป็นการหาระยะเวลาในการทำคำสั่ง analogRead() ของ Arduino API ซึ่งเป็นฟังก์ชันที่ใช้สำหรับการอ่านค่าแอนะล็อกอินพุต

#include "soc/timer_group_struct.h"
#include "soc/timer_group_reg.h"

typedef uint32_t counter_t;
// Note: value range for 32-bit unsigned int: 0 .. 4,294,967,295

const int LED_PIN = 5;
const uint32_t NUM_ITERS = 100000UL;

#define SEP_LINE "=============================="

void disableWDT() {
  // Disable the interrupt watchdog timer for timer groups: 1 and 0
#if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0)
  TIMERG1.wdtwprotect.wdt_wkey = TIMG_WDT_WKEY_VALUE; // Unlock timer config
  TIMERG1.wdtfeed.val = 1; // Reset feed count.
  TIMERG1.wdtconfig0.wdt_en = 0; // Disable timer.
  TIMERG1.wdtwprotect.wdt_wkey = 0; // Lock timer config.

  TIMERG0.wdtwprotect.wdt_wkey = TIMG_WDT_WKEY_VALUE;
  TIMERG0.wdtfeed.val = 1;
  TIMERG0.wdtconfig0.wdt_en = 0;
  TIMERG0.wdtwprotect.wdt_wkey = 0;
#else
  TIMERG1.wdt_wprotect = TIMG_WDT_WKEY_VALUE; // Unlock timer config
  TIMERG1.wdt_feed = 1; // Reset feed count.
  TIMERG1.wdt_config0.en = 0; // Disable timer.
  TIMERG1.wdt_wprotect = 0; // Lock timer config.

  TIMERG0.wdt_wprotect = TIMG_WDT_WKEY_VALUE;
  TIMERG0.wdt_feed = 1;
  TIMERG0.wdt_config0.en = 0;
  TIMERG0.wdt_wprotect = 0;
#endif
}

void setup() {
  Serial.begin(115200);
  Serial.flush();
  delay(100);
  Serial.println("\n\n\n");
  pinMode( LED_PIN, OUTPUT );
  digitalWrite( LED_PIN, LOW );
  disableWDT(); // Disable the Watchdog Timer.
}

void loop() {
  uint32_t t_start, t_exec, t_cycles;
  t_start  = micros();
  t_cycles = ESP.getCycleCount();
  digitalWrite( LED_PIN, HIGH );
  portDISABLE_INTERRUPTS(); // Disable interrupts.
  for (counter_t i=0; i < NUM_ITERS; i++) {
    (void)analogRead(A0); // Read analog input at A0 pin.
  }
  portENABLE_INTERRUPTS(); // Re-enable interrupts.
  digitalWrite( LED_PIN, LOW );
  t_exec   = micros() - t_start;
  t_cycles = ESP.getCycleCount() - t_cycles;

  uint32_t freq = ESP.getCpuFreqMHz();
  Serial.println( "\n\n" SEP_LINE  SEP_LINE );
  Serial.printf( "CPU Freq. [MHz]  : %lu\n", freq );
  Serial.printf( "Number of loops  : %lu\n", NUM_ITERS );
  Serial.printf( "Exec time [usec] : %lu\n", t_exec );
  Serial.printf( "Exec time [usec] : %.4lf per loop\n", 
                ((double)t_exec)/NUM_ITERS );
  Serial.printf( "Exec time [cyc.] : %lu\n", t_cycles );
  Serial.printf( "Exec time [cyc.] : %lu per loop\n", 
                t_cycles/NUM_ITERS );
  Serial.printf( "Exec time [usec] : %.4lf per loop\n", 
                ((double)clockCyclesToMicroseconds(t_cycles))/NUM_ITERS);
  // Wait for 5 seconds before repeating the loop.
  delay(5000);
}

 

จากตัวอย่างข้อความเอาต์พุต จะใช้เวลาในการคำสั่ง analogRead() ต่อหนึ่งครั้ง ประมาณ 81.08 usec (รวม Loop Overhead ด้วย)

รูป: ตัวอย่างข้อความเอาต์พุต

 

ลองเปรียบเทียบกับรูปแบบการเขียนโค้ดอีกแนวทางหนึ่ง เพื่อหาระยะเวลาในการทำคำสั่ง analogRead() ได้เช่นกัน

const uint32_t NUM_ITERS = 100000UL;

void setup() {
  Serial.begin(115200);
  Serial.flush();
  delay(100);
  Serial.println("\n\n\n");
  // Set analog read resolution to 12 bits
  analogReadResolution(12); 
}

void loop() {
  uint32_t c0, c1, cyc_diff;

  cyc_diff = 0;
  for (uint32_t i=0; i < NUM_ITERS; i++) {
    portDISABLE_INTERRUPTS(); // Disable interrupts.
    asm volatile("rsr %0, ccount" : "=a"(c0));
    (void)analogRead(A0);     // Read analog input at A0 pin.
    asm volatile( "rsr %0, ccount" : "=a"(c1) );
    portENABLE_INTERRUPTS();  // Re-enable interrupts.
    cyc_diff += (c1-c0-1);    // Accumulate the cycle count difference.
  } 
  double t_usec;
  cyc_diff /= NUM_ITERS; // Compute the average cycle count.
  Serial.printf( "#Cycles per call  : %lu\n", cyc_diff );
  t_usec = ((double)cyc_diff)/ESP.getCpuFreqMHz();
  Serial.printf( "analogRead() call : %.3lf usec\n\n", t_usec );

  // Wait for 5 seconds before repeating the loop.
  delay(5000);
}

จากข้อความเอาต์พุต แสดงให้เห็นว่า การเรียกฟังก์ชัน analogRead(...) ในแต่ละครั้ง จะใช้เวลาประมาณ 80.88 usec (ค่าตัวเลขนี้ไม่ได้รวม Loop Overhead)

รูป: ตัวอย่างข้อความเอาต์พุต

 


โค้ดตัวอย่างที่ 5#

โค้ดตัวอย่างนี้จะคล้ายกับโค้ดตัวอย่างที่ 4 แต่เปลี่ยนจากการหาระยะเวลาในการทำคำสั่ง analogRead(...) มาเป็นคำสั่ง dacWrite(...) ซึ่งใช้สำหรับการสร้างสัญญาณแอนะล็อกเป็นเอาต์พุต ชิป ESP32 SoC มีวงจร DAC จำนวน 2 ช่อง (DAC1 และ DAC2)

const uint32_t NUM_ITERS = 100000UL;

void setup() {
  Serial.begin(115200);
  Serial.flush();
  delay(100);
  Serial.println("\n\n\n");
  // Set analog write resolution to 8 bits
  //analogWriteResolution(DAC1, 8); 
}

void loop() {
  uint32_t c0, c1, cyc_diff;

  cyc_diff = 0;
  for (uint32_t i=0; i < NUM_ITERS; i++) {
    portDISABLE_INTERRUPTS(); // Disable interrupts.
    asm volatile("rsr %0, ccount" : "=a"(c0));
    dacWrite( DAC1, 127 );  // Write DAC value to DAC1 channel.
    asm volatile( "rsr %0, ccount" : "=a"(c1) );
    portENABLE_INTERRUPTS();  // Re-enable interrupts.
    cyc_diff += (c1-c0-1);    // Accumulate the cycle count difference.
  } 
  double t_usec;
  cyc_diff /= NUM_ITERS; // Compute the average cycle count.
  Serial.printf( "#Cycles per call : %lu\n", cyc_diff );
  t_usec = ((double)cyc_diff)/ESP.getCpuFreqMHz();
  Serial.printf( "dacWrite() call  : %.3lf usec\n\n", t_usec );

  // Wait for 5 seconds before repeating the loop.
  delay(5000);
}

 

จากข้อความเอาต์พุต แสดงให้เห็นว่า การเรียกฟังก์ชัน dacWrite(...) ในแต่ละครั้ง จะใช้เวลาประมาณ 17.883 usec (ค่าตัวเลขนี้ไม่ได้รวม Loop Overhead)

รูป: ตัวอย่างข้อความเอาต์พุต

 


โค้ดตัวอย่างที่ 6#

โค้ดตัวอย่างถัดไปสาธิตการหาระยะเวลาในการสื่อสารกันระหว่างทาสก์ (Task) ของ FreeRTOS โดยมีทาสก์หนึ่งที่ถูกสร้างขึ้นมา (เรียกว่า User Task) และรอการสื่อสารด้วยวิธี Task Notification จากทาสก์หลัก (Main Task) ESP32 มีซีพียู 2 ชุด คือ CPU 0 และ CPU 1 ในตัวอย่างนี้ ทั้งสองทาสก์ทำงานโดยใช้ซีพียูตัวเดียวกัน คือ CPU 1

TaskHandle_t userTask;

volatile uint32_t c0, c1;

void userTaskFunc(void *parameter) {
  (void)parameter;
  uint32_t count = 0;
  for (;;) {
    // Wait for notification from the main task
    ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
    asm volatile("rsr %0, ccount" : "=a"(c1));
    uint32_t cyc_diff = c1-c0-1;
    Serial.printf( "User task running on core %d!\n", xPortGetCoreID() );
    Serial.printf( "User task's priority: %u\n", uxTaskPriorityGet(NULL) );
    Serial.printf( "Notification received: #%lu\n", ++count );
    Serial.printf( "Cycle difference: %lu\n", cyc_diff );
    // Convert cycle count difference to time duration in nanoseconds.
    Serial.printf( "Time difference: %.3lf usec\n\n",
                 ((double)cyc_diff)/ESP.getCpuFreqMHz() );
  }
}

void setup() {
  Serial.begin(115200);
  delay(1000);
  // Create a new FreeRTOS task
  xTaskCreate(
    userTaskFunc,    // Task entry function
    "UserTask",      // Task name
    4096,            // Stack size (in words)
    NULL,            // Task parameter
    2,               // Task priority (higher than the main task)
    &userTask        // Task handle
  );
  delay(1000);
  Serial.println( "\n\nESP32-FreeRTOS Demo..." );
  Serial.printf( "Main task's priority: %u\n", uxTaskPriorityGet(NULL)  );
  Serial.printf( "Main task running on core %d!\n", xPortGetCoreID() );
}

void loop() {
  // Save the CPU cycle count.
  asm volatile("rsr %0, ccount" : "=a"(c0));
  // Send notification to the user task.
  xTaskNotifyGive(userTask);  
  // Delay for 5 seconds
  vTaskDelay( pdMS_TO_TICKS(5000) ); 
}

 

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

รูป: ตัวอย่างข้อความเอาต์พุต

 

การอ่านค่าจากรีจิสเตอร์ของตัวนับไซเคิล ซึ่งมี 2 ชุด แยกกันระหว่างซีพียู CPU 0 และ CPU 1 ดังนั้นถ้าให้ทาสก์ทั้งสองทำงานด้วยซีพียูต่างกัน (หรือซีพียูเดียวกันก็ได้) ก็แนะนำให้ใช้คำสั่ง esp_timer_get_time() เพื่อใช้ในการจับเวลาด้วยวงจร Hardware Timer ของ ESP32 และมีตัวอย่างการเขียนโค้ดดังนี้

TaskHandle_t userTask;

volatile uint32_t ts0, ts1;

void userTaskFunc(void *parameter) {
  (void)parameter;
  uint32_t count = 0;
  for (;;) {
    // Wait for notification from the main task
    ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
    ts1 = esp_timer_get_time();
    uint32_t t_diff = ts1 - ts0;
    Serial.printf( "User task running on core %d!\n", xPortGetCoreID() );
    Serial.printf( "User task's priority: %u\n", uxTaskPriorityGet(NULL) );
    Serial.printf( "Notification received: #%lu\n", ++count );
    Serial.printf( "Time difference: %lu usec\n\n", t_diff );
  }
}

void setup() {
  Serial.begin(115200);
  delay(1000);
  // Create a new FreeRTOS task
  xTaskCreatePinnedToCore(
    userTaskFunc,   // Taskentry function
    "UserTAsk",     // Task name
    4096,           // Stack size (in words)
    NULL,           // Task parameter
    2,              // Task priority (higher than the main task)
    &userTask,      // Task handle
    1               // Core ID (either 0 or 1)
  );

  delay(1000);
  Serial.println( "\n\nESP32-FreeRTOS Demo..." );
  Serial.printf( "Main task's priority: %u\n", uxTaskPriorityGet(NULL)  );
  Serial.printf( "Main task running on core %d!\n", xPortGetCoreID() );
}

void loop() {
  // Save the CPU cycle count.
  ts0 = esp_timer_get_time();
  // Send notification to the user task.
  xTaskNotifyGive(userTask);  
  // Delay for 5 seconds
  vTaskDelay( pdMS_TO_TICKS(5000) ); 
}

รูป: ตัวอย่างข้อความเอาต์พุต

หากทดลองเลือกใช้ CPU Core ให้ User Task ทำงานต่างซีพียูจาก Main Task ก็จะเห็นได้ว่า ระยะเวลาที่ต้องใช้ จะมากขึ้น (เช่น เพิ่มขึ้นจาก 12 ~ 15 usec เป็น ** 17 ~ 20 usec** โดยประมาณ)

 


กล่าวสรุป#

บทความนี้ได้นำเสนอวิธีการและตัวอย่างการเขียนโค้ด Arduino เพื่อหาระยะเวลาในการทำคำสั่งสำหรับชิป ESP32 ยกตัวอย่างเช่น การเรียกใช้ฟังก์ชัน digitalWrite() analogRead() และ dacWrite() เป็นต้น ซึ่งวิธีนี้จะช่วยให้นักพัฒนาสามารถนำไปใช้ เพื่อทำความเข้าใจและตรวจสอบการทำงานของโค้ด การวิเคราะห์พฤติกรรมการทำงานในเชิงเวลาของฮาร์ดแวร์ได้ง่ายขึ้น

 


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

Created: 2023-10-06 | Last Updated: 2023-11-18