Radio Control Transmitter Decoding






The radio control frequency bandplan
Info on PPM and PCM

Here we look at the signal from a Futaba T6XA model airplane radio control transmitter. This tx can operate in Pulse Position Modulation or even better Pulse Code Modulation. First we look at PPM. Using the USRP with the TV-RX module and cutting-pasting code to focus on my channel 34 frequency (72.47Mhz) we get the following demodulated narrow-band-FM signal:



which was taken with the engine throttle control at a minimum. That would be the space between the 3rd and 4th pulse. The first space, between pulse 1 & 2, is the ailerons, and the next space is the elevator. This tx has 3 more channels for rudder, flaps and landing gear. This next plot



has the throttle control at max, where you can see the 3rd space is now wider. The challange would be to write the software to convert these pulse positions into numbers, e.g., 0-255 over the range of each control.

Switching the tx to PCM, we get this plot:



where each channel servo position information is digitally encoded. It turns out that PCM formats are unpublished, proprietary secrets but appearently Futaba has been reverse-engineered here. It seems to match, the long gap seen above is the ~3ms frame sync (256000 samples/second, approx 700 samples = 2.7mSec)

The code used:

#!/usr/bin/env python

from gnuradio import gr, eng_notation
from gnuradio import usrp
from gnuradio import tv_rx
from gnuradio.eng_option import eng_option
from gnuradio.wxgui import slider
from optparse import OptionParser
import sys
import math

from gnuradio.wxgui import stdgui, fftsink, scopesink
import wx


class wfm_rx_graph (stdgui.gui_flow_graph):
    def __init__(self,frame,panel,vbox,argv):
        stdgui.gui_flow_graph.__init__ (self,frame,panel,vbox,argv)

        RF_freq = 72.47e6
        print "RF_freq ", RF_freq
        adc_rate = 64e6

        which_side = 1
        decim = 250
        quad_rate = adc_rate / decim               # 256 kHz
        
        # usrp is data source
        if which_side == 0:
            src = usrp.source_c (0, decim, 1, 0xf0f0f0f0, 0)
        else:
            src = usrp.source_c (0, decim, 1, 0xf0f0f0f2, 0)

        # set up frontend
        dboard = tv_rx.tv_rx (src, which_side)
        self.dboard = dboard

        (success, actual_freq) = dboard.set_freq(RF_freq)

        assert success
        IF_freq = -5.75e6 - RF_freq + actual_freq
        src.set_rx_freq (0, IF_freq)
        print "actual freq ", actual_freq
        print "IF freq ", IF_freq

	max_dev = 5e3
        k = quad_rate/(2*math.pi*max_dev)
        guts = gr.quadrature_demod_cf(k)

        audio_taps = gr.firdes.low_pass (50.0,            # gain
                                         quad_rate,      # sampling rate
                                         5e3,          # Audio LPF cutoff
                                         2e3,          # Transition band
                                         gr.firdes.WIN_HAMMING)  # filter type
        self.audio_filter = gr.fir_filter_fff(1, audio_taps)

        
        # data file as final sink
        file_sink = gr.null_sink (gr.sizeof_float)
	# file_sink = gr.file_sink ( gr.sizeof_float , "rc_out")
       
        # now wire it all together
        self.connect (src, guts)
        self.connect (guts, self.audio_filter, file_sink)

        if 1:
            pre_demod, fft_win1 = \
                       fftsink.make_fft_sink_c (self, panel, "Pre-Demodulation",
                                                512, quad_rate)
            self.connect (src, pre_demod)
            vbox.Add (fft_win1, 4, wx.EXPAND)


        if 1:
            post_filt, scope_win4 = \
		scopesink.make_scope_sink_f (self, panel, "Rx Data", quad_rate)
            self.connect (self.audio_filter, post_filt)
            vbox.Add (scope_win4, 4, wx.EXPAND)

        vbox.Add(slider.slider(panel, 0, 115, self.set_gain), 1, wx.ALIGN_CENTER)
        vbox.Add(slider.slider(panel, -200, 200, self.set_offset_i), 1, wx.ALIGN_CENTER)

    def set_gain (self,gain):
        self.dboard.set_gain(gain)

    def set_offset_i(self, offset):
        self.u.set_adc_offset(self.which_side*2+0, offset)


if __name__ == '__main__':
    app = stdgui.stdapp (wfm_rx_graph, "WFM RX")
    app.MainLoop ()