การวัดความถี่ของสัญญาณดิจิทัลแบบมีคาบด้วย Arduino - ATmega328P#

Keywords: Arduino, ATmega328P, Frequency Measurement


การวัดความถี่ของสัญญาณดิจิทัลแบบมีคาบ#

บทความนี้กล่าวถึง การวัดความถี่ของสัญญาณดิจิทัลแบบมีคาบ (Frequency Measurement) โดยใช้บอร์ดไมโครคอนโทรลเลอร์ Arduino Uno / Nano

การวัดความถี่ของสัญญาณดิจิทัลแบบมีคาบ (Periodic Digital Signal) โดยใช้บอร์ดไมโครคอนโทรลเลอร์ Arduino สามารถทำได้หลายวิธี และขอกล่าวถึงสองวิธี

  • วิธีแรกเป็นการวัดความกว้างของคาบ (Period) ซึ่งเป็นระยะห่างเชิงเวลาของขอบสัญญาณ เช่น เมื่อเกิดขอบขาขึ้นถัดกันสองครั้ง (Two Consecutive Rising Edges) โดยเปิดใช้งาน External Interrupt เพื่อคอยตรวจดูว่า มีขอบขาขึ้นของสัญญาณหรือไม่ และบันทึกเวลาเมื่อเกิดขอบขาขึ้นของสัญญาณแต่ละครั้ง แล้วหาผลต่างค่าเวลาทั้งสองเหตุการณ์ดังกล่าว ก็จะได้คาบเวลา ดังนั้นไมโครคอนโทรลเลอร์จะต้องมีระบบฐานเวลาที่ใช้วงจรตัวนับ (Timer/Counter) เช่น มีความละเอียดในการจับเวลา 1 ไมโครวินาที เป็นต้น การวัดความกว้างของคาบ จะทำเพียงครั้งเดียวหรือวัดค่าสำหรับหลายคาบ แล้วนำมาหาค่าเฉลี่ยก็ได้ ถ้าในช่วงเวลาดังกล่าว สัญญาณมีคาบหรือความถี่คงที่ จากนั้นเมื่อได้ความกว้างของคาบ ก็สามารถคำนวณความถี่ของสัญญาณอินพุตได้
  • วิธีที่สองคือ การใช้สัญญาณที่มีคาบนั้น เป็นสัญญาณอินพุตและป้อนให้วงจรตัวนับ (Timer/Counter) ที่อยู่ภายในไมโครคอนโทรลเลอร์ แล้วนำค่าของตัวนับที่ได้ในช่วงเวลาที่กำหนดไว้ (ช่วงที่จับเวลา) มาคำนวณเป็นความถี่ได้ ระยะเวลาในการนับจะถูกกำหนดโดยวงจรตัวนับอีกตัวหนึ่งของไมโครคอนโทรลเลอร์

ในการตั้งค่าใช้งานสำหรับวงจรตัวนับที่อยู่ภายใน ATmega328P ไม่มีคำสั่งของ Arduino API ไว้ให้ใช้งาน ดังนั้นผู้ใช้จะต้องกำหนดค่าในรีจิสเตอร์ต่าง ๆ ของวงจร Timers ที่เกี่ยวข้อง ให้ถูกต้อง

ความแตกต่างระหว่าง Timer กับ Counter

  • Timer เป็นวงจรตัวนับที่มีการเพิ่มค่าขึ้นด้วยอัตราคงที่ เช่น ทำงานตามจังหวะของสัญญาณ Clock ที่มีความถี่คงที่
  • Counter เป็นวงจรตัวนับมีการเพิ่มค่าขึ้นอยู่กับเหตุการณ์หรือเงื่อนไขที่เกิดขึ้นตามที่ได้กำหนดไว้ เช่น นับจำนวนพัลส์ แต่ถ้าการเกิดพัลส์มีอัตราคงที่ Counter ก็จะทำงานเหมือน Timer

 


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

โค้ด Arduino Sketch ต่อไปนี้ สาธิตการวัดความกว้างของคาบสัญญาณอินพุตที่ป้อนเข้าขา D2 ของ Arduino Uno / Nano ซึ่งสามารถเปิดใช้งานอินเทอร์รัพท์ภายนอกได้ที่ขาดังกล่าว

เมื่อเกิดเหตุการณ์ขอบขาขึ้นของสัญญาณอินพุตที่ขา D2 จะทำให้เกิดอินเทอร์รัพท์จากภายนอก (External Interrupt) แล้วให้ฟังก์ชัน ISR ที่เกี่ยวข้องทำหน้าที่อ่านค่าของตัวนับ Timer1 ภายในชิป ATmega328P ที่ถูกใช้เป็นตัวสร้างฐานเวลา

วงจรตัวนับ Timer1 จะถูกตั้งค่าให้มีความละเอียดในการนับ 0.5 ไมโครวินาที (μsec) ซึ่งใช้ตัวหารความถี่เท่ากับ 8 (จากความถี่ 16MHz ของ CPU ดังนั้น 16MHz/8 = 2MHz เป็นความถี่ในการนับ หรือ มีคาบเวลาเท่ากับ 0.5 usec)

ในการจับเวลาของขอบขาขึ้นแต่ละครั้ง จะต้องบันทึกเวลาสองครั้ง และมีระยะเวลาห่างกันทั้งหมดเท่ากับ 4 คาบ แล้วนำค่าที่บันทึกได้ มาคำนวณผลต่างแล้วหาร 4 และคูณด้วย 0.5 ไมโครวินาที (หรือ หารด้วย 8 เพียงครั้งเดียว) จึงจะได้เป็นคาบเวลา (เฉลี่ย) ในหน่วยเป็นไมโครวินาที

วิธีการวัดคาบเวลาหรือความถี่ในลักษณะนี้ สามารถวัดสัญญาณที่มีความถี่ไม่เกิน 100kHz (โดยประมาณ) หรือมีคาบ 10 ไมโครวินาที เนื่องจากถ้าใช้ความถี่สูงมาก ฟังก์ชัน ISR จะตอบสนองต่ออินเทอร์รัพท์ได้ไม่ทัน ค่าที่วัดจะมีความผิดพลาดสูงกว่า ในกรณีที่ใช้ความถี่ต่ำ

ในตัวอย่างนี้มีการสร้างสัญญาณ PWM โดยใช้วงจร Timer2 (มีรีจิสเตอร์ตัวนับขนาด 8 บิต) ให้เป็นเอาต์พุตที่ขา Arduino D11 / PB3 แล้วนำไปป้อนให้ขา Arduino D2 เพื่อใช้เป็นสัญญาณทดสอบ หากไม่มีแหล่งกำเนิดสัญญาณภายนอก

ในตัวอย่างนี้ วงจร Timer2 จะถูกตั้งค่าให้ทำงานในโหมด Fast PWM และตั้งค่าบิต WGM2[2:0]="111" เมื่อค่าในรีจิสเตอร์ TCNT2 มีการนับขึ้นจาก 0x00 (BOTTOM) มาได้เท่ากับค่าสูงสุดซึ่งระบุไว้ในรีจิสเตอร์ OCR2A (TOP) จะถูกรีเซตค่ากลับไปเริ่มต้นนับใหม่ และเหตุการณ์นี้เรียกว่า Compare Match

นอกจากนั้นยังได้มีการกำหนดให้ค่าบิต COM2A[1:0]="01" ซึ่งเป็นการเปิดใช้งานขาสำหรับ Output Compare Match A (OC2A) ที่ตรงกับขา PB3 และตั้งค่าให้มีการสลับค่าลอจิกที่ขาดังกล่าว (Toggle OCR2A pin) เมื่อเกิดเหตุการณ์ Compare Match

ช่วงของความถี่ของสัญญาณ PWM () ก็ขึ้นอยู่กับการตั้งค่าตัวหารความถี่ () และให้ความถี่ของซีพียู () เท่ากับ 16MHz และมีเงื่อนไข ดังนี้

ยกตัวอย่างเช่น ถ้าใช้ตัวหารความถี่เท่ากับ 8 ดังนั้น ความถี่ต่ำสุดของสัญญาณ PWM ที่เลือกใช้ได้ สามารถคำนวณได้ดังนี้

ข้อสังเกต: การทำงานของโค้ดตัวอย่างนี้ มีการปิดการทำงานของวงจร Timer0 ชั่วคราว โดยปรกติแล้ว Arduino Sketch จะใช้ Timer0 ในการสร้างฐานเวลาของระบบที่มีการเกิดอินเทอร์รัพท์ทุก ๆ 4 ไมโครวินาที และใช้กับคำสั่ง เช่น micros() และ millis()

#define FREQ_PIN     (2)   // Arduino D2 pin
#define PWM_PIN      (11)  // Arduino D11 pin

volatile uint16_t ovf_count  = 0;
volatile uint8_t  edge_count = 0;
volatile boolean  done   = false;
volatile uint32_t saved_ticks[2] = {0,0};

ISR(TIMER1_OVF_vect) { 
   ovf_count++; // Increment Timer1 overflow counter
}

// Initialize Timer1
void init_timer1() {
   uint8_t SREG_tmp = SREG;  // Save the status register
   ovf_count = 0;            // Reset overflow counter
   cli();                    // Disable interrupts
   TIMSK1 = 0;
   TCNT1  = 0;               // Reset Timer1 count register
   TCCR1A = 0;
   TCCR1B = 0;
   TIFR1 |= _BV(TOV1);       // Clear Timer1 overflow flag
   // Use Timer1 in Normal mode, f_CPU/8: 16MHz/8=2MHz => 0.5usec step
   TIMSK1 |= (1<<TOIE1);     // Enable Timer1 overflow interrupt
   TCCR1B |= (1<<CS11);      // Start Timer1 
   SREG = SREG_tmp;          // Restore the status register
}

void ext_isr() { // ISR for External Interrupt
  static uint32_t saved_value;
  uint32_t tickcount = get_tickcount();
  if (!done) {
    if (edge_count==1) {
      saved_ticks[0] = tickcount;
    }
    else if (edge_count==5) {
      saved_ticks[1] = tickcount;
      done = true;
    }
    edge_count++; // Increment the rising-edge counter
  }
}

// Get the current value of Timer1 counter (with 0.5usec precision)
uint32_t get_tickcount() { 
   // Save the SREG register
   uint8_t SREG_tmp = SREG;
   // Disable global interrupt
   cli();
   // Read the current value of TCNT1 register of Timer1 
   uint16_t count_value = TCNT1;
   // In normal operation the Timer/Counter Overflow Flag (TOV1)
   // will be set in the same clock cycle as the TCNT1 becomes zero.
   if ( TIFR1 & _BV(TOV1) ) { // Check Timer1 overflow flag
      TIFR1 |= _BV(TOV1);     // Clear overflow flag
      ++ovf_count;            // Increment the overflow counter
   }
   // Calculate Timer1 tick count
   uint32_t ticks = ovf_count;
   ticks = ((ticks << 16) + count_value);
   // Restore the SREG register
   SREG = SREG_tmp;
   return ticks;
}

void measure( uint32_t *period, uint32_t *freq ) {
  // Save the current values of TCCR0A/B
  uint8_t TCCR0A_tmp = TCCR0A;
  uint8_t TCCR0B_tmp = TCCR0B;
  uint32_t tick_diff;
  // Set TCCR0A/B registers to 0 (disable Timer0)
  TCCR0A = 0;
  TCCR0B = 0;
  edge_count = 0;
  done = false;
  ovf_count = 0;
  // Wait until the done flag is set.
  while (!done){}
  tick_diff = saved_ticks[1] - saved_ticks[0];
  // Restore the saved values of TCCR0A/B
  TCCR0A = TCCR0A_tmp;
  TCCR0B = TCCR0B_tmp;
  *period = 10*tick_diff/8;
  *period -= 25*(*period)/10000; // Correction
  *freq = 100*1000000ul/(*period);
}

void set_timer2_prescaler( uint16_t prescaler ) {
  uint8_t bits = 0;  // CS[2:0] bits
  switch (prescaler) {
    case 1:    bits = 1; break; // 0b001
    case 8:    bits = 2; break; // 0b010
    case 32:   bits = 3; break; // 0b011
    case 64:   bits = 4; break; // 0b100
    case 128:  bits = 5; break; // 0b101
    case 256:  bits = 6; break; // 0b110
    case 1024: bits = 7; break; // 0b111
    default:   break; // Timer stopped
  }
  TCCR2B &= ~(_BV(CS22) | _BV(CS21) | _BV(CS20));
  if (bits & 1)  { TCCR2B |= _BV(CS20); }
  if (bits & 2)  { TCCR2B |= _BV(CS21); }
  if (bits & 4)  { TCCR2B |= _BV(CS22); }
}

void init_timer2_pwm( uint16_t prescaler, uint8_t max_value ) {
  DDRB |= _BV(DDB3); // Set direction of PB3 pin to output.
  // Use Fast PWM (non-inverting) with 50% duty cycle
  // Enable Timer2 OC2A output for PWM
  // WGM2[2:0]  = "111" => BOTTOM=0, TOP=OCR2A
  // COM2A[1:0] = "01"  => Toggle OC2A on compare match.
  TCCR2A = _BV(WGM21) | _BV(WGM20) | _BV(COM2A0); 
  TCCR2B = _BV(WGM22);
  set_timer2_prescaler( prescaler );
  OCR2A  = max_value; // Set the MAX value for period
}

void setup() {
  Serial.begin( 115200 );
  Serial.println( "Frequency Measurement with Arduino Board" );
  init_timer1();
  // freq. = 16MHz/(8*10)/2  = 100kHz
  // freq. = 16MHz/(8*256)/2 = 3906.Hz
  // Use Timer2 to create a PWM signal, frequency of 100kHz
  init_timer2_pwm( 8, 10-1 );
  delay(100);
  // Enable the external interrupt
  attachInterrupt( digitalPinToInterrupt(FREQ_PIN), 
                   ext_isr, RISING );
}

void loop() {
  static char sbuf[40];
  uint32_t period, freq;
  // Measure the period and frequency of the input signal
  measure( &period, &freq ); 
  // The period value must be divided by 10.
  sprintf( sbuf, "Period = %lu.%1u us, ", 
           period/10, period%10 );
  Serial.print( sbuf );
  // The frequency value must be divided by 10.
  sprintf( sbuf, "Freq. = %lu.%1u Hz", 
           freq/10, freq%10 );
  Serial.println( sbuf );
  delay(1000);
}

รูป: ตัวอย่างการจำลองการทำงานของโค้ดด้วย Wokwi Simulator สำหรับความถี่ PWM เท่ากับ 100kHz (มีคาบเท่ากับ 10 ไมโครวินาที)

รูป: การแสดงสัญญาณเอาต์พุตที่มีการบันทึกได้จากการใช้ Virtual Logic Analyzer ของ Wokwi Simulator

รูป: ตัวอย่างการทดสอบกับอุปกรณ์จริง (ความถี่ของสัญญาณ PWM=100kHz)

รูป: ตัวอย่างการทดสอบกับอุปกรณ์จริง (สร้างสัญญาณภายนอกโดยใช้ Function Generator ความถี่ 125kHz)

รูป: ตัวอย่างการทดสอบกับอุปกรณ์จริง (สร้างสัญญาณภายนอกโดยใช้ Function Generator ความถี่ 50Hz)

 


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

ตัวอย่างนี้สาธิตการใช้วงจร Timer1 โดยมีการใช้สัญญาณดิจิทัล-อินพุตจากภายนอกมาเป็นสัญญาณ Clock สำหรับกำหนดจังหวะการนับของวงจรดังกล่าว และมีการเปิดใช้งานวงจร Timer2 ซึ่งทำให้เกิดอินเทอร์รัพท์ทุก ๆ 1 มิลลิวินาที เพื่อกำหนดช่วงเวลาในการนับของวงจร Timer1 เช่น กำหนดช่วงเวลาเท่ากับ 1000 มิลลิวินาที สำหรับการนับจำนวนพัลส์ที่เกิดขึ้นกับสัญญาณอินพุตของ Timer1 เมื่อได้จำนวนพัลส์ (เก็บค่าไว้ในตัวแปร pulse_count) ที่เกิดขึ้นในช่วง 1000 มิลลิวินาที ก็จะถูกนำไปคำนวณเป็นความถี่ (หน่วยเป็น Hz)

รีจิสเตอร์ตัวนับของ Timer1 มีขนาด 16 บิต และถ้านับด้วยความถี่สูง ก็มีโอกาสที่จะเกิด Overflow ได้ ดังนั้นจึงตัองมีการตรวจสอบและบันทึกจำนวนครั้งของเหตุการณ์ดังกล่าวที่เกิดขึ้น (เก็บค่าไว้ในตัวแปร ovf_count)

ในตัวอย่างนี ยังได้มีการสร้างสัญญาณ PWM ที่มีเอาต์พุตออกที่ขา D6 (ใช้วงจร Timer0) โดยใช้คำสั่ง `analogWrite(...) แล้วนำไปป้อนเป็นอินพุตให้ขา D5 ในกรณีที่ไม่มีสัญญาณทดสอบจากภายนอก

#define  FREQ_PIN (5)  // Arduino D5 pin (T1 pin)

volatile uint16_t ovf_count  = 0;
volatile uint32_t pulse_count = 0;
volatile uint16_t interval_tick_count = 0;
volatile boolean  done = false;

void start_timers( uint16_t msec ) {
  uint8_t SREG_tmp = SREG; // Save the SREG value
  done = false;
  ovf_count = 0;
  interval_tick_count = msec; 

  cli(); // Disable global interrupt  
  // Disable Timer1
  TCCR1A = 0;  // Reset Timer1 control register A
  TCCR1B = 0;  // Reset Timer1 control register B 
  TCNT1  = 0;  // Reset Timer1 counter value to 0
  TIFR1 |= (1<<TOV1);    // Clear Timer/Counter 1 overflow flag
  TIMSK1 &= ~(1<<TOIE1); // Disable Timer1 Overflow Interrupt 

  // Disable Timer2
  TCCR2A = 0;  // Reset Timer2 control register A
  TCCR2B = 0;  // Reset Timer2 control register B  
  TCNT2 = 0;   // Reset Timer2 counter value to 0

  // Setup Timer2
  // Set Timer2 prescaler = 128 -> 16MHz/128 = 125kHz
  TCCR2B |= (1<<CS22) | (1<<CS20); 
  // Use Timer2 in CTC (Clear Timer on Compare Match) mode 
  // -> WGM22=0, WGM21=1, WGM20=0
  // Note that in CTC mode the counter is automatically cleared to zero
  // when the counter value (TCNT2) matches the OCR2A.
  OCR2A  = 125-1; // -> 16MHz/128/125 = 1kHz or 1msec timing interval
  TIMSK2 |= (1<<OCIE2A); // Enable Timer2 Output Compare Match A Interrupt

  // Setup Timer1 and use external clock source (rising edge) on T1 pin
  // Timer1 will be used to count events on the T1 pin (Arduino D5 pin).
  // It will operate in normal mode (WGM1[3:0]="0000"), no interrupt.
  TCCR1B |= (1<<CS12) | (1<<CS11) | (1<<CS10); // Start Timer1
  TCCR2A |= (1<<WGM21);  // Start Timer2 in CTC mode
  SREG = SREG_tmp;       // Restore the SREG value
}

ISR(TIMER2_COMPA_vect) { // for Timer2 Output Compare Match A Interrupt
  pulse_count = TCNT1;
  if ( TIFR1 & (1<<TOV1) ) { // Check overflow
     TIFR1 |= (1<<TOV1);     // Clear Timer/Counter 1 overflow flag
     ovf_count++;            // Increment Timer1 overflow count
  }
  if (interval_tick_count-- == 0) {  // Timeout
     TCCR1A = 0;  // Reset Timer1 control register A
     TCCR1B = 0;  // Reset Timer1 control register B 
     TCCR2A = 0;  // Reset Timer2 control register A
     TCCR2B = 0;  // Reset Timer2 control register B 
     pulse_count = ((uint32_t)ovf_count << 16) + pulse_count; 
     done = true;
  }
}

uint32_t measure() {
  start_timers( 1000 /*msec*/ ); // Start Timer1/Timer2
  while( !done ) {} // Wait until the done flag is set
  return pulse_count; // pulses per second (Hz)
}

void setup() {
  Serial.begin( 115200 );
  Serial.println( "Arduino Frequency Measurement" );
   // Create a PWM signal (50% duty cycle) on Arduino D6 pin
   // Default PWM frequency = 16 Mhz/64/256 = 976.56Hz
  analogWrite( 6, 127 );
  delay(100);
}

void loop() {
  static char sbuf[40];
  uint32_t freq = measure();
  sprintf( sbuf, "Freq. = %lu Hz", freq );
  Serial.println( sbuf );
  delay(10);
}

รูป: การจำลองการทำงานด้วย Wokwi Simulator

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

รูป: ตัวอย่างการทดสอบกับอุปกรณ์จริง (วัดความถี่ของสัญญาณ PWM ที่ขา D6)

รูป: ตัวอย่างการวัดสัญญาณด้วย USB Logic Analyzer และแสดงผลด้วยซอฟต์แวร์ PulseView (วัดคาบได้ประมาณ 1022.67 usec และความถี่ประมาณ977.83 Hz)

 


กล่าวสรุป#

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

 


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

Created: 2023-04-08 | Last Revision: 2023-05-05