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 ()