Arduino I/O Toggle: ตัวอย่างการเขียนโค้ดและวัดสัญญาณเอาต์พุต#

Keywords: AVR, ATmega328P, Arduino Uno, I/O Toggle


ตัวอย่างการเขียนโค้ดสำหรับ Arduino: I/O Toggle#

บทความนี้กล่าวถึง ตัวอย่างการเขียนโค้ดภาษา C/C++ และ Inline AVR Assembly สำหรับ Arduino Uno / Nano ที่มีชิปไมโครคอนโทรลเลอร์ 8 บิต ATmega328P ซึ่งสามารถเขียนได้หลายรูปแบบ พร้อมตัวอย่างรูปคลื่นสัญญาณที่ได้จากการวัดสัญญาณเอาต์พุตด้วยออสซิลโลสโคปแบบดิจิทัล (Digital Oscilloscope)


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

ถ้าจะลองเขียนโค้ด Arduino Sketch เพื่อทำให้ขา D13 สลับสถานะลอจิก เปลี่ยนจาก LOW และ HIGH โดยใช้คำสั่ง digitalWrite() ก็มีตัวอย่างง่าย ๆ ดังนี้

#include <Arduino.h>

#defined LED_PIN (13)

void setup() {
  pinMode( LED_PIN, OUTPUT );
}

void loop() { // period 6.76us. 3.2us+, 3.56us-, freq. 148kHz
  digitalWrite( LED_PIN, HIGH ); 
  digitalWrite( LED_PIN, LOW ); 
}

เมื่อนำไปทดลองกับบอร์ด Uno (ATmega328P, 5V, 16MHz) แล้ววัดสัญญาณเอาต์พุต จะได้รูปคลื่นสัญญาณสี่เหลี่ยมที่มีความถี่ 148 kHz คาบ 6.76 usec (ไมโครวินาที) และช่วงที่เป็น HIGH และ LOW มีความกว้างประมาณ 3.2 usec และ 3.56 usec ตามลำดับ

รูป: คลื่นสัญญาณเอาต์พุตสำหรับโค้ดตัวอย่างที่ 1 (Time/Div = 1 usec)

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

รูป: คลื่นสัญญาณเอาต์พุตสำหรับโค้ดตัวอย่างที่ 1 (Time/Div = 2 usec, Persistent Mode)


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

ในโค้ดตัวอย่างนี้มีการใช้ตัวแปรภายในแบบ static ชื่อ state และในการทำคำสั่งในฟังก์ชัน loop() แต่ละรอบ จะมีการสลับค่าลอจิกของตัวแปรนี้ และใช้ค่าของตัวแปรเพื่ออัปเดทเอาต์พุต ด้วยคำสั่ง digitalWrite()

#include <Arduino.h>

#defined LED_PIN (13)

void setup() {
  pinMode( LED_PIN, OUTPUT );
}

void loop() {
  static uint8_t state = 0;
  digitalWrite( LED_PIN, state ^= 1 ); // Toggle LED
}

รูปคลื่นสัญญาณเอาต์พุตที่วัดได้ มีความถี่ 157 kHz คาบ 6.36 usec และช่วงที่เป็น HIGH และ LOW มีความกว้างประมาณ 3.12 usec และ 3.24 usec ตามลำดับ ซึ่งถือว่าใกล้เคียงกับผลลัพธ์ที่ได้จากโค้ดตัวอย่างที่ 1

รูป: คลื่นสัญญาณเอาต์พุตสำหรับโค้ดตัวอย่างที่ 2 (Time/Div = 2 usec)


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

ในโค้ดตัวอย่างนี้มีลักษณะการทำงานเหมือนโค้ดในตัวอย่างที่ 2 แต่ว่า มีการใช้ประโยคคำสั่ง while(1) {...} อีกชั้นหนึ่ง และมีคำสั่ง digitalWrite() อยู่ภายใน ดังนั้นเมื่อเข้าสู่การทำงานของฟังก์ชัน loop() แล้ว จะไม่มีการจบการทำงานของฟังก์ชันนี้

#include <Arduino.h>

#defined LED_PIN (13)

void setup() {
  pinMode( LED_PIN, OUTPUT );
}

void loop() {
  static uint8_t state = 0;
  while(1) {
    digitalWrite( LED_PIN, state ^= 1 ); // Toggle LED
  }
}

รูปคลื่นสัญญาณเอาต์พุตที่วัดได้ มีความถี่ 163 kHz คาบ 6.12 usec และช่วงที่เป็น HIGH และ LOW มีความกว้างประมาณ 3.00 usec และ 3.12 usec ตามลำดับ ซึ่งมีความถี่สูงกว่าผลลัพธ์ที่ได้จากโค้ดตัวอย่างที่ 2

รูป: คลื่นสัญญาณเอาต์พุตสำหรับโค้ดตัวอย่างที่ 3 (Time/Div = 2 usec)


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

โค้ดในตัวอย่างนี้ ไม่ใช้ฟังก์ชัน pinMode() และ digitalWrite() ของ Arduino API แต่ใช้วิธีเข้าถึงรีจิสเตอร์ที่เกี่ยวข้องกับการทำงานของขา GPIO เช่น ขา PB5 (ขาที่ตรงกับบิตที่ 5 ของพอร์ต B) หมายถึง Arduino D13 Pin

#include <Arduino.h>

void setup() {
  DDRB  |=  (1 << 5);  // set DDRB bit 5 = 1 (output direction)
  PORTB &= ~(1 << 5);  // clear bit PB5 (output low at PB5)
}

void loop() { // period 500 ns, freq = 2MHz
  PORTB |=  (1 << 5);  // set bit PB5 (output high at PB5)  // 128ns+
  PORTB &= ~(1 << 5);  // clear bit PB5 (output low at PB5) // 372ns-
}

รูปคลื่นสัญญาณเอาต์พุตที่วัดได้ มีความถี่ 2 MHz คาบ 0.500 usec และช่วงที่เป็น HIGH และ LOW มีความกว้างประมาณ 0.128 usec และ 0.372 usec ตามลำดับ ซึ่งมีความถี่สูงกว่าผลลัพธ์ที่ได้จากโค้ดตัวอย่างที่ 1-3 หลายเท่า ดังนั้นการใช้ฟังก์ชัน digitalWrite() เพื่อกำหนดสถานะของขา GPIOจะใช้เวลาในการประมวลผลมากกว่าอย่างเห็นได้ชัด

รูป: คลื่นสัญญาณเอาต์พุตสำหรับโค้ดตัวอย่างที่ 4 (Time/Div = 0.2 usec)


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

โค้ดในตัวอย่างนี้ ได้มีการดัดแปลงแก้ไขจากโค้ดในตัวอย่างที่ 4 โดยใช้เพียงคำสั่งเดียวในฟังก์ชัน loop() คือ ประโยคคำสั่ง PINB |= (1 << 5); เพื่อสลับสถานะลอจิกของขาเอาต์พุต PB5 (ตรงกับขา Arduino D13)

#include <Arduino.h>

void setup() {
  DDRB  |= (1 << 5);  // set DDRB bit 5 = 1 (output direction)
  PORTB |= (1 << 5);  // set PORTB bit 5 = 1
}

void loop() { // period 750ns, 376ns+, 374ns-, freq. = 1.33MHz
  PINB |= (1 << 5); // Toggle PB5 by writing 1 to PINB5, 375ns
}

รูปคลื่นสัญญาณเอาต์พุตที่วัดได้ มีความถี่ 1.33 MHz คาบ 0.750 usec (ได้ความถี่ต่ำกว่าในตัวอย่างที่ 4) ในขณะที่ช่วงที่เป็น HIGH และ LOW มีความกว้างใกล้เคียงกัน ประมาณ 0.376 usec และ 0.374 usec ตามลำดับ

รูป: คลื่นสัญญาณเอาต์พุตสำหรับโค้ดตัวอย่างที่ 5 (Time/Div = 0.2 usec)


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

โค้ดตัวอย่างนี้แตกต่างจากโค้ดตัวอย่างที่ 5 เพียงเล็กน้อย ตรงที่มีการใช้ประโยคคำสั่ง while(1) {...} ซึ่งภายในมีเพียงหนึ่งคำสั่ง PINB |= (1 << 5); ที่ทำให้เกิดการสลับสถานะลอจิกที่ขา PB5 ของพอร์ต B และจะไม่มีการออกจากการทำงานของคำสั่ง while(1) {...} หนึ่งรอบใช้เวลาเท่ากับ 6 CPU Cycles

#include <Arduino.h>

void setup() {
  DDRB  |= (1 << 5);  // set DDRB bit 5 = 1 (output direction)
  PORTB |= (1 << 5);  // set PORTB bit 5 = 1
}

void loop() { // period = 500ns, freq. = 2MHz
  while(1) {
    PINB |= (1 << 5); // Toggle PB5, 252ns+,248ns-
  }
}

รูปคลื่นสัญญาณเอาต์พุตที่วัดได้ มีความถี่ 2 MHz คาบ 0.500 usec ในขณะที่ช่วงที่เป็น HIGH และ LOW มีความกว้างใกล้เคียงกัน ประมาณ 0.252 usec และ 0.248 usec ตามลำดับ

การทำคำสั่งหนึ่งรอบของประโยคคำสั่ง while(1) {...} เท่ากับ 0.25 usec หรือ 250 nsec โดยประมาณ หรือคิดเป็น 4 CPU Cycles = 4/16MHz = 0.25 usec

รูป: คลื่นสัญญาณเอาต์พุตสำหรับโค้ดตัวอย่างที่ 6 (Time/Div = 0.2 usec)


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

โค้ดตัวอย่างนี้สาธิตการใช้คำสั่งในภาษา AVR Assembly โดยใช้วิธีการแทรกโค้ดคำสั่ง asm volatile(...) ไว้ในโค้ด Arduino Sketch ในฟังก์ชัน loop() {...} ดังนั้นจึงเป็นการทำคำสั่งด้วยภาษา AVR Assembly และมีการเซตบิต (sbi หรือ Set Bit) และเคลียร์บิต (cbi หรือ Clear Bit) สำหรับตำแหน่ง PB5 ในรีจิสเตอร์ PORTB สลับกันไปเรื่อย ๆ ซ้ำไปไม่หยุด โดยย้อนกลับไปทำที่ตำแหน่ง L0 ด้วยคำสั่ง rjmp (Relative Jump)

#include <Arduino.h>

void setup() { 
  DDRB  |= (1 << 5);  // set DDRB  bit 5 = 1 (output direction)
  PORTB |= (1 << 5);  // set PORTB bit 5 = 1 (output high at PB5)
}

void loop() { // period = 374ns, 126ns+, 248ns-, freq. 2.67MHz
  asm volatile (
    "L0: sbi %0,%1   \n\t"   // [2C] set bit 5 in register PORTB
    "    cbi %0,%1   \n\t"   // [2C] clear bit 5 in register PORTB
    "    rjmp L0     \n\t"   // [2C] relative jump (backward) to label 0:
    :: "I" (_SFR_IO_ADDR(PORTB)), "I" (PORTB5) 
  );
 // "I": used as input arguments %0 and %1 respectively
}

การทำคำสั่งหนึ่งรอบของประโยคคำสั่ง while(1) {...} เท่ากับ 0.375 usec หรือ 375 nsec โดยประมาณ หรือคิดเป็น 6 CPU Cycles = 6/16MHz = 0.375 usec

รูป: คลื่นสัญญาณเอาต์พุตสำหรับโค้ดตัวอย่างที่ 7 (Time/Div = 0.2 usec)


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

โค้ดตัวอย่างนี้สาธิตการใช้คำสั่งในภาษา AVR Assembly ในฟังก์ชัน loop() {...} และมีการเขียนค่าบิตเป็น 1 ลงในตำแหน่ง PINB5 ของรีจิสเตอร์ PINB โดยใช้คำสั่ง sbi ซึ่งจะทำให้เกิดการสลับสถานะลอจิก

#include <Arduino.h>

void setup() {
  DDRB  |= (1 << 5);  // set  DDRB bit 5 = 1 (output direction)
  PORTB |= (1 << 5);  // set PORTB bit 5 = 1 (output high at PB5)
}

void loop() { // period=500ns, 252ns+, 248ns-, freq. 2MHz
  asm volatile (
    "L0: sbi %0,%1    \n\t"   // [2C]
    "    rjmp L0      \n\t"   // [2C]
    :: "I" (_SFR_IO_ADDR(PINB)), "I" (PINB5)
  );
}

รูปคลื่นสัญญาณเอาต์พุตที่วัดได้ มีความถี่ 2 MHz คาบ 0.500 usec ในขณะที่ช่วงที่เป็น HIGH และ LOW มีความกว้างใกล้เคียงกัน ประมาณ 0.252 usec และ 0.248 usec ตามลำดับ

รูป: คลื่นสัญญาณเอาต์พุตสำหรับโค้ดตัวอย่างที่ 8 (Time/Div = 0.2 usec)


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

ตัวอย่างนี้เปรียบเทียบการใช้ตัวแปรที่มีชนิดข้อมูลเป็น float และ uint32_t (ขนาด 32 บิต) และใช้เป็นตัวนับของการทำซ้ำในประโยคคำสั่ง for() {...} ซึ่งภายในมีคำสั่งที่ทำให้เกิดการสลับสถานะลอจิกที่ขา PB5

โค้ดที่มีการใช้ตัวแปร float ให้นับขึ้นทีละ 0.001f จาก 0.0f ถึง 1000.0f มีดังนี้

#include <Arduino.h>

void setup() {
  DDRB |= _BV(PB5); // output direction on PB5 pin
}

void loop() { // 12.3us+/- period 24.60us, freq=40.6kHz
  for ( float i=0.0f; i < 1000.0f; i+=0.001f ) {
     PINB |= _BV(PB5); // Toggle output
  }
}

ข้อสังเกต: คำสั่ง _BV(5) ให้ผลเหมือนกับ (1<<5)

โค้ดที่มีการใช้ตัวแปร uint32_t ให้ตัวแปร i นับขึ้นจาก 0 ถึง 1000000 ทีละ 1 มีดังนี้

void setup() {
  DDRB |= _BV(PB5); // output direction on PB5 pin
}

void loop() { // 500ns+, 495ns-, period 995ns, freq=1MHz
  for ( uint32_t i=0; i < 1000000ul; i+=1 ) {
     PINB |= _BV(PB5); // Toggle output
  }
}

ในทั้งสองกรณี ได้มีการวัดอัตราการเปลี่ยนแปลงสถานะลอจิกในช่วงที่มีการนับ จะเห็นได้ว่า การใช้ตัวแปรแบบ float และมีการคำนวณนั้น ใช้เวลามากกว่าการใช้ตัวแปรแบบ uint32_t

รูป: คลื่นสัญญาณเอาต์พุตในกรณีที่ใช้ตัวแปร float (ได้ความถี่ 40.60 kHz)

รูป: คลื่นสัญญาณเอาต์พุตในกรณีที่ใช้ตัวแปร uint32_t (ได้ความถี่ 1.00 MHz)


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

โค้ดในตัวอย่างนี้สาธิตการเขียนโปรแกรมเพื่อใช้งานวงจรตัวนับ Timer1 ภายในชิป ATmega328P บนบอร์ด Arduino Uno / Nano วงจร Timer1 จะถูกตั้งค่าให้ทำงานในโหมด CTC (Clear Timer on Compare) ให้ทำงานด้วยความถี่เท่ากับความถี่ของซีพียู (ไม่มีตัวหารความถี่) ซึ่งเท่ากับ 16MHz และมีการเปิดใช้งานอินเทอร์รัพท์ที่เกี่ยวข้อง

ค่าในรีจิสเตอร์ OCR1A ถูกกำหนดให้เป็น 1 ซึ่งใช้สำหรับการเปรียบเทียบกับค่าในรีจิสเตอร์ของตัวนับ TCNT1 เมื่อ TCNT1 เริ่มนับจาก 0 และนับขึ้นจนได้เท่ากับค่าใน OCR1A จะทำให้ TCNT1 กลับไปเริ่มนับที่ 0 ใหม่อีกครั้ง และจะเกิดอินเทอร์รัพท์ที่มีชื่อว่า TIMER1_COMPA_vect จากนั้นฟังก์ชัน ISR(TIMER1_COMPA_vect) {...} จะทำคำสั่งเพื่อสลับสถานะลอจิกของขา PB5

#include <avr/io.h>
#include <avr/interrupt.h>

ISR(TIMER1_COMPA_vect) {
  PINB |= _BV(PB5); // Toggle PB5
}

// Set up Timer1 to toggle the LED pin as fast as possible
void initTimer1() {
  TCCR1A = 0;
  TCCR1B = 0;
  TCNT1  = 0;
  OCR1A  = 1;
  // Use CTC (Clear Timer on Compare match) mode 
  TCCR1B |= (1 << WGM12) | (1 << CS10);
  // Enable Output Compare Match Interrupt
  TIMSK1 |= (1 << OCIE1A);
}

// 275kHz, perdiod=3.64us, pw+=1.82us, pw-=1.82us

int main() {
  DDRB |= _BV(PB5); // Set output direction on PB5 pin (D13)
  initTimer1();     // Initialize Timer1
  sei(); // Enable global interrupts.
  while(1);
  return 0;
}

ตัวอย่างคลื่นสัญญาณเอาต์พุตที่ได้จากการวัดด้วยออสซิลโลสโคป มีลักษณะดังนี้

รูป: คลื่นสัญญาณเอาต์พุต (วัดความถี่ได้ 276kHz)

ข้อสังเกต: ความเร็วในการสลับสถานะลอจิก ถูกจำกัดด้วยระยะเวลาในการตอบสนองต่ออินเทอร์รัพท์ของชิป AVR

ถ้าลองเปลี่ยนค่า OCR1A ให้สูงขึ้นเป็น 15 จะยังไม่เห็นการเปลี่ยนแปลง แต่ถ้าเป็น 31 จะเริ่มเห็นการเปลี่ยนแปลง และความถี่ที่ได้จะลดลง (ได้ประมาณ 252kHz) ตามรูปคลื่นสัญญาณต่อไปนี้

รูป: คลื่นสัญญาณเอาต์พุต (วัดความถี่ได้ 252kHz)


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

โค้ดในตัวอย่างนี้สาธิตการใช้งานวงจรตัวนับ Timer1 ภายในชิป ATmega328P โดยจะทำงานในโหมด Fast PWM (TOP=ICR1) ซึ่งจะต้องกำหนดค่าบิต WGM10=0, WGM11=1, WGM12=1 และ WGM13=1 ตามลำดับ

ค่าในรีจิสเตอร์ ICR1 ถูกกำหนดให้เป็นค่าสูงสุดของการนับ และเมื่อ TCINT1 นับถึงค่าดังกล่าว จะกลับไปเริ่มนับที่ 0 ใหม่ และทำให้ขาเอาต์พุต PB1/OC1A ที่เกี่ยวข้องกับการทำงานของ Timer1 ในโหมดนี้ (กำหนดค่าบิต COM1A1=1 ในรีจิสเตอร์ TCCR1A) มีสถานะลอจิกเป็น HIGH เมื่อเริ่มนับที่ 0 ในแต่ละรอบ

สัญญาณเอาต์พุตที่ขาดังกล่าว จะเปลี่ยนสถานะลอจิกจาก HIGH เป็น LOW เมื่อตัวนับ TCINT1 ถึงค่าของรีจิสเตอร์ ICR1

#include <avr/io.h>
#include <avr/interrupt.h>

void initTimer1() {
  TCCR1A = 0;
  TCCR1B = 0;
  // Set timer1 in fast PWM mode:
  // f_CPU/2 = 16MHz/(2) = 8MHz, 50% duty cycle
  ICR1  = (2)-1;
  OCR1A = ICR1 >> 1; // 50% duty cycle
  TCCR1A |= (1 << WGM11) | (1 << COM1A1);
  TCCR1B |= (1 << WGM12) | (1 << WGM13) | (1 << CS10);
}

int main() {  
  DDRB |= (1 << PB1); // Set pin PB1 (D9 pin) as output
  initTimer1();
  while(1);
  return 0;
}

รูป: คลื่นสัญญาณเอาต์พุต (วัดความถี่ได้ 8MHz)

 


กล่าวสรุป#

บทความนี้ได้นำเสนอตัวอย่างการเขียนโค้ดสำหรับบอร์ด Arduino Uno/Nano ที่ใช้ชิป ATmega328P (8-bit, 16MHz, 5V) และทำให้เกิดการสลับสถานะลอจิกที่ขาเอาต์พุตหนึ่งขา และการใช้ออสซิลโลสโคป เพื่อวัดสัญญาณเอาต์พุตและศึกษาพฤติกรรมที่เกิดขึ้นจากรูปแบบต่าง ๆ ในการเขียนโค้ด

 


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

Created: 2023-02-05 | Last Updated: 2023-02-06