Pages: 1 [2] 3 4 ... 8   Go Down
Author Topic: realtime clock, microseconds, etc.  (Read 9777 times)
0 Members and 2 Guests are viewing this topic.
0
Offline Offline
God Member
*****
Karma: 1
Posts: 513
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

It looks like Don Kinzer is off to a good start with it (in hptics):
http://www.arduino.cc/cgi-bin/yabb2/YaBB.pl?num=1226257074/1#1

though with the divisions it is a bit more computing than I'm proposing, which at it's core could be expressed as:
  ((m << smiley-cool + t) <<2;

I wanted to keep it light so you can get a new microseconds timestamp from an interrupt without much concern, so it overflows every 128 seconds internally instead of computing the seconds portion as well, but just adding timer0_seconds * 1000000 will get you there.  

But I figure being able to track any number of signals at low microsecond accuracy with a period of up to just over 2 minutes is pretty useful.

Like so much of microprocessors, always a compromise smiley
« Last Edit: November 10, 2008, 09:24:14 am by dcb » Logged

Austin, TX USA
Offline Offline
God Member
*****
Karma: 5
Posts: 997
Arduino rocks
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

I agree with dcb that there seems to be a great deal of interest in built-in microsecond and "macrosecond" timing.  Thanks for the thought you've put into this!  

I'm a little uncomfortable with the having values that overflow on non-word boundaries (like the 128 seconds proposed above).  It makes it difficult to compute time deltas.  Remember the trouble we had because 0011's millis() didn't overflow on the word boundary?  I think it's important to be able to write delta expressions like:

   while (micros() - start > 20) // wait 20 microseconds

without worrying about how the overflow will affect the calculation.

David, I like the way the current wiring.c implementation handles SIG_OVERFLOW0.  I don't see that we need to change it to build a micros() function that overflows at the 32-bit boundary.  This should work at both 16 and 8 MHz without changing wiring.c at all:

   unsigned long micros()
    {
      unsigned long u;
      uint8_t oldSREG = SREG;
      cli();
      u = 1000 * timer0_millis + (timer0_clock_cycles + 64UL * TCNT0) / clockCyclesPerMicrosecond();
      SREG = oldSREG;
      return u;
  }

(Don't be alarmed by all the multiplies and divides.  All but one will/can be optimized as bit shifts.  I agree with dcb's strategy of keeping things as streamlined as possible.)

Mikal
« Last Edit: November 10, 2008, 02:37:12 pm by mikalhart » Logged

Austin, TX USA
Offline Offline
God Member
*****
Karma: 5
Posts: 997
Arduino rocks
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

I would also like to throw my support behind adding a "millis_overflow" counter to the SIG_OVERFLOW handler.  This would be minimally invasive:

volatile unsigned long timer0_clock_cycles = 0;
volatile unsigned long timer0_millis = 0;
volatile unsigned long timer0_millis_overflow = 0;

SIGNAL(SIG_OVERFLOW0)
{
  // timer 0 prescale factor is 64 and the timer overflows at 256
  timer0_clock_cycles += 64UL * 256UL;
  while (timer0_clock_cycles > clockCyclesPerMicrosecond() * 1000UL) {
    timer0_clock_cycles -= clockCyclesPerMicrosecond() * 1000UL;
    timer0_millis++;
   if (timer0_millis == 0)
      timer0_millis_overflow++;

  }
}

but would solve many problems with "macrosecond" timekeeping.  With this technology, anyone can track total micro/milliseconds elapsed without having to periodically call some kind of library refresh function every 49 days or 4 years or whatever.  Overflow measured in centuries.

Mikal
« Last Edit: November 10, 2008, 02:33:54 pm by mikalhart » Logged

Austin, TX USA
Offline Offline
God Member
*****
Karma: 5
Posts: 997
Arduino rocks
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

David, to my thinking, date/time functions like hour(), day(), etc. don't belong in the wiring.c "kernel".  Tracking "clock-based" time seems to be a different problem than tracking "elapsed" time.  While nearly every Arduino project depends on the latter -- millis() and delay(), etc. -- the need for "clock" time seems more limited.  In my opinion, this should be placed in a library -- like mem's DateTime! smiley

Mikal
Logged

Portland, OR, USA
Offline Offline
Jr. Member
**
Karma: 0
Posts: 78
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

The code dcb suggested for a microseconds function has the same problem that I also had in my first proposed code for hpticks().  The problem is that the if the timer overflows between the time the cli instruction is executed and when the TCNT0 register is read, the result will be off by 256 timer ticks.

This problem can be resolved by checking for the timer overflow flag being set and the value of TCNT0 being 0, indicating that the timer just rolled over.  The difficulty with implementing is is that the name of the register containing TOV0 is different depending on which AVR is being used.  A suggested solution, which addresses the register name difference, is shown below.
Code:
// define the timer interrupt flag register if necessary
#if !defined(TIFR0)
  #if defined(TIFR)
    #define TIFR0 TIFR
  #else
    #error AVR device not supported
  #endif
#endif

unsigned long microSeconds()
{
  unsigned long m, t;
  uint8_t oldSREG = SREG;
  cli();
  t = TCNT0;
  if ((TIFR0 & _BV(TOV0)) && (t == 0))
    t = 256;
  m = timer0_count;
  SREG = oldSREG;
  return ((m << 8) + t) * 4;
}

The CPU speed dependency can be removed by replacing the return value computation with that shown below.  This has the limitation, however, of working correctly only when F_CPU is an integral multiple of 10^6.
Code:
 return ((m << 8) + t) * (64 / (F_CPU / 1000000L));
« Last Edit: November 10, 2008, 02:47:30 pm by dkinzer » Logged

Don

ZBasic Microcontrollers
http://www.zbasic.net

Austin, TX USA
Offline Offline
God Member
*****
Karma: 5
Posts: 997
Arduino rocks
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

Thanks for the interesting analysis, Don.

Quote
The CPU speed dependency can be removed by replacing the return value computation with that shown below.  This has the limitation, however, of working correctly only when F_CPU is an integral multiple of 10^6.

Code:
return ((m << 8) + t) * (64 / (F_CPU / 1000000L));

Doesn't it also rely on F_CPU being divisible into 64 million?  I bring this up only because there has been some discussion of supporting processors at 20MHz and it seems like this might cause a problem if F_CPU were 20000000.

Mikal
« Last Edit: November 10, 2008, 03:03:23 pm by mikalhart » Logged

0
Offline Offline
God Member
*****
Karma: 1
Posts: 513
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

"The code dcb suggested for a microseconds function has the same problem that I also had "

Absolutely right, I have since been paying much closer attention to your TCNT0 handling Don smiley
« Last Edit: November 10, 2008, 03:26:46 pm by dcb » Logged

Portland, OR, USA
Offline Offline
Jr. Member
**
Karma: 0
Posts: 78
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

Quote
Doesn't it also rely on F_CPU being divisible into 64 million?  I bring this up only because there has been some discussion of supporting processors at 20MHz and it seems like this might cause a problem if F_CPU were 20000000.
Quite right.  I overlooked that aspect of it.  In my implementation of a higher precision timing function (see the link in my previous post), the function returns Timer0 ticks.  This mitigates the 20MHz problem or, at least, defers it until conversion to microseconds is later done.

The Arduino-like device that I'm testing runs at 20MHz and my hpticks() function works correctly on it (the second attempt, at least).  One issue to consider is that the suggested implementation of a microseconds function has a resolution of F_CPU / 64 since it is based on Timer0 ticks and Timer0 is clocked at 1/64th of the CPU frequency.  Although a microseconds() function may be more aesthetically pleasing, the implementation essentially "wastes" a portion of the 32-bit value range.  A function that returns Timer0 ticks will have the same resolution as a microseconds() function but will have a larger useful range.  Moreover, the range will be constant irrespective of the CPU speed.

For measuring elapsed time, you can still think in terms of microseconds but convert the desired number of microseconds to Timer0 ticks (with either rounding or truncation as needed) before comparing it to the difference between two readings.
Logged

Don

ZBasic Microcontrollers
http://www.zbasic.net

Forum Administrator
Cambridge, MA
Offline Offline
Faraday Member
*****
Karma: 12
Posts: 3538
View Profile
WWW
 Bigger Bigger  Smaller Smaller  Reset Reset

Hey guys, very nice work so far!

I would like to include a micros() function in the core if you think it's accurate / precise enough to be useful (particularly at 16 and 8 MHz).  Can anyone run through an analysis of the resolution you could get from Don's latest function?  

I think a ticks() function is too confusing to include in the core, although it would be a great thing to post on the playground: http://www.arduino.cc/playground/Main/GeneralCodeLibrary.  

The second(), minute(), hour(), etc. functions are not as high of a priority.

I don't think we need to worry about programs that run for more than a week or two.  Beyond that, it's okay if you have to do some extra work to make things reliable (e.g. call a function every few days).  

Does anyone want to summarize this discussion for the developers mailing list: http://mail.arduino.cc/mailman/listinfo/developers_arduino.cc?
Logged

0
Offline Offline
God Member
*****
Karma: 1
Posts: 513
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

Ok, I borrowed Dons hptics function (since it didn't require a change to wiring.c) and his latest TCNT0 handling and the tics2microseconds code (it is the micros function in the code block below)

The basic test harness is below.  It uses timer2 to keep timer0 honest, and keeps track of the total differences between calls to micros()


16mhz atmega168
t2ms   t0ms   micros    sum(micros + prev micros)
...  
267500 267500 267500560 267500560
268000 268000 268000272 268000272
268500 268500 65552     65552
269000 269000 565264    565264
...

8mhz atmega168 (no crystal, lilypad bootloader&fuses)
t2ms   t0ms   micros    sum(micros + prev micros)  
...
536000 536000 536000560 536000560
536500 536500 536500272 536500272
537001 537001 131120    131120
537501 537501 630832    630832
...


So given the 1 to 1 relation of micros and the sum of changes to micros, and their agreement with millis and timer2, I'm fairly confident that this thing works pretty well smiley    If there was noise from TCNT0 or elsewhere, I would expect it to show up when I subtract the current micros() from the last micros() and accumulate those differences, but it does not seem to be there.

Do note it rolls over at ~260 seconds at 16mhz.  Which isn't so bad considering you can measure the distance to the moon in 3 seconds with a laser or time a sonic echo from 27 miles away with that range.  

I do want to do some performance tests with the above changes and the wiring.c changes though, if you wind up polling microseconds then you need a fast microseconds.  But we definitely have some good stuff to work with here.

Code:

extern volatile unsigned long timer0_clock_cycles;
extern volatile unsigned long timer0_millis;

unsigned long micros(){
  uint16_t t0;
  unsigned long clock_cycles;
  unsigned long millis;

  uint8_t sreg = SREG;
  cli();
  
  t0 = TCNT0;
  if ((TIFR0 & _BV(TOV0)) && (t0 == 0))
    t0 = 256;

  // get a snapshot of the other timer values
  clock_cycles = timer0_clock_cycles;
  millis = timer0_millis;
  SREG = sreg;

  // compute the number of Timer0 ticks represented by the data
  unsigned long t0_ticks = (clock_cycles / 64) + (millis * (1000L * clockCyclesPerMicrosecond() / 64)) + t0;
  return ((t0_ticks) * 64L / (F_CPU / 1000000L));
}



volatile unsigned long timer2_clock_cycles = 0;
volatile unsigned long timer2_millis = 0;
ISR(TIMER2_OVF_vect){
// timer 2 prescale factor is 64 and the timer overflows at 256
  timer2_clock_cycles += 64UL * 256UL;
  while (timer2_clock_cycles > clockCyclesPerMicrosecond() * 1000UL) {
    timer2_clock_cycles -= clockCyclesPerMicrosecond() * 1000UL;
    timer2_millis++;
  }
}

unsigned long millis2(){
        unsigned long m;
      uint8_t oldSREG = SREG;
      cli();
      m = timer2_millis;
      SREG = oldSREG;
      return m;
}

void init2(){
      // on the ATmega168, timer 0 is also used for fast hardware pwm
      // (using phase-correct PWM would mean that timer 0 overflowed half as often
      // resulting in different millis() behavior on the ATmega8 and ATmega168)
#if defined(__AVR_ATmega168__)
        TCCR2A=1<<WGM20|1<<WGM21;
//      sbi(TCCR2A, WGM21);
//      sbi(TCCR2A, WGM20);
#endif  
      // set timer 0 prescale factor to 64
#if defined(__AVR_ATmega168__)
      TCCR2B=1<<CS22;
//      sbi(TCCR2B, CS21);
//      sbi(TCCR2B, CS20);
#else
      TCCR2=1<<CS21|1<<CS20
#endif
      // enable timer 0 overflow interrupt
#if defined(__AVR_ATmega168__)
      TIMSK2|=1<<TOIE2;
#else
      TIMSK|=1<<TOIE2;
#endif

}


void setup(){
  init2();
  Serial.begin(9600);
}

unsigned long nextMillis=500;
unsigned long microsSum;
unsigned long lastMicros;

void loop(){
  while(millis2()<nextMillis);
  unsigned long micros0;
  unsigned long millis0;
  unsigned long millisb;
  micros0=micros();
  millis0=millis();
  millisb=millis2();
  microsSum=microsSum + (micros0-lastMicros);
  lastMicros = micros0;
  Serial.print(millisb);
  Serial.print(" ");
  Serial.print(millis0);
  Serial.print(" ");
  Serial.print(micros0);
  Serial.print(" ");
  Serial.print(microsSum);
  Serial.println(" ");
  
  nextMillis+=500;
}

« Last Edit: November 11, 2008, 07:43:20 am by dcb » Logged

Forum Administrator
Cambridge, MA
Offline Offline
Faraday Member
*****
Karma: 12
Posts: 3538
View Profile
WWW
 Bigger Bigger  Smaller Smaller  Reset Reset

dcb: thanks for doing these calculations.  One other thing that would be useful to check is the running total of the difference between the micros delta and 500,000 (i.e. how much does the change in micros differ from the expected change).  The running total of micros deltas just says that the values are always increasing, but not necessarily that they do so by the right amount each time.  Also, I'm wondering what the minimum increment (i.e. best resolution) you can get from the micros() function is.  Is it on the order of 1 us, or more like 10?  100?

Also, I'm worried that if the values returned from micros() don't overflow on a regular data size boundary (e.g. 2^16 or 2^32), they will be difficult to work with.  What if keep a running count of the micros() inside the timer0 overflow as we do with millis now?  Or can we somehow truncate to, say, an unsigned int so that the overflow works out properly?
Logged

Austin, TX USA
Offline Offline
God Member
*****
Karma: 5
Posts: 997
Arduino rocks
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

Quote
Also, I'm wondering what the minimum increment (i.e. best resolution) you can get from the micros() function is.

Even ignoring the amount of time the micros() function call itself takes, the resolution would be capped at 4us (8us on the 8MHz processor) for the simple reason that a single tick of TCNT0 takes 64 * 1/16 =4us.

Mikal
Logged

0
Offline Offline
God Member
*****
Karma: 1
Posts: 513
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

"how much does the change in micros differ from the expected change)"

The last column was the sum of deltas, and the first column is the millis() from timer2, after 168 seconds they are still spot on to the first 6 digits.  The sum of deltas has not wandered off and is tracking at the millisecond level accurately, which implies that it is also accurate.

"what the minimum increment"
best case is 4us @ 16mhz, but need to test.


"if the values returned from micros() don't overflow on a regular data size boundary (e.g. 2^16 or 2^32), they will be difficult to work with."

Understandable concern.  I think there should be a companion elapsedMicros(ulong start, ulong end) function in any event, to detect the overflow and do the right thing so it may be ok if the return value has a more arbitrary overflow point as long as it is predictable.


"What if keep a running count of the micros() inside the timer0 overflow as we do with millis now?"

I'm struggling to see the implementation there.

But I think this problem can be handled nicely by keeping track of the number of calls to timer0ovf and not touching when we update millis.  Then Dons function a few posts earlier will work, and it is 5 times faster than the one in the test harness by my measure, and will overflow every 45 days or so.

so this is what wiring.c might look like.

Code:
volatile unsigned long timer0_tics = 0;
volatile unsigned long timer0_clock_cycles = 0;
volatile unsigned long timer0_millis = 0;

SIGNAL(TIMER0_OVF_vect)
{
      timer0_tics++;
      // timer 0 prescale factor is 64 and the timer overflows at 256
      timer0_clock_cycles += 64UL * 256UL;
      while (timer0_clock_cycles > clockCyclesPerMicrosecond() * 1000UL) {
            timer0_clock_cycles -= clockCyclesPerMicrosecond() * 1000UL;
            timer0_millis++;
      }
}

« Last Edit: November 11, 2008, 08:59:22 am by dcb » Logged

0
Offline Offline
God Member
*****
Karma: 1
Posts: 513
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

and here the updated test program that uses the timer0_tics functionality added to wiring.c.

16mhz notes:
micros() is not rolling over at 268 seconds,
it takes about 3 microseconds to call and return from this version of micros()
no drift noticed after 268 seconds.

Code:

extern volatile unsigned long timer0_clock_cycles;
extern volatile unsigned long timer0_millis;
extern volatile unsigned long timer0_tics;

unsigned long micros()
{
  unsigned long m, t;
  uint8_t oldSREG = SREG;
  cli();
  t = TCNT0;
  if ((TIFR0 & _BV(TOV0)) && (t == 0))
    t = 256;
  m = timer0_tics;
  SREG = oldSREG;
  return ((m << 8) + t) * 4;
}
 



volatile unsigned long timer2_clock_cycles = 0;
volatile unsigned long timer2_millis = 0;
ISR(TIMER2_OVF_vect){
// timer 2 prescale factor is 64 and the timer overflows at 256
  timer2_clock_cycles += 64UL * 256UL;
  while (timer2_clock_cycles > clockCyclesPerMicrosecond() * 1000UL) {
    timer2_clock_cycles -= clockCyclesPerMicrosecond() * 1000UL;
    timer2_millis++;
  }
}

unsigned long millis2(){
        unsigned long m;
      uint8_t oldSREG = SREG;
      cli();
      m = timer2_millis;
      SREG = oldSREG;
      return m;
}

void init2(){
      // on the ATmega168, timer 0 is also used for fast hardware pwm
      // (using phase-correct PWM would mean that timer 0 overflowed half as often
      // resulting in different millis() behavior on the ATmega8 and ATmega168)
#if defined(__AVR_ATmega168__)
        TCCR2A=1<<WGM20|1<<WGM21;
//      sbi(TCCR2A, WGM21);
//      sbi(TCCR2A, WGM20);
#endif  
      // set timer 0 prescale factor to 64
#if defined(__AVR_ATmega168__)
      TCCR2B=1<<CS22;
//      sbi(TCCR2B, CS21);
//      sbi(TCCR2B, CS20);
#else
      TCCR2=1<<CS21|1<<CS20
#endif
      // enable timer 0 overflow interrupt
#if defined(__AVR_ATmega168__)
      TIMSK2|=1<<TOIE2;
#else
      TIMSK|=1<<TOIE2;
#endif

}


void setup(){
  init2();
  Serial.begin(9600);
}

unsigned long nextMillis=500;
unsigned long microsSum;
unsigned long lastMicros;

void loop(){
// performanceTest();
 accuracyTest();
}

void performanceTest(){ //15564   //3036

  unsigned long micros0a;
  unsigned long micros0=micros();
  for(int x = 0; x < 1000; x++)
    micros0a=micros();
  Serial.println(micros0a-micros0,DEC);
}

void accuracyTest(){
  while(millis2()<nextMillis);
  unsigned long micros0;
  unsigned long millis0;
  unsigned long millisb;
  micros0=micros();
  millis0=millis();
  millisb=millis2();
  microsSum=microsSum + (micros0-lastMicros);
  lastMicros = micros0;
  Serial.print(millisb);
  Serial.print(" ");
  Serial.print(millis0);
  Serial.print(" ");
  Serial.print(micros0);
  Serial.print(" ");
  Serial.print(microsSum);
  Serial.println(" ");
  
  nextMillis+=500;
}


Logged

0
Offline Offline
God Member
*****
Karma: 1
Posts: 513
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

8mhz initial notes

micros function returns 1/2 the expected value
performance is 1/2 of 16mhz (~6microseconds per call)
no drift noticed after short test run.
Logged

Pages: 1 [2] 3 4 ... 8   Go Up
Jump to: