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 of millis(). This way the timing
resolution is 4 µs instead of 1 to 2 milliseconds.
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.