4
\$\begingroup\$

I wrote a program myself to implement the millis() and delay() function without the arduino library. I included a counting variable which counts every second and send its value every second via serial port. What I found is its value runs away from the true value by almost 2 seconds every 3 minutes. Is there anything wrong with my code? Or is that Serial.print() the culprit which may lag that routine? How much time does that Serial.print() takes to execute?

Here is the code:

Edit: I edited the code like this, but the count on the arduino still lag around 4seconds after 4 minutes. It lags 13 seconds after 10 minutes ie, it counts only 587 seconds after actual 600 seconds.

Edit 2: Here is my updated code. Still there is lag in the timing. I get a lag of around 6 seconds in 5 minutes.

#include "Arduino.h"
#include <avr/io.h>
#include <avr/interrupt.h>


void toggle_led(void);

unsigned long volatile millis_count = 0;
volatile char state = 1;
unsigned long volatile current_count = 0;
unsigned int volatile count = 0;

ISR(TIMER0_COMPA_vect) {        //Timer interrupt ISR

    millis_count++;
    if (millis_count - current_count == 1000) {
        current_count = millis_count;
        toggle_led();
        Serial.println(count++);
    }

}


int main(void) {

    init();

    TCCR0B = 0b11;  //Timer settings for interrupt at every millisecond
    OCR0A = 249;
    TIMSK0 |= 0b10;

    sei();
    Serial.begin(9600);
    DDRB |= 1<<5;

    for (;;) {



    }
}

void toggle_led(void) {

    PORTB ^= (1<<5);
}
\$\endgroup\$
4
  • 2
    \$\begingroup\$ Am no AVR (or embedded programming) expert, but was wondering if you couldn't make your toggle_led() more efficient by avoiding state, and just do: PORTB ^= 1<<5; Also, I was reading recently in an Atmel document that it is best to avoid calling functions from within an ISR, so might be bettwe to make toggle_led() a macro instead. After-all, it is very small, called at very few places, so the perfect recipe. \$\endgroup\$
    – bdutta74
    Commented Jan 8, 2012 at 17:15
  • \$\begingroup\$ Oh one more thing. If you have a logic analyzer, in some cases you can completely do away with Serial console usage for program debugging, i.e. using pin toggling. Serial library is rather heavy both in terms of runtime performance, as well as impact on code size (~550B). This is especially important when you are doing it within ISR. With a slow sampling rate and RLE compression of samples, you could use the pin-toggling (for debug), quite effectively, with very little performance cost (unlike Serial library). \$\endgroup\$
    – bdutta74
    Commented Jan 8, 2012 at 18:28
  • 2
    \$\begingroup\$ You should have as little code as possible inside an interrupt service routine. Especially not heavy routines like Serial.println(). \$\endgroup\$
    – Majenko
    Commented Jan 8, 2012 at 18:39
  • \$\begingroup\$ The bug was I didnt enable CTC mode in the Timer settings. So the internal timer counter always overflowed to 255. Adding "TCCR0A = 0b10;" to the main() solved the problem. Thanks to all for the support. \$\endgroup\$
    – 0xakhil
    Commented Jan 8, 2012 at 18:55

4 Answers 4

7
\$\begingroup\$

Your delay will be the length of the newdelay() function plus the time it takes to send the serial data.

You have:

  1. Send the count through serial
  2. wait 1000ms
  3. toggle LED.

Each of those steps takes time.

To get an exact 1000ms time you can either trigger the serial sending from a 1 second interrupt, or you can examine the millisecond count within your loop and send the serial data when the milliseconds loop through 1000:

unsigned long lastmil;

lastmil == newmillis();
for(;;)
{
    if(((newmillis()%1000) == 0) && (lastmil != newmillis()))
    {
        lastmil = newmillis();
        Serial.println(count++);
        toggle_led();
    }
}

(another way to stop it looping multiple times for a single millisecond would be to add a short delay inside the if(..) to ensure that the routine takes at least 1ms)

The Serial.println() function will take a varying amount of time depending on:

  1. The baud rate in use
  2. The number of characters sent

At 9600 baud you are sending 9600 symbols per second. With "8N1" format that's 10 symbols per byte. So for a string of 3 digits, plus the carriage return and line feed, that will be 10 symbols × 5 characters, which is 50 symbols.

9600 baud is 0.0001041670.000104167s per symbol (or 104µS per symbol), so 50 symbols will be 0.00520835s (or 5.20835ms).

That is the actual transmission time. You then also have to add on to that the time taken to actually do the formatting of the data and the calling of the serial routines. These all take an unspecified amount of time. To find this out you will need to know the assembly code the routine compiles into then find the number of clock cycles each instruction takes and total them up.

\$\endgroup\$
6
  • \$\begingroup\$ Can you tell me how much time does that Serial.print() consumes? \$\endgroup\$
    – 0xakhil
    Commented Jan 8, 2012 at 12:30
  • \$\begingroup\$ See my edit for detail. \$\endgroup\$
    – Majenko
    Commented Jan 8, 2012 at 12:36
  • 1
    \$\begingroup\$ 8n1 takes ten bit-times, not nine: one start bit, 8 data, and one stop bit. \$\endgroup\$
    – JustJeff
    Commented Jan 8, 2012 at 14:57
  • \$\begingroup\$ Of course it does - was forgetting the stop bit \$\endgroup\$
    – Majenko
    Commented Jan 8, 2012 at 14:59
  • \$\begingroup\$ @majenko the code you provided wont work because the if condition will be true for several 'for' loop cycles within one second. \$\endgroup\$
    – 0xakhil
    Commented Jan 8, 2012 at 15:16
6
\$\begingroup\$

Majenko has explained in detail why you are seeing time drift. The question now should be how to do it right. You have already encountered one of the several reasons busy waits are a poor way to do long term timing.

I don't know the AVR hardware the arduino is based on, but I'm sure it has timers. There must be a way to set up a periodic interrupt completely in hardware using one of these timers. A 1 kHz (1ms period) interrupt is often a good tradeoff between timing resolution, not using too much of the CPU, and a small enough value the hardware can do natively. This interrupt routine will be called 1000 times per second regularly by the hardware regardless of whatever else your forground routine is doing. It can easily count multiple 1 ms ticks to make ticks with longer periods, like 1 second. Since 1 second is quite slow, the interrupt routine can set a flag every second that the foregrond routine checks in its main event loop, then resets.

Depending on what else is going on, you could possibly not use a interrupt routine but have the hardware just set a flag every clock tick. In that case you probably want to make the clock tick longer, like every 10 ms. The foreground code handles that in the main event loop and divides it to the 1 s tick you actually want.

\$\endgroup\$
3
\$\begingroup\$

Following up on Olin's answer... that's actually how the Arduino millis() function is implemented... There is a volatile 32-bit global counter variable called timer0_millis that is maintained/updated by the TIMER0_OVF_vect ISR, and the millis function itself just stops interrupts briefly to read that value into a local variable and return it. You can review the implementation in the "wiring.c" file in the Arduino core library to see how they actually did it.

\$\endgroup\$
1
\$\begingroup\$

Besides the mentioned answers, there is another possibility of time drift. The crystals used to clock the arduino have a pretty wide tolerance range too, so that the crystal can itself induce a drift. If you plan to use this for a long period of time and are required to be precise in your timing measurements, I would look for an external clock chip, for example the DS1307, which is easy to connect to the arduino. These chips usually use a "high-precision" 32khz crystal to keep time (the same crystal used in common wall clocks).

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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