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