คำสั่ง 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 ไม่ดีพอ
Comments