2

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.

  • 1
    ±1 ms accuracy is borderline for millis(). Use micros() instead. – Edgar Bonet Feb 8 '18 at 11:04
  • Thanks for pointing that out. I added that my event time is actually around 150 ms. The micros() function is stated in the documentation to be accurate up to 16000us/16ms only. But does not state how much is the drift. Might need to test it out.. – James Low Feb 8 '18 at 11:07
  • 1
    Have you looked at the source code for delay()? What makes you think your while loop would use more power than the while loop in the delay() function? – Majenko Feb 8 '18 at 11:11
  • 2
    The micros() function is stated in the documentation to be accurate up to 16000us/16ms only.” That's incorrect. In what documentation did you find that? micros() can be used to measure times up to 71.6 minutes, which is its rollover period. The drift of micros() 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. – Edgar Bonet Feb 8 '18 at 11:19
  • @EdgarBonet "In what documentation did you find that?" The official delayMicroseconds reference page: arduino.cc/reference/en/language/functions/time/… – per1234 Feb 8 '18 at 13:25
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:

  1. This is using micros() instead of millis(). This way the timing resolution is 4 µs instead of 1 to 2 milliseconds.
  2. last_time is updated as last_time += period instead of last_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.

0

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 Feb 8 '18 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 Feb 8 '18 at 11:41
0

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:

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.