10. เวลา: delay กับ millis
ทำไม delay() ทำให้โปรแกรมหยุดนิ่ง และใช้ millis() จับเวลาโดยไม่บล็อก พร้อมตัวอย่างจากโค้ดโดรน
เวลา สรุปก่อนอ่าน
- delay() หยุดรอ ง่ายแต่ระหว่างรอโปรแกรมทำอย่างอื่นไม่ได้ เหมือนยืนจ้องหม้อต้มน้ำจนเดือด
- millis() ดูนาฬิกาว่าผ่านไปกี่มิลลิวินาที ไม่หยุดรอ เหมือนตั้งนาฬิกาปลุกแล้วไปทำอย่างอื่นได้
- ในโค้ดโดรน ภารกิจส่วนใหญ่ใช้
delayเพราะเป็นลำดับตายตัว ใช้millis()เมื่อต้องทำสองอย่างพร้อมกัน เช่น รอความสูงนิ่งไปพร้อมอ่านปุ่ม
โค้ดโดรนเต็มไปด้วยเรื่องเวลา "บินหน้า 1.5 วินาที" "รอความสูงนิ่ง 0.5 วินาที" "ถือเซอร์โว 1.5 วินาที" Arduino มีเครื่องมือสามอย่างสำหรับเรื่องนี้
| ฟังก์ชัน | หน่วย | ทำอะไร |
|---|---|---|
delay(ms) | มิลลิวินาที | หยุดรอ ทำอย่างอื่นไม่ได้ |
delayMicroseconds(us) | ไมโครวินาที | หยุดรอสั้นๆ (แม่นถึงราว 16,383 µs) |
millis() | มิลลิวินาที | บอกเวลาที่ผ่านไปตั้งแต่เปิดบอร์ด ไม่หยุดโปรแกรม |
micros() | ไมโครวินาที | เหมือน millis() แต่ละเอียดกว่า |
1. delay(): ง่ายแต่หยุดทุกอย่าง
drone.takeOff();
delay(3000); // รอ 3 วินาที
drone.setHeight(120);โค้ดภารกิจของชมรมเกือบทั้งหมดเป็นแบบนี้ และใช้ได้ดี เพราะภารกิจเป็นลำดับตายตัว "ทำอย่างนี้ รอ ทำอย่างนั้น" ตัวอย่างจริง move_forward_cm เองก็คือ delay
// protocol.cpp
void Arduino::move_forward_cm(uint16_t distance_cm, uint8_t speed) {
set_direction(1, speed); // 1 = forward
delay(ms_round(distance_cm * MS_PER_CM_FWD));
poshold(speed);
}ระหว่างนั้น (100 cm × 15.46 ms ≈ 1,546 ms) loop() ไม่ทำอะไรอื่น ปุ่มที่กดเข้ามาถูก interrupt เก็บไว้ในตัวแปร แต่ loop() ยังไม่ได้ไปอ่าน
ตัวอย่างชีวิตจริง: ต้มบะหมี่
ตั้งนาฬิกาปลุก 3 นาที (millis) แล้วไปทำอย่างอื่นได้ ต่างจากยืนจ้องหม้อ 3 นาทีโดยไม่ทำอะไร (delay) ผลคือบะหมี่สุกเท่ากัน แต่ถ้ามีโทรศัพท์เข้า คนยืนจ้องหม้อรับสายไม่ได้
2. millis(): จดเวลาแล้วเช็กเอา
หลักการคือ จำเวลาตอนเริ่ม แล้ววนถามว่า "ผ่านไปเท่าไรแล้ว"
uint32_t t0 = millis();
while (millis() - t0 < 1000) {
// ทำงานอื่นตรงนี้ได้
}โค้ดชมรมใช้รูปแบบนี้ทั่วไป ตัวอย่างอยู่ใน DroneEasy::servoHold
// DroneEasy.cpp
void DroneEasy::servoHold(int pin, int pulse_us, uint16_t hold_ms) const {
const uint32_t t0 = millis();
while (millis() - t0 < hold_ms) servoPulse50Hz(pin, pulse_us);
}ถือพัลส์เซอร์โวนาน hold_ms ที่กำหนด และ reach_height_shoot_stable() ใช้ millis() กันค้าง (หมดเวลา 3,500 ms) ดูผังที่หน้า เงื่อนไขและลูป
เขียน millis() - t0 ไม่ใช่ millis() > t0 + 1000
millis() นับต่อเนื่อง 32 บิต เต็มที่ราว 49.7 วัน แล้ววนกลับ 0 การลบกันด้วยชนิด unsigned ให้ผลถูกแม้วนข้ามขอบ ส่วนการบวกแล้วเทียบ อาจผิดที่ขอบนั้น ใช้แบบลบเสมอ
3. กระพริบ LED แบบไม่หยุด
เทียบกับ Blink ในหน้า สเก็ตช์แรก เวอร์ชันนี้กระพริบเท่าเดิมแต่ loop() ว่างทำอย่างอื่นได้ (ทดสอบคอมไพล์สำหรับ Nano แล้ว ใช้ 848 ไบต์)
const unsigned long BLINK_MS = 500;
unsigned long lastToggle = 0;
bool ledOn = false;
void setup() {
pinMode(LED_BUILTIN, OUTPUT);
}
void loop() {
if (millis() - lastToggle >= BLINK_MS) {
lastToggle = millis();
ledOn = !ledOn;
digitalWrite(LED_BUILTIN, ledOn);
}
// ตรงนี้ทำงานอื่นต่อได้ ไม่ต้องรอ
}lastToggleจำเวลาที่สลับครั้งล่าสุด- ทุกรอบ
loop()เช็กว่าผ่านไป 500 ms หรือยัง ถ้ายังก็ข้ามไป !ledOnคือกลับค่า จริงเป็นเท็จ เท็จเป็นจริง
ในโค้ดชมรม ถ้าอยากให้ loop() เช็กปุ่มทุกรอบแล้วค่อยบินเมื่อถึงเวลา ก็ใช้โครงสร้างแบบนี้ได้ แต่ตอนนี้ภารกิจของชมรมเป็นลำดับตายตัว delay พอแล้ว
4. จะใช้อะไรเมื่อไร
| สถานการณ์ | ใช้ |
|---|---|
| ภารกิจเป็นลำดับ ไม่ต้องทำอย่างอื่นระหว่างรอ | delay() |
| รอให้โดรนทำท่าเสร็จก่อนสั่งต่อ | delay() |
| ต้องอ่านเซนเซอร์หรือปุ่มระหว่างรอ | millis() |
| ต้องมีหมดเวลา กันค้าง | millis() |
| จังหวะละเอียดระดับ µs (พัลส์เซอร์โว) | delayMicroseconds() |
ลองทำ
- อัปโหลดเวอร์ชัน
millis()ข้างบน แล้วเพิ่มบรรทัดในloop()ที่Serial.println(millis())สังเกตว่าข้อความพิมพ์ถี่ (ถูกจำกัดด้วยความเร็ว 9600 baud ราว 80 บรรทัดต่อวินาที) ขณะที่ LED ยังกระพริบ 1 Hz - คำนวณว่า
move_forward_cm(250)ใช้เวลากี่ ms (คำตอบ: 250 × 15.46 = 3,865 ms) - เขียน LED กระพริบสองดวงที่จังหวะ 300 กับ 700 ms พร้อมกัน โดยไม่ใช้
delay()
พังตรงไหนบ่อย
- เก็บเวลาใน
intล้นที่ 32,767 ใช้unsigned longหรือuint32_t - เขียน
millis() > t0 + 1000ผิดตอนวนข้ามขอบ ใช้millis() - t0 >= 1000 - ใส่
delay()ใหญ่ในโค้ดที่ต้องตอบสนองเร็ว ปุ่มดูเหมือนไม่ตอบ - ลืมอัปเดต
lastToggleเงื่อนไขจริงทุกรอบ LED สลับถี่จนมองไม่ทัน delayMicrosecondsเกิน 16,383 ไม่แม่น โค้ดชมรมใช้ 20,000 − พัลส์ (ราว 17,500–19,500) ในเซอร์โว ซึ่งเกินเลยขีดนั้น ถ้าต้องการความแม่นให้แยกเป็นdelay()+delayMicroseconds()(ดูโค้ดที่คอมเมนต์ไว้ในServo.ino)