Câu trả lời ngắn gọn: đừng cố gắng để xử lý rollover Millis, thay vào đó hãy viết mã rollover an toàn. Mã ví dụ của bạn từ hướng dẫn là tốt. Nếu bạn cố gắng phát hiện cuộn qua để thực hiện các biện pháp khắc phục, rất có thể bạn đang làm sai điều gì đó. Hầu hết các chương trình Arduino chỉ phải quản lý các sự kiện kéo dài thời lượng tương đối ngắn, như gỡ nút trong 50 ms hoặc bật máy sưởi trong 12 giờ ... Sau đó, và ngay cả khi chương trình có nghĩa là chạy trong nhiều năm, cuộn dây millis không nên là một mối quan tâm.
Cách chính xác để quản lý (hay đúng hơn là tránh phải quản lý) vấn đề tái đầu tư là nghĩ về con unsigned long
số được trả về theo
millis()
thuật ngữ mô đun . Đối với những người có khuynh hướng toán học, một số quen thuộc với khái niệm này rất hữu ích khi lập trình. Bạn có thể thấy toán học đang hoạt động trong bài viết của Mill Gammon () tràn ngập ... một điều tồi tệ? . Đối với những người không muốn xem qua các chi tiết tính toán, tôi cung cấp ở đây một cách nghĩ khác (hy vọng đơn giản hơn) về nó. Nó được dựa trên sự phân biệt đơn giản giữa khoảnh khắc và khoảng thời gian . Miễn là các xét nghiệm của bạn chỉ liên quan đến việc so sánh thời lượng, bạn sẽ ổn thôi.
Lưu ý trên micros () : Mọi thứ được nói ở đây millis()
đều áp dụng như nhau micros()
, ngoại trừ thực tế là micros()
cứ sau 71,6 phút, và setMillis()
chức năng được cung cấp dưới đây không ảnh hưởng micros()
.
Thời gian, dấu thời gian và thời lượng
Khi giao dịch với thời gian, chúng ta phải làm cho sự phân biệt giữa ít nhất hai khái niệm khác nhau: khoảnh khắc và khoảng thời gian . Ngay lập tức là một điểm trên trục thời gian. Thời lượng là độ dài của một khoảng thời gian, tức là khoảng cách thời gian giữa các thời điểm xác định thời điểm bắt đầu và kết thúc của khoảng thời gian. Sự khác biệt giữa các khái niệm này không phải lúc nào cũng rất sắc nét trong ngôn ngữ hàng ngày. Ví dụ, nếu tôi nói “ Tôi sẽ trở lại trong năm phút ”, sau đó “ lăm phút ” là ước tính
thời gian vắng mặt của tôi, trong khi đó “ trong năm phút ” là ngay lập tức
dự đoán của tôi sẽ trở lại. Giữ sự khác biệt trong tâm trí là rất quan trọng, bởi vì đó là cách đơn giản nhất để hoàn toàn tránh được vấn đề tái đầu tư.
Giá trị trả về của millis()
có thể được hiểu là thời lượng: thời gian trôi qua từ khi bắt đầu chương trình cho đến bây giờ. Sự giải thích này, tuy nhiên, bị phá vỡ ngay khi millis tràn ra. Nhìn chung sẽ hữu ích hơn rất nhiều khi nghĩ đến việc millis()
trả về
dấu thời gian , tức là nhãn của nhãn xác nhận xác định tức thời. Có thể lập luận rằng cách giải thích này bị các nhãn này không rõ ràng, vì chúng được sử dụng lại sau mỗi 49,7 ngày. Tuy nhiên, điều này hiếm khi là một vấn đề: trong hầu hết các ứng dụng nhúng, bất cứ điều gì xảy ra 49,7 ngày trước là lịch sử cổ đại mà chúng ta không quan tâm. Vì vậy, tái chế các nhãn cũ không phải là một vấn đề.
Không so sánh dấu thời gian
Cố gắng tìm ra cái nào trong số hai dấu thời gian lớn hơn cái kia không có ý nghĩa gì. Thí dụ:
unsigned long t1 = millis();
delay(3000);
unsigned long t2 = millis();
if (t2 > t1) { ... }
Ngây thơ, người ta sẽ mong muốn điều kiện if ()
luôn luôn đúng. Nhưng nó thực sự sẽ là sai nếu millis tràn trong
delay(3000)
. Nghĩ rằng t1 và t2 là nhãn có thể tái chế là cách đơn giản nhất để tránh lỗi: nhãn t1 rõ ràng đã được gán cho tức thì trước t2, nhưng trong 49,7 ngày, nó sẽ được gán lại cho tức thì trong tương lai. Do đó, t1 xảy ra cả trước và sau t2. Điều này sẽ làm rõ rằng biểu hiện t2 > t1
không có ý nghĩa.
Nhưng, nếu đây chỉ là những nhãn hiệu, câu hỏi rõ ràng là: làm thế nào chúng ta có thể thực hiện bất kỳ phép tính thời gian hữu ích nào với chúng? Câu trả lời là: bằng cách giới hạn bản thân trong hai phép tính duy nhất có ý nghĩa đối với dấu thời gian:
later_timestamp - earlier_timestamp
mang lại một khoảng thời gian, cụ thể là lượng thời gian trôi qua giữa tức thời trước đó và tức thời sau đó. Đây là hoạt động số học hữu ích nhất liên quan đến dấu thời gian.
timestamp ± duration
mang lại dấu thời gian sau một thời gian (nếu sử dụng +) hoặc trước (nếu -) dấu thời gian ban đầu. Không hữu ích như âm thanh, vì dấu thời gian kết quả có thể được sử dụng chỉ trong hai loại tính toán ...
Nhờ các tính năng mô-đun, cả hai đều được đảm bảo hoạt động tốt trên rollover millis, ít nhất là miễn là sự chậm trễ liên quan là ngắn hơn 49,7 ngày.
So sánh thời lượng là tốt
Thời lượng chỉ là lượng mili giây trôi qua trong một khoảng thời gian. Miễn là chúng ta không cần phải xử lý thời lượng dài hơn 49,7 ngày, bất kỳ hoạt động nào có ý nghĩa về mặt vật lý cũng sẽ có ý nghĩa về mặt tính toán. Ví dụ, chúng ta có thể nhân một khoảng thời gian với tần suất để có được một số khoảng thời gian. Hoặc chúng ta có thể so sánh hai thời lượng để biết cái nào dài hơn. Ví dụ, đây là hai triển khai thay thế của delay()
. Đầu tiên, lỗi một:
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;
}
}
Và đây là một trong những chính xác:
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;
}
}
Hầu hết các lập trình viên C sẽ viết các vòng lặp ở trên dưới dạng terser, như
while (millis() < start + ms) ; // BUGGY version
và
while (millis() - start < ms) ; // CORRECT version
Mặc dù trông chúng có vẻ giống nhau, nhưng việc phân biệt dấu thời gian / thời lượng sẽ làm rõ cái nào là lỗi và cái nào đúng.
Nếu tôi thực sự cần so sánh dấu thời gian thì sao?
Tốt hơn nên cố gắng tránh tình huống. Nếu điều đó là không thể tránh khỏi, vẫn còn hy vọng nếu biết rằng các chất tương ứng đã đủ gần: gần hơn 24,85 ngày. Có, độ trễ quản lý tối đa 49,7 ngày của chúng tôi đã bị cắt giảm một nửa.
Giải pháp rõ ràng là chuyển đổi vấn đề so sánh dấu thời gian của chúng tôi thành vấn đề so sánh thời lượng. Nói rằng chúng ta cần biết t1 tức thì là trước hay sau t2. Chúng tôi chọn một số tham chiếu tức thì trong quá khứ chung của chúng và so sánh thời lượng từ tham chiếu này cho đến cả t1 và t2. Thời gian tham chiếu có được bằng cách trừ một khoảng thời gian đủ dài từ t1 hoặc 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
Điều này có thể được đơn giản hóa như:
if (t1 - t2 + LONG_ENOUGH_DURATION < LONG_ENOUGH_DURATION)
// t1 is before t2
Nó là cám dỗ để đơn giản hóa hơn nữa vào if (t1 - t2 < 0)
. Rõ ràng, điều này không hoạt động, bởi vì t1 - t2
, được tính là một số không dấu, không thể âm. Điều này, tuy nhiên, mặc dù không di động, nhưng hoạt động:
if ((signed long)(t1 - t2) < 0) // works with gcc
// t1 is before t2
Từ khóa signed
ở trên là dư thừa (một đồng bằng long
luôn được ký), nhưng nó giúp làm rõ ý định. Chuyển đổi thành một chữ ký dài tương đương với cài đặt LONG_ENOUGH_DURATION
bằng 24,85 ngày. Thủ thuật này không khả dụng vì theo tiêu chuẩn C, kết quả là việc thực hiện được xác định . Nhưng vì trình biên dịch gcc hứa hẹn sẽ làm điều đúng đắn , nó hoạt động đáng tin cậy trên Arduino. Nếu chúng tôi muốn tránh thực hiện hành vi được xác định, so sánh đã ký ở trên tương đương về mặt toán học với điều này:
#include <limits.h>
if (t1 - t2 > LONG_MAX) // too big to be believed
// t1 is before t2
với vấn đề duy nhất là sự so sánh có vẻ ngược. Nó cũng tương đương, miễn là 32 bit, đối với thử nghiệm một bit này:
if ((t1 - t2) & 0x80000000) // test the "sign" bit
// t1 is before t2
Ba thử nghiệm cuối cùng thực sự được gcc biên dịch thành cùng một mã máy.
Làm cách nào để kiểm tra bản phác thảo của tôi chống lại cuộn dây millis
Nếu bạn tuân theo các giới luật ở trên, bạn sẽ được tốt. Nếu bạn vẫn muốn kiểm tra, hãy thêm chức năng này vào bản phác thảo của bạn:
#include <util/atomic.h>
void setMillis(unsigned long ms)
{
extern unsigned long timer0_millis;
ATOMIC_BLOCK (ATOMIC_RESTORESTATE) {
timer0_millis = ms;
}
}
và bây giờ bạn có thể du hành thời gian chương trình của bạn bằng cách gọi
setMillis(destination)
. Nếu bạn muốn nó đi qua hàng triệu lần tràn qua nhiều lần, như Phil Connors hồi tưởng lại Ngày con rắn, bạn có thể đặt cái này vào trong loop()
:
// 6-second time loop starting at rollover - 3 seconds
if (millis() - (-3000) >= 6000)
setMillis(-3000);
Dấu thời gian âm ở trên (-3000) được trình biên dịch ngầm chuyển thành một dấu dài không dấu tương ứng với 3000 mili giây trước khi cuộn qua (nó được chuyển đổi thành 4294964296).
Điều gì xảy ra nếu tôi thực sự cần theo dõi thời lượng rất dài?
Nếu bạn cần bật một rơle và tắt nó ba tháng sau đó, thì bạn thực sự cần phải theo dõi các tràn millis. Có nhiều cách để làm như vậy. Giải pháp đơn giản nhất có thể chỉ đơn giản là mở rộng millis()
đến 64 bit:
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;
}
Điều này về cơ bản là đếm các sự kiện tái đầu tư và sử dụng số này là 32 bit quan trọng nhất của số đếm mili giây 64 bit. Để tính toán này hoạt động bình thường, hàm cần được gọi ít nhất một lần sau mỗi 49,7 ngày. Tuy nhiên, nếu nó chỉ được gọi một lần trong 49,7 ngày, đối với một số trường hợp, có thể kiểm tra (new_low32 < low32)
không thành công và mã bị mất một số lượng high32
. Sử dụng millis () để quyết định khi nào thực hiện cuộc gọi duy nhất tới mã này trong một "gói" millis (cửa sổ 49,7 ngày cụ thể) có thể rất nguy hiểm, tùy thuộc vào cách sắp xếp khung thời gian. Để an toàn, nếu sử dụng millis () để xác định thời điểm thực hiện các cuộc gọi duy nhất đến millis64 (), nên có ít nhất hai cuộc gọi trong mỗi cửa sổ 49,7 ngày.
Mặc dù vậy, hãy ghi nhớ rằng số học 64 bit đắt tiền trên Arduino. Có thể đáng để giảm độ phân giải thời gian để giữ ở mức 32 bit.