ทำหลายงานพร้อมกันบน ESP32 โดยไม่ใช้ delay() | เขียนโค้ดแบบไม่บล็อกสำหรับมือใหม่

ถ้าคุณเคยเขียน ESP32 แล้วรู้สึกว่า “พอเพิ่มงานอีกนิด ระบบเริ่มค้าง” บอกเลยว่าคุณไม่ได้เจอคนเดียว อาการแบบนี้มักเกิดตอนเริ่มต่อ LED, ปุ่มกด, sensor, relay หรือ WiFi เข้าไปพร้อมกัน แล้วในโค้ดยังใช้ delay() อยู่หลายจุด ทำให้ loop หลักถูกบล็อกโดยไม่รู้ตัว

ถ้าคุณเพิ่งเริ่มต้นกับบอร์ดนี้จริง ๆ แนะนำให้ปูพื้นจากบทความ ESP32 คืออะไร ก่อน จะช่วยให้เห็นภาพรวมของบอร์ด การใช้งาน และแนวทางต่อยอดได้ชัดขึ้น ส่วนถ้าคุณอยากไล่อ่านเนื้อหาเป็นหมวดแบบค่อย ๆ เข้าใจไปทีละเรื่อง สามารถเข้าไปที่หน้า Tutorials เพื่อเลือกบทความที่เหมาะกับระดับของคุณได้เลย

บทความนี้จะพาคุณเข้าใจแบบภาษาคนทำของว่า ทำไม delay() ถึงทำให้ระบบบล็อก และจะเปลี่ยนไปเขียนโค้ดแบบ ไม่ใช้ delay() ได้ยังไง โดยใช้แนวคิด millis() ที่มือใหม่ก็เริ่มตามได้ทันที และต่อยอดไปงานจริงได้ง่ายกว่าเดิมมาก

ทำไมพอใช้ 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 กับ millis บน ESP32 สำหรับการเขียนโค้ดแบบไม่บล็อก

แบบใช้ 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()
ความง่ายตอนเริ่มง่ายมากยากขึ้นนิดหน่อย
ทำหลายงานพร้อมกันไม่เหมาะเหมาะมาก
ระบบตอบสนองต่อปุ่ม/อินพุตช้าหรือสะดุดลื่นกว่า
เหมาะกับโปรเจกต์ใหญ่ไม่ค่อยเหมาะเหมาะ
การต่อยอดภายหลังยากง่ายกว่า
ถ้าคุณกำลังเจออาการโค้ดค้าง หน่วง หรือปุ่มกดไม่ค่อยตอบสนอง แนะนำให้อ่านบทความ ESP32 ใช้ delay() แล้วค้าง แก้ยังไง เพิ่มด้วย เพราะจะช่วยให้คุณเห็นชัดขึ้นว่า ปัญหาไม่ได้อยู่ที่บอร์ดอย่างเดียว แต่หลายครั้งเกิดจากการใช้ delay() ไปบล็อก loop หลัก จนงานอื่นทำไม่ทัน

โครงสร้างพื้นฐานของโค้ดแบบไม่ใช้ delay()

รู้จัก currentMillis, previousMillis, interval

  • currentMillis = เวลาปัจจุบัน
  • previousMillis = เวลาที่งานนี้เพิ่งทำล่าสุด
  • interval = เวลาที่ต้องการรอระหว่างแต่ละรอบ
โครงสร้างการเขียน loop บน ESP32 แบบไม่ใช้ delay สำหรับหลายงานพร้อมกัน

รูปแบบเช็กเวลาแล้วค่อยทำงาน

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 ได้ต่อเนื่อง
Tip จากงานจริง: ถ้าปุ่มกดเริ่มอ่านเพี้ยน ให้ต่อ debounce เพิ่ม อย่าโทษ 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 ให้ใช้แบบนี้

  • sensorPreviousMillis
  • relayPreviousMillis
  • displayPreviousMillis

เวลาโปรเจกต์โต คุณจะกลับมาอ่านแล้วไม่หลง

เมื่อไหร่ควรแยกเป็นฟังก์ชัน

ถ้างานไหนเริ่มยาว ให้แยกเป็นฟังก์ชัน เช่น

void readSensorTask() {
  // อ่าน sensor
}

void controlRelayTask() {
  // คุม relay
}

แบบนี้ช่วยให้โค้ดเป็นระบบขึ้นมาก และต่อยอดง่ายกว่าการยัดทุกอย่างไว้ใน loop()

ตัวอย่าง ESP32 อ่าน sensor และควบคุม relay หลายงานพร้อมกันในระบบ Smart Farm

ข้อผิดพลาดที่เจอบ่อย แม้จะเลิกใช้ 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 พร้อมกัน ควรเลี่ยง

ยากขึ้นช่วงแรกนิดเดียว แต่คุ้มมาก เพราะพอเข้าใจแล้วคุณจะออกแบบโปรเจกต์ได้ดีขึ้นเยอะ

ส่วนใหญ่ใช่ เพราะแต่ละงานมีช่วงเวลาของตัวเอง

โดยภาพรวม ESP32 มีทรัพยากรมากกว่า เหมาะกับงานหลายส่วนพร้อมกันมากกว่า แต่ถ้าโค้ดคุณยังใช้ delay() หนัก ๆ ก็ยังค้างได้เหมือนกัน

อาจเพราะยังมีโค้ดบล็อกแบบอื่นอยู่ เช่น loop ยาว, sensor อ่านช้า, ส่ง serial เยอะ หรือส่ง network ถี่เกินไป

เพราะชนิดข้อมูลนี้เหมาะกับค่าที่คืนจาก millis() และช่วยลดปัญหาจากการคำนวณเวลา

ไม่จำเป็น งานส่วนใหญ่ควรกำหนด interval ที่เหมาะสม จะช่วยให้ระบบเบาและนิ่งขึ้น

งานที่มีปุ่มกด, relay, sensor, WiFi, web server หรือมีหลายเงื่อนไขควบคุมพร้อมกัน

ให้เริ่มจากแยกฟังก์ชัน, แยกไฟล์, ใช้ timer helper และวางโครงสร้าง state ให้ชัดก่อน

ควรใช้มาก เพราะงาน Smart Farm มักมี sensor, relay, ปั๊มน้ำ, พัดลม, แจ้งเตือน และ logic หลายส่วนทำงานร่วมกัน

Shopping Cart
Scroll to Top