============================================
Reciprocal frequency counter with LCD output
============================================
(c) 2010 Matthias Hopf
License: GPL v2
This describes a software only reciprocal frequency counter for the ATtiny2313
(ATmega not tested yet) with a usable frequency range of 0Hz..10MHz (when
running at 20MHz), sub-Hz resolution, and 2.5ppm relative accuracy or better.
As the ATtiny samples its input before detecting edges, the input signal has to
remain stable for at least 50ns on each level. If the low/high ratio is exactly
50:50, this is fulfilled at 10MHz. If the ratio is worse, 10MHz is not
achieved.
The input signal is (by default) sampled twice a second - except for slow
signals, as at least a single full cycle has to be sampled. Thus the maximum
time between two frequency samples is
MAX (0.5s, 2.0 / f)
The determined frequency is printed on an at least 10 characters wide display
(8 chars possible with limitations, 2x8 required for debugging).
Calculations are done with dedicated 64bit fix-point math routines.
Operating Principle
-------------------
The software counts the number of system clocks and falling edges of the input
signal for the sampling period. It makes sure that counting is started at a
falling edge, and stopped after the sampling period at a falling edge as well.
Due to this the sampling period may be extended.
After counting has stopped, two numbers are available: n_CLK (number of system
clocks) and n_EVT (number of input events). The frequency results then as
f = n_EVT * f_CPU / n_CLK
In order to get sub-Hz resolution, this has to be calculated in fixpoint
arithmetics. This easily exceeds the range of 32bit arithmetics, thus 64-by-32
multiplication and division routines even working on ATtinys are included.
gcc's implementation is currently way too fat for 2k flash devices.
Hardware layout
---------------
The LCD routines require D4..D7 to be ordered continuously, and all signals
routed through a single port. The input sampling routine requires the input
signal to be able to trigger an external high-priority interrupt (INT0) and be
the source for counter 0 (T0), thus these two inputs have to be connected.
No other requirements. A quartz crystal is highly suggested for accuracy.
Implementation details
----------------------
Timers (counters) are started and stopped in INT0, triggered on falling edges
of the input signal. The interrupt is active only once when measurement is to
be started and deactivates itself after starting the timers.
The hardware timers are extended by overflow interrupt routines to 32bit
counters (well, actually 40bit in timer0's case, but the top 8bit are unused).
Timer0 (8bit) counts input events (falling edges), timer1 (16bit) counts system
clocks. If timer1 overflows and at least s_rate seconds have passed, the INT0
handler is activated again in order to shut down the counters at the next
falling edge.
The main routine waits for the counters to be stopped, calculates the frequency
and prints it on the LCD. During measurement the cursor blinks, on (1,2) while
waiting for the first falling edge, on (1,3) while counting during the regular
sample period, and on (1,4) while counting, waiting for the final falling edge.
lcd_hd44780u.h contains short LCD handling routines. Timings need verification,
though.
uint64_ops.h contains shift, multiply, and division routines that require much
less space than current libgcc's 64bit routines. Even the C implementation is
good enough, the asm is even better. Requires additional validation, though.
Multiply and divide work on one 64bit and one 32bit number each.
Accuracy
--------
Due to the software implementation a number of factors are involved in the
accuracy. I will lay out the worst case scenarios here.
The calculations are not trivial and the scenario is difficult, thus no
guarantees about their correctness are made.
- Crystal frequency accuracy Typical 30ppm (max. 100ppm)
This affects the absolute measured frequency, but not relative effects.
Ignoring for now.
- Crystal temperature stability
As long as the temperature of the crystal stays constant,
this is typically below 1ppm. => 1ppm
- Principal clock counting off-by-one possibility
Falling edges trigger starting and stopping of counters. They may not fall
together with system clock edges.
Result: n_CLK off by 1; n_CLK >= s_rate * f_cpu.
At 0.5s and 20MHz this makes => 0.1ppm
- Overflow interrupt handler running while falling edge is triggered
See appendix for cycle counts. Mostly discussing C here, because is shows
more possible issues.
Timers cannot be activated in the same cycle, and not by a hardware event
(falling input edge) as in hardware solutions. Also, there is a delay between
falling event edge and started/stopped timers. Interestingly, all these
effects cancel each other out, if the timers are started with the same delay
as they are stopped - which is the case due to the interrupt handler layout.
The only exception is that an timer overflow event may happen just before a
falling edge would trigger INT0. By that the actually measured phase will be
off.
For low frequencies this can even be timer1. As timer1 arms the INT0
interrupt, this can only happen for frequencies that don't have a falling
edge for at least 65536 cycles. That is for f < 305.18Hz. If the timer1
interrupt is finished when the falling edge triggers INT0, no error occurs,
that would be after 65598 cycles (f > 304.89Hz). Then n_CLK can be off by a
maximum of 63. Similar issues happen at factors of these (small) frequency
bands.
Thus relative error at s_rate = 0.5s, 304.89-305.18Hz / n => 6.3ppm
(Assembler: 14 cycles max => 305.11-305.18Hz / n => 1.4ppm)
For higher frequencies, only timer0 interrupt may interfere. This can only
happen if exact multiples of 256 events occur before s_rate is reached, that
is for f = n * 512Hz. So these two worst case possibilities (timer0 and
timer1) clearly don't overlap.
As INT0 has a higher priority than timer0, the events have to be different,
as INT0 is only armed by timer1.
So if more than 69 cycles are between events, this cannot happen.
Thus f has to be > 290kHz @20MHz CPU.
(Assembler: 14 cycles max => f > 1.43MHz)
Highest absolute error occurs, if timer1 is triggered, then timer0 before the
timer1 routine arms INT0, and as soon as timer0 is actually running INT0 is
triggered. Then INT0 handling will be delayed by up to 69-16 = 53 cycles
(Assembler: 14 - 4 = 10 cycles).
Requirements:
- EVT trigger before arming INT0, 53 cycles in timer1. No possibility to
be extended by timer0 (to be triggered). That requires f > 377kHz.
- EVT trigger before arming INT0, next after timer1 is finished running, now
running timer0. That requires f<1.25MHz.
It can only happen for very specific frequencies between 377kHz and 1.25MHz,
for most frequencies one falling edge will trigger INT0 before timer0 can
even run.
Interestingly, relative error would be lowest if arming INT0 would take
longer (32 cycles)...
Thus relative error at s_rate = 0.5s, 377kHz-1.25Mhz => 5.3ppm
(Assembler: conditions have to be checked => 1.0ppm)
The frequency ranges of the errors don't overlap, thus taking the maximum.
- Calculation and Rounding
- const uint64_t fact = F_CPU * pow (10, MIN_RES) + .5;
Constant integer value; .5 only to fix potential pow errors.
- frq += nom >> 1;
Already included in "Principal clock counting off-by-one possibility"
- frq = uint64_div32 (frq, nom);
Max. error: 1 digit => 1 digit
- frq += 5;
Max. error: 1 digit; stays same as previous 1 digit error is nuked by
frq = uint64_div32 (frq, 10);
=> Accuracy: Max. relative deviation 7.4ppm + 1 digit
Examples: Display reads Actual range
.00100Hz .00099-.00101
1.00000Hz .99998-1.00002
1000.000Hz 999.991-1000.009
1000000Hz 999991-1000009
Now using assembler interrupt handlers that dont't keep interrupts disabled, it
boils down to a max. relative deviation of 2.5ppm + 1 digit. Even then, the
conditions for the maximum deviation to actually occur are rare at worst.
Examples: Display reads Actual range
.00100Hz .00099-.00101
1000000Hz 999996-1000004
The max. absolute deviation is almost solely related to quality of the crystal.
Expect 100ppm with cheap quartz crystals, that is 0.01% or 1/10000.
Appendix A: Counting cycles for all interrupt handlers in C
-----------------------------------------------------------
INT0 Cycles:
cmd before INT0 1-4 (mov/rts) (last command has to be finished)
INT0 4
rjmp handler 2
push __zero_reg__ 2 ISR(INT0_vect)
push r0 2
in r0,__SREG__ 1
push r0 2
clr __zero_reg__ 1
push r24 2
push r25 2
in r24,83-32 1 TCCR0B ^= 0x06 << CS00;
ldi r25,lo8(6) 1
eor r24,r25 1
out 83-32,r24 1 (timer0 activation)
in r24,78-32 1 TCCR1B ^= 0x01 << CS10
ldi r25,lo8(1) 1
eor r24,r25 1
out 78-32,r24 1 (timer1 activation)
(rest irrelevant)
Total: 20-23 to timer0, 27-30 to timer1
TIMER0_OVF cycles:
cmd before INT0 1-4 (mov/rts) (last command has to be finished)
TIMER1_OVF 4
rjmp handler 2
push __zero_reg__ 2 ISR(TIMER0_OVF_vect) {
push r0 2
in r0,__SREG__ 1
push r0 2
clr __zero_reg__ 1
push r24 2
push r25 2
push r26 2
push r27 2
lds r24,cnt_t0 2 cnt_t0++;
lds r25,(cnt_t0)+1 2
lds r26,(cnt_t0)+2 2
lds r27,(cnt_t0)+3 2
adiw r24,1 2
adc r26,__zero_reg__ 1
adc r27,__zero_reg__ 1
sts cnt_t0,r24 2
sts (cnt_t0)+1,r25 2
sts (cnt_t0)+2,r26 2
sts (cnt_t0)+3,r27 2
pop r27 2 }
pop r26 2
pop r25 2
pop r24 2
pop r0 2
out __SREG__,r0 1
pop r0 2
pop __zero_reg__ 2
reti 4
next cmd 1-4 (mov/rts) (next command issued before INT0 is served)
Total cycles: 63-69
TIMER1_OVF cycles:
cmd before INT0 1-4 (mov/rts) (last command has to be finished)
TIMER1_OVF 4
rjmp handler 2
push __zero_reg__ 2 ISR(TIMER1_OVF_vect)
push r0 2
in r0,__SREG__ 1
push r0 2
clr __zero_reg__ 1
push r24 2
push r25 2
lds r24,cnt_t1 2 cnt_t1++;
lds r25,(cnt_t1)+1 2
adiw r24,1 2
sts (cnt_t1)+1,r25 2
sts cnt_t1,r24 2
lds r24,cnt_t1 2 if (cnt_t1 == (uint16_t) (MIN_SAMPLE_TIME * F_CPU / 65536.0)) {
lds r25,(cnt_t1)+1 2
cpi r24,152 1
cpc r25,__zero_reg__ 1
brne .L27 2 (not true for general validation)
ldi r24,lo8(64) 2 GIMSK = 0x40;
out 91-32,r24 1
out 90-32,r24 1 EIFR = 0x40;
pop r25 2 } }
pop r24 2
pop r0 2
out __SREG__,r0 1
pop r0 2
pop __zero_reg__ 2
reti 4
next cmd 1-4 (mov/rts) (next command issued before INT0 is served)
Total cycles: 57-63 , INT0 enablement->end: 16-19
Appendix B: Counting cycles for all interrupt handlers in assembler
-------------------------------------------------------------------
INT0 Cycles: see C
TIMER0_OVF cycles:
cmd before INT0 1-4 (mov/rts) (last command has to be finished)
TIMER1_OVF 4
rjmp handler 2
sei 1
push r24 2 (command after interrupts enabled)
cli: 10-14
TIMER1_OVF cycles:
cmd before INT0 1-4 (mov/rts) (last command has to be finished)
TIMER1_OVF 4
rjmp handler 2
sei 1
push r24 2 (command after interrupts enabled)
(cli 1)
(ldi r24, 64 1)
(out %1, r24 1)
out %2, r24 1
sei 1
pop r25 2 (command after interrupts enabled)
cli cycles: 10-14 , INT0 enablement->sei: 4