เลิกใช้ Delay แบบไร้สาระกันเสียทีเถอะ

แนะนำกันก่อน
เรื่องนี้เริ่มต้นจากการสังเกตปัญหายอดนิยมในการพัฒนาโปรแกรมบน Arduino หรือผู้เริ่มต้นเรียนรู้การพัฒนาโปรแกรมบนภาษาอื่นๆ เช่น C หรือ Python ที่มักจะมีการใช้ฟังก์ชันในการรอเวลา (Time Delay) ไม่ว่าจะเป็นการรอเวลาเพื่อตอบกลับการร้องขอ การรอเวลาให้ระบบปลายทางพร้อมทำงาน หรือการรอให้ครบกำหนดเวลาในการทำงาน เป็นต้น
ซึ่งการรอเวลานั้นมีความจำเป็นในการพัฒนาโปรแกรมอยู่มาก เพราะระบบจะทำงานได้ดี มีความจำเป็นที่จะต้องทำให้อุปกรณ์ที่เชื่อมต่อกันอยู่ ทำงานประสานกันได้ดี แต่ในขณะเดียวกัน ถ้าหากในระบบมีการรอเวลากันอย่างมากมาย ก็จะกระทบกับการทำงานของระบบเช่นกัน ทั้งทำให้ระบบทำงานได้ช้า ตอบสนองช้า และความล่าช้านี้อาจจะก่อให้เกิดปัญหากับเสถียรภาพในการทำงานของระบบโดยรวมด้วย ดังนั้นแล้วการใช้ฟังก์ชันเพื่อรอเวลานั้น มีข้อควรระวังในการใช้งานอยู่ไม่น้อย
แต่การที่เราจะเลี่ยงไม่ใช้ฟังก์ชันรอเวลาพวกนี้ มันไม่ได้ง่ายเลย มันจะต้องเริ่มต้นด้วยการวางแผนอย่างเป็นระเบียบ เพื่อทำให้ระบบที่เราพัฒนาสามารถทำงานร่วมกันได้ ดังนั้นในบทความนี้จะแนะนำแนวคิดในการพัฒนาโปรแกรมที่ลดการใช้ฟังก์ชันรอเวลาเหล่านี้ลงกัน
เริ่มต้นด้วยไฟกระพริบ
ในบทความตอนนี้จะพัฒนาไฟกระพริบด้วยโปรแกรม Arduino โดยใช้บอร์ด ESP32 DOIT-Devkit-V1 ในการพัฒนา และจะแบ่งออกเป็น 4 ส่วนดังนี้
- ไฟกระพริบปกติ ที่ใช้ Delay โดยจะติด 1 วินาที และดับ 1 วินาที สลับกันไป
- ไฟกระพริบที่ไม่ใช้ Delay โดยจะติด 1 วินาที และดับ 1 วินาที สลับกันไป
- ไฟกระพริบ ที่ใช้ Delay โดยเพิ่มปุ่มกด เพื่อ Start/Stop ไฟกระพริบ
- ไฟกระพริบที่ไม่ใช้ Delay โดยเพิ่มปุ่มกด เพื่อ Start/Stop ไฟกระพริบ
ทั้ง 4 แบบนี้ จะทำให้เราเห็นเงื่อนไขว่า การพัฒนาโปรแกรมไฟกระพริบ แบบที่ใช้ และไม่ใช้ Delay นั้นต่างกันอย่างไร และเมื่อมีการเพิ่มปุ่มกด เพื่อควบคุมหลอดไฟแล้ว มีความแตกต่างกันอย่างไร
เตรียมบอร์ด
สิ่งที่ต้องใช้คือ Arduino Compatible board สักรุ่นหนึ่ง
LED Board หรือ หลอด LED ที่สามารถใช้งานได้ จำนวน 2 หลอด โดยในโปรแกรมนั้นต่อกับขา 4 เพื่อทำไฟกระพริบ และอีก 1 หลอด ต่อกับ Button Switch 1 หลอด เพื่อแสดงสถานะให้เห็นว่ามีการกดหรือไม่
Button Switch สำหรับควบคุมบอร์ด 1 switch
ไฟกระพริบปกติที่ใช้ Delay
เป็นโค้ดที่ธรรมดา และพื้นฐานมากที่สุดแบบหนึ่งคือ เริ่มที่ตั้งค่าขาที่ต่อกับ LED ให้เป็น LOW เพื่อให้ LED ติดสว่าง จากนั้นรอไป 1 วินาที แล้วค่อยตั้งค่าขาที่ต่อกับ LED ให้เป็น HIGH อีกครั้งหนึ่ง เพื่อให้ LED ดับลง แล้วก็รอไปอีก 1 วินาที วนๆไปแบบนี้
โดยปกติเมื่อเขียนออกมาเป็น Chart ก็จะเป็นประมาณนี้

และกลายเป็นโค้ดแบบนี้
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | define LED_PIN 4 define BTN_PIN 18 void setup() { // put your setup code here, to run once: pinMode(LED_PIN, OUTPUT); pinMode(BTN_PIN, INPUT); } void loop() { // put your main code here, to run repeatedly: digitalWrite(LED_PIN, LOW); //turn LED ON delay(1000); digitalWrite(LED_PIN, HIGH); //turn LED ON delay(1000); } |
เมื่อรันโปรแกรมแล้ว เราก็จะได้ไฟกระพริบแบบง่ายๆ แบบนี้ครับ

ทีนี้เราลองมาใช้อีกวิธีกันดูบ้าง
ไฟกระพริบที่ไม่ใช้ Delay
เมื่อเราไม่คิดจะใช้ฟังก์ชัน Delay เราต้องหาระบบเวลามากำกับเพื่อให้ระบบสามารถวัดจังหวะเวลาได้ ใน Arduino ซึ่งเราจะใช้ฟังก์ชันแบบนี้เป็นฐานเวลาในการทำงานของโปรแกรม
ฟังก์ชันฐานเวลา คือ สิ่งที่ต้องใช้
สำหรับ Arduino จะมีฟังก์ชันที่ชื่อ millis() ซึ่งจะมีระบบนับเวลาในฟังชั่น และผลที่ได้คือ ค่าเวลา ที่นับจากเริ่มต้น เช่นเมื่อระบบเริ่มต้นผ่านไป 1 วินาที ค่าที่จะได้มีค่า 1000 และเมื่อผ่านไปอีก 1 วินาที ค่าที่ได้จะเป็น 2000 และจะเพิ่มขึ้นไปเรื่อยๆ ค่าสูงสุดที่นับได้คือ 4,294,967,295 หรือประมาณ 710 วัน เมื่อค่าเกินจำนวนนี้ เวลาจะกลับไปเริ่มนับจาก 0 อีกครั้ง ฟังก์ชันนี้จึงเหมาะสมอย่างมากที่จะเอามาใช้กำกับฐานเวลาในการพัฒนาโปรแกรม
แนวคิดในการพัฒนาโปรแกรม คือ สิ่งต้องเปลี่ยน
จากไดอะแกรมตัวอย่างไฟกระพริบแบบที่ใช้ Delay ที่เรียกว่า Flow Chart ดูจะไม่ค่อยเหมาะสมกับการออกแบบในระบบฐานเวลาแบบนี้ (ออกแบบได้ แต่ค่อนข้างยุ่งยาก และต้องคิดอะไรซับซ้อนอย่างมาก) วิธีที่เห็นว่าจะเหมาะสมกว่า ควรจะเป็น แนวคิดแบบ State-Machine Diagram ที่ใช้การอ้างอิงจาก Event ที่เกิดขึ้น เพื่อเปลี่ยน State ปัจจุบันไปยัง State ต่างๆของระบบ

ดังนั้นเราจึงสามารถวาง Event ของระบบให้ขึ้นกับฐานเวลาได้ ดังเช่นในตัวอย่างของไฟกระพริบนี้

ในไดอะแกรมนี้จะเห็นได้ว่า Event ปกติคือการวนอ่าน ค่าเวลา จากฟังก์ชัน millis() มาเก็บไว้ใน thistime
ส่วนสถานะของ LED นั้นจะถูกควบคุม ON หรือ OFF ด้วย Event 2 อย่างคือ
- ค่าเวลาใน thistime
- สถานะปัจจุบันของ LED
ถ้าหาก ฐานเวลาหารลงตัวกับ 1000 ก็จะต้องมาดูว่า สถานะของ LED นั้น ON อยู่หรือ OFF อยู่
ถ้าหาก ON อยู่ ระบบจะต้องสั่งให้สถานะกลายเป็น OFF ระบบจะต้องไปที่ฟังก์ชันสีฟ้า
แต่ถ้าหาก OFF อยู่ ระบบจะต้องสั่งสถานะให้ ON ระบบจะต้องไปที่สีส้ม
จากเงื่อนไขเหล่านี้ ก็นำมาพัฒนาเป็นโปรแกรมได้แบบนี้
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | define LED_PIN 4 define BTN_PIN 18 define BTN_PRESS HIGH int operate = HIGH; int btn = LOW; unsigned long thisTime = 0; unsigned long task1time = 0; unsigned long task1period = 20; unsigned long task2time = 0; unsigned long task2period = 1000; int LEDState = HIGH; void setup() { // put your setup code here, to run once: Serial.begin(19200); pinMode(LED_PIN, OUTPUT); pinMode(BTN_PIN, INPUT); Serial.print("Start : "); Serial.println(operate); } void loop() { // put your main code here, to run repeatedly: if (thisTime != millis()){ thisTime = millis(); } if ((thisTime % task1period == 0) &&(thisTime != task1time)){ task1time = thisTime; // Serial.print("do 1st task - task1time: "); // Serial.println(task1time); } if ((thisTime % task2period == 0) &&(thisTime != task2time)){ task2time = thisTime; Serial.print("do 2nd task - task2time: "); Serial.println(task2time); if (LEDState == HIGH){ LEDState = LOW; digitalWrite(LED_PIN, LOW); //turn LED ON }else{ LEDState = HIGH; digitalWrite(LED_PIN, HIGH); //turn LED OFF } } } |
และเมื่อลองให้ระบบทำงาน จะพบว่าสองวิธีนี้ไม่ได้ให้ผลลัพท์ที่แตกต่างกันเลย

แต่มีความยากง่ายในการพัฒนาที่แตกต่างกัน จำนวนบรรทัดในการทำงานที่แตกต่างกัน วิธีคิดที่แตกต่างกัน เพียงแค่เราไม่ใช้ Delay เท่านั้น ทำให้การพัฒนาโปรแกรมมีข้อจำกัดมากขึ้น และยากขึ้นอย่างเห็นได้ชัด
“….ถ้าเช่นนั้นแล้ว ทำไมไม่ใช้ Delay เสียล่ะ มันง่ายกว่ามากเลยนะ….”
แน่นอนว่าการทำไฟกระพริบโดยใช้ Delay นั้นง่ายดายมาก
ทีนี้เราลองมาเพิ่มเงื่อนไขง่ายๆอีกชั้นหนึ่ง โดยการกำหนดให้มีสวิตช์เพิ่มขึ้นมาอีก 1 ตัว เพื่อควบคุมให้หลอดไฟกระพริบ หรือหยุดกระพริบ โดยการกดปุ่มเพียงเท่านั้น
ไฟกระพริบที่มี Delay และมีสวิตช์ควบคุม
เราจะมาเขียน Flow Chart ใหม่ เพื่อเพิ่ม สวิตช์ เข้าไปในระบบ
โดยให้ สวิตช์ ไปกำหนดสถานะว่าให้ระบบทำงาน หรือหยุดทำงาน ทุกๆรอบ
จะได้ ไดอะแกรม แบบนี้

เมื่อแปลงจากไดอะแกรมมาเป็นโค้ดแล้ว จะได้แบบนี้
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | define LED_PIN 4 define BTN_PIN 18 define BTN_PRESS HIGH int operate = HIGH; int btn = LOW; int prevbtn = LOW; void setup() { // put your setup code here, to run once: Serial.begin(19200); pinMode(LED_PIN, OUTPUT); pinMode(BTN_PIN, INPUT); Serial.print("Start : "); Serial.println(operate); } void loop() { // put your main code here, to run repeatedly: Serial.print("operate : "); Serial.println(operate); if (operate == HIGH){ digitalWrite(LED_PIN, LOW); //turn LED ON delay(1000); digitalWrite(LED_PIN, HIGH); //turn LED OFF delay(1000); } btn = digitalRead(BTN_PIN); Serial.print("btn : "); Serial.println(btn); if ((btn == BTN_PRESS)&&(prevbtn != BTN_PRESS)){ // Debouncing Serial.println("btn pressed"); operate = !operate; delay(100); } prevbtn = btn; } |
แน่นอนว่าผู้อ่านอาจจะมีวิธีอื่นที่แตกต่างออกไปบ้าง แต่ก็คงไม่ได้แตกต่างกันมากนัก
และแน่นอนอีกเช่นกันว่า มันทำงานได้ เราสามารถกดปุ่มเพื่อหยุดไฟกระพริบ และกดอีกครั้งเพื่อให้ไฟกระพริบทำงาน แม้จะต้องกดอย่างระมัดระวังสักเล็กน้อย และค้างไว้สักหน่อยก็ตาม

ทีนี้เรามาดูอีกวิธีที่ไม่ใช้ Delay กันบ้างว่าระบบจะแตกต่างจากเดิมมากขึ้นแค่ไหน
ไฟกระพริบที่ไม่ใช้ Delay และมีสวิตช์ควบคุม
สิ่งที่เราต้องเพิ่มใน State Machine Diagram นั้นคือ Event ของสวิตช์ ซึ่งเราสามารถเพิ่มเข้าไปได้โดยตรง แต่ในเมื่อเราใช้ระบบฐานเวลาแล้ว สวิตช์เราก็ควรอยู่บนระบบฐานเวลาเช่นกัน โดยเราจะกำหนดให้มีการตรวจสอบสถานะของสวิตช์ทุกๆ 20 มิลลิวินาที
ถ้าหากตรวจสอบว่ามีการกดสวิตช์ ระบบก็จะไปเปลี่ยนสถานะว่าให้ไฟกระพริบทำงาน หรือหยุดทำงาน เมื่อทำเป็น Diagram ก็จะได้แบบนี้

และเมื่อนำมาพัฒนาเป็นโค้ดก็จะได้แบบนี้
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | #define LED_PIN 4 #define BTN_PIN 18 #define BTN_PRESS HIGH int operate = HIGH; int btn = LOW; int prevbtn = LOW; unsigned long thisTime = 0; unsigned long task1time = 0; unsigned long task1period = 50; unsigned long task2time = 0; unsigned long task2period = 1000; int LEDState = HIGH; void setup() { // put your setup code here, to run once: Serial.begin(19200); pinMode(LED_PIN, OUTPUT); pinMode(BTN_PIN, INPUT); Serial.print("Start : "); Serial.println(operate); } void loop() { // put your main code here, to run repeatedly: if (thisTime != millis()){ thisTime = millis(); } if ((thisTime % task1period == 0) &&(thisTime != task1time)){ task1time = thisTime; Serial.print("do 1st task - task1time: "); Serial.println(task1time); btn = digitalRead(BTN_PIN); Serial.print("btn : "); Serial.println(btn); if ((btn == BTN_PRESS)||(prevbtn != BTN_PRESS)){ Serial.println("btn pressed"); operate = !operate; } prevbtn = btn; } if ((thisTime % task2period == 0) &&(thisTime != task2time)){ task2time = thisTime; Serial.print("do 2nd task - task2time: "); Serial.println(task2time); if (operate == HIGH){ if (LEDState == HIGH){ LEDState = LOW; digitalWrite(LED_PIN, LOW); //turn LED ON }else{ LEDState = HIGH; digitalWrite(LED_PIN, HIGH); //turn LED OFF } } } } |
เมื่อลองรันโปรแกรมแล้วจะพบว่าการตอบสนองของการกดปุ่มจะดีกว่าแบบที่ Delay ค่อนข้างมาก แบบรู้สึกได้เลยทีเดียว แต่แน่นอนว่าการใช้ฐานเวลาเป็นตัวกำหนดการทำงานแบบนี้ก็มีข้อจำกัดที่ควรระวังเช่นกัน ในเรื่องเวลาที่จะต้องใช้ในการโปรเซสในแต่ละช่วง จะไม่ควรเกินกว่าเวลาที่ได้จำกัดไว้
เช่นในกรณีนี้ เราจำกัดว่าจะอ่านค่าของปุ่มกด ทุกๆ 20 มิลลิวินาที ดังนั้นไม่ว่าเราจะทำอะไรในส่วนนี้ จะต้องรวมกันแล้วน้อยกว่า 20 มิลลิวินาที ซึ่งถ้าหากว่าเกินจากนั้น จะมีผลกระทบต่อเวลาในช่วงอื่นอย่างแน่นอน

ค่อนข้างเห็นได้ชัดว่าการใช้ระบบฐานเวลานั้น ยังคงมีความยุ่งยากมากกว่าใช้ฟังก์ชัน Delay
แต่เมื่อได้ลองให้โปรแกรมได้ทำงาน และได้ลองใช้งานแล้ว จะพบว่าการใช้ระบบฐานเวลาแบบนี้กลับให้ประสิทธิภาพ และคุณภาพ ในการใช้งานที่เหนือกว่าการใช้ Delay อย่างมาก
ซึ่งแนวคิดนี้สามารถนำไปประยุกต์ใช้ในการพัฒนาโปรแกรมอื่นๆได้เช่นกัน
หวังเป็นอย่างยิ่งว่าบทความเล็กๆ สั้นๆนี้ จะทำให้ทุกท่านที่กำลังติดปัญหาการพัฒนาโปรแกรมที่มีการทำงานหลายส่วน และอยู่กันคนละช่วงเวลา ไม่ว่าจะเป็นการติดต่อกับ GPS Module, Display Module หรือระบบบัสอย่าง RS-485, Modbus RTU เมื่ออ่านบทความนี้แล้วจะทำให้ทุกท่านได้แนวคิดที่สามารถหาทางออกจากการ Delay เยอะๆในโปรแกรมของท่านผู้อ่านได้นะครับ