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++;
}
}
}
1 Answer 1
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 eve×ばつ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.
-
( 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 86v6gt– 6v6gt2022年10月24日 10:01:38 +00:00Commented 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.Edgar Bonet– Edgar Bonet2022年10月24日 11:06:16 +00:00Commented Oct 24, 2022 at 11:06
-
Thank you, Edgar, for an extremely comprehensive answer. :)2022年10月25日 07:00:41 +00:00Commented Oct 25, 2022 at 7:00
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.