คำตอบสั้น ๆ : อย่าพยายาม "จัดการ" โรลโอเวอร์มิลลิวินาทีเขียนรหัสปลอดภัยโรลโอเวอร์แทน โค้ดตัวอย่างของคุณจากบทช่วยสอนนั้นใช้ได้ หากคุณพยายามตรวจสอบโรลโอเวอร์เพื่อใช้มาตรการแก้ไขโอกาสที่คุณทำสิ่งผิดปกติ โปรแกรม Arduino ส่วนใหญ่จะต้องจัดการกับเหตุการณ์ที่ขยายระยะเวลาค่อนข้างสั้นเช่นการกดปุ่ม 50 มิลลิวินาทีหรือเปิดเครื่องทำความร้อนเป็นเวลา 12 ชั่วโมง ... จากนั้นแม้ว่าโปรแกรมนั้นจะทำงานเป็นเวลาหลายปีก็ตาม การโรลโอเวอร์ของ millis ไม่ควรเป็นปัญหา
วิธีที่ถูกต้องในการจัดการ (หรือมากกว่าหลีกเลี่ยงการจัดการ) ปัญหาโรลโอเวอร์คือการคิดของunsigned long
จำนวนกลับโดย
millis()
ในแง่ของเลขคณิตมอดุลาร์ สำหรับความโน้มเอียงทางคณิตศาสตร์ความคุ้นเคยกับแนวคิดนี้มีประโยชน์มากเมื่อเขียนโปรแกรม คุณสามารถดูการใช้งานทางคณิตศาสตร์ในบทความมิลลินิคแกมมอนล้น () ล้น ... เป็นเรื่องเลวร้ายหรือเปล่า? . สำหรับผู้ที่ไม่ต้องการอ่านรายละเอียดการคำนวณฉันเสนอทางเลือก (หวังว่าง่ายกว่า) ที่นี่ในการคิดเกี่ยวกับมัน มันขึ้นอยู่กับความแตกต่างระหว่างที่เรียบง่ายจังหวะและระยะเวลา ตราบใดที่การทดสอบของคุณเกี่ยวข้องกับการเปรียบเทียบระยะเวลาคุณควรจะดี
หมายเหตุเกี่ยวกับไมโคร () : ทุกอย่างว่านี่เกี่ยวกับการmillis()
ใช้อย่างเท่าเทียมกันที่จะmicros()
ยกเว้นความจริงที่ว่าmicros()
ม้วนมากกว่าทุก 71.6 นาทีและฟังก์ชั่นให้ไว้ด้านล่างไม่ได้ส่งผลกระทบต่อsetMillis()
micros()
อินสแตนซ์การประทับเวลาและระยะเวลา
เมื่อจัดการกับเวลาที่เราจะต้องทำให้ความแตกต่างระหว่างอย่างน้อยสองแนวคิดที่แตกต่างกัน: จังหวะและระยะเวลา ทันทีคือจุดบนแกนเวลา ระยะเวลาคือความยาวของช่วงเวลานั่นคือระยะเวลาในระหว่าง instants ที่กำหนดเริ่มต้นและจุดสิ้นสุดของช่วงเวลา ความแตกต่างระหว่างแนวคิดเหล่านี้ไม่ได้คมชัดมากในภาษาทุกวัน ตัวอย่างเช่นถ้าฉันพูดว่า " ฉันจะกลับมาในห้านาที " ดังนั้น " ห้านาที " คือระยะเวลาโดยประมาณที่
ฉันไม่อยู่ขณะที่ " ในห้านาที " เป็นช่วงเวลาทันที
จากการทำนายของฉันกลับมา การแยกความแตกต่างในใจเป็นสิ่งสำคัญเพราะเป็นวิธีที่ง่ายที่สุดในการหลีกเลี่ยงปัญหาโรลโอเวอร์
ค่าที่ส่งคืนของmillis()
สามารถตีความได้ว่าเป็นช่วงเวลา: เวลาที่ผ่านไปตั้งแต่เริ่มต้นโปรแกรมจนถึงปัจจุบัน อย่างไรก็ตามการตีความนี้แบ่งออกทันทีที่มิลลิวินาทีล้น โดยทั่วไปแล้วจะมีประโยชน์มากกว่าหากคิดว่าmillis()
จะส่งคืนการ
ประทับเวลาเช่น "ป้ายกำกับ" ที่ระบุว่ามีการโต้ตอบแบบทันที อาจเป็นที่ถกเถียงกันอยู่ว่าการตีความนี้มีความคลุมเครือเนื่องจากฉลากเหล่านี้ถูกใช้ซ้ำทุก ๆ 49.7 วัน อย่างไรก็ตามนี่เป็นปัญหาที่ไม่ค่อยเกิดขึ้น: ในแอปพลิเคชันที่ฝังตัวส่วนใหญ่สิ่งใดก็ตามที่เกิดขึ้นเมื่อ 49.7 วันก่อนเป็นประวัติศาสตร์โบราณที่เราไม่สนใจ ดังนั้นการรีไซเคิลฉลากเก่าจึงไม่ควรเป็นปัญหา
อย่าเปรียบเทียบการประทับเวลา
การพยายามค้นหาว่าช่วงเวลาใดในเวลาสองช่วงที่มากกว่าช่วงอื่นก็ไม่สมเหตุสมผล ตัวอย่าง:
unsigned long t1 = millis();
delay(3000);
unsigned long t2 = millis();
if (t2 > t1) { ... }
อย่างไร้เดียงสาใครจะคาดหวังว่าเงื่อนไขของการif ()
เป็นจริงเสมอ delay(3000)
แต่มันจริงจะเท็จถ้ามิลลิวินาทีล้นในช่วง
การคิดว่า t1 และ t2 เป็นป้ายกำกับที่สามารถรีไซเคิลได้เป็นวิธีที่ง่ายที่สุดในการหลีกเลี่ยงข้อผิดพลาด: ฉลาก t1 ได้รับการกำหนดให้ชัดเจนในทันทีก่อนหน้า t2 แต่ใน 49.7 วันจะถูกกำหนดใหม่ให้กับทันทีในอนาคต ดังนั้น t1 จะเกิดขึ้นทั้งก่อนและหลัง t2 นี่ควรทำให้ชัดเจนว่าการแสดงออกt2 > t1
ไม่มีเหตุผล
แต่ถ้าเป็นเพียงป้ายคำถามที่ชัดเจนคือเราจะคำนวณเวลาที่มีประโยชน์กับพวกเขาได้อย่างไร คำตอบคือ: โดยการ จำกัด ตัวเองให้คำนวณเพียงสองครั้งเท่านั้นที่สมเหตุสมผลสำหรับการบันทึกเวลา:
later_timestamp - earlier_timestamp
ทำให้ช่วงเวลาคือจำนวนเวลาที่ผ่านไประหว่างอินสแตนซ์ก่อนหน้านี้และช่วงเวลาต่อมา นี่เป็นการดำเนินการทางคณิตศาสตร์ที่มีประโยชน์ที่สุดที่เกี่ยวข้องกับการประทับเวลา
timestamp ± duration
ทำให้เกิดการประทับเวลาซึ่งเป็นเวลาหลังจาก (ถ้าใช้ +) หรือก่อน (ถ้า -) การประทับเวลาเริ่มต้น ไม่มีประโยชน์เท่าที่ฟังดูเนื่องจากการประทับเวลาที่ได้สามารถใช้ในการคำนวณเพียงสองชนิด ...
ต้องขอบคุณการคำนวณแบบแยกส่วนทั้งสองสิ่งนี้รับประกันว่าจะทำงานได้ดีในโรลโอเวอร์มิลลิวินาทีอย่างน้อยตราบใดที่ความล่าช้าที่เกี่ยวข้องนั้นสั้นกว่า 49.7 วัน
การเปรียบเทียบระยะเวลาถือว่าใช้ได้
ระยะเวลาเป็นเพียงจำนวนมิลลิวินาทีที่ผ่านไปในบางช่วงเวลา ตราบใดที่เราไม่จำเป็นต้องจัดการกับระยะเวลานานกว่า 49.7 วันการดำเนินการใด ๆ ที่ทำให้เกิดความรู้สึกทางร่างกายก็ควรทำให้เกิดความรู้สึกคำนวณ ตัวอย่างเช่นเราสามารถคูณระยะเวลาด้วยความถี่เพื่อให้ได้จำนวนจุด หรือเราสามารถเปรียบเทียบช่วงเวลาสองช่วงเพื่อรู้ว่าระยะใดยาวกว่ากัน delay()
ยกตัวอย่างเช่นที่นี่มีสองการใช้งานทางเลือกของการ ขั้นแรกให้รถ:
void myDelay(unsigned long ms) { // ms: duration
unsigned long start = millis(); // start: timestamp
unsigned long finished = start + ms; // finished: timestamp
for (;;) {
unsigned long now = millis(); // now: timestamp
if (now >= finished) // comparing timestamps: BUG!
return;
}
}
และนี่คือสิ่งที่ถูกต้อง:
void myDelay(unsigned long ms) { // ms: duration
unsigned long start = millis(); // start: timestamp
for (;;) {
unsigned long now = millis(); // now: timestamp
unsigned long elapsed = now - start; // elapsed: duration
if (elapsed >= ms) // comparing durations: OK
return;
}
}
โปรแกรมเมอร์ C ส่วนใหญ่จะเขียนลูปข้างต้นในรูปแบบ terser เช่น
while (millis() < start + ms) ; // BUGGY version
และ
while (millis() - start < ms) ; // CORRECT version
ถึงแม้ว่าพวกเขาจะดูคล้ายคลึงกันหลอกลวงความแตกต่างของการประทับเวลา / ระยะเวลาควรทำให้ชัดเจนว่าอันใดที่เป็นรถและอันที่ถูกต้อง
ถ้าฉันต้องการเปรียบเทียบการประทับเวลาจริงๆ
ดีกว่าพยายามหลีกเลี่ยงสถานการณ์ หากไม่สามารถหลีกเลี่ยงได้ก็ยังคงมีความหวังว่าเป็นที่ทราบกันว่าสัญชาตญาณดังกล่าวอยู่ใกล้พอ: ใกล้กว่า 24.85 วัน ใช่ความล่าช้าในการจัดการสูงสุดของเรา 49.7 วันเพิ่งถูกลดลงครึ่งหนึ่ง
ทางออกที่ชัดเจนคือการแปลงปัญหาการเปรียบเทียบเวลาประทับของเราเป็นปัญหาการเปรียบเทียบระยะเวลา สมมติว่าเราจำเป็นต้องรู้ว่า instant t1 นั้นเป็นก่อนหรือหลัง t2 เราเลือกการอ้างอิงทันทีในอดีตทั่วไปของพวกเขาและเปรียบเทียบระยะเวลาจากการอ้างอิงนี้จนกระทั่งทั้ง t1 และ t2 การอ้างอิงทันทีสามารถทำได้โดยการลบระยะเวลานานพอจาก t1 หรือ t2:
unsigned long reference_instant = t2 - LONG_ENOUGH_DURATION;
unsigned long from_reference_until_t1 = t1 - reference_instant;
unsigned long from_reference_until_t2 = t2 - reference_instant;
if (from_reference_until_t1 < from_reference_until_t2)
// t1 is before t2
สิ่งนี้สามารถทำให้ง่ายขึ้นเป็น:
if (t1 - t2 + LONG_ENOUGH_DURATION < LONG_ENOUGH_DURATION)
// t1 is before t2
มันเป็นการดึงดูดให้ลดความซับซ้อนลงไปif (t1 - t2 < 0)
อีก เห็นได้ชัดว่าสิ่งนี้ใช้ไม่ได้เพราะt1 - t2
การคำนวณเป็นตัวเลขที่ไม่ได้ลงชื่อไม่สามารถเป็นค่าลบได้ อย่างไรก็ตามสิ่งนี้ถึงแม้จะไม่ใช่แบบพกพา แต่ก็ใช้งานได้:
if ((signed long)(t1 - t2) < 0) // works with gcc
// t1 is before t2
คำหลักsigned
ข้างต้นซ้ำซ้อน (มีการlong
ลงชื่อแบบธรรมดาเสมอ) แต่ช่วยทำให้เจตนาชัดเจนขึ้น การแปลงเป็นแบบยาวที่ลงชื่อจะเท่ากับการตั้งค่าLONG_ENOUGH_DURATION
เท่ากับ 24.85 วัน เคล็ดลับคือไม่พกพาเพราะตามมาตรฐานซีผลที่ได้คือการดำเนินการตามที่กำหนดไว้ แต่เนื่องจากคอมไพเลอร์ gcc สัญญาว่าจะทำสิ่งที่ถูกต้องจึงทำงานได้อย่างน่าเชื่อถือบน Arduino หากเราต้องการหลีกเลี่ยงพฤติกรรมการใช้งานที่กำหนดไว้การเปรียบเทียบที่ได้รับการลงนามข้างต้นนั้นเทียบเท่ากับทางคณิตศาสตร์ดังนี้
#include <limits.h>
if (t1 - t2 > LONG_MAX) // too big to be believed
// t1 is before t2
ด้วยปัญหาเดียวที่การเปรียบเทียบดูย้อนหลัง นอกจากนี้ยังเทียบเท่าได้ตราบใดที่ความยาว 32- บิตสำหรับการทดสอบแบบบิตเดียวนี้:
if ((t1 - t2) & 0x80000000) // test the "sign" bit
// t1 is before t2
การทดสอบสามครั้งสุดท้ายนั้นรวบรวมโดย gcc ในรหัสเครื่องเดียวกัน
ฉันจะทดสอบภาพร่างของฉันกับเมาส์แบบโรลโอเวอร์ได้อย่างไร
หากคุณทำตามข้อบังคับข้างต้นคุณควรจะดีทั้งหมด หากคุณยังคงต้องการทดสอบเพิ่มฟังก์ชันนี้ลงในร่างของคุณ:
#include <util/atomic.h>
void setMillis(unsigned long ms)
{
extern unsigned long timer0_millis;
ATOMIC_BLOCK (ATOMIC_RESTORESTATE) {
timer0_millis = ms;
}
}
setMillis(destination)
และตอนนี้คุณสามารถใช้เวลาเดินทางโปรแกรมของคุณโดยการเรียก
หากคุณต้องการให้มันไหลผ่านมิลลิวินาทีซ้ำไปซ้ำมาอีกครั้งเช่น Phil Connors ที่ทำให้ Groundhog Day กลับคืนมาคุณสามารถใส่สิ่งนี้ไว้ภายในloop()
:
// 6-second time loop starting at rollover - 3 seconds
if (millis() - (-3000) >= 6000)
setMillis(-3000);
การประทับเวลาเชิงลบด้านบน (-3000) จะถูกแปลงโดยคอมไพเลอร์เป็นแบบยาวที่ไม่ได้ลงนามซึ่งสอดคล้องกับ 3000 มิลลิวินาทีก่อนหน้าการโรลโอเวอร์ (มันถูกแปลงเป็น 4294964296)
ถ้าฉันต้องการติดตามระยะเวลาที่ยาวนานจริง ๆ ล่ะ
หากคุณต้องการเปิดรีเลย์และปิดสามเดือนต่อมาคุณต้องติดตามมิลลิวินาทีมากเกินไป มีหลายวิธีที่จะทำเช่นนั้น ทางออกที่ตรงไปตรงมาที่สุดอาจขยายmillis()
ไปถึง 64 บิต:
uint64_t millis64() {
static uint32_t low32, high32;
uint32_t new_low32 = millis();
if (new_low32 < low32) high32++;
low32 = new_low32;
return (uint64_t) high32 << 32 | low32;
}
นี่คือการนับเหตุการณ์แบบโรลโอเวอร์และการใช้การนับนี้เป็น 32 บิตที่สำคัญที่สุดของการนับ 64 บิตมิลลิวินาที เพื่อให้การนับนี้ทำงานอย่างถูกต้องฟังก์ชันจะต้องมีการเรียกใช้อย่างน้อยหนึ่งครั้งในทุก ๆ 49.7 วัน แต่ถ้ามันจะเรียกว่าเพียงครั้งเดียวต่อวัน 49.7 สำหรับบางกรณีก็เป็นไปได้ว่าการตรวจสอบล้มเหลวและรหัสพลาดนับของ(new_low32 < low32)
high32
การใช้มิลลิวินาที () เพื่อตัดสินใจว่าเมื่อใดที่จะเรียกใช้รหัสนี้เพียงครั้งเดียวใน "ห่อ" หนึ่งมิลลิวินาที (หน้าต่าง 49.7 วันที่เฉพาะเจาะจง) อาจเป็นอันตรายได้มากขึ้นอยู่กับว่ากรอบเวลาเข้าแถวกันอย่างไร เพื่อความปลอดภัยหากใช้ millis () เพื่อกำหนดเวลาที่จะโทรออกไปยัง millis64 เท่านั้น () ควรมีการโทรอย่างน้อยสองครั้งในทุก ๆ 49.7 วัน
จำไว้ว่าเลขคณิต 64 บิตนั้นแพงใน Arduino มันอาจจะคุ้มค่าที่จะลดความละเอียดของเวลาเพื่อให้อยู่ที่ 32 บิต