4

millis() uses timer0 (linked to CPU clock) to count time, but ADC_sleep mode stops the CPU clock, therefore millis() will drift (lag behind) after each ADC conversion performed in ADC_sleep mode.

With the standard number of CPU cycles needed for the ADC conversion (ADC prescaler=128 multiplied by ADC clock cycles=13), and with the standard prescaler for timer0 (64), it would look like millis() loses 13.5 tics (about 0.1 ms) for every ADC conversion performed in ADC_sleep mode.

I could update the timer0 register (TCNT0) by increasing it manually by 14 but first of all I would still have a 0.5 ticks drift (or a bit less, since the update operation would take some cycles), second I would miss the overflow event (the one Arduino uses internally for time tracking) every time the ADC conversion is started with TCNT0 > 230.

How should I proceed to use ADC_sleep mode during ADC conversions but still keep millis() accurate?

3

Rather than trying to update TCNT0, it might be better to track the number of cycles lost to ADC conversions, and in an intermediate routine – eg, omillis() – compensate for those cycles. [Edit: See better alternative, below]

In more detail:

• At each conversion, add about 128·13 cycles (or perhaps 128·13.5, to account for average prescaler delay) to a cycle counter:
long lostcycles; ...; lostcycles += ADCcycles;

• Within each omillis() call, say return millis()+lostcycles/16000

• Or perhaps say return millis()+lostcycles>>14 to avoid wasting time on a division. This would be off by 2% on the correction, but I presume the correction is small, say 5% of total time, leading to about 0.1% error because of using a shift instead of a divide.

Re: “I would miss the overflow event (the one Arduino uses internally for time tracking) every time the ADC conversion is started with TCNT0 > 230”

That's incorrect [if using suggested approach]. Even if the timer 0 overflow is handled a hundred microseconds late, it will be handled ok. [As noted in Edgar Bonet's comment, a method that just adds to TCNT0 without checking arithmetic overflow, as in original approach, could miss a timer tick.]

Note, the example code using SLEEP_MODE_ADC in the “Sleep during reading” section of ADC conversion on the Arduino on Gammon Forum suggests that one should check for the case where a timer or other interrupt occurs during an ADC conversion. While SLEEP_MODE_ADC stops clkI/O, clkCPU, and clkFLASH, it leaves Timer 2, WDT, and some other potential interrupt sources active.

A better alternative:

As Gerben points out, it's slightly cleaner to update the count variable maintained by millis(), rather than using an intermediate routine. For background, refer to his answer to an earlier question. Here is an adaptation [untested] of the idea to the current case:

unsigned int lostcycles=0;
...;
void accountForADC() {
  enum { ADCcycles = 13*128 + 64;  ms_cycles=16000 }; // adjust appropriately
  extern volatile unsigned long timer0_millis;
  lostcycles += ADCcycles;
  while (lostcycles >= ms_cycles) { // This loop usually runs one pass only
    byte statusReg = SREG; // Save interrupt status
    cli();                 // Turn off interrupts
    ++timer0_millis;       // Need interrupts off for atomicity
    SREG = statusReg;      // Restore old status
    lostcycles -= ms_cycles;
  }
}

After startup, on the tenth call to accountForADC() this code would add 1 to timer0_millis to account for a millisecond lost to ADC conversions. lostcycles -= ms_cycles would set lostcycles to 1280. After another 9 calls we again add 1 to timer0_millis and reduce lostcycles; and so forth.

  • At the end keeping track of the number of conversions was my idea as well, thanks for confirmation (yours is cleaner than my idea) – FarO Oct 10 '16 at 18:56
  • @OlafM, thanks! Note, I miscalculated the cycles to add -- 108 us is wrong units; I presume 13*128 = 1664 cycles is the number to use – James Waldby - jwpat7 Oct 10 '16 at 19:09
  • You definitely can miss the overflow event if, along the lines of the OP's original idea, you do TCNT0 += 26; and that operation overflows. – Edgar Bonet Oct 10 '16 at 19:09
  • @EdgarBonet, true! Something the data sheet warns about. – James Waldby - jwpat7 Oct 10 '16 at 19:10
  • 1
    @Gerben, thanks for pointing that out... have edited answer accordingly – James Waldby - jwpat7 Oct 11 '16 at 19:17
2

I won't tell you how to do what you ask as I don't really know how to do it. But I have the gut feeling that it can be done. Only it would be really, really hard, so this brings the question: is it worth it?

To avoid posting a pure opinion-based answer, I'll try to give you some leads:

  • You can set the timer 0 with single-CPU-cycle resolution simply by resetting its prescaler whenever you set a new value.
  • You can make very precise delays with the _delay_us() function. The argument should be a compile-time constant. _delay_us(0.125) does exactly what it says it does.
  • Beware of the prescaler of the ADC clock: it will induce some jitter on the ADC timings unless you reset it as well. AFAIK, the only way to reset that prescaler is to start the ADC conversion using some trigger source in auto-trigger mode. This may cost you a timer.
  • If you miss the overflow event, you can just manually call the relevant ISR (TIMER0_OVF_vect(), which you declare with ISR(TIMER0_OVF_vect);). Sounds silly, but it works, with the only caveat that it forcibly enables interrupts upon return.

To know how many tics exactly you are loosing, you could spend a few evenings carefully studying the timings in the datasheet, or you could try to calibrate against a stable frequency source. I would recommend the latter.

1

I think you read the datasheet wrong. The conversion takes 13 ADC cycles (except for the first one), not 13.5.

Secondly; the conversion start at the next rising edge of the ADC clock, not directly. So because the prescaler is 128, it can take an additional 127 main clock cycles before the next ADC clock's rising edge. So that means and ADC conversion takes between 13 and 13.9921875 ADC clock cycles.

I'm also not sure how long it takes the uC to restart the CPU clock. So there might me more time "missed" here.

Though you must be aware that the crystal oscillator isn't that accurate to begin with.

  • In my mind I thought that the CPU clock would stop when the ADC actually starts, therefore the "from 0 to 127" cycles of delay was not considered. I see it's more complex than that. – FarO Oct 10 '16 at 21:53
1

The time to start a conversion is not fixed. From my page about timers we can see this:

Tip:

There is an extra time before a conversion starts. The conversion starts on the leading edge of the ADC clock, not the moment the code asks for it. In the case of a scaler of 128, there could be 127 extra (processor) clock cycles added, because the hardware has to wait for the next ADC clock cycle. This could add 7.938 µs to the conversion time.

It looks like the prescaler can be reset by writing 0 to ADEN. According to the datasheet:

The prescaler starts counting from the moment the ADC is switched on by setting the ADEN bit in ADCSRA. The prescaler keeps running for as long as the ADEN bit is set, and is continuously reset when ADEN is low.

If you do that however then you add to the conversion time, as it will now take 25 ADC clock cycles rather than 13.


  • As others have suggested, if accurate time is what you want, then an external clock chip could well be the answer. The chip and parts are likely to come to a dollar or so.

  • An alternative would be to get an external ADC chip, thus moving the problem of noise-reduction to a chip which is (hopefully) designed to cope with it.

  • Another possibility would be to put an external clock input to Timer 1 and run that asynchronously. As I read the data sheet, that timer will keep running in ADC sleep mode, and thus you could use that for timing. (That wouldn't help with millis() and micros() but you could write something similar that used Timer 1).

    Edit - as Edgar Bonet pointed out, I misread the datasheet. It is Timer 2 that can do this, not Timer 1. And the pins to run the timer asynchronously are not on the PDIP chip on the Uno, and are not brought out to board pins on the Mega.

  • Only Timer 2 can run asynchronously, at lest on the Uno and the Mega. And you cannot use the asynchronous mode on those boards because the required pins (TOSC1/TOSC2) are not available. – Edgar Bonet Oct 11 '16 at 9:02
  • Whoops! Misread the datasheet. Corrected my answer, thanks. – Nick Gammon Oct 11 '16 at 20:07

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.