2

I'm making a stopwatch using an attiny85, the idea was to use the timer interrupts to count the seconds my theory was: since I set the Attiny85 to run at 1Mhz, I can use a presale of 1024; 1000000 / 1024 = 976.5625 hz, 976.5625hz = 1.024ms, 125ms / 1.024ms = 122.07, so that I need to count to 122 to get a 125ms interrupt, then in the ISR function I can count to 8 to get around 1 second, ( 122 * 1.024 ) * 8 = 999.424ms

What is the problem? well, the problem is that I'm not getting something close to 999.424ms, I compere it with my phone's stopwatch and the attiny seems to start well, but then it gets about 2-3 slower than my phone's stopwatch

#include <TM1637Display.h>

#define CLK 4
#define DIO 3
#define button 1

TM1637Display display (CLK, DIO);
int intr_count = 0;
byte dots = 0b01000000;

volatile int seg;
volatile int min;
volatile bool start = false;

void setup() {
  display.setBrightness(5);
  display.clear();
  pinMode(button, INPUT);

  cli();

  TCCR0A  = 0;
  TCCR0B  = 0;
  TCCR0B |= B00000101;
  TIMSK  |= B00010000;

  OCR0A   = 122;

  sei();
}

void start_counting(){
  if (digitalRead(button) == HIGH){
    start = !start;
    if(start == false){
//    display.clear();
      seg = 0;
      min = 0;
    }
    while(digitalRead(button) == HIGH);
  }
}

void loop() {
  start_counting();
  display.showNumberDecEx(min, dots, true, 2, 0);
  display.showNumberDec(seg, true, 2, 2);
  
}

ISR(TIMER0_COMPA_vect) {
  TCNT0 = 0;
  if(start == true){
    
    if (intr_count == 8) {
      
      seg++;
      
      if(seg == 60){
        min++;
        min = min % 60;
      }

      seg = seg % 60;

      intr_count = 0;
    } else {
      intr_count++;
    }
  }
}
9
  • Are you running this from the internal oscillator? "2-3 slower" 2 to 3 what slower? Milliseconds? And this much slower over the course of what amount of real time?
    – timemage
    Oct 24, 2022 at 0:37
  • 1
    @timemage yes I'm running the attiny from the internal oscillator . "2 - 3 " seconds slower, time drift I should say , for example starting both stopwatches at the same time when my phone is 00:10 the attiny stopwatch is 00:08, when my phone is 1:00 the attiny stopwatch is 00:57, on the first seconds the stopwatches seem to be synchronized, but after couple of seconds my phone seems to "count the seconds" much faster Oct 24, 2022 at 2:02
  • display.showNumberDec(seg, true, 2, 2); This, for example, is a non-atomic read operation on an int16_t. It could have been half updated by the ISR at the time of the call. Best is to suspend interrupts while making a copy of seg and display that.
    – 6v6gt
    Oct 24, 2022 at 2:14
  • " ( 122 * 1.024 ) * 8 = 999.424ms" . I guess then OCR0A should be 121 since it starts from 0.
    – 6v6gt
    Oct 24, 2022 at 2:22
  • @6v6gt Yeah. I noticed that also, but it's likely under the accuracy of the RC osc itself. It also makes sense to use a mode of the counter that automatically reset TCNT rather than doing that in software, but I don't know how important that will be ultimately either. I probably won't be looking at seriously again for another 10+ hours.
    – timemage
    Oct 24, 2022 at 4:36

1 Answer 1

4

I see three issues with this approach.

The first is that you are using a very low quality, uncalibrated time source. The frequency of the internal RC oscillator is good to within a few percent only. It is also very unstable, and hugely dependent on temperature. Using an external 16 Mhz ceramic oscillator should give you a frequency that is no worse than 0.5% off, with a typical error about 0.1%. It is still somewhat unstable though. If you instead are using a crystal oscillator, then you can expect an error about ten times smaller (in the range of 100 ppm), and a very good stability. Depending on you quality requirements, 100 ppm may still not be good enough, in which case you still have the option to measure the drift rate and calibrate it out (see below).

The second problem, which has already been raised by 6v6g, is that your timer is counting from zero to 122 inclusive, which gives a period of 123 timer clock cycles. Take a look at the timing diagrams in the datasheet if you need to convince yourself of this. If you want a period of 122, you should set OCR0A to 121.

The third problem is that you are resetting the timer with the ISR. This may work in this program, but it is a very fragile approach worth avoiding. The problem is that everything the microcontroller does takes time, and your ISR may even get delayed by another interrupt. If the timer is incremented after rising the interrupt flag but before you reset it, you are missing one count. If you want to have a continuous time scale, never reset your timer. You can either:

  • let it run continuously, undisturbed (and do OCR0A += 122 within the ISR),

  • or let it reset itself by using the appropriate waveform generation mode (CTC or fast PWM).

Lastly, here is a trick you may use to calibrate your stop watch. Instead of counting the interrupts until the counter reaches 8 (which assumes an interrupt period of exactly 1/8 s), increment a nanosecond counter within the ISR, and count one second once you have one billion nanoseconds:

const uint32_t nanoseconds_per_interrupt = 124928000;

ISR(TIMER0_COMPA_vect) {
    static uint32_t nanoseconds;
    nanoseconds += nanoseconds_per_interrupt;
    if (nanoseconds >= 1000000) {
        nanoseconds -= 1000000;
        seconds++;
        // then update minutes, hours... if needed
    }
}

This will increment the seconds every eight interrupts... most of the time. From time to time, however, it will take nine interrupts. This adds a little bit of jitter, but prevents the timing errors from accumulating and making the clock drift. If the jitter is visible, you can reduce it by shortening the interrupt period.

The value 124928000 I wrote above assumes the ISR is called every 122×1024 cycles of a 1 MHz clock. You can change it to fit a different clock frequency or a different OCR0A setting. The trick is: once you measure the drift rate of your stop watch, you can tweak this number to remove that drift. For example, if you measure the stopwatch and find it is running 0.04% too slow, then you increase that number by 0.04% (that would give 124977971) and the drift is gone.

Edit: In a comment, 6v6gt pointed out a fourth issue in your code, which is likely the biggest issue. Your logic for counting interrupts is:

if (intr_count == 8) {
    intr_count = 0;
} else {
    intr_count++;
}

This is counting from 0 to 8 inclusive, and looping with a period of 9 interrupts. A safer idiom for looping with a period of 8 would be:

if (++intr_count >= 8) {
    intr_count = 0;
}

Although I would still recommend counting nanoseconds if you want the ability to calibrate your clock.

3
  • ( 122 * 1.024 ) * 8 = 999.424ms Maybe the following from the OP is also wrong for the same reason the OCR0A was wrong: if (intr_count == 8) { . . It is call 7 (calls running from 0) of the ISR that indicates the end of a full second, not call 8
    – 6v6gt
    Oct 24, 2022 at 10:01
  • @6v6gt: Indeed you are right, and this looks like it should introduce a huge error. Added that to the answer. Oct 24, 2022 at 11:06
  • Thank you, Edgar, for an extremely comprehensive answer. :)
    – Nick Gammon
    Oct 25, 2022 at 7:00

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.