ฉันจะจัดการโรลโอเวอร์ของ millis () ได้อย่างไร


73

ฉันต้องอ่านเซ็นเซอร์ทุก ๆ ห้านาที แต่เนื่องจากร่างของฉันมีงานอื่นให้ทำฉันไม่สามารถdelay()อ่านได้ มีการสอนแบบกะพริบโดยไม่ชักช้าแนะนำฉันรหัสตามบรรทัดเหล่านี้:

void loop()
{
    unsigned long currentMillis = millis();

    // Read the sensor when needed.
    if (currentMillis - previousMillis >= interval) {
        previousMillis = currentMillis;
        readSensor();
    }

    // Do other stuff...
}

ปัญหาคือว่าmillis()จะย้อนกลับไปที่ศูนย์หลังจากประมาณ 49.7 วัน เนื่องจากแบบร่างของฉันมีวัตถุประสงค์เพื่อให้ทำงานได้นานกว่านั้นฉันต้องตรวจสอบให้แน่ใจว่าการวางเมาส์แบบโรลโอเวอร์ไม่ทำให้ร่างของฉันล้มเหลว ฉันสามารถตรวจสอบเงื่อนไขการโรลโอเวอร์ ( currentMillis < previousMillis) ได้อย่างง่ายดายแต่ฉันไม่แน่ใจว่าต้องทำอย่างไร

ดังนั้นคำถามของฉัน: อะไรจะเป็นวิธีที่เหมาะสม / ง่ายที่สุดในการจัดการ millis()โรลโอเวอร์?


5
บทบรรณาธิการ: นี่ไม่ใช่คำถามของฉันอย่างแน่นอน แต่เป็นการสอนในรูปแบบคำถาม / คำตอบ ฉันได้เห็นความสับสนมากมายบนอินเทอร์เน็ต (รวมถึงที่นี่) เกี่ยวกับหัวข้อนี้และไซต์นี้ดูเหมือนจะเป็นที่ที่ชัดเจนในการค้นหาคำตอบ นี่คือเหตุผลที่ฉันให้การสอนนี้ที่นี่
Edgar Bonet

2
ฉันจะทำpreviousMillis += intervalแทนpreviousMillis = currentMillisถ้าฉันต้องการความถี่ผลลัพธ์ที่แน่นอน
Jasen

4
@ Jasen: ถูกต้อง! previousMillis += intervalถ้าคุณต้องการความถี่อย่างต่อเนื่องและมั่นใจว่าการประมวลผลของคุณใช้เวลาน้อยกว่าintervalแต่สำหรับรับประกันความล่าช้าอย่างน้อยpreviousMillis = currentMillis interval
Edgar Bonet

เราต้องการคำถามที่พบบ่อยสำหรับสิ่งเช่นนี้

หนึ่งใน "กลอุบาย" ที่ฉันใช้คือการแบ่งเบาภาระใน Arduino โดยใช้ int ที่เล็กที่สุดที่มีช่วงเวลา ตัวอย่างเช่นสำหรับช่วงเวลาสูงสุด 1 นาทีฉันเขียนuint16_t previousMillis; const uint16_t interval = 45000; ... uint16_t currentMillis = (uint16_t) millis(); if ((currentMillis - previousMillis) >= interval) ...
frarugi87

คำตอบ:


95

คำตอบสั้น ๆ : อย่าพยายาม "จัดการ" โรลโอเวอร์มิลลิวินาทีเขียนรหัสปลอดภัยโรลโอเวอร์แทน โค้ดตัวอย่างของคุณจากบทช่วยสอนนั้นใช้ได้ หากคุณพยายามตรวจสอบโรลโอเวอร์เพื่อใช้มาตรการแก้ไขโอกาสที่คุณทำสิ่งผิดปกติ โปรแกรม 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ไม่มีเหตุผล

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

  1. later_timestamp - earlier_timestampทำให้ช่วงเวลาคือจำนวนเวลาที่ผ่านไประหว่างอินสแตนซ์ก่อนหน้านี้และช่วงเวลาต่อมา นี่เป็นการดำเนินการทางคณิตศาสตร์ที่มีประโยชน์ที่สุดที่เกี่ยวข้องกับการประทับเวลา
  2. 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 บิต


2
ดังนั้นคุณกำลังบอกว่ารหัสที่เขียนในคำถามจริงจะทำงานได้อย่างถูกต้อง?
Jasen

3
@ Jasen: แน่นอน! ฉันดูเหมือนคนมากกว่าหนึ่งครั้งที่พยายาม "แก้ไข" ปัญหาที่ไม่มีอยู่ในตอนแรก
Edgar Bonet

2
ฉันดีใจที่ฉันพบสิ่งนี้ ฉันเคยมีคำถามนี้มาก่อน
เซบาสเตียนฟรีแมน

1
หนึ่งในคำตอบที่ดีที่สุดและมีประโยชน์ที่สุดใน StackExchange! ขอบคุณมาก! :)
Falko

นี่เป็นคำตอบที่น่าอัศจรรย์สำหรับคำถามนี้ ฉันกลับมาที่คำตอบนี้โดยทั่วไปปีละครั้งเพราะฉันเป็นคนหวาดระแวงของการหมุนวน
เจฟฟรีย์เงินสด

17

TL; DRเวอร์ชั่นย่อ:

An unsigned longคือ 0 ถึง 4,294,967,295 (2 ^ 32 - 1)

สมมุติว่าpreviousMillisมีค่า 4,294,967,290 (5 ms ก่อนโรลโอเวอร์) และcurrentMillis10 (10ms หลังจากโรลโอเวอร์) จากนั้นcurrentMillis - previousMillisเป็นจริง 16 (ไม่ใช่ -4,294,967,280) เนื่องจากผลลัพธ์จะถูกคำนวณเป็นความยาวที่ไม่ได้ลงชื่อ (ซึ่งไม่สามารถลบได้ดังนั้นตัวมันเองจะหมุนไปรอบ ๆ ) คุณสามารถตรวจสอบได้ง่ายๆโดย:

Serial.println( ( unsigned long ) ( 10 - 4294967290 ) ); // 16

ดังนั้นรหัสข้างต้นจะทำงานได้อย่างสมบูรณ์แบบ เคล็ดลับคือการคำนวณความแตกต่างของเวลาเสมอและไม่เปรียบเทียบค่าเวลาสองค่า


วิธีประมาณ15msก่อนโรลโอเวอร์และ10msหลังจากโรลโอเวอร์ (เช่น 49.7 วันหลังจาก ) 15> 10แต่ตราประทับ15msนั้นเกือบหนึ่งเดือนครึ่งแล้ว 15-10> 0 และ 10-15> 0 unsignedลอจิกดังนั้นจึงไม่มีประโยชน์ที่นี่!
ps95

@ prakharsingh95 10ms-15ms จะกลายเป็น ~ 49.7 วัน - 5ms ซึ่งเป็นความแตกต่างที่ถูกต้อง คณิตศาสตร์ใช้งานได้จนกว่าจะmillis()หมุนสองครั้ง แต่ไม่น่าเป็นไปได้ที่จะเกิดปัญหากับรหัส
BrettAM

ให้ฉันใช้ถ้อยคำใหม่ สมมติว่าคุณมีการประทับเวลาสองครั้ง 200ms และ 10ms คุณจะบอกได้อย่างไรว่าตัวไหนที่ถูกรีด?
ps95

@ prakharsingh95 สิ่งที่เก็บไว้ในpreviousMillisนั้นจะต้องมีการวัดมาก่อนcurrentMillisดังนั้นหากcurrentMillisมีขนาดเล็กกว่าpreviousMillisโรลโอเวอร์ที่เกิดขึ้น คณิตศาสตร์เกิดขึ้นเพื่อหาว่าหากไม่มีโรลโอเวอร์สองตัวเกิดขึ้นคุณไม่จำเป็นต้องคิดเลย
BrettAM

1
อาโอเค. ถ้าคุณทำt2-t1และถ้าคุณสามารถรับประกันt1ได้วัดมาก่อนt2แล้วมันก็เทียบเท่ากับการลงนาม (t2-t1)% 4,294,967,295ดังนั้นการพันอัตโนมัติ !. นีซ แต่ถ้ามีสอง rollovers หรือinterval> 4,294,967,295 ล่ะ
ps95

1

ห่อmillis()ในชั้นเรียน!

ตรรกะ:

  1. ใช้รหัสแทนmillis()โดยตรง
  2. เปรียบเทียบการกลับรายการโดยใช้รหัส นี่คือสะอาดและแบบโรลโอเวอร์ที่เป็นอิสระ
  3. สำหรับแอปพลิเคชั่นที่เฉพาะเจาะจงเพื่อคำนวณความแตกต่างที่แน่นอนระหว่างสอง id ติดตามการกลับรายการและการประทับ คำนวณความแตกต่าง

การติดตามการกลับรายการ:

  1. อัพเดทแสตมป์ท้องถิ่นเป็นระยะ ๆ millis()ได้เร็วกว่า สิ่งนี้จะช่วยให้คุณทราบว่าmillis()มีสิ่งใดล้น
  2. ช่วงเวลาของตัวจับเวลาจะกำหนดความแม่นยำ
class Timer {

public:
    static long last_stamp;
    static long *stamps;
    static int *reversals;
    static int count;
    static int reversal_count;

    static void setup_timer() {
        // Setup Timer2 overflow to fire every 8ms (125Hz)
        //   period [sec] = (1 / f_clock [sec]) * prescale * (255-count)
        //                  (1/16000000)  * 1024 * (255-130) = .008 sec


        TCCR2B = 0x00;        // Disable Timer2 while we set it up

        TCNT2  = 130;         // Reset Timer Count  (255-130) = execute ev 125-th T/C clock
        TIFR2  = 0x00;        // Timer2 INT Flag Reg: Clear Timer Overflow Flag
        TIMSK2 = 0x01;        // Timer2 INT Reg: Timer2 Overflow Interrupt Enable
        TCCR2A = 0x00;        // Timer2 Control Reg A: Wave Gen Mode normal
        TCCR2B = 0x07;        // Timer2 Control Reg B: Timer Prescaler set to 1024

        count = 0;
        stamps = new long[50];
        reversals = new int [10];
        reversal_count =0;
    }

    static long get_stamp () {
        stamps[count++] = millis();
        return count-1;
    }

    static bool compare_stamps_by_id(int s1, int s2) {
        return s1 > s2;
    }

    static long long get_stamp_difference(int s1, int s2) {
        int no_of_reversals = 0;
        for(int j=0; j < reversal_count; j++)
        if(reversals[j] < s2 && reversals[j] > s1)
            no_of_reversals++;
        return stamps[s2]-stamps[s1] + 49.7 * 86400 * 1000;       
    }

};

long Timer::last_stamp;
long *Timer::stamps;
int *Timer::reversals;
int Timer::count;
int Timer::reversal_count;

ISR(TIMER2_OVF_vect) {

    long stamp = millis();
    if(stamp < Timer::last_stamp) // reversal
        Timer::reversals[Timer::reversal_count++] = Timer::count;
    else 
        ; // no reversal
    Timer::last_stamp = stamp;    
    TCNT2 = 130;     // reset timer ct to 130 out of 255
    TIFR2 = 0x00;    // timer2 int flag reg: clear timer overflow flag
};

// Usage

void setup () {
    Timer::setup_timer();

    long s1 = Timer::get_stamp();
    delay(3000);
    long s2 = Timer::get_stamp();

    Timer::compare_stamps_by_id(s1, s2); // true

    Timer::get_stamp_difference(s1, s2); // return true difference, taking into account reversals
}

สินเชื่อจับเวลา


9
ฉันแก้ไขรหัสเพื่อลบข้อผิดพลาด maaaaany ที่ป้องกันไม่ให้รวบรวม สิ่งนี้จะทำให้คุณเสียค่าใช้จ่ายประมาณ 232 ไบต์ของ RAM และสองช่องสัญญาณ PWM มันจะเริ่มทำลายหน่วยความจำหลังจากคุณget_stamp()51 ครั้ง การเปรียบเทียบความล่าช้าแทนการประทับเวลาจะมีประสิทธิภาพมากขึ้นอย่างแน่นอน
Edgar Bonet

1

ฉันรักคำถามนี้และคำตอบที่ยอดเยี่ยมก็สร้างขึ้น ก่อนอื่นให้ความเห็นอย่างรวดเร็วเกี่ยวกับคำตอบก่อนหน้า (ฉันรู้ฉันรู้ แต่ฉันยังไม่มีตัวแทนที่จะแสดงความคิดเห็น :-)

คำตอบของ Edgar Bonet นั้นน่าทึ่งมาก ฉันเขียนโปรแกรมมา 35 ปีแล้วและเรียนรู้สิ่งใหม่วันนี้ ขอขอบคุณ. ที่กล่าวว่าฉันเชื่อรหัสสำหรับ"เกิดอะไรขึ้นถ้าฉันต้องการติดตามระยะเวลานานจริง ๆ " หยุดพักเว้นแต่ว่าคุณจะเรียก millis64 () อย่างน้อยหนึ่งครั้งต่อช่วงเวลาโรลโอเวอร์ nitpicky จริงๆและไม่น่าจะเป็นปัญหาในการใช้งานจริง แต่มีคุณไป

ตอนนี้ถ้าคุณต้องการให้ timestamps ครอบคลุมช่วงเวลาใด ๆ (มิลลิวินาที 64 บิตอยู่ที่ประมาณครึ่งพันล้านปีโดยการคำนวณของฉัน) มันดูง่ายที่จะขยายการใช้มิลลิวินาทีที่มีอยู่ไปเป็น 64 บิต

การเปลี่ยนแปลงเหล่านี้เป็น attinycore / wiring.c (ฉันทำงานกับ ATTiny85) ดูเหมือนจะใช้งานได้ (ฉันสมมติว่ารหัสสำหรับ AVR อื่น ๆ นั้นคล้ายกันมาก) ดูบรรทัดที่มีความคิดเห็น // BFB และฟังก์ชัน millis64 () ใหม่ เห็นได้ชัดว่ามันจะเป็นทั้งใหญ่กว่า (98 ไบต์ของรหัสข้อมูล 4 ไบต์) และช้ากว่าและเมื่อเอ็ดการ์ชี้ให้เห็นคุณเกือบจะสามารถบรรลุเป้าหมายของคุณได้ด้วยความเข้าใจที่ดีขึ้นของคณิตศาสตร์เลขจำนวนเต็มที่ไม่ได้ลงนาม .

volatile unsigned long long timer0_millis = 0;      // BFB: need 64-bit resolution

#if defined(__AVR_ATtiny24__) || defined(__AVR_ATtiny44__) || defined(__AVR_ATtiny84__)
ISR(TIM0_OVF_vect)
#else
ISR(TIMER0_OVF_vect)
#endif
{
    // copy these to local variables so they can be stored in registers
    // (volatile variables must be read from memory on every access)
    unsigned long long m = timer0_millis;       // BFB: need 64-bit resolution
    unsigned char f = timer0_fract;

    m += MILLIS_INC;
    f += FRACT_INC;
    if (f >= FRACT_MAX) {
        f -= FRACT_MAX;
        m += 1;
    }

    timer0_fract = f;
    timer0_millis = m;
    timer0_overflow_count++;
}

// BFB: 64-bit version
unsigned long long millis64()
{
    unsigned long long m;
    uint8_t oldSREG = SREG;

    // disable interrupts while we read timer0_millis or we might get an
    // inconsistent value (e.g. in the middle of a write to timer0_millis)
    cli();
    m = timer0_millis;
    SREG = oldSREG;

    return m;
}

1
คุณพูดถูกฉันmillis64()จะทำงานก็ต่อเมื่อมีการเรียกใช้บ่อยกว่าระยะเวลาโรลโอเวอร์ ฉันแก้ไขคำตอบเพื่อชี้ให้เห็นข้อ จำกัด นี้ รุ่นของคุณไม่มีปัญหานี้ แต่มีข้อเสียเปรียบอีกประการหนึ่ง: เป็นเลขคณิต 64 บิตในบริบทของการขัดจังหวะซึ่งบางครั้งจะเพิ่มเวลาแฝงในการตอบสนองต่อการขัดจังหวะอื่น ๆ
Edgar Bonet
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.