15
$\begingroup$

I am trying to transmit binary data from one device to another over an audio channel (speaker/mic). I use AFSK (Audio Frequency Shift Keying) as in Packet Radio, with $1200 \text{ Baud}$ and two frequencies $f_{mark} = 1200 \text{ Hz}$ and $f_{space} = 2200 \text{ Hz}$. I played around a bit in Ruby and my first implementation simply imitates a classic incoherent demodulator, which works fine, so far.

The problem is, i am trying to port this to a mobile platform where performance is a concern and my current solution is too slow. I have found numerous ways to demodulate AFSK in software:

  • Sliding DFT (FFT)
  • Sliding Görtzel Filter
  • Phase Locked Loop
  • Zero Crossing

What would be the way to go? There are just too many options to choose from. I am sure there are even more options available. Perhaps there exist even better solutions than the ones i named above? Does someone even have a code examples for me? I am concerned with

  • Performance (should run on mobile Platform, say an iOS or Android device)
  • Stability (should be able to handle some noise)

Any suggestions and hints are greatly appreciated!

$\endgroup$
2
  • 3
    $\begingroup$ I think you're likely selling short the capabilities of the mobile devices that you're targeting. Remember that modern devices are multicore processors with clock speeds in excess of 1 GHz. Processing a <10 ksps signal with an FSK demodulator shouldn't present a performance problem. But there shouldn't be any reason why your existing approach (which sounds to me like mark/space filtering) shouldn't be able to run in real time on a modern mobile platform. Even a more sophisticated PLL-based approach should fit comfortably in your processing envelope. I'd profile your existing code a bit. $\endgroup$
    – Jason R
    Aug 19, 2012 at 22:43
  • 1
    $\begingroup$ Have a look at mine and Teaching DSP through the Practical Case Study of an FSK Modem. $\endgroup$ Aug 26, 2012 at 8:46

3 Answers 3

9
$\begingroup$

I think that you could get the best performance in terms of demodulator bit-error rate (BER) with a phase-locked loop. You need it to be fast, though. I think your best bet for a fast algorithm that still performs reasonably well is zero crossing.

On a side note, I would like to suggest that you change the 2200 Hz to 2400 Hz. A naive implementation of the 1200/2200 Hz scheme would yield discontinuities, as seen about two thirds into the plot below, where the 2200 Hz transitions to 1200 Hz.

1200 Hz and 2200 Hz

In order to minimize the bandwidth that you're using and avoid discontinuities that will distort the signal you'll need to make the phase continuous. Even if you make the transmitter phase continuous, though, there will still be the issue that the 2200 Hz symbols will not always have the same number of zero crossings due to the different phases. Usually they will have four zero crossings, but sometimes they will have three. The 1200 Hz symbols on the other hand, will always have two zero crossings because the baud rate divides evenly into the FSK frequency.

You can solve both of these problems by changing the 2200 Hz to 2400 Hz. Then the symbols will always start and end at 0 degrees (thus automatically making them phase continuous), and they will always have the same number of zero crossings- two and four.

1200 Hz and 2400 Hz

$\endgroup$
3
  • $\begingroup$ Hey Jim, thanks for your detailed answer! My modulator actually does CPFSK, therefore discontinuities are not an issue. I deliberately picked 1200 and 2200 Hz because the harmonics do not overlap as much as with multiples of 1200. Or am i wrong here? PLLs sound great, but i have really no idea of how to implement them. Do you happen to know any good sources about software PLLs? $\endgroup$ Aug 21, 2012 at 18:22
  • $\begingroup$ @Patrick No, you are correct that 1200 and 2400 Hz will have overlapping harmonics. In the context of zero-crossing, though, I don't think the harmonics matter. And no, I'm afraid I don't know of a good online source about PLL's. $\endgroup$
    – Jim Clay
    Aug 21, 2012 at 18:52
  • 1
    $\begingroup$ This is not correct. AFSK 1200 follows Bell 202, and it says the tones should be 1200 and 2200. The discontinuity should never happen in the transmitter side. Check out open source AFSK 1200 modulators, the modulation is done by keeping track a phase increment for every tone: if tone==LOW then last_phase += ph_low else last_phase += ph_high endif; next_sample = sin(last_phase); $\endgroup$
    – vz0
    Oct 6, 2016 at 14:57
5
$\begingroup$

I made a decoder for AFSK (Bell 202 standard) using correlation receivers for 1200 Hz and 2200 Hz, with very good results.

Since the phase of the signal during a symbol is unknown, a solution is to work in the complex domain: instead of multiplying by real sinusoids, multiply by complex exponentials. This means multiplying by $\sin$ and $\cos$ independently, then integrating each, and calculating the absolute (square) value.

The resulting amplitude is quite independent from the signal phase, and the output SNR is very good.

$\endgroup$
1
  • $\begingroup$ This is exactly what i have tried before and what i have called „classic incoherent demodulator“. Maybe my implementation is erroneous, but i fear that it suffers from buffer overflows due to slow processing. Thanks anyway! $\endgroup$ Aug 21, 2012 at 18:17
0
$\begingroup$

In the case of RTTY 45.45 baud, you will also have symbols that aren't an integer number of samples, so you need a function that can be called each sample and then signal in its return value when that symbol has ended. And you need a phase accumulator, which keeps a running tally on where the phase of the sine wave is.

To send symbols whose length isn't an integer multiple of the sample rate you need this function...

int millisecondTimer(double milliseconds, double samplerate, int resettime)
{

    static int fracsample=0;
    static int counter=0;
    static int retvalue=0;
    static int first=1;
    static double oldmilliseconds=1.0;
    static int whole_samples=0;
    static int samerror=32768;
    if(resettime==1)
    {
        samerror=0;
        counter=0;
        retvalue=1;
        first=1;
    }
    if(first==1 || milliseconds !=oldmilliseconds)
    {
        double samplesneeded=1;
        double wholesamples=0;
        samplesneeded=(samplerate) * (milliseconds /1000.0);
        samerror=(modf(samplesneeded, &wholesamples)) * 32768.0;
        whole_samples=wholesamples;
        first=0;
    }

    if(counter<=whole_samples)
    {
        retvalue=2;
        counter++;
    }
    else
    {
        counter-=whole_samples;
        retvalue=1;
        fracsample+=samerror;
        oldmilliseconds=milliseconds;
        if(fracsample>=32768)
        {
            fracsample-=32768;
            counter--;
        }

    }
    return retvalue;
}

To use it, generate the next sample of sine wave and call this function, then check if the return value is NOT equal to two. If it's not equal to two, advance to the next symbol and decide whether you are sending a mark of space, then call this function again inside the block of code which executes when you found out that the return value is not equal to two.

And here's the phase accumulator from the Rockbox firmware, with a change to allow changes in amplitude (full volume is 32767, 180 degrees out of phase full volume is -32768).

signed short lerpsin(float frequency,signed short amplitude,unsigned long samplerate)
{
    /* 128 sixteen bit sine samples + guard point */
    static unsigned long phase=0;
    unsigned int pos =0;
    unsigned short frac=0;
    static unsigned long step=0;
    static float old_frequency=0;
    signed short diff=0;
    static const signed short sinetab[129] =
    {
        0,   1607,   3211,   4807,   6392,   7961,   9511,  11038,
        12539,  14009,  15446,  16845,  18204,  19519,  20787,  22004,
        23169,  24278,  25329,  26318,  27244,  28105,  28897,  29621,
        30272,  30851,  31356,  31785,  32137,  32412,  32609,  32727,
        32767,  32727,  32609,  32412,  32137,  31785,  31356,  30851,
        30272,  29621,  28897,  28105,  27244,  26318,  25329,  24278,
        23169,  22004,  20787,  19519,  18204,  16845,  15446,  14009,
        12539,  11038,   9511,   7961,   6392,   4807,   3211,   1607,
        0,  -1607,  -3211,  -4807,  -6392,  -7961,  -9511, -11038,
        -12539, -14009, -15446, -16845, -18204, -19519, -20787, -22004,
        -23169, -24278, -25329, -26318, -27244, -28105, -28897, -29621,
        -30272, -30851, -31356, -31785, -32137, -32412, -32609, -32727,
        -32767, -32727, -32609, -32412, -32137, -31785, -31356, -30851,
        -30272, -29621, -28897, -28105, -27244, -26318, -25329, -24278,
        -23169, -22004, -20787, -19519, -18204, -16845, -15446, -14009,
        -12539, -11038, -9511,   -7961,  -6392,  -4807,  -3211,  -1607,
        0,
    };
    if(frequency!=old_frequency)
    {
        step = 0x100000000ull*frequency / samplerate;
    }
    phase+=step;
    pos = phase >> 25;
    frac = (phase & 0x01ffffff) >> 9;
    diff = sinetab[pos + 1] - sinetab[pos];
    old_frequency=frequency;
    return ((-((sinetab[pos] + (frac*diff >> 16)))) * amplitude) >> 15;
}
$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.