If your signal is a pure 1 kHz tone, then you should use Nick
Gammon's counting method, as it is the most straight-forward solution.
If, however, you are picking the signal through a microphone, chances
are you will also get some background noise, and this can make the
counting method unreliable.
In such a case, you will have to use some kind of digital filter to
extract the desired signal from the noise. I would recommend using
homodyne detection,
which is both simple and very robust against wide-spectrum noise: let
f = 1 kHz be the frequency you want to detect, and t
be the current time, then
- compute cos(2 π f t ) and sin(2 π f t ): these two periodic signals,
in quadrature to one another, are called “the local oscillator”
- multiply your signal independently by the above cos() and sin()
- low-pass filter the two products, which gives you the
I and Q demodulated signals
- compute the instantaneous power = I2 + Q2.
You know the phone is ringing when this power goes above some
experimentally-determined threshold.
I know all this sounds like heavy math, but it need not be:
- All the computations are amenable to efficient fixed-point
implementations, and you can do the math in parallel with the
analog-to-digital conversion.
- The scheme works even with crude approximations of the sin() and cos()
functions. Even square waves (sin() and cos() being always +1 or −1)
work quite decently.
- The low-pass filter can be implemented as an
exponential moving
average,
which is only a bit shift, an add and a subtract.
I can try to write some sample code, but I would go through the trouble
only if there is interest. If your signal is so clean that Nick Gammon's
method works reliably, there probably is no point.
EDIT: I wrote a program to demonstrate this approach, just for the
fun. It's available as a GitHub gist:
homodyne.ino.
I am not copying the whole program here, just adding some explanations:
The first thing is to take care of the sampling frequency. Whenever one
wants to do signal processing, it is very desirable to sample at a fixed
and known frequency. This is next to impossible to achieve by looping
over analogRead()
, because the CPU time needed to go through the loop
is hard to predict, as it can be influenced by interrupts and by
conditional branches. analogRead()
is also highly inefficient, as it
locks the CPU in a busy loop while it waits for the analog-to-digital
converter (ADC) to do its job.
The solution is to put the ADC in the so called "free running mode". In
this mode, it starts a conversion as soon as the previous one is
finished, which gives a very steady sampling rate of 9.615 kHz (one
sample every 104 µs). The samples are handled by an interrupt
service routine called ISR(ADC_vect)
which gets triggered every time a
sample is ready. This is also where all the signal processing is done.
Below is a detailed explanation of every line of code in the ISR:
int8_t sample = ADCH - 128;
Reading the ADC result is the first thing to do in the ISR. Since we do
not need the full 10 bit resolution, the ADC was configured to
left-adjust its result. Then we get an 8-bit result by just reading the
high byte of the ADC data register. We subtract 128 to remove the DC
offset and get a signed number in the range [−128 .. +127].
Next, we update the phase of our local oscillator:
static uint16_t phase;
phase += PHASE_INC;
We need not worry about possible overflows: since the phase is kept in
units of 1/216 cycles, the increment automatically works
modulo one cycle. The phase increment should theoretically be 6815.744,
which is rounded to 6816. This makes the local oscillator fast by
0.0376 Hz, which is negligible given the accuracy of the Arduino
clock. The phase could be kept in an 8-bit variable, but the rounding
error would then make the oscillator fast by 14 Hz, which can be
borderline unacceptable.
Next we need to build two square waves in quadrature, which will be used
as rough approximations of sin() and cos(). But since the most
significant bit of the phase is on during half of the cycle, it can be
used as one of our square waves. The other wave is obtained in a similar
way, after adding 1/4 cycle to the phase. So here is how we multiply our
sample by the square waves:
int8_t x = sample;
if (((phase>>8) + 0x00) & 0x80) x = -1 - x;
int8_t y = sample;
if (((phase>>8) + 0x40) & 0x80) y = -1 - y;
The expression phase>>8
is only meant to hint the compiler that it
needs only to consider the most significant byte of the phase. This
should normally not be needed, but I noticed that, without this hint,
gcc generates suboptimal code. The -1
in the above lines are for
avoiding a possible overflow.
Next comes the low-pass filter:
signal_I += x - (signal_I >> LOG_TAU);
signal_Q += y - (signal_Q >> LOG_TAU);
Where signal_I
and signal_Q
are volatile int16_t
globals, and
LOG_TAU
is 6. This filters with a time constant
τ = 64 × 104 µs = 6.656 ms
which translates to a −3 dB bandwidth of
Δf = 1/(2πτ) = 23.9 Hz.
which is fairly narrow for a 1 kHz signal. The significance of τ is
that, once the tone starts, it takes the filter about
2.3 τ ≈ 15 ms to reach 90% of its final output. Both
τ and Δf can be changed by simply changing LOG_TAU
.
This ISR takes about 176 CPU cycles (11 µs, including the
4 cycles needed to put the CPU in interrupt mode) every
1664 cycles (104 µs). This amounts to about 10.6% of the
available CPU power. And since everything is done inside the ISR, the
program is free to do whatever else needs to be done while the data
acquisition and digital filtering happens in the background.
I tested the program by sending it a sine wave of 4.36 V
peak-to-peak amplitude. The reported power fluctuated quite a bit, but
here are the approximate average readings as a function of the input
frequency:
frequency | power
-----------+-------
929 Hz | 480
976 Hz | 2400
1005 Hz | 4600
1028 Hz | 2700
1079 Hz | 480