ถ้าคุณเคยเขียน ESP32 แล้วรู้สึกว่า “พอเพิ่มงานอีกนิด ระบบเริ่มค้าง” บอกเลยว่าคุณไม่ได้เจอคนเดียว อาการแบบนี้มักเกิดตอนเริ่มต่อ LED, ปุ่มกด, sensor, relay หรือ WiFi เข้าไปพร้อมกัน แล้วในโค้ดยังใช้ delay() อยู่หลายจุด ทำให้ loop หลักถูกบล็อกโดยไม่รู้ตัว
ถ้าคุณเพิ่งเริ่มต้นกับบอร์ดนี้จริง ๆ แนะนำให้ปูพื้นจากบทความ ESP32 คืออะไร ก่อน จะช่วยให้เห็นภาพรวมของบอร์ด การใช้งาน และแนวทางต่อยอดได้ชัดขึ้น ส่วนถ้าคุณอยากไล่อ่านเนื้อหาเป็นหมวดแบบค่อย ๆ เข้าใจไปทีละเรื่อง สามารถเข้าไปที่หน้า Tutorials เพื่อเลือกบทความที่เหมาะกับระดับของคุณได้เลย
บทความนี้จะพาคุณเข้าใจแบบภาษาคนทำของว่า ทำไม delay() ถึงทำให้ระบบบล็อก และจะเปลี่ยนไปเขียนโค้ดแบบ ไม่ใช้ delay() ได้ยังไง โดยใช้แนวคิด millis() ที่มือใหม่ก็เริ่มตามได้ทันที และต่อยอดไปงานจริงได้ง่ายกว่าเดิมมาก
สารบัญ
- ทำไมพอใช้ delay() หลายจุด ESP32 ถึงเริ่มค้าง
- แนวคิดการทำหลายงานพร้อมกันบน ESP32 จริง ๆ คืออะไร
- delay() กับ millis() ต่างกันยังไง
- โครงสร้างพื้นฐานของโค้ดแบบไม่ใช้ delay()
- ตัวอย่างที่ 1 กระพริบ LED โดยไม่ใช้ delay()
- ตัวอย่างที่ 2 กระพริบ LED ไปพร้อมกับอ่านปุ่มกด
- ตัวอย่างที่ 3 อ่าน sensor และคุม relay คนละจังหวะเวลา
- วิธีจัดหลายงานใน loop() ให้โตเป็นโปรเจกต์จริงได้
- ข้อผิดพลาดที่เจอบ่อย แม้จะเลิกใช้ delay() แล้ว
- millis() พอเมื่อไหร่ และควรใช้ Timer library เมื่อไหร่
- สรุป
- FAQ คำถามที่คนเริ่มต้นถามบ่อย
ทำไมพอใช้ delay() หลายจุด ESP32 ถึงเริ่มค้าง
delay() คืออะไรแบบเข้าใจง่ายๆ
delay() คือการสั่งให้โปรแกรม “หยุดรอ” ตามเวลาที่กำหนด เช่น delay(1000) คือหยุดรอ 1 วินาที ก่อนจะไปทำบรรทัดถัดไป
ตอนเริ่มต้น มันดูง่ายมาก และเหมาะกับตัวอย่างพื้นฐานอย่างไฟกระพริบ แต่พอคุณเริ่มทำงานจริง เช่น อ่าน sensor, รับปุ่มกด, คุม relay, ส่งข้อมูลขึ้น WiFi หรือแสดงผลบนจอ ปัญหาจะเริ่มชัดทันที
ทำไมมือใหม่ถึงเริ่มจาก delay() เกือบทุกคน
- เขียนง่าย
- เห็นผลเร็ว
- ตัวอย่างในอินเทอร์เน็ตมีเยอะ
- เหมาะกับงาน demo สั้น ๆ
แต่ข้อเสียคือ พอโปรเจกต์โตขึ้น delay() จะกลายเป็นตัวบล็อก loop หลัก ทำให้ระบบไม่ตอบสนองเท่าที่ควร
อาการที่บอกว่าระบบของคุณกำลังถูกบล็อก
- ปุ่มกดตอบสนองช้า
- relay เปิด-ปิดหน่วง
- sensor อ่านค่าไม่ทัน
- WiFi หรือ web server ดูอืด
- โค้ดเพิ่มอีกนิดเดียวก็เริ่มงงและแก้ยาก
delay() เยอะ ESP32 ไม่ได้เสีย แต่โค้ดของคุณกำลัง “บังคับให้มันนั่งรอ” จนงานอื่นทำไม่ทันแนวคิดการทำหลายงานพร้อมกันบน ESP32 จริง ๆ คืออะไร
ESP32 ไม่ได้ทำทุกอย่างพร้อมกันแบบเวทมนตร์
เวลาคนพูดว่า ESP32 “ทำหลายงานพร้อมกัน” สำหรับงานเริ่มต้นส่วนใหญ่ สิ่งที่เกิดขึ้นจริงคือ loop หลักวิ่งเร็วมาก แล้วคอยเช็กว่า “ตอนนี้ถึงเวลาทำงานไหนหรือยัง”
ถ้าแต่ละงานเขียนแบบไม่บล็อก ระบบจะดูเหมือนทำหลายอย่างพร้อมกัน เช่น
- ไฟกระพริบอยู่
- ปุ่มยังตอบสนอง
- sensor ยังอ่านค่าได้
- relay ยังควบคุมได้ตามเงื่อนไข
คำว่า non-blocking แปลว่าอะไร
non-blocking คือการเขียนโค้ดแบบที่งานหนึ่งไม่ไปขวางงานอื่น
แทนที่จะบอกว่า “หยุดรอ 1 วินาที” เราจะเปลี่ยนเป็น “เช็กว่าครบ 1 วินาทีหรือยัง ถ้าครบค่อยทำ”
ทำไม millis() ถึงช่วยให้ระบบลื่นขึ้น
millis() ใช้สำหรับอ่านเวลาที่ผ่านไปตั้งแต่บอร์ดเริ่มทำงาน เราจึงใช้มันเป็นตัวจับจังหวะได้ โดยไม่ต้องหยุดทั้งโปรแกรม
นี่คือหัวใจของการเขียนโค้ดแบบไม่ใช้ delay()
delay() กับ millis() ต่างกันยังไง

แบบใช้ delay()
digitalWrite(ledPin, HIGH);
delay(1000);
digitalWrite(ledPin, LOW);
delay(1000);โค้ดนี้ทำงานได้ แต่ระหว่างรอ 1 วินาที ระบบแทบไม่ได้ไปทำงานอื่น
แบบใช้ millis()
unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= interval) {
previousMillis = currentMillis;
ledState = !ledState;
digitalWrite(ledPin, ledState);
}โค้ดนี้ไม่ได้สั่ง “หยุดรอ” แต่คอยเช็กว่าเวลาครบหรือยัง ทำให้ loop ยังวิ่งต่อและไปดูงานอื่นได้
ตารางเปรียบเทียบให้เห็นภาพชัด
| หัวข้อ | delay() | millis() |
|---|---|---|
| ความง่ายตอนเริ่ม | ง่ายมาก | ยากขึ้นนิดหน่อย |
| ทำหลายงานพร้อมกัน | ไม่เหมาะ | เหมาะมาก |
| ระบบตอบสนองต่อปุ่ม/อินพุต | ช้าหรือสะดุด | ลื่นกว่า |
| เหมาะกับโปรเจกต์ใหญ่ | ไม่ค่อยเหมาะ | เหมาะ |
| การต่อยอดภายหลัง | ยาก | ง่ายกว่า |
delay() ไปบล็อก loop หลัก จนงานอื่นทำไม่ทันโครงสร้างพื้นฐานของโค้ดแบบไม่ใช้ delay()
รู้จัก currentMillis, previousMillis, interval
currentMillis= เวลาปัจจุบันpreviousMillis= เวลาที่งานนี้เพิ่งทำล่าสุดinterval= เวลาที่ต้องการรอระหว่างแต่ละรอบ

รูปแบบเช็กเวลาแล้วค่อยทำงาน
if (currentMillis - previousMillis >= interval) {
previousMillis = currentMillis;
// ทำงานที่ต้องการ
}รูปแบบนี้คือแกนหลักของการเขียนโค้ดแบบไม่บล็อก
โครง mindset ที่มือใหม่ควรจำ
อย่าคิดว่า “รอแล้วค่อยทำ” ให้คิดว่า “เช็กเรื่อย ๆ ว่าถึงเวลาหรือยัง”
ตัวอย่างที่ 1 กระพริบ LED โดยไม่ใช้ delay()
โค้ดตัวอย่าง
const int ledPin = 2;
unsigned long previousMillis = 0;
const unsigned long interval = 1000;
bool ledState = false;
void setup() {
pinMode(ledPin, OUTPUT);
}
void loop() {
unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= interval) {
previousMillis = currentMillis;
ledState = !ledState;
digitalWrite(ledPin, ledState);
}
}อธิบายทีละส่วน
- กำหนดให้ LED อยู่ที่ขา 2
- เก็บเวลารอบล่าสุดไว้ใน
previousMillis - ตั้งให้กระพริบทุก 1000 ms
- ทุกครั้งที่ครบเวลา จะสลับสถานะ LED
จุดที่คนเริ่มต้นมักพลาด
- ใช้ชนิดข้อมูลผิด ควรใช้
unsigned long - ลืมอัปเดต
previousMillis - เอาโค้ดเช็กเวลาไปใส่ในฟังก์ชันที่มี
delay()ซ่อนอยู่
ตัวอย่างที่ 2 กระพริบ LED ไปพร้อมกับอ่านปุ่มกด
ปัญหาของโค้ดแบบ delay
ถ้าคุณใช้ delay(1000) เพื่อให้ LED กระพริบ ระหว่างนั้นถ้ากดปุ่ม ระบบอาจเช็กไม่ทัน หรือกว่าจะอ่านค่าปุ่มได้ก็ช้าไปแล้ว
วิธีแยกงานให้ LED กับปุ่มทำงานพร้อมกัน
const int ledPin = 2;
const int buttonPin = 4;
unsigned long previousMillis = 0;
const unsigned long interval = 500;
bool ledState = false;
void setup() {
pinMode(ledPin, OUTPUT);
pinMode(buttonPin, INPUT_PULLUP);
Serial.begin(115200);
}
void loop() {
unsigned long currentMillis = millis();
// งานที่ 1: กระพริบ LED
if (currentMillis - previousMillis >= interval) {
previousMillis = currentMillis;
ledState = !ledState;
digitalWrite(ledPin, ledState);
}
// งานที่ 2: อ่านปุ่มตลอดเวลา
if (digitalRead(buttonPin) == LOW) {
Serial.println("Button Pressed");
}
}ใช้งานจริงกับปุ่มสั่งรีเลย์ได้ยังไง
แนวคิดเดียวกันนี้ใช้ได้เลยกับงานจริง เช่น
- ไฟสถานะกระพริบอยู่
- ปุ่มกดสั่งเปิดปั๊มน้ำ
- ขณะเดียวกันระบบยังอ่าน sensor ได้ต่อเนื่อง
millis() ก่อน เพราะหลายครั้งปัญหามาจากสวิตช์เด้งตัวอย่างที่ 3 อ่าน sensor และคุม relay คนละจังหวะเวลา
อ่าน sensor ทุก 2 วินาที
เช็ก relay ทุก 500 ms
ทำไมโครงสร้างนี้เหมาะกับ Smart Farm
ในงานจริง ไม่จำเป็นต้องให้ทุกอย่างทำงานถี่เท่ากัน เช่น
- sensor อุณหภูมิอ่านทุก 2 วินาทีก็พอ
- เช็กเงื่อนไข relay ทุก 500 ms
- อัปเดต serial monitor ทุก 1 วินาที
const int relayPin = 23;
const int sensorPin = 34;
unsigned long sensorPreviousMillis = 0;
unsigned long relayPreviousMillis = 0;
const unsigned long sensorInterval = 2000;
const unsigned long relayInterval = 500;
int sensorValue = 0;
void setup() {
pinMode(relayPin, OUTPUT);
Serial.begin(115200);
}
void loop() {
unsigned long currentMillis = millis();
// งานที่ 1: อ่าน sensor
if (currentMillis - sensorPreviousMillis >= sensorInterval) {
sensorPreviousMillis = currentMillis;
sensorValue = analogRead(sensorPin);
Serial.print("Sensor Value: ");
Serial.println(sensorValue);
}
// งานที่ 2: ควบคุม relay
if (currentMillis - relayPreviousMillis >= relayInterval) {
relayPreviousMillis = currentMillis;
if (sensorValue < 2000) {
digitalWrite(relayPin, HIGH);
} else {
digitalWrite(relayPin, LOW);
}
}
}นี่คือจุดเริ่มต้นของโค้ดแบบใช้งานจริงในสาย Smart Farm, ระบบรดน้ำ, ตู้ควบคุม, ระบบแจ้งเตือน หรือโปรเจกต์ IoT หลายแบบ
วิธีจัดหลายงานใน loop() ให้โตเป็นโปรเจกต์จริงได้
แยกงานเป็น block
อย่าเขียนทุกอย่างกองรวมกันในเงื่อนไขเดียว ควรแยกเป็นงาน เช่น
- งานอ่าน sensor
- งานเช็กปุ่ม
- งานควบคุม relay
- งานส่งข้อมูล
ตั้งชื่อตัวแปรเวลาให้สื่อความหมาย
แทนที่จะใช้ previousMillis1, previousMillis2
ให้ใช้แบบนี้
sensorPreviousMillisrelayPreviousMillisdisplayPreviousMillis
เวลาโปรเจกต์โต คุณจะกลับมาอ่านแล้วไม่หลง
เมื่อไหร่ควรแยกเป็นฟังก์ชัน
ถ้างานไหนเริ่มยาว ให้แยกเป็นฟังก์ชัน เช่น
void readSensorTask() {
// อ่าน sensor
}
void controlRelayTask() {
// คุม relay
}แบบนี้ช่วยให้โค้ดเป็นระบบขึ้นมาก และต่อยอดง่ายกว่าการยัดทุกอย่างไว้ใน loop()

ข้อผิดพลาดที่เจอบ่อย แม้จะเลิกใช้ delay() แล้ว
ยังมี delay() ซ่อนอยู่ในฟังก์ชันอื่น
หลายคนเอา delay() ออกจาก loop() แล้วคิดว่าจบ แต่ในฟังก์ชันอ่าน sensor หรือฟังก์ชันแสดงผลยังมี delay() อยู่ ทำให้ระบบยังบล็อกเหมือนเดิม
ใช้ millis() แต่โค้ดยังบล็อกเพราะงานข้างในหนักเกิน
ถึงคุณจะเช็กเวลาแบบถูกต้อง แต่ถ้าในบล็อกนั้นมีงานหนัก เช่น วนลูปยาว ๆ, อ่านค่าซ้ำเยอะเกินไป, หรือพิมพ์ Serial เยอะมาก ระบบก็ยังช้าได้
อ่าน sensor หรือส่งข้อมูลถี่เกินจำเป็น
งานบางอย่างไม่ต้องทำทุก loop เช่น
- DHT ไม่จำเป็นต้องอ่านทุกไม่กี่ ms
- ส่งข้อมูล WiFi ถี่เกินไปอาจทำให้ระบบอืด
- relay ไม่จำเป็นต้องสั่งซ้ำตลอดเวลา ถ้าสถานะยังไม่เปลี่ยน
delay() เป็นแค่ก้าวแรก แต่ถ้าโครงคิดยังเป็นแบบ “สั่งทุกอย่างถี่ที่สุดเท่าที่ทำได้” ระบบก็ยังไม่ลื่นอยู่ดีmillis() พอเมื่อไหร่ และควรใช้ Timer library เมื่อไหร่
งานเล็กถึงกลาง millis() ก็พอ
ถ้าคุณมีงานไม่เยอะมาก เช่น 2–5 งานใน loop ใช้ millis() ได้สบาย และเป็นพื้นฐานที่ควรเข้าใจก่อน
งานหลายอุปกรณ์ควรใช้ helper library
ถ้าเริ่มมีหลาย timer หลายเงื่อนไข การใช้ timer helper หรือตัวช่วยอย่าง library จะทำให้โค้ดสะอาดขึ้น และลดโอกาสเขียนพลาด
ถ้าโปรเจกต์เริ่มโต ควรจัดโครงสร้างยังไง
- แยก task เป็นฟังก์ชัน
- แยกไฟล์ตามหน้าที่
- ใช้ state ที่ชัดเจน
- อย่าปล่อยให้ logic สำคัญกระจุกใน
loop()เดียวแบบยาวมาก
สำหรับมือใหม่ แนะนำให้เข้าใจ millis() ให้แน่นก่อน แล้วค่อยไปต่อเรื่อง timer abstraction, scheduler หรือ FreeRTOS ในภายหลัง
สรุป
ถ้าคุณอยากให้ ESP32 ทำหลายงานพร้อมกันได้ลื่นขึ้น สิ่งแรกที่ต้องเปลี่ยนไม่ใช่บอร์ด แต่คือวิธีคิดในการเขียนโค้ด
delay()เหมาะกับตัวอย่างง่าย ๆ และงาน demo- ถ้าจะทำหลายงานในระบบเดียว ควรเริ่มใช้แนวคิด non-blocking
millis()คือพื้นฐานสำคัญที่ช่วยให้ loop ยังวิ่งต่อและเช็กหลายงานได้- เมื่อเข้าใจหลักนี้แล้ว คุณจะต่อยอดไปงาน sensor, relay, WiFi, Smart Farm และ automation ได้ง่ายขึ้นมาก
สรุปแบบสั้นที่สุด: อย่าสั่งให้ ESP32 “หยุดรอ” แต่ให้มัน “คอยเช็กเวลา” แล้วทำงานตามจังหวะที่กำหนด
FAQ คำถามที่คนเริ่มต้นถามบ่อย
ใช้ delay() นิดหน่อยได้ไหม
ได้ในงานเล็กมากหรือ demo สั้น ๆ แต่ถ้าเป็นงานที่ต้องตอบสนองต่อปุ่ม, sensor, relay หรือ WiFi พร้อมกัน ควรเลี่ยง
millis() ยากกว่า delay() มากไหม
millis() ยากกว่า delay() มากไหมยากขึ้นช่วงแรกนิดเดียว แต่คุ้มมาก เพราะพอเข้าใจแล้วคุณจะออกแบบโปรเจกต์ได้ดีขึ้นเยอะ
ถ้ามีหลายงาน ต้องมี previousMillis หลายตัวไหม
previousMillis หลายตัวไหมส่วนใหญ่ใช่ เพราะแต่ละงานมีช่วงเวลาของตัวเอง
ESP32 ทำหลายงานพร้อมกันดีกว่า Arduino Uno ไหม
โดยภาพรวม ESP32 มีทรัพยากรมากกว่า เหมาะกับงานหลายส่วนพร้อมกันมากกว่า แต่ถ้าโค้ดคุณยังใช้ delay() หนัก ๆ ก็ยังค้างได้เหมือนกัน
ทำไมเอา delay() ออกแล้วระบบยังช้า
delay() ออกแล้วระบบยังช้าอาจเพราะยังมีโค้ดบล็อกแบบอื่นอยู่ เช่น loop ยาว, sensor อ่านช้า, ส่ง serial เยอะ หรือส่ง network ถี่เกินไป
ควรใช้ unsigned long กับ millis() เพราะอะไร
unsigned long กับ millis() เพราะอะไรเพราะชนิดข้อมูลนี้เหมาะกับค่าที่คืนจาก millis() และช่วยลดปัญหาจากการคำนวณเวลา
ต้องอ่าน sensor ทุก loop ไหม
ไม่จำเป็น งานส่วนใหญ่ควรกำหนด interval ที่เหมาะสม จะช่วยให้ระบบเบาและนิ่งขึ้น
งานแบบไหนควรเลิกใช้ delay() ก่อน
งานที่มีปุ่มกด, relay, sensor, WiFi, web server หรือมีหลายเงื่อนไขควบคุมพร้อมกัน
ถ้าโปรเจกต์เริ่มซับซ้อน ควรไปทางไหนต่อ
ให้เริ่มจากแยกฟังก์ชัน, แยกไฟล์, ใช้ timer helper และวางโครงสร้าง state ให้ชัดก่อน
ถ้าจะทำ Smart Farm ควรใช้แนวนี้ไหม
ควรใช้มาก เพราะงาน Smart Farm มักมี sensor, relay, ปั๊มน้ำ, พัดลม, แจ้งเตือน และ logic หลายส่วนทำงานร่วมกัน



