Skip to content

Latest commit

 

History

History
148 lines (88 loc) · 8.78 KB

README.md

File metadata and controls

148 lines (88 loc) · 8.78 KB

Micro APRS DeModulation

DeModulation of AFSK samples into AX25 and APRS phrases.

Reviewing existing AFSK demodulation schemes primarily turned up FFT based demodulation implemented in C or using advanced math libraries. In these implementations, band pass filters are designed to isolate mark and space frequencies individually to detect bits. Exemplary examples used a variety of settings and gain values running many FFTs in parallel, to optimially detect across as many corner cases as possible.

While this seems like a great idea, the idea of implementing FFT in Python, especially considering embedded systems and small computers as target ports, seems implausible considering the computational intensity.

Alternatively, I decided to go with an auto-correlator detection based approach. This approach is integer only friendly, quite fir "looking" in implementation, and easy to optimize into C or specialized emitters (eg. viper emitter in micropython).

Diving in...

AFSK Auto-Correlation Based Detection

Correlator Concept

The block diagram for the correlator is as follows:

$$ H(t) = sin(\omega t) sin(\omega(t+d)) $$

Where $d$ is the delay introduced by the delay block. By trig identity:

$$ sin(\alpha) sin(\beta) = \frac{1}{2} \left(cos(\alpha-\beta)-cos(\alpha+\beta) \right) $$

We find the output is:

$$ H(t) = \frac{1}{2} cos(\omega_{mark} d) - \frac{1}{2} cos((d+2t)\omega_{mark})$$

The first term, the DC component, ie. $\frac{1}{2} cos(\omega_{mark} d)$ , is our signal. Graphically, here we can see all the terms together.


Mark and Space Detection

Next step is to detect both mark and space frequencies. A clever concept I found for 2 FSK is to optimially pick the delay $d$ as to maximize the difference between the two DC terms $\frac{1}{2} cos(\omega_{mark} d)$ and $\frac{1}{2} cos(\omega_{space} d)$

$$ MarkSpace(t) = \frac{1}{2} cos(\omega_{mark} d_{optimal}) - \frac{1}{2} cos(\omega_{space} d_{optimal}) $$

where $d_{optimal}$ is chosen to maximize $MarkSpace(t)$. Below, we show MarkSpace(t), Mark, and Space. Scanning $d_{optimal}$, we find $d_{optimal} = 446 \micro s$. Graphically, $d_{optimal} = 446 \micro s$ is indicated by the red dashed line. If you watch carefully, you will notice the DC level for mark and space are at their extremes.


Armed with this knowledge we now know if we delay the AFSK input signal by $d_{optimal} = 446 \micro s$, and filter out the ripple terms, we are able to detect marks as values < 0 and spaces as values > 0. Incredibly, the detection algorithm only cost us a delay element (if sampling at 22050 Hz, a delay or buffer depth of 10 samples) and some multiplications!

Top Level Diagram

To put everything together, we need two more blocks:

  • A bandpass filter in the front, isolating the Mark/Space frequencies of interest.
  • A low pass filter in the back, removing the correlator ripples leaving just the DC "value detection" term.
  • A signal detector which includes clock recovery and bit sampler.

DeModulation Block Diagram

Bandpass FIR filters

Now onto the input filter. The goal here is to remove any DC offsets and any 'noise' out of band. While I found that sane values can be picked to get a functional demodulator, I recommend final parameter tuning using real-world test, eg. track 2 of the TNC test CD.

I've picked a gain of 1x of 0dB at mark (1200Hz) and space (2200Hz) frequencies.

Using Mathematica's filter designer:

taps = LeastSquaresFilterKernel[{"Bandpass", {fMark (2 \[Pi])/fs - 
      fPad (2 \[Pi])/fs, fSpace (2 \[Pi])/fs + fPad (2 \[Pi])/fs}}, 
   n];

In Python, you can find the filter taps similarly as done in the afsk/bandpass_fir_design function:

    from scipy import signal
    coefs = signal.firls(ncoefs,
                        (0, fmark-width, fmark, fspace, fspace+width, fs/2),
                        (0, 0,           amark, aspace, 0,            0),
                        fs=fs)

For the location of zeros, we apply the Z transform and solve for the solution (in Mathematica again):

zexpr = ListZTransform[taps, z];
zs = z /. NSolve[zexpr == 0, z];

I always recommend looking at the Z-plane when designing filter, as the zero placement really helps one understand how the coefficients shape the magnitude response. The resulting filter looks as follows:

Bandpass Filter

Lowpass FIR filters

Same process for the low pass filter. Here, my initial filter design cutoff is 1200Hz. For fun, including phase, group delay, and step response. In the Z place, notice how the zeros on the real axis boost the gain, and how the zeros along the unit circle suppress the magnitude after our cutoff.

Lowpass filter

Output

After the low pass filtering of the correlated output, we have

Incredibly, it turns out afsk demod comes down to:

  • Shift: (generate the delayed signal)
  • Multiply: (multiply delayed signal to signal)
  • Convolve: (run the low pass fir)

Digitizer

Here we convert the samples into bits. Many implementations I found have feedback based clock recovery schemes. However, I found that simple edge detection is doing a surprisingly good job, even for real-life tests and Track 2 of the TNC test CD. This is the route I went here. The other benefit being this scheme allows for decoding a signal after just a single header, while other methods used 25+ header bytes to lock the clock (I believe).

All-in-all, I didn't really do too much here. This is an area of future work, but it's working to my satisfaction, requires very little compute in the spirit of this project.

"One bad apple doesn't spoil the bunch" (CRC fixing)

Direwolf documentation does a great job walking through methodology. Though we've taken a different approach, one idea we are borrowing is the concept of attempting to "fix" a frame if everything looks good except for a CRC error. In our demodulation flow, we flip every 2 bits, computing the crc, to see if we can find a fix. This was the trick that got us over the 1000 decodes.

Going for a high score 🚀

The defacto score for APRS decoding is the number of error-free frames deocded in the TNC Test CD track 2. In the battle of the TNCs . While Direwolf still clocks in at the top, this implementation looks like a solid second.

The way I was able to get 1000+ decoded frames was by primarily sweeping through the band pass and low pass filter parameters. The solution space is quite bumpy, and ended up doing a more-or-less brute force search to finally derive the default values.

End results 1010 Decoded Frames! 🍻

References and Acknowledgements

Lots of good work out there which assisted me in my process, most notibly the following: