I was trying to get the timing in between events right, (Event time is around 150ms and i need it to be accurate within +-1ms) and came upon this implementation. The actual application will not be affected by the roll over, but would like to see if any of you guys (experts) can point out any flaws in using an empty while loop for time events, besides slightly higher power consumption.
This implementation gave me very accurate time spacings:
unsigned long previousMillis = millis();
Serial.print("ONE: ");
Serial.println(millis());
while(millis() - previousMillis < 1000);
Serial.print("SECOND: ");
Serial.println(millis());
compared with...
Serial.print("ONE: ");
Serial.println(millis());
delay(1000);
Serial.print("SECOND: ");
Serial.println(millis());
Which yields some errors after every step.
3 Answers 3
There are many options for executing a task periodically. The most straightforward is to use a slight variation of code given in the Blink Without Delay[] Arduino tutorial:
const uint32_t PERIOD = 150000; // 150 ms
void loop() {
static uint32_t last_time;
if (micros() - last_time >= PERIOD) {
last_time += period;
do_periodic_task();
}
do_other_tasks();
}
Note two changes relative to the code in the tutorial:
- This is using
micros()
instead ofmillis()
. This way the timing resolution is 4 μs instead of 1 to 2 milliseconds. last_time
is updated aslast_time += period
instead oflast_time = micros()
. This way, even if the periodic task got delayed by the other tasks, these delays don't add up.
The timing will not be perfect: you will have both drift and jitter.
The drift comes from the inaccuracy of the ceramic resonator clocking
the microcontroller. These resonators have typically 0.5% frequency
tolerance, although the typical frequency error is more likely to be
around ±0.1% (i.e. 0.15 ms error in a 150 ms period). Note
that a poorly written code would add it's own drift, e.g. if you do
last_time = micros()
, but this does not.
The jitter comes from the fact that the clock is checked only once per loop iteration, and your periodic task can then be delayed by the other tasks. The amplitude of the jitter is the maximum time taken by a loop iteration.
Despite these imperfections, this would be my first choice unless I really needed better timing accuracy.
If you need lower drift, one option is to calibrate your Arduino clock.
You measure how fast or slow it is, and you tweak the PERIOD
constant
accordingly. For example, if you measure the clock to be 700 ppm
slow, you would fix it like this:
const uint32_t NOMINAL_PERIOD = 150000; // 150 ms
// Adjust the period to account for clock drift.
const float FREQUENCY_OFFSET = -700e-6; // -700 ppm
const uint32_t PERIOD = NOMINAL_PERIOD * (1 + FREQUENCY_OFFSET);
This technique allows you to tune the frequency with about 6.7 ppm resolution, for a period of 150 ms. You cannot reasonably expect anything better, as the frequency of the ceramic resonator is not very stable anyway.
If you need lower jitter, one option is to perform a blocking wait: you ask the processor to do nothing but wait until it's time to perform the periodic task. This is similar to the code you posted in your question:
void loop() {
static uint32_t last_time;
while (micros() - last_time < PERIOD) ; // busy wait
last_time += period;
do_periodic_task();
}
This will not completely suppress the jitter, but it will reduce it to
just the time taken by the while
loop. However, there is a big issue
with this way of coding: now your processor does nothing but the
periodic task. It may not be a problem right now, but as your project
grows, it is more than likely that at some point you will want to handle
extra tasks like, e.g. responding to some user input.
If you want to avoid completely blocking the processor on the busy wait, yet you want the smallest possible jitter, you could try some sort of compromise: you let the processor take care of other tasks until it is almost time to perform the periodic task. Then you switch to the blocking mode where the processor does nothing but wait for the clock. Here, "almost time" would be defined by the maximum time your loop can take:
const uint32_t MAX_LOOP_TIME = 2000; // assume 2 ms
void loop() {
static uint32_t last_time;
if (micros() - last_time >= PERIOD - MAX_LOOP_TIME) {
while (micros() - last_time < PERIOD) ; // busy wait
last_time += period;
do_periodic_task();
}
do_other_tasks();
}
Another option, that has been mentioned in comments, is to trigger the task by a timer. This will work if it is a very short task that can safely be run with interrupts disabled. You have four 16-bit timers on your Mega, so you can presumably spare one for timing this task. This could be done with Timer 1 as follows (warning: untested code):
const int TIMER_PRESCALER = 64;
const float F_TIMER = F_CPU / TIMER_PRESCALER;
const float PERIOD = 150e-3;
const uint16_t TIMER_PERIOD = F_TIMER*PERIOD + 0.5; // round to nearest
void setup() {
TCCR1B = 0; // stop the timer
OCR1A = TIMER_PERIOD - 1;
TIFR1 |= _BV(OCF1A); // clear TIMER1_COMPA interrupt flag
TIMSK1 = _BV(OCIE1A); // enable TIMER1_COMPA interrupt
TCCR1A = 0; // no PWM
TCCR1B = _BV(WGM12) // CTC mode, TOP = OCR1A
| _BV(CS10) // clock at F_CPU / 64
| _BV(CS11); // ditto
}
ISR(TIMER1_COMPA) {
do_periodic_task();
}
Note that TIMER_PERIOD
will be 37500. If you want to tune it to
account for an inaccurate clock frequency, your tuning resolution will
be about 26.7 ppm.
The interrupt technique should get you sub-microsecond jitter most of
the time. However, every now and then the Timer 0 interrupt (the
one responsible for updating the millis()
counter) will delay your
interrupts for a few microseconds. Beware also that some libraries can
delay interrupts for excessive amounts of time. Software Serial is an
infamous offender.
Those two are not the same "program". The first one start counting before Serial.print
. The other first print and then delay for a second.
We have two piece of code:
- The first take 1000ms to reach "SECOND" (plus some overhead).
- The second take 1000ms + time needed for two Serial.prints and a millis() call to reach "SECOND" (plus the same overhead).
The first approach is better, because you start counting before doing operations of undetermined length. So, it's immune to any factor affecting running time, like time spent in interrupts. And you don't need to retune it if you change the sketch.
-
That's interesting, I didn't notice that! The Serial.print is just for checking. I will implement Port Manipulation with this method so the accuracy should be sufficient. Just want to see if using the while loop like this might cause some issues unknown to my skill level.James Low– James Low2018年02月08日 11:10:27 +00:00Commented Feb 8, 2018 at 11:10
-
@JamesLow. There are some fine points. If delay is too big, the watchdog could reset the board. And on a ESP8266 based system, you have to call
yield
inside the do-while to let processor manage Wi-Fi.user31481– user314812018年02月08日 11:41:27 +00:00Commented Feb 8, 2018 at 11:41
For better accuracy, you should not be "delaying until it's time to do something", but instead "checking if it's time to do it yet".
It's all very well saying you want something to happen every 150ms and delaying between actions for 150ms, but that won't yield you an event that happens every 150ms - it yields you an event that takes X amount of time with a delay of Y between each event, resulting in an overall period of X+Y, which is not what you want.
I cover the proper ways to deal with timing of regular events in this existing answer:
millis()
. Usemicros()
instead.delay()
? What makes you think yourwhile
loop would use more power than thewhile
loop in thedelay()
function?micros()
can be used to measure times up to 71.6 minutes, which is its rollover period. The drift ofmicros()
is the drift of the ceramic oscillator clocking your MCU. Typically less than 0.5%. Any timing method (delay()
,millis()
,micros()
, hardware timers, etc...) will suffer the same drift, unless using an external time source, like an RTC.