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.
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 ofseg
and display that.