การเขียนโค้ด Arduino สำหรับ LED Blink ด้วยวิธีที่แตกต่างกัน#
บทความนี้นำเสนอตัวอย่างการเขียนโค้ด Arduino Sketch เพื่อทำให้ LED บนบอร์ดไมโครคอนโทรลเลอร์ Arduino Uno / Nano กระพริบด้วยอัตราคงที่ โดยใช้วิธีที่แตกต่างกันมากกว่าหนึ่งวิธี โค้ดตัวอย่างในบทความนี้สามารถนำไปทดลองได้กับ WokWi Simulator เพื่อจำลองการทำงานเสมือนจริง และ Arduino IDE เพื่อนำไปอัปโหลดไปยังบอร์ดไมโครคอนโทรลเลอร์
Keywords: Arduino Nano / Uno, Arduino Sketch, LED Blink, WokWi Simulator
▷ ตัวอย่างที่ 1#
โค้ดตัวอย่างแรกเริ่มต้นด้วยการสร้างฟังก์ชัน setup() ที่มีการกำหนดให้ขา LED_PIN 
ซึ่งตรงกับ LED_BUILTIN เป็นเอาต์พุตแบบดิจิทัล และเขียนค่าเอาต์พุตให้เป็น LOW
โดยใช้คำสั่ง digitalWrite( LED_PIN, LOW );
ถัดไปเป็นฟังก์ชัน loop() ซึ่งจะถูกเรียกให้ทำงานซ้ำโดยอัตโนมัติ
ภายในมีคำสั่งที่เรียกฟังก์ชัน led_blink1( LED_PIN ); 
ในส่วนของการสร้างฟังก์ชัน void led_blink1( int pin ) { ... } 
มีการทำคำสั่ง digitalWrite(...);
เพื่อกำหนดสถานะลอจิก HIGH หรือ LOW ของขาที่ระบุโดยตัวแปรหรืออาร์กิวเมนต์ชื่อ pin และคำสั่ง delay( DELAY_MS );
เพื่อการหน่วงเวลาไว้เท่ากับค่าตัวเลขของ DELAY_MS ก่อนทำคำสั่งต่อไปตามลำดับ
#define LED_PIN   (LED_BUILTIN) // the default LED pin
#define DELAY_MS  (500)
void setup() {
  // Set the LED pin direction to output
  pinMode( LED_PIN, OUTPUT );
  // Output LOW to the LED pin
  digitalWrite( LED_PIN, LOW );
}
void loop() {
  led_blink1( LED_PIN );
}
void led_blink1( int pin ) {
  // Output HIGH to the LED pin
  digitalWrite( pin, HIGH );
  // Delay for a few hundred milliseconds
  delay( DELAY_MS );
  // Output LOW to the LED pin
  digitalWrite( pin, LOW );
  // Delay for a few hundred milliseconds
  delay( DELAY_MS );
}

รูป: ตัวอย่างการจำลองการทำงานของโค้ดโดยใช้ WokWi Simulator
▷ ตัวอย่างที่ 2#
ในโค้ดตัวอย่างนี้ มีการสร้างฟังก์ชันและเรียกใช้ led_blink2(...) มาแทนที่และเปรียบเทียบกับฟังก์ชัน led_blink1(...) ในตัวอย่างแรก
แต่มีรูปแบบการเขียนโค้ดที่แตกต่างกัน ซึ่งมีการประกาศและใช้งานตัวแปรภายในฟังก์ชันและเป็นแบบ static ที่มีชื่อว่า state 
เพื่อใช้กำหนดสถานะลอจิกของขาเอาต์พุต LED_PIN
ตัวแปร state จะถูกสร้างขึ้นให้มีค่าเริ่มต้นเป็น 0 และสามารถเก็บค่าในหน่วยความจำได้เมื่อจบการทำงานของฟังก์ชัน
led_blink2(...) และใช้ตัวแปรนี้ได้อีก เมื่อมีการเรียกฟังก์ชันในครั้งถัดไป
คำสั่ง digitalWrite( pin, state ^= 1 ); จะทำให้ตัวแปร state เปลี่ยนค่าลอจิก (สลับค่าลอจิก
ระหว่าง 0 กับ 1) แล้วนำค่าใหม่ของตัวแปรนี้ ไปใช้ในการเขียนค่าเอาต์พุตของขา pin ดังนั้นจะทำให้ LED เปลี่ยนสถานะลอจิก
#define LED_PIN   (LED_BUILTIN) // the default LED pin
#define DELAY_MS  (500)
void setup() {
  // Set the LED pin direction to output
  pinMode( LED_PIN, OUTPUT );
  // Output LOW to the LED pin
  digitalWrite( LED_PIN, LOW );
}
void loop() {
  led_blink2( LED_PIN );
}
void led_blink2( int pin ) {
  // Use a static local variable to keep the LED state 
  static uint8_t state = 0;
  // Toggle the LED pin
  digitalWrite( pin, state ^= 1 );
  // Delay for a few hundred milliseconds
  delay( DELAY_MS );
}
▷ ตัวอย่างที่ 3#
ในโค้ดตัวอย่างนี้ มีการสร้างและเรียกใช้ฟังก์ชัน led_blink3(...) ที่มีความแตกต่างจากฟังก์ชันในตัวอย่างที่แล้ว
คำสั่ง digitalWrite( pin, !digitalRead(pin) ); เป็นการอ่านค่าของ pin ในขณะนั้น
แล้วนำค่ามาสลับลอจิก (Bit Inverse) แล้วนำไปใช้ในการเขียนค่าเอาต์พุตของขา pin 
ดังนั้นจะทำให้ LED เปลี่ยนสถานะลอจิก
#define LED_PIN   (LED_BUILTIN) // the default LED pin
#define DELAY_MS  (500)
void setup() {
  // Set the LED pin direction to output
  pinMode( LED_PIN, OUTPUT );
  // Output LOW to the LED pin
  digitalWrite( LED_PIN, LOW );
}
void loop() {
  led_blink3( LED_PIN );
}
void led_blink3( int pin ) {
  // Toggle the LED pin
  digitalWrite( pin, !digitalRead(pin) );
  // Delay for a few hundred milliseconds
  delay( DELAY_MS );
}
▷ ตัวอย่างที่ 4#
ในโค้ดตัวอย่างนี้ มีการสร้างและเรียกใช้ฟังก์ชัน led_blink4(...)
ที่มีความแตกต่างจากฟังก์ชันก่อนหน้านี้ โดยใช้วิธีการอ่านเวลาของระบบในขณะนั้น ด้วยคำสั่ง millis()
ซึ่งจะได้ค่าเป็นเลขจำนวนเต็มที่มีชนิดข้อมูลเป็น uint32_t และมีหน่วยเป็นมิลลิวินาที เก็บไว้ในตัวแปร now 
แล้วนำมาเปรียบเทียบกับค่าที่ได้เก็บบันทึกไว้ในอีกตัวแปรหนึ่งคือ last_update_time
ถ้าคำนวณผลต่างได้มากกว่าหรือเท่ากับ DELAY_MS ก็ให้สลับสถานะลอจิกของเอาต์พุต pin
โดยเรียกฟังก์ชัน led_toggle(...) แล้วอัปเดตค่าของตัวแปร last_update_time
ให้เท่ากับค่าของตัวแปร now
#define LED_PIN   (LED_BUILTIN) // the default LED pin
#define DELAY_MS  (500)
void setup() {
  // Set the LED pin direction to output
  pinMode( LED_PIN, OUTPUT );
  // Output LOW to the LED pin
  digitalWrite( LED_PIN, LOW );
}
void loop() {
  led_blink4( LED_PIN );
}
void led_toggle( int pin=LED_PIN ) {
  // Toggle the specified pin
  digitalWrite( pin, !digitalRead(pin) );
}
void led_blink4( int pin ) {
  static uint32_t last_update_time = 0;
  uint32_t now = millis();
  if ( now - last_update_time >= DELAY_MS ) {
    // Save the last update time
    last_update_time = now;
    // Toggle the LED pin
    led_toggle( pin );
  }
}
▷ ตัวอย่างที่ 5#
โค้ดตัวอย่างนี้สาธิตการเปิดใช้งานวงจร Timer1 ของชิปไมโครคอนโทรลเลอร์ ATmega328P
มีการสร้างและเรียกใช้ฟังก์ชัน void init_timer1() เพื่อตั้งค่าการทำงานของวงจร Timer1
(มีขนาดของตัวนับเท่ากับ 16 บิต) เช่น การตั้งค่าตัวหารความถี่ เพื่อทำให้วงจรนับทำงานด้วยมีความถี่ลดลงจาก 16MHz 
ให้เท่ากับ 2Hz หรือ มีคาบเท่ากับ 500 มิลลิวินาที
นอกจากนั้นแล้ว ยังมีการเปิดใช้งานอินเทอร์รัพท์ที่เกี่ยวข้องกับ Timer1 เมื่อนับครบหนึ่งรอบ 
โดยนับจาก 0 ถึง (31250-1) แล้วเริ่มต้นนับใหม่ ฟังก์ชันที่เกี่ยวข้องกับอินเทอร์รัพท์  (หรือ ISR) ของตัวนับนี้คือ ISR(TIMER1_COMPA_vect)
ซึ่งจะทำหน้าที่เพิ่มค่าของตัวแปร counter ขึ้นครั้งละหนึ่ง
ในฟังก์ชัน led_blink5(...) จะมีการเรียกฟังก์ชัน init_timer1() เพียงครั้งเดียว
และในการทำงานของฟังก์ชันเมื่อถูกเรียก จะมีการตรวจสอบดูว่า ค่าของตัวแปร counter มีการเปลี่ยนแปลงหรือไม่
โดยเปรียบเทียบกับค่าของตัวแปร last_counter ที่บันทึกค่าเพื่อการเปรียบเทียบ ถ้ามีการเปลี่ยนแปลง (ค่าของตัวแปร
last_counter ไม่เท่ากับ counter) ก็ให้สลับสถานะลอจิกของเอาต์พุต pin
แล้วอัปเดตค่าของตัวแปร last_counter ให้เท่ากับค่า counter
#define LED_PIN   (LED_BUILTIN) // the default LED pin
void setup() {
  // Set LED pin direction to output
  pinMode( LED_PIN, OUTPUT );
  // Output LOW to the LED pin
  digitalWrite( LED_PIN, LOW );
}
void loop() {
  led_blink5( LED_PIN );
}
void led_toggle( int pin=LED_PIN ) {
  // Toggle the specified pin
  digitalWrite( pin, !digitalRead(pin) );
}
// This global variable is used by ISR.
volatile uint32_t counter = 0;
void led_blink5( int pin ) {
  static uint8_t timer1_initialized = 0;
  static uint32_t last_counter = 0;
  if ( timer1_initialized == 0 ) { // For the first function call
    // Mark timer1 as initialized
    timer1_initialized = 1;
    // Initialize Timer1
    init_timer1();
  }
  else if ( last_counter != counter ) {
    // Save the last counter value
    last_counter = counter;
    // Toggle the LED pin
    led_toggle( pin );
  }
}
void init_timer1() {
  // Set Timer1 to CTC (Clear Timer on Compare) mode
  TCCR1A = 0;
  TCCR1B = (1 << WGM12);
  // Set the timer compare value to generate an interrupt every 500 msec
  OCR1A  = (31250-1); // (16MHz /256 /2) - 1
  // Enable Timer1 compare interrupt
  TIMSK1 = (1 << OCIE1A);  
  // Set Timer1 prescaler to 256 and start the timer
  TCCR1B |= (1 << CS12);
  // Enable interrupts
  sei();
}
ISR(TIMER1_COMPA_vect) {
  counter++;
}
▷ ตัวอย่างที่ 6#
โค้ดตัวอย่างนี้สาธิตการใช้ไลบรารีสำหรับ Arduino ที่มีชื่อว่า
TimerOne
โดยจะต้องตั้งค่าสำหรับช่วงเวลาในการทำงาน และฟังก์ชัน Callback ที่จะถูกเรียกให้ทำงานเมื่อเกิดเหตุการณ์ดังกล่าว
- Timer1.initialize( 500000 );เป็นการตั้งค่าเวลา (ช่วงเวลาหรือคาบเวลา) ให้ทำงานทุก ๆ 500000 ไมโครวินาที หรือ 500 มิลลิวินาที
- Timer1.attachInterrupt( led_toggle );เป็นการระบุว่า ให้ใช้ฟังก์ชัน- led_toggleเป็นฟังก์ชันสำหรับ Callback ที่จะทำงานตามคาบเวลาที่กำหนดไว้
#include <TimerOne.h> // if the TimerOne library is used.
#define LED_PIN   (LED_BUILTIN) // the default LED pin
void setup() {
  // Set LED pin direction to output
  pinMode( LED_PIN, OUTPUT );
  // Output LOW to the LED pin
  digitalWrite( LED_PIN, LOW );
}
void loop() {
  led_blink6( LED_PIN );
}
void led_toggle() {
  // Toggle the specified pin
  digitalWrite( LED_PIN, !digitalRead(LED_PIN) );
}
void led_blink6( int pin ) {
  static uint8_t timer1_initialized = 0;
  if ( timer1_initialized == 0 ) { // For the first function call
    // Mark timer1 as initialized
    timer1_initialized = 1;
    // Set period in microseconds
    Timer1.initialize( 500000 ); 
    // Attach a callback function
    Timer1.attachInterrupt( led_toggle );
  }
}
▷ ตัวอย่างที่ 7#
โค้ดตัวอย่างนี้สาธิตการเปิดใช้งานวงจร WDT (Watchdog Timer) ของ ATmega328P
ซึ่งเป็นวงจรตัวนับ และมีการสร้างและเรียกใช้ฟังก์ชัน init_wdt(...)
เพื่อตั้งค่าให้นับแล้วทำให้เกิดอินเทอร์รัพท์ ทุก 500 มิลลิวินาที (โดยประมาณ)  และมีฟังก์ชัน ISR(WDT_vect)
ที่จะทำงานโดยอัตโนมัติ เมื่อเกิดอินเทอร์รัพท์ในแต่ละครั้ง ซึ่งจะเพิ่มค่าของตัวแปร counter ครั้งละหนึ่ง
ในฟังก์ชัน led_blink7(...) จะมีการเปรียบเทียบระหว่างค่าของตัวแปร
last_counter และ counter ถ้ามีค่าไม่เท่ากัน ให้สลับค่าลอจิกของขาเอาต์พุต pin
แล้วอัปเดตค่าของตัวแปร last_counter ให้เท่ากับค่า counter
#include <avr/wdt.h>  // if WDT is used.
#define LED_PIN   (LED_BUILTIN) // the default LED pin
// This global variable is used by ISR.
volatile uint32_t counter = 0;
void setup() {
  // Set LED pin direction to output
  pinMode( LED_PIN, OUTPUT );
  // Output LOW to the LED pin
  digitalWrite( LED_PIN, LOW );
}
void loop() {
  led_blink7( LED_PIN );
}
void led_toggle( int pin=LED_PIN ) {
  // Toggle the specified pin
  digitalWrite( pin, !digitalRead(pin) );
}
void led_blink7( int pin ) {
  static uint8_t wdt_initialized = 0;
  static uint32_t last_counter = 0;
  if ( wdt_initialized == 0 ) {
    // Mark WDT as initialized
    wdt_initialized = 1;
    // Initialize WDT
    init_wdt();
  }
  else if ( last_counter != counter ) {
    // Save the last counter value
    last_counter = counter;
    // Toggle the LED pin
    led_toggle( pin );
  }
}
void init_wdt() {
  // Make sure the WDT is disabled before changing the settings
  wdt_reset(); 
  // Set up WDT with 500ms timeout (approximately).
  wdt_enable(WDTO_500MS);
  // Enable WDT interrupt.
  WDTCSR |= (1<<WDIE);
  // Reset the WDT
  wdt_reset(); 
  // Enable interrupts
  sei(); 
}
ISR(WDT_vect) {
  counter++;
}

รูป: การวัดสัญญาณเอาต์พุตด้วยออสซิลโลสโคป (RIGOL DS1054Z) ที่ได้จากการทำงานของโค้ดตัวอย่างที่ 7
▷ ตัวอย่างที่ 8#
โค้ดตัวอย่างนี้สาธิตการใช้งานวงจร Timer1 ในโหมดการสร้างสัญญาณ Fast PWM (Pulse Width Modulation)
โดยตั้งค่าตัวหารความถี่ (Prescaler) ให้เท่ากับ 256 และกำหนดค่าในรีจิสเตอร์ ICR1 มีค่าสูงสุดเป็น (62500-1)  หรือคาบของการนับขึ้น
ตัวเลข 62500  คำนวณได้จากความถี่ของซีพียู (16MHz) หารด้วย 256
ค่าในรีจิสเตอร์ OCR1A จะต้องถูกกำหนดให้เป็นครึ่งหนึ่งของค่าในรีจิสเตอร์ ICR1 เพื่อทำให้ได้สัญญาณ PWM
ที่มีค่า Duty Cycle เท่ากับ 50%
ข้อสังเกต: สัญญาณเอาต์พุต PWM ที่ได้จากการทำงานของ Timer1 ในตัวอย่างนี้ ตรงกับขา Arduino D9 Pin
#define LED_PIN   (9)
void setup() {
  // Set the output pin for PWM
  pinMode(LED_PIN, OUTPUT);
  // Initialize Timer1 for PWM output on D9 pin
  init_timer1_pwm();
}
void loop() {
  // empty
}
void init_timer1_pwm() {
  // Set prescaler to 256.
  // Use fast-PWM, non-inverting mode 14: WGM1[3:0]="1110".
  // Set OC1A pin at BOTTOM (0x0000). 
  // Clear OC1A pin on Compare Match.
  TCCR1A = (1 << COM1A1) | (1 << WGM11);
  TCCR1B = (1 << WGM13)  | (1 << WGM12) | (1 << CS12);
  // Set top value: 16MHz/(256)/1Hz = 62500.
  ICR1 = 62500-1; 
  // Use 50% duty cycle
  OCR1A = ICR1 / 2;
}

รูป: การวัดสัญญาณเอาต์พุตด้วยออสซิลโลสโคป (RIGOL DS1054Z) ที่ได้จากการทำงานของโค้ดตัวอย่างที่ 8
▷ ตัวอย่างที่ 9#
ในตัวอย่างนี้ มีการสาธิตการใช้ไลบรารี "Arduino FreeRTOS" (Arduino_FreeRTOS.h)
สำหรับ Arduino Uno / Nano / MEGA
และสร้างทาสก์ (FreeRTOS Task) โดยใช้คำสั่ง xTaskCreate(...)
และมีการสร้างฟังก์ชัน led_toggle_task(...) สำหรับทาสก์ดังกล่าว เพื่อให้ทำหน้าที่สลับสถานะลอจิกของขาเอาต์พุตด้วยอัตราคงที่ (ประมาณ 512 มิลลิวินาที)
นอกจากนั้นแล้วยังมีการสาธิตการสร้างคลาสที่มีชื่อว่า LED ในภาษา C++ 
เพื่อใช้ในการกำหนดหรือเปลี่ยนสถานะลอจิกของขาเอาต์พุตสำหรับ LED  
#include <Arduino_FreeRTOS.h> // if the Arduino FreeRTOS library is used.
#define LED_PIN   (LED_BUILTIN)
#define DELAY_MS  (500)
// Define a LED class with inline member functions
class LED {
  public:
    LED(int pin, int state=0) : _pin(pin), _state(state) { // Constructor
      pinMode( _pin, OUTPUT );
      digitalWrite( _pin, _state ); 
    }
    int toggle() { // Toggle the state of the LED pin
      digitalWrite(_pin, _state^=1 ); 
      return _state;
    }
    LED& operator=(bool value) { // Implemen an assignment operator
      _state = value;
      digitalWrite(_pin, _state);
      return *this;
    }
    operator bool() const { // Return the current state of the LED pin
      return _state;
    }
  private: 
    int _pin;   // Used to keed Arduino pin number for the LED pin
    int _state; // Used to keep the current LED state
};
// Create an LED object
LED led(LED_PIN);
void led_toggle_task(void *pvParameters) {
  LED _led = *((LED *)pvParameters);
  while (1) {
    // Toggle the LED state
    //_led.toggle(); // Method 1
    _led = !_led;   // Method 2
    // Delay for a few OS ticks
    vTaskDelay( DELAY_MS / portTICK_PERIOD_MS );
  }
}
void setup( ) {
  // Create a FreeRTOS task which is used to toggle the LED pin
  xTaskCreate(led_toggle_task,  /* task function */
              "task",           /* task name */
              96,               /* task stack size */
              (void*)&led,      /* task function's parameter */
              1,                /* task priority */
              NULL              /* task handle */ 
  ); 
}
void loop() {  
  // empty or yield() 
  yield();
}
▷ กล่าวสรุป#
บทความนี้นำเสนอตัวอย่างการเขียนโค้ด Arduino Sketch ในรูปแบบที่แตกต่างกันเพื่อทำให้ขาเอาต์พุต สำหรับ LED เปลี่ยนสถานะลอจิก ทำให้ LED กระพริบได้ และจะเห็นได้ว่า บางวิธีก็จะเจาะจงใช้วงจรภายในของชิป ไมโครคอนโทรลเลอร์ ATmega328P เช่น วงจรตัวนับ Timer1 และ WDT เป็นต้น
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
Created: 2023-03-30 | Last Updated: 2023-04-01