top of page

ถ้าไม่ delay() แล้วจะยังไง


คำสั่ง delay() หรือ หน่วงเวลา เป็นคำสั่งหยุดการทำงานของโปรแกรมตามเวลาที่กำหนด เช่น การทำไฟกระพริบ ที่ไฟติดดับตามเวลา มักจะใช้คำสั่ง delay() เข้ามากำหนดเวลาที่ไฟจะติด-ดับ อย่างไรก็ตาม delay() เป็นคำสั่งแบบ blocking คือระหว่างที่คำสั่ง delay() ทำงาน คำสั่งอื่น ๆ จะไม่สามารถทำงานได้เลย หากเขียนโปรแกรมที่ต้องการให้หลาย ๆ โปรแกรมทำงานพร้อมกันตามเวลา จะเขียนโปรแกรมได้ยุ่งยากมาก ในบทความนี้จึงนำเสนอ 3 วิธี ยกเลิกใช้คำสั่ง delay() แต่โปรแกรมยังทำงานตามเวลาอยู่ และโปรแกรมอื่น ๆ ยังทำงานไปพร้อม ๆ กันได้ โดยในบทความนี้ใช้บอร์ด Arduino Uno ในการทดสอบ

วิธีที่ 1 คำสั่ง millis()

คำสั่ง millis() เป็นคำสั่งที่จะให้ค่าเวลาปัจจุบันนับตั้งแต่ CPU เริ่มทำงานมา ในหน่วยมิลลิวินาที เช่น เสียบไฟเข้าบอร์ดครั้งแรก คำสั่ง millis() จะให้ค่า 0 แต่เมื่อเสียบค้างไว้ 10 วินาที จะให้ค่า 10 000 และเสียบค้างไว้อีกเรื่อย ๆ จนครบ 30 วิ คำสั่ง millis() ก็จะให้ค่ามาเป็น 30 000 การใช้งานจริงจะใช้ร่วมกับตัวแปร โดยจะบันทึกเวลาที่ทำงานล่าสุดไว้ นำมาลบกับเวลาปัจจุบัน เปรียบเทียบกับเวลาที่ตั้ง หากเงื่อนไขเป็นจริง โปรแกรมในปีกกาจะทำงาน ตัวอย่างการเขียนโปรแกรมไฟกระพริบโดยใช้คำสั่ง millis() แสดงดังด้านล่าง

#include <Arduino.h>

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

void loop() {
  static unsigned long timer1 = 0; // ตั้งตัวแปร timer1 เก็บค่าเวลาที่ทำงานล่าสุด
  if ((timer1 == 0) || ((millis() - timer1) > 1000)) { // ตรวจสอบว่าครบเวลาแล้วหรือยัง โดยนำเวลาปัจจุบันลบเวลาล่าสุด เทียบกับเวลาที่ตั้ง (1000 คือเวลาที่ตั้ง คือ 1 วินาที)
    timer1 = millis(); // อัพเดทเวลาที่ทำโปรแกรมล่าสุด
    digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); // สลับสถานะการทำงานของหลอดแอลอีดีบนบอร์ด
  }
}

หากต้องการเพิ่มโปรแกรมที่ต้องการให้ทำงานควบคู่กับโปรแกรมเดิม ทำได้โดยก๊อบชุดโค้ดมาแก้ตัวแปร ตัวอย่างการเพิ่มโค้ดชุดที่ 2 แสดงข้อความ Hello! ทุก ๆ 2 วินาที

#include <Arduino.h>

void setup() {
  Serial.begin(9600);
  pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
  static unsigned long timer1 = 0; // ตั้งตัวแปร timer1 เก็บค่าเวลาที่ทำงานล่าสุด
  if ((timer1 == 0) || ((millis() - timer1) > 1000)) { // ตรวจสอบว่าครบเวลาแล้วหรือยัง โดยนำเวลาปัจจุบันลบเวลาล่าสุด เทียบกับเวลาที่ตั้ง (1000 คือเวลาที่ตั้ง คือ 1 วินาที)
    timer1 = millis(); // อัพเดทเวลาที่ทำโปรแกรมล่าสุด
    digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); // สลับสถานะการทำงานของหลอดแอลอีดีบนบอร์ด
  }
  
  // โปรแกรมที่ 2
  static unsigned long timer2 = 0; // ตั้งตัวแปร timer2 เก็บค่าเวลาที่ทำงานล่าสุด
  if ((timer2 == 0) || ((millis() - timer2) > 2000)) { // ตรวจสอบว่าครบเวลาแล้วหรือยัง โดยนำเวลาปัจจุบันลบเวลาล่าสุด เทียบกับเวลาที่ตั้ง (2000 คือเวลาที่ตั้ง คือ 1 วินาที)
    timer2 = millis(); // อัพเดทเวลาที่ทำโปรแกรมล่าสุด
    Serial.println("Hello !");
  }
}

วิธีที่ 2 ใช้ไลบารี่ Ticker

ไลบารี่ Ticker เทียบเท่ากับการใช้ millis() (หลักการทำงานเดียวกัน) แต่โค้ดโดยรวมเข้าใจได้ง่ายมากกว่า มีการจัดการได้ง่ายกว่า โดยการใช้งานคือนำโค้ดส่วนที่ต้องการให้ทำงานซ้ำ ๆ ตามเวลาไปสร้างเป็นฟังก์ชั่น แล้วให้ไลบารี่ Ticker ไปเรียกใช้ฟังก์ชั่นตามเวลาที่กำหนด

ติดตั้งไลบารี่ Ticker ตามขั้นตอน แล้วใช้โค้ดโปรแกรมไฟกระพริบต่อไปนี้ในการทดสอบ

#include "Ticker.h"

void blink() { // สร้างฟังก์ชั่น blink เก็บโค้ดส่วนที่จะให้ทำงานตามเวลา
  digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); // กำหนดหลอดแอลอีดีติด-ดับสลับกัน
}

Ticker timer1(blink, 300); // สร้าง Ticker พร้อมกำหนดให้เรียกฟังก์ชั่น blink ทำงานทุก ๆ 0.3 วินาที (300 mS = 0.3 S) ตั้งชื่อ timer1

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  Serial.begin(9600);

  timer1.start(); // สั่งให้ timer1 ทำงาน
}

void loop() {
  timer1.update(); // ใช้คำสั่ง .update() เพื่อให้ Ticker ทำงานถูกต้อง
}

ผลที่ได้คือไฟกระพริบทุก ๆ 0.3 วินาที

การเพิ่ม Ticker ทำได้โดยสร้างฟังก์ชั่นเพิ่ม สร้างออปเจค Ticker เพิ่ม แล้วเพิ่ม .start() และ .update() ตัวอย่างเพิ่ม Ticker แสดงผลข้อความ Hello !!! ใน Serial Monitor ทุก ๆ 2 วินาที แสดงดังโค้ดด้านล่าง

#include "Ticker.h"

void blink() { // สร้างฟังก์ชั่น blink เก็บโค้ดส่วนที่จะให้ทำงานตามเวลา
  digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); // กำหนดหลอดแอลอีดีติด-ดับสลับกัน
}

void printHello() {
  Serial.println("Hello !!!");
}

Ticker timer1(blink, 300); // สร้าง Ticker พร้อมกำหนดให้เรียกฟังก์ชั่น blink ทำงานทุก ๆ 0.3 วินาที (300 mS = 0.3 S) ตั้งชื่อ timer1
Ticker timer2(printHello, 2000); // สร้าง Ticker พร้อมกำหนดให้เรียกฟังก์ชั่น printHello ทำงานทุก ๆ 0.3 วินาที (300 mS = 0.3 S) ตั้งชื่อ timer2

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  Serial.begin(9600);

  timer1.start(); // สั่งให้ timer1 ทำงาน
  timer2.start(); // สั่งให้ timer2 ทำงาน
}

void loop() {
  timer1.update(); // ใช้คำสั่ง .update() เพื่อให้ Ticker ทำงานถูกต้อง
  timer2.update(); // ใช้คำสั่ง .update() เพื่อให้ Ticker ทำงานถูกต้อง
}

ผลที่ได้คือไฟกระพริบทุก ๆ 0.3 วินาที และมีคำว่า Hello !!! แสดงบน Serial Monitor ทุก ๆ 2 วินาที

วิธีที่ 3 ใช้ Timer

Timer เป็นวงจรนับเวลาที่มีอยู่ในไมโครคฮนโทรลเลอร์ทุกตัว แต่ละตัวจะมี Timer ไม่เหมือนกัน การเขียนโปรแกรมใช้งาน Timer ทำได้ทั้งเขียนลงรีจีสเตอร์ตรง ๆ และใช้ไลบารี่ สำหรับ Arduino Uno ไลบารี่ที่ใช้ได้คือ TimerOne โดยตัวอย่างการใช้ TimerOne ทำไฟกระพริบทุก ๆ 0.3 วินาที แสดงด้านล่าง

#include "TimerOne.h"

void blink() { // สร้างฟังก์ชั่น blink เก็บโค้ดส่วนที่จะให้ทำงานตามเวลา
  digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); // กำหนดหลอดแอลอีดีติด-ดับสลับกัน
}


void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  Timer1.initialize(300000); // กำหนด Timer1 ทำงานทุก ๆ 0.3 วินาที (300 mS = 300_000 uS)
  Timer1.attachInterrupt(blink); // กำหนดให้ Timer1 เรียกฟังก์ชั่น blink 
}

void loop() {

}

ข้อเสียประการสำคัญของการใช้ Timer คือ Timer มักจะถูกผูกเข้ากับขา PWM ต่าง ๆ บนบอร์ด Arduino ด้วย ทำให้หากใช้ Timer ในลักษณะกำหนดเวลาโปรแกรมทำงาน จะทำให้บางขาไม่สามารถใช้งาน PWM ได้ รวมทั้ง Timer มักมีให้ใช้จำนวนน้อย (บนบอร์ด Arduino มีให้ใช้เพียง 3 ตัว) ทำให้รันหลาย ๆ โปรแกรมพร้อม ๆ กันไม่ได้

วิธีที่ 4 ใช้ FreeRTOS

FreeRTOS เป็น RTOS (Real Time Operating System) โดยเป็นไลบารี่/ซอฟแวร์ ที่ออกแบบมาเพื่อช่วยจัดการการทำงานของโค้ดโปรแกรมที่ต้องทำงานพร้อม ๆ กันหลายโปรแกรม (เรียกว่า Multitasking, แต่ละโปรแกรมเรียกว่า Task) โดยเฉพาะ นอกจากนี้ยังมีคำสั่งสื่อสารระหว่างโค้ดโปรแกรม (ระหว่าง Task) รวมทั้งมีระบบบริหารจัดการการเข้าใช้ทรัพยากรจากหลาย Task พร้อมกันอีกด้วย

สำหรับในบทความนี้จะแนะนำการใช้งาน FreeRTOS สร้าง Task เบื้องต้นเท่านั้น ส่วนอื่น ๆ ของ FreeRTOS จะนำเสนอในโอกาศต่อไป

การสร้าง Task ใน FreeRTOS ทำได้โดยใช้คำสั่ง xTaskCreate โดยเปรียบเสมือนการสร้าง void setup(), void loop() ขึ้นมาใหม่ได้เรื่อย ๆ ภายในแต่ละ Task สามารถใช้ delay() หน่วงเวลาตามปกติได้เลย

ตัวอย่างการสร้าง Task ไฟกระพริบทุก ๆ 0.3 วินาที แสดงดังโค้ดด้านล่าง

#include "Arduino.h"
#include <Arduino_FreeRTOS.h>

TaskHandle_t blinkTaskHandler; // สร้างตัวแปร TaskHandler ใช้จัดการ blinkTask ในอนาคต

void blinkTask(void*) { // สร้างฟังก์ชั่น blinkTask เก็บโค้ดส่วนที่จะให้ทำงานใน Task นี้
  // โค้ดโปรแกรมที่ปกติอยู่ใน void setup()
  pinMode(LED_BUILTIN, OUTPUT);

  while(1) { // โค้ดโปรแกรมที่ปกติอยู่ใน void loop() นำมาใส่ภายใต้ปีกกานี้แทน
    digitalWrite(LED_BUILTIN, HIGH); // กำหนดหลอดแอลอีดีติด
    delay(300); // หน่วงเวลา 0.3 วินาที (300 mS = 0.3 S)
    digitalWrite(LED_BUILTIN, LOW); // กำหนดหลอดแอลอีดีดับ
    delay(300); // หน่วงเวลา 0.3 วินาที (300 mS = 0.3 S)
  }
  vTaskDelete(NULL);
}

void printHello(void*) {
  Serial.println("Hello !!!");
}

void setup() {
  xTaskCreate(
    blinkTask, // นำฟังก์ชั่น blinkTask มาใช้สร้างเป็น Task ใหม่
    "blinkTask", // กำหนดชื่อ Task นี้เป็น blinkTask
    200, // ขนาดของ Stack (แรม) สูงสุดที่ Task นี้ร้องขอได้
    NULL,
    10, // คำสำคัญของ Task นี้ โดยค่ายิ่งมาก ยิ่งสำคัญมาก Task ที่สำคัญมากกว่าจะได้โอกาศทำงานมากกว่า
    &blinkTaskHandler // ชี้ตัวแปร TaskHandler ใช้จัดการ Task ในอนาคต
  );
}

void loop() {

}

การสร้าง Task ทำได้โดยสร้างตัวแปรชนิด TaskHandle_t เพิ่ม สร้างฟังก์ชั่นที่ใช้เก็บโค้ดเป็น Task ใหม่เพิ่ม และใช้คำสั่ง xTaskCreate() สร้าง Task เพิ่ม ตัวอย่างการสร้าง Task ชื่อ printHelloTask แสดงผลข้อความ Hello !!! ใน Serial Monitor ทุก ๆ 2 วินาที แสดงดังโค้ดด้านล่าง

#include "Arduino.h"
#include <Arduino_FreeRTOS.h>

TaskHandle_t blinkTaskHandler; // สร้างตัวแปร TaskHandler ใช้จัดการ blinkTask ในอนาคต
TaskHandle_t printHelloTaskHandler; // สร้างตัวแปร TaskHandler ใช้จัดการ printHelloTask ในอนาคต

void blinkTask(void*) { // สร้างฟังก์ชั่น blinkTask เก็บโค้ดส่วนที่จะให้ทำงานใน Task นี้
  // โค้ดโปรแกรมที่ปกติอยู่ใน void setup()
  pinMode(LED_BUILTIN, OUTPUT);

  while(1) { // โค้ดโปรแกรมที่ปกติอยู่ใน void loop() นำมาใส่ภายใต้ปีกกานี้แทน
    digitalWrite(LED_BUILTIN, HIGH); // กำหนดหลอดแอลอีดีติด
    delay(300); // หน่วงเวลา 0.3 วินาที (300 mS = 0.3 S)
    digitalWrite(LED_BUILTIN, LOW); // กำหนดหลอดแอลอีดีดับ
    delay(300); // หน่วงเวลา 0.3 วินาที (300 mS = 0.3 S)
  }
  vTaskDelete(NULL);
}

void printHelloTask(void*) { // สร้างฟังก์ชั่น printHelloTask เก็บโค้ดส่วนที่จะให้ทำงานใน Task นี้
  // โค้ดโปรแกรมที่ปกติอยู่ใน void setup()
  Serial.begin(9600);

  while(1) { // โค้ดโปรแกรมที่ปกติอยู่ใน void loop() นำมาใส่ภายใต้ปีกกานี้แทน
    Serial.println("Hello !!!");
    delay(3000); // หน่วงเวลา 3 วินาที (3000 mS = 3 S)
  }
  vTaskDelete(NULL);
}

void setup() {
  xTaskCreate(
    blinkTask, // นำฟังก์ชั่น blinkTask มาใช้สร้างเป็น Task ใหม่
    "blinkTask", // กำหนดชื่อ Task นี้เป็น blinkTask
    200, // ขนาดของ Stack (แรม) สูงสุดที่ Task นี้ร้องขอได้
    NULL,
    10, // คำสำคัญของ Task นี้ โดยค่ายิ่งมาก ยิ่งสำคัญมาก Task ที่สำคัญมากกว่าจะได้โอกาศทำงานมากกว่า
    &blinkTaskHandler // ชี้ตัวแปร TaskHandler ใช้จัดการ Task ในอนาคต
  );

  xTaskCreate(
    printHelloTask, // นำฟังก์ชั่น blinkTask มาใช้สร้างเป็น Task ใหม่
    "printHelloTask", // กำหนดชื่อ Task นี้เป็น blinkTask
    100, // ขนาดของ Stack (แรม) สูงสุดที่ Task นี้ร้องขอได้
    NULL,
    9, // คำสำคัญของ Task นี้ โดยค่ายิ่งมาก ยิ่งสำคัญมาก Task ที่สำคัญมากกว่าจะได้โอกาศทำงานมากกว่า
    &printHelloTaskHandler // ชี้ตัวแปร TaskHandler ใช้จัดการ Task ในอนาคต
  );
}

void loop() {

}

การเปลี่ยนมาใช้ FreeRTOS อาจจะดูใช้งานง่าย เพราะโค้ดเดิม ๆ สามารถยกมาใช้งานได้เลยโดยไม่ต้องเปลี่ยนแนวคิด อย่างไรก็ตามการทำงานรูปแบบ OS ความซับซ้อนจะเพิ่มขึ้นอย่างมากเมื่อต้องกำหนดขนาดแรม (Stack size) ให้เพียงพอกับทุกงาน และหากมีการสื่อสารระหว่างงานต้องนำแนวคิด Flag, Mutex, Queue และอื่น ๆ มาใช้เพิ่มเติม นอกจากนี้การใช้ระบบ OS บางครั้งอาจจะทำให้สเถียรภาพของระบบลดลงหากมีการจัดการแรมและขั้นตอนการทำงานในแต่ละ Task ไม่ดีพอ


ดู 1,460 ครั้ง0 ความคิดเห็น

コメント


bottom of page